├── .circleci └── config.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── automerge-dependabot.yml │ ├── goreleaser-check.yml │ ├── lint.yml │ ├── pages.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── Dockerfile-goreleaser ├── LICENSE ├── README.md ├── cmd └── root.go ├── cosign.pub ├── data ├── 1000-domains ├── 10000-domains ├── 2-domains ├── 500-domains └── alexa ├── docs ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── _config.yml ├── _includes │ └── head_custom.html ├── assets │ ├── demo.gif │ ├── favicon.ico │ ├── logo.png │ ├── logo.svg │ └── longlogo.svg ├── basics.md ├── concurrency.md ├── doh.md ├── domainsources.md ├── doq.md ├── dot.md ├── edns0.md ├── examples.md ├── failoncondition.md ├── graphs.md ├── graphs │ ├── errorrate-lineplot.svg │ ├── latency-boxplot.svg │ ├── latency-histogram.svg │ ├── latency-lineplot.svg │ ├── responses-barchart.svg │ └── throughput-lineplot.svg ├── index.md ├── installation.md ├── jsonoutput.md ├── kubernetes.md ├── plaindns.md ├── querytypes.md ├── randomizing.md ├── ratelimit.md ├── requestdelay.md ├── requestlog.md ├── timeouts.md └── workerconnections.md ├── go.mod ├── go.sum ├── main.go ├── pkg ├── dnsbench │ ├── benchmark.go │ ├── benchmark_common_api_test.go │ ├── benchmark_doh_api_test.go │ ├── benchmark_doq_api_test.go │ ├── benchmark_dot_api_test.go │ ├── benchmark_plaindns_api_test.go │ ├── benchmark_test.go │ ├── doc.go │ ├── query_factory.go │ ├── request_logging.go │ ├── result.go │ ├── result_test.go │ ├── server_test.go │ └── testdata │ │ ├── test.crt │ │ └── test.key ├── printutils │ └── printutils.go └── reporter │ ├── doc.go │ ├── jsonreporter.go │ ├── merge.go │ ├── merge_test.go │ ├── plot.go │ ├── plot_test.go │ ├── report.go │ ├── report_test.go │ ├── stdreporter.go │ └── testdata │ ├── dnssecReport │ ├── dohReport │ ├── errorReport │ ├── jsonDnssecReport │ ├── jsonDohReport │ ├── jsonReport │ ├── successReport │ ├── test-boxplot-latency.svg │ ├── test-errorrate-lineplot.svg │ ├── test-histogram-latency.svg │ ├── test-latency-lineplot.svg │ ├── test-responses-barchart.svg │ └── test-throughput-lineplot.svg └── scripts ├── completions.sh └── manpages.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. See: https://circleci.com/docs/2.0/configuration-reference 2 | version: 2.1 3 | orbs: 4 | codecov: codecov/codecov@5.0.3 5 | jobs: 6 | build: 7 | working_directory: ~/repo 8 | docker: 9 | - image: cimg/go:1.22 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | keys: 14 | - go-mod-v5-{{ checksum "go.sum" }} 15 | - run: 16 | name: Install Dependencies 17 | command: go mod download 18 | - save_cache: 19 | key: go-mod-v5-{{ checksum "go.sum" }} 20 | paths: 21 | - "/go/pkg/mod" 22 | - run: 23 | name: Run tests 24 | command: | 25 | mkdir -p /tmp/test-reports 26 | gotestsum --junitfile /tmp/test-reports/unit-tests.xml -- -coverprofile=coverage.out -covermode=atomic -json -race ./... 27 | - codecov/upload 28 | - store_artifacts: 29 | path: /tmp/test-reports 30 | - store_test_results: 31 | path: /tmp/test-reports 32 | 33 | workflows: 34 | version: 2 35 | tests: 36 | jobs: 37 | - build 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What happened** 11 | Please describe what happened 12 | 13 | **What you expected to happen** 14 | Please describe what did you expect to happen 15 | 16 | **dnspyre command** 17 | ``` 18 | dnspyre --server '8.8.8.8' google.com 19 | ``` 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the feature** 11 | Please describe the feature, you would like to add 12 | 13 | **Why do you need this feature** 14 | Please describe why do you need this feature. Describe the use case 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: 'bundler' 8 | directory: '/docs' 9 | schedule: 10 | interval: 'weekly' 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /.github/workflows/automerge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Approve a PR 19 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 20 | run: gh pr review --approve "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | - name: Enable auto-merge for Dependabot PRs 25 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 26 | run: gh pr merge --auto --rebase "$PR_URL" 27 | env: 28 | PR_URL: ${{github.event.pull_request.html_url}} 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser-check.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser-check 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | goreleaser-check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - name: Check goreleaser configuration 18 | uses: goreleaser/goreleaser-action@v6 19 | with: 20 | distribution: goreleaser 21 | version: latest 22 | args: check 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | golangci: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v4 13 | - name: Install Go 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version-file: go.mod 17 | - name: Lint 18 | uses: golangci/golangci-lint-action@v6 19 | with: 20 | version: v1.57.2 21 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages 7 | name: Deploy Jekyll site to Pages 8 | 9 | on: 10 | push: 11 | branches: [ "master" ] 12 | paths: 13 | - "docs/**" 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 19 | permissions: 20 | contents: read 21 | pages: write 22 | id-token: write 23 | 24 | # Allow one concurrent deployment 25 | concurrency: 26 | group: "pages" 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | # Build job 31 | build: 32 | runs-on: ubuntu-latest 33 | defaults: 34 | run: 35 | working-directory: docs 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | - name: Setup Ruby 40 | uses: ruby/setup-ruby@v1 41 | with: 42 | ruby-version: '3.3' # Not needed with a .ruby-version file 43 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 44 | cache-version: 1 # Increment this number if you need to re-download cached gems 45 | working-directory: '${{ github.workspace }}/docs' 46 | - name: Setup Pages 47 | id: pages 48 | uses: actions/configure-pages@v5 49 | - name: Build with Jekyll 50 | # Outputs to the './_site' directory by default 51 | run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" 52 | env: 53 | JEKYLL_ENV: production 54 | - name: Upload artifact 55 | # Automatically uploads an artifact from the './_site' directory by default 56 | uses: actions/upload-pages-artifact@v3 57 | with: 58 | path: "docs/_site/" 59 | 60 | # Deployment job 61 | deploy: 62 | environment: 63 | name: github-pages 64 | url: ${{ steps.deployment.outputs.page_url }} 65 | runs-on: ubuntu-latest 66 | needs: build 67 | steps: 68 | - name: Deploy to GitHub Pages 69 | id: deployment 70 | uses: actions/deploy-pages@v4 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | # Release is triggered upon pushing a new tag 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | permissions: write-all 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version-file: go.mod 23 | - name: Login to GitHub Container Registry 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ghcr.io 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.RELEASE_TOKEN }} 29 | - uses: sigstore/cosign-installer@v3.7.0 30 | - name: Run GoReleaser 31 | uses: goreleaser/goreleaser-action@v6 32 | with: 33 | distribution: goreleaser 34 | version: latest 35 | args: release --clean 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 38 | COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} 39 | COSIGN_PRIVATE_KEY: ${{secrets.COSIGN_PRIVATE_KEY}} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Output of the go coverage tool, specifically when used with LiteIDE 27 | *.out 28 | 29 | # External packages folder 30 | vendor/ 31 | .idea/ 32 | *.log 33 | 34 | *.iml 35 | bin/ 36 | dist/ 37 | report/ 38 | dnspyre 39 | 40 | /graphs*/ 41 | 42 | # Jekyll documentation 43 | _site 44 | .jekyll-cache 45 | completions 46 | manpages/ 47 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | modules-download-mode: readonly 4 | linters: 5 | disable-all: true 6 | enable: 7 | - gci 8 | - gocritic 9 | - godot 10 | - gofmt 11 | - gofumpt 12 | - goimports 13 | - gomoddirectives 14 | - gomodguard 15 | - gosec 16 | - gosimple 17 | - govet 18 | - ineffassign 19 | - revive 20 | - staticcheck 21 | - stylecheck 22 | - testifylint 23 | - typecheck 24 | - unconvert 25 | - unparam 26 | - unused 27 | - usestdlibvars 28 | - wastedassign 29 | - whitespace 30 | 31 | issues: 32 | include: 33 | - EXC0012 # disable excluding of issues about comments from revive 34 | 35 | linters-settings: 36 | godot: 37 | # list of regexps for excluding particular comment lines from check 38 | exclude: 39 | - '^ @.*' # swaggo comments like // @title 40 | - '^ (\d+)(\.|\)).*' # enumeration comments like // 1. or // 1) 41 | gosec: 42 | config: 43 | global: 44 | audit: true 45 | excludes: 46 | - G104 47 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | - go mod tidy 5 | - go generate ./... 6 | - sh scripts/completions.sh 7 | - sh scripts/manpages.sh 8 | builds: 9 | - env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - windows 14 | - darwin 15 | ldflags: 16 | - -X 'github.com/tantalor93/dnspyre/v3/cmd.Version={{.Version}}-{{ .Os }}-{{ .Arch }}' 17 | archives: 18 | - id: dnspyre 19 | name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 20 | files: 21 | - LICENSE 22 | - README.md 23 | - completions/* 24 | - manpages/* 25 | checksum: 26 | name_template: 'checksums.txt' 27 | snapshot: 28 | version_template: "{{ incpatch .Version }}-next" 29 | changelog: 30 | sort: asc 31 | brews: 32 | - repository: 33 | owner: tantalor93 34 | name: homebrew-dnspyre 35 | directory: Formula 36 | homepage: https://tantalor93.github.io/dnspyre/ 37 | install: |- 38 | bin.install "dnspyre" 39 | bash_completion.install "completions/dnspyre.bash" => "_dnspyre" 40 | zsh_completion.install "completions/dnspyre.zsh" => "_dnspyre" 41 | man1.install "manpages/dnspyre.1.gz" 42 | dockers: 43 | - use: docker 44 | id: dnspyre 45 | dockerfile: "Dockerfile-goreleaser" 46 | image_templates: 47 | - "ghcr.io/tantalor93/dnspyre:{{ .Tag }}" 48 | - "ghcr.io/tantalor93/dnspyre:v{{ .Major }}" 49 | - "ghcr.io/tantalor93/dnspyre:v{{ .Major }}.{{ .Minor }}" 50 | - "ghcr.io/tantalor93/dnspyre:latest" 51 | build_flag_templates: 52 | - "--label=org.opencontainers.image.created={{.Date}}" 53 | - "--label=org.opencontainers.image.authors=Ondřej Benkovský " 54 | - "--label=org.opencontainers.image.url=https://tantalor93.github.io/dnspyre" 55 | - "--label=org.opencontainers.image.documentation=https://tantalor93.github.io/dnspyre" 56 | - "--label=org.opencontainers.image.source=https://github.com/Tantalor93/dnspyre" 57 | - "--label=org.opencontainers.image.version={{.Version}}" 58 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 59 | - "--label=org.opencontainers.image.licenses=MIT" 60 | - "--label=org.opencontainers.image.title={{.ProjectName}}" 61 | - "--label=org.opencontainers.image.description=tool for a high QPS DNS benchmark" 62 | nfpms: 63 | - package_name: dnspyre 64 | homepage: https://tantalor93.github.io/dnspyre 65 | maintainer: Ondřej Benkovský 66 | description: |- 67 | tool for a high QPS DNS benchmark 68 | license: MIT 69 | formats: 70 | - apk 71 | - deb 72 | - rpm 73 | signs: 74 | - cmd: cosign 75 | stdin: "{{ .Env.COSIGN_PASSWORD }}" 76 | args: 77 | - "sign-blob" 78 | - "--key=env://COSIGN_PRIVATE_KEY" 79 | - "--output-signature=${signature}" 80 | - "${artifact}" 81 | - "--yes" # needed on cosign 2.0.0+ 82 | artifacts: all 83 | docker_signs: 84 | - cmd: cosign 85 | stdin: "{{ .Env.COSIGN_PASSWORD }}" 86 | args: 87 | - "sign" 88 | - "--key=env://COSIGN_PRIVATE_KEY" 89 | - "${artifact}" 90 | - "--yes" # needed on cosign 2.0.0+ 91 | artifacts: all 92 | -------------------------------------------------------------------------------- /Dockerfile-goreleaser: -------------------------------------------------------------------------------- 1 | FROM alpine:latest as certs 2 | RUN apk --update add ca-certificates 3 | 4 | FROM scratch 5 | COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 6 | COPY dnspyre /dnspyre 7 | ENTRYPOINT ["/dnspyre"] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rahul Powar 4 | Copyright (c) 2021 Ondřej Benkovský 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dnspyre 2 | 3 | [![Release](https://img.shields.io/github/release/Tantalor93/dnspyre/all.svg)](https://github.com/tantalor93/dnspyre/releases) 4 | [![Go version](https://img.shields.io/github/go-mod/go-version/Tantalor93/dnspyre)](https://github.com/Tantalor93/dnspyre/blob/master/go.mod#L3) 5 | [![](https://godoc.org/github.com/Tantalor93/dnspyre/v3?status.svg)](https://godoc.org/github.com/tantalor93/dnspyre/v3/pkg) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) 7 | [![Tantalor93](https://circleci.com/gh/Tantalor93/dnspyre/tree/master.svg?style=svg)](https://circleci.com/gh/Tantalor93/dnspyre?branch=master) 8 | [![lint](https://github.com/Tantalor93/dnspyre/actions/workflows/lint.yml/badge.svg?branch=master)](https://github.com/Tantalor93/dnspyre/actions/workflows/lint.yml) 9 | [![goreleaser-check](https://github.com/Tantalor93/dnspyre/actions/workflows/goreleaser-check.yml/badge.svg?branch=master)](https://github.com/Tantalor93/dnspyre/actions/workflows/goreleaser-check.yml) 10 | [![codecov](https://codecov.io/gh/Tantalor93/dnspyre/branch/master/graph/badge.svg?token=MC6PK2OLMK)](https://codecov.io/gh/Tantalor93/dnspyre) 11 | [![Go Report Card](https://goreportcard.com/badge/github.com/tantalor93/dnspyre/v2)](https://goreportcard.com/report/github.com/tantalor93/dnspyre/v2) 12 | 13 | ![dnspyre logo](./docs/assets/logo.png) 14 | 15 | dnspyre is a command-line DNS benchmark tool built to stress test and measure the performance of DNS servers. You can easily run benchmark from MacOS, Linux or Windows systems. 16 | 17 | This tool is based and originally forked from [dnstrace](https://github.com/redsift/dnstrace), but was largely rewritten and enhanced with additional functionality. 18 | 19 | This tool supports wide variety of options to customize DNS benchmark and benchmark output. For example, you can: 20 | * benchmark DNS servers using DNS queries over UDP or TCP 21 | * benchmark DNS servers with all kinds of query types like A, AAAA, CNAME, HTTPS, ... (`--type` option) 22 | * benchmark DNS servers with a lot of parallel queries and connections (`--number`, `--concurrency` options) 23 | * benchmark DNS servers for a specified duration (`--duration` option) 24 | * benchmark DNS servers with DoT ([DNS over TLS](https://datatracker.ietf.org/doc/html/rfc7858)) 25 | * benchmark DNS servers using DoH ([DNS over HTTPS](https://datatracker.ietf.org/doc/html/rfc8484)) 26 | * benchmark DNS servers using DoQ ([DNS over QUIC](https://datatracker.ietf.org/doc/rfc9250/)) 27 | * benchmark DNS servers with uneven random load from provided high volume resources (see `--probability` option) 28 | * plot benchmark results via CLI histogram or plot the benchmark results as boxplot, histogram, line graphs and export them via all kind of image formats like png, svg and pdf. (see `--plot` and `--plotf` options) 29 | 30 | ![demo](docs/assets/demo.gif) 31 | 32 | ## Documentation 33 | For installation guide, examples and more, see the [documentation page](https://tantalor93.github.io/dnspyre/) 34 | -------------------------------------------------------------------------------- /cosign.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL1eWDPqIRLtUgcuYRIGqRgnkp6Bz 3 | i+AgYeQWSWuWkFzEFtRzsLuP+eFlTVjHt0pGGNQW0PEomqYQ6o68czw2Ag== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /data/2-domains: -------------------------------------------------------------------------------- 1 | google.com 2 | idnes.cz 3 | -------------------------------------------------------------------------------- /data/500-domains: -------------------------------------------------------------------------------- 1 | www.google.com. 2 | www.facebook.com. 3 | mail.google.com. 4 | www.youtube.com. 5 | mail.live.com. 6 | mail.yahoo.com. 7 | images.google.com. 8 | en.wikipedia.org. 9 | twitter.com. 10 | www.baidu.com. 11 | www.amazon.com. 12 | www.linkedin.com. 13 | www.flickr.com. 14 | apps.facebook.com. 15 | www.vkontakte.ru. 16 | login.live.com. 17 | search.conduit.com. 18 | www.blogger.com. 19 | www.imdb.com. 20 | jp.youtube.com. 21 | www.microsoft.com. 22 | search.yahoo.com. 23 | www.yandex.ru. 24 | www.orkut.com.br. 25 | new.facebook.com. 26 | www.pornhub.com. 27 | auction1.taobao.com. 28 | www.xvideos.com. 29 | runonce.msn.com. 30 | www.yahoo.com. 31 | www.msn.com. 32 | qzone.qq.com. 33 | www.livejasmin.com. 34 | tieba.baidu.com. 35 | rapidshare.com. 36 | auctions.yahoo.co.jp. 37 | www.youporn.com. 38 | image.baidu.com. 39 | www.orkut.com. 40 | www.mediafire.com. 41 | ja.wikipedia.org. 42 | news.bbc.co.uk. 43 | www.4shared.com. 44 | www.youku.com. 45 | viewmorepics.myspace.com. 46 | www.xhamster.com. 47 | www.ameblo.jp. 48 | www.ask.com. 49 | www.soso.com. 50 | search.yahoo.co.jp. 51 | www.megavideo.com. 52 | www.yahoo.co.jp. 53 | www.orkut.co.in. 54 | www.apple.com. 55 | ad.doubleclick.net. 56 | www.hi5.com. 57 | www.bbc.co.uk. 58 | www.megaupload.com. 59 | de.wikipedia.org. 60 | www.tudou.com. 61 | es.youtube.com. 62 | www.cnn.com. 63 | news.qq.com. 64 | win.mail.ru. 65 | photo.qq.com. 66 | cgi.ebay.com. 67 | shop.ebay.com. 68 | www.thepiratebay.org. 69 | www.tube8.com. 70 | www.mixi.jp. 71 | www.kaixin001.com. 72 | zhidao.baidu.com. 73 | www.redtube.com. 74 | uk.youtube.com. 75 | sports.sina.com.cn. 76 | adwords.google.com. 77 | www.nytimes.com. 78 | www.dailymotion.com. 79 | blog.sina.com.cn. 80 | www.paypal.com. 81 | store.taobao.com. 82 | ad.xtendmedia.com. 83 | www.digg.com. 84 | www.weather.com. 85 | picasaweb.google.com. 86 | translate.google.com. 87 | www.adobe.com. 88 | www.amazon.co.jp. 89 | www.amazon.de. 90 | es.wikipedia.org. 91 | webmail.aol.com. 92 | ad.yieldmanager.com. 93 | fr.youtube.com. 94 | www.twitpic.com. 95 | www.taringa.net. 96 | in.youtube.com. 97 | www.netflix.com. 98 | www.tianya.cn. 99 | news.163.com. 100 | it.youtube.com. 101 | www.cnzz.com. 102 | profile.myspace.com. 103 | www.ehow.com. 104 | www.spiegel.de. 105 | my.mail.ru. 106 | search1.taobao.com. 107 | search.mywebsearch.com. 108 | mail.yahoo.co.jp. 109 | messaging.myspace.com. 110 | www.nicovideo.jp. 111 | www.wretch.cc. 112 | www.bit.ly. 113 | news.sina.com.cn. 114 | www.qq.com. 115 | g.doubleclick.net. 116 | www.depositfiles.com. 117 | news.yahoo.com. 118 | www.youjizz.com. 119 | www.huffingtonpost.com. 120 | video.xnxx.com. 121 | www.nasza-klasa.pl. 122 | www.aweber.com. 123 | www.download.com. 124 | office.microsoft.com. 125 | mail.yandex.ru. 126 | video.google.com. 127 | www.godaddy.com. 128 | home.myspace.com. 129 | www.hulu.com. 130 | www.tagged.com. 131 | www.torrentz.com. 132 | my.ebay.com. 133 | blog.livedoor.jp. 134 | mail.qq.com. 135 | www.fastbrowsersearch.com. 136 | finance.sina.com.cn. 137 | www.photobucket.com. 138 | fr.msn.com. 139 | www.addthis.com. 140 | serv.clicksor.com. 141 | search.live.com. 142 | www.badoo.com. 143 | imgcache.qq.com. 144 | pic.sogou.com. 145 | www.partypoker.com. 146 | www.bild.de. 147 | item.rakuten.co.jp. 148 | www.adultfriendfinder.com. 149 | shell.windows.com. 150 | www.amazon.co.uk. 151 | www.taobao.com. 152 | www.56.com. 153 | v.youku.com. 154 | groups.google.com. 155 | br.youtube.com. 156 | de.youtube.com. 157 | 360.yahoo.com. 158 | www.ezinearticles.com. 159 | www.wordpress.com. 160 | sports.espn.go.com. 161 | fr.wikipedia.org. 162 | ru.wikipedia.org. 163 | blog.163.com. 164 | forums.digitalpoint.com. 165 | jp.msn.com. 166 | www.foxnews.com. 167 | store.apple.com. 168 | www.aol.com. 169 | ent.qq.com. 170 | news.sohu.com. 171 | get.adobe.com. 172 | www.linkbucks.com. 173 | www.filestube.com. 174 | china.alibaba.com. 175 | www.metacafe.com. 176 | music.soso.com. 177 | www.xing.com. 178 | reviews.cnet.com. 179 | www.vimeo.com. 180 | www.ku6.com. 181 | www.hubpages.com. 182 | www.douban.com. 183 | www.wordpress.org. 184 | www.squidoo.com. 185 | www.istockphoto.com. 186 | search.msn.com. 187 | www.dailymail.co.uk. 188 | www.mail.ru. 189 | cgi.ebay.de. 190 | www.isohunt.com. 191 | www.scribd.com. 192 | hi.baidu.com. 193 | maps.google.com. 194 | www.keezmovies.com. 195 | docs.google.com. 196 | news.google.com. 197 | login.yahoo.com. 198 | books.google.com. 199 | update.microsoft.com. 200 | whois.domaintools.com. 201 | www.leboncoin.fr. 202 | search.twitter.com. 203 | dictionary.reference.com. 204 | it.wikipedia.org. 205 | mail.163.com. 206 | www.spankwire.com. 207 | finance.yahoo.com. 208 | www.tumblr.com. 209 | mp3.baidu.com. 210 | trade.taobao.com. 211 | dict.leo.org. 212 | wiki.answers.com. 213 | shop.ebay.de. 214 | ad.z5x.net. 215 | community.livejournal.com. 216 | www.guardian.co.uk. 217 | www.nba.com. 218 | www.tnaflix.com. 219 | br.msn.com. 220 | service.gmx.net. 221 | www.allegro.pl. 222 | www.vnexpress.net. 223 | www.sourceforge.net. 224 | www.yelp.com. 225 | www.marca.com. 226 | images.yandex.ru. 227 | friends.myspace.com. 228 | news.ifeng.com. 229 | answers.yahoo.com. 230 | www.clickbank.com. 231 | 2.livejasmin.com. 232 | skydrive.live.com. 233 | www.sina.com.cn. 234 | email.secureserver.net. 235 | upload.wikimedia.org. 236 | www.reddit.com. 237 | www.megaporn.com. 238 | www.myspace.com. 239 | web.archive.org. 240 | www.uol.com.br. 241 | dailynews.yahoo.co.jp. 242 | members.livejasmin.com. 243 | www.telegraph.co.uk. 244 | www.gamespot.com. 245 | www.kaskus.us. 246 | finance.google.com. 247 | video.baidu.com. 248 | www.51.la. 249 | studivz.net. 250 | www.onemanga.com. 251 | www.mafiawars.com. 252 | prodigy.msn.com. 253 | www.mashable.com. 254 | www.walmart.com. 255 | spaces.live.com. 256 | www.geocities.jp. 257 | www.163.com. 258 | s3.amazonaws.com. 259 | photo.163.com. 260 | www.liveinternet.ru. 261 | list.taobao.com. 262 | so.tudou.com. 263 | www.mapquest.com. 264 | sharp.admagnet.net. 265 | www.answers.com. 266 | guide.opendns.com. 267 | www.mozilla.com. 268 | www.yourfilehost.com. 269 | love.mail.ru. 270 | www.brothersoft.com. 271 | www.w3schools.com. 272 | ent.sina.com.cn. 273 | image.soso.com. 274 | www.tripadvisor.com. 275 | tw.yahoo.com. 276 | shop.ebay.co.uk. 277 | www.metroflog.com. 278 | www.etsy.com. 279 | www.imagebam.com. 280 | www.wordreference.com. 281 | www.slideshare.net. 282 | www.megaclick.com. 283 | www.plentyoffish.com. 284 | msnbc.msn.com. 285 | book.sina.com.cn. 286 | au.youtube.com. 287 | www.commentcamarche.net. 288 | www.gizmodo.com. 289 | money.cnn.com. 290 | timesofindia.indiatimes.com. 291 | members.ezinearticles.com. 292 | ui.constantcontact.com. 293 | www.globo.com. 294 | www.sogou.com. 295 | in.yahoo.com. 296 | sports.yahoo.com. 297 | bid.yahoo.com. 298 | groups.yahoo.com. 299 | reg.youdao.com. 300 | news.cnet.com. 301 | www.empflix.com. 302 | www.techcrunch.com. 303 | media.fastclick.net. 304 | www.ziddu.com. 305 | www.stumbleupon.com. 306 | www.tinypic.com. 307 | www.dmm.co.jp. 308 | www.uploading.com. 309 | www.expedia.com. 310 | www.repubblica.it. 311 | online.wsj.com. 312 | www.xe.com. 313 | wenwen.soso.com. 314 | games.espn.go.com. 315 | espn.go.com. 316 | www.pandora.com. 317 | ak.fbcdn.net. 318 | www.articlesbase.com. 319 | www.streamate.com. 320 | addons.mozilla.org. 321 | mx.youtube.com. 322 | wrs.yahoo.com. 323 | pl.youtube.com. 324 | tw-in-f132.google.com. 325 | es.msn.com. 326 | profile.live.com. 327 | www.ikea.com. 328 | www.alibaba.com. 329 | www.skype.com. 330 | www.reuters.com. 331 | cgi.ebay.co.uk. 332 | video.sina.com.cn. 333 | www.btjunkie.org. 334 | www.letitbit.net. 335 | www.bestbuy.com. 336 | www.espn.go.com. 337 | rate.taobao.com. 338 | www.justin.tv. 339 | www.target.com. 340 | a.tribalfusion.com. 341 | www.rambler.ru. 342 | www.hardsextube.com. 343 | www.sohu.com. 344 | lady.qq.com. 345 | www.last.fm. 346 | www.verycd.com. 347 | it.msn.com. 348 | creativeproxy.uimserv.net. 349 | sfbay.craigslist.org. 350 | commons.wikimedia.org. 351 | www.zimbio.com. 352 | c5.zedo.com. 353 | globoesporte.globo.com. 354 | www.rakuten.co.jp. 355 | shopping.yahoo.co.jp. 356 | www.washingtonpost.com. 357 | newt1.adultadworld.com. 358 | detail.zol.com.cn. 359 | esearch.rakuten.co.jp. 360 | tech.sina.com.cn. 361 | sympatico.msn.ca. 362 | www.4399.com. 363 | techrepublic.com.com. 364 | so.youku.com. 365 | screenname.aol.com. 366 | www.comcast.com. 367 | www.livescore.com. 368 | web.fc2.com. 369 | www.kakaku.com. 370 | www.booking.com. 371 | www.veoh.com. 372 | search.babylon.com. 373 | static.theplanet.com. 374 | www.meinvz.net. 375 | www.tuenti.com. 376 | www.marketgid.com. 377 | delicious.com. 378 | www.wer-kennt-wen.de. 379 | uk.ask.com. 380 | www.fedex.com. 381 | www.cricinfo.com. 382 | www.wikipedia.org. 383 | www.boston.com. 384 | news.xinhuanet.com. 385 | fotolog.com. 386 | my.ebay.de. 387 | www.thesun.co.uk. 388 | www.ebay.com. 389 | sportsillustrated.cnn.com. 390 | www.miniclip.com. 391 | www.gougou.com. 392 | sports.sohu.com. 393 | ks.pconline.com.cn. 394 | www.newegg.com. 395 | www.drudgereport.com. 396 | d.hatena.ne.jp. 397 | forum.kooora.com. 398 | www.slutload.com. 399 | www.elmundo.es. 400 | members.cj.com. 401 | d3.zedo.com. 402 | newyork.craigslist.org. 403 | msn.foxsports.com. 404 | video.filestube.com. 405 | www.4tube.com. 406 | www.bloomberg.com. 407 | www.web.de. 408 | pl.wikipedia.org. 409 | pt.wikipedia.org. 410 | my.yahoo.com. 411 | cn.yahoo.com. 412 | www.cam4.com. 413 | www.zshare.net. 414 | sports.163.com. 415 | www.engadget.com. 416 | www.avg.com. 417 | post.soso.com. 418 | www.goal.com. 419 | ad.harrenmedianetwork.com. 420 | www.deviantart.com. 421 | www.shufuni.com. 422 | www.ustream.tv. 423 | www.beemp3.com. 424 | mail.126.com. 425 | member1.taobao.com. 426 | www.irctc.co.in. 427 | www.hudong.com. 428 | jbbs.livedoor.jp. 429 | www.stackoverflow.com. 430 | www.tom.com. 431 | msdn.microsoft.com. 432 | www.thefreedictionary.com. 433 | www.mpnrs.com. 434 | www.warez-bb.org. 435 | www.typepad.com. 436 | www.terra.com.br. 437 | www.odesk.com. 438 | www.friendster.com. 439 | www.people.com. 440 | mall.taobao.com. 441 | ent.163.com. 442 | www.surveymonkey.com. 443 | www.126.com. 444 | www.milliyet.com.tr. 445 | yule.sohu.com. 446 | www.fox.com. 447 | zhangmen.baidu.com. 448 | baike.baidu.com. 449 | www.atwiki.jp. 450 | www.usatoday.com. 451 | www.downtr.net. 452 | kankan.xunlei.com. 453 | www.demonoid.com. 454 | www.blackhatworld.com. 455 | www.freelancer.com. 456 | www.anonym.to. 457 | uk.msn.com. 458 | comment.myspace.com. 459 | www.seriesyonkis.com. 460 | www.360buy.com. 461 | www.pconline.com.cn. 462 | nlm.nih.gov. 463 | sports.qq.com. 464 | dzh.mop.com. 465 | www.traidnt.net. 466 | wireless.att.com. 467 | www.break.com. 468 | losangeles.craigslist.org. 469 | it.netlog.com. 470 | soccernet.espn.go.com. 471 | www.warriorforum.com. 472 | www.pornhublive.com. 473 | www.clicksor.com. 474 | www.time.com. 475 | www.advmaker.ru. 476 | support.microsoft.com. 477 | go.microsoft.com. 478 | www.musica.com. 479 | www.neobux.com. 480 | www.gutefrage.net. 481 | hp.infoseek.co.jp. 482 | www.eyny.com. 483 | ent.xunlei.com. 484 | headlines.yahoo.co.jp. 485 | www.corriere.it. 486 | www.freakshare.net. 487 | www.elpais.com. 488 | www.lenta.ru. 489 | search.aol.com. 490 | www.careerbuilder.com. 491 | www.chip.de. 492 | www.forbes.com. 493 | www.dantri.com.vn. 494 | www.kooora.com. 495 | www.indeed.com. 496 | www.nu.nl. 497 | eu1.badoo.com. 498 | www.abcnews.go.com. 499 | www.rediff.com. 500 | www.immobilienscout24.de. 501 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "jekyll", "~> 4.3.4" # installed by `gem jekyll` 4 | # gem "webrick" # required when using Ruby >= 3 and Jekyll <= 4.2.2 5 | 6 | gem "just-the-docs", "0.10.1" # pinned to the current release 7 | # gem "just-the-docs" # always download the latest release 8 | 9 | gem "jekyll-relative-links", "0.7.0" 10 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.7) 5 | public_suffix (>= 2.0.2, < 7.0) 6 | bigdecimal (3.1.9) 7 | colorator (1.1.0) 8 | concurrent-ruby (1.3.4) 9 | em-websocket (0.5.3) 10 | eventmachine (>= 0.12.9) 11 | http_parser.rb (~> 0) 12 | eventmachine (1.2.7) 13 | ffi (1.17.1-arm64-darwin) 14 | ffi (1.17.1-x86_64-darwin) 15 | ffi (1.17.1-x86_64-linux-gnu) 16 | forwardable-extended (2.6.0) 17 | google-protobuf (4.29.2-arm64-darwin) 18 | bigdecimal 19 | rake (>= 13) 20 | google-protobuf (4.29.2-x86_64-darwin) 21 | bigdecimal 22 | rake (>= 13) 23 | google-protobuf (4.29.2-x86_64-linux) 24 | bigdecimal 25 | rake (>= 13) 26 | http_parser.rb (0.8.0) 27 | i18n (1.14.6) 28 | concurrent-ruby (~> 1.0) 29 | jekyll (4.3.4) 30 | addressable (~> 2.4) 31 | colorator (~> 1.0) 32 | em-websocket (~> 0.5) 33 | i18n (~> 1.0) 34 | jekyll-sass-converter (>= 2.0, < 4.0) 35 | jekyll-watch (~> 2.0) 36 | kramdown (~> 2.3, >= 2.3.1) 37 | kramdown-parser-gfm (~> 1.0) 38 | liquid (~> 4.0) 39 | mercenary (>= 0.3.6, < 0.5) 40 | pathutil (~> 0.9) 41 | rouge (>= 3.0, < 5.0) 42 | safe_yaml (~> 1.0) 43 | terminal-table (>= 1.8, < 4.0) 44 | webrick (~> 1.7) 45 | jekyll-include-cache (0.2.1) 46 | jekyll (>= 3.7, < 5.0) 47 | jekyll-relative-links (0.7.0) 48 | jekyll (>= 3.3, < 5.0) 49 | jekyll-sass-converter (3.0.0) 50 | sass-embedded (~> 1.54) 51 | jekyll-seo-tag (2.8.0) 52 | jekyll (>= 3.8, < 5.0) 53 | jekyll-watch (2.2.1) 54 | listen (~> 3.0) 55 | just-the-docs (0.10.1) 56 | jekyll (>= 3.8.5) 57 | jekyll-include-cache 58 | jekyll-seo-tag (>= 2.0) 59 | rake (>= 12.3.1) 60 | kramdown (2.5.1) 61 | rexml (>= 3.3.9) 62 | kramdown-parser-gfm (1.1.0) 63 | kramdown (~> 2.0) 64 | liquid (4.0.4) 65 | listen (3.9.0) 66 | rb-fsevent (~> 0.10, >= 0.10.3) 67 | rb-inotify (~> 0.9, >= 0.9.10) 68 | mercenary (0.4.0) 69 | pathutil (0.16.2) 70 | forwardable-extended (~> 2.6) 71 | public_suffix (6.0.1) 72 | rake (13.2.1) 73 | rb-fsevent (0.11.2) 74 | rb-inotify (0.11.1) 75 | ffi (~> 1.0) 76 | rexml (3.4.0) 77 | rouge (4.5.1) 78 | safe_yaml (1.0.5) 79 | sass-embedded (1.83.1-arm64-darwin) 80 | google-protobuf (~> 4.29) 81 | sass-embedded (1.83.1-x86_64-darwin) 82 | google-protobuf (~> 4.29) 83 | sass-embedded (1.83.1-x86_64-linux-gnu) 84 | google-protobuf (~> 4.29) 85 | terminal-table (3.0.2) 86 | unicode-display_width (>= 1.1.1, < 3) 87 | unicode-display_width (2.6.0) 88 | webrick (1.9.1) 89 | 90 | PLATFORMS 91 | arm64-darwin-21 92 | universal-darwin-22 93 | universal-darwin-23 94 | x86_64-darwin-19 95 | x86_64-linux 96 | 97 | DEPENDENCIES 98 | jekyll (~> 4.3.4) 99 | jekyll-relative-links (= 0.7.0) 100 | just-the-docs (= 0.10.1) 101 | 102 | BUNDLED WITH 103 | 2.3.9 104 | -------------------------------------------------------------------------------- /docs/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 just-the-docs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # just-the-docs-template 2 | 3 | ## Building and previewing your site locally 4 | 5 | Assuming [Jekyll] and [Bundler] are installed on your computer: 6 | 7 | 1. Change your working directory to the root directory of your site. 8 | 9 | 2. Run `bundle install`. 10 | 11 | 3. Run `bundle exec jekyll serve` to build your site and preview it at `localhost:4000`. 12 | 13 | The built site is stored in the directory `_site`. 14 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: dnspyre 2 | description: CLI tool for a high QPS DNS benchmark 3 | theme: just-the-docs 4 | 5 | url: https://tantalor93.github.io/dnspyre 6 | 7 | aux_links: 8 | GitHub: https://github.com/tantalor93/dnspyre 9 | 10 | nav_external_links: 11 | - title: Source 12 | url: https://github.com/tantalor93/dnspyre 13 | 14 | plugins: 15 | - jekyll-relative-links 16 | 17 | logo: "/assets/logo.png" 18 | favicon_ico: "/assets/favicon.ico" 19 | 20 | callouts: 21 | note: 22 | color: purple 23 | warning: 24 | color: red 25 | important: 26 | color: blue 27 | -------------------------------------------------------------------------------- /docs/_includes/head_custom.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tantalor93/dnspyre/0cde3372b4b4e13e91bdf2f939b45aa16c5929dc/docs/assets/demo.gif -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tantalor93/dnspyre/0cde3372b4b4e13e91bdf2f939b45aa16c5929dc/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tantalor93/dnspyre/0cde3372b4b4e13e91bdf2f939b45aa16c5929dc/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/basics.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basics 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Basics 8 | *dnspyre* is a tool for benchmarking DNS servers, it works by spawning configured number of concurrent worker thread, where each worker thread 9 | is sending DNS queries for a domains provided to the *dnspyre* tool. The *dnspyre* runs until one of the conditions is met: 10 | * configured number of repetitions of domain queries is sent (if `--number` flag is specified) 11 | * the required duration of benchmark run elapses (if `--duration` flag is specified) 12 | * benchmark is interrupted with the SIGINT signal 13 | 14 | ## Run benchmark with the configured number of repetitions 15 | This example runs the benchmark in 10 parallel threads, where each thread will send 2 `example.com.` DNS queries 16 | of type `A` one after another to the `8.8.8.8` server 17 | 18 | ``` 19 | dnspyre -n 2 -c 10 --server 8.8.8.8 example.com 20 | ``` 21 | 22 | ## Run benchmark over specified time 23 | This example runs the benchmark in 10 parallel threads for a duration of 30 seconds while sending `example.com` DNS queries of type `A` 24 | to the `8.8.8.8` server 25 | 26 | ``` 27 | dnspyre --duration 30s -c 10 --server 8.8.8.8 google.com 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/concurrency.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Concurrency 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Concurrency 8 | *dnspyre* by default benchmarks a DNS server using a single thread (worker). This can be adjusted using `-c` (`--concurrency`) flag. 9 | 10 | In this example, the benchmark runs in 10 parallel threads (option `-c`) , where each thread will send 2 (option `-n`) `example.com.` DNS queries 11 | of type `A` one after another to the `8.8.8.8` server 12 | 13 | ``` 14 | dnspyre -n 2 -c 10 --server 8.8.8.8 example.com 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/doh.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DoH 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # DoH 8 | *dnspyre* supports running benchmarks against [RFC-8484](https://www.rfc-editor.org/rfc/rfc8484) compatible DNS over HTTPS servers 9 | 10 | ``` 11 | dnspyre --server 'https://1.1.1.1' google.com 12 | ``` 13 | 14 | or 15 | 16 | ``` 17 | dnspyre --server https://cloudflare-dns.com google.com 18 | ``` 19 | 20 | See other examples of customization of DoH benchmarks 21 | * TOC 22 | {:toc} 23 | 24 | 25 | ## DoH via GET/POST 26 | you can also specify whether the DoH is done via GET or POST using `--doh-method` 27 | 28 | ``` 29 | dnspyre --server 'https://1.1.1.1' --doh-method get google.com 30 | ``` 31 | 32 | benchmarking DoH server via DoH over POST method 33 | 34 | ``` 35 | dnspyre --server 'https://1.1.1.1' --doh-method post google.com 36 | ``` 37 | 38 | ## DoH/1.1, DoH/2, DoH/3 39 | you can also specify whether the DoH is done over HTTP/1.1, HTTP/2, HTTP/3 using `--doh-protocol`, for example: 40 | 41 | ``` 42 | dnspyre --server 'https://1.1.1.1' --doh-protocol 2 google.com 43 | ``` 44 | 45 | ## DoH via plain HTTP 46 | even plain HTTP without TLS can be used as transport for DoH requests, this is configured based on server URL containing either `https://` or `http://` 47 | 48 | ``` 49 | dnspyre --server http://127.0.0.1 google.com 50 | ``` 51 | 52 | ## DoH with self-signed certificates 53 | In some cases you might want to skip invalid and self-signed certificates, this can be achieved by using `--insecure` argument 54 | 55 | ``` 56 | dnspyre --server https://127.0.0.1 --insecure google.com 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/domainsources.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Domain sources 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Domain sources 8 | *dnspyre* benchmarks DNS servers by querying the domains specified as arguments to the tool, the domains to the tool can be passed in a various ways: 9 | 10 | 11 | ## Domains provided directly as arguments 12 | You can pass an arbitrary number of domains to be used for the DNS benchmark, by specifying more arguments. In this example, domains 13 | `redsift.io`, `example.com`, `google.com` are used to generate DNS queries 14 | 15 | ``` 16 | dnspyre -n 10 -c 10 --server 8.8.8.8 redsift.io example.com google.com 17 | ``` 18 | 19 | ## Domains provided using file on local filesystem 20 | Instead of specifying domains as arguments to the *dnspyre* tool, you can just specify a file containing domains to be used by the tool. 21 | By referencing the file using `@`. In this example, the domains are read from the `data/2-domains` file. 22 | 23 | ``` 24 | dnspyre -n 10 -c 10 --server 8.8.8.8 @data/2-domains 25 | ``` 26 | 27 | ## Domains provided using file publicly available using HTTP(s) 28 | The file containing hostnames does not need to be available locally, it can be also downloaded from the remote location using HTTP(s). 29 | In this example, the domains are downloaded from the https://raw.githubusercontent.com/Tantalor93/dnspyre/master/data/2-domains 30 | 31 | ``` 32 | dnspyre -n 10 -c 10 --server 8.8.8.8 https://raw.githubusercontent.com/Tantalor93/dnspyre/master/data/2-domains 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/doq.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DoQ 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # DoQ 8 | *dnspyre* supports running benchmarks against [RFC-9250](https://datatracker.ietf.org/doc/rfc9250/) compatible DNS over QUIC servers 9 | 10 | ``` 11 | dnspyre --server quic://dns.adguard-dns.com google.com 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/dot.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DoT 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # DoT 8 | *dnspyre* supports running benchmarks against [RFC-7858](https://datatracker.ietf.org/doc/html/rfc7858) compatible DNS over TLS servers 9 | 10 | ``` 11 | dnspyre --dot --server 8.8.8.8:853 idnes.cz 12 | ``` 13 | 14 | also you can provide a DNS server hostname instead of the server IP address 15 | 16 | ``` 17 | dnspyre --dot --server dns.google google.com 18 | ``` 19 | 20 | ## DoT with self-signed certificates 21 | In some cases you might want to skip invalid and self-signed certificates, this can be achieved by using `--insecure` argument 22 | 23 | ``` 24 | dnspyre --server 127.0.0.1:5553 --dot --insecure google.com 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/edns0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: EDNS0 and DNSSEC 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # EDNS0 and DNSSEC 8 | *dnspyre* supports sending DNS requests with [EDNS0](https://datatracker.ietf.org/doc/html/rfc6891) extension, currently these EDNS0 features are supported: 9 | 10 | ## UDP message size 11 | advertisement for support of larger DNS response size (UDP message size) using `--edns0` flag 12 | 13 | ``` 14 | dnspyre --server '1.1.1.1' google.com --edns0=1024 15 | ``` 16 | 17 | ## DNSSEC 18 | [DNSSEC](https://datatracker.ietf.org/doc/html/rfc9364) security extension using `--dnssec` flag, by using this flag the *dnspyre* will also 19 | count the **number of domains that were successfully validated by DNS resolver** 20 | 21 | ``` 22 | dnspyre --server '1.1.1.1' cloudflare.com --dnssec 23 | ``` 24 | 25 | ## EDNS0 options 26 | sending various EDNS0 options using `--ednsopt` flag, you have to specify the decimal **EDNS0 option code** (see [IANA registry](https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-11)) and hex-string representing **EDNS0 option data**, 27 | data format depends on the EDNS0 option 28 | 29 | for example to send [client subnet EDNS0 option](https://datatracker.ietf.org/doc/html/rfc7871) for subnet `81.0.198.170/24` you specify code `8` and data `000118005100c6` (`0001` = IPv4 Family, `18` = source mask `/24`, `00` = no additional scope, `5100C6AA` = `81.0.198.170` ) 30 | 31 | ``` 32 | dnspyre --server '8.8.8.8' aws.amazon.com --ednsopt '8:000118005100c6' 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | layout: default 4 | nav_order: 2 5 | has_children: true 6 | --- 7 | 8 | # Examples 9 | -------------------------------------------------------------------------------- /docs/failoncondition.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fail on condition 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Fail on condition 8 | v3.1.0 9 | {: .label .label-yellow } 10 | *dnspyre* by default always returns a zero exit code, but this behaviour can be adjusted by using `--fail ` flag, 11 | which can be used to specify predefined conditions that will cause a *dnspyre* to return a non-zero exit code. 12 | 13 | Currently, the dnspyre supports these fail conditions: 14 | * `ioerror` = *dnspyre* exits with a non-zero status code if there is at least 1 IO error (*dnspyre* failed to send DNS request or receive DNS response) 15 | * `negative` = *dnspyre* exits with a non-zero status code if there is at least 1 negative DNS answer (`NXDOMAIN` or `NODATA` response) 16 | * `error` = *dnspyre* exits with a non-zero status code if there is at least 1 error DNS response (`SERVFAIL`, `FORMERR`, `REFUSED`, etc.) 17 | * `idmismatch` = *dnspyre* exits with a non-zero status code if there is at least 1 ID mismatch between DNS request and response 18 | 19 | So for example to return a non-zero exit code, when benchmark fails to send request or receive response you would specify `--fail ioerror` flag 20 | ``` 21 | dnspyre --server 1.2.3.4 google.com --fail ioerror 22 | ``` 23 | 24 | These fail conditions can be combined, this is achieved by repeating the `--flag` flag multiple times with different conditions. 25 | ``` 26 | dnspyre --server 8.8.8.8 nxdomain.cz --fail ioerror --fail error --fail negative 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/graphs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plotting graphs 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Plotting graphs 8 | *dnspyre* is able to also visualize the benchmark results as graphs, plotting is enabled by using `--plot` option and providing valid path where to export graphs. 9 | Graphs are exported into the new subdirectory `graphs-` on provided path 10 | 11 | For example, this command 12 | 13 | ``` 14 | dnspyre -d 30s -c 20 --server 8.8.8.8 --plot . https://raw.githubusercontent.com/Tantalor93/dnspyre/master/data/10000-domains 15 | ``` 16 | 17 | generates these graphs: 18 | * response latency histogram, see [Latency histogram](#latency-histogram) section 19 | * response latency boxplot, see [Latency boxplot](#latency-boxplot) section 20 | * barchart of response codes, see [Response codes barchart](#response-codes-barchart) section 21 | * throughput of DNS server during the benchmark, see [Throughput line graph](#throughput-line-graph) section 22 | * line graphs of observed latencies of responses of DNS server, see [Latency line plot](#latency-line-plot) section 23 | * error rate over time, see [Error rate over time plot](#error-rate-over-time-plot) section 24 | 25 | ## Latency histogram 26 | Shows the distribution of response latencies 27 | 28 | ![latency histogram](graphs/latency-histogram.svg) 29 | 30 | ## Latency boxplot 31 | Shows the distribution of response latencies 32 | 33 | ![latency boxplot](graphs/latency-boxplot.svg) 34 | 35 | ## Response codes barchart 36 | Shows the distribution of DNS server response codes 37 | 38 | ![responses bar](graphs/responses-barchart.svg) 39 | 40 | ## Throughput line graph 41 | Shows the throughput of DNS requests during benchmark execution 42 | 43 | ![throughput line](graphs/throughput-lineplot.svg) 44 | 45 | ## Latency line plot 46 | Shows the latencies of DNS responses during benchmark execution 47 | 48 | ![latency line](graphs/latency-lineplot.svg) 49 | 50 | ## Error rate over time plot 51 | Shows the number of IO errors during benchmark execution 52 | 53 | ![error rate line](graphs/errorrate-lineplot.svg) 54 | -------------------------------------------------------------------------------- /docs/graphs/errorrate-lineplot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Error rate over time 10 | Time of test (s) 12 | 30 14 | 31 16 | 32 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Number of errors (per sec) 33 | 34 | 194 36 | 195 38 | 196 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /docs/graphs/latency-histogram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Latencies distribution 10 | Latencies (ms) 12 | 1000 14 | 2000 16 | 3000 18 | 4000 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Number of requests 45 | 46 | 0 48 | 3000 50 | 6000 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /docs/graphs/responses-barchart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Response code distribution 10 | Response codes 12 | 13 | Number of requests 15 | 16 | 0 18 | 3000 20 | 6000 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | NOERROR 40 | 41 | 42 | SERVFAIL 44 | 45 | 46 | NXDOMAIN 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/graphs/throughput-lineplot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Throughput per second 10 | Time of test (s) 12 | 4 14 | 16 16 | 28 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Number of requests (per sec) 27 | 28 | 50 30 | 200 32 | 350 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | layout: home 4 | nav_order: 0 5 | --- 6 | 7 | # dnspyre 8 | 9 | *dnspyre* is a command-line DNS benchmark tool built to stress test and measure the performance of DNS servers. You can easily run benchmark from MacOS, Linux or Windows systems. 10 | This tool is based and originally forked from [dnstrace](https://github.com/redsift/dnstrace), but was largely rewritten and enhanced with additional functionality. 11 | 12 | This tool supports wide variety of options to customize DNS benchmark and benchmark output. For example, you can: 13 | * benchmark DNS servers using DNS queries over UDP or TCP, see [plain DNS example](plaindns.md) 14 | * benchmark DNS servers with all kinds of query types like A, AAAA, CNAME, HTTPS, ... (`--type` option) 15 | * benchmark DNS servers with a lot of parallel queries and connections (`--number`, `--concurrency` options) 16 | * benchmark DNS servers for a specified duration (`--duration` option) 17 | * benchmark DNS servers with DoT ([DNS over TLS](https://datatracker.ietf.org/doc/html/rfc7858)), see [DoT example](dot.md) 18 | * benchmark DNS servers using DoH ([DNS over HTTPS](https://datatracker.ietf.org/doc/html/rfc8484)), see [DoH example](doh.md) 19 | * benchmark DNS servers using DoQ ([DNS over QUIC](https://datatracker.ietf.org/doc/rfc9250/)), see [DoQ example](doq.md) 20 | * benchmark DNS servers with uneven random load from provided high volume resources (see `--probability` option) 21 | * plot benchmark results via CLI histogram or plot the benchmark results as boxplot, histogram, line graphs and export them via all kind of image formats like png, svg and pdf. (see `--plot` and `--plotf` options) 22 | 23 | ![demo](assets/demo.gif) 24 | 25 | ## Usage 26 | 27 | ``` 28 | usage: dnspyre [] ... 29 | 30 | A high QPS DNS benchmark. 31 | 32 | 33 | Flags: 34 | --[no-]help Show context-sensitive help (also try --help-long and --help-man). 35 | -s, --server="127.0.0.1" Server represents (plain DNS, DoT, DoH or DoQ) server, which will be benchmarked. Format depends on the 36 | DNS protocol, that should be used for DNS benchmark. For plain DNS (either over UDP or TCP) the format is 37 | [:port], if port is not provided then port 53 is used. For DoT the format is [:port], 38 | if port is not provided then port 853 is used. For DoH the format is https://[:port][/path] or 39 | http://[:port][/path], if port is not provided then either 443 or 80 port is used. If no path is 40 | provided, then /dns-query is used. For DoQ the format is quic://[:port], if port is not provided 41 | then port 853 is used. 42 | -t, --type=A ... Query type. Repeatable flag. If multiple query types are specified then each query will be duplicated for 43 | each type. 44 | -n, --number=NUMBER How many times the provided queries are repeated. Note that the total number of queries issued = 45 | types*number*concurrency*len(queries). 46 | -c, --concurrency=1 Number of concurrent queries to issue. 47 | -l, --rate-limit=0 Apply a global questions / second rate limit. 48 | --rate-limit-worker=0 Apply a questions / second rate limit for each concurrent worker specified by --concurrency option. 49 | --query-per-conn=0 Queries on a connection before creating a new one. 0: unlimited. Applicable for plain DNS and DoT, 50 | this option is not considered for DoH or DoQ. 51 | -r, --[no-]recurse Allow DNS recursion. Enabled by default. 52 | --probability=1 Each provided hostname will be used with provided probability. Value 1 and above means that each hostname 53 | will be used by each concurrent benchmark goroutine. Useful for randomizing queries across benchmark 54 | goroutines. 55 | --ednsopt="" code[:value], Specify EDNS option with code point code and optionally payload of value as a hexadecimal 56 | string. code must be an arbitrary numeric value. 57 | --[no-]dnssec Allow DNSSEC (sets DO bit for all DNS requests to 1) 58 | --edns0=0 Configures EDNS0 usage in DNS requests send by benchmark and configures EDNS0 buffer size to the 59 | specified value. When 0 is configured, then EDNS0 is not used. 60 | --[no-]tcp Use TCP for DNS requests. 61 | --[no-]dot Use DoT (DNS over TLS) for DNS requests. 62 | --write=1s write timeout. 63 | --read=3s read timeout. 64 | --connect=1s connect timeout. 65 | --request=5s request timeout. 66 | --[no-]codes Enable counting DNS return codes. Enabled by default. 67 | --min=400µs Minimum value for timing histogram. 68 | --max=MAX Maximum value for timing histogram. 69 | --precision=[1-5] Significant figure for histogram precision. 70 | --[no-]distribution Display distribution histogram of timings to stdout. Enabled by default. 71 | --csv=/path/to/file.csv Export distribution to CSV. 72 | --[no-]json Report benchmark results as JSON. 73 | --[no-]silent Disable stdout. 74 | --[no-]color ANSI Color output. Enabled by default. 75 | --plot=/path/to/folder Plot benchmark results and export them to the directory. 76 | --plotf=svg Format of graphs. Supported formats: svg, png and jpg. 77 | --doh-method=post HTTP method to use for DoH requests. Supported values: get, post. 78 | --doh-protocol=1.1 HTTP protocol to use for DoH requests. Supported values: 1.1, 2 and 3. 79 | --[no-]insecure Disables server TLS certificate validation. Applicable for DoT, DoH and DoQ. 80 | -d, --duration=1m Specifies for how long the benchmark should be executing, the benchmark will run for the specified time 81 | while sending DNS requests in an infinite loop based on the data source. After running for the specified 82 | duration, the benchmark is canceled. This option is exclusive with --number option. The duration is 83 | specified in GO duration format e.g. 10s, 15m, 1h. 84 | --[no-]progress Controls whether the progress bar is shown. Enabled by default. 85 | --fail=ioerror ... Controls conditions upon which the dnspyre will exit with a non-zero exit code. Repeatable flag. 86 | Supported options are 'ioerror' (fail if there is at least 1 IO error), 'negative' (fail if there is at 87 | least 1 negative DNS answer), 'error' (fail if there is at least 1 error DNS response), 'idmismatch' 88 | (fail there is at least 1 ID mismatch between DNS request and response). 89 | --[no-]log-requests Controls whether the Benchmark requests are logged. Requests are logged into the file specified by 90 | --log-requests-path flag. Disabled by default. 91 | --log-requests-path="requests.log" 92 | Specifies path to the file, where the request logs will be logged. If the file exists, the logs will be 93 | appended to the file. If the file does not exist, the file will be created. 94 | --[no-]separate-worker-connections 95 | Controls whether the concurrent workers will try to share connections to the server or not. When enabled 96 | the workers will use separate connections. Disabled by default. 97 | --request-delay="0s" Configures delay to be added before each request done by worker. Delay can be either constant or 98 | randomized. Constant delay is configured as single duration (e.g. 500ms, 2s, etc.). 99 | Randomized delay is configured as interval of two durations - (e.g. 1s-2s, 100 | 500ms-2s, etc.), where the actual delay is random value from the interval that is randomized after each 101 | request. 102 | --[no-]version Show application version. 103 | 104 | Args: 105 | Queries to issue. It can be a local file referenced using @, for example @data/2-domains. It can also be 106 | resource accessible using HTTP, like https://raw.githubusercontent.com/Tantalor93/dnspyre/master/data/1000-domains, 107 | in that case, the file will be downloaded and saved in-memory. These data sources can be combined, for example "google.com 108 | @data/2-domains https://raw.githubusercontent.com/Tantalor93/dnspyre/master/data/2-domains" 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | layout: default 4 | nav_order: 1 5 | --- 6 | 7 | # Installation 8 | 9 | You can install *dnspyre* using [Homebrew](https://brew.sh) package manager 10 | 11 | ``` 12 | brew tap tantalor93/dnspyre 13 | brew install dnspyre 14 | ``` 15 | 16 | Also you can use standard [Go tooling](https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies) to install *dnspyre* 17 | 18 | ``` 19 | go install github.com/tantalor93/dnspyre/v3@latest 20 | ``` 21 | 22 | Or you can download latest *dnspyre* binary archive for your operating system and architecture [here](https://github.com/Tantalor93/dnspyre/releases/latest) 23 | 24 | ## Bash/ZSH Shell completion 25 | When *dnspyre* is installed using [Homebrew](https://brew.sh), the shell completions are installed automatically, if Homebrew is configured to [install them](https://docs.brew.sh/Shell-Completion) 26 | 27 | Otherwise you have to setup completions manually: 28 | 29 | For **ZSH**, add to your `~/.zprofile` (or equivalent ZSH configuration file) 30 | 31 | ``` 32 | eval "$(dnspyre --completion-script-zsh)" 33 | ``` 34 | 35 | For **Bash**, add to your `~/.bash_profile` (or equivalent Bash configuration file) 36 | 37 | ``` 38 | eval "$(dnspyre --completion-script-bash)" 39 | ``` 40 | 41 | # Docker image 42 | if you don't want to install *dnspyre* locally, you can use prepared [Docker image](https://github.com/Tantalor93/dnspyre/pkgs/container/dnspyre), 43 | for example 44 | 45 | ``` 46 | docker run ghcr.io/tantalor93/dnspyre --server 8.8.8.8 google.com 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/jsonoutput.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: JSON output 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # JSON output 8 | By specifying `--json` flag, *dnspyre* can output benchmark results in a JSON format, which is better for further automatic processing 9 | 10 | ``` 11 | dnspyre --duration 5s --server 8.8.8.8 google.com --json 12 | ``` 13 | 14 | example of chaining of *dnspyre* with [jq](https://stedolan.github.io/jq/) for getting pretty JSON 15 | 16 | ``` 17 | dnspyre --duration 5s --server 8.8.8.8 google.com --no-distribution --json | jq '.' 18 | ``` 19 | 20 | like this 21 | 22 | ``` 23 | { 24 | "totalRequests": 276, 25 | "totalSuccessCodes": 276, 26 | "totalErrors": 0, 27 | "totalIDmismatch": 0, 28 | "totalTruncatedResponses": 0, 29 | "responseRcodes": { 30 | "NOERROR": 276 31 | }, 32 | "questionTypes": { 33 | "A": 276 34 | }, 35 | "queriesPerSecond": 55.18, 36 | "benchmarkDurationSeconds": 5, 37 | "latencyStats": { 38 | "minMs": 12, 39 | "meanMs": 18, 40 | "stdMs": 13, 41 | "maxMs": 176, 42 | "p99Ms": 71, 43 | "p95Ms": 33, 44 | "p90Ms": 24, 45 | "p75Ms": 15, 46 | "p50Ms": 14 47 | } 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/kubernetes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Kubernetes 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Kubernetes 8 | 9 | One of the use cases for using *dnspyre* [Docker image](https://github.com/Tantalor93/dnspyre/pkgs/container/dnspyre) is testing the performance of 10 | the internal DNS server from inside of your Kubernetes cluster. This can be achieved by running a *dnspyre* docker image inside a Kubernetes pod, 11 | for example by running a kubectl command like this: 12 | 13 | ``` 14 | kubectl run dnspyre --restart=Never --image=ghcr.io/tantalor93/dnspyre -- https://raw.githubusercontent.com/Tantalor93/dnspyre/master/data/alexa --server kube-dns.kube-system.svc.cluster.local --duration 1m 15 | ``` 16 | 17 | and then check the output using 18 | 19 | ``` 20 | kubectl logs dnspyre 21 | ``` 22 | 23 | You might want to test the performance from multiple instances/pods, this can be easily achieved by deploying *dnspyre* in multiple pods, 24 | for example using Kubernetes Deployment : 25 | 26 | ``` 27 | apiVersion: apps/v1 28 | kind: Deployment 29 | metadata: 30 | name: dnspyre-deployment 31 | spec: 32 | replicas: 2 33 | selector: 34 | matchLabels: 35 | app: dnspyre 36 | template: 37 | metadata: 38 | labels: 39 | app: dnspyre 40 | spec: 41 | containers: 42 | - name: dnspyre 43 | image: ghcr.io/tantalor93/dnspyre 44 | command: 45 | - "/dnspyre" 46 | args: 47 | - "https://raw.githubusercontent.com/Tantalor93/dnspyre/master/data/alexa" 48 | - "--server" 49 | - "kube-dns.kube-system.svc.cluster.local" 50 | - "--duration" 51 | - "1m" 52 | - "-c" 53 | - "100" 54 | resources: 55 | limits: 56 | cpu: "1" 57 | memory: "900Mi" 58 | requests: 59 | cpu: "0.1" 60 | memory: "128Mi" 61 | ``` 62 | and then applying this Deployment to your cluster: 63 | 64 | ``` 65 | kubectl apply -f dnspyre-deployment.yml 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/plaindns.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plain DNS 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Plain DNS 8 | *dnspyre* supports running benchmarks against DNS servers using plain DNS over UDP (default option) 9 | 10 | ``` 11 | dnspyre --server 8.8.8.8 google.com 12 | ``` 13 | or you can benchmark using DNS over TCP 14 | 15 | ``` 16 | dnspyre --tcp --server 8.8.8.8 google.com 17 | ``` 18 | 19 | also you can provide a DNS server hostname instead of the server IP address 20 | 21 | ``` 22 | dnspyre --server dns.google google.com 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/querytypes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DNS query types 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # DNS query types 8 | You can choose, which type of query to send to the DNS server using `-t` option, by default *dnspyre* generates *A* (IPv4 DNS query). 9 | In this example, the *dnspyre* is configured to send *AAAA* queries (IPv6 queries) 10 | 11 | ``` 12 | dnspyre -n 2 -c 10 --server 8.8.8.8 -t AAAA example.com 13 | ``` 14 | 15 | ## Combining multiple query types in the benchmark 16 | Multiple DNS query types can be specified for *dnspyre* tool. This can be achieved by repeating type `-t`, the queries for domains will be 17 | repeated for each specified type 18 | 19 | ``` 20 | dnspyre -n 10 -c 10 --server 8.8.8.8 -t A -t AAAA example.com 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/randomizing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Randomizing benchmarks 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Randomizing benchmarks 8 | In some cases, you might want to randomize the DNS queries generated by the *dnspyre*. This is especially useful, 9 | when you are running a *dnspyre* in concurrent mode (`--concurrency` > 1) and you would like to better simulate real-life scenarios. 10 | Without using `--probability` flag, all the concurrent workers generated by the benchmark run are generating exactly the same queries, this 11 | might have a downside that most of the DNS queries will be served from the cache. 12 | 13 | You can randomize queries fired by each concurrent thread by using `--probability` flag and specifying probability lesser than 1, 14 | in this example, roughly every third hostname from the data source is used by each concurrent benchmark thread. 15 | 16 | together with probability option this can be used for generating arbitrary random load 17 | 18 | ``` 19 | dnspyre --duration 30s -c 10 --server 8.8.8.8 -t A -t AAAA https://raw.githubusercontent.com/Tantalor93/dnspyre/master/data/1000-domains --probability 0.33 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/ratelimit.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Rate limiting 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Rate limiting 8 | *dnspyre* supports rate limiting number of queries send per second, for that you can use `--rate-limit` and `--rate-limit-worker` flags 9 | 10 | `--rate-limit` is used for setting a global rate limit, meaning that all concurrent workers spawned based on `--concurrency` flag will share this limit. 11 | It might happen, that some workers will be starving and the load generated by the `dnspyre` will not be evenly generated from all workers. 12 | 13 | For example this will generate load for 10 seconds using 10 concurrent workers and limit the load to 1 query per second 14 | 15 | ``` 16 | dnspyre --duration 10s -c 10 --rate-limit 1 --server '8.8.8.8' google.com 17 | ``` 18 | 19 | \ 20 | `--rate-limit-worker` is used for setting a rate limit **separately** for each concurrent worker spawned based on `--concurrency` flag. 21 | 22 | For example this will generate load for 10 seconds using 10 concurrent workers and limit the load generated by each worker to 1 query per second 23 | 24 | ``` 25 | dnspyre --duration 10s -c 10 --rate-limit-worker 1 --server '8.8.8.8' google.com 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/requestdelay.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Delaying requests 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Delaying requests 8 | v3.4.0 9 | {: .label .label-yellow } 10 | *dnspyre* by default generates queries one after another as soon as the previous query is finished. In some cases you might want to delay 11 | the queries. This is possible using `--request-delay` flag. This option allows user to specify either constant or randomized delay to be added 12 | before sending query. 13 | 14 | ## Constant delay 15 | To specify constant delay, you can specify arbitrary GO duration as parameter to the `--request-delay` flag. Each parallel worker will 16 | always wait for the specified duration before sending another query 17 | 18 | ``` 19 | dnspyre --duration 10s --server '1.1.1.1' google.com --request-delay 2s 20 | ``` 21 | 22 | ## Randomized delay 23 | To specify randomized delay, you can specify interval of GO durations `-` as parameter to the `--request-delay` flag. 24 | Each parallel worker will always wait for the random duration specified by the interval. If you specify interval `1s-2s`, workers will wait 25 | between 1 second and 2 seconds before sending another query 26 | 27 | ``` 28 | dnspyre --duration 10s --server '1.1.1.1' google.com --request-delay 1s-2s 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/requestlog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Request logging 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Request logging 8 | v3.2.0 9 | {: .label .label-yellow } 10 | *dnspyre* can also log all DNS requests it produces. Request logging can be enabled by running *dnspyre* with flag `--log-requests` 11 | 12 | ``` 13 | dnspyre --server 8.8.8.8 google.com -n 5 --log-requests 14 | ``` 15 | 16 | The request logs will be by default available in file `requests.log`. The log file path can be configured using `--log-requests-path`. 17 | If the file does not exist, it is created. If it exists, the logs are appended. 18 | 19 | ``` 20 | dnspyre --server 8.8.8.8 google.com -n 5 --log-requests --log-requests-path /tmp/requests.log 21 | ``` 22 | 23 | The request logs look like this: 24 | ``` 25 | 2024/04/28 21:45:14 worker:[0] reqid:[37449] qname:[google.com.] qtype:[A] respid:[37449] rcode:[NOERROR] respflags:[qr rd ra] err:[] duration:[75.875086ms] 26 | 2024/04/28 21:45:14 worker:[0] reqid:[34625] qname:[google.com.] qtype:[A] respid:[34625] rcode:[NOERROR] respflags:[qr rd ra] err:[] duration:[15.643628ms] 27 | 2024/04/28 21:45:14 worker:[0] reqid:[798] qname:[google.com.] qtype:[A] respid:[798] rcode:[NOERROR] respflags:[qr rd ra] err:[] duration:[12.087964ms] 28 | 2024/04/28 21:45:14 worker:[0] reqid:[54943] qname:[google.com.] qtype:[A] respid:[54943] rcode:[NOERROR] respflags:[qr rd ra] err:[] duration:[12.975761ms] 29 | 2024/04/28 21:45:14 worker:[0] reqid:[509] qname:[google.com.] qtype:[A] respid:[509] rcode:[NOERROR] respflags:[qr rd ra] err:[] duration:[11.784968ms] 30 | ``` 31 | You can see: 32 | * which worker executed the request (`worker`) 33 | * what was the request and response ID (`reqid` and `respid`) 34 | * what domain was queried and what type (`qname` and `qtype`) 35 | * what were the DNS response flags (`respflags`) 36 | * roundtrip duration (`duration`) 37 | -------------------------------------------------------------------------------- /docs/timeouts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuring timeouts 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Configuring timeouts 8 | *dnspyre* supports configuring various timeouts applied on outgoing DNS requests: 9 | * **connect timeout** - timeout for establishing connection to a DNS server, configurable using `--connect` flag 10 | * **write timeout** - timeout for writing a request to a DNS server, configurable using `--write` flag 11 | * **read timeout** - timeout for reading a response from a DNS server, configurable using `--read` flag 12 | * **request timeout** - overall timeout for establishing connection, sending request and reading response, configurable using `--request` flag 13 | 14 | For example to limit request timeout to 100ms, you would pass `--request` flag with value `100ms` 15 | 16 | ``` 17 | dnspyre --request 100ms --duration 10s --server 'quic://dns.adguard-dns.com' https://raw.githubusercontent.com/Tantalor93/dnspyre/master/data/1000-domains 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/workerconnections.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuring connection sharing 3 | layout: default 4 | parent: Examples 5 | --- 6 | 7 | # Configuring connection sharing 8 | v3.3.0 9 | {: .label .label-yellow } 10 | *dnspyre* by default tries to share connections between spawned concurrent workers as much as possible, so for example 11 | if DoH benchmark over HTTPS/2 with multiple concurrent workers is executed, then all the workers will share same single HTTPS/2 connection 12 | to the DNS server 13 | 14 | ``` 15 | dnspyre --server https://1.1.1.1 google.com -c 5 --doh-protocol 2 16 | ``` 17 | 18 | If you want to avoid sharing connection between concurrent workers, you can use `--separate-worker-connections` flag 19 | 20 | ``` 21 | dnspyre --server https://1.1.1.1 google.com -c 5 --doh-protocol 2 --separate-worker-connections 22 | ``` 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tantalor93/dnspyre/v3 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 7 | github.com/alecthomas/kingpin/v2 v2.4.0 8 | github.com/fatih/color v1.18.0 9 | github.com/miekg/dns v1.1.64 10 | github.com/montanaflynn/stats v0.7.1 11 | github.com/olekukonko/tablewriter v0.0.5 12 | github.com/quic-go/quic-go v0.44.0 13 | github.com/schollz/progressbar/v3 v3.17.1 14 | github.com/stretchr/testify v1.10.0 15 | github.com/tantalor93/doh-go v0.3.0 16 | github.com/tantalor93/doq-go v0.12.0 17 | go-hep.org/x/hep v0.36.0 18 | go.uber.org/ratelimit v0.3.1 19 | golang.org/x/net v0.35.0 20 | gonum.org/v1/gonum v0.15.1 21 | gonum.org/v1/plot v0.15.2 22 | ) 23 | 24 | require ( 25 | codeberg.org/go-fonts/liberation v0.4.1 // indirect 26 | codeberg.org/go-latex/latex v0.0.1 // indirect 27 | codeberg.org/go-pdf/fpdf v0.10.0 // indirect 28 | git.sr.ht/~sbinet/gg v0.6.0 // indirect 29 | github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect 30 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 31 | github.com/benbjohnson/clock v1.3.0 // indirect 32 | github.com/campoy/embedmd v1.0.0 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 35 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 36 | github.com/gonuts/binary v0.2.0 // indirect 37 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect 38 | github.com/mattn/go-colorable v0.1.13 // indirect 39 | github.com/mattn/go-isatty v0.0.20 // indirect 40 | github.com/mattn/go-runewidth v0.0.16 // indirect 41 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 42 | github.com/onsi/ginkgo/v2 v2.9.5 // indirect 43 | github.com/pmezard/go-difflib v1.0.0 // indirect 44 | github.com/quic-go/qpack v0.4.0 // indirect 45 | github.com/rivo/uniseg v0.4.7 // indirect 46 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 47 | go.uber.org/mock v0.4.0 // indirect 48 | golang.org/x/crypto v0.33.0 // indirect 49 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 50 | golang.org/x/image v0.24.0 // indirect 51 | golang.org/x/mod v0.23.0 // indirect 52 | golang.org/x/sync v0.11.0 // indirect 53 | golang.org/x/sys v0.30.0 // indirect 54 | golang.org/x/term v0.29.0 // indirect 55 | golang.org/x/text v0.22.0 // indirect 56 | golang.org/x/tools v0.30.0 // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | ) 59 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/tantalor93/dnspyre/v3/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /pkg/dnsbench/benchmark_common_api_test.go: -------------------------------------------------------------------------------- 1 | package dnsbench_test 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "strconv" 9 | "testing" 10 | "time" 11 | 12 | "github.com/miekg/dns" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | "github.com/tantalor93/dnspyre/v3/pkg/dnsbench" 16 | ) 17 | 18 | func assertResult(t *testing.T, rs []*dnsbench.ResultStats) { 19 | t.Helper() 20 | if assert.Len(t, rs, 2, "Run(ctx) rstats") { 21 | rs0 := rs[0] 22 | rs1 := rs[1] 23 | assertResultStats(t, rs0) 24 | assertResultStats(t, rs1) 25 | assertTimings(t, rs0) 26 | assertTimings(t, rs1) 27 | } 28 | } 29 | 30 | func assertResultStats(t *testing.T, rs *dnsbench.ResultStats) { 31 | t.Helper() 32 | assert.NotNil(t, rs.Hist, "Run(ctx) rstats histogram") 33 | 34 | if assert.NotNil(t, rs.Codes, "Run(ctx) rstats codes") { 35 | assert.EqualValues(t, 2, rs.Codes[0], "Run(ctx) rstats codes NOERROR, state:"+fmt.Sprint(rs.Codes)) 36 | } 37 | 38 | if assert.NotNil(t, rs.Qtypes, "Run(ctx) rstats qtypes") { 39 | assert.EqualValues(t, 1, rs.Qtypes[dns.TypeToString[dns.TypeA]], "Run(ctx) rstats qtypes A, state:"+fmt.Sprint(rs.Codes)) 40 | assert.EqualValues(t, 1, rs.Qtypes[dns.TypeToString[dns.TypeAAAA]], "Run(ctx) rstats qtypes AAAA, state:"+fmt.Sprint(rs.Codes)) 41 | } 42 | 43 | assert.EqualValues(t, 2, rs.Counters.Total, "Run(ctx) total counter") 44 | assert.Zero(t, rs.Counters.IOError, "error counter") 45 | assert.EqualValues(t, 2, rs.Counters.Success, "Run(ctx) success counter") 46 | assert.Zero(t, rs.Counters.IDmismatch, "Run(ctx) mismatch counter") 47 | assert.Zero(t, rs.Counters.Truncated, "Run(ctx) truncated counter") 48 | } 49 | 50 | func assertTimings(t *testing.T, rs *dnsbench.ResultStats) { 51 | t.Helper() 52 | if assert.Len(t, rs.Timings, 2, "Run(ctx) rstats timings") { 53 | t0 := rs.Timings[0] 54 | t1 := rs.Timings[1] 55 | assert.NotZero(t, t0.Duration, "Run(ctx) rstats timings duration") 56 | assert.NotZero(t, t0.Start, "Run(ctx) rstats timings start") 57 | assert.NotZero(t, t1.Duration, "Run(ctx) rstats timings duration") 58 | assert.NotZero(t, t1.Start, "Run(ctx) rstats timings start") 59 | } 60 | } 61 | 62 | type requestLog struct { 63 | worker int 64 | requestid int 65 | qname string 66 | qtype string 67 | respid int 68 | rcode string 69 | respflags string 70 | err string 71 | duration time.Duration 72 | } 73 | 74 | func assertRequestLogStructure(t *testing.T, reader io.Reader) { 75 | t.Helper() 76 | pattern := `.*worker:\[(.*)\] reqid:\[(.*)\] qname:\[(.*)\] qtype:\[(.*)\] respid:\[(.*)\] rcode:\[(.*)\] respflags:\[(.*)\] err:\[(.*)\] duration:\[(.*)\]$` 77 | regex := regexp.MustCompile(pattern) 78 | scanner := bufio.NewScanner(reader) 79 | var requestLogs []requestLog 80 | for scanner.Scan() { 81 | line := scanner.Text() 82 | 83 | matches := regex.FindStringSubmatch(line) 84 | require.Len(t, matches, 10, "request log does not have expected structure") 85 | 86 | workerID, err := strconv.Atoi(matches[1]) 87 | require.NoError(t, err, "worker ID is not number") 88 | 89 | requestID, err := strconv.Atoi(matches[2]) 90 | require.NoError(t, err, "request ID is not number") 91 | 92 | qname := matches[3] 93 | qtype := matches[4] 94 | 95 | respID, err := strconv.Atoi(matches[5]) 96 | require.NoError(t, err, "response ID is not number") 97 | 98 | rcode := matches[6] 99 | respflags := matches[7] 100 | errstr := matches[8] 101 | 102 | dur, err := time.ParseDuration(matches[9]) 103 | require.NoError(t, err, "duration is not correct time duration") 104 | 105 | requestLogs = append(requestLogs, requestLog{ 106 | worker: workerID, 107 | requestid: requestID, 108 | qname: qname, 109 | qtype: qtype, 110 | respid: respID, 111 | rcode: rcode, 112 | respflags: respflags, 113 | err: errstr, 114 | duration: dur, 115 | }) 116 | } 117 | 118 | workerIDs := map[int]int{} 119 | qtypes := map[string]int{} 120 | 121 | for _, v := range requestLogs { 122 | workerIDs[v.worker]++ 123 | qtypes[v.qtype]++ 124 | 125 | assert.Equal(t, "example.org.", v.qname) 126 | assert.NotZero(t, v.requestid) 127 | assert.NotZero(t, v.respid) 128 | assert.Equal(t, "NOERROR", v.rcode) 129 | assert.Equal(t, "qr rd", v.respflags) 130 | assert.Equal(t, "", v.err) 131 | assert.NotZero(t, v.duration) 132 | } 133 | assert.Equal(t, map[int]int{0: 2, 1: 2}, workerIDs) 134 | assert.Equal(t, map[string]int{"AAAA": 2, "A": 2}, qtypes) 135 | } 136 | 137 | // A returns an A record from rr. It panics on errors. 138 | func A(rr string) *dns.A { r, _ := dns.NewRR(rr); return r.(*dns.A) } 139 | -------------------------------------------------------------------------------- /pkg/dnsbench/benchmark_dot_api_test.go: -------------------------------------------------------------------------------- 1 | package dnsbench_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "fmt" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/miekg/dns" 14 | "github.com/stretchr/testify/suite" 15 | "github.com/tantalor93/dnspyre/v3/pkg/dnsbench" 16 | ) 17 | 18 | type DoTTestSuite struct { 19 | suite.Suite 20 | } 21 | 22 | func TestDoTTestSuite(t *testing.T) { 23 | suite.Run(t, new(DoTTestSuite)) 24 | } 25 | 26 | func (suite *DoTTestSuite) TestBenchmark_Run() { 27 | cert, err := tls.LoadX509KeyPair("testdata/test.crt", "testdata/test.key") 28 | suite.Require().NoError(err) 29 | 30 | certs, err := os.ReadFile("testdata/test.crt") 31 | suite.Require().NoError(err) 32 | 33 | pool, err := x509.SystemCertPool() 34 | suite.Require().NoError(err) 35 | 36 | pool.AppendCertsFromPEM(certs) 37 | config := tls.Config{ 38 | ServerName: "localhost", 39 | RootCAs: pool, 40 | Certificates: []tls.Certificate{cert}, 41 | MinVersion: tls.VersionTLS12, 42 | } 43 | 44 | server := NewServer(dnsbench.TLSTransport, &config, func(w dns.ResponseWriter, r *dns.Msg) { 45 | ret := new(dns.Msg) 46 | ret.SetReply(r) 47 | ret.Answer = append(ret.Answer, A("example.org. IN A 127.0.0.1")) 48 | 49 | // wait some time to actually have some observable duration 50 | time.Sleep(time.Millisecond * 500) 51 | 52 | w.WriteMsg(ret) 53 | }) 54 | defer server.Close() 55 | 56 | buf := bytes.Buffer{} 57 | bench := dnsbench.Benchmark{ 58 | Queries: []string{"example.org"}, 59 | Types: []string{"A", "AAAA"}, 60 | Server: server.Addr, 61 | TCP: false, 62 | Concurrency: 2, 63 | Count: 1, 64 | Probability: 1, 65 | WriteTimeout: 1 * time.Second, 66 | ReadTimeout: 3 * time.Second, 67 | ConnectTimeout: 1 * time.Second, 68 | RequestTimeout: 5 * time.Second, 69 | Rcodes: true, 70 | Recurse: true, 71 | Insecure: true, 72 | DOT: true, 73 | Writer: &buf, 74 | } 75 | 76 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 77 | defer cancel() 78 | rs, err := bench.Run(ctx) 79 | 80 | suite.Require().NoError(err, "expected no error from benchmark run") 81 | assertResult(suite.T(), rs) 82 | suite.Equal(fmt.Sprintf("Using 1 hostnames\nBenchmarking %s via tcp-tls with 2 concurrent requests \n", server.Addr), buf.String()) 83 | } 84 | 85 | func (suite *DoTTestSuite) TestBenchmark_Run_truncated() { 86 | cert, err := tls.LoadX509KeyPair("testdata/test.crt", "testdata/test.key") 87 | suite.Require().NoError(err) 88 | 89 | certs, err := os.ReadFile("testdata/test.crt") 90 | suite.Require().NoError(err) 91 | 92 | pool, err := x509.SystemCertPool() 93 | suite.Require().NoError(err) 94 | 95 | pool.AppendCertsFromPEM(certs) 96 | config := tls.Config{ 97 | ServerName: "localhost", 98 | RootCAs: pool, 99 | Certificates: []tls.Certificate{cert}, 100 | MinVersion: tls.VersionTLS12, 101 | } 102 | 103 | server := NewServer(dnsbench.TLSTransport, &config, func(w dns.ResponseWriter, r *dns.Msg) { 104 | ret := new(dns.Msg) 105 | ret.SetReply(r) 106 | ret.Answer = append(ret.Answer, A("example.org. IN A 127.0.0.1")) 107 | ret.Truncated = true 108 | 109 | // wait some time to actually have some observable duration 110 | time.Sleep(time.Millisecond * 500) 111 | 112 | w.WriteMsg(ret) 113 | }) 114 | defer server.Close() 115 | 116 | bench := dnsbench.Benchmark{ 117 | Queries: []string{"example.org"}, 118 | Types: []string{"A", "AAAA"}, 119 | Server: server.Addr, 120 | TCP: false, 121 | Concurrency: 2, 122 | Count: 1, 123 | Probability: 1, 124 | WriteTimeout: 1 * time.Second, 125 | ReadTimeout: 3 * time.Second, 126 | ConnectTimeout: 1 * time.Second, 127 | RequestTimeout: 5 * time.Second, 128 | Rcodes: true, 129 | Recurse: true, 130 | Insecure: true, 131 | DOT: true, 132 | } 133 | 134 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 135 | defer cancel() 136 | rs, err := bench.Run(ctx) 137 | 138 | suite.Require().NoError(err, "expected no error from benchmark run") 139 | suite.Require().Len(rs, 2, "expected results from two workers") 140 | 141 | suite.EqualValues(2, rs[0].Counters.Total, "there should be executions") 142 | suite.EqualValues(2, rs[0].Counters.Truncated, "there should be truncated messages") 143 | suite.EqualValues(2, rs[1].Counters.Total, "there should be executions") 144 | suite.EqualValues(2, rs[1].Counters.Truncated, "there should be truncated messages") 145 | } 146 | 147 | func (suite *DoTTestSuite) TestBenchmark_Run_error() { 148 | cert, err := tls.LoadX509KeyPair("testdata/test.crt", "testdata/test.key") 149 | suite.Require().NoError(err) 150 | 151 | certs, err := os.ReadFile("testdata/test.crt") 152 | suite.Require().NoError(err) 153 | 154 | pool, err := x509.SystemCertPool() 155 | suite.Require().NoError(err) 156 | 157 | pool.AppendCertsFromPEM(certs) 158 | config := tls.Config{ 159 | ServerName: "localhost", 160 | RootCAs: pool, 161 | Certificates: []tls.Certificate{cert}, 162 | MinVersion: tls.VersionTLS12, 163 | } 164 | 165 | server := NewServer(dnsbench.TLSTransport, &config, func(_ dns.ResponseWriter, _ *dns.Msg) { 166 | }) 167 | defer server.Close() 168 | 169 | bench := dnsbench.Benchmark{ 170 | Queries: []string{"example.org"}, 171 | Types: []string{"A", "AAAA"}, 172 | Server: server.Addr, 173 | TCP: false, 174 | Concurrency: 2, 175 | Count: 1, 176 | Probability: 1, 177 | WriteTimeout: 100 * time.Millisecond, 178 | ReadTimeout: 300 * time.Millisecond, 179 | ConnectTimeout: 100 * time.Millisecond, 180 | RequestTimeout: 500 * time.Millisecond, 181 | Rcodes: true, 182 | Recurse: true, 183 | Insecure: true, 184 | DOT: true, 185 | } 186 | 187 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 188 | defer cancel() 189 | rs, err := bench.Run(ctx) 190 | 191 | suite.Require().NoError(err, "expected no error from benchmark run") 192 | suite.Require().Len(rs, 2, "expected results from two workers") 193 | 194 | suite.EqualValues(2, rs[0].Counters.Total, "there should be executions") 195 | suite.EqualValues(2, rs[0].Counters.IOError, "there should be errors") 196 | suite.EqualValues(2, rs[1].Counters.Total, "there should be executions") 197 | suite.EqualValues(2, rs[1].Counters.IOError, "there should be errors") 198 | } 199 | -------------------------------------------------------------------------------- /pkg/dnsbench/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package dnsbench 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestBenchmark_init(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | benchmark Benchmark 15 | wantServer string 16 | wantRequestLogPath string 17 | wantErr bool 18 | wantRequestDelayStart time.Duration 19 | wantRequestDelayEnd time.Duration 20 | }{ 21 | { 22 | name: "server - IPv4", 23 | benchmark: Benchmark{Server: "8.8.8.8"}, 24 | wantServer: "8.8.8.8:53", 25 | }, 26 | { 27 | name: "server - IPv4 with port", 28 | benchmark: Benchmark{Server: "8.8.8.8:53"}, 29 | wantServer: "8.8.8.8:53", 30 | }, 31 | { 32 | name: "server - IPv6", 33 | benchmark: Benchmark{Server: "fddd:dddd::"}, 34 | wantServer: "[fddd:dddd::]:53", 35 | }, 36 | { 37 | name: "server - IPv6", 38 | benchmark: Benchmark{Server: "fddd:dddd::"}, 39 | wantServer: "[fddd:dddd::]:53", 40 | }, 41 | { 42 | name: "server - IPv6 with port", 43 | benchmark: Benchmark{Server: "fddd:dddd::"}, 44 | wantServer: "[fddd:dddd::]:53", 45 | }, 46 | { 47 | name: "server - DoT with IP address", 48 | benchmark: Benchmark{Server: "8.8.8.8", DOT: true}, 49 | wantServer: "8.8.8.8:853", 50 | }, 51 | { 52 | name: "server - HTTPS url", 53 | benchmark: Benchmark{Server: "https://1.1.1.1"}, 54 | wantServer: "https://1.1.1.1/dns-query", 55 | }, 56 | { 57 | name: "server - HTTP url", 58 | benchmark: Benchmark{Server: "http://127.0.0.1"}, 59 | wantServer: "http://127.0.0.1/dns-query", 60 | }, 61 | { 62 | name: "server - custom HTTP url", 63 | benchmark: Benchmark{Server: "http://127.0.0.1/custom/dns-query"}, 64 | wantServer: "http://127.0.0.1/custom/dns-query", 65 | }, 66 | { 67 | name: "server - QUIC url", 68 | benchmark: Benchmark{Server: "quic://dns.adguard-dns.com"}, 69 | wantServer: "dns.adguard-dns.com:853", 70 | }, 71 | { 72 | name: "server - QUIC url with port", 73 | benchmark: Benchmark{Server: "quic://localhost:853"}, 74 | wantServer: "localhost:853", 75 | }, 76 | { 77 | name: "count and duration specified at once", 78 | benchmark: Benchmark{Server: "8.8.8.8", Count: 10, Duration: time.Minute}, 79 | wantErr: true, 80 | }, 81 | { 82 | name: "invalid EDNS0 buffer size", 83 | benchmark: Benchmark{Server: "8.8.8.8", Edns0: 1}, 84 | wantErr: true, 85 | }, 86 | { 87 | name: "Missing server", 88 | benchmark: Benchmark{}, 89 | wantErr: true, 90 | }, 91 | { 92 | name: "invalid format of ednsopt", 93 | benchmark: Benchmark{Server: "8.8.8.8", EdnsOpt: "test"}, 94 | wantErr: true, 95 | }, 96 | { 97 | name: "invalid format of ednsopt, code is not decimal", 98 | benchmark: Benchmark{Server: "8.8.8.8", EdnsOpt: "test:74657374"}, 99 | wantErr: true, 100 | }, 101 | { 102 | name: "invalid format of ednsopt, data is not hexadecimal string", 103 | benchmark: Benchmark{Server: "8.8.8.8", EdnsOpt: "65518:test"}, 104 | wantErr: true, 105 | }, 106 | { 107 | name: "request log - default path", 108 | benchmark: Benchmark{Server: "8.8.8.8", RequestLogEnabled: true}, 109 | wantServer: "8.8.8.8:53", 110 | wantRequestLogPath: DefaultRequestLogPath, 111 | }, 112 | { 113 | name: "constant delay", 114 | benchmark: Benchmark{Server: "8.8.8.8", RequestDelay: "2s"}, 115 | wantServer: "8.8.8.8:53", 116 | wantRequestDelayStart: 2 * time.Second, 117 | }, 118 | { 119 | name: "random delay", 120 | benchmark: Benchmark{Server: "8.8.8.8", RequestDelay: "2s-3s"}, 121 | wantServer: "8.8.8.8:53", 122 | wantRequestDelayStart: 2 * time.Second, 123 | wantRequestDelayEnd: 3 * time.Second, 124 | }, 125 | { 126 | name: "invalid delay", 127 | benchmark: Benchmark{Server: "8.8.8.8", RequestDelay: "invalid"}, 128 | wantErr: true, 129 | }, 130 | } 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | err := tt.benchmark.init() 134 | 135 | require.Equal(t, tt.wantErr, err != nil) 136 | if !tt.wantErr { 137 | assert.Equal(t, tt.wantServer, tt.benchmark.Server) 138 | assert.Equal(t, tt.wantRequestLogPath, tt.benchmark.RequestLogPath) 139 | assert.Equal(t, tt.wantRequestDelayStart, tt.benchmark.requestDelayStart) 140 | assert.Equal(t, tt.wantRequestDelayEnd, tt.benchmark.requestDelayEnd) 141 | } 142 | }) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /pkg/dnsbench/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package dnsbench contains functionality for executing various plain DNS, DoH and DoQ Benchmarks. 3 | Each DNS benchmark is represented by Benchmark struct that is used to set up benchmark as desired 4 | and then execute the benchmark using Benchmark.Run. Each execution of Benchmark.Run returns slice 5 | of ResultStats, where each element of the slice represents results of a single benchmark worker. 6 | */ 7 | package dnsbench 8 | -------------------------------------------------------------------------------- /pkg/dnsbench/query_factory.go: -------------------------------------------------------------------------------- 1 | package dnsbench 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net" 7 | "net/http" 8 | 9 | "github.com/miekg/dns" 10 | "github.com/quic-go/quic-go/http3" 11 | "github.com/tantalor93/doh-go/doh" 12 | "github.com/tantalor93/doq-go/doq" 13 | "golang.org/x/net/http2" 14 | ) 15 | 16 | func workerQueryFactory(b *Benchmark) func() queryFunc { 17 | switch { 18 | case b.useDoH: 19 | return dohQueryFactory(b) 20 | case b.useQuic: 21 | return doqQueryFactory(b) 22 | default: 23 | return dnsQueryFactory(b) 24 | } 25 | } 26 | 27 | func dnsQueryFactory(b *Benchmark) func() queryFunc { 28 | return func() queryFunc { 29 | dnsClient := getDNSClient(b) 30 | var co *dns.Conn 31 | var i int64 32 | // this allows DoT and plain DNS protocols to support counting queries per connection 33 | // and granular control of the connection 34 | return func(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { 35 | if co != nil && b.QperConn > 0 && i%b.QperConn == 0 { 36 | co.Close() 37 | co = nil 38 | } 39 | i++ 40 | if co == nil { 41 | var err error 42 | co, err = dnsClient.DialContext(ctx, b.Server) 43 | if err != nil { 44 | return nil, err 45 | } 46 | } 47 | r, _, err := dnsClient.ExchangeWithConnContext(ctx, msg, co) 48 | if err != nil { 49 | co.Close() 50 | co = nil 51 | return nil, err 52 | } 53 | return r, nil 54 | } 55 | } 56 | } 57 | 58 | func doqQueryFactory(b *Benchmark) func() queryFunc { 59 | if b.SeparateWorkerConnections { 60 | return func() queryFunc { 61 | quicClient := getDoQClient(b) 62 | return quicClient.Send 63 | } 64 | } 65 | quicClient := getDoQClient(b) 66 | return func() queryFunc { 67 | return quicClient.Send 68 | } 69 | } 70 | 71 | func dohQueryFactory(b *Benchmark) func() queryFunc { 72 | if b.SeparateWorkerConnections { 73 | return func() queryFunc { 74 | return dohQuery(b) 75 | } 76 | } 77 | dohQuery := dohQuery(b) 78 | return func() queryFunc { 79 | return dohQuery 80 | } 81 | } 82 | 83 | func dohQuery(b *Benchmark) queryFunc { 84 | var tr http.RoundTripper 85 | switch b.DohProtocol { 86 | case HTTP3Proto: 87 | // nolint:gosec 88 | tr = &http3.RoundTripper{TLSClientConfig: &tls.Config{InsecureSkipVerify: b.Insecure}} 89 | case HTTP2Proto: 90 | // nolint:gosec 91 | tr = &http2.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: b.Insecure}} 92 | case HTTP1Proto: 93 | fallthrough 94 | default: 95 | // nolint:gosec 96 | tr = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: b.Insecure}} 97 | } 98 | c := http.Client{Transport: tr, Timeout: b.ReadTimeout} 99 | dohClient := doh.NewClient(b.Server, doh.WithHTTPClient(&c)) 100 | 101 | switch b.DohMethod { 102 | case PostHTTPMethod: 103 | return dohClient.SendViaPost 104 | case GetHTTPMethod: 105 | return dohClient.SendViaGet 106 | default: 107 | return dohClient.SendViaPost 108 | } 109 | } 110 | 111 | func getDoQClient(b *Benchmark) *doq.Client { 112 | h, _, _ := net.SplitHostPort(b.Server) 113 | return doq.NewClient(b.Server, 114 | // nolint:gosec 115 | doq.WithTLSConfig(&tls.Config{ServerName: h, InsecureSkipVerify: b.Insecure}), 116 | doq.WithReadTimeout(b.ReadTimeout), 117 | doq.WithWriteTimeout(b.WriteTimeout), 118 | doq.WithConnectTimeout(b.ConnectTimeout), 119 | ) 120 | } 121 | 122 | func getDNSClient(b *Benchmark) *dns.Client { 123 | network := UDPTransport 124 | if b.TCP { 125 | network = TCPTransport 126 | } 127 | if b.DOT { 128 | network = TLSTransport 129 | } 130 | 131 | return &dns.Client{ 132 | Net: network, 133 | DialTimeout: b.ConnectTimeout, 134 | WriteTimeout: b.WriteTimeout, 135 | ReadTimeout: b.ReadTimeout, 136 | Timeout: b.RequestTimeout, 137 | // nolint:gosec 138 | TLSConfig: &tls.Config{InsecureSkipVerify: b.Insecure}, 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /pkg/dnsbench/request_logging.go: -------------------------------------------------------------------------------- 1 | package dnsbench 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | func logRequest(workerID uint32, req dns.Msg, resp *dns.Msg, err error, dur time.Duration) { 12 | rcode := "" 13 | respid := "" 14 | respflags := "" 15 | if resp != nil { 16 | rcode = dns.RcodeToString[resp.Rcode] 17 | respid = fmt.Sprint(resp.Id) 18 | respflags = getFlags(resp) 19 | } 20 | log.Printf("worker:[%v] reqid:[%d] qname:[%s] qtype:[%s] respid:[%s] rcode:[%s] respflags:[%s] err:[%v] duration:[%v]", 21 | workerID, req.Id, req.Question[0].Name, dns.TypeToString[req.Question[0].Qtype], respid, rcode, respflags, err, dur) 22 | } 23 | 24 | func getFlags(resp *dns.Msg) string { 25 | respflags := "" 26 | if resp.Response { 27 | respflags += "qr" 28 | } 29 | if resp.Authoritative { 30 | respflags += " aa" 31 | } 32 | if resp.Truncated { 33 | respflags += " tc" 34 | } 35 | if resp.RecursionDesired { 36 | respflags += " rd" 37 | } 38 | if resp.RecursionAvailable { 39 | respflags += " ra" 40 | } 41 | if resp.Zero { 42 | respflags += " z" 43 | } 44 | if resp.AuthenticatedData { 45 | respflags += " ad" 46 | } 47 | if resp.CheckingDisabled { 48 | respflags += " cd" 49 | } 50 | return respflags 51 | } 52 | -------------------------------------------------------------------------------- /pkg/dnsbench/result.go: -------------------------------------------------------------------------------- 1 | package dnsbench 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/HdrHistogram/hdrhistogram-go" 8 | "github.com/miekg/dns" 9 | "github.com/tantalor93/doh-go/doh" 10 | ) 11 | 12 | // Counters represents various counters of benchmark results. 13 | type Counters struct { 14 | // Total is counter of all requests. 15 | Total int64 16 | // IOError is counter of all requests for which there was no answer. 17 | IOError int64 18 | // Success is counter of all responses which were successful (NOERROR, but not NODATA!). 19 | Success int64 20 | // Negative is counter of all responses which were negative (NODATA/NXDOMAIN). 21 | Negative int64 22 | // Error is counter of all responses which were not negative (NODATA/NXDOMAIN) or success (NOERROR response). 23 | Error int64 24 | // IDmismatch is counter of all responses which ID mismatched the request ID. 25 | IDmismatch int64 26 | // Truncated is counter of all responses which had truncated flag. 27 | Truncated int64 28 | } 29 | 30 | // Datapoint one datapoint of benchmark (single DNS request). 31 | type Datapoint struct { 32 | Duration time.Duration 33 | Start time.Time 34 | } 35 | 36 | // ErrorDatapoint one datapoint representing single IO error of benchmark. 37 | // Datapoint one datapoint of benchmark (single DNS request). 38 | type ErrorDatapoint struct { 39 | Start time.Time 40 | Err error 41 | } 42 | 43 | // ResultStats is a representation of benchmark results of single concurrent thread. 44 | type ResultStats struct { 45 | Codes map[int]int64 46 | Qtypes map[string]int64 47 | Hist *hdrhistogram.Histogram 48 | Timings []Datapoint 49 | Counters *Counters 50 | Errors []ErrorDatapoint 51 | AuthenticatedDomains map[string]struct{} 52 | DoHStatusCodes map[int]int64 53 | } 54 | 55 | func newResultStats(b *Benchmark) *ResultStats { 56 | st := &ResultStats{Hist: hdrhistogram.New(b.HistMin.Nanoseconds(), b.HistMax.Nanoseconds(), b.HistPre)} 57 | if b.Rcodes { 58 | st.Codes = make(map[int]int64) 59 | } 60 | st.Qtypes = make(map[string]int64) 61 | if b.useDoH { 62 | st.DoHStatusCodes = make(map[int]int64) 63 | } 64 | st.Counters = &Counters{} 65 | return st 66 | } 67 | 68 | func (rs *ResultStats) record(req *dns.Msg, resp *dns.Msg, err error, time time.Time, duration time.Duration) { 69 | rs.Counters.Total++ 70 | 71 | if rs.DoHStatusCodes != nil { 72 | statusError := doh.UnexpectedServerHTTPStatusError{} 73 | if err != nil && errors.As(err, &statusError) { 74 | rs.DoHStatusCodes[statusError.HTTPStatus()]++ 75 | } 76 | if err == nil { 77 | rs.DoHStatusCodes[200]++ 78 | } 79 | } 80 | 81 | if rs.Qtypes != nil { 82 | rs.Qtypes[dns.TypeToString[req.Question[0].Qtype]]++ 83 | } 84 | 85 | if err != nil { 86 | rs.Counters.IOError++ 87 | rs.Errors = append(rs.Errors, ErrorDatapoint{Start: time, Err: err}) 88 | return 89 | } 90 | 91 | if resp.Truncated { 92 | rs.Counters.Truncated++ 93 | } 94 | 95 | if resp.Rcode == dns.RcodeSuccess { 96 | if resp.Id != req.Id { 97 | rs.Counters.IDmismatch++ 98 | return 99 | } 100 | if len(resp.Answer) == 0 { 101 | // NODATA negative response 102 | rs.Counters.Negative++ 103 | } else { 104 | rs.Counters.Success++ 105 | } 106 | } 107 | if resp.Rcode == dns.RcodeNameError { 108 | rs.Counters.Negative++ 109 | } 110 | if resp.Rcode != dns.RcodeSuccess && resp.Rcode != dns.RcodeNameError { 111 | // assume every rcode not NOERROR or NXDOMAIN is error 112 | rs.Counters.Error++ 113 | } 114 | 115 | if rs.Codes != nil { 116 | var c int64 117 | if v, ok := rs.Codes[resp.Rcode]; ok { 118 | c = v 119 | } 120 | c++ 121 | rs.Codes[resp.Rcode] = c 122 | } 123 | if resp.AuthenticatedData { 124 | if rs.AuthenticatedDomains == nil { 125 | rs.AuthenticatedDomains = make(map[string]struct{}) 126 | } 127 | rs.AuthenticatedDomains[req.Question[0].Name] = struct{}{} 128 | } 129 | 130 | rs.Hist.RecordValue(duration.Nanoseconds()) 131 | rs.Timings = append(rs.Timings, Datapoint{Duration: duration, Start: time}) 132 | } 133 | -------------------------------------------------------------------------------- /pkg/dnsbench/result_test.go: -------------------------------------------------------------------------------- 1 | package dnsbench 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/miekg/dns" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | var now = time.Now() 14 | 15 | func TestResultStats_record(t *testing.T) { 16 | type args struct { 17 | req *dns.Msg 18 | resp *dns.Msg 19 | err error 20 | time time.Time 21 | duration time.Duration 22 | dohBenchmark bool 23 | } 24 | tests := []struct { 25 | name string 26 | args args 27 | want *ResultStats 28 | }{ 29 | { 30 | name: "record success", 31 | args: args{ 32 | req: &dns.Msg{ 33 | MsgHdr: dns.MsgHdr{Id: 1}, 34 | Question: []dns.Question{ 35 | { 36 | Name: "example.org.", 37 | Qclass: dns.ClassINET, 38 | Qtype: dns.TypeA, 39 | }, 40 | }, 41 | }, 42 | resp: &dns.Msg{ 43 | MsgHdr: dns.MsgHdr{Id: 1, Rcode: dns.RcodeSuccess, Response: true}, 44 | Answer: []dns.RR{&dns.A{A: net.ParseIP("127.0.0.1")}}, 45 | }, 46 | time: time.Now(), 47 | duration: time.Millisecond, 48 | }, 49 | want: &ResultStats{ 50 | Codes: map[int]int64{ 51 | dns.RcodeSuccess: 1, 52 | }, 53 | Qtypes: map[string]int64{ 54 | "A": 1, 55 | }, 56 | Timings: []Datapoint{ 57 | { 58 | Duration: time.Millisecond, 59 | Start: now, 60 | }, 61 | }, 62 | Counters: &Counters{ 63 | Total: 1, 64 | Success: 1, 65 | }, 66 | }, 67 | }, 68 | { 69 | name: "record nxdomain", 70 | args: args{ 71 | req: &dns.Msg{ 72 | MsgHdr: dns.MsgHdr{Id: 1}, 73 | Question: []dns.Question{ 74 | { 75 | Name: "example.org.", 76 | Qclass: dns.ClassINET, 77 | Qtype: dns.TypeA, 78 | }, 79 | }, 80 | }, 81 | resp: &dns.Msg{ 82 | MsgHdr: dns.MsgHdr{Id: 1, Rcode: dns.RcodeNameError, Response: true}, 83 | }, 84 | time: time.Now(), 85 | duration: time.Millisecond, 86 | }, 87 | want: &ResultStats{ 88 | Codes: map[int]int64{ 89 | dns.RcodeNameError: 1, 90 | }, 91 | Qtypes: map[string]int64{ 92 | "A": 1, 93 | }, 94 | Timings: []Datapoint{ 95 | { 96 | Duration: time.Millisecond, 97 | Start: now, 98 | }, 99 | }, 100 | Counters: &Counters{ 101 | Total: 1, 102 | Negative: 1, 103 | }, 104 | }, 105 | }, 106 | { 107 | name: "record nodata", 108 | args: args{ 109 | req: &dns.Msg{ 110 | MsgHdr: dns.MsgHdr{Id: 1}, 111 | Question: []dns.Question{ 112 | { 113 | Name: "example.org.", 114 | Qclass: dns.ClassINET, 115 | Qtype: dns.TypeA, 116 | }, 117 | }, 118 | }, 119 | resp: &dns.Msg{ 120 | MsgHdr: dns.MsgHdr{Id: 1, Rcode: dns.RcodeSuccess, Response: true}, 121 | }, 122 | time: time.Now(), 123 | duration: time.Millisecond, 124 | }, 125 | want: &ResultStats{ 126 | Codes: map[int]int64{ 127 | dns.RcodeSuccess: 1, 128 | }, 129 | Qtypes: map[string]int64{ 130 | "A": 1, 131 | }, 132 | Timings: []Datapoint{ 133 | { 134 | Duration: time.Millisecond, 135 | Start: now, 136 | }, 137 | }, 138 | Counters: &Counters{ 139 | Total: 1, 140 | Negative: 1, 141 | }, 142 | }, 143 | }, 144 | { 145 | name: "record dns error", 146 | args: args{ 147 | req: &dns.Msg{ 148 | MsgHdr: dns.MsgHdr{Id: 1}, 149 | Question: []dns.Question{ 150 | { 151 | Name: "example.org.", 152 | Qclass: dns.ClassINET, 153 | Qtype: dns.TypeA, 154 | }, 155 | }, 156 | }, 157 | resp: &dns.Msg{ 158 | MsgHdr: dns.MsgHdr{Id: 1, Rcode: dns.RcodeServerFailure, Response: true}, 159 | }, 160 | time: time.Now(), 161 | duration: time.Millisecond, 162 | }, 163 | want: &ResultStats{ 164 | Codes: map[int]int64{ 165 | dns.RcodeServerFailure: 1, 166 | }, 167 | Qtypes: map[string]int64{ 168 | "A": 1, 169 | }, 170 | Timings: []Datapoint{ 171 | { 172 | Duration: time.Millisecond, 173 | Start: now, 174 | }, 175 | }, 176 | Counters: &Counters{ 177 | Total: 1, 178 | Error: 1, 179 | }, 180 | }, 181 | }, 182 | { 183 | name: "record IO error", 184 | args: args{ 185 | req: &dns.Msg{ 186 | MsgHdr: dns.MsgHdr{Id: 1}, 187 | Question: []dns.Question{ 188 | { 189 | Name: "example.org.", 190 | Qclass: dns.ClassINET, 191 | Qtype: dns.TypeA, 192 | }, 193 | }, 194 | }, 195 | err: errors.New("test error"), 196 | time: time.Now(), 197 | duration: time.Millisecond, 198 | }, 199 | want: &ResultStats{ 200 | Codes: map[int]int64{}, 201 | Qtypes: map[string]int64{ 202 | "A": 1, 203 | }, 204 | Errors: []ErrorDatapoint{ 205 | { 206 | Err: errors.New("test error"), 207 | Start: now, 208 | }, 209 | }, 210 | Counters: &Counters{ 211 | Total: 1, 212 | IOError: 1, 213 | }, 214 | }, 215 | }, 216 | { 217 | name: "record truncated response", 218 | args: args{ 219 | req: &dns.Msg{ 220 | MsgHdr: dns.MsgHdr{Id: 1}, 221 | Question: []dns.Question{ 222 | { 223 | Name: "example.org.", 224 | Qclass: dns.ClassINET, 225 | Qtype: dns.TypeA, 226 | }, 227 | }, 228 | }, 229 | resp: &dns.Msg{ 230 | MsgHdr: dns.MsgHdr{Id: 1, Rcode: dns.RcodeSuccess, Response: true, Truncated: true}, 231 | Answer: []dns.RR{&dns.A{A: net.ParseIP("127.0.0.1")}}, 232 | }, 233 | time: time.Now(), 234 | duration: time.Millisecond, 235 | }, 236 | want: &ResultStats{ 237 | Codes: map[int]int64{ 238 | dns.RcodeSuccess: 1, 239 | }, 240 | Qtypes: map[string]int64{ 241 | "A": 1, 242 | }, 243 | Timings: []Datapoint{ 244 | { 245 | Duration: time.Millisecond, 246 | Start: now, 247 | }, 248 | }, 249 | Counters: &Counters{ 250 | Total: 1, 251 | Truncated: 1, 252 | Success: 1, 253 | }, 254 | }, 255 | }, 256 | { 257 | name: "record response ID mismatch", 258 | args: args{ 259 | req: &dns.Msg{ 260 | MsgHdr: dns.MsgHdr{Id: 1}, 261 | Question: []dns.Question{ 262 | { 263 | Name: "example.org.", 264 | Qclass: dns.ClassINET, 265 | Qtype: dns.TypeA, 266 | }, 267 | }, 268 | }, 269 | resp: &dns.Msg{ 270 | MsgHdr: dns.MsgHdr{Id: 2, Rcode: dns.RcodeSuccess, Response: true}, 271 | Answer: []dns.RR{&dns.A{A: net.ParseIP("127.0.0.1")}}, 272 | }, 273 | time: time.Now(), 274 | duration: time.Millisecond, 275 | }, 276 | want: &ResultStats{ 277 | Codes: map[int]int64{}, 278 | Qtypes: map[string]int64{ 279 | "A": 1, 280 | }, 281 | Counters: &Counters{ 282 | Total: 1, 283 | IDmismatch: 1, 284 | }, 285 | }, 286 | }, 287 | { 288 | name: "record DoH success", 289 | args: args{ 290 | req: &dns.Msg{ 291 | MsgHdr: dns.MsgHdr{Id: 1}, 292 | Question: []dns.Question{ 293 | { 294 | Name: "example.org.", 295 | Qclass: dns.ClassINET, 296 | Qtype: dns.TypeA, 297 | }, 298 | }, 299 | }, 300 | resp: &dns.Msg{ 301 | MsgHdr: dns.MsgHdr{Id: 1, Rcode: dns.RcodeSuccess, Response: true}, 302 | Answer: []dns.RR{&dns.A{A: net.ParseIP("127.0.0.1")}}, 303 | }, 304 | time: time.Now(), 305 | duration: time.Millisecond, 306 | dohBenchmark: true, 307 | }, 308 | want: &ResultStats{ 309 | Codes: map[int]int64{ 310 | dns.RcodeSuccess: 1, 311 | }, 312 | Qtypes: map[string]int64{ 313 | "A": 1, 314 | }, 315 | Timings: []Datapoint{ 316 | { 317 | Duration: time.Millisecond, 318 | Start: now, 319 | }, 320 | }, 321 | Counters: &Counters{ 322 | Total: 1, 323 | Success: 1, 324 | }, 325 | DoHStatusCodes: map[int]int64{ 326 | 200: 1, 327 | }, 328 | }, 329 | }, 330 | { 331 | name: "record authenticated domain", 332 | args: args{ 333 | req: &dns.Msg{ 334 | MsgHdr: dns.MsgHdr{Id: 1}, 335 | Question: []dns.Question{ 336 | { 337 | Name: "example.org.", 338 | Qclass: dns.ClassINET, 339 | Qtype: dns.TypeA, 340 | }, 341 | }, 342 | }, 343 | resp: &dns.Msg{ 344 | MsgHdr: dns.MsgHdr{Id: 1, Rcode: dns.RcodeSuccess, Response: true, AuthenticatedData: true}, 345 | Answer: []dns.RR{&dns.A{A: net.ParseIP("127.0.0.1")}}, 346 | }, 347 | time: time.Now(), 348 | duration: time.Millisecond, 349 | }, 350 | want: &ResultStats{ 351 | Codes: map[int]int64{ 352 | dns.RcodeSuccess: 1, 353 | }, 354 | Qtypes: map[string]int64{ 355 | "A": 1, 356 | }, 357 | Timings: []Datapoint{ 358 | { 359 | Duration: time.Millisecond, 360 | Start: now, 361 | }, 362 | }, 363 | Counters: &Counters{ 364 | Total: 1, 365 | Success: 1, 366 | }, 367 | AuthenticatedDomains: map[string]struct{}{ 368 | "example.org.": {}, 369 | }, 370 | }, 371 | }, 372 | } 373 | for _, tt := range tests { 374 | t.Run(tt.name, func(t *testing.T) { 375 | b := Benchmark{ 376 | Rcodes: true, 377 | useDoH: tt.args.dohBenchmark, 378 | } 379 | rs := newResultStats(&b) 380 | 381 | rs.record(tt.args.req, tt.args.resp, tt.args.err, now, tt.args.duration) 382 | 383 | // null the Histogram for simple assertion excluding the histogram 384 | rs.Hist = nil 385 | 386 | assert.Equal(t, tt.want, rs) 387 | }) 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /pkg/dnsbench/server_test.go: -------------------------------------------------------------------------------- 1 | package dnsbench_test 2 | 3 | import ( 4 | "crypto/tls" 5 | 6 | "github.com/miekg/dns" 7 | "github.com/tantalor93/dnspyre/v3/pkg/dnsbench" 8 | ) 9 | 10 | // Server represents simple DNS server. 11 | type Server struct { 12 | Addr string 13 | inner *dns.Server 14 | } 15 | 16 | // Close shuts down running DNS server instance. 17 | func (s *Server) Close() { 18 | s.inner.Shutdown() 19 | } 20 | 21 | // NewServer creates and starts new DNS server instance. 22 | func NewServer(network string, tlsConfig *tls.Config, f dns.HandlerFunc) *Server { 23 | ch := make(chan bool) 24 | s := &dns.Server{Net: network, Addr: "127.0.0.1:0", TLSConfig: tlsConfig, NotifyStartedFunc: func() { close(ch) }, Handler: f} 25 | 26 | go func() { 27 | if err := s.ListenAndServe(); err != nil { 28 | panic(err) 29 | } 30 | }() 31 | 32 | <-ch 33 | server := Server{inner: s} 34 | if network == dnsbench.UDPTransport { 35 | server.Addr = s.PacketConn.LocalAddr().String() 36 | } else { 37 | server.Addr = s.Listener.Addr().String() 38 | } 39 | return &server 40 | } 41 | -------------------------------------------------------------------------------- /pkg/dnsbench/testdata/test.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDRTCCAi2gAwIBAgIJAPM9R5AP6u6uMA0GCSqGSIb3DQEBCwUAMFExCzAJBgNV 3 | BAYTAkNaMQ0wCwYDVQQHDARCcm5vMRIwEAYDVQQDDAlsb2NhbGhvc3QxHzAdBgkq 4 | hkiG9w0BCQEWEG9iZW5reUBnbWFpbC5jb20wIBcNMjMwNDA1MTIxNzQ5WhgPMjA1 5 | MDA4MjExMjE3NDlaMFExCzAJBgNVBAYTAkNaMQ0wCwYDVQQHDARCcm5vMRIwEAYD 6 | VQQDDAlsb2NhbGhvc3QxHzAdBgkqhkiG9w0BCQEWEG9iZW5reUBnbWFpbC5jb20w 7 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8i7skiBpumUBRZV5wFrKZ 8 | KS8nMw6EmRwSjvPiNolq3dYDW+MVjWiMUan5On5u/SJrUA6CMaHlilBPH0K3riab 9 | wlfcF+zTDVV9mldwo5yDRnEn8ynX3A1L9ldqwKm/VIm/2DnRgR00BfTQ3PW7eeVw 10 | HGMHleZfW1PczXA2+RJ1sT6CrYv1kK2D6AeakTrp8+VGm8ViuawjWrdFTCtS83TO 11 | h7e51boVuBOAVzG2wJUr/z3jKNwqhr4dr1hxYR6Sif/GVwtxTjXzK3KxckEK58R8 12 | UPbxMOYrzmjQXXtgKsAtZtXR1+kZe/UaxCkxeQTgGqxVXxr2+t4VjiLxyDdSGy9Z 13 | AgMBAAGjHjAcMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0B 14 | AQsFAAOCAQEAT7CF0he93zRgAUad36sWaH8QF+Dai+yifFEBXuZtw0sfjFL1m5lw 15 | hvJLCTlVya3zmg2vnVZEnT6mUWIq5c5ZSrVjSWicrzdbGPyKpP/tx+TX6aFLRkO5 16 | IxsH9tFNzGguSZmAjB3w3ir0mH+FoHQqKtpUpKtZAF8FFpvaDSCCiqPNvt1hxlhh 17 | aNyMDSXm8UrK4B+duG8H9+u240oi7NYRsUjfvae0NkJRZpzmTOGi3yJ34HZq63Vo 18 | YdYcW8imkc3Aqe1wmE9xq6lDS2jHcTPcZllvRbaPSg6bT7mpA+fHpvYRxdKcxm/6 19 | Ob5vNqqB/IZecZsV7VEeigpjcQ7k9ShabQ== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /pkg/dnsbench/testdata/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpgIBAAKCAQEAvIu7JIgabplAUWVecBaymSkvJzMOhJkcEo7z4jaJat3WA1vj 3 | FY1ojFGp+Tp+bv0ia1AOgjGh5YpQTx9Ct64mm8JX3Bfs0w1VfZpXcKOcg0ZxJ/Mp 4 | 19wNS/ZXasCpv1SJv9g50YEdNAX00Nz1u3nlcBxjB5XmX1tT3M1wNvkSdbE+gq2L 5 | 9ZCtg+gHmpE66fPlRpvFYrmsI1q3RUwrUvN0zoe3udW6FbgTgFcxtsCVK/894yjc 6 | Koa+Ha9YcWEekon/xlcLcU418ytysXJBCufEfFD28TDmK85o0F17YCrALWbV0dfp 7 | GXv1GsQpMXkE4BqsVV8a9vreFY4i8cg3UhsvWQIDAQABAoIBAQCXy7uIZtc48cL5 8 | hS4p+ewiKSkgWxe2I3qZamPpXNT3p8/0dlb19BoW1myNDc3a14uNcC+uG/1myxtr 9 | CBTzwo6s2iNYPB7bsCGC9O6u7dpFSkIx0rB+bFh8LsEkXiaLtqkMPi4WgOedCaqX 10 | OT3RiQryXrhP1Bxb6zAyVWehqpd1DjGvFSsuh6upySFdRkhgxd0IPqVyKrUN517h 11 | 6YrQvAVfiiM/9nGeyKs65cMUKxtSUvJ54eWrq/W/1mfXfpsQIGU0j5BHtlhw7hpq 12 | 57YmPydci0MiEwL/4N/cm5g8OUtpf++BkU4t8k2agfL5BYl/xmiEWXjJfcZfNa4q 13 | vhrl/wDxAoGBAPDbZNyY0ebIVPlmdlstyP2H2RTJSDB2tVvrTo02F2V7EpE9U0DU 14 | 9GDLTlxykvKBSjaVWTODMm8kHyGOgYJykZfkurjSE1zMmXxbCZHZNyOYa0P1jig5 15 | +il1hVsYtYnxv5eVBXUE75Fq5kXa2/0DCHXcnN1s6fcxTOdkmtHiLRR9AoGBAMhm 16 | YsW2JtB33gzi/2s4upcE23SSoWiw3DLgL9m7OdYpyYLWLwXzofQBhQgfxg9SnDPQ 17 | 40vqsxVKsiAzBQPjessx2iulPQLtzOuxRQDZ/ZxCFXjts00UpFiYzPP1Qm8Mse/H 18 | uQaj0nycrgHfEzXc3ikMN6BQbYFy0AlMZ0G3l8kNAoGBAOw0TIrmN9tbFbJsYJ/0 19 | m8q/Qg3Xg7s7f4pAjo1/wZwdAU18Vbwb2/ldCGEzX5cBYlV7S7pv7LMAOqN/DNVw 20 | JAZRIykDpEd6wv8ojI8C9ccrv+4qz5n1mba08O4wokBA28L9OxfSmlsC+gcBpoJP 21 | SinEC/Y+zIhGSgQXLpmsdMdFAoGBAJFIaOMdIxaPUBPXnYSGI//ILbFdfFcBoSHh 22 | Fc+rxEpXERghXwXZktfrIh65qkUBhuypy58GD76n4GsnkcM375XQFnL94DV3YNCe 23 | O4BaiVn3Lsn0ycBf7qWsYSmb7QXFFlrXWHRP4BEnJZ+Fsc+iyAzKJqm6pSjrq5aS 24 | JWmnSAshAoGBAOpYeK8cLqrdr4PvkRCfxflJrc6c/qCVBncUVY9ciYYd9vpHfTZV 25 | x8nPrJIvNDWIwsMiSs2by9egJRLhESnZyF57104IBgC/Oq+r58/xa9HgJza41cAh 26 | 1nK4PGhI2iNAF/t0p3GI1j+RU8fj6AF4UfPUEOj0iVAIySswfFFUJlII 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /pkg/printutils/printutils.go: -------------------------------------------------------------------------------- 1 | package printutils 2 | 3 | import ( 4 | "github.com/fatih/color" 5 | ) 6 | 7 | var ( 8 | // ErrFprintf is a wrapper for printing colored errors. 9 | ErrFprintf = color.New(color.FgRed).FprintfFunc() 10 | // SuccessFprintf is a wrapper for printing colored successes. 11 | SuccessFprintf = color.New(color.FgGreen).FprintfFunc() 12 | // NeutralFprintf is a wrapper for printing neutral information. 13 | NeutralFprintf = color.New().FprintfFunc() 14 | 15 | highlightColor = color.New(color.FgYellow) 16 | // HighlightSprintf is a wrapper for highlighting formatted strings with color. 17 | HighlightSprintf = highlightColor.SprintfFunc() 18 | // HighlightSprint is a wrapper for highlighting strings with color. 19 | HighlightSprint = highlightColor.SprintFunc() 20 | ) 21 | -------------------------------------------------------------------------------- /pkg/reporter/doc.go: -------------------------------------------------------------------------------- 1 | // Package reporter contains functionality for creating reports and printing results of executed dnsbench.Benchmark. 2 | package reporter 3 | -------------------------------------------------------------------------------- /pkg/reporter/jsonreporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "encoding/json" 5 | "math" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | type jsonReporter struct{} 12 | 13 | type latencyStats struct { 14 | MinMs int64 `json:"minMs"` 15 | MeanMs int64 `json:"meanMs"` 16 | StdMs int64 `json:"stdMs"` 17 | MaxMs int64 `json:"maxMs"` 18 | P99Ms int64 `json:"p99Ms"` 19 | P95Ms int64 `json:"p95Ms"` 20 | P90Ms int64 `json:"p90Ms"` 21 | P75Ms int64 `json:"p75Ms"` 22 | P50Ms int64 `json:"p50Ms"` 23 | } 24 | 25 | type histogramPoint struct { 26 | LatencyMs int64 `json:"latencyMs"` 27 | Count int64 `json:"count"` 28 | } 29 | 30 | type jsonResult struct { 31 | TotalRequests int64 `json:"totalRequests"` 32 | TotalSuccessResponses int64 `json:"totalSuccessResponses"` 33 | TotalNegativeResponses int64 `json:"totalNegativeResponses"` 34 | TotalErrorResponses int64 `json:"totalErrorResponses"` 35 | TotalIOErrors int64 `json:"totalIOErrors"` 36 | TotalIDmismatch int64 `json:"totalIDmismatch"` 37 | TotalTruncatedResponses int64 `json:"totalTruncatedResponses"` 38 | ResponseRcodes map[string]int64 `json:"responseRcodes,omitempty"` 39 | QuestionTypes map[string]int64 `json:"questionTypes"` 40 | QueriesPerSecond float64 `json:"queriesPerSecond"` 41 | BenchmarkDurationSeconds float64 `json:"benchmarkDurationSeconds"` 42 | LatencyStats latencyStats `json:"latencyStats"` 43 | LatencyDistribution []histogramPoint `json:"latencyDistribution,omitempty"` 44 | TotalDNSSECSecuredDomains *int `json:"totalDNSSECSecuredDomains,omitempty"` 45 | DohHTTPResponseStatusCodes map[int]int64 `json:"dohHTTPResponseStatusCodes,omitempty"` 46 | } 47 | 48 | func (s *jsonReporter) print(params reportParameters) error { 49 | codeTotalsMapped := make(map[string]int64) 50 | if params.benchmark.Rcodes { 51 | for k, v := range params.codeTotals { 52 | codeTotalsMapped[dns.RcodeToString[k]] = v 53 | } 54 | } 55 | 56 | var res []histogramPoint 57 | 58 | if params.benchmark.HistDisplay { 59 | dist := params.hist.Distribution() 60 | for _, d := range dist { 61 | res = append(res, histogramPoint{ 62 | LatencyMs: time.Duration(d.To/2 + d.From/2).Milliseconds(), 63 | Count: d.Count, 64 | }) 65 | } 66 | 67 | var dedupRes []histogramPoint 68 | i := -1 69 | for _, r := range res { 70 | if i >= 0 && i < len(res) { 71 | if dedupRes[i].LatencyMs == r.LatencyMs { 72 | dedupRes[i].Count += r.Count 73 | } else { 74 | dedupRes = append(dedupRes, r) 75 | i++ 76 | } 77 | } else { 78 | dedupRes = append(dedupRes, r) 79 | i++ 80 | } 81 | } 82 | } 83 | 84 | result := jsonResult{ 85 | TotalRequests: params.totalCounters.Total, 86 | TotalSuccessResponses: params.totalCounters.Success, 87 | TotalNegativeResponses: params.totalCounters.Negative, 88 | TotalErrorResponses: params.totalCounters.Error, 89 | TotalIOErrors: params.totalCounters.IOError, 90 | TotalIDmismatch: params.totalCounters.IDmismatch, 91 | TotalTruncatedResponses: params.totalCounters.Truncated, 92 | QueriesPerSecond: math.Round(float64(params.totalCounters.Total)/params.benchmarkDuration.Seconds()*100) / 100, 93 | BenchmarkDurationSeconds: roundDuration(params.benchmarkDuration).Seconds(), 94 | ResponseRcodes: codeTotalsMapped, 95 | QuestionTypes: params.qtypeTotals, 96 | LatencyStats: latencyStats{ 97 | MinMs: time.Duration(params.hist.Min()).Milliseconds(), 98 | MeanMs: time.Duration(params.hist.Mean()).Milliseconds(), 99 | StdMs: time.Duration(params.hist.StdDev()).Milliseconds(), 100 | MaxMs: time.Duration(params.hist.Max()).Milliseconds(), 101 | P99Ms: time.Duration(params.hist.ValueAtQuantile(99)).Milliseconds(), 102 | P95Ms: time.Duration(params.hist.ValueAtQuantile(95)).Milliseconds(), 103 | P90Ms: time.Duration(params.hist.ValueAtQuantile(90)).Milliseconds(), 104 | P75Ms: time.Duration(params.hist.ValueAtQuantile(75)).Milliseconds(), 105 | P50Ms: time.Duration(params.hist.ValueAtQuantile(50)).Milliseconds(), 106 | }, 107 | LatencyDistribution: res, 108 | DohHTTPResponseStatusCodes: params.dohResponseStatusesTotals, 109 | } 110 | if params.benchmark.DNSSEC { 111 | totalDNSSECSecuredDomains := len(params.authenticatedDomains) 112 | result.TotalDNSSECSecuredDomains = &totalDNSSECSecuredDomains 113 | } 114 | 115 | return json.NewEncoder(params.outputWriter).Encode(result) 116 | } 117 | -------------------------------------------------------------------------------- /pkg/reporter/merge.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "sort" 7 | 8 | "github.com/HdrHistogram/hdrhistogram-go" 9 | "github.com/tantalor93/dnspyre/v3/pkg/dnsbench" 10 | ) 11 | 12 | // BenchmarkResultStats represents merged results of the dnsbench.Benchmark execution. 13 | type BenchmarkResultStats struct { 14 | Codes map[int]int64 15 | Qtypes map[string]int64 16 | Hist *hdrhistogram.Histogram 17 | Timings []dnsbench.Datapoint 18 | Counters dnsbench.Counters 19 | Errors []dnsbench.ErrorDatapoint 20 | GroupedErrors map[string]int 21 | AuthenticatedDomains map[string]struct{} 22 | DoHStatusCodes map[int]int64 23 | } 24 | 25 | // Merge takes results of the executed dnsbench.Benchmark and merges them. 26 | func Merge(b *dnsbench.Benchmark, stats []*dnsbench.ResultStats) BenchmarkResultStats { 27 | totals := BenchmarkResultStats{ 28 | Codes: make(map[int]int64), 29 | Qtypes: make(map[string]int64), 30 | Hist: hdrhistogram.New(b.HistMin.Nanoseconds(), b.HistMax.Nanoseconds(), b.HistPre), 31 | GroupedErrors: make(map[string]int), 32 | AuthenticatedDomains: make(map[string]struct{}), 33 | DoHStatusCodes: make(map[int]int64), 34 | } 35 | 36 | for _, s := range stats { 37 | for _, err := range s.Errors { 38 | errorString := errString(err) 39 | 40 | if v, ok := totals.GroupedErrors[errorString]; ok { 41 | totals.GroupedErrors[errorString] = v + 1 42 | } else { 43 | totals.GroupedErrors[errorString] = 1 44 | } 45 | } 46 | totals.Errors = append(totals.Errors, s.Errors...) 47 | 48 | totals.Hist.Merge(s.Hist) 49 | totals.Timings = append(totals.Timings, s.Timings...) 50 | if s.Codes != nil { 51 | for k, v := range s.Codes { 52 | totals.Codes[k] += v 53 | } 54 | } 55 | if s.Qtypes != nil { 56 | for k, v := range s.Qtypes { 57 | totals.Qtypes[k] += v 58 | } 59 | } 60 | if s.DoHStatusCodes != nil { 61 | for k, v := range s.DoHStatusCodes { 62 | totals.DoHStatusCodes[k] += v 63 | } 64 | } 65 | if s.Counters != nil { 66 | totals.Counters = dnsbench.Counters{ 67 | Total: totals.Counters.Total + s.Counters.Total, 68 | IOError: totals.Counters.IOError + s.Counters.IOError, 69 | Success: totals.Counters.Success + s.Counters.Success, 70 | Negative: totals.Counters.Negative + s.Counters.Negative, 71 | Error: totals.Counters.Error + s.Counters.Error, 72 | IDmismatch: totals.Counters.IDmismatch + s.Counters.IDmismatch, 73 | Truncated: totals.Counters.Truncated + s.Counters.Truncated, 74 | } 75 | } 76 | if b.DNSSEC { 77 | for k := range s.AuthenticatedDomains { 78 | totals.AuthenticatedDomains[k] = struct{}{} 79 | } 80 | } 81 | } 82 | 83 | // sort data points from the oldest to the earliest, so we can better plot time dependant graphs (like line) 84 | sort.SliceStable(totals.Timings, func(i, j int) bool { 85 | return totals.Timings[i].Start.Before(totals.Timings[j].Start) 86 | }) 87 | 88 | // sort error data points from the oldest to the earliest, so we can better plot time dependant graphs (like line) 89 | sort.SliceStable(totals.Errors, func(i, j int) bool { 90 | return totals.Errors[i].Start.Before(totals.Errors[j].Start) 91 | }) 92 | return totals 93 | } 94 | 95 | func errString(err dnsbench.ErrorDatapoint) string { 96 | var errorString string 97 | var netOpErr *net.OpError 98 | var resolveErr *net.DNSError 99 | 100 | switch { 101 | case errors.As(err.Err, &resolveErr): 102 | errorString = resolveErr.Err + " " + resolveErr.Name 103 | case errors.As(err.Err, &netOpErr): 104 | errorString = netOpErr.Op + " " + netOpErr.Net 105 | if netOpErr.Addr != nil { 106 | errorString += " " + netOpErr.Addr.String() 107 | } 108 | default: 109 | errorString = err.Err.Error() 110 | } 111 | return errorString 112 | } 113 | -------------------------------------------------------------------------------- /pkg/reporter/merge_test.go: -------------------------------------------------------------------------------- 1 | package reporter_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | "github.com/HdrHistogram/hdrhistogram-go" 9 | "github.com/miekg/dns" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/tantalor93/dnspyre/v3/pkg/dnsbench" 12 | "github.com/tantalor93/dnspyre/v3/pkg/reporter" 13 | ) 14 | 15 | func TestMerge(t *testing.T) { 16 | start := time.Now() 17 | stats := []*dnsbench.ResultStats{ 18 | { 19 | Codes: map[int]int64{ 20 | dns.RcodeSuccess: 2, 21 | dns.RcodeNameError: 1, 22 | dns.RcodeServerFailure: 1, 23 | }, 24 | Qtypes: map[string]int64{ 25 | "A": 2, 26 | "AAAA": 2, 27 | }, 28 | Hist: histogramWithValues(time.Second, 2*time.Second, time.Second), 29 | Timings: []dnsbench.Datapoint{ 30 | { 31 | Start: start, 32 | Duration: time.Second, 33 | }, 34 | { 35 | Start: start.Add(time.Second), 36 | Duration: 2 * time.Second, 37 | }, 38 | { 39 | Start: start.Add(2 * time.Second), 40 | Duration: time.Second, 41 | }, 42 | }, 43 | Counters: &dnsbench.Counters{ 44 | Success: 2, 45 | Negative: 1, 46 | Truncated: 1, 47 | IOError: 2, 48 | Error: 1, 49 | IDmismatch: 1, 50 | Total: 8, 51 | }, 52 | Errors: []dnsbench.ErrorDatapoint{ 53 | { 54 | Start: start.Add(3 * time.Second), 55 | Err: errors.New("test"), 56 | }, 57 | { 58 | Start: start.Add(4 * time.Second), 59 | Err: errors.New("test"), 60 | }, 61 | }, 62 | AuthenticatedDomains: map[string]struct{}{ 63 | "google.com.": {}, 64 | }, 65 | DoHStatusCodes: map[int]int64{ 66 | 200: 5, 67 | 503: 1, 68 | }, 69 | }, 70 | { 71 | Codes: map[int]int64{ 72 | dns.RcodeSuccess: 1, 73 | dns.RcodeNameError: 1, 74 | dns.RcodeServerFailure: 1, 75 | }, 76 | Qtypes: map[string]int64{ 77 | "A": 1, 78 | "AAAA": 2, 79 | }, 80 | Hist: histogramWithValues(time.Second, 2*time.Second), 81 | Timings: []dnsbench.Datapoint{ 82 | { 83 | Start: start.Add(time.Second), 84 | Duration: time.Second, 85 | }, 86 | { 87 | Start: start.Add(3 * time.Second), 88 | Duration: 2 * time.Second, 89 | }, 90 | }, 91 | Counters: &dnsbench.Counters{ 92 | Success: 1, 93 | Negative: 1, 94 | Truncated: 1, 95 | IOError: 1, 96 | Error: 1, 97 | IDmismatch: 1, 98 | Total: 6, 99 | }, 100 | Errors: []dnsbench.ErrorDatapoint{ 101 | { 102 | Start: start.Add(3 * time.Second), 103 | Err: errors.New("test2"), 104 | }, 105 | }, 106 | AuthenticatedDomains: map[string]struct{}{ 107 | "google.com.": {}, 108 | }, 109 | DoHStatusCodes: map[int]int64{ 110 | 200: 4, 111 | 500: 1, 112 | }, 113 | }, 114 | } 115 | 116 | want := reporter.BenchmarkResultStats{ 117 | Codes: map[int]int64{ 118 | dns.RcodeSuccess: 3, 119 | dns.RcodeNameError: 2, 120 | dns.RcodeServerFailure: 2, 121 | }, 122 | Qtypes: map[string]int64{ 123 | "A": 3, 124 | "AAAA": 4, 125 | }, 126 | Hist: histogramWithValues(time.Second, 2*time.Second, time.Second, time.Second, 2*time.Second), 127 | Timings: []dnsbench.Datapoint{ 128 | { 129 | Start: start, 130 | Duration: time.Second, 131 | }, 132 | { 133 | Start: start.Add(time.Second), 134 | Duration: 2 * time.Second, 135 | }, 136 | { 137 | Start: start.Add(time.Second), 138 | Duration: time.Second, 139 | }, 140 | { 141 | Start: start.Add(2 * time.Second), 142 | Duration: time.Second, 143 | }, 144 | { 145 | Start: start.Add(3 * time.Second), 146 | Duration: 2 * time.Second, 147 | }, 148 | }, 149 | Counters: dnsbench.Counters{ 150 | Success: 3, 151 | Negative: 2, 152 | Truncated: 2, 153 | IOError: 3, 154 | Error: 2, 155 | IDmismatch: 2, 156 | Total: 14, 157 | }, 158 | Errors: []dnsbench.ErrorDatapoint{ 159 | { 160 | Start: start.Add(3 * time.Second), 161 | Err: errors.New("test"), 162 | }, 163 | { 164 | Start: start.Add(3 * time.Second), 165 | Err: errors.New("test2"), 166 | }, 167 | { 168 | Start: start.Add(4 * time.Second), 169 | Err: errors.New("test"), 170 | }, 171 | }, 172 | GroupedErrors: map[string]int{ 173 | "test": 2, 174 | "test2": 1, 175 | }, 176 | AuthenticatedDomains: map[string]struct{}{ 177 | "google.com.": {}, 178 | }, 179 | DoHStatusCodes: map[int]int64{ 180 | 200: 9, 181 | 500: 1, 182 | 503: 1, 183 | }, 184 | } 185 | 186 | res := reporter.Merge(&dnsbench.Benchmark{DNSSEC: true, HistMin: 0, HistMax: 5 * time.Second, HistPre: 1}, stats) 187 | 188 | assert.Equal(t, want, res) 189 | } 190 | 191 | func histogramWithValues(durations ...time.Duration) *hdrhistogram.Histogram { 192 | hst := hdrhistogram.New(0, 5*time.Second.Nanoseconds(), 1) 193 | for _, v := range durations { 194 | hst.RecordValue(v.Nanoseconds()) 195 | } 196 | return hst 197 | } 198 | -------------------------------------------------------------------------------- /pkg/reporter/plot.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "math" 7 | "os" 8 | "sort" 9 | "time" 10 | 11 | "github.com/miekg/dns" 12 | "github.com/montanaflynn/stats" 13 | "github.com/tantalor93/dnspyre/v3/pkg/dnsbench" 14 | "go-hep.org/x/hep/hplot" 15 | "gonum.org/v1/gonum/stat" 16 | "gonum.org/v1/plot" 17 | "gonum.org/v1/plot/plotter" 18 | "gonum.org/v1/plot/plotutil" 19 | "gonum.org/v1/plot/vg" 20 | "gonum.org/v1/plot/vg/draw" 21 | ) 22 | 23 | func plotHistogramLatency(file string, times []dnsbench.Datapoint) { 24 | if len(times) == 0 { 25 | // nothing to plot 26 | return 27 | } 28 | var values plotter.Values 29 | for _, v := range times { 30 | values = append(values, float64(v.Duration.Milliseconds())) 31 | } 32 | p := plot.New() 33 | p.Title.Text = "Latencies distribution" 34 | 35 | hist, err := plotter.NewHist(values, numBins(values)) 36 | if err != nil { 37 | panic(err) 38 | } 39 | p.X.Label.Text = "Latencies (ms)" 40 | p.X.Tick.Marker = hplot.Ticks{N: 5, Format: "%.0f"} 41 | p.Y.Label.Text = "Number of requests" 42 | p.Y.Tick.Marker = hplot.Ticks{N: 5, Format: "%.0f"} 43 | hist.FillColor = color.RGBA{R: 175, G: 238, B: 238, A: 255} 44 | p.Add(hist) 45 | 46 | if err := p.Save(6*vg.Inch, 6*vg.Inch, file); err != nil { 47 | fmt.Fprintln(os.Stderr, "Failed to save plot.", err) 48 | } 49 | } 50 | 51 | // numBins calculates number of bins for histogram. 52 | func numBins(values plotter.Values) int { 53 | n := float64(len(values)) 54 | 55 | // small dataset 56 | if n < 100 { 57 | sqrt := math.Sqrt(n) 58 | return int(math.Min(15, sqrt)) 59 | } 60 | 61 | // medium dataset - use Rice's rule 62 | if n < 1000 { 63 | rice := 2 * math.Cbrt(n) 64 | return int(math.Min(30, rice)) 65 | } 66 | 67 | // large dataset - use Doane's rule 68 | // Calculate skewness 69 | skewness := stat.Skew(values, nil) 70 | 71 | // Calculate standard error of skewness 72 | sigmaG := math.Sqrt(6 * (n - 2) / ((n + 1) * (n + 3))) 73 | doane := 1 + math.Log2(n) + math.Log2(1+math.Abs(skewness)/sigmaG) 74 | return int(math.Min(50, doane)) 75 | } 76 | 77 | func plotBoxPlotLatency(file, server string, times []dnsbench.Datapoint) { 78 | if len(times) == 0 { 79 | // nothing to plot 80 | return 81 | } 82 | var values plotter.Values 83 | for _, v := range times { 84 | values = append(values, float64(v.Duration.Milliseconds())) 85 | } 86 | p := plot.New() 87 | p.Title.Text = "Latencies distribution" 88 | p.Y.Label.Text = "Latencies (ms)" 89 | p.Y.Tick.Marker = hplot.Ticks{N: 3, Format: "%.0f"} 90 | p.NominalX(server) 91 | 92 | boxplot, err := plotter.NewBoxPlot(vg.Length(120), 0, values) 93 | if err != nil { 94 | panic(err) 95 | } 96 | boxplot.FillColor = color.RGBA{R: 127, G: 188, B: 165, A: 255} 97 | p.Add(boxplot) 98 | 99 | if err := p.Save(6*vg.Inch, 6*vg.Inch, file); err != nil { 100 | fmt.Fprintln(os.Stderr, "Failed to save plot.", err) 101 | } 102 | } 103 | 104 | func plotResponses(file string, rcodes map[int]int64) { 105 | if len(rcodes) == 0 { 106 | // nothing to plot 107 | return 108 | } 109 | sortedKeys := make([]int, 0) 110 | for k := range rcodes { 111 | sortedKeys = append(sortedKeys, k) 112 | } 113 | sort.Ints(sortedKeys) 114 | 115 | colors := []color.Color{ 116 | color.RGBA{R: 122, G: 195, B: 106, A: 255}, 117 | color.RGBA{R: 241, G: 90, B: 96, A: 255}, 118 | color.RGBA{R: 90, G: 155, B: 212, A: 255}, 119 | color.RGBA{R: 250, G: 167, B: 91, A: 255}, 120 | color.RGBA{R: 158, G: 103, B: 171, A: 255}, 121 | color.RGBA{R: 206, G: 112, B: 88, A: 255}, 122 | color.RGBA{R: 215, G: 127, B: 180, A: 255}, 123 | } 124 | colors = append(colors, plotutil.DarkColors...) 125 | 126 | p := plot.New() 127 | p.Title.Text = "Response code distribution" 128 | p.NominalX("Response codes") 129 | 130 | width := vg.Points(40) 131 | 132 | c := 0 133 | off := -vg.Length(len(rcodes)/2) * width 134 | for _, v := range sortedKeys { 135 | bar, err := plotter.NewBarChart(plotter.Values{float64(rcodes[v])}, width) 136 | if err != nil { 137 | panic(err) 138 | } 139 | p.Legend.Add(dns.RcodeToString[v], bar) 140 | bar.Color = colors[c%len(colors)] 141 | bar.Offset = off 142 | p.Add(bar) 143 | c++ 144 | off += width 145 | } 146 | 147 | p.Y.Label.Text = "Number of requests" 148 | p.Y.Tick.Marker = hplot.Ticks{N: 3, Format: "%.0f"} 149 | p.Legend.Top = true 150 | 151 | if err := p.Save(6*vg.Inch, 6*vg.Inch, file); err != nil { 152 | fmt.Fprintln(os.Stderr, "Failed to save plot.", err) 153 | } 154 | } 155 | 156 | func plotLineThroughput(file string, benchStart time.Time, times []dnsbench.Datapoint) { 157 | if len(times) == 0 { 158 | // nothing to plot 159 | return 160 | } 161 | var values plotter.XYs 162 | m := make(map[int64]int64) 163 | 164 | if len(times) != 0 { 165 | for _, v := range times { 166 | offset := v.Start.Unix() - benchStart.Unix() 167 | if _, ok := m[offset]; !ok { 168 | m[offset] = 0 169 | } 170 | m[offset]++ 171 | } 172 | } 173 | 174 | for k, v := range m { 175 | values = append(values, plotter.XY{X: float64(k), Y: float64(v)}) 176 | } 177 | 178 | sort.SliceStable(values, func(i, j int) bool { 179 | return values[i].X < values[j].X 180 | }) 181 | 182 | p := plot.New() 183 | p.Title.Text = "Throughput per second" 184 | p.X.Label.Text = "Time of test (s)" 185 | p.X.Tick.Marker = hplot.Ticks{N: 3, Format: "%.0f"} 186 | p.Y.Label.Text = "Number of requests (per sec)" 187 | p.Y.Tick.Marker = hplot.Ticks{N: 3, Format: "%.0f"} 188 | 189 | l, err := plotter.NewLine(values) 190 | l.Width = vg.Points(0.5) 191 | l.FillColor = color.RGBA{R: 175, G: 238, B: 238, A: 255} 192 | if err != nil { 193 | panic(err) 194 | } 195 | p.Add(l) 196 | 197 | scatter, err := plotter.NewScatter(values) 198 | scatter.GlyphStyle.Shape = draw.CircleGlyph{} 199 | 200 | if err != nil { 201 | panic(err) 202 | } 203 | p.Add(scatter) 204 | 205 | if err := p.Save(6*vg.Inch, 6*vg.Inch, file); err != nil { 206 | fmt.Fprintln(os.Stderr, "Failed to save plot.", err) 207 | } 208 | } 209 | 210 | type latencyMeasurements struct { 211 | p99 float64 212 | p95 float64 213 | p90 float64 214 | p50 float64 215 | } 216 | 217 | func plotLineLatencies(file string, benchStart time.Time, times []dnsbench.Datapoint) { 218 | if len(times) == 0 { 219 | // nothing to plot 220 | return 221 | } 222 | 223 | measurements := make(map[int64]latencyMeasurements) 224 | timings := make([]float64, 0) 225 | last := times[0].Start.Unix() - benchStart.Unix() 226 | 227 | for _, v := range times { 228 | offset := v.Start.Unix() - benchStart.Unix() 229 | if offset != last { 230 | collectMeasurements(timings, measurements, last) 231 | last = offset 232 | } 233 | timings = append(timings, float64(v.Duration.Milliseconds())) 234 | } 235 | collectMeasurements(timings, measurements, last) 236 | 237 | var p99values plotter.XYs 238 | var p95values plotter.XYs 239 | var p90values plotter.XYs 240 | var p50values plotter.XYs 241 | 242 | for k, v := range measurements { 243 | p99values = append(p99values, plotter.XY{X: float64(k), Y: v.p99}) 244 | p95values = append(p95values, plotter.XY{X: float64(k), Y: v.p95}) 245 | p90values = append(p90values, plotter.XY{X: float64(k), Y: v.p90}) 246 | p50values = append(p50values, plotter.XY{X: float64(k), Y: v.p50}) 247 | } 248 | 249 | less := func(xys plotter.XYs) func(i, j int) bool { 250 | return func(i, j int) bool { 251 | return xys[i].X < xys[j].X 252 | } 253 | } 254 | 255 | sort.SliceStable(p99values, less(p99values)) 256 | sort.SliceStable(p95values, less(p95values)) 257 | sort.SliceStable(p90values, less(p90values)) 258 | sort.SliceStable(p50values, less(p50values)) 259 | 260 | p := plot.New() 261 | p.Title.Text = "Response latencies" 262 | p.X.Label.Text = "Time of test (s)" 263 | p.Y.Label.Text = "Latency (ms)" 264 | 265 | plotLine(p, p99values, plotutil.DarkColors[0], plotutil.SoftColors[0], "p99") 266 | plotLine(p, p95values, plotutil.DarkColors[1], plotutil.SoftColors[1], "p95") 267 | plotLine(p, p90values, plotutil.DarkColors[2], plotutil.SoftColors[2], "p90") 268 | plotLine(p, p50values, plotutil.DarkColors[3], plotutil.SoftColors[3], "p50") 269 | 270 | p.Legend.Top = true 271 | 272 | if err := p.Save(6*vg.Inch, 6*vg.Inch, file); err != nil { 273 | fmt.Fprintln(os.Stderr, "Failed to save plot.", err) 274 | } 275 | } 276 | 277 | func collectMeasurements(timings []float64, measurements map[int64]latencyMeasurements, offset int64) { 278 | p99, err := stats.Percentile(timings, 99) 279 | if err != nil { 280 | panic(err) 281 | } 282 | p95, err := stats.Percentile(timings, 95) 283 | if err != nil { 284 | panic(err) 285 | } 286 | p90, err := stats.Percentile(timings, 90) 287 | if err != nil { 288 | panic(err) 289 | } 290 | p50, err := stats.Percentile(timings, 50) 291 | if err != nil { 292 | panic(err) 293 | } 294 | measure := latencyMeasurements{} 295 | measure.p99 = p99 296 | measure.p95 = p95 297 | measure.p90 = p90 298 | measure.p50 = p50 299 | measurements[offset] = measure 300 | } 301 | 302 | func plotErrorRate(file string, benchStart time.Time, times []dnsbench.ErrorDatapoint) { 303 | if len(times) == 0 { 304 | // nothing to plot 305 | return 306 | } 307 | var values plotter.XYs 308 | m := make(map[int64]int64) 309 | 310 | for _, v := range times { 311 | offset := v.Start.Unix() - benchStart.Unix() 312 | if _, ok := m[offset]; !ok { 313 | m[offset] = 0 314 | } 315 | m[offset]++ 316 | } 317 | 318 | for k, v := range m { 319 | values = append(values, plotter.XY{X: float64(k), Y: float64(v)}) 320 | } 321 | 322 | sort.SliceStable(values, func(i, j int) bool { 323 | return values[i].X < values[j].X 324 | }) 325 | 326 | p := plot.New() 327 | p.Title.Text = "Error rate over time" 328 | p.X.Label.Text = "Time of test (s)" 329 | p.X.Tick.Marker = hplot.Ticks{N: 3, Format: "%.0f"} 330 | p.Y.Label.Text = "Number of errors (per sec)" 331 | p.Y.Tick.Marker = hplot.Ticks{N: 3, Format: "%.0f"} 332 | 333 | l, err := plotter.NewLine(values) 334 | l.Width = vg.Points(0.5) 335 | 336 | if err != nil { 337 | panic(err) 338 | } 339 | p.Add(l) 340 | 341 | scatter, err := plotter.NewScatter(values) 342 | if err != nil { 343 | panic(err) 344 | } 345 | scatter.GlyphStyle.Color = color.RGBA{R: 238, G: 46, B: 47, A: 255} 346 | scatter.GlyphStyle.Shape = draw.CircleGlyph{} 347 | 348 | p.Add(scatter) 349 | 350 | if err := p.Save(6*vg.Inch, 6*vg.Inch, file); err != nil { 351 | fmt.Fprintln(os.Stderr, "Failed to save plot.", err) 352 | } 353 | } 354 | 355 | func plotLine(p *plot.Plot, values plotter.XYs, color color.Color, fill color.Color, name string) { 356 | l, err := plotter.NewLine(values) 357 | l.Color = color 358 | if err != nil { 359 | panic(err) 360 | } 361 | l.FillColor = fill 362 | p.Add(l) 363 | p.Legend.Add(name, l) 364 | scatter, err := plotter.NewScatter(values) 365 | if err != nil { 366 | panic(err) 367 | } 368 | scatter.GlyphStyle.Color = color 369 | scatter.GlyphStyle.Shape = draw.CircleGlyph{} 370 | p.Add(scatter) 371 | } 372 | -------------------------------------------------------------------------------- /pkg/reporter/plot_test.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/tantalor93/dnspyre/v3/pkg/dnsbench" 11 | "gonum.org/v1/plot/plotter" 12 | ) 13 | 14 | var testStart = time.Now() 15 | 16 | var testDatapoints = []dnsbench.Datapoint{ 17 | {Start: testStart, Duration: 100 * time.Millisecond}, 18 | {Start: testStart.Add(time.Second), Duration: 200 * time.Millisecond}, 19 | {Start: testStart.Add(2 * time.Second), Duration: 300 * time.Millisecond}, 20 | {Start: testStart.Add(3 * time.Second), Duration: 100 * time.Millisecond}, 21 | {Start: testStart.Add(4 * time.Second), Duration: 150 * time.Millisecond}, 22 | {Start: testStart.Add(5 * time.Second), Duration: 200 * time.Millisecond}, 23 | {Start: testStart.Add(6 * time.Second), Duration: 200 * time.Millisecond}, 24 | {Start: testStart.Add(7 * time.Second), Duration: 300 * time.Millisecond}, 25 | {Start: testStart.Add(8 * time.Second), Duration: 350 * time.Millisecond}, 26 | {Start: testStart.Add(9 * time.Second), Duration: 100 * time.Millisecond}, 27 | {Start: testStart.Add(10 * time.Second), Duration: 200 * time.Millisecond}, 28 | } 29 | 30 | var testErrorDatapoints = []dnsbench.ErrorDatapoint{ 31 | {Start: testStart.Add(2 * time.Second)}, 32 | {Start: testStart.Add(3 * time.Second)}, 33 | {Start: testStart.Add(4 * time.Second)}, 34 | {Start: testStart.Add(5 * time.Second)}, 35 | {Start: testStart.Add(6 * time.Second)}, 36 | {Start: testStart.Add(7 * time.Second)}, 37 | } 38 | 39 | var testRcodes = map[int]int64{ 40 | 0: 8, 41 | 2: 1, 42 | 3: 2, 43 | } 44 | 45 | func Test_plotHistogramLatency(t *testing.T) { 46 | dir := t.TempDir() 47 | 48 | file := dir + "/histogram-latency.svg" 49 | plotHistogramLatency(file, testDatapoints) 50 | 51 | expected, err := os.ReadFile("testdata/test-histogram-latency.svg") 52 | require.NoError(t, err) 53 | 54 | actual, err := os.ReadFile(file) 55 | require.NoError(t, err) 56 | 57 | assert.Equal(t, expected, actual, "generated histogram latency plot does not equal to expected 'test-histogram-latency.png'") 58 | } 59 | 60 | func Test_plotBoxPlotLatency(t *testing.T) { 61 | dir := t.TempDir() 62 | 63 | file := dir + "/boxplot-latency.svg" 64 | plotBoxPlotLatency(file, "127.0.0.1", testDatapoints) 65 | 66 | expected, err := os.ReadFile("testdata/test-boxplot-latency.svg") 67 | require.NoError(t, err) 68 | 69 | actual, err := os.ReadFile(file) 70 | require.NoError(t, err) 71 | 72 | assert.Equal(t, expected, actual, "generated boxplot latency plot does not equal to expected 'test-boxplot-latency.png'") 73 | } 74 | 75 | func Test_plotResponses(t *testing.T) { 76 | dir := t.TempDir() 77 | 78 | file := dir + "/responses-barchart.svg" 79 | plotResponses(file, testRcodes) 80 | 81 | expected, err := os.ReadFile("testdata/test-responses-barchart.svg") 82 | require.NoError(t, err) 83 | 84 | actual, err := os.ReadFile(file) 85 | require.NoError(t, err) 86 | 87 | assert.Equal(t, expected, actual, "generated responses plot does not equal to expected 'test-responses-barchart.png'") 88 | } 89 | 90 | func Test_plotLineThroughput(t *testing.T) { 91 | dir := t.TempDir() 92 | 93 | file := dir + "/throughput-lineplot.svg" 94 | plotLineThroughput(file, testStart, testDatapoints) 95 | 96 | expected, err := os.ReadFile("testdata/test-throughput-lineplot.svg") 97 | require.NoError(t, err) 98 | 99 | actual, err := os.ReadFile(file) 100 | require.NoError(t, err) 101 | 102 | assert.Equal(t, expected, actual, "generated line throughput plot does not equal to expected 'test-throughput-lineplot.png'") 103 | } 104 | 105 | func Test_plotLineLatencies(t *testing.T) { 106 | dir := t.TempDir() 107 | 108 | file := dir + "/latency-lineplot.svg" 109 | plotLineLatencies(file, testStart, testDatapoints) 110 | 111 | expected, err := os.ReadFile("testdata/test-latency-lineplot.svg") 112 | require.NoError(t, err) 113 | 114 | actual, err := os.ReadFile(file) 115 | require.NoError(t, err) 116 | 117 | assert.Equal(t, expected, actual, "generated line latencies plot does not equal to expected 'test-latency-lineplot.png'") 118 | } 119 | 120 | func Test_plotErrorRate(t *testing.T) { 121 | dir := t.TempDir() 122 | 123 | file := dir + "/errorrate-lineplot.svg" 124 | plotErrorRate(file, testStart, testErrorDatapoints) 125 | 126 | expected, err := os.ReadFile("testdata/test-errorrate-lineplot.svg") 127 | require.NoError(t, err) 128 | 129 | actual, err := os.ReadFile(file) 130 | require.NoError(t, err) 131 | 132 | assert.Equal(t, expected, actual, "generated error rate plot does not equal to expected 'test-errorrate-lineplot.png") 133 | } 134 | 135 | func Test_numBins(t *testing.T) { 136 | tests := []struct { 137 | name string 138 | values plotter.Values 139 | want int 140 | }{ 141 | { 142 | name: "small dataset", 143 | values: dataset(25), 144 | want: 5, 145 | }, 146 | { 147 | name: "medium dataset", 148 | values: dataset(500), 149 | want: 15, 150 | }, 151 | { 152 | name: "large dataset", 153 | values: dataset(2000), 154 | want: 11, 155 | }, 156 | { 157 | name: "single item dataset", 158 | values: dataset(1), 159 | want: 1, 160 | }, 161 | } 162 | for _, tt := range tests { 163 | t.Run(tt.name, func(t *testing.T) { 164 | assert.Equal(t, tt.want, numBins(tt.values)) 165 | }) 166 | } 167 | } 168 | 169 | // dataset generates uniformorly distributed dataset. 170 | func dataset(len int) plotter.Values { 171 | values := make(plotter.Values, len) 172 | for i := range values { 173 | values[i] = float64(i) 174 | } 175 | return values 176 | } 177 | -------------------------------------------------------------------------------- /pkg/reporter/report.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/HdrHistogram/hdrhistogram-go" 11 | "github.com/tantalor93/dnspyre/v3/pkg/dnsbench" 12 | ) 13 | 14 | type orderedMap struct { 15 | m map[string]int 16 | order []string 17 | } 18 | 19 | type reportParameters struct { 20 | benchmark *dnsbench.Benchmark 21 | outputWriter io.Writer 22 | hist *hdrhistogram.Histogram 23 | codeTotals map[int]int64 24 | totalCounters dnsbench.Counters 25 | qtypeTotals map[string]int64 26 | topErrs orderedMap 27 | authenticatedDomains map[string]struct{} 28 | benchmarkDuration time.Duration 29 | dohResponseStatusesTotals map[int]int64 30 | } 31 | 32 | type reportPrinter interface { 33 | print(params reportParameters) error 34 | } 35 | 36 | // PrintReport prints formatted benchmark result to stdout, exports graphs and generates CSV output if configured. 37 | // If there is a fatal error while printing report, an error is returned. 38 | func PrintReport(b *dnsbench.Benchmark, stats []*dnsbench.ResultStats, benchStart time.Time, benchDuration time.Duration) error { 39 | totals := Merge(b, stats) 40 | 41 | top3errs := make(map[string]int) 42 | top3errorsInOrder := make([]string, 0) 43 | 44 | for i := 0; i < 3; i++ { 45 | max := 0 46 | maxerr := "" 47 | for k, v := range totals.GroupedErrors { 48 | if _, ok := top3errs[k]; v > max && !ok { 49 | maxerr = k 50 | max = v 51 | } 52 | } 53 | if max != 0 { 54 | top3errs[maxerr] = max 55 | top3errorsInOrder = append(top3errorsInOrder, maxerr) 56 | } 57 | } 58 | 59 | if len(b.PlotDir) != 0 { 60 | if err := directoryExists(b.PlotDir); err != nil { 61 | return fmt.Errorf("unable to plot results: %w", err) 62 | } 63 | 64 | now := time.Now().Format("2006-01-02T15-04-05") 65 | dir := filepath.Join(b.PlotDir, fmt.Sprintf("graphs-%s", now)) 66 | if err := os.Mkdir(dir, os.ModePerm); err != nil { 67 | return fmt.Errorf("unable to plot results: %w", err) 68 | } 69 | plotHistogramLatency(fileName(b, dir, "latency-histogram"), totals.Timings) 70 | plotBoxPlotLatency(fileName(b, dir, "latency-boxplot"), b.Server, totals.Timings) 71 | plotResponses(fileName(b, dir, "responses-barchart"), totals.Codes) 72 | plotLineThroughput(fileName(b, dir, "throughput-lineplot"), benchStart, totals.Timings) 73 | plotLineLatencies(fileName(b, dir, "latency-lineplot"), benchStart, totals.Timings) 74 | plotErrorRate(fileName(b, dir, "errorrate-lineplot"), benchStart, totals.Errors) 75 | } 76 | 77 | var csv *os.File 78 | if b.Csv != "" { 79 | f, err := os.Create(b.Csv) 80 | if err != nil { 81 | return fmt.Errorf("failed to create file for CSV export due to '%v'", err) 82 | } 83 | 84 | csv = f 85 | } 86 | 87 | defer func() { 88 | if csv != nil { 89 | csv.Close() 90 | } 91 | }() 92 | 93 | if csv != nil { 94 | writeBars(csv, totals.Hist.Distribution()) 95 | } 96 | 97 | if b.Silent { 98 | return nil 99 | } 100 | topErrs := orderedMap{m: top3errs, order: top3errorsInOrder} 101 | params := reportParameters{ 102 | benchmark: b, 103 | outputWriter: b.Writer, 104 | hist: totals.Hist, 105 | codeTotals: totals.Codes, 106 | totalCounters: totals.Counters, 107 | qtypeTotals: totals.Qtypes, 108 | topErrs: topErrs, 109 | authenticatedDomains: totals.AuthenticatedDomains, 110 | benchmarkDuration: benchDuration, 111 | dohResponseStatusesTotals: totals.DoHStatusCodes, 112 | } 113 | return printer(b).print(params) 114 | } 115 | 116 | func directoryExists(plotDir string) error { 117 | stat, err := os.Stat(plotDir) 118 | if err != nil { 119 | if os.IsNotExist(err) { 120 | return fmt.Errorf("'%s' path does not point to an existing directory", plotDir) 121 | } 122 | return err 123 | } else if !stat.IsDir() { 124 | return fmt.Errorf("'%s' is not a path to a directory", plotDir) 125 | } 126 | return nil 127 | } 128 | 129 | func printer(b *dnsbench.Benchmark) reportPrinter { 130 | switch { 131 | case b.JSON: 132 | return &jsonReporter{} 133 | default: 134 | return &standardReporter{} 135 | } 136 | } 137 | 138 | func fileName(b *dnsbench.Benchmark, dir, name string) string { 139 | return dir + "/" + name + "." + b.PlotFormat 140 | } 141 | 142 | func writeBars(f *os.File, bars []hdrhistogram.Bar) { 143 | f.WriteString("From (ns), To (ns), Count\n") 144 | 145 | for _, b := range bars { 146 | f.WriteString(b.String()) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /pkg/reporter/report_test.go: -------------------------------------------------------------------------------- 1 | package reporter_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "github.com/HdrHistogram/hdrhistogram-go" 15 | "github.com/miekg/dns" 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | "github.com/tantalor93/dnspyre/v3/pkg/dnsbench" 19 | "github.com/tantalor93/dnspyre/v3/pkg/reporter" 20 | ) 21 | 22 | func Test_PrintReport(t *testing.T) { 23 | buffer := bytes.Buffer{} 24 | b, rs := testReportData(&buffer) 25 | 26 | err := reporter.PrintReport(&b, []*dnsbench.ResultStats{&rs}, time.Now(), time.Second) 27 | 28 | require.NoError(t, err) 29 | assert.Equal(t, readResource("successReport"), buffer.String()) 30 | } 31 | 32 | func Test_PrintReport_dnssec(t *testing.T) { 33 | buffer := bytes.Buffer{} 34 | b, rs := testReportData(&buffer) 35 | b.DNSSEC = true 36 | rs.AuthenticatedDomains = map[string]struct{}{"example.org.": {}} 37 | 38 | err := reporter.PrintReport(&b, []*dnsbench.ResultStats{&rs}, time.Now(), time.Second) 39 | require.NoError(t, err) 40 | assert.Equal(t, readResource("dnssecReport"), buffer.String()) 41 | } 42 | 43 | func Test_PrintReport_doh(t *testing.T) { 44 | buffer := bytes.Buffer{} 45 | b, rs := testReportData(&buffer) 46 | rs.DoHStatusCodes = map[int]int64{ 47 | 200: 2, 48 | 500: 1, 49 | } 50 | 51 | err := reporter.PrintReport(&b, []*dnsbench.ResultStats{&rs}, time.Now(), time.Second) 52 | require.NoError(t, err) 53 | assert.Equal(t, readResource("dohReport"), buffer.String()) 54 | } 55 | 56 | func Test_PrintReport_json(t *testing.T) { 57 | buffer := bytes.Buffer{} 58 | b, rs := testReportData(&buffer) 59 | b.JSON = true 60 | b.Rcodes = true 61 | b.HistDisplay = true 62 | 63 | err := reporter.PrintReport(&b, []*dnsbench.ResultStats{&rs}, time.Now(), time.Second) 64 | require.NoError(t, err) 65 | assert.Equal(t, readResource("jsonReport"), buffer.String()) 66 | } 67 | 68 | func Test_PrintReport_json_dnssec(t *testing.T) { 69 | buffer := bytes.Buffer{} 70 | b, rs := testReportData(&buffer) 71 | b.JSON = true 72 | b.Rcodes = true 73 | b.HistDisplay = true 74 | b.DNSSEC = true 75 | rs.AuthenticatedDomains = map[string]struct{}{"example.org.": {}} 76 | 77 | err := reporter.PrintReport(&b, []*dnsbench.ResultStats{&rs}, time.Now(), time.Second) 78 | require.NoError(t, err) 79 | assert.Equal(t, readResource("jsonDnssecReport"), buffer.String()) 80 | } 81 | 82 | func Test_PrintReport_json_doh(t *testing.T) { 83 | buffer := bytes.Buffer{} 84 | b, rs := testReportData(&buffer) 85 | b.JSON = true 86 | b.Rcodes = true 87 | b.HistDisplay = true 88 | rs.DoHStatusCodes = map[int]int64{ 89 | 200: 2, 90 | } 91 | 92 | err := reporter.PrintReport(&b, []*dnsbench.ResultStats{&rs}, time.Now(), time.Second) 93 | require.NoError(t, err) 94 | assert.Equal(t, readResource("jsonDohReport"), buffer.String()) 95 | } 96 | 97 | func Test_PrintReport_errors(t *testing.T) { 98 | buffer := bytes.Buffer{} 99 | b, rs := testReportDataWithServerDNSErrors(&buffer) 100 | 101 | err := reporter.PrintReport(&b, []*dnsbench.ResultStats{&rs}, time.Now(), time.Second) 102 | require.NoError(t, err) 103 | assert.Equal(t, readResource("errorReport"), buffer.String()) 104 | } 105 | 106 | func Test_PrintReport_plot(t *testing.T) { 107 | dir := t.TempDir() 108 | 109 | buffer := bytes.Buffer{} 110 | b, rs := testReportData(&buffer) 111 | b.PlotDir = dir 112 | b.PlotFormat = dnsbench.DefaultPlotFormat 113 | 114 | err := reporter.PrintReport(&b, []*dnsbench.ResultStats{&rs}, time.Now(), time.Second) 115 | 116 | require.NoError(t, err) 117 | 118 | testDir, err := os.ReadDir(dir) 119 | 120 | require.NoError(t, err) 121 | require.Len(t, testDir, 1) 122 | 123 | graphsDir := testDir[0].Name() 124 | assert.True(t, strings.HasPrefix(graphsDir, "graphs-")) 125 | 126 | graphsDirContent, err := os.ReadDir(filepath.Join(dir, graphsDir)) 127 | require.NoError(t, err) 128 | 129 | var graphFiles []string 130 | for _, v := range graphsDirContent { 131 | graphFiles = append(graphFiles, v.Name()) 132 | } 133 | 134 | assert.ElementsMatch(t, graphFiles, 135 | []string{ 136 | "errorrate-lineplot.svg", "latency-boxplot.svg", "latency-histogram.svg", "latency-lineplot.svg", 137 | "responses-barchart.svg", "throughput-lineplot.svg", 138 | }, 139 | ) 140 | } 141 | 142 | func Test_PrintReport_plot_error(t *testing.T) { 143 | dir := t.TempDir() 144 | 145 | buffer := bytes.Buffer{} 146 | b, rs := testReportData(&buffer) 147 | b.PlotDir = dir + "/non-existing-directory" 148 | b.PlotFormat = dnsbench.DefaultPlotFormat 149 | 150 | err := reporter.PrintReport(&b, []*dnsbench.ResultStats{&rs}, time.Now(), time.Second) 151 | 152 | require.Error(t, err) 153 | } 154 | 155 | func testReportData(testOutputWriter io.Writer) (dnsbench.Benchmark, dnsbench.ResultStats) { 156 | b := dnsbench.Benchmark{ 157 | HistPre: 1, 158 | Writer: testOutputWriter, 159 | } 160 | 161 | h := hdrhistogram.New(0, 0, 1) 162 | h.RecordValue(5) 163 | h.RecordValue(10) 164 | d1 := dnsbench.Datapoint{Duration: 5, Start: time.Unix(0, 0)} 165 | d2 := dnsbench.Datapoint{Duration: 10, Start: time.Unix(0, 0)} 166 | addr, err := net.ResolveUDPAddr("udp", "8.8.8.8:53") 167 | if err != nil { 168 | panic(err) 169 | } 170 | saddr1, err := net.ResolveUDPAddr("udp", "127.0.0.1:65359") 171 | if err != nil { 172 | panic(err) 173 | } 174 | saddr2, err := net.ResolveUDPAddr("udp", "127.0.0.1:65360") 175 | if err != nil { 176 | panic(err) 177 | } 178 | rs := dnsbench.ResultStats{ 179 | Codes: map[int]int64{ 180 | dns.RcodeSuccess: 2, 181 | }, 182 | Qtypes: map[string]int64{ 183 | "A": 2, 184 | }, 185 | Hist: h, 186 | Timings: []dnsbench.Datapoint{d1, d2}, 187 | Counters: &dnsbench.Counters{ 188 | Total: 1, 189 | IOError: 6, 190 | Success: 4, 191 | IDmismatch: 10, 192 | Truncated: 7, 193 | Negative: 8, 194 | Error: 9, 195 | }, 196 | Errors: []dnsbench.ErrorDatapoint{ 197 | {Start: time.Unix(0, 0), Err: errors.New("test2")}, 198 | {Start: time.Unix(0, 0), Err: errors.New("test")}, 199 | {Start: time.Unix(0, 0), Err: &net.OpError{Op: "read", Net: "udp", Addr: addr, Source: saddr1}}, 200 | {Start: time.Unix(0, 0), Err: &net.OpError{Op: "read", Net: "udp", Addr: addr, Source: saddr2}}, 201 | {Start: time.Unix(0, 0), Err: errors.New("test2")}, 202 | {Start: time.Unix(0, 0), Err: errors.New("test2")}, 203 | }, 204 | } 205 | return b, rs 206 | } 207 | 208 | func testReportDataWithServerDNSErrors(testOutputWriter io.Writer) (dnsbench.Benchmark, dnsbench.ResultStats) { 209 | b := dnsbench.Benchmark{ 210 | HistPre: 1, 211 | Writer: testOutputWriter, 212 | } 213 | h := hdrhistogram.New(0, 0, 1) 214 | rs := dnsbench.ResultStats{ 215 | Codes: map[int]int64{}, 216 | Qtypes: map[string]int64{ 217 | "A": 3, 218 | }, 219 | Hist: h, 220 | Timings: []dnsbench.Datapoint{}, 221 | Counters: &dnsbench.Counters{ 222 | Total: 3, 223 | IOError: 3, 224 | }, 225 | Errors: []dnsbench.ErrorDatapoint{ 226 | {Start: time.Unix(0, 0), Err: &net.DNSError{Err: "no such host", Name: "unknown.host.com"}}, 227 | {Start: time.Unix(0, 0), Err: &net.DNSError{Err: "no such host", Name: "unknown.host.com"}}, 228 | {Start: time.Unix(0, 0), Err: &net.DNSError{Err: "no such host", Name: "unknown.host.com"}}, 229 | }, 230 | } 231 | return b, rs 232 | } 233 | 234 | func readResource(resource string) string { 235 | open, err := os.Open("testdata/" + resource) 236 | if err != nil { 237 | panic(err) 238 | } 239 | all, err := io.ReadAll(open) 240 | if err != nil { 241 | panic(err) 242 | } 243 | data := string(all) 244 | return data 245 | } 246 | -------------------------------------------------------------------------------- /pkg/reporter/stdreporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "io" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/HdrHistogram/hdrhistogram-go" 11 | "github.com/miekg/dns" 12 | "github.com/olekukonko/tablewriter" 13 | "github.com/tantalor93/dnspyre/v3/pkg/dnsbench" 14 | "github.com/tantalor93/dnspyre/v3/pkg/printutils" 15 | ) 16 | 17 | type standardReporter struct{} 18 | 19 | func (s *standardReporter) print(params reportParameters) error { 20 | printProgress(params.outputWriter, params.totalCounters) 21 | 22 | if len(params.codeTotals) > 0 { 23 | printutils.NeutralFprintf(params.outputWriter, "\nDNS response codes:\n") 24 | for i := dns.RcodeSuccess; i <= dns.RcodeBadCookie; i++ { 25 | printFn := printutils.ErrFprintf 26 | if i == dns.RcodeSuccess { 27 | printFn = printutils.SuccessFprintf 28 | } 29 | if i == dns.RcodeNameError { 30 | printFn = printutils.NeutralFprintf 31 | } 32 | if c, ok := params.codeTotals[i]; ok { 33 | printFn(params.outputWriter, "\t%s:\t%d\n", dns.RcodeToString[i], c) 34 | } 35 | } 36 | } 37 | 38 | var dohResponseStatuses []int 39 | for key := range params.dohResponseStatusesTotals { 40 | dohResponseStatuses = append(dohResponseStatuses, key) 41 | } 42 | sort.Ints(dohResponseStatuses) 43 | 44 | if len(params.dohResponseStatusesTotals) > 0 { 45 | printutils.NeutralFprintf(params.outputWriter, "\nDoH HTTP response status codes:\n") 46 | for _, st := range dohResponseStatuses { 47 | if st == 200 { 48 | printutils.SuccessFprintf(params.outputWriter, "\t%d:\t%d\n", st, params.dohResponseStatusesTotals[st]) 49 | } else { 50 | printutils.ErrFprintf(params.outputWriter, "\t%d:\t%d\n", st, params.dohResponseStatusesTotals[st]) 51 | } 52 | } 53 | } 54 | 55 | if len(params.qtypeTotals) > 0 { 56 | printutils.NeutralFprintf(params.outputWriter, "\nDNS question types:\n") 57 | for k, v := range params.qtypeTotals { 58 | printutils.SuccessFprintf(params.outputWriter, "\t%s:\t%d\n", k, v) 59 | } 60 | } 61 | 62 | if params.benchmark.DNSSEC { 63 | printutils.NeutralFprintf(params.outputWriter, 64 | "\nNumber of domains secured using DNSSEC: %s\n", printutils.HighlightSprint(len(params.authenticatedDomains))) 65 | } 66 | 67 | printutils.NeutralFprintf(params.outputWriter, "\nTime taken for tests:\t%s\n", 68 | printutils.HighlightSprint(roundDuration(params.benchmarkDuration))) 69 | printutils.NeutralFprintf(params.outputWriter, "Questions per second:\t%s\n", 70 | printutils.HighlightSprintf("%0.1f", float64(params.totalCounters.Total)/params.benchmarkDuration.Seconds())) 71 | 72 | min := time.Duration(params.hist.Min()) 73 | mean := time.Duration(params.hist.Mean()) 74 | sd := time.Duration(params.hist.StdDev()) 75 | max := time.Duration(params.hist.Max()) 76 | p99 := time.Duration(params.hist.ValueAtQuantile(99)) 77 | p95 := time.Duration(params.hist.ValueAtQuantile(95)) 78 | p90 := time.Duration(params.hist.ValueAtQuantile(90)) 79 | p75 := time.Duration(params.hist.ValueAtQuantile(75)) 80 | p50 := time.Duration(params.hist.ValueAtQuantile(50)) 81 | 82 | if tc := params.hist.TotalCount(); tc > 0 { 83 | printutils.NeutralFprintf(params.outputWriter, "DNS timings, %s datapoints\n", printutils.HighlightSprint(tc)) 84 | printutils.NeutralFprintf(params.outputWriter, "\t min:\t\t%s\n", printutils.HighlightSprint(roundDuration(min))) 85 | printutils.NeutralFprintf(params.outputWriter, "\t mean:\t\t%s\n", printutils.HighlightSprint(roundDuration(mean))) 86 | printutils.NeutralFprintf(params.outputWriter, "\t [+/-sd]:\t%s\n", printutils.HighlightSprint(roundDuration(sd))) 87 | printutils.NeutralFprintf(params.outputWriter, "\t max:\t\t%s\n", printutils.HighlightSprint(roundDuration(max))) 88 | printutils.NeutralFprintf(params.outputWriter, "\t p99:\t\t%s\n", printutils.HighlightSprint(roundDuration(p99))) 89 | printutils.NeutralFprintf(params.outputWriter, "\t p95:\t\t%s\n", printutils.HighlightSprint(roundDuration(p95))) 90 | printutils.NeutralFprintf(params.outputWriter, "\t p90:\t\t%s\n", printutils.HighlightSprint(roundDuration(p90))) 91 | printutils.NeutralFprintf(params.outputWriter, "\t p75:\t\t%s\n", printutils.HighlightSprint(roundDuration(p75))) 92 | printutils.NeutralFprintf(params.outputWriter, "\t p50:\t\t%s\n", printutils.HighlightSprint(roundDuration(p50))) 93 | 94 | dist := params.hist.Distribution() 95 | if params.benchmark.HistDisplay && tc > 1 { 96 | printutils.NeutralFprintf(params.outputWriter, "\nDNS distribution, %s datapoints\n", printutils.HighlightSprint(tc)) 97 | printBars(params.outputWriter, dist) 98 | } 99 | } 100 | 101 | sumerrs := 0 102 | for _, v := range params.topErrs.m { 103 | sumerrs += v 104 | } 105 | 106 | if len(params.topErrs.m) > 0 { 107 | printutils.ErrFprintf(params.outputWriter, "\nTotal Errors: %d\n", sumerrs) 108 | printutils.ErrFprintf(params.outputWriter, "Top errors:\n") 109 | for _, err := range params.topErrs.order { 110 | printutils.ErrFprintf(params.outputWriter, "%s\t%d (%.2f)%%\n", err, params.topErrs.m[err], 111 | (float64(params.topErrs.m[err])/float64(sumerrs))*100) 112 | } 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func printProgress(w io.Writer, c dnsbench.Counters) { 119 | printutils.NeutralFprintf(w, "\nTotal requests:\t\t%s\n", printutils.HighlightSprint(c.Total)) 120 | 121 | if c.IOError > 0 { 122 | printutils.ErrFprintf(w, "Read/Write errors:\t%d\n", c.IOError) 123 | } 124 | 125 | if c.IDmismatch > 0 { 126 | printutils.ErrFprintf(w, "ID mismatch errors:\t%d\n", c.IDmismatch) 127 | } 128 | 129 | if c.Success > 0 { 130 | printutils.SuccessFprintf(w, "DNS success responses:\t%d\n", c.Success) 131 | } 132 | if c.Negative > 0 { 133 | printutils.NeutralFprintf(w, "DNS negative responses:\t%d\n", c.Negative) 134 | } 135 | if c.Error > 0 { 136 | printutils.ErrFprintf(w, "DNS error responses:\t%d\n", c.Error) 137 | } 138 | 139 | if c.Truncated > 0 { 140 | printutils.ErrFprintf(w, "Truncated responses:\t%d\n", c.Truncated) 141 | } 142 | } 143 | 144 | func printBars(w io.Writer, bars []hdrhistogram.Bar) { 145 | counts := make([]int64, 0, len(bars)) 146 | lines := make([][]string, 0, len(bars)) 147 | added := false 148 | var max int64 149 | 150 | for _, b := range bars { 151 | if b.Count == 0 && !added { 152 | // trim the start 153 | continue 154 | } 155 | if b.Count > max { 156 | max = b.Count 157 | } 158 | 159 | added = true 160 | 161 | line := make([]string, 3) 162 | lines = append(lines, line) 163 | counts = append(counts, b.Count) 164 | 165 | line[0] = roundDuration(time.Duration(b.To/2 + b.From/2)).String() 166 | line[2] = strconv.FormatInt(b.Count, 10) 167 | } 168 | 169 | for i, l := range lines { 170 | l[1] = makeBar(counts[i], max) 171 | } 172 | 173 | table := tablewriter.NewWriter(w) 174 | table.SetHeader([]string{"Latency", "", "Count"}) 175 | table.SetBorder(false) 176 | table.AppendBulk(lines) 177 | table.Render() 178 | } 179 | 180 | func makeBar(c int64, max int64) string { 181 | if c == 0 { 182 | return "" 183 | } 184 | t := int((43 * float64(c) / float64(max)) + 0.5) 185 | return strings.Repeat(printutils.HighlightSprint("▄"), t) 186 | } 187 | 188 | func roundDuration(dur time.Duration) time.Duration { 189 | if dur > time.Minute { 190 | return dur.Round(10 * time.Second) 191 | } 192 | if dur > time.Second { 193 | return dur.Round(10 * time.Millisecond) 194 | } 195 | if dur > time.Millisecond { 196 | return dur.Round(10 * time.Microsecond) 197 | } 198 | if dur > time.Microsecond { 199 | return dur.Round(10 * time.Nanosecond) 200 | } 201 | return dur 202 | } 203 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/dnssecReport: -------------------------------------------------------------------------------- 1 | 2 | Total requests: 1 3 | Read/Write errors: 6 4 | ID mismatch errors: 10 5 | DNS success responses: 4 6 | DNS negative responses: 8 7 | DNS error responses: 9 8 | Truncated responses: 7 9 | 10 | DNS response codes: 11 | NOERROR: 2 12 | 13 | DNS question types: 14 | A: 2 15 | 16 | Number of domains secured using DNSSEC: 1 17 | 18 | Time taken for tests: 1s 19 | Questions per second: 1.0 20 | DNS timings, 2 datapoints 21 | min: 5ns 22 | mean: 7ns 23 | [+/-sd]: 2ns 24 | max: 10ns 25 | p99: 10ns 26 | p95: 10ns 27 | p90: 10ns 28 | p75: 10ns 29 | p50: 5ns 30 | 31 | Total Errors: 6 32 | Top errors: 33 | test2 3 (50.00)% 34 | read udp 8.8.8.8:53 2 (33.33)% 35 | test 1 (16.67)% 36 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/dohReport: -------------------------------------------------------------------------------- 1 | 2 | Total requests: 1 3 | Read/Write errors: 6 4 | ID mismatch errors: 10 5 | DNS success responses: 4 6 | DNS negative responses: 8 7 | DNS error responses: 9 8 | Truncated responses: 7 9 | 10 | DNS response codes: 11 | NOERROR: 2 12 | 13 | DoH HTTP response status codes: 14 | 200: 2 15 | 500: 1 16 | 17 | DNS question types: 18 | A: 2 19 | 20 | Time taken for tests: 1s 21 | Questions per second: 1.0 22 | DNS timings, 2 datapoints 23 | min: 5ns 24 | mean: 7ns 25 | [+/-sd]: 2ns 26 | max: 10ns 27 | p99: 10ns 28 | p95: 10ns 29 | p90: 10ns 30 | p75: 10ns 31 | p50: 5ns 32 | 33 | Total Errors: 6 34 | Top errors: 35 | test2 3 (50.00)% 36 | read udp 8.8.8.8:53 2 (33.33)% 37 | test 1 (16.67)% 38 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/errorReport: -------------------------------------------------------------------------------- 1 | 2 | Total requests: 3 3 | Read/Write errors: 3 4 | 5 | DNS question types: 6 | A: 3 7 | 8 | Time taken for tests: 1s 9 | Questions per second: 3.0 10 | 11 | Total Errors: 3 12 | Top errors: 13 | no such host unknown.host.com 3 (100.00)% 14 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/jsonDnssecReport: -------------------------------------------------------------------------------- 1 | {"totalRequests":1,"totalSuccessResponses":4,"totalNegativeResponses":8,"totalErrorResponses":9,"totalIOErrors":6,"totalIDmismatch":10,"totalTruncatedResponses":7,"responseRcodes":{"NOERROR":2},"questionTypes":{"A":2},"queriesPerSecond":1,"benchmarkDurationSeconds":1,"latencyStats":{"minMs":0,"meanMs":0,"stdMs":0,"maxMs":0,"p99Ms":0,"p95Ms":0,"p90Ms":0,"p75Ms":0,"p50Ms":0},"latencyDistribution":[{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":1},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":1}],"totalDNSSECSecuredDomains":1} 2 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/jsonDohReport: -------------------------------------------------------------------------------- 1 | {"totalRequests":1,"totalSuccessResponses":4,"totalNegativeResponses":8,"totalErrorResponses":9,"totalIOErrors":6,"totalIDmismatch":10,"totalTruncatedResponses":7,"responseRcodes":{"NOERROR":2},"questionTypes":{"A":2},"queriesPerSecond":1,"benchmarkDurationSeconds":1,"latencyStats":{"minMs":0,"meanMs":0,"stdMs":0,"maxMs":0,"p99Ms":0,"p95Ms":0,"p90Ms":0,"p75Ms":0,"p50Ms":0},"latencyDistribution":[{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":1},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":1}],"dohHTTPResponseStatusCodes":{"200":2}} 2 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/jsonReport: -------------------------------------------------------------------------------- 1 | {"totalRequests":1,"totalSuccessResponses":4,"totalNegativeResponses":8,"totalErrorResponses":9,"totalIOErrors":6,"totalIDmismatch":10,"totalTruncatedResponses":7,"responseRcodes":{"NOERROR":2},"questionTypes":{"A":2},"queriesPerSecond":1,"benchmarkDurationSeconds":1,"latencyStats":{"minMs":0,"meanMs":0,"stdMs":0,"maxMs":0,"p99Ms":0,"p95Ms":0,"p90Ms":0,"p75Ms":0,"p50Ms":0},"latencyDistribution":[{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":1},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":0},{"latencyMs":0,"count":1}]} 2 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/successReport: -------------------------------------------------------------------------------- 1 | 2 | Total requests: 1 3 | Read/Write errors: 6 4 | ID mismatch errors: 10 5 | DNS success responses: 4 6 | DNS negative responses: 8 7 | DNS error responses: 9 8 | Truncated responses: 7 9 | 10 | DNS response codes: 11 | NOERROR: 2 12 | 13 | DNS question types: 14 | A: 2 15 | 16 | Time taken for tests: 1s 17 | Questions per second: 1.0 18 | DNS timings, 2 datapoints 19 | min: 5ns 20 | mean: 7ns 21 | [+/-sd]: 2ns 22 | max: 10ns 23 | p99: 10ns 24 | p95: 10ns 25 | p90: 10ns 26 | p75: 10ns 27 | p50: 5ns 28 | 29 | Total Errors: 6 30 | Top errors: 31 | test2 3 (50.00)% 32 | read udp 8.8.8.8:53 2 (33.33)% 33 | test 1 (16.67)% 34 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/test-boxplot-latency.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Latencies distribution 10 | 127.0.0.1 12 | 13 | Latencies (ms) 15 | 16 | 100 18 | 200 20 | 300 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/test-errorrate-lineplot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Error rate over time 10 | Time of test (s) 12 | 2 14 | 4 16 | 6 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Number of errors (per sec) 28 | 29 | 0 31 | 1 33 | 2 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/test-histogram-latency.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Latencies distribution 10 | Latencies (ms) 12 | 100 14 | 150 16 | 200 18 | 250 20 | 300 22 | 350 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Number of requests 54 | 55 | 0 57 | 1 59 | 2 61 | 3 63 | 4 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/test-responses-barchart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Response code distribution 10 | Response codes 12 | 13 | Number of requests 15 | 16 | 0 18 | 4 20 | 8 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | NOERROR 42 | 43 | 44 | SERVFAIL 46 | 47 | 48 | NXDOMAIN 50 | 51 | 52 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/test-throughput-lineplot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Throughput per second 10 | Time of test (s) 12 | 0 14 | 5 16 | 10 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Number of requests (per sec) 33 | 34 | 0 36 | 1 38 | 2 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /scripts/completions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # scripts/completions.sh 3 | set -e 4 | rm -rf completions 5 | mkdir completions 6 | for sh in bash zsh; do 7 | go run main.go --completion-script-"$sh" >"completions/dnspyre.$sh" 8 | done 9 | -------------------------------------------------------------------------------- /scripts/manpages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # scripts/manpages.sh 3 | set -e 4 | rm -rf manpages 5 | mkdir manpages 6 | go run main.go --help-man >"manpages/dnspyre.1" 7 | gzip --best "manpages/dnspyre.1" 8 | --------------------------------------------------------------------------------