diff --git a/.codecov.yml b/.codecov.yml index f91e5c1f..8a3d121b 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,8 +1,8 @@ -coverage: - status: - project: - default: - target: 40% - threshold: null - patch: false - changes: false +'coverage': + 'status': + 'project': + 'default': + 'target': '40%' + 'threshold': null + 'patch': false + 'changes': false diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 9889db34..278d7d3e 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,18 +1,13 @@ -#!/bin/bash -set -e; +#!/bin/sh -found=0 -git diff --cached --name-only | grep -q '.js$' && found=1 -if [ $found == 1 ]; then - npm --prefix client run lint || exit 1 - npm run test --prefix client || exit 1 +set -e -f -u + +if [ "$(git diff --cached --name-only -- '*.js')" ] +then + make js-lint js-test fi -found=0 -git diff --cached --name-only | grep -q '.go$' && found=1 -if [ $found == 1 ]; then - make lint-go || exit 1 - go test ./... || exit 1 +if [ "$(git diff --cached --name-only -- '*.go' 'go.mod')" ] +then + make go-lint go-test fi - -exit 0; diff --git a/.github/stale.yml b/.github/stale.yml index fa56e823..90e24289 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,19 +1,19 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - 'bug' - - 'enhancement' - - 'feature request' - - 'localization' -# Label to use when marking an issue as stale -staleLabel: 'wontfix' -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > +# Number of days of inactivity before an issue becomes stale. +'daysUntilStale': 60 +# Number of days of inactivity before a stale issue is closed. +'daysUntilClose': 7 +# Issues with these labels will never be considered stale. +'exemptLabels': +- 'bug' +- 'enhancement' +- 'feature request' +- 'localization' +# Label to use when marking an issue as stale. +'staleLabel': 'wontfix' +# Comment to post when marking an issue as stale. Set to `false` to disable. +'markComment': > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false +# Comment to post when closing a stale issue. Set to `false` to disable. +'closeComment': false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c599dbd..b2851e53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,170 +1,145 @@ -name: build +'name': 'build' -env: - GO_VERSION: 1.14 - NODE_VERSION: 13 +'env': + 'GO_VERSION': '1.14' + 'NODE_VERSION': '13' -on: - push: - branches: - - '*' - tags: - - v* - pull_request: +'on': + 'push': + 'branches': + - '*' + 'tags': + - 'v*' + 'pull_request': -jobs: +'jobs': + 'test': + 'runs-on': '${{ matrix.os }}' + 'env': + 'GO111MODULE': 'on' + 'GOPROXY': 'https://goproxy.io' + 'strategy': + 'fail-fast': false + 'matrix': + 'os': + - 'ubuntu-latest' + - 'macOS-latest' + - 'windows-latest' + 'steps': + - 'name': 'Checkout' + 'uses': 'actions/checkout@v2' + 'with': + 'fetch-depth': 0 + - 'name': 'Set up Go' + 'uses': 'actions/setup-go@v2' + 'with': + 'go-version': '${{ env.GO_VERSION }}' + - 'name': 'Set up Node' + 'uses': 'actions/setup-node@v1' + 'with': + 'node-version': '${{ env.NODE_VERSION }}' + - 'name': 'Set up Go modules cache' + 'uses': 'actions/cache@v2' + 'with': + 'path': '~/go/pkg/mod' + 'key': "${{ runner.os }}-go-${{ hashFiles('go.sum') }}" + 'restore-keys': '${{ runner.os }}-go-' + - 'name': 'Get npm cache directory' + 'id': 'npm-cache' + 'run': 'echo "::set-output name=dir::$(npm config get cache)"' + - 'name': 'Set up npm cache' + 'uses': 'actions/cache@v2' + 'with': + 'path': '${{ steps.npm-cache.outputs.dir }}' + 'key': "${{ runner.os }}-node-${{ hashFiles('client/package-lock.json') }}" + 'restore-keys': '${{ runner.os }}-node-' + - 'name': 'Run make ci' + 'shell': 'bash' + 'run': 'make ci' + - 'name': 'Upload coverage' + 'uses': 'codecov/codecov-action@v1' + 'if': "success() && matrix.os == 'ubuntu-latest'" + 'with': + 'token': '${{ secrets.CODECOV_TOKEN }}' + 'file': './coverage.txt' + 'app': + 'runs-on': 'ubuntu-latest' + 'needs': 'test' + 'steps': + - 'name': 'Checkout' + 'uses': 'actions/checkout@v2' + 'with': + 'fetch-depth': 0 + - 'name': 'Set up Go' + 'uses': 'actions/setup-go@v2' + 'with': + 'go-version': '${{ env.GO_VERSION }}' + - 'name': 'Set up Node' + 'uses': 'actions/setup-node@v1' + 'with': + 'node-version': '${{ env.NODE_VERSION }}' + - 'name': 'Set up Go modules cache' + 'uses': 'actions/cache@v2' + 'with': + 'path': '~/go/pkg/mod' + 'key': "${{ runner.os }}-go-${{ hashFiles('go.sum') }}" + 'restore-keys': '${{ runner.os }}-go-' + - 'name': 'Get npm cache directory' + 'id': 'npm-cache' + 'run': 'echo "::set-output name=dir::$(npm config get cache)"' + - 'name': 'Set up node_modules cache' + 'uses': 'actions/cache@v2' + 'with': + 'path': '${{ steps.npm-cache.outputs.dir }}' + 'key': "${{ runner.os }}-node-${{ hashFiles('client/package-lock.json') }}" + 'restore-keys': '${{ runner.os }}-node-' + - 'name': 'Set up Snapcraft' + 'run': 'sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft' + - 'name': 'Set up GoReleaser' + 'run': 'curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | BINDIR="$(go env GOPATH)/bin" sh' + - 'name': 'Run snapshot build' + 'run': 'make release' - test: - runs-on: ${{ matrix.os }} - env: - GO111MODULE: on - GOPROXY: https://goproxy.io - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - - macOS-latest - - windows-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: ${{ env.GO_VERSION }} + 'docker': + 'runs-on': 'ubuntu-latest' + 'needs': 'test' + 'steps': + - 'name': 'Checkout' + 'uses': 'actions/checkout@v2' + 'with': + 'fetch-depth': 0 + - 'name': 'Set up QEMU' + 'uses': 'docker/setup-qemu-action@v1' + - 'name': 'Set up Docker Buildx' + 'uses': 'docker/setup-buildx-action@v1' + - 'name': 'Docker Buildx (build)' + 'run': 'make docker-multi-arch' - - - name: Set up Node - uses: actions/setup-node@v1 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Set up Go modules cache - uses: actions/cache@v2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: Get npm cache directory - id: npm-cache - run: | - echo "::set-output name=dir::$(npm config get cache)" - - - name: Set up npm cache - uses: actions/cache@v2 - with: - path: ${{ steps.npm-cache.outputs.dir }} - key: ${{ runner.os }}-node-${{ hashFiles('client/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Run make ci - shell: bash - run: | - make ci - - - name: Upload coverage - uses: codecov/codecov-action@v1 - if: success() && matrix.os == 'ubuntu-latest' - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.txt - - app: - runs-on: ubuntu-latest - needs: test - steps: - - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Set up Node - uses: actions/setup-node@v1 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Set up Go modules cache - uses: actions/cache@v2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: Get npm cache directory - id: npm-cache - run: | - echo "::set-output name=dir::$(npm config get cache)" - - - name: Set up node_modules cache - uses: actions/cache@v2 - with: - path: ${{ steps.npm-cache.outputs.dir }} - key: ${{ runner.os }}-node-${{ hashFiles('client/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Set up Snapcraft - run: | - sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft - - - name: Set up GoReleaser - run: | - curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | BINDIR="$(go env GOPATH)/bin" sh - - - name: Run snapshot build - run: | - make release - - docker: - runs-on: ubuntu-latest - needs: test - steps: - - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Docker Buildx (build) - run: | - make docker-multi-arch - - notify: - needs: [app, docker] - # Secrets are not passed to workflows that are triggered by a pull request from a fork - if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} - runs-on: ubuntu-latest - steps: - - - name: Conclusion - uses: technote-space/workflow-conclusion-action@v1 - - - name: Send Slack notif - uses: 8398a7/action-slack@v3 - with: - status: ${{ env.WORKFLOW_CONCLUSION }} - fields: repo,message,commit,author - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + 'notify': + 'needs': + - 'app' + - 'docker' + # Secrets are not passed to workflows that are triggered by a pull request + # from a fork. + # + # Use always() to signal to the runner that this job must run even if the + # previous ones failed. + 'if': + ${{ always() && + ( + github.event_name == 'push' || + github.event.pull_request.head.repo.full_name == github.repository + ) + }} + 'runs-on': 'ubuntu-latest' + 'steps': + - 'name': 'Conclusion' + 'uses': 'technote-space/workflow-conclusion-action@v1' + - 'name': 'Send Slack notif' + 'uses': '8398a7/action-slack@v3' + 'with': + 'status': '${{ env.WORKFLOW_CONCLUSION }}' + 'fields': 'repo, message, commit, author, job' + 'env': + 'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}' + 'SLACK_WEBHOOK_URL': '${{ secrets.SLACK_WEBHOOK_URL }}' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2ddf108f..d003dee9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,47 +1,52 @@ -name: golangci-lint -on: - push: - tags: - - v* - branches: - - '*' - pull_request: -jobs: - golangci: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: golangci-lint - uses: golangci/golangci-lint-action@v1 - with: - # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.27 - - eslint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Install modules - run: npm --prefix client ci - - name: Run ESLint - run: npm --prefix client run lint - - - notify: - needs: [golangci,eslint] - # Secrets are not passed to workflows that are triggered by a pull request from a fork - if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} - runs-on: ubuntu-latest - steps: - - - name: Conclusion - uses: technote-space/workflow-conclusion-action@v1 - - - name: Send Slack notif - uses: 8398a7/action-slack@v3 - with: - status: ${{ env.WORKFLOW_CONCLUSION }} - fields: repo,message,commit,author - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} +'name': 'lint' +'on': + 'push': + 'tags': + - 'v*' + 'branches': + - '*' + 'pull_request': +'jobs': + 'go-lint': + 'runs-on': 'ubuntu-latest' + 'steps': + - 'uses': 'actions/checkout@v2' + - 'name': 'run-lint' + 'run': > + make go-install-tools go-lint + 'eslint': + 'runs-on': 'ubuntu-latest' + 'steps': + - 'uses': 'actions/checkout@v2' + - 'name': 'Install modules' + 'run': 'npm --prefix client ci' + - 'name': 'Run ESLint' + 'run': 'npm --prefix client run lint' + 'notify': + 'needs': + - 'go-lint' + - 'eslint' + # Secrets are not passed to workflows that are triggered by a pull request + # from a fork. + # + # Use always() to signal to the runner that this job must run even if the + # previous ones failed. + 'if': + ${{ always() && + ( + github.event_name == 'push' || + github.event.pull_request.head.repo.full_name == github.repository + ) + }} + 'runs-on': 'ubuntu-latest' + 'steps': + - 'name': 'Conclusion' + 'uses': 'technote-space/workflow-conclusion-action@v1' + - 'name': 'Send Slack notif' + 'uses': '8398a7/action-slack@v3' + 'with': + 'status': '${{ env.WORKFLOW_CONCLUSION }}' + 'fields': 'repo, message, commit, author, job' + 'env': + 'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}' + 'SLACK_WEBHOOK_URL': '${{ secrets.SLACK_WEBHOOK_URL }}' diff --git a/.gitignore b/.gitignore index e7f55736..beba3ce1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,22 @@ -.DS_Store -/.vscode -.idea -/AdGuardHome -/AdGuardHome.exe -/AdGuardHome.yaml -/AdGuardHome.log -/data/ -/build/ -/dist/ -/client/node_modules/ -/querylog.json -/querylog.json.1 -coverage.txt - -# Test output -dnsfilter/tests/top-1m.csv -dnsfilter/tests/dnsfilter.TestLotsOfRules*.pprof - -# Snapcraft build temporary files -*.snap -launchpad_credentials -snapcraft_login -snapcraft.yaml.bak - -# IntelliJ IDEA project files -*.iml - -# Packr +# Please, DO NOT put your text editors' temporary files here. The more are +# added, the harder it gets to maintain and manage projects' gitignores. Put +# them into your global gitignore file instead. +# +# See https://stackoverflow.com/a/7335487/1892060. +# +# Only build, run, and test outputs here. Sorted. *-packr.go +*.db +*.snap +/bin/ +/build/ +/data/ +/dist/ +/dnsfilter/tests/dnsfilter.TestLotsOfRules*.pprof +/dnsfilter/tests/top-1m.csv +/launchpad_credentials +/querylog.json* +/snapcraft_login +AdGuardHome* +coverage.txt +node_modules/ diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index be43ec74..00000000 --- a/.golangci.yml +++ /dev/null @@ -1,79 +0,0 @@ -# options for analysis running -run: - # default concurrency is a available CPU number - concurrency: 4 - - # timeout for analysis, e.g. 30s, 5m, default is 1m - deadline: 2m - - # which files to skip: they will be analyzed, but issues from them - # won't be reported. Default value is empty list, but there is - # no need to include all autogenerated files, we confidently recognize - # autogenerated files. If it's not please let us know. - skip-files: - - ".*generated.*" - - dnsfilter/rule_to_regexp.go - - util/pprof.go - - ".*_test.go" - - client/.* - - build/.* - - dist/.* - - -# all available settings of specific linters -linters-settings: - errcheck: - # [deprecated] comma-separated list of pairs of the form pkg:regex - # the regex is used to ignore names within pkg. (default "fmt:.*"). - # see https://github.com/kisielk/errcheck#the-deprecated-method for details - ignore: fmt:.*,net:SetReadDeadline,net/http:^Write - gocyclo: - min-complexity: 20 - lll: - line-length: 200 - -linters: - enable: - - deadcode - - errcheck - - govet - - ineffassign - - staticcheck - - structcheck - - unused - - varcheck - - bodyclose - - depguard - - dupl - - gocyclo - - goimports - - golint - - gosec - - misspell - - stylecheck - - unconvert - disable-all: true - fast: true - -issues: - # List of regexps of issue texts to exclude, empty list by default. - # But independently from this option we use default exclude patterns, - # it can be disabled by `exclude-use-default: false`. To list all - # excluded by default patterns execute `golangci-lint run --help` - exclude: - # structcheck cannot detect usages while they're there - - .parentalServer. is unused - - .safeBrowsingServer. is unused - # errcheck - - Error return value of .s.closeConn. is not checked - - Error return value of ..*.Shutdown. - # goconst - - string .forcesafesearch.google.com. has 3 occurrences - # gosec: Profiling endpoint is automatically exposed on /debug/pprof - - G108 - # gosec: Subprocess launched with function call as argument or cmd arguments - - G204 - # gosec: Potential DoS vulnerability via decompression bomb - - G110 - # gosec: Expect WriteFile permissions to be 0600 or less - - G306 diff --git a/.goreleaser.yml b/.goreleaser.yml index edb4bd2b..c533712b 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,106 +1,115 @@ -project_name: AdGuardHome +'project_name': 'AdGuardHome' -env: - - GO111MODULE=on - - GOPROXY=https://goproxy.io +'env': +- 'GO111MODULE=on' +- 'GOPROXY=https://goproxy.io' -before: - hooks: - - go mod download - - go generate ./... +'before': + 'hooks': + - 'go mod download' + - 'go generate ./...' -builds: - - main: ./main.go - ldflags: - - -s -w -X main.version={{.Version}} -X main.channel={{.Env.CHANNEL}} -X main.goarm={{.Env.GOARM}} - env: - - CGO_ENABLED=0 - goos: - - darwin - - linux - - freebsd - - windows - goarch: - - 386 - - amd64 - - arm - - arm64 - - mips - - mipsle - - mips64 - - mips64le - goarm: - - 5 - - 6 - - 7 - gomips: - - softfloat - ignore: - - goos: freebsd - goarch: mips - - goos: freebsd - goarch: mipsle +'builds': +- 'main': './main.go' + 'ldflags': + - '-s -w -X main.version={{.Version}} -X main.channel={{.Env.CHANNEL}} -X main.goarm={{.Env.GOARM}}' + 'env': + - 'CGO_ENABLED=0' + 'goos': + - 'darwin' + - 'linux' + - 'freebsd' + - 'windows' + 'goarch': + - '386' + - 'amd64' + - 'arm' + - 'arm64' + - 'mips' + - 'mipsle' + - 'mips64' + - 'mips64le' + 'goarm': + - '5' + - '6' + - '7' + 'gomips': + - 'softfloat' + 'ignore': + - 'goos': 'freebsd' + 'goarch': 'mips' + - 'goos': 'freebsd' + 'goarch': 'mipsle' -archives: - - # Archive name template. - # Defaults: - # - if format is `tar.gz`, `tar.xz`, `gz` or `zip`: - # - `{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}` - # - if format is `binary`: - # - `{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}` - name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" - wrap_in_directory: "AdGuardHome" - format_overrides: - - goos: windows - format: zip - - goos: darwin - format: zip - files: - - LICENSE.txt - - README.md +'archives': +- # Archive name template. + # Defaults: + # - if format is `tar.gz`, `tar.xz`, `gz` or `zip`: + # - `{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}` + # - if format is `binary`: + # - `{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}` + 'name_template': '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}' + 'wrap_in_directory': 'AdGuardHome' + 'format_overrides': + - 'goos': 'windows' + 'format': 'zip' + - 'goos': 'darwin' + 'format': 'zip' + 'files': + - 'LICENSE.txt' + - 'README.md' -snapcrafts: - - name: adguard-home - base: core18 - name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' - summary: Network-wide ads & trackers blocking DNS server - description: | - AdGuard Home is a network-wide software for blocking ads & tracking. After - you set it up, it'll cover ALL your home devices, and you don't need any - client-side software for that. +'snapcrafts': +- 'name': 'adguard-home' + 'base': 'core20' + 'name_template': '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + 'summary': 'Network-wide ads & trackers blocking DNS server' + 'description': | + AdGuard Home is a network-wide software for blocking ads & tracking. After + you set it up, it'll cover ALL your home devices, and you don't need any + client-side software for that. - It operates as a DNS server that re-routes tracking domains to a "black hole," - thus preventing your devices from connecting to those servers. It's based - on software we use for our public AdGuard DNS servers -- both share a lot - of common code. - grade: stable - confinement: strict - publish: false - license: GPL-3.0 - extra_files: - - source: scripts/snap/local/adguard-home-web.sh - destination: adguard-home-web.sh - mode: 0755 - - source: scripts/snap/gui/adguard-home-web.desktop - destination: meta/gui/adguard-home-web.desktop - mode: 0644 - - source: scripts/snap/gui/adguard-home-web.png - destination: meta/gui/adguard-home-web.png - mode: 0644 - apps: - adguard-home: - command: AdGuardHome -w $SNAP_DATA --no-check-update - plugs: - # Add the "netrwork-bind" plug to bind to interfaces. - - network-bind - # Add the "netrwork-control" plug to be able to bind to ports below - # 1024 (cap_net_bind_service) and also to bind to a particular - # interface using SO_BINDTODEVICE (cap_net_raw). - - network-control - daemon: simple - adguard-home-web: - command: adguard-home-web.sh - plugs: [ desktop ] + It operates as a DNS server that re-routes tracking domains to a "black hole," + thus preventing your devices from connecting to those servers. It's based + on software we use for our public AdGuard DNS servers -- both share a lot + of common code. + 'grade': 'stable' + 'confinement': 'strict' + 'publish': false + 'license': 'GPL-3.0' + 'extra_files': + - 'source': 'scripts/snap/local/adguard-home-web.sh' + 'destination': 'adguard-home-web.sh' + 'mode': 0755 + - 'source': 'scripts/snap/gui/adguard-home-web.desktop' + 'destination': 'meta/gui/adguard-home-web.desktop' + 'mode': 0644 + - 'source': 'scripts/snap/gui/adguard-home-web.png' + 'destination': 'meta/gui/adguard-home-web.png' + 'mode': 0644 + 'apps': + 'adguard-home': + 'command': 'AdGuardHome -w $SNAP_DATA --no-check-update' + 'plugs': + # Add the "netrwork-bind" plug to bind to interfaces. + - 'network-bind' + # Add the "netrwork-observe" plug to be able to bind to ports below 1024 + # (cap_net_bind_service) and also to bind to a particular interface using + # SO_BINDTODEVICE (cap_net_raw). + - 'network-observe' + 'daemon': 'simple' + 'adguard-home-web': + 'command': 'adguard-home-web.sh' + 'plugs': + - 'desktop' -checksum: - name_template: 'checksums.txt' +'checksum': + 'name_template': 'checksums.txt' + +'snapshot': + # TODO(a.garipov): A temporary solution to trim the prerelease versions. + # A real solution would consist of making a better versioning scheme that also + # doesn't break SemVer or Snapcraft. + # + # See https://github.com/AdguardTeam/AdGuardHome/issues/2412. + 'name_template': '{{ slice .Tag 0 8 }}-SNAPSHOT-{{ .ShortCommit }}' diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 45dee068..b91b8586 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -1833,16 +1833,22 @@ Response: 200 OK { - "reason":"FilteredBlackList", - "filter_id":1, - "rule":"||doubleclick.net^", - "service_name": "...", // set if reason=FilteredBlockedService - - // if reason=ReasonRewrite: - "cname": "...", - "ip_addrs": ["1.2.3.4", ...], + "reason":"FilteredBlackList", + "rules":{ + "filter_list_id":42, + "text":"||doubleclick.net^", + }, + // If we have "reason":"FilteredBlockedService". + "service_name": "...", + // If we have "reason":"Rewrite". + "cname": "...", + "ip_addrs": ["1.2.3.4", ...] } +There are also deprecated properties `filter_id` and `rule` on the top level of +the response object. Their usaga should be replaced with +`rules[*].filter_list_id` and `rules[*].text` correspondingly. See the +_OpenAPI_ documentation and the `./openapi/CHANGELOG.md` file. ## Log-in page diff --git a/CHANGELOG.md b/CHANGELOG.md index b7e8f4d5..e63c913c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,17 +9,115 @@ and this project adheres to ## [Unreleased] + + ### Added -- This changelog :-) (#2294). +- `$dnsrewrite` modifier for filters ([#2102]). +- The host checking API and the query logs API can now return multiple matched + rules ([#2102]). +- Detecting of network interface configured to have static IP address via + `/etc/network/interfaces` ([#2302]). +- DNSCrypt protocol support ([#1361]). +- A 5 second wait period until a DHCP server's network interface gets an IP + address ([#2304]). +- `$dnstype` modifier for filters ([#2337]). +- HTTP API request body size limit ([#2305]). + +[#1361]: https://github.com/AdguardTeam/AdGuardHome/issues/1361 +[#2102]: https://github.com/AdguardTeam/AdGuardHome/issues/2102 +[#2302]: https://github.com/AdguardTeam/AdGuardHome/issues/2302 +[#2304]: https://github.com/AdguardTeam/AdGuardHome/issues/2304 +[#2305]: https://github.com/AdguardTeam/AdGuardHome/issues/2305 +[#2337]: https://github.com/AdguardTeam/AdGuardHome/issues/2337 + +### Changed + +- When `dns.bogus_nxdomain` option is used, the server will now transform + responses if there is at least one bogus address instead of all of them + ([#2394]). The new behavior is the same as in `dnsmasq`. +- Post-updating relaunch possibility is now determined OS-dependently ([#2231], + [#2391]). +- Made the mobileconfig HTTP API more robust and predictable, add parameters and + improve error response ([#2358]). +- Improved HTTP requests handling and timeouts ([#2343]). +- Our snap package now uses the `core20` image as its base ([#2306]). +- Various internal improvements ([#2267], [#2271], [#2297]). + +[#2231]: https://github.com/AdguardTeam/AdGuardHome/issues/2231 +[#2267]: https://github.com/AdguardTeam/AdGuardHome/issues/2267 +[#2271]: https://github.com/AdguardTeam/AdGuardHome/issues/2271 +[#2297]: https://github.com/AdguardTeam/AdGuardHome/issues/2297 +[#2306]: https://github.com/AdguardTeam/AdGuardHome/issues/2306 +[#2343]: https://github.com/AdguardTeam/AdGuardHome/issues/2343 +[#2358]: https://github.com/AdguardTeam/AdGuardHome/issues/2358 +[#2391]: https://github.com/AdguardTeam/AdGuardHome/issues/2391 +[#2394]: https://github.com/AdguardTeam/AdGuardHome/issues/2394 + +### Fixed + +- Inability to set DNS cache TTL limits ([#2459]). +- Possible freezes on slower machines ([#2225]). +- A mitigation against records being shown in the wrong order on the query log + page ([#2293]). +- A JSON parsing error in query log ([#2345]). +- Incorrect detection of the IPv6 address of an interface as well as another + infinite loop in the `/dhcp/find_active_dhcp` HTTP API ([#2355]). + +[#2225]: https://github.com/AdguardTeam/AdGuardHome/issues/2225 +[#2293]: https://github.com/AdguardTeam/AdGuardHome/issues/2293 +[#2345]: https://github.com/AdguardTeam/AdGuardHome/issues/2345 +[#2355]: https://github.com/AdguardTeam/AdGuardHome/issues/2355 +[#2459]: https://github.com/AdguardTeam/AdGuardHome/issues/2459 + +### Removed + +- Support for pre-v0.99.3 format of query logs ([#2102]). + +## [v0.104.3] - 2020-11-19 + +### Fixed + +- The accidentally exposed profiler HTTP API ([#2336]). + +[#2336]: https://github.com/AdguardTeam/AdGuardHome/issues/2336 + + + +## [v0.104.2] - 2020-11-19 + +### Added + +- This changelog :-) ([#2294]). - `HACKING.md`, a guide for developers. ### Changed -- Improved tests output (#2273). +- Improved tests output ([#2273]). ### Fixed +- Query logs from file not loading after the ones buffered in memory ([#2325]). +- Unnecessary errors in query logs when switching between log files ([#2324]). - `404 Not Found` errors on the DHCP settings page on *Windows*. The page now - correctly shows that DHCP is not currently available on that OS (#2295). -- Infinite loop in `/dhcp/find_active_dhcp` (#2301). + correctly shows that DHCP is not currently available on that OS ([#2295]). +- Infinite loop in `/dhcp/find_active_dhcp` ([#2301]). + +[#2273]: https://github.com/AdguardTeam/AdGuardHome/issues/2273 +[#2294]: https://github.com/AdguardTeam/AdGuardHome/issues/2294 +[#2295]: https://github.com/AdguardTeam/AdGuardHome/issues/2295 +[#2301]: https://github.com/AdguardTeam/AdGuardHome/issues/2301 +[#2324]: https://github.com/AdguardTeam/AdGuardHome/issues/2324 +[#2325]: https://github.com/AdguardTeam/AdGuardHome/issues/2325 + + + + +[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.3...HEAD +[v0.104.3]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.2...v0.104.3 +[v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.1...v0.104.2 diff --git a/HACKING.md b/HACKING.md index 8df23efd..a16d6496 100644 --- a/HACKING.md +++ b/HACKING.md @@ -1,10 +1,17 @@ - # AdGuardHome Developer Guidelines + # *AdGuardHome* Developer Guidelines -As of **2020-11-12**, this document is still a work-in-progress. Some of the -rules aren't enforced, and others might change. Still, this is a good place to -find out about how we **want** our code to look like. +As of **December 2020**, this document is partially a work-in-progress, but +should still be followed. Some of the rules aren't enforced as thoroughly or +remain broken in old code, but this is still the place to find out about what we +**want** our code to look like. -## Git +The rules are mostly sorted in the alphabetical order. + +## *Git* + + * Call your branches either `NNNN-fix-foo` (where `NNNN` is the ID of the + *GitHub* issue you worked on in this branch) or just `fix-foo` if there was + no *GitHub* issue. * Follow the commit message header format: @@ -13,28 +20,33 @@ find out about how we **want** our code to look like. ``` Where `pkg` is the package where most changes took place. If there are - several such packages, just write `all`. + several such packages, or the change is top-level only, write `all`. - * Keep your commit messages to be no wider than eighty (**80**) columns. + * Keep your commit messages, including headers, to eighty (**80**) columns. - * Only use lowercase letters in your commit message headers. + * Only use lowercase letters in your commit message headers. The rest of the + message should follow the plain text conventions below. -## Go + The only exceptions are direct mentions of identifiers from the source code + and filenames like `HACKING.md`. - * . +## *Go* - * . +> Not Golang, not GO, not GOLANG, not GoLang. It is Go in natural language, +> golang for others. - * +— [@rakyll](https://twitter.com/rakyll/status/1229850223184269312) + + ### Code And Naming + + * Avoid `goto`. * Avoid `init` and use explicit initialization functions instead. * Avoid `new`, especially with structs. - * Document everything, including unexported top-level identifiers, to build - a habit of writing documentation. - - * Don't put variable names into any kind of quotes. + * Constructors should validate their arguments and return meaningful errors. + As a corollary, avoid lazy initialization. * Don't use naked `return`s. @@ -48,7 +60,17 @@ find out about how we **want** our code to look like. * Eschew external dependencies, including transitive, unless absolutely necessary. - * No `goto`. + * Name benchmarks and tests using the same convention as examples. For + example: + + ```go + func TestFunction(t *testing.T) { /* … */ } + func TestFunction_suffix(t *testing.T) { /* … */ } + func TestType_Method(t *testing.T) { /* … */ } + func TestType_Method_suffix(t *testing.T) { /* … */ } + ``` + + * Name the deferred errors (e.g. when closing something) `cerr`. * No shadowing, since it can often lead to subtle bugs, especially with errors. @@ -56,29 +78,61 @@ find out about how we **want** our code to look like. * Prefer constants to variables where possible. Reduce global variables. Use [constant errors] instead of `errors.New`. - * Put comments above the documented entity, **not** to the side, to improve - readability. + * Unused arguments in anonymous functions must be called `_`: - * Use `gofumpt --extra -s`. - - **TODO(a.garipov):** Add to the linters. + ```go + v.onSuccess = func(_ int, msg string) { + // … + } + ``` * Use linters. * Use named returns to improve readability of function signatures. + * Write logs and error messages in lowercase only to make it easier to `grep` + logs and error messages without using the `-i` flag. + +[constant errors]: https://dave.cheney.net/2016/04/07/constant-errors +[Linus said]: https://www.kernel.org/doc/html/v4.17/process/coding-style.html#indentation + + ### Commenting + + * See also the *Text, Including Comments* section below. + + * Document everything, including unexported top-level identifiers, to build + a habit of writing documentation. + + * Don't put identifiers into any kind of quotes. + + * Put comments above the documented entity, **not** to the side, to improve + readability. + * When a method implements an interface, start the doc comment with the standard template: ```go // Foo implements the Fooer interface for *foo. func (f *foo) Foo() { - // … + // … } ``` - * Write logs and error messages in lowercase only to make it easier to `grep` - logs and error messages without using the `-i` flag. + When the implemented interface is unexported: + + ```go + // Unwrap implements the hidden wrapper interface for *fooError. + func (err *fooError) Unwrap() (unwrapped error) { + // … + } + ``` + + ### Formatting + + * Add an empty line before `break`, `continue`, `fallthrough`, and `return`, + unless it's the only statement in that block. + + * Use `gofumpt --extra -s`. * Write slices of struct like this: @@ -95,11 +149,64 @@ find out about how we **want** our code to look like. }} ``` -[constant errors]: https://dave.cheney.net/2016/04/07/constant-errors -[Linus said]: https://www.kernel.org/doc/html/v4.17/process/coding-style.html#indentation + ### Recommended Reading + + * . + + * . + + * + +## *Markdown* + + * **TODO(a.garipov):** Define our *Markdown* conventions. + +## Shell Scripting + + * Avoid bashisms and GNUisms, prefer *POSIX* features only. + + * Prefer `'raw strings'` to `"double quoted strings"` whenever possible. + + * Put spaces within `$( cmd )`, `$(( expr ))`, and `{ cmd; }`. + + * Put utility flags in the ASCII order and **don't** group them together. For + example, `ls -1 -A -q`. + + * `snake_case`, not `camelCase`. + + * Use `set -e -f -u` and also `set -x` in verbose mode. + + * Use the `"$var"` form instead of the `$var` form, unless word splitting is + required. + + * When concatenating, always use the form with curly braces to prevent + accidental bad variable names. That is, `"${var}_tmp.txt"` and **not** + `"$var_tmp.txt"`. The latter will try to lookup variable `var_tmp`. + + * When concatenating, surround the whole string with quotes. That is, use + this: + + ```sh + dir="${TOP_DIR}/sub" + ``` + + And **not** this: + + ```sh + # Bad! + dir="${TOP_DIR}"/sub + ``` ## Text, Including Comments + * End sentences with appropriate punctuation. + + * Headers should be written with all initial letters capitalized, except for + references to variable names that start with a lowercase letter. + + * Start sentences with a capital letter, unless the first word is a reference + to a variable name that starts with a lowercase letter. + * Text should wrap at eighty (**80**) columns to be more readable, to use a common standard, and to allow editing or diffing side-by-side without wrapping. @@ -111,7 +218,7 @@ find out about how we **want** our code to look like. * Use double spacing between sentences to make sentence borders more clear. - * Use the serial comma (a.k.a. Oxford comma) to improve comprehension, + * Use the serial comma (a.k.a. *Oxford* comma) to improve comprehension, decrease ambiguity, and use a common standard. * Write todos like this: @@ -126,17 +233,17 @@ find out about how we **want** our code to look like. // TODO(usr1, usr2): Fix the frobulation issue. ``` -## Markdown +## *YAML* - * **TODO(a.garipov):** Define our Markdown conventions. + * **TODO(a.garipov):** Define naming conventions for schema names in our + *OpenAPI* *YAML* file. And just generally OpenAPI conventions. -## YAML + * **TODO(a.garipov):** Find a *YAML* formatter or write our own. - * **TODO(a.garipov):** Find a YAML formatter or write our own. + * All strings, including keys, must be quoted. Reason: the [*NO-rway Law*]. - * All strings, including keys, must be quoted. Reason: the [NO-rway Law]. - - * Indent with two (**2**) spaces. + * Indent with two (**2**) spaces. *YAML* documents can get pretty + deeply-nested. * No extra indentation in multiline arrays: @@ -147,7 +254,10 @@ find out about how we **want** our code to look like. - 'value-3' ``` - * Prefer single quotes for string to prevent accidental escaping, unless - escaping is required. + * Prefer single quotes for strings to prevent accidental escaping, unless + escaping is required or there are single quotes inside the string (e.g. for + *GitHub Actions*). -[NO-rway Law]: https://news.ycombinator.com/item?id=17359376 + * Use `>` for multiline strings, unless you need to keep the line breaks. + +[*NO-rway Law*]: https://news.ycombinator.com/item?id=17359376 diff --git a/Makefile b/Makefile index 24805894..2bdee355 100644 --- a/Makefile +++ b/Makefile @@ -26,13 +26,16 @@ # * DOCKER_IMAGE_NAME - adguard/adguard-home # * DOCKER_OUTPUT - type=image,name=adguard/adguard-home,push=true -GOPATH := $(shell go env GOPATH) +GO := go +GOPATH := $(shell $(GO) env GOPATH) PWD := $(shell pwd) TARGET=AdGuardHome BASE_URL="https://static.adguard.com/adguardhome/$(CHANNEL)" GPG_KEY := devteam@adguard.com GPG_KEY_PASSPHRASE := GPG_CMD := gpg --detach-sig --default-key $(GPG_KEY) --pinentry-mode loopback --passphrase $(GPG_KEY_PASSPHRASE) +VERBOSE := -v +REBUILD_CLIENT = 1 # See release target DIST_DIR=dist @@ -64,7 +67,9 @@ endif # Version properties COMMIT=$(shell git rev-parse --short HEAD) -TAG_NAME=$(shell git describe --abbrev=0) +# TODO(a.garipov): The cut call is a temporary solution to trim +# prerelease versions. See the comment in .goreleaser.yml. +TAG_NAME=$(shell git describe --abbrev=0 | cut -c 1-8) RELEASE_VERSION=$(TAG_NAME) SNAPSHOT_VERSION=$(RELEASE_VERSION)-SNAPSHOT-$(COMMIT) @@ -109,7 +114,7 @@ $(error DOCKER_IMAGE_NAME value is not set) endif # OS-specific flags -TEST_FLAGS := --race -v +TEST_FLAGS := --race $(VERBOSE) ifeq ($(OS),Windows_NT) TEST_FLAGS := endif @@ -120,10 +125,11 @@ all: build init: git config core.hooksPath .githooks -build: client_with_deps - go mod download - PATH=$(GOPATH)/bin:$(PATH) go generate ./... - CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(VERSION) -X main.channel=$(CHANNEL) -X main.goarm=$(GOARM)" +build: + test '$(REBUILD_CLIENT)' = '1' && $(MAKE) client_with_deps || exit 0 + $(GO) mod download + PATH=$(GOPATH)/bin:$(PATH) $(GO) generate ./... + CGO_ENABLED=0 $(GO) build -ldflags="-s -w -X main.version=$(VERSION) -X main.channel=$(CHANNEL) -X main.goarm=$(GOARM)" PATH=$(GOPATH)/bin:$(PATH) packr clean client: @@ -150,47 +156,40 @@ docker: @echo Now you can run the docker image: @echo docker run --name "adguard-home" -p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 443:443/tcp -p 853:853/tcp -p 3000:3000/tcp $(DOCKER_IMAGE_NAME) -lint: lint-js lint-go +lint: js-lint go-lint -lint-js: dependencies - @echo Running js linter +js-lint: dependencies npm --prefix client run lint -lint-go: - @echo Running go linter - golangci-lint run +go-install-tools: + env GO=$(GO) sh ./scripts/go-install-tools.sh -test: test-js test-go +go-lint: + env GO=$(GO) PATH="$$PWD/bin:$$PATH" sh ./scripts/go-lint.sh -test-js: +test: js-test go-test + +js-test: npm run test --prefix client -test-go: - go test $(TEST_FLAGS) --coverprofile coverage.txt ./... +go-test: + $(GO) test $(TEST_FLAGS) --coverprofile coverage.txt ./... ci: client_with_deps - go mod download + $(GO) mod download $(MAKE) test dependencies: npm --prefix client ci - go mod download + $(GO) mod download clean: - # make build output - rm -f AdGuardHome - rm -f AdGuardHome.exe - # tests output - rm -rf data - rm -f coverage.txt - # static build output - rm -rf build - # dist folder - rm -rf $(DIST_DIR) - # client deps - rm -rf client/node_modules - # packr-generated files - PATH=$(GOPATH)/bin:$(PATH) packr clean || true + rm -f ./AdGuardHome ./AdGuardHome.exe ./coverage.txt + rm -f -r ./build/ ./client/node_modules/ ./data/ ./$(DIST_DIR)/ +# Set the GOPATH explicitly in case make clean is called from under sudo +# after a Docker build. + env PATH="$(GOPATH)/bin:$$PATH" packr clean + rm -f -r ./bin/ docker-multi-arch: DOCKER_CLI_EXPERIMENTAL=enabled \ @@ -208,7 +207,7 @@ docker-multi-arch: @echo docker run --name "adguard-home" -p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 443:443/tcp -p 853:853/tcp -p 3000:3000/tcp $(DOCKER_IMAGE_NAME) release: client_with_deps - go mod download + $(GO) mod download @echo Starting release build: version $(VERSION), channel $(CHANNEL) CHANNEL=$(CHANNEL) $(GORELEASER_COMMAND) $(call write_version_file,$(VERSION)) diff --git a/README.md b/README.md index 4c352000..5af39b8e 100644 --- a/README.md +++ b/README.md @@ -171,9 +171,6 @@ You will need this to build AdGuard Home: * [node.js](https://nodejs.org/en/download/) v10.16.2 or later. * [npm](https://www.npmjs.com/) v6.14 or later. -Optionally, for Go devs: - * [golangci-lint](https://github.com/golangci/golangci-lint) - ### Building Open Terminal and execute these commands: @@ -186,7 +183,7 @@ make Check the [`Makefile`](https://github.com/AdguardTeam/AdGuardHome/blob/master/Makefile) to learn about other commands. -**Building for a different platform.** You can build AdGuard for any OS/ARCH just like any other Golang project. +**Building for a different platform.** You can build AdGuard for any OS/ARCH just like any other Go project. In order to do this, specify `GOOS` and `GOARCH` env variables before running make. For example: @@ -258,7 +255,7 @@ curl -sSL https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scrip * Beta channel builds * Linux: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz) - * Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz) (recommended for Rapsberry Pi), [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz) + * Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz) (recommended for Raspberry Pi), [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz) * Linux MIPS: [32-bit MIPS](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz), [32-bit MIPSLE](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz), [64-bit MIPS](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz), [64-bit MIPSLE](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz) * Windows: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_386.zip) * MacOS: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_386.zip) @@ -267,7 +264,7 @@ curl -sSL https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scrip * Edge channel builds * Linux: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_386.tar.gz) - * Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv6.tar.gz) (recommended for Rapsberry Pi), [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv7.tar.gz) + * Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv6.tar.gz) (recommended for Raspberry Pi), [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv7.tar.gz) * Linux MIPS: [32-bit MIPS](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips_softfloat.tar.gz), [32-bit MIPSLE](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mipsle_softfloat.tar.gz), [64-bit MIPS](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips64_softfloat.tar.gz), [64-bit MIPSLE](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips64le_softfloat.tar.gz) * Windows: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_windows_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_windows_386.zip) * MacOS: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_386.zip) @@ -331,4 +328,4 @@ For a full list of all node.js packages in use, please take a look at [client/pa ## Privacy -Our main idea is that you are the one, who should be in control of your data. So it is only natural, that AdGuard Home does not collect any usage statistics, and does not use any web services unless you configure it to do so. Full policy with every bit that _could in theory be_ sent by AdGuard Home is available [here](https://adguard.com/en/privacy/home.html). \ No newline at end of file +Our main idea is that you are the one, who should be in control of your data. So it is only natural, that AdGuard Home does not collect any usage statistics, and does not use any web services unless you configure it to do so. Full policy with every bit that _could in theory be_ sent by AdGuard Home is available [here](https://adguard.com/en/privacy/home.html). diff --git a/client/src/__tests__/helpers.test.js b/client/src/__tests__/helpers.test.js index 309b40e6..a5ed121b 100644 --- a/client/src/__tests__/helpers.test.js +++ b/client/src/__tests__/helpers.test.js @@ -273,15 +273,15 @@ describe('sortIp', () => { }); }); describe('invalid input', () => { - const originalError = console.error; + const originalWarn = console.warn; beforeEach(() => { - console.error = jest.fn(); + console.warn = jest.fn(); }); afterEach(() => { - expect(console.error).toHaveBeenCalled(); - console.error = originalError; + expect(console.warn).toHaveBeenCalled(); + console.warn = originalWarn; }); test('invalid strings', () => { diff --git a/client/src/components/Filters/Rewrites/Table.js b/client/src/components/Filters/Rewrites/Table.js index 5bc1f678..45638ec0 100644 --- a/client/src/components/Filters/Rewrites/Table.js +++ b/client/src/components/Filters/Rewrites/Table.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ReactTable from 'react-table'; import { withTranslation } from 'react-i18next'; +import { sortIp } from '../../../helpers/helpers'; class Table extends Component { cellWrap = ({ value }) => ( @@ -21,6 +22,7 @@ class Table extends Component { { Header: this.props.t('answer'), accessor: 'answer', + sortMethod: sortIp, Cell: this.cellWrap, }, { diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index 28f8a59c..20f1f828 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -259,7 +259,7 @@ let Form = (props) => {
link, ]}> tags_desc diff --git a/client/src/components/Settings/Dhcp/Interfaces.js b/client/src/components/Settings/Dhcp/Interfaces.js index 987a84a9..d2760e3d 100644 --- a/client/src/components/Settings/Dhcp/Interfaces.js +++ b/client/src/components/Settings/Dhcp/Interfaces.js @@ -72,12 +72,12 @@ const Interfaces = () => { (store) => store.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name, ); - const interfaceValue = interface_name && interfaces[interface_name]; - if (processingInterfaces || !interfaces) { return null; } + const interfaceValue = interface_name && interfaces[interface_name]; + return
{ const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName; const getToggleDhcpButton = () => { - const otherDhcpFound = check && (check.v4.other_server.found === STATUS_RESPONSE.YES - || check.v6.other_server.found === STATUS_RESPONSE.YES); - const filledConfig = interface_name && (Object.values(v4) .every(Boolean) || Object.values(v6) .every(Boolean)); @@ -141,7 +138,7 @@ const Dhcp = () => { className={className} onClick={enabled ? onClickDisable : onClickEnable} disabled={processingDhcp || processingConfig - || (!enabled && (!filledConfig || !check || otherDhcpFound))} + || (!enabled && (!filledConfig || !check))} > {enabled ? 'dhcp_disable' : 'dhcp_enable'} ; diff --git a/client/src/components/Settings/Encryption/Form.js b/client/src/components/Settings/Encryption/Form.js index 8d69f665..1619ea3e 100644 --- a/client/src/components/Settings/Encryption/Form.js +++ b/client/src/components/Settings/Encryption/Form.js @@ -232,7 +232,7 @@ let Form = (props) => { + link , ]} diff --git a/client/src/components/Settings/FiltersConfig/Form.js b/client/src/components/Settings/FiltersConfig/Form.js index 7d41fe8a..d76631ea 100644 --- a/client/src/components/Settings/FiltersConfig/Form.js +++ b/client/src/components/Settings/FiltersConfig/Form.js @@ -7,7 +7,7 @@ import flow from 'lodash/flow'; import { CheckboxField, toNumber } from '../../../helpers/form'; import { FILTERS_INTERVALS_HOURS, - FILTERS_LINK, + FILTERS_RELATIVE_LINK, FORM_NAME, } from '../../../helpers/constants'; @@ -45,7 +45,7 @@ const Form = (props) => { } = props; const components = { - a: , + a: , }; return ( diff --git a/client/src/components/ui/EncryptionTopline.js b/client/src/components/ui/EncryptionTopline.js index b013130c..6ff18136 100644 --- a/client/src/components/ui/EncryptionTopline.js +++ b/client/src/components/ui/EncryptionTopline.js @@ -6,6 +6,50 @@ import { useSelector } from 'react-redux'; import Topline from './Topline'; import { EMPTY_DATE } from '../../helpers/constants'; +const EXPIRATION_ENUM = { + VALID: 'VALID', + EXPIRED: 'EXPIRED', + EXPIRING: 'EXPIRING', +}; + +const EXPIRATION_STATE = { + [EXPIRATION_ENUM.EXPIRED]: { + toplineType: 'danger', + i18nKey: 'topline_expired_certificate', + }, + [EXPIRATION_ENUM.EXPIRING]: { + toplineType: 'warning', + i18nKey: 'topline_expiring_certificate', + }, +}; + +const getExpirationFlags = (not_after) => { + const DAYS_BEFORE_EXPIRATION = 5; + + const now = Date.now(); + const isExpiring = isAfter(addDays(now, DAYS_BEFORE_EXPIRATION), not_after); + const isExpired = isAfter(now, not_after); + + return { + isExpiring, + isExpired, + }; +}; + +const getExpirationEnumKey = (not_after) => { + const { isExpiring, isExpired } = getExpirationFlags(not_after); + + if (isExpired) { + return EXPIRATION_ENUM.EXPIRED; + } + + if (isExpiring) { + return EXPIRATION_ENUM.EXPIRING; + } + + return EXPIRATION_ENUM.VALID; +}; + const EncryptionTopline = () => { const not_after = useSelector((state) => state.encryption.not_after); @@ -13,30 +57,21 @@ const EncryptionTopline = () => { return null; } - const isAboutExpire = isAfter(addDays(Date.now(), 30), not_after); - const isExpired = isAfter(Date.now(), not_after); + const expirationStateKey = getExpirationEnumKey(not_after); - if (isExpired) { - return ( - - link]}> - topline_expired_certificate - - - ); + if (expirationStateKey === EXPIRATION_ENUM.VALID) { + return null; } - if (isAboutExpire) { - return ( - + const { toplineType, i18nKey } = EXPIRATION_STATE[expirationStateKey]; + + return ( + link]}> - topline_expiring_certificate + {i18nKey} - ); - } - - return false; + ); }; export default EncryptionTopline; diff --git a/client/src/components/ui/Guide.js b/client/src/components/ui/Guide.js index cef1c6f8..a06af83b 100644 --- a/client/src/components/ui/Guide.js +++ b/client/src/components/ui/Guide.js @@ -2,22 +2,25 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Trans, useTranslation } from 'react-i18next'; import i18next from 'i18next'; +import { useSelector } from 'react-redux'; import Tabs from './Tabs'; import Icons from './Icons'; +import { getPathWithQueryString } from '../../helpers/helpers'; const MOBILE_CONFIG_LINKS = { DOT: '/apple/dot.mobileconfig', DOH: '/apple/doh.mobileconfig', }; - -const renderMobileconfigInfo = ({ label, components }) =>
  • +const renderMobileconfigInfo = ({ label, components, server_name }) =>
  • {label}
  • ; @@ -38,37 +41,8 @@ const renderLi = ({ label, components }) =>
  • ; -const dnsPrivacyList = [{ - title: 'Android', - list: [ - { - label: 'setup_dns_privacy_android_1', - }, - { - label: 'setup_dns_privacy_android_2', - components: [ - { - key: 0, - href: 'https://adguard.com/adguard-android/overview.html', - }, - text, - ], - }, - { - label: 'setup_dns_privacy_android_3', - components: [ - { - key: 0, - href: 'https://getintra.org/', - }, - text, - ], - }, - ], -}, -{ - title: 'iOS', - list: [ +const getDnsPrivacyList = (server_name) => { + const iosList = [ { label: 'setup_dns_privacy_ios_2', components: [ @@ -79,13 +53,6 @@ const dnsPrivacyList = [{ text, ], }, - { - label: 'setup_dns_privacy_4', - components: { - highlight: , - }, - renderComponent: renderMobileconfigInfo, - }, { label: 'setup_dns_privacy_ios_1', components: [ @@ -93,68 +60,114 @@ const dnsPrivacyList = [{ key: 0, href: 'https://itunes.apple.com/app/id1452162351', }, - text, - { - key: 2, - href: 'https://dnscrypt.info/stamps', - }, + text, + { + key: 2, + href: 'https://dnscrypt.info/stamps', + }, ], - }, - ], -}, -{ - title: 'setup_dns_privacy_other_title', - list: [ - { - label: 'setup_dns_privacy_other_1', - }, - { - label: 'setup_dns_privacy_other_2', - components: [ - { - key: 0, - href: 'https://github.com/AdguardTeam/dnsproxy', - }, - ], - }, - { - href: 'https://github.com/jedisct1/dnscrypt-proxy', - label: 'setup_dns_privacy_other_3', - components: [ - { - key: 0, - href: 'https://github.com/jedisct1/dnscrypt-proxy', - }, + }]; + /* Insert second element if can generate .mobileconfig links */ + if (server_name) { + iosList.splice(1, 0, { + label: 'setup_dns_privacy_4', + components: { + highlight: , + }, + renderComponent: ({ label, components }) => renderMobileconfigInfo({ + label, + components, + server_name, + }), + }); + } + + return [{ + title: 'Android', + list: [ + { + label: 'setup_dns_privacy_android_1', + }, + { + label: 'setup_dns_privacy_android_2', + components: [ + { + key: 0, + href: 'https://adguard.com/adguard-android/overview.html', + }, text, - ], - }, - { - label: 'setup_dns_privacy_other_4', - components: [ - { - key: 0, - href: 'https://support.mozilla.org/kb/firefox-dns-over-https', - }, + ], + }, + { + label: 'setup_dns_privacy_android_3', + components: [ + { + key: 0, + href: 'https://getintra.org/', + }, text, - ], - }, - { - label: 'setup_dns_privacy_other_5', - components: [ - { - key: 0, - href: 'https://dnscrypt.info/implementations', - }, - { - key: 1, - href: 'https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Clients', - }, - ], - }, - ], -}, -]; + ], + }, + ], + }, + { + title: 'iOS', + list: iosList, + }, + { + title: 'setup_dns_privacy_other_title', + list: [ + { + label: 'setup_dns_privacy_other_1', + }, + { + label: 'setup_dns_privacy_other_2', + components: [ + { + key: 0, + href: 'https://github.com/AdguardTeam/dnsproxy', + }, + ], + }, + { + href: 'https://github.com/jedisct1/dnscrypt-proxy', + label: 'setup_dns_privacy_other_3', + components: [ + { + key: 0, + href: 'https://github.com/jedisct1/dnscrypt-proxy', + }, + text, + ], + }, + { + label: 'setup_dns_privacy_other_4', + components: [ + { + key: 0, + href: 'https://support.mozilla.org/kb/firefox-dns-over-https', + }, + text, + ], + }, + { + label: 'setup_dns_privacy_other_5', + components: [ + { + key: 0, + href: 'https://dnscrypt.info/implementations', + }, + { + key: 1, + href: 'https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Clients', + }, + ], + }, + ], + }, + ]; +}; const renderDnsPrivacyList = ({ title, list }) =>
    {title} @@ -172,6 +185,7 @@ const getTabs = ({ tlsAddress, httpsAddress, showDnsPrivacyNotice, + server_name, t, }) => ({ Router: { @@ -277,7 +291,7 @@ const getTabs = ({ setup_dns_privacy_3
    - {dnsPrivacyList.map(renderDnsPrivacyList)} + {getDnsPrivacyList(server_name).map(renderDnsPrivacyList)} }
    ; @@ -299,6 +313,7 @@ const renderContent = ({ title, list, getTitle }) =>
    { const { t } = useTranslation(); + const server_name = useSelector((state) => state.encryption.server_name); const tlsAddress = dnsAddresses?.filter((item) => item.includes('tls://')) ?? ''; const httpsAddress = dnsAddresses?.filter((item) => item.includes('https://')) ?? ''; const showDnsPrivacyNotice = httpsAddress.length < 1 && tlsAddress.length < 1; @@ -309,6 +324,7 @@ const Guide = ({ dnsAddresses }) => { tlsAddress, httpsAddress, showDnsPrivacyNotice, + server_name, t, }); diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 7bce8387..af10524f 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -53,10 +53,10 @@ export const REPOSITORY = { export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html'; export const PORT_53_FAQ_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ#bindinuse'; export const UPSTREAM_CONFIGURATION_WIKI_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams'; -export const FILTERS_LINK = '#filters'; - export const GETTING_STARTED_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'; +export const FILTERS_RELATIVE_LINK = '#filters'; + export const ADDRESS_IN_USE_TEXT = 'address already in use'; export const INSTALL_FIRST_STEP = 1; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 0ace3f08..0cefcfdf 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -687,7 +687,7 @@ export const sortIp = (a, b) => { return 0; } catch (e) { - console.error(e); + console.warn(e); return 0; } }; diff --git a/client/src/helpers/validators.js b/client/src/helpers/validators.js index 9b853120..f2fee026 100644 --- a/client/src/helpers/validators.js +++ b/client/src/helpers/validators.js @@ -64,7 +64,7 @@ export const validateClientId = (value) => { if (!value) { return undefined; } - const formattedValue = value ? value.trim() : value; + const formattedValue = value.trim(); if (formattedValue && !( R_IPV4.test(formattedValue) || R_IPV6.test(formattedValue) diff --git a/client/src/install/Setup/AddressList.js b/client/src/install/Setup/AddressList.js index 90bccfd2..15cf7113 100644 --- a/client/src/install/Setup/AddressList.js +++ b/client/src/install/Setup/AddressList.js @@ -12,7 +12,7 @@ const renderItem = ({ return
  • {isDns ? {dnsAddress} - : {webAddress} + : {webAddress} }
  • ; }; diff --git a/client/src/reducers/encryption.js b/client/src/reducers/encryption.js index a5cd7cbf..8fe9a2cb 100644 --- a/client/src/reducers/encryption.js +++ b/client/src/reducers/encryption.js @@ -9,6 +9,8 @@ const encryption = handleActions({ const newState = { ...state, ...payload, + /* TODO: handle property delete on api refactor */ + server_name: payload.server_name || '', processing: false, }; return newState; @@ -20,6 +22,7 @@ const encryption = handleActions({ const newState = { ...state, ...payload, + server_name: payload.server_name || '', processingConfig: false, }; return newState; @@ -49,6 +52,7 @@ const encryption = handleActions({ subject, warning_validation, dns_names, + server_name: payload.server_name || '', processingValidate: false, }; return newState; diff --git a/go.mod b/go.mod index 467dfee6..10866d88 100644 --- a/go.mod +++ b/go.mod @@ -3,35 +3,36 @@ module github.com/AdguardTeam/AdGuardHome go 1.14 require ( - github.com/AdguardTeam/dnsproxy v0.33.2 - github.com/AdguardTeam/golibs v0.4.3 - github.com/AdguardTeam/urlfilter v0.12.3 + github.com/AdguardTeam/dnsproxy v0.33.7 + github.com/AdguardTeam/golibs v0.4.4 + github.com/AdguardTeam/urlfilter v0.14.0 github.com/NYTimes/gziphandler v1.1.1 - github.com/beefsack/go-rate v0.0.0-20200827232406-6cde80facd47 // indirect + github.com/ameshkov/dnscrypt/v2 v2.0.1 github.com/fsnotify/fsnotify v1.4.9 + github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663 + github.com/gobuffalo/envy v1.9.0 // indirect github.com/gobuffalo/packr v1.30.1 + github.com/gobuffalo/packr/v2 v2.8.1 // indirect github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 - github.com/insomniacslk/dhcp v0.0.0-20200621044212-d74cd86ad5b8 - github.com/joomcode/errorx v1.0.3 // indirect - github.com/kardianos/service v1.1.0 + github.com/insomniacslk/dhcp v0.0.0-20201112113307-4de412bc85d8 + github.com/kardianos/service v1.2.0 + github.com/karrick/godirwalk v1.16.1 // indirect github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 github.com/miekg/dns v1.1.35 - github.com/rogpeppe/go-internal v1.5.2 // indirect + github.com/rogpeppe/go-internal v1.6.2 // indirect github.com/satori/go.uuid v1.2.0 - github.com/sirupsen/logrus v1.6.0 // indirect - github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c + github.com/sirupsen/logrus v1.7.0 // indirect + github.com/spf13/cobra v1.1.1 // indirect github.com/stretchr/testify v1.6.1 - github.com/u-root/u-root v6.0.0+incompatible - go.etcd.io/bbolt v1.3.4 - golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 - golang.org/x/net v0.0.0-20201110031124-69a78807bb2b + github.com/u-root/u-root v7.0.0+incompatible + go.etcd.io/bbolt v1.3.5 + golang.org/x/crypto v0.0.0-20201217014255-9d1352758620 + golang.org/x/net v0.0.0-20201216054612-986b41b23924 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect - golang.org/x/sys v0.0.0-20201109165425-215b40eba54c - golang.org/x/text v0.3.4 // indirect + golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v2 v2.3.0 - gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect - howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 + howett.net/plist v0.0.0-20201026045517-117a925f2150 ) diff --git a/go.sum b/go.sum index 65a1fd0c..764a2fa0 100644 --- a/go.sum +++ b/go.sum @@ -2,62 +2,92 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/AdguardTeam/dnsproxy v0.33.2 h1:k5aMcsw3TA/G2DR8EjIkwutDPuuRkKh8xij4cFWC6Fk= -github.com/AdguardTeam/dnsproxy v0.33.2/go.mod h1:kLi6lMpErnZThy5haiRSis4q0KTB8uPWO4JQsU1EDJA= +github.com/AdguardTeam/dnsproxy v0.33.7 h1:DXsLTJoBSUejB2ZqVHyMG0/kXD8PzuVPbLCsGKBdaDc= +github.com/AdguardTeam/dnsproxy v0.33.7/go.mod h1:dkI9VWh43XlOzF2XogDm1EmoVl7PANOR4isQV6X9LZs= github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o= github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= -github.com/AdguardTeam/golibs v0.4.3 h1:nXTLLLlIyU4BSRF0An5azS0uimSK/YpIMOBAO0/v1RY= -github.com/AdguardTeam/golibs v0.4.3/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= +github.com/AdguardTeam/golibs v0.4.4 h1:cM9UySQiYFW79zo5XRwnaIWVzfW4eNXmZktMrWbthpw= +github.com/AdguardTeam/golibs v0.4.4/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU= -github.com/AdguardTeam/urlfilter v0.12.3 h1:FMjQG0eTgrr8xA3z2zaLVcCgGdpzoECPGWwgPjtwPNs= -github.com/AdguardTeam/urlfilter v0.12.3/go.mod h1:1fcCQx5TGJANrQN6sHNNM9KPBl7qx7BJml45ko6vru0= +github.com/AdguardTeam/urlfilter v0.14.0 h1:+aAhOvZDVGzl5gTERB4pOJCL1zxMyw7vLecJJ6TQTCw= +github.com/AdguardTeam/urlfilter v0.14.0/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= -github.com/ameshkov/dnscrypt/v2 v2.0.0 h1:i83G8MeGLrAFgUL8GSu98TVhtFDEifF7SIS7Qi/RZ3U= -github.com/ameshkov/dnscrypt/v2 v2.0.0/go.mod h1:nbZnxJt4edIPx2Haa8n2XtC2g5AWcsdQiSuXkNH8eDI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/ameshkov/dnscrypt/v2 v2.0.1 h1:igNVNM6NLBOqYUzHXaDUxn8i+wJXOsosY0/xEBirixA= +github.com/ameshkov/dnscrypt/v2 v2.0.1/go.mod h1:nbZnxJt4edIPx2Haa8n2XtC2g5AWcsdQiSuXkNH8eDI= github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4JhnPug= github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I= -github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beefsack/go-rate v0.0.0-20200827232406-6cde80facd47 h1:M57m0xQqZIhx7CEJgeLSvRFKEK1RjzRuIXiA3HfYU7g= github.com/beefsack/go-rate v0.0.0-20200827232406-6cde80facd47/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= @@ -67,27 +97,45 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663 h1:jI2GiiRh+pPbey52EVmbU6kuLiXqwy4CXZ4gwUBj8Y0= +github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663/go.mod h1:35JbSyV/BYqHwwRA6Zr1uVDm1637YlNOU61wI797NPI= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.5 h1:AKODKU3pDH1RzZzm6YZu77YWtEAq6uh1rLIAQlay2qc= github.com/go-test/deep v1.0.5/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.9.0 h1:eZR0DuEgVLfeIb1zIKt3bT4YovIMf9O9LXQeCZLXpqE= +github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= github.com/gobuffalo/logger v1.0.0 h1:xw9Ko9EcC5iAFprrjJ6oZco9UpzS5MQ4jAwghsLHdy4= github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc= +github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM= +github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= github.com/gobuffalo/packr/v2 v2.5.1 h1:TFOeY2VoGamPjQLiNDT3mn//ytzk236VMO2j7iHxJR4= github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/gobuffalo/packr/v2 v2.8.1 h1:tkQpju6i3EtMXJ9uoF5GT6kB+LMTimDWD8Xvbz6zDVA= +github.com/gobuffalo/packr/v2 v2.8.1/go.mod h1:c/PLlOuTU+p3SybaJATW3H6lX/iK7xEz5OeMf+NnJpg= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= @@ -105,6 +153,7 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -113,85 +162,144 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/insomniacslk/dhcp v0.0.0-20200621044212-d74cd86ad5b8 h1:u+vle+5E78+cT/CSMD5/Y3NUpMgA83Yu2KhG+Zbco/k= -github.com/insomniacslk/dhcp v0.0.0-20200621044212-d74cd86ad5b8/go.mod h1:CfMdguCK66I5DAUJgGKyNz8aB6vO5dZzkm9Xep6WGvw= +github.com/insomniacslk/dhcp v0.0.0-20201112113307-4de412bc85d8 h1:R1oP0/QEyvaL7dm+mBQouQ9V1X6gqQr5taZA1yaq5zQ= +github.com/insomniacslk/dhcp v0.0.0-20201112113307-4de412bc85d8/go.mod h1:TKl4jN3Voofo4UJIicyNhWGp/nlQqQkFxmwIFTvBkKI= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/joomcode/errorx v1.0.1 h1:CalpDWz14ZHd68fIqluJasJosAewpz2TFaJALrUxjrk= github.com/joomcode/errorx v1.0.1/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ= github.com/joomcode/errorx v1.0.3 h1:3e1mi0u7/HTPNdg6d6DYyKGBhA5l9XpsfuVE29NxnWw= github.com/joomcode/errorx v1.0.3/go.mod h1:eQzdtdlNyN7etw6YCS4W4+lu442waxZYw5yvz0ULrRo= +github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= +github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= +github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= +github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kardianos/service v1.1.0 h1:QV2SiEeWK42P0aEmGcsAgjApw/lRxkwopvT+Gu6t1/0= -github.com/kardianos/service v1.1.0/go.mod h1:RrJI2xn5vve/r32U5suTbeaSGoMU6GbNPoj36CVYcHc= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g= +github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/karrick/godirwalk v1.10.12 h1:BqUm+LuJcXjGv1d2mj3gBiQyrQ57a0rYoAmhvJQ7RDU= github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/karrick/godirwalk v1.15.8/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucas-clemente/quic-go v0.18.1 h1:DMR7guC0NtVS8zNZR3IO7NARZvZygkSC56GGtC6cyys= -github.com/lucas-clemente/quic-go v0.18.1/go.mod h1:yXttHsSNxQi8AWijC/vLP+OJczXqzHSOcJrM5ITUlCg= +github.com/lucas-clemente/quic-go v0.19.3 h1:eCDQqvGBB+kCTkA0XrAFtNe81FMa0/fn4QSoeAbmiF4= +github.com/lucas-clemente/quic-go v0.19.3/go.mod h1:ADXpNbTQjq1hIzCpB+y/k5iz4n4z4IwqoLb94Kh5Hu8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/marten-seemann/qpack v0.2.0/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= github.com/marten-seemann/qtls v0.10.0 h1:ECsuYUKalRL240rRD4Ri33ISb7kAQ3qGDlrrl55b2pc= github.com/marten-seemann/qtls v0.10.0/go.mod h1:UvMd1oaYDACI99/oZUYLzMCkBXQVT0aGm99sJhbT8hs= -github.com/marten-seemann/qtls-go1-15 v0.1.0 h1:i/YPXVxz8q9umso/5y474CNcHmTpA+5DH+mFPjx6PZg= -github.com/marten-seemann/qtls-go1-15 v0.1.0/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= github.com/marten-seemann/qtls-go1-15 v0.1.1 h1:LIH6K34bPVttyXnUWixk0bzH6/N07VxbSabxn5A5gZQ= github.com/marten-seemann/qtls-go1-15 v0.1.1/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= +github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= +github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= +github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg= github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= -github.com/miekg/dns v1.1.34/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs= github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= @@ -200,27 +308,45 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.5.2 h1:qLvObTrvO/XRCqmkKxUlOBc48bI3efyDuAZe25QiF0w= github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0= +github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v2.20.3+incompatible h1:0JVooMPsT7A7HqEYdydp/OfjSOYSjhXV7w1hkKj/NPQ= github.com/shirou/gopsutil v2.20.3+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= @@ -244,24 +370,35 @@ github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c h1:gqEdF4VwBu3lTKGHS9rXE9x1/pEaSwCXRLOZRF6qtlw= -github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -272,65 +409,114 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/u-root/u-root v6.0.0+incompatible h1:YqPGmRoRyYmeg17KIWFRSyVq6LX5T6GSzawyA6wG6EE= -github.com/u-root/u-root v6.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/u-root/u-root v7.0.0+incompatible h1:u+KSS04pSxJGI5E7WE4Bs9+Zd75QjFv+REkjy/aoAc8= +github.com/u-root/u-root v7.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= -go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 h1:sYNJzB4J8toYPQTM6pAkcmBRgw9SnQKP9oXCHfgy604= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620 h1:3wPMTskHO3+O6jqTEXyFcsnuxMQOqYSaHsDxcbUXpqA= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 h1:lwlPPsmjDKK0J6eG6xDWd5XPehI0R024zxjDnw3esPA= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= +golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -342,22 +528,34 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03i golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -367,9 +565,21 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201109165425-215b40eba54c h1:+B+zPA6081G5cEb2triOIJpcvSW4AYzmIyWAqMn2JAc= -golang.org/x/sys v0.0.0-20201109165425-215b40eba54c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201214095126-aec9a390925b h1:tv7/y4pd+sR8bcNb2D6o7BNU6zjWm0VjQLac+w7fNNM= +golang.org/x/sys v0.0.0-20201214095126-aec9a390925b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs= +golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -381,17 +591,33 @@ golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -400,23 +626,38 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -430,6 +671,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= @@ -437,10 +679,13 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -449,17 +694,18 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 h1:AQkaJpH+/FmqRjmXZPELom5zIERYZfwTjnHpfoVMQEc= -howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +howett.net/plist v0.0.0-20201026045517-117a925f2150 h1:s7O/9fwMNd6O1yXyQ8zv+U7dfl8k+zdiLWAY8h7XdVI= +howett.net/plist v0.0.0-20201026045517-117a925f2150/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= diff --git a/internal/agherr/agherr.go b/internal/agherr/agherr.go index dee29466..aedf2a8b 100644 --- a/internal/agherr/agherr.go +++ b/internal/agherr/agherr.go @@ -65,3 +65,9 @@ func (e *manyError) Unwrap() error { return e.underlying[0] } + +// wrapper is a copy of the hidden errors.wrapper interface for tests, linting, +// etc. +type wrapper interface { + Unwrap() error +} diff --git a/internal/agherr/agherr_test.go b/internal/agherr/agherr_test.go index 3940bdee..123c45ef 100644 --- a/internal/agherr/agherr_test.go +++ b/internal/agherr/agherr_test.go @@ -41,6 +41,8 @@ func TestError_Error(t *testing.T) { } func TestError_Unwrap(t *testing.T) { + var _ wrapper = &manyError{} + const ( errSimple = iota errWrapped @@ -48,7 +50,7 @@ func TestError_Unwrap(t *testing.T) { ) errs := []error{ errSimple: errors.New("a"), - errWrapped: fmt.Errorf("%w", errors.New("nested")), + errWrapped: fmt.Errorf("err: %w", errors.New("nested")), errNil: nil, } testCases := []struct { diff --git a/internal/aghio/limitedreadcloser.go b/internal/aghio/limitedreadcloser.go new file mode 100644 index 00000000..7690705a --- /dev/null +++ b/internal/aghio/limitedreadcloser.go @@ -0,0 +1,59 @@ +// Package aghio contains extensions for io package's types and methods +package aghio + +import ( + "fmt" + "io" +) + +// LimitReachedError records the limit and the operation that caused it. +type LimitReachedError struct { + Limit int64 +} + +// Error implements error interface for LimitReachedError. +// TODO(a.garipov): Think about error string format. +func (lre *LimitReachedError) Error() string { + return fmt.Sprintf("attempted to read more than %d bytes", lre.Limit) +} + +// limitedReadCloser is a wrapper for io.ReadCloser with limited reader and +// dealing with agherr package. +type limitedReadCloser struct { + limit int64 + n int64 + rc io.ReadCloser +} + +// Read implements Reader interface. +func (lrc *limitedReadCloser) Read(p []byte) (n int, err error) { + if lrc.n == 0 { + return 0, &LimitReachedError{ + Limit: lrc.limit, + } + } + if int64(len(p)) > lrc.n { + p = p[0:lrc.n] + } + n, err = lrc.rc.Read(p) + lrc.n -= int64(n) + return n, err +} + +// Close implements Closer interface. +func (lrc *limitedReadCloser) Close() error { + return lrc.rc.Close() +} + +// LimitReadCloser wraps ReadCloser to make it's Reader stop with +// ErrLimitReached after n bytes read. +func LimitReadCloser(rc io.ReadCloser, n int64) (limited io.ReadCloser, err error) { + if n < 0 { + return nil, fmt.Errorf("aghio: invalid n in LimitReadCloser: %d", n) + } + return &limitedReadCloser{ + limit: n, + n: n, + rc: rc, + }, nil +} diff --git a/internal/aghio/limitedreadcloser_test.go b/internal/aghio/limitedreadcloser_test.go new file mode 100644 index 00000000..1f10e32b --- /dev/null +++ b/internal/aghio/limitedreadcloser_test.go @@ -0,0 +1,108 @@ +package aghio + +import ( + "fmt" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLimitReadCloser(t *testing.T) { + testCases := []struct { + name string + n int64 + want error + }{{ + name: "positive", + n: 1, + want: nil, + }, { + name: "zero", + n: 0, + want: nil, + }, { + name: "negative", + n: -1, + want: fmt.Errorf("aghio: invalid n in LimitReadCloser: -1"), + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := LimitReadCloser(nil, tc.n) + assert.Equal(t, tc.want, err) + }) + } +} + +func TestLimitedReadCloser_Read(t *testing.T) { + testCases := []struct { + name string + limit int64 + rStr string + want int + err error + }{{ + name: "perfectly_match", + limit: 3, + rStr: "abc", + want: 3, + err: nil, + }, { + name: "eof", + limit: 3, + rStr: "", + want: 0, + err: io.EOF, + }, { + name: "limit_reached", + limit: 0, + rStr: "abc", + want: 0, + err: &LimitReachedError{ + Limit: 0, + }, + }, { + name: "truncated", + limit: 2, + rStr: "abc", + want: 2, + err: nil, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + readCloser := ioutil.NopCloser(strings.NewReader(tc.rStr)) + buf := make([]byte, tc.limit+1) + + lreader, err := LimitReadCloser(readCloser, tc.limit) + assert.Nil(t, err) + + n, err := lreader.Read(buf) + assert.Equal(t, n, tc.want) + assert.Equal(t, tc.err, err) + }) + } +} + +func TestLimitedReadCloser_LimitReachedError(t *testing.T) { + testCases := []struct { + name string + want string + err error + }{{ + name: "simplest", + want: "attempted to read more than 0 bytes", + err: &LimitReachedError{ + Limit: 0, + }, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, tc.err.Error()) + }) + } +} diff --git a/internal/dhcpd/check_other_dhcp.go b/internal/dhcpd/checkother.go similarity index 98% rename from internal/dhcpd/check_other_dhcp.go rename to internal/dhcpd/checkother.go index aba9e446..19512686 100644 --- a/internal/dhcpd/check_other_dhcp.go +++ b/internal/dhcpd/checkother.go @@ -26,7 +26,7 @@ func CheckIfOtherDHCPServersPresentV4(ifaceName string) (bool, error) { return false, fmt.Errorf("couldn't find interface by name %s: %w", ifaceName, err) } - ifaceIPNet, err := ifaceIPv4Addrs(iface) + ifaceIPNet, err := ifaceIPAddrs(iface, ipVersion4) if err != nil { return false, fmt.Errorf("getting ipv4 addrs for iface %s: %w", ifaceName, err) } @@ -94,12 +94,11 @@ func CheckIfOtherDHCPServersPresentV4(ifaceName string) (bool, error) { continue } - if ok { - return true, nil - } if err != nil { return false, err } + + return ok, nil } } @@ -162,7 +161,7 @@ func CheckIfOtherDHCPServersPresentV6(ifaceName string) (bool, error) { return false, fmt.Errorf("dhcpv6: net.InterfaceByName: %s: %w", ifaceName, err) } - ifaceIPNet, err := ifaceIPv6Addrs(iface) + ifaceIPNet, err := ifaceIPAddrs(iface, ipVersion6) if err != nil { return false, fmt.Errorf("getting ipv6 addrs for iface %s: %w", ifaceName, err) } @@ -216,12 +215,11 @@ func CheckIfOtherDHCPServersPresentV6(ifaceName string) (bool, error) { continue } - if ok { - return true, nil - } if err != nil { return false, err } + + return ok, nil } } diff --git a/internal/dhcpd/check_other_dhcp_windows.go b/internal/dhcpd/checkother_windows.go similarity index 100% rename from internal/dhcpd/check_other_dhcp_windows.go rename to internal/dhcpd/checkother_windows.go diff --git a/internal/dhcpd/db.go b/internal/dhcpd/db.go index b01f9635..2618f12e 100644 --- a/internal/dhcpd/db.go +++ b/internal/dhcpd/db.go @@ -74,7 +74,6 @@ func (s *Server) dbLoad() { } else { v6DynLeases = append(v6DynLeases, &lease) } - } else { if obj[i].Expiry == leaseExpireStatic { staticLeases = append(staticLeases, &lease) diff --git a/internal/dhcpd/dhcpd.go b/internal/dhcpd/dhcpd.go index 999e9c7a..ecb7d48d 100644 --- a/internal/dhcpd/dhcpd.go +++ b/internal/dhcpd/dhcpd.go @@ -52,6 +52,7 @@ type ServerConfig struct { HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"` } +// OnLeaseChangedT is a callback for lease changes. type OnLeaseChangedT func(flags int) // flags for onLeaseChanged() @@ -74,16 +75,12 @@ type Server struct { onLeaseChanged []OnLeaseChangedT } +// ServerInterface is an interface for servers. type ServerInterface interface { Leases(flags int) []Lease SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) } -// CheckConfig checks the configuration -func (s *Server) CheckConfig(config ServerConfig) error { - return nil -} - // Create - create object func Create(config ServerConfig) *Server { s := &Server{} diff --git a/internal/dhcpd/dhcphttp.go b/internal/dhcpd/dhcphttp.go index f4ce801b..06a4cc79 100644 --- a/internal/dhcpd/dhcphttp.go +++ b/internal/dhcpd/dhcphttp.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/AdguardTeam/AdGuardHome/internal/sysutil" "github.com/AdguardTeam/AdGuardHome/internal/util" "github.com/AdguardTeam/golibs/jsonutil" "github.com/AdguardTeam/golibs/log" @@ -205,9 +206,9 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { s.dbLoad() if s.conf.Enabled { - staticIP, err := HasStaticIP(newconfig.InterfaceName) + staticIP, err := sysutil.IfaceHasStaticIP(newconfig.InterfaceName) if !staticIP && err == nil { - err = SetStaticIP(newconfig.InterfaceName) + err = sysutil.IfaceSetStaticIP(newconfig.InterfaceName) if err != nil { httpError(r, w, http.StatusInternalServerError, "Failed to configure static IP: %s", err) return @@ -282,7 +283,7 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { } } if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 { - jsonIface.GatewayIP = getGatewayIP(iface.Name) + jsonIface.GatewayIP = sysutil.GatewayIP(iface.Name) response[iface.Name] = jsonIface } } @@ -299,6 +300,7 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { // . Check if a static IP is configured for the network interface // Respond with results func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) { + // This use of ReadAll is safe, because request's body is now limited. body, err := ioutil.ReadAll(r.Body) if err != nil { msg := fmt.Sprintf("failed to read request body: %s", err) @@ -318,7 +320,7 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque found4, err4 := CheckIfOtherDHCPServersPresentV4(interfaceName) staticIP := map[string]interface{}{} - isStaticIP, err := HasStaticIP(interfaceName) + isStaticIP, err := sysutil.IfaceHasStaticIP(interfaceName) staticIPStatus := "yes" if err != nil { staticIPStatus = "error" @@ -508,6 +510,9 @@ func (s *Server) registerHandlers() { } // jsonError is a generic JSON error response. +// +// TODO(a.garipov): Merge together with the implementations in .../home and +// other packages after refactoring the web handler registering. type jsonError struct { // Message is the error message, an opaque string. Message string `json:"message"` diff --git a/internal/dhcpd/nclient4/client.go b/internal/dhcpd/nclient4/client.go index bb0473f1..b7f073c2 100644 --- a/internal/dhcpd/nclient4/client.go +++ b/internal/dhcpd/nclient4/client.go @@ -19,9 +19,7 @@ import ( "context" "errors" "fmt" - "log" "net" - "os" "sync" "sync/atomic" "time" @@ -317,26 +315,6 @@ func WithTimeout(d time.Duration) ClientOpt { } } -// WithSummaryLogger logs one-line DHCPv4 message summaries when sent & received. -func WithSummaryLogger() ClientOpt { - return func(c *Client) (err error) { - c.logger = ShortSummaryLogger{ - Printfer: log.New(os.Stderr, "[dhcpv4] ", log.LstdFlags), - } - return - } -} - -// WithDebugLogger logs multi-line full DHCPv4 messages when sent & received. -func WithDebugLogger() ClientOpt { - return func(c *Client) (err error) { - c.logger = DebugLogger{ - Printfer: log.New(os.Stderr, "[dhcpv4] ", log.LstdFlags), - } - return - } -} - // WithLogger set the logger (see interface Logger). func WithLogger(newLogger Logger) ClientOpt { return func(c *Client) (err error) { diff --git a/internal/dhcpd/nclient4/client_test.go b/internal/dhcpd/nclient4/client_test.go index 353a9ed7..99f99640 100644 --- a/internal/dhcpd/nclient4/client_test.go +++ b/internal/dhcpd/nclient4/client_test.go @@ -79,7 +79,7 @@ func serveAndClient(ctx context.Context, responses [][]*dhcpv4.DHCPv4, opts ...C return mc, serverConn } -func ComparePacket(got *dhcpv4.DHCPv4, want *dhcpv4.DHCPv4) error { +func ComparePacket(got, want *dhcpv4.DHCPv4) error { if got == nil && got == want { return nil } @@ -92,7 +92,7 @@ func ComparePacket(got *dhcpv4.DHCPv4, want *dhcpv4.DHCPv4) error { return nil } -func pktsExpected(got []*dhcpv4.DHCPv4, want []*dhcpv4.DHCPv4) error { +func pktsExpected(got, want []*dhcpv4.DHCPv4) error { if len(got) != len(want) { return fmt.Errorf("got %d packets, want %d packets", len(got), len(want)) } @@ -309,10 +309,10 @@ func TestMultipleSendAndRead(t *testing.T) { newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x44, 0x44, 0x44, 0x44}), }, server: [][]*dhcpv4.DHCPv4{ - []*dhcpv4.DHCPv4{ // Response for first packet. + { // Response for first packet. newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), }, - []*dhcpv4.DHCPv4{ // Response for second packet. + { // Response for second packet. newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x44, 0x44, 0x44, 0x44}), }, }, diff --git a/internal/dhcpd/nclient4/conn_unix.go b/internal/dhcpd/nclient4/conn_unix.go index 51ec98cb..39009d69 100644 --- a/internal/dhcpd/nclient4/conn_unix.go +++ b/internal/dhcpd/nclient4/conn_unix.go @@ -17,17 +17,13 @@ import ( "github.com/u-root/u-root/pkg/uio" ) -var ( - // BroadcastMac is the broadcast MAC address. - // - // Any UDP packet sent to this address is broadcast on the subnet. - BroadcastMac = net.HardwareAddr([]byte{255, 255, 255, 255, 255, 255}) -) +// BroadcastMac is the broadcast MAC address. +// +// Any UDP packet sent to this address is broadcast on the subnet. +var BroadcastMac = net.HardwareAddr([]byte{255, 255, 255, 255, 255, 255}) -var ( - // ErrUDPAddrIsRequired is an error used when a passed argument is not of type "*net.UDPAddr". - ErrUDPAddrIsRequired = errors.New("must supply UDPAddr") -) +// ErrUDPAddrIsRequired is an error used when a passed argument is not of type "*net.UDPAddr". +var ErrUDPAddrIsRequired = errors.New("must supply UDPAddr") // NewRawUDPConn returns a UDP connection bound to the interface and port // given based on a raw packet socket. All packets are broadcasted. @@ -68,7 +64,7 @@ func NewBroadcastUDPConn(rawPacketConn net.PacketConn, boundAddr *net.UDPAddr) n } } -func udpMatch(addr *net.UDPAddr, bound *net.UDPAddr) bool { +func udpMatch(addr, bound *net.UDPAddr) bool { if bound == nil { return true } diff --git a/internal/dhcpd/nclient4/ipv4.go b/internal/dhcpd/nclient4/ipv4.go index 5d961852..50a2d684 100644 --- a/internal/dhcpd/nclient4/ipv4.go +++ b/internal/dhcpd/nclient4/ipv4.go @@ -281,7 +281,7 @@ func (b UDP) Checksum() uint16 { // CalculateChecksum calculates the checksum of the udp packet, given the total // length of the packet and the checksum of the network-layer pseudo-header // (excluding the total length) and the checksum of the payload. -func (b UDP) CalculateChecksum(partialChecksum uint16, totalLen uint16) uint16 { +func (b UDP) CalculateChecksum(partialChecksum, totalLen uint16) uint16 { // Add the length portion of the checksum to the pseudo-checksum. tmp := make([]byte, 2) binary.BigEndian.PutUint16(tmp, totalLen) @@ -336,13 +336,13 @@ func ChecksumCombine(a, b uint16) uint16 { // given destination protocol and network address, ignoring the length // field. Pseudo-headers are needed by transport layers when calculating // their own checksum. -func PseudoHeaderChecksum(protocol TransportProtocolNumber, srcAddr net.IP, dstAddr net.IP) uint16 { +func PseudoHeaderChecksum(protocol TransportProtocolNumber, srcAddr, dstAddr net.IP) uint16 { xsum := Checksum([]byte(srcAddr), 0) xsum = Checksum([]byte(dstAddr), xsum) return Checksum([]byte{0, uint8(protocol)}, xsum) } -func udp4pkt(packet []byte, dest *net.UDPAddr, src *net.UDPAddr) []byte { +func udp4pkt(packet []byte, dest, src *net.UDPAddr) []byte { ipLen := IPv4MinimumSize udpLen := UDPMinimumSize diff --git a/internal/dhcpd/network_utils.go b/internal/dhcpd/network_utils.go deleted file mode 100644 index 41a1d7ec..00000000 --- a/internal/dhcpd/network_utils.go +++ /dev/null @@ -1,312 +0,0 @@ -package dhcpd - -import ( - "errors" - "fmt" - "io/ioutil" - "net" - "os/exec" - "regexp" - "runtime" - "strings" - - "github.com/AdguardTeam/AdGuardHome/internal/util" - - "github.com/AdguardTeam/golibs/file" - - "github.com/AdguardTeam/golibs/log" -) - -// HasStaticIP check if the network interface has a static IP configured -// -// Supports: Raspbian. -func HasStaticIP(ifaceName string) (bool, error) { - if runtime.GOOS == "linux" { - body, err := ioutil.ReadFile("/etc/dhcpcd.conf") - if err != nil { - return false, err - } - - return hasStaticIPDhcpcdConf(string(body), ifaceName), nil - } - - if runtime.GOOS == "darwin" { - return hasStaticIPDarwin(ifaceName) - } - - return false, fmt.Errorf("cannot check if IP is static: not supported on %s", runtime.GOOS) -} - -// SetStaticIP sets a static IP for the network interface. -func SetStaticIP(ifaceName string) error { - if runtime.GOOS == "linux" { - return setStaticIPDhcpdConf(ifaceName) - } - - if runtime.GOOS == "darwin" { - return setStaticIPDarwin(ifaceName) - } - - return fmt.Errorf("cannot set static IP on %s", runtime.GOOS) -} - -// for dhcpcd.conf -func hasStaticIPDhcpcdConf(dhcpConf, ifaceName string) bool { - lines := strings.Split(dhcpConf, "\n") - nameLine := fmt.Sprintf("interface %s", ifaceName) - withinInterfaceCtx := false - - for _, line := range lines { - line = strings.TrimSpace(line) - - if withinInterfaceCtx && len(line) == 0 { - // an empty line resets our state - withinInterfaceCtx = false - } - - if len(line) == 0 || line[0] == '#' { - continue - } - line = strings.TrimSpace(line) - - if !withinInterfaceCtx { - if line == nameLine { - // we found our interface - withinInterfaceCtx = true - } - } else { - if strings.HasPrefix(line, "interface ") { - // we found another interface - reset our state - withinInterfaceCtx = false - continue - } - if strings.HasPrefix(line, "static ip_address=") { - return true - } - } - } - return false -} - -// Get gateway IP address -func getGatewayIP(ifaceName string) string { - cmd := exec.Command("ip", "route", "show", "dev", ifaceName) - log.Tracef("executing %s %v", cmd.Path, cmd.Args) - d, err := cmd.Output() - if err != nil || cmd.ProcessState.ExitCode() != 0 { - return "" - } - - fields := strings.Fields(string(d)) - if len(fields) < 3 || fields[0] != "default" { - return "" - } - - ip := net.ParseIP(fields[2]) - if ip == nil { - return "" - } - - return fields[2] -} - -// setStaticIPDhcpdConf - updates /etc/dhcpd.conf and sets the current IP address to be static -func setStaticIPDhcpdConf(ifaceName string) error { - ip := util.GetSubnet(ifaceName) - if len(ip) == 0 { - return errors.New("can't get IP address") - } - - ip4, _, err := net.ParseCIDR(ip) - if err != nil { - return err - } - gatewayIP := getGatewayIP(ifaceName) - add := updateStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, ip4.String()) - - body, err := ioutil.ReadFile("/etc/dhcpcd.conf") - if err != nil { - return err - } - - body = append(body, []byte(add)...) - err = file.SafeWrite("/etc/dhcpcd.conf", body) - if err != nil { - return err - } - - return nil -} - -// updates dhcpd.conf content -- sets static IP address there -// for dhcpcd.conf -func updateStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string { - var body []byte - - add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n", - ifaceName, ip) - body = append(body, []byte(add)...) - - if len(gatewayIP) != 0 { - add = fmt.Sprintf("static routers=%s\n", - gatewayIP) - body = append(body, []byte(add)...) - } - - add = fmt.Sprintf("static domain_name_servers=%s\n\n", - dnsIP) - body = append(body, []byte(add)...) - - return string(body) -} - -// Check if network interface has a static IP configured -// Supports: MacOS. -func hasStaticIPDarwin(ifaceName string) (bool, error) { - portInfo, err := getCurrentHardwarePortInfo(ifaceName) - if err != nil { - return false, err - } - - return portInfo.static, nil -} - -// setStaticIPDarwin - uses networksetup util to set the current IP address to be static -// Additionally it configures the current DNS servers as well -func setStaticIPDarwin(ifaceName string) error { - portInfo, err := getCurrentHardwarePortInfo(ifaceName) - if err != nil { - return err - } - - if portInfo.static { - return errors.New("IP address is already static") - } - - dnsAddrs, err := getEtcResolvConfServers() - if err != nil { - return err - } - - args := make([]string, 0) - args = append(args, "-setdnsservers", portInfo.name) - args = append(args, dnsAddrs...) - - // Setting DNS servers is necessary when configuring a static IP - code, _, err := util.RunCommand("networksetup", args...) - if err != nil { - return err - } - if code != 0 { - return fmt.Errorf("failed to set DNS servers, code=%d", code) - } - - // Actually configures hardware port to have static IP - code, _, err = util.RunCommand("networksetup", "-setmanual", - portInfo.name, portInfo.ip, portInfo.subnet, portInfo.gatewayIP) - if err != nil { - return err - } - if code != 0 { - return fmt.Errorf("failed to set DNS servers, code=%d", code) - } - - return nil -} - -// getCurrentHardwarePortInfo gets information the specified network interface -func getCurrentHardwarePortInfo(ifaceName string) (hardwarePortInfo, error) { - // First of all we should find hardware port name - m := getNetworkSetupHardwareReports() - hardwarePort, ok := m[ifaceName] - if !ok { - return hardwarePortInfo{}, fmt.Errorf("could not find hardware port for %s", ifaceName) - } - - return getHardwarePortInfo(hardwarePort) -} - -// getNetworkSetupHardwareReports parses the output of the `networksetup -listallhardwareports` command -// it returns a map where the key is the interface name, and the value is the "hardware port" -// returns nil if it fails to parse the output -func getNetworkSetupHardwareReports() map[string]string { - _, out, err := util.RunCommand("networksetup", "-listallhardwareports") - if err != nil { - return nil - } - - re, err := regexp.Compile("Hardware Port: (.*?)\nDevice: (.*?)\n") - if err != nil { - return nil - } - - m := make(map[string]string) - - matches := re.FindAllStringSubmatch(out, -1) - for i := range matches { - port := matches[i][1] - device := matches[i][2] - m[device] = port - } - - return m -} - -// hardwarePortInfo - information obtained using MacOS networksetup -// about the current state of the internet connection -type hardwarePortInfo struct { - name string - ip string - subnet string - gatewayIP string - static bool -} - -func getHardwarePortInfo(hardwarePort string) (hardwarePortInfo, error) { - h := hardwarePortInfo{} - - _, out, err := util.RunCommand("networksetup", "-getinfo", hardwarePort) - if err != nil { - return h, err - } - - re := regexp.MustCompile("IP address: (.*?)\nSubnet mask: (.*?)\nRouter: (.*?)\n") - - match := re.FindStringSubmatch(out) - if len(match) == 0 { - return h, errors.New("could not find hardware port info") - } - - h.name = hardwarePort - h.ip = match[1] - h.subnet = match[2] - h.gatewayIP = match[3] - - if strings.Index(out, "Manual Configuration") == 0 { - h.static = true - } - - return h, nil -} - -// Gets a list of nameservers currently configured in the /etc/resolv.conf -func getEtcResolvConfServers() ([]string, error) { - body, err := ioutil.ReadFile("/etc/resolv.conf") - if err != nil { - return nil, err - } - - re := regexp.MustCompile("nameserver ([a-zA-Z0-9.:]+)") - - matches := re.FindAllStringSubmatch(string(body), -1) - if len(matches) == 0 { - return nil, errors.New("found no DNS servers in /etc/resolv.conf") - } - - addrs := make([]string, 0) - for i := range matches { - addrs = append(addrs, matches[i][1]) - } - - return addrs, nil -} diff --git a/internal/dhcpd/network_utils_test.go b/internal/dhcpd/network_utils_test.go deleted file mode 100644 index fb6ef11e..00000000 --- a/internal/dhcpd/network_utils_test.go +++ /dev/null @@ -1,63 +0,0 @@ -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris - -package dhcpd - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestHasStaticIPDhcpcdConf(t *testing.T) { - dhcpdConf := `#comment -# comment - -interface eth0 -static ip_address=192.168.0.1/24 - -# interface wlan0 -static ip_address=192.168.1.1/24 - -# comment -` - assert.True(t, !hasStaticIPDhcpcdConf(dhcpdConf, "wlan0")) - - dhcpdConf = `#comment -# comment - -interface eth0 -static ip_address=192.168.0.1/24 - -# interface wlan0 -static ip_address=192.168.1.1/24 - -# comment - -interface wlan0 -# comment -static ip_address=192.168.2.1/24 -` - assert.True(t, hasStaticIPDhcpcdConf(dhcpdConf, "wlan0")) -} - -func TestSetStaticIPDhcpcdConf(t *testing.T) { - dhcpcdConf := ` -interface wlan0 -static ip_address=192.168.0.2/24 -static routers=192.168.0.1 -static domain_name_servers=192.168.0.2 - -` - s := updateStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "192.168.0.1", "192.168.0.2") - assert.Equal(t, dhcpcdConf, s) - - // without gateway - dhcpcdConf = ` -interface wlan0 -static ip_address=192.168.0.2/24 -static domain_name_servers=192.168.0.2 - -` - s = updateStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "", "192.168.0.2") - assert.Equal(t, dhcpcdConf, s) -} diff --git a/internal/dhcpd/router_adv.go b/internal/dhcpd/routeradv.go similarity index 96% rename from internal/dhcpd/router_adv.go rename to internal/dhcpd/routeradv.go index e3d386f0..f1d63c7d 100644 --- a/internal/dhcpd/router_adv.go +++ b/internal/dhcpd/routeradv.go @@ -180,15 +180,18 @@ func (ra *raCtx) Init() error { data := createICMPv6RAPacket(params) var err error + success := false ipAndScope := ra.ipAddr.String() + "%" + ra.ifaceName ra.conn, err = icmp.ListenPacket("ip6:ipv6-icmp", ipAndScope) if err != nil { return fmt.Errorf("dhcpv6 ra: icmp.ListenPacket: %w", err) } - success := false defer func() { if !success { - ra.Close() + cerr := ra.Close() + if cerr != nil { + log.Error("closing context: %s", cerr) + } } }() @@ -227,13 +230,15 @@ func (ra *raCtx) Init() error { return nil } -// Close - close module -func (ra *raCtx) Close() { +// Close closes the module. +func (ra *raCtx) Close() (err error) { log.Debug("dhcpv6 ra: closing") ra.stop.Store(1) if ra.conn != nil { - ra.conn.Close() + return ra.conn.Close() } + + return nil } diff --git a/internal/dhcpd/router_adv_test.go b/internal/dhcpd/routeradv_test.go similarity index 100% rename from internal/dhcpd/router_adv_test.go rename to internal/dhcpd/routeradv_test.go diff --git a/internal/dhcpd/v4.go b/internal/dhcpd/v4.go index 9ad032c1..81ba3a1d 100644 --- a/internal/dhcpd/v4.go +++ b/internal/dhcpd/v4.go @@ -11,12 +11,14 @@ import ( "time" "github.com/AdguardTeam/golibs/log" + "github.com/go-ping/ping" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/server4" - "github.com/sparrc/go-ping" ) -// v4Server - DHCPv4 server +// v4Server is a DHCPv4 server. +// +// TODO(a.garipov): Think about unifying this and v6Server. type v4Server struct { srv *server4.Server leasesLock sync.Mutex @@ -244,6 +246,7 @@ func (s *v4Server) addrAvailable(target net.IP) bool { pinger, err := ping.NewPinger(target.String()) if err != nil { log.Error("ping.NewPinger(): %v", err) + return true } @@ -255,7 +258,12 @@ func (s *v4Server) addrAvailable(target net.IP) bool { reply = true } log.Debug("dhcpv4: Sending ICMP Echo to %v", target) - pinger.Run() + + err = pinger.Run() + if err != nil { + log.Error("pinger.Run(): %v", err) + return true + } if reply { log.Info("dhcpv4: IP conflict: %v is already used by another device", target) @@ -554,27 +562,6 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4 } } -// ifaceIPv4Addrs returns the interface's IPv4 addresses. -func ifaceIPv4Addrs(iface *net.Interface) (ips []net.IP, err error) { - addrs, err := iface.Addrs() - if err != nil { - return nil, err - } - - for _, a := range addrs { - ipnet, ok := a.(*net.IPNet) - if !ok { - continue - } - - if ip := ipnet.IP.To4(); ip != nil { - ips = append(ips, ip) - } - } - - return ips, nil -} - // Start starts the IPv4 DHCP server. func (s *v4Server) Start() error { if !s.conf.Enabled { @@ -589,26 +576,14 @@ func (s *v4Server) Start() error { log.Debug("dhcpv4: starting...") - dnsIPAddrs, err := ifaceIPv4Addrs(iface) + dnsIPAddrs, err := ifaceDNSIPAddrs(iface, ipVersion4, defaultMaxAttempts, defaultBackoff) if err != nil { - return fmt.Errorf("dhcpv4: getting ipv4 addrs for iface %s: %w", ifaceName, err) + return fmt.Errorf("dhcpv4: interface %s: %w", ifaceName, err) } - switch len(dnsIPAddrs) { - case 0: - log.Debug("dhcpv4: no ipv4 address for interface %s", iface.Name) - + if len(dnsIPAddrs) == 0 { + // No available IP addresses which may appear later. return nil - case 1: - // Some Android devices use 8.8.8.8 if there is no secondary DNS - // server. Fix that by setting the secondary DNS address to our - // address as well. - // - // See https://github.com/AdguardTeam/AdGuardHome/issues/1708. - log.Debug("dhcpv4: setting secondary dns ip to iself for interface %s", iface.Name) - dnsIPAddrs = append(dnsIPAddrs, dnsIPAddrs[0]) - default: - // Go on. } s.conf.dnsIPAddrs = dnsIPAddrs diff --git a/internal/dhcpd/v46.go b/internal/dhcpd/v46.go new file mode 100644 index 00000000..c8301875 --- /dev/null +++ b/internal/dhcpd/v46.go @@ -0,0 +1,123 @@ +package dhcpd + +import ( + "fmt" + "net" + "time" + + "github.com/AdguardTeam/golibs/log" +) + +// ipVersion is a documentational alias for int. Use it when the integer means +// IP version. +type ipVersion = int + +// IP version constants. +const ( + ipVersion4 ipVersion = 4 + ipVersion6 ipVersion = 6 +) + +// netIface is the interface for network interface methods. +type netIface interface { + Addrs() ([]net.Addr, error) +} + +// ifaceIPAddrs returns the interface's IP addresses. +func ifaceIPAddrs(iface netIface, ipv ipVersion) (ips []net.IP, err error) { + addrs, err := iface.Addrs() + if err != nil { + return nil, err + } + + for _, a := range addrs { + var ip net.IP + switch a := a.(type) { + case *net.IPAddr: + ip = a.IP + case *net.IPNet: + ip = a.IP + default: + continue + } + + // Assume that net.(*Interface).Addrs can only return valid IPv4 + // and IPv6 addresses. Thus, if it isn't an IPv4 address, it + // must be an IPv6 one. + switch ipv { + case ipVersion4: + if ip4 := ip.To4(); ip4 != nil { + ips = append(ips, ip4) + } + case ipVersion6: + if ip6 := ip.To4(); ip6 == nil { + ips = append(ips, ip) + } + default: + return nil, fmt.Errorf("invalid ip version %d", ipv) + } + } + + return ips, nil +} + +// Currently used defaults for ifaceDNSAddrs. +const ( + defaultMaxAttempts int = 10 + + defaultBackoff time.Duration = 500 * time.Millisecond +) + +// ifaceDNSIPAddrs returns IP addresses of the interface suitable to send to +// clients as DNS addresses. If err is nil, addrs contains either no addresses +// or at least two. +// +// It makes up to maxAttempts attempts to get the addresses if there are none, +// each time using the provided backoff. Sometimes an interface needs a few +// seconds to really ititialize. +// +// See https://github.com/AdguardTeam/AdGuardHome/issues/2304. +func ifaceDNSIPAddrs( + iface netIface, + ipv ipVersion, + maxAttempts int, + backoff time.Duration, +) (addrs []net.IP, err error) { + var n int +waitForIP: + for n = 1; n <= maxAttempts; n++ { + addrs, err = ifaceIPAddrs(iface, ipv) + if err != nil { + return nil, fmt.Errorf("getting ip addrs: %w", err) + } + + switch len(addrs) { + case 0: + log.Debug("dhcpv%d: attempt %d: no ip addresses", ipv, n) + + time.Sleep(backoff) + case 1: + // Some Android devices use 8.8.8.8 if there is not + // a secondary DNS server. Fix that by setting the + // secondary DNS address to the same address. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/1708. + log.Debug("dhcpv%d: setting secondary dns ip to itself", ipv) + addrs = append(addrs, addrs[0]) + + fallthrough + default: + break waitForIP + } + } + + if len(addrs) == 0 { + // Don't return errors in case the users want to try and enable + // the DHCP server later. + log.Error("dhcpv%d: no ip address for interface after %d attempts and %s", ipv, n, time.Duration(n)*backoff) + } else { + log.Debug("dhcpv%d: got addresses %s after %d attempts", ipv, addrs, n) + } + + return addrs, nil +} diff --git a/internal/dhcpd/v46_test.go b/internal/dhcpd/v46_test.go new file mode 100644 index 00000000..6007205d --- /dev/null +++ b/internal/dhcpd/v46_test.go @@ -0,0 +1,189 @@ +package dhcpd + +import ( + "errors" + "net" + "testing" + + "github.com/AdguardTeam/AdGuardHome/internal/agherr" + "github.com/stretchr/testify/assert" +) + +type fakeIface struct { + addrs []net.Addr + err error +} + +// Addrs implements the netIface interface for *fakeIface. +func (iface *fakeIface) Addrs() (addrs []net.Addr, err error) { + if iface.err != nil { + return nil, iface.err + } + + return iface.addrs, nil +} + +func TestIfaceIPAddrs(t *testing.T) { + const errTest agherr.Error = "test error" + + ip4 := net.IP{1, 2, 3, 4} + addr4 := &net.IPNet{IP: ip4} + + ip6 := net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6} + addr6 := &net.IPNet{IP: ip6} + + testCases := []struct { + name string + iface netIface + ipv ipVersion + want []net.IP + wantErr error + }{{ + name: "ipv4_success", + iface: &fakeIface{addrs: []net.Addr{addr4}, err: nil}, + ipv: ipVersion4, + want: []net.IP{ip4}, + wantErr: nil, + }, { + name: "ipv4_success_with_ipv6", + iface: &fakeIface{addrs: []net.Addr{addr6, addr4}, err: nil}, + ipv: ipVersion4, + want: []net.IP{ip4}, + wantErr: nil, + }, { + name: "ipv4_error", + iface: &fakeIface{addrs: []net.Addr{addr4}, err: errTest}, + ipv: ipVersion4, + want: nil, + wantErr: errTest, + }, { + name: "ipv6_success", + iface: &fakeIface{addrs: []net.Addr{addr6}, err: nil}, + ipv: ipVersion6, + want: []net.IP{ip6}, + wantErr: nil, + }, { + name: "ipv6_success_with_ipv4", + iface: &fakeIface{addrs: []net.Addr{addr6, addr4}, err: nil}, + ipv: ipVersion6, + want: []net.IP{ip6}, + wantErr: nil, + }, { + name: "ipv6_error", + iface: &fakeIface{addrs: []net.Addr{addr6}, err: errTest}, + ipv: ipVersion6, + want: nil, + wantErr: errTest, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, gotErr := ifaceIPAddrs(tc.iface, tc.ipv) + assert.Equal(t, tc.want, got) + assert.True(t, errors.Is(gotErr, tc.wantErr)) + }) + } +} + +type waitingFakeIface struct { + addrs []net.Addr + err error + n int +} + +// Addrs implements the netIface interface for *waitingFakeIface. +func (iface *waitingFakeIface) Addrs() (addrs []net.Addr, err error) { + if iface.err != nil { + return nil, iface.err + } + + if iface.n == 0 { + return iface.addrs, nil + } + + iface.n-- + + return nil, nil +} + +func TestIfaceDNSIPAddrs(t *testing.T) { + const errTest agherr.Error = "test error" + + ip4 := net.IP{1, 2, 3, 4} + addr4 := &net.IPNet{IP: ip4} + + ip6 := net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6} + addr6 := &net.IPNet{IP: ip6} + + testCases := []struct { + name string + iface netIface + ipv ipVersion + want []net.IP + wantErr error + }{{ + name: "ipv4_success", + iface: &fakeIface{addrs: []net.Addr{addr4}, err: nil}, + ipv: ipVersion4, + want: []net.IP{ip4, ip4}, + wantErr: nil, + }, { + name: "ipv4_success_with_ipv6", + iface: &fakeIface{addrs: []net.Addr{addr6, addr4}, err: nil}, + ipv: ipVersion4, + want: []net.IP{ip4, ip4}, + wantErr: nil, + }, { + name: "ipv4_error", + iface: &fakeIface{addrs: []net.Addr{addr4}, err: errTest}, + ipv: ipVersion4, + want: nil, + wantErr: errTest, + }, { + name: "ipv4_wait", + iface: &waitingFakeIface{ + addrs: []net.Addr{addr4}, + err: nil, + n: 1, + }, + ipv: ipVersion4, + want: []net.IP{ip4, ip4}, + wantErr: nil, + }, { + name: "ipv6_success", + iface: &fakeIface{addrs: []net.Addr{addr6}, err: nil}, + ipv: ipVersion6, + want: []net.IP{ip6, ip6}, + wantErr: nil, + }, { + name: "ipv6_success_with_ipv4", + iface: &fakeIface{addrs: []net.Addr{addr6, addr4}, err: nil}, + ipv: ipVersion6, + want: []net.IP{ip6, ip6}, + wantErr: nil, + }, { + name: "ipv6_error", + iface: &fakeIface{addrs: []net.Addr{addr6}, err: errTest}, + ipv: ipVersion6, + want: nil, + wantErr: errTest, + }, { + name: "ipv6_wait", + iface: &waitingFakeIface{ + addrs: []net.Addr{addr6}, + err: nil, + n: 1, + }, + ipv: ipVersion6, + want: []net.IP{ip6, ip6}, + wantErr: nil, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, gotErr := ifaceDNSIPAddrs(tc.iface, tc.ipv, 2, 0) + assert.Equal(t, tc.want, got) + assert.True(t, errors.Is(gotErr, tc.wantErr)) + }) + } +} diff --git a/internal/dhcpd/v46_windows.go b/internal/dhcpd/v46_windows.go index 152899b9..a02d9101 100644 --- a/internal/dhcpd/v46_windows.go +++ b/internal/dhcpd/v46_windows.go @@ -1,47 +1,23 @@ +// +build windows + package dhcpd // 'u-root/u-root' package, a dependency of 'insomniacslk/dhcp' package, doesn't build on Windows import "net" -type winServer struct { -} +type winServer struct{} -func (s *winServer) ResetLeases(leases []*Lease) { -} -func (s *winServer) GetLeases(flags int) []Lease { - return nil -} -func (s *winServer) GetLeasesRef() []*Lease { - return nil -} -func (s *winServer) AddStaticLease(lease Lease) error { - return nil -} -func (s *winServer) RemoveStaticLease(l Lease) error { - return nil -} -func (s *winServer) FindMACbyIP(ip net.IP) net.HardwareAddr { - return nil -} - -func (s *winServer) WriteDiskConfig4(c *V4ServerConf) { -} -func (s *winServer) WriteDiskConfig6(c *V6ServerConf) { -} - -func (s *winServer) Start() error { - return nil -} -func (s *winServer) Stop() { -} -func (s *winServer) Reset() { -} - -func v4Create(conf V4ServerConf) (DHCPServer, error) { - return &winServer{}, nil -} - -func v6Create(conf V6ServerConf) (DHCPServer, error) { - return &winServer{}, nil -} +func (s *winServer) ResetLeases(leases []*Lease) {} +func (s *winServer) GetLeases(flags int) []Lease { return nil } +func (s *winServer) GetLeasesRef() []*Lease { return nil } +func (s *winServer) AddStaticLease(lease Lease) error { return nil } +func (s *winServer) RemoveStaticLease(l Lease) error { return nil } +func (s *winServer) FindMACbyIP(ip net.IP) net.HardwareAddr { return nil } +func (s *winServer) WriteDiskConfig4(c *V4ServerConf) {} +func (s *winServer) WriteDiskConfig6(c *V6ServerConf) {} +func (s *winServer) Start() error { return nil } +func (s *winServer) Stop() {} +func (s *winServer) Reset() {} +func v4Create(conf V4ServerConf) (DHCPServer, error) { return &winServer{}, nil } +func v6Create(conf V6ServerConf) (DHCPServer, error) { return &winServer{}, nil } diff --git a/internal/dhcpd/v6.go b/internal/dhcpd/v6.go index b24be499..2dd41b5c 100644 --- a/internal/dhcpd/v6.go +++ b/internal/dhcpd/v6.go @@ -17,7 +17,9 @@ import ( const valueIAID = "ADGH" // value for IANA.ID -// v6Server - DHCPv6 server +// v6Server is a DHCPv6 server. +// +// TODO(a.garipov): Think about unifying this and v4Server. type v6Server struct { srv *server6.Server leasesLock sync.Mutex @@ -537,27 +539,6 @@ func (s *v6Server) packetHandler(conn net.PacketConn, peer net.Addr, req dhcpv6. } } -// ifaceIPv6Addrs returns the interface's IPv6 addresses. -func ifaceIPv6Addrs(iface *net.Interface) (ips []net.IP, err error) { - addrs, err := iface.Addrs() - if err != nil { - return nil, err - } - - for _, a := range addrs { - ipnet, ok := a.(*net.IPNet) - if !ok { - continue - } - - if ip := ipnet.IP.To16(); ip != nil { - ips = append(ips, ip) - } - } - - return ips, nil -} - // initialize RA module func (s *v6Server) initRA(iface *net.Interface) error { // choose the source IP address - should be link-local-unicast @@ -591,24 +572,16 @@ func (s *v6Server) Start() error { return fmt.Errorf("dhcpv6: finding interface %s by name: %w", ifaceName, err) } - log.Debug("dhcpv4: starting...") + log.Debug("dhcpv6: starting...") - dnsIPAddrs, err := ifaceIPv6Addrs(iface) + dnsIPAddrs, err := ifaceDNSIPAddrs(iface, ipVersion6, defaultMaxAttempts, defaultBackoff) if err != nil { - return fmt.Errorf("dhcpv6: getting ipv6 addrs for iface %s: %w", ifaceName, err) + return fmt.Errorf("dhcpv6: interface %s: %w", ifaceName, err) } - switch len(dnsIPAddrs) { - case 0: - log.Debug("dhcpv6: no ipv6 address for interface %s", iface.Name) - + if len(dnsIPAddrs) == 0 { + // No available IP addresses which may appear later. return nil - case 1: - // See the comment in (*v4Server).Start. - log.Debug("dhcpv6: setting secondary dns ip to iself for interface %s", iface.Name) - dnsIPAddrs = append(dnsIPAddrs, dnsIPAddrs[0]) - default: - // Go on. } s.conf.dnsIPAddrs = dnsIPAddrs @@ -624,7 +597,7 @@ func (s *v6Server) Start() error { return nil } - log.Debug("DHCPv6: starting...") + log.Debug("dhcpv6: listening...") if len(iface.HardwareAddr) != 6 { return fmt.Errorf("dhcpv6: invalid MAC %s", iface.HardwareAddr) @@ -655,7 +628,10 @@ func (s *v6Server) Start() error { // Stop - stop server func (s *v6Server) Stop() { - s.ra.Close() + err := s.ra.Close() + if err != nil { + log.Error("dhcpv6: s.ra.Close: %s", err) + } // DHCPv6 server may not be initialized if ra_slaac_only=true if s.srv == nil { @@ -663,10 +639,11 @@ func (s *v6Server) Stop() { } log.Debug("DHCPv6: stopping") - err := s.srv.Close() + err = s.srv.Close() if err != nil { log.Error("DHCPv6: srv.Close: %s", err) } + // now server.Serve() will return s.srv = nil } diff --git a/internal/dnsfilter/blocked_services.go b/internal/dnsfilter/blocked.go similarity index 96% rename from internal/dnsfilter/blocked_services.go rename to internal/dnsfilter/blocked.go index 08990e0a..48b02932 100644 --- a/internal/dnsfilter/blocked_services.go +++ b/internal/dnsfilter/blocked.go @@ -188,7 +188,7 @@ func BlockedSvcKnown(s string) bool { } // ApplyBlockedServices - set blocked services settings for this DNS request -func (d *Dnsfilter) ApplyBlockedServices(setts *RequestFilteringSettings, list []string, global bool) { +func (d *DNSFilter) ApplyBlockedServices(setts *RequestFilteringSettings, list []string, global bool) { setts.ServicesRules = []ServiceEntry{} if global { d.confLock.RLock() @@ -210,7 +210,7 @@ func (d *Dnsfilter) ApplyBlockedServices(setts *RequestFilteringSettings, list [ } } -func (d *Dnsfilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) { d.confLock.RLock() list := d.Config.BlockedServices d.confLock.RUnlock() @@ -223,7 +223,7 @@ func (d *Dnsfilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Req } } -func (d *Dnsfilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) { list := []string{} err := json.NewDecoder(r.Body).Decode(&list) if err != nil { @@ -241,7 +241,7 @@ func (d *Dnsfilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Requ } // registerBlockedServicesHandlers - register HTTP handlers -func (d *Dnsfilter) registerBlockedServicesHandlers() { +func (d *DNSFilter) registerBlockedServicesHandlers() { d.Config.HTTPRegister("GET", "/control/blocked_services/list", d.handleBlockedServicesList) d.Config.HTTPRegister("POST", "/control/blocked_services/set", d.handleBlockedServicesSet) } diff --git a/internal/dnsfilter/dnsfilter.go b/internal/dnsfilter/dnsfilter.go index a4ac31b8..4a1b255e 100644 --- a/internal/dnsfilter/dnsfilter.go +++ b/internal/dnsfilter/dnsfilter.go @@ -1,4 +1,4 @@ -// Package dnsfilter implements a DNS filter. +// Package dnsfilter implements a DNS request and response filter. package dnsfilter import ( @@ -91,12 +91,12 @@ type filtersInitializerParams struct { blockFilters []Filter } -// Dnsfilter holds added rules and performs hostname matches against the rules -type Dnsfilter struct { +// DNSFilter matches hostnames and DNS requests against filtering rules. +type DNSFilter struct { rulesStorage *filterlist.RuleStorage filteringEngine *urlfilter.DNSEngine - rulesStorageWhite *filterlist.RuleStorage - filteringEngineWhite *urlfilter.DNSEngine + rulesStorageAllow *filterlist.RuleStorage + filteringEngineAllow *urlfilter.DNSEngine engineLock sync.RWMutex parentalServer string // access via methods @@ -127,15 +127,16 @@ const ( // NotFilteredNotFound - host was not find in any checks, default value for result NotFilteredNotFound Reason = iota - // NotFilteredWhiteList - the host is explicitly whitelisted - NotFilteredWhiteList - // NotFilteredError - there was a transitive error during check + // NotFilteredAllowList - the host is explicitly allowed + NotFilteredAllowList + // NotFilteredError is returned when there was an error during + // checking. Reserved, currently unused. NotFilteredError // reasons for filtering - // FilteredBlackList - the host was matched to be advertising host - FilteredBlackList + // FilteredBlockList - the host was matched to be advertising host + FilteredBlockList // FilteredSafeBrowsing - the host was matched to be malicious/phishing FilteredSafeBrowsing // FilteredParental - the host was matched to be outside of parental control settings @@ -147,38 +148,60 @@ const ( // FilteredBlockedService - the host is blocked by "blocked services" settings FilteredBlockedService - // ReasonRewrite - rewrite rule was applied + // ReasonRewrite is returned when there was a rewrite by + // a legacy DNS Rewrite rule. ReasonRewrite - // RewriteEtcHosts - rewrite by /etc/hosts rule - RewriteEtcHosts + // RewriteAutoHosts is returned when there was a rewrite by + // autohosts rules (/etc/hosts and so on). + RewriteAutoHosts + + // DNSRewriteRule is returned when a $dnsrewrite filter rule was + // applied. + DNSRewriteRule ) +// TODO(a.garipov): Resync with actual code names or replace completely +// in HTTP API v1. var reasonNames = []string{ - "NotFilteredNotFound", - "NotFilteredWhiteList", - "NotFilteredError", + NotFilteredNotFound: "NotFilteredNotFound", + NotFilteredAllowList: "NotFilteredWhiteList", + NotFilteredError: "NotFilteredError", - "FilteredBlackList", - "FilteredSafeBrowsing", - "FilteredParental", - "FilteredInvalid", - "FilteredSafeSearch", - "FilteredBlockedService", + FilteredBlockList: "FilteredBlackList", + FilteredSafeBrowsing: "FilteredSafeBrowsing", + FilteredParental: "FilteredParental", + FilteredInvalid: "FilteredInvalid", + FilteredSafeSearch: "FilteredSafeSearch", + FilteredBlockedService: "FilteredBlockedService", - "Rewrite", - "RewriteEtcHosts", + ReasonRewrite: "Rewrite", + + RewriteAutoHosts: "RewriteEtcHosts", + + DNSRewriteRule: "DNSRewriteRule", } func (r Reason) String() string { - if uint(r) >= uint(len(reasonNames)) { + if r < 0 || int(r) >= len(reasonNames) { return "" } + return reasonNames[r] } +// In returns true if reasons include r. +func (r Reason) In(reasons ...Reason) bool { + for _, reason := range reasons { + if r == reason { + return true + } + } + return false +} + // GetConfig - get configuration -func (d *Dnsfilter) GetConfig() RequestFilteringSettings { +func (d *DNSFilter) GetConfig() RequestFilteringSettings { c := RequestFilteringSettings{} // d.confLock.RLock() c.SafeSearchEnabled = d.Config.SafeSearchEnabled @@ -189,7 +212,7 @@ func (d *Dnsfilter) GetConfig() RequestFilteringSettings { } // WriteDiskConfig - write configuration -func (d *Dnsfilter) WriteDiskConfig(c *Config) { +func (d *DNSFilter) WriteDiskConfig(c *Config) { d.confLock.Lock() *c = d.Config c.Rewrites = rewriteArrayDup(d.Config.Rewrites) @@ -200,7 +223,7 @@ func (d *Dnsfilter) WriteDiskConfig(c *Config) { // SetFilters - set new filters (synchronously or asynchronously) // When filters are set asynchronously, the old filters continue working until the new filters are ready. // In this case the caller must ensure that the old filter files are intact. -func (d *Dnsfilter) SetFilters(blockFilters, allowFilters []Filter, async bool) error { +func (d *DNSFilter) SetFilters(blockFilters, allowFilters []Filter, async bool) error { if async { params := filtersInitializerParams{ allowFilters: allowFilters, @@ -234,7 +257,7 @@ func (d *Dnsfilter) SetFilters(blockFilters, allowFilters []Filter, async bool) } // Starts initializing new filters by signal from channel -func (d *Dnsfilter) filtersInitializer() { +func (d *DNSFilter) filtersInitializer() { for { params := <-d.filtersInitializerChan err := d.initFiltering(params.allowFilters, params.blockFilters) @@ -246,23 +269,31 @@ func (d *Dnsfilter) filtersInitializer() { } // Close - close the object -func (d *Dnsfilter) Close() { +func (d *DNSFilter) Close() { d.engineLock.Lock() defer d.engineLock.Unlock() d.reset() } -func (d *Dnsfilter) reset() { +func (d *DNSFilter) reset() { + var err error + if d.rulesStorage != nil { - _ = d.rulesStorage.Close() + err = d.rulesStorage.Close() + if err != nil { + log.Error("dnsfilter: rulesStorage.Close: %s", err) + } } - if d.rulesStorageWhite != nil { - d.rulesStorageWhite.Close() + + if d.rulesStorageAllow != nil { + err = d.rulesStorageAllow.Close() + if err != nil { + log.Error("dnsfilter: rulesStorageAllow.Close: %s", err) + } } } type dnsFilterContext struct { - stats Stats safebrowsingCache cache.Cache parentalCache cache.Cache safeSearchCache cache.Cache @@ -270,34 +301,63 @@ type dnsFilterContext struct { var gctx dnsFilterContext // global dnsfilter context -// Result holds state of hostname check -type Result struct { - IsFiltered bool `json:",omitempty"` // True if the host name is filtered - Reason Reason `json:",omitempty"` // Reason for blocking / unblocking - Rule string `json:",omitempty"` // Original rule text - IP net.IP `json:",omitempty"` // Not nil only in the case of a hosts file syntax - FilterID int64 `json:",omitempty"` // Filter ID the rule belongs to - - // for ReasonRewrite: - CanonName string `json:",omitempty"` // CNAME value - - // for RewriteEtcHosts: - ReverseHosts []string `json:",omitempty"` - - // for ReasonRewrite & RewriteEtcHosts: - IPList []net.IP `json:",omitempty"` // list of IP addresses - - // for FilteredBlockedService: - ServiceName string `json:",omitempty"` // Name of the blocked service +// ResultRule contains information about applied rules. +type ResultRule struct { + // FilterListID is the ID of the rule's filter list. + FilterListID int64 `json:",omitempty"` + // Text is the text of the rule. + Text string `json:",omitempty"` + // IP is the host IP. It is nil unless the rule uses the + // /etc/hosts syntax or the reason is FilteredSafeSearch. + IP net.IP `json:",omitempty"` } -// Matched can be used to see if any match at all was found, no matter filtered or not +// Result contains the result of a request check. +// +// All fields transitively have omitempty tags so that the query log +// doesn't become too large. +// +// TODO(a.garipov): Clarify relationships between fields. Perhaps +// replace with a sum type or an interface? +type Result struct { + // IsFiltered is true if the request is filtered. + IsFiltered bool `json:",omitempty"` + + // Reason is the reason for blocking or unblocking the request. + Reason Reason `json:",omitempty"` + + // Rules are applied rules. If Rules are not empty, each rule + // is not nil. + Rules []*ResultRule `json:",omitempty"` + + // ReverseHosts is the reverse lookup rewrite result. It is + // empty unless Reason is set to RewriteAutoHosts. + ReverseHosts []string `json:",omitempty"` + + // IPList is the lookup rewrite result. It is empty unless + // Reason is set to RewriteAutoHosts or ReasonRewrite. + IPList []net.IP `json:",omitempty"` + + // CanonName is the CNAME value from the lookup rewrite result. + // It is empty unless Reason is set to ReasonRewrite. + CanonName string `json:",omitempty"` + + // ServiceName is the name of the blocked service. It is empty + // unless Reason is set to FilteredBlockedService. + ServiceName string `json:",omitempty"` + + // DNSRewriteResult is the $dnsrewrite filter rule result. + DNSRewriteResult *DNSRewriteResult `json:",omitempty"` +} + +// Matched returns true if any match at all was found regardless of +// whether it was filtered or not. func (r Reason) Matched() bool { return r != NotFilteredNotFound } -// CheckHostRules tries to match the host against filtering rules only -func (d *Dnsfilter) CheckHostRules(host string, qtype uint16, setts *RequestFilteringSettings) (Result, error) { +// CheckHostRules tries to match the host against filtering rules only. +func (d *DNSFilter) CheckHostRules(host string, qtype uint16, setts *RequestFilteringSettings) (Result, error) { if !setts.FilteringEnabled { return Result{}, nil } @@ -305,9 +365,9 @@ func (d *Dnsfilter) CheckHostRules(host string, qtype uint16, setts *RequestFilt return d.matchHost(host, qtype, *setts) } -// CheckHost tries to match the host against filtering rules, -// then safebrowsing and parental if they are enabled -func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFilteringSettings) (Result, error) { +// CheckHost tries to match the host against filtering rules, then +// safebrowsing and parental control rules, if they are enabled. +func (d *DNSFilter) CheckHost(host string, qtype uint16, setts *RequestFilteringSettings) (Result, error) { // sometimes DNS clients will try to resolve ".", which is a request to get root servers if host == "" { return Result{Reason: NotFilteredNotFound}, nil @@ -326,15 +386,12 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFiltering // Now check the hosts file -- do we have any rules for it? // just like DNS rewrites, it has higher priority than filtering rules. if d.Config.AutoHosts != nil { - matched, err := d.checkAutoHosts(host, qtype, &result) + matched := d.checkAutoHosts(host, qtype, &result) if matched { - return result, err + return result, nil } } - // Then check the filter lists. - // if request is blocked -- it should be blocked. - // if it is whitelisted -- we should do nothing with it anymore. if setts.FilteringEnabled { result, err = d.matchHost(host, qtype, *setts) if err != nil { @@ -393,18 +450,18 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFiltering return Result{}, nil } -func (d *Dnsfilter) checkAutoHosts(host string, qtype uint16, result *Result) (matched bool, err error) { +func (d *DNSFilter) checkAutoHosts(host string, qtype uint16, result *Result) (matched bool) { ips := d.Config.AutoHosts.Process(host, qtype) if ips != nil { - result.Reason = RewriteEtcHosts + result.Reason = RewriteAutoHosts result.IPList = ips - return true, nil + return true } revHosts := d.Config.AutoHosts.ProcessReverse(host, qtype) if len(revHosts) != 0 { - result.Reason = RewriteEtcHosts + result.Reason = RewriteAutoHosts // TODO(a.garipov): Optimize this with a buffer. result.ReverseHosts = make([]string, len(revHosts)) @@ -412,10 +469,10 @@ func (d *Dnsfilter) checkAutoHosts(host string, qtype uint16, result *Result) (m result.ReverseHosts[i] = revHosts[i] + "." } - return true, nil + return true } - return false, nil + return false } // Process rewrites table @@ -425,9 +482,7 @@ func (d *Dnsfilter) checkAutoHosts(host string, qtype uint16, result *Result) (m // . repeat for the new domain name (Note: we return only the last CNAME) // . Find A or AAAA record for a domain name (exact match or by wildcard) // . if found, set IP addresses (IPv4 or IPv6 depending on qtype) in Result.IPList array -func (d *Dnsfilter) processRewrites(host string, qtype uint16) Result { - var res Result - +func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) { d.confLock.RLock() defer d.confLock.RUnlock() @@ -442,7 +497,8 @@ func (d *Dnsfilter) processRewrites(host string, qtype uint16) Result { log.Debug("Rewrite: CNAME for %s is %s", host, rr[0].Answer) if host == rr[0].Answer { // "host == CNAME" is an exception - res.Reason = 0 + res.Reason = NotFilteredNotFound + return res } @@ -484,9 +540,16 @@ func matchBlockedServicesRules(host string, svcs []ServiceEntry) Result { res.Reason = FilteredBlockedService res.IsFiltered = true res.ServiceName = s.Name - res.Rule = rule.Text() - log.Debug("Blocked Services: matched rule: %s host: %s service: %s", - res.Rule, host, s.Name) + + ruleText := rule.Text() + res.Rules = []*ResultRule{{ + FilterListID: int64(rule.GetFilterListID()), + Text: ruleText, + }} + + log.Debug("blocked services: matched rule: %s host: %s service: %s", + ruleText, host, s.Name) + return res } } @@ -553,12 +616,12 @@ func createFilteringEngine(filters []Filter) (*filterlist.RuleStorage, *urlfilte } // Initialize urlfilter objects. -func (d *Dnsfilter) initFiltering(allowFilters, blockFilters []Filter) error { +func (d *DNSFilter) initFiltering(allowFilters, blockFilters []Filter) error { rulesStorage, filteringEngine, err := createFilteringEngine(blockFilters) if err != nil { return err } - rulesStorageWhite, filteringEngineWhite, err := createFilteringEngine(allowFilters) + rulesStorageAllow, filteringEngineAllow, err := createFilteringEngine(allowFilters) if err != nil { return err } @@ -567,8 +630,8 @@ func (d *Dnsfilter) initFiltering(allowFilters, blockFilters []Filter) error { d.reset() d.rulesStorage = rulesStorage d.filteringEngine = filteringEngine - d.rulesStorageWhite = rulesStorageWhite - d.filteringEngineWhite = filteringEngineWhite + d.rulesStorageAllow = rulesStorageAllow + d.filteringEngineAllow = filteringEngineAllow d.engineLock.Unlock() // Make sure that the OS reclaims memory as soon as possible @@ -578,36 +641,48 @@ func (d *Dnsfilter) initFiltering(allowFilters, blockFilters []Filter) error { return nil } +// matchHostProcessAllowList processes the allowlist logic of host +// matching. +func (d *DNSFilter) matchHostProcessAllowList(host string, dnsres urlfilter.DNSResult) (res Result, err error) { + var rule rules.Rule + if dnsres.NetworkRule != nil { + rule = dnsres.NetworkRule + } else if len(dnsres.HostRulesV4) > 0 { + rule = dnsres.HostRulesV4[0] + } else if len(dnsres.HostRulesV6) > 0 { + rule = dnsres.HostRulesV6[0] + } + + if rule == nil { + return Result{}, fmt.Errorf("invalid dns result: rules are empty") + } + + log.Debug("Filtering: found allowlist rule for host %q: %q list_id: %d", + host, rule.Text(), rule.GetFilterListID()) + + return makeResult(rule, NotFilteredAllowList), nil +} + // matchHost is a low-level way to check only if hostname is filtered by rules, // skipping expensive safebrowsing and parental lookups. -func (d *Dnsfilter) matchHost(host string, qtype uint16, setts RequestFilteringSettings) (Result, error) { +func (d *DNSFilter) matchHost(host string, qtype uint16, setts RequestFilteringSettings) (res Result, err error) { d.engineLock.RLock() // Keep in mind that this lock must be held no just when calling Match() // but also while using the rules returned by it. defer d.engineLock.RUnlock() - ureq := urlfilter.DNSRequest{} - ureq.Hostname = host - ureq.ClientIP = setts.ClientIP - ureq.ClientName = setts.ClientName - ureq.SortedClientTags = setts.ClientTags + ureq := urlfilter.DNSRequest{ + Hostname: host, + SortedClientTags: setts.ClientTags, + ClientIP: setts.ClientIP, + ClientName: setts.ClientName, + DNSType: qtype, + } - if d.filteringEngineWhite != nil { - rr, ok := d.filteringEngineWhite.MatchRequest(ureq) + if d.filteringEngineAllow != nil { + dnsres, ok := d.filteringEngineAllow.MatchRequest(ureq) if ok { - var rule rules.Rule - if rr.NetworkRule != nil { - rule = rr.NetworkRule - } else if rr.HostRulesV4 != nil { - rule = rr.HostRulesV4[0] - } else if rr.HostRulesV6 != nil { - rule = rr.HostRulesV6[0] - } - - log.Debug("Filtering: found whitelist rule for host %q: %q list_id: %d", - host, rule.Text(), rule.GetFilterListID()) - res := makeResult(rule, NotFilteredWhiteList) - return res, nil + return d.matchHostProcessAllowList(host, dnsres) } } @@ -615,68 +690,87 @@ func (d *Dnsfilter) matchHost(host string, qtype uint16, setts RequestFilteringS return Result{}, nil } - rr, ok := d.filteringEngine.MatchRequest(ureq) - if !ok { + dnsres, ok := d.filteringEngine.MatchRequest(ureq) + + // Check DNS rewrites first, because the API there is a bit + // awkward. + if dnsr := dnsres.DNSRewrites(); len(dnsr) > 0 { + res = d.processDNSRewrites(dnsr) + if res.Reason == DNSRewriteRule && res.CanonName == host { + // A rewrite of a host to itself. Go on and + // try matching other things. + } else { + return res, nil + } + } else if !ok { return Result{}, nil } - if rr.NetworkRule != nil { + if dnsres.NetworkRule != nil { log.Debug("Filtering: found rule for host %q: %q list_id: %d", - host, rr.NetworkRule.Text(), rr.NetworkRule.GetFilterListID()) - reason := FilteredBlackList - if rr.NetworkRule.Whitelist { - reason = NotFilteredWhiteList + host, dnsres.NetworkRule.Text(), dnsres.NetworkRule.GetFilterListID()) + reason := FilteredBlockList + if dnsres.NetworkRule.Whitelist { + reason = NotFilteredAllowList } - res := makeResult(rr.NetworkRule, reason) - return res, nil + + return makeResult(dnsres.NetworkRule, reason), nil } - if qtype == dns.TypeA && rr.HostRulesV4 != nil { - rule := rr.HostRulesV4[0] // note that we process only 1 matched rule + if qtype == dns.TypeA && dnsres.HostRulesV4 != nil { + rule := dnsres.HostRulesV4[0] // note that we process only 1 matched rule log.Debug("Filtering: found rule for host %q: %q list_id: %d", host, rule.Text(), rule.GetFilterListID()) - res := makeResult(rule, FilteredBlackList) - res.IP = rule.IP.To4() + res = makeResult(rule, FilteredBlockList) + res.Rules[0].IP = rule.IP.To4() + return res, nil } - if qtype == dns.TypeAAAA && rr.HostRulesV6 != nil { - rule := rr.HostRulesV6[0] // note that we process only 1 matched rule + if qtype == dns.TypeAAAA && dnsres.HostRulesV6 != nil { + rule := dnsres.HostRulesV6[0] // note that we process only 1 matched rule log.Debug("Filtering: found rule for host %q: %q list_id: %d", host, rule.Text(), rule.GetFilterListID()) - res := makeResult(rule, FilteredBlackList) - res.IP = rule.IP + res = makeResult(rule, FilteredBlockList) + res.Rules[0].IP = rule.IP + return res, nil } - if rr.HostRulesV4 != nil || rr.HostRulesV6 != nil { + if dnsres.HostRulesV4 != nil || dnsres.HostRulesV6 != nil { // Question Type doesn't match the host rules // Return the first matched host rule, but without an IP address var rule rules.Rule - if rr.HostRulesV4 != nil { - rule = rr.HostRulesV4[0] - } else if rr.HostRulesV6 != nil { - rule = rr.HostRulesV6[0] + if dnsres.HostRulesV4 != nil { + rule = dnsres.HostRulesV4[0] + } else if dnsres.HostRulesV6 != nil { + rule = dnsres.HostRulesV6[0] } log.Debug("Filtering: found rule for host %q: %q list_id: %d", host, rule.Text(), rule.GetFilterListID()) - res := makeResult(rule, FilteredBlackList) - res.IP = net.IP{} + res = makeResult(rule, FilteredBlockList) + res.Rules[0].IP = net.IP{} + return res, nil } return Result{}, nil } -// Construct Result object +// makeResult returns a properly constructed Result. func makeResult(rule rules.Rule, reason Reason) Result { - res := Result{} - res.FilterID = int64(rule.GetFilterListID()) - res.Rule = rule.Text() - res.Reason = reason - if reason == FilteredBlackList { + res := Result{ + Reason: reason, + Rules: []*ResultRule{{ + FilterListID: int64(rule.GetFilterListID()), + Text: rule.Text(), + }}, + } + + if reason == FilteredBlockList { res.IsFiltered = true } + return res } @@ -686,7 +780,7 @@ func InitModule() { } // New creates properly initialized DNS Filter that is ready to be used. -func New(c *Config, blockFilters []Filter) *Dnsfilter { +func New(c *Config, blockFilters []Filter) *DNSFilter { if c != nil { cacheConf := cache.Config{ EnableLRU: true, @@ -708,7 +802,7 @@ func New(c *Config, blockFilters []Filter) *Dnsfilter { } } - d := new(Dnsfilter) + d := new(DNSFilter) err := d.initSecurityServices() if err != nil { @@ -746,7 +840,7 @@ func New(c *Config, blockFilters []Filter) *Dnsfilter { // Start - start the module: // . start async filtering initializer goroutine // . register web handlers -func (d *Dnsfilter) Start() { +func (d *DNSFilter) Start() { d.filtersInitializerChan = make(chan filtersInitializerParams, 1) go d.filtersInitializer() @@ -756,12 +850,3 @@ func (d *Dnsfilter) Start() { d.registerBlockedServicesHandlers() } } - -// -// stats -// - -// GetStats return dns filtering stats since startup. -func (d *Dnsfilter) GetStats() Stats { - return gctx.stats -} diff --git a/internal/dnsfilter/dnsfilter_test.go b/internal/dnsfilter/dnsfilter_test.go index bfe06caa..2bae12de 100644 --- a/internal/dnsfilter/dnsfilter_test.go +++ b/internal/dnsfilter/dnsfilter_test.go @@ -41,7 +41,7 @@ func purgeCaches() { } } -func NewForTest(c *Config, filters []Filter) *Dnsfilter { +func NewForTest(c *Config, filters []Filter) *DNSFilter { setts = RequestFilteringSettings{} setts.FilteringEnabled = true if c != nil { @@ -58,38 +58,48 @@ func NewForTest(c *Config, filters []Filter) *Dnsfilter { return d } -func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) { +func (d *DNSFilter) checkMatch(t *testing.T, hostname string) { t.Helper() - ret, err := d.CheckHost(hostname, dns.TypeA, &setts) + res, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } - if !ret.IsFiltered { + if !res.IsFiltered { t.Errorf("Expected hostname %s to match", hostname) } } -func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname, ip string, qtype uint16) { +func (d *DNSFilter) checkMatchIP(t *testing.T, hostname, ip string, qtype uint16) { t.Helper() - ret, err := d.CheckHost(hostname, qtype, &setts) + + res, err := d.CheckHost(hostname, qtype, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } - if !ret.IsFiltered { + + if !res.IsFiltered { t.Errorf("Expected hostname %s to match", hostname) } - if ret.IP == nil || ret.IP.String() != ip { - t.Errorf("Expected ip %s to match, actual: %v", ip, ret.IP) + + if len(res.Rules) == 0 { + t.Errorf("Expected result to have rules") + + return + } + + r := res.Rules[0] + if r.IP == nil || r.IP.String() != ip { + t.Errorf("Expected ip %s to match, actual: %v", ip, r.IP) } } -func (d *Dnsfilter) checkMatchEmpty(t *testing.T, hostname string) { +func (d *DNSFilter) checkMatchEmpty(t *testing.T, hostname string) { t.Helper() - ret, err := d.CheckHost(hostname, dns.TypeA, &setts) + res, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } - if ret.IsFiltered { + if res.IsFiltered { t.Errorf("Expected hostname %s to not match", hostname) } } @@ -120,26 +130,43 @@ func TestEtcHostsMatching(t *testing.T) { d.checkMatchIP(t, "block.com", "0.0.0.0", dns.TypeA) // ...but empty IPv6 - ret, err := d.CheckHost("block.com", dns.TypeAAAA, &setts) - assert.True(t, err == nil && ret.IsFiltered && ret.IP != nil && len(ret.IP) == 0) - assert.True(t, ret.Rule == "0.0.0.0 block.com") + res, err := d.CheckHost("block.com", dns.TypeAAAA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + assert.Equal(t, "0.0.0.0 block.com", res.Rules[0].Text) + assert.Len(t, res.Rules[0].IP, 0) + } // IPv6 d.checkMatchIP(t, "ipv6.com", addr6, dns.TypeAAAA) // ...but empty IPv4 - ret, err = d.CheckHost("ipv6.com", dns.TypeA, &setts) - assert.True(t, err == nil && ret.IsFiltered && ret.IP != nil && len(ret.IP) == 0) + res, err = d.CheckHost("ipv6.com", dns.TypeA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + assert.Equal(t, "::1 ipv6.com", res.Rules[0].Text) + assert.Len(t, res.Rules[0].IP, 0) + } // 2 IPv4 (return only the first one) - ret, err = d.CheckHost("host2", dns.TypeA, &setts) - assert.True(t, err == nil && ret.IsFiltered) - assert.True(t, ret.IP != nil && ret.IP.Equal(net.ParseIP("0.0.0.1"))) + res, err = d.CheckHost("host2", dns.TypeA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + loopback4 := net.IP{0, 0, 0, 1} + assert.Equal(t, res.Rules[0].IP, loopback4) + } // ...and 1 IPv6 address - ret, err = d.CheckHost("host2", dns.TypeAAAA, &setts) - assert.True(t, err == nil && ret.IsFiltered) - assert.True(t, ret.IP != nil && ret.IP.Equal(net.ParseIP("::1"))) + res, err = d.CheckHost("host2", dns.TypeAAAA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + loopback6 := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + assert.Equal(t, res.Rules[0].IP, loopback6) + } } // SAFE BROWSING @@ -151,7 +178,6 @@ func TestSafeBrowsing(t *testing.T) { d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil) defer d.Close() - gctx.stats.Safebrowsing.Requests = 0 d.checkMatch(t, "wmconvirus.narod.ru") assert.True(t, strings.Contains(logOutput.String(), "SafeBrowsing lookup for wmconvirus.narod.ru")) @@ -206,13 +232,11 @@ func TestCheckHostSafeSearchYandex(t *testing.T) { // Check host for each domain for _, host := range yandex { - result, err := d.CheckHost(host, dns.TypeA, &setts) - if err != nil { - t.Errorf("SafeSearch doesn't work for yandex domain `%s` cause %s", host, err) - } - - if result.IP.String() != "213.180.193.56" { - t.Errorf("SafeSearch doesn't work for yandex domain `%s`", host) + res, err := d.CheckHost(host, dns.TypeA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + assert.Equal(t, res.Rules[0].IP.String(), "213.180.193.56") } } } @@ -226,13 +250,11 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) { // Check host for each domain for _, host := range googleDomains { - result, err := d.CheckHost(host, dns.TypeA, &setts) - if err != nil { - t.Errorf("SafeSearch doesn't work for %s cause %s", host, err) - } - - if result.IP == nil { - t.Errorf("SafeSearch doesn't work for %s", host) + res, err := d.CheckHost(host, dns.TypeA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + if assert.Len(t, res.Rules, 1) { + assert.NotEqual(t, res.Rules[0].IP.String(), "0.0.0.0") } } } @@ -242,40 +264,30 @@ func TestSafeSearchCacheYandex(t *testing.T) { defer d.Close() domain := "yandex.ru" - var result Result - var err error - - // Check host with disabled safesearch - result, err = d.CheckHost(domain, dns.TypeA, &setts) - if err != nil { - t.Fatalf("Cannot check host due to %s", err) - } - if result.IP != nil { - t.Fatalf("SafeSearch is not enabled but there is an answer for `%s` !", domain) - } + // Check host with disabled safesearch. + res, err := d.CheckHost(domain, dns.TypeA, &setts) + assert.Nil(t, err) + assert.False(t, res.IsFiltered) + assert.Len(t, res.Rules, 0) d = NewForTest(&Config{SafeSearchEnabled: true}, nil) defer d.Close() - result, err = d.CheckHost(domain, dns.TypeA, &setts) + res, err = d.CheckHost(domain, dns.TypeA, &setts) if err != nil { t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err) } - // Fir yandex we already know valid ip - if result.IP.String() != "213.180.193.56" { - t.Fatalf("Wrong IP for %s safesearch: %s", domain, result.IP.String()) + // For yandex we already know valid ip. + if assert.Len(t, res.Rules, 1) { + assert.Equal(t, res.Rules[0].IP.String(), "213.180.193.56") } - // Check cache + // Check cache. cachedValue, isFound := getCachedResult(gctx.safeSearchCache, domain) - - if !isFound { - t.Fatalf("Safesearch cache doesn't work for %s!", domain) - } - - if cachedValue.IP.String() != "213.180.193.56" { - t.Fatalf("Wrong IP in cache for %s safesearch: %s", domain, cachedValue.IP.String()) + assert.True(t, isFound) + if assert.Len(t, cachedValue.Rules, 1) { + assert.Equal(t, cachedValue.Rules[0].IP.String(), "213.180.193.56") } } @@ -283,13 +295,10 @@ func TestSafeSearchCacheGoogle(t *testing.T) { d := NewForTest(nil, nil) defer d.Close() domain := "www.google.ru" - result, err := d.CheckHost(domain, dns.TypeA, &setts) - if err != nil { - t.Fatalf("Cannot check host due to %s", err) - } - if result.IP != nil { - t.Fatalf("SafeSearch is not enabled but there is an answer!") - } + res, err := d.CheckHost(domain, dns.TypeA, &setts) + assert.Nil(t, err) + assert.False(t, res.IsFiltered) + assert.Len(t, res.Rules, 0) d = NewForTest(&Config{SafeSearchEnabled: true}, nil) defer d.Close() @@ -313,25 +322,17 @@ func TestSafeSearchCacheGoogle(t *testing.T) { } } - result, err = d.CheckHost(domain, dns.TypeA, &setts) - if err != nil { - t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err) + res, err = d.CheckHost(domain, dns.TypeA, &setts) + assert.Nil(t, err) + if assert.Len(t, res.Rules, 1) { + assert.True(t, res.Rules[0].IP.Equal(ip)) } - if result.IP.String() != ip.String() { - t.Fatalf("Wrong IP for %s safesearch: %s. Should be: %s", - domain, result.IP.String(), ip) - } - - // Check cache + // Check cache. cachedValue, isFound := getCachedResult(gctx.safeSearchCache, domain) - - if !isFound { - t.Fatalf("Safesearch cache doesn't work for %s!", domain) - } - - if cachedValue.IP.String() != ip.String() { - t.Fatalf("Wrong IP in cache for %s safesearch: %s", domain, cachedValue.IP.String()) + assert.True(t, isFound) + if assert.Len(t, cachedValue.Rules, 1) { + assert.True(t, cachedValue.Rules[0].IP.Equal(ip)) } } @@ -364,10 +365,11 @@ const nl = "\n" const ( blockingRules = `||example.org^` + nl - whitelistRules = `||example.org^` + nl + `@@||test.example.org` + nl + allowlistRules = `||example.org^` + nl + `@@||test.example.org` + nl importantRules = `@@||example.org^` + nl + `||test.example.org^$important` + nl regexRules = `/example\.org/` + nl + `@@||test.example.org^` + nl maskRules = `test*.example.org^` + nl + `exam*.com` + nl + dnstypeRules = `||example.org^$dnstype=AAAA` + nl + `@@||test.example.org^` + nl ) var tests = []struct { @@ -376,44 +378,51 @@ var tests = []struct { hostname string isFiltered bool reason Reason + dnsType uint16 }{ - {"sanity", "||doubleclick.net^", "www.doubleclick.net", true, FilteredBlackList}, - {"sanity", "||doubleclick.net^", "nodoubleclick.net", false, NotFilteredNotFound}, - {"sanity", "||doubleclick.net^", "doubleclick.net.ru", false, NotFilteredNotFound}, - {"sanity", "||doubleclick.net^", "wmconvirus.narod.ru", false, NotFilteredNotFound}, + {"sanity", "||doubleclick.net^", "www.doubleclick.net", true, FilteredBlockList, dns.TypeA}, + {"sanity", "||doubleclick.net^", "nodoubleclick.net", false, NotFilteredNotFound, dns.TypeA}, + {"sanity", "||doubleclick.net^", "doubleclick.net.ru", false, NotFilteredNotFound, dns.TypeA}, + {"sanity", "||doubleclick.net^", "wmconvirus.narod.ru", false, NotFilteredNotFound, dns.TypeA}, - {"blocking", blockingRules, "example.org", true, FilteredBlackList}, - {"blocking", blockingRules, "test.example.org", true, FilteredBlackList}, - {"blocking", blockingRules, "test.test.example.org", true, FilteredBlackList}, - {"blocking", blockingRules, "testexample.org", false, NotFilteredNotFound}, - {"blocking", blockingRules, "onemoreexample.org", false, NotFilteredNotFound}, + {"blocking", blockingRules, "example.org", true, FilteredBlockList, dns.TypeA}, + {"blocking", blockingRules, "test.example.org", true, FilteredBlockList, dns.TypeA}, + {"blocking", blockingRules, "test.test.example.org", true, FilteredBlockList, dns.TypeA}, + {"blocking", blockingRules, "testexample.org", false, NotFilteredNotFound, dns.TypeA}, + {"blocking", blockingRules, "onemoreexample.org", false, NotFilteredNotFound, dns.TypeA}, - {"whitelist", whitelistRules, "example.org", true, FilteredBlackList}, - {"whitelist", whitelistRules, "test.example.org", false, NotFilteredWhiteList}, - {"whitelist", whitelistRules, "test.test.example.org", false, NotFilteredWhiteList}, - {"whitelist", whitelistRules, "testexample.org", false, NotFilteredNotFound}, - {"whitelist", whitelistRules, "onemoreexample.org", false, NotFilteredNotFound}, + {"allowlist", allowlistRules, "example.org", true, FilteredBlockList, dns.TypeA}, + {"allowlist", allowlistRules, "test.example.org", false, NotFilteredAllowList, dns.TypeA}, + {"allowlist", allowlistRules, "test.test.example.org", false, NotFilteredAllowList, dns.TypeA}, + {"allowlist", allowlistRules, "testexample.org", false, NotFilteredNotFound, dns.TypeA}, + {"allowlist", allowlistRules, "onemoreexample.org", false, NotFilteredNotFound, dns.TypeA}, - {"important", importantRules, "example.org", false, NotFilteredWhiteList}, - {"important", importantRules, "test.example.org", true, FilteredBlackList}, - {"important", importantRules, "test.test.example.org", true, FilteredBlackList}, - {"important", importantRules, "testexample.org", false, NotFilteredNotFound}, - {"important", importantRules, "onemoreexample.org", false, NotFilteredNotFound}, + {"important", importantRules, "example.org", false, NotFilteredAllowList, dns.TypeA}, + {"important", importantRules, "test.example.org", true, FilteredBlockList, dns.TypeA}, + {"important", importantRules, "test.test.example.org", true, FilteredBlockList, dns.TypeA}, + {"important", importantRules, "testexample.org", false, NotFilteredNotFound, dns.TypeA}, + {"important", importantRules, "onemoreexample.org", false, NotFilteredNotFound, dns.TypeA}, - {"regex", regexRules, "example.org", true, FilteredBlackList}, - {"regex", regexRules, "test.example.org", false, NotFilteredWhiteList}, - {"regex", regexRules, "test.test.example.org", false, NotFilteredWhiteList}, - {"regex", regexRules, "testexample.org", true, FilteredBlackList}, - {"regex", regexRules, "onemoreexample.org", true, FilteredBlackList}, + {"regex", regexRules, "example.org", true, FilteredBlockList, dns.TypeA}, + {"regex", regexRules, "test.example.org", false, NotFilteredAllowList, dns.TypeA}, + {"regex", regexRules, "test.test.example.org", false, NotFilteredAllowList, dns.TypeA}, + {"regex", regexRules, "testexample.org", true, FilteredBlockList, dns.TypeA}, + {"regex", regexRules, "onemoreexample.org", true, FilteredBlockList, dns.TypeA}, - {"mask", maskRules, "test.example.org", true, FilteredBlackList}, - {"mask", maskRules, "test2.example.org", true, FilteredBlackList}, - {"mask", maskRules, "example.com", true, FilteredBlackList}, - {"mask", maskRules, "exampleeee.com", true, FilteredBlackList}, - {"mask", maskRules, "onemoreexamsite.com", true, FilteredBlackList}, - {"mask", maskRules, "example.org", false, NotFilteredNotFound}, - {"mask", maskRules, "testexample.org", false, NotFilteredNotFound}, - {"mask", maskRules, "example.co.uk", false, NotFilteredNotFound}, + {"mask", maskRules, "test.example.org", true, FilteredBlockList, dns.TypeA}, + {"mask", maskRules, "test2.example.org", true, FilteredBlockList, dns.TypeA}, + {"mask", maskRules, "example.com", true, FilteredBlockList, dns.TypeA}, + {"mask", maskRules, "exampleeee.com", true, FilteredBlockList, dns.TypeA}, + {"mask", maskRules, "onemoreexamsite.com", true, FilteredBlockList, dns.TypeA}, + {"mask", maskRules, "example.org", false, NotFilteredNotFound, dns.TypeA}, + {"mask", maskRules, "testexample.org", false, NotFilteredNotFound, dns.TypeA}, + {"mask", maskRules, "example.co.uk", false, NotFilteredNotFound, dns.TypeA}, + + {"dnstype", dnstypeRules, "onemoreexample.org", false, NotFilteredNotFound, dns.TypeA}, + {"dnstype", dnstypeRules, "example.org", false, NotFilteredNotFound, dns.TypeA}, + {"dnstype", dnstypeRules, "example.org", true, FilteredBlockList, dns.TypeAAAA}, + {"dnstype", dnstypeRules, "test.example.org", false, NotFilteredAllowList, dns.TypeA}, + {"dnstype", dnstypeRules, "test.example.org", false, NotFilteredAllowList, dns.TypeAAAA}, } func TestMatching(t *testing.T) { @@ -425,15 +434,15 @@ func TestMatching(t *testing.T) { d := NewForTest(nil, filters) defer d.Close() - ret, err := d.CheckHost(test.hostname, dns.TypeA, &setts) + res, err := d.CheckHost(test.hostname, test.dnsType, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", test.hostname, err) } - if ret.IsFiltered != test.isFiltered { - t.Errorf("Hostname %s has wrong result (%v must be %v)", test.hostname, ret.IsFiltered, test.isFiltered) + if res.IsFiltered != test.isFiltered { + t.Errorf("Hostname %s has wrong result (%v must be %v)", test.hostname, res.IsFiltered, test.isFiltered) } - if ret.Reason != test.reason { - t.Errorf("Hostname %s has wrong reason (%v must be %v)", test.hostname, ret.Reason.String(), test.reason.String()) + if res.Reason != test.reason { + t.Errorf("Hostname %s has wrong reason (%v must be %v)", test.hostname, res.Reason.String(), test.reason.String()) } }) } @@ -458,16 +467,20 @@ func TestWhitelist(t *testing.T) { defer d.Close() // matched by white filter - ret, err := d.CheckHost("host1", dns.TypeA, &setts) + res, err := d.CheckHost("host1", dns.TypeA, &setts) assert.True(t, err == nil) - assert.True(t, !ret.IsFiltered && ret.Reason == NotFilteredWhiteList) - assert.True(t, ret.Rule == "||host1^") + assert.True(t, !res.IsFiltered && res.Reason == NotFilteredAllowList) + if assert.Len(t, res.Rules, 1) { + assert.True(t, res.Rules[0].Text == "||host1^") + } // not matched by white filter, but matched by block filter - ret, err = d.CheckHost("host2", dns.TypeA, &setts) + res, err = d.CheckHost("host2", dns.TypeA, &setts) assert.True(t, err == nil) - assert.True(t, ret.IsFiltered && ret.Reason == FilteredBlackList) - assert.True(t, ret.Rule == "||host2^") + assert.True(t, res.IsFiltered && res.Reason == FilteredBlockList) + if assert.Len(t, res.Rules, 1) { + assert.True(t, res.Rules[0].Text == "||host2^") + } } // CLIENT SETTINGS @@ -498,8 +511,8 @@ func TestClientSettings(t *testing.T) { // blocked by filters r, _ = d.CheckHost("example.org", dns.TypeA, &setts) - if !r.IsFiltered || r.Reason != FilteredBlackList { - t.Fatalf("CheckHost FilteredBlackList") + if !r.IsFiltered || r.Reason != FilteredBlockList { + t.Fatalf("CheckHost FilteredBlockList") } // blocked by parental @@ -551,11 +564,11 @@ func BenchmarkSafeBrowsing(b *testing.B) { defer d.Close() for n := 0; n < b.N; n++ { hostname := "wmconvirus.narod.ru" - ret, err := d.CheckHost(hostname, dns.TypeA, &setts) + res, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { b.Errorf("Error while matching host %s: %s", hostname, err) } - if !ret.IsFiltered { + if !res.IsFiltered { b.Errorf("Expected hostname %s to match", hostname) } } @@ -567,11 +580,11 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { hostname := "wmconvirus.narod.ru" - ret, err := d.CheckHost(hostname, dns.TypeA, &setts) + res, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { b.Errorf("Error while matching host %s: %s", hostname, err) } - if !ret.IsFiltered { + if !res.IsFiltered { b.Errorf("Expected hostname %s to match", hostname) } } diff --git a/internal/dnsfilter/dnsrewrite.go b/internal/dnsfilter/dnsrewrite.go new file mode 100644 index 00000000..1239fbad --- /dev/null +++ b/internal/dnsfilter/dnsrewrite.go @@ -0,0 +1,80 @@ +package dnsfilter + +import ( + "github.com/AdguardTeam/urlfilter/rules" + "github.com/miekg/dns" +) + +// DNSRewriteResult is the result of application of $dnsrewrite rules. +type DNSRewriteResult struct { + RCode rules.RCode `json:",omitempty"` + Response DNSRewriteResultResponse `json:",omitempty"` +} + +// DNSRewriteResultResponse is the collection of DNS response records +// the server returns. +type DNSRewriteResultResponse map[rules.RRType][]rules.RRValue + +// processDNSRewrites processes DNS rewrite rules in dnsr. It returns +// an empty result if dnsr is empty. Otherwise, the result will have +// either CanonName or DNSRewriteResult set. +func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) { + if len(dnsr) == 0 { + return Result{} + } + + var rules []*ResultRule + dnsrr := &DNSRewriteResult{ + Response: DNSRewriteResultResponse{}, + } + + for _, nr := range dnsr { + dr := nr.DNSRewrite + if dr.NewCNAME != "" { + // NewCNAME rules have a higher priority than + // the other rules. + rules := []*ResultRule{{ + FilterListID: int64(nr.GetFilterListID()), + Text: nr.RuleText, + }} + + return Result{ + Reason: DNSRewriteRule, + Rules: rules, + CanonName: dr.NewCNAME, + } + } + + switch dr.RCode { + case dns.RcodeSuccess: + dnsrr.RCode = dr.RCode + dnsrr.Response[dr.RRType] = append(dnsrr.Response[dr.RRType], dr.Value) + rules = append(rules, &ResultRule{ + FilterListID: int64(nr.GetFilterListID()), + Text: nr.RuleText, + }) + default: + // RcodeRefused and other such codes have higher + // priority. Return immediately. + rules := []*ResultRule{{ + FilterListID: int64(nr.GetFilterListID()), + Text: nr.RuleText, + }} + dnsrr = &DNSRewriteResult{ + RCode: dr.RCode, + } + + return Result{ + Reason: DNSRewriteRule, + Rules: rules, + DNSRewriteResult: dnsrr, + } + } + } + + return Result{ + Reason: DNSRewriteRule, + Rules: rules, + DNSRewriteResult: dnsrr, + } +} diff --git a/internal/dnsfilter/dnsrewrite_test.go b/internal/dnsfilter/dnsrewrite_test.go new file mode 100644 index 00000000..4918ccc0 --- /dev/null +++ b/internal/dnsfilter/dnsrewrite_test.go @@ -0,0 +1,202 @@ +package dnsfilter + +import ( + "net" + "path" + "testing" + + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" +) + +func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) { + const text = ` +|cname^$dnsrewrite=new_cname + +|a_record^$dnsrewrite=127.0.0.1 + +|aaaa_record^$dnsrewrite=::1 + +|txt_record^$dnsrewrite=NOERROR;TXT;hello_world + +|refused^$dnsrewrite=REFUSED + +|a_records^$dnsrewrite=127.0.0.1 +|a_records^$dnsrewrite=127.0.0.2 + +|aaaa_records^$dnsrewrite=::1 +|aaaa_records^$dnsrewrite=::2 + +|disable_one^$dnsrewrite=127.0.0.1 +|disable_one^$dnsrewrite=127.0.0.2 +@@||disable_one^$dnsrewrite=127.0.0.1 + +|disable_cname^$dnsrewrite=127.0.0.1 +|disable_cname^$dnsrewrite=new_cname +@@||disable_cname^$dnsrewrite=new_cname + +|disable_cname_many^$dnsrewrite=127.0.0.1 +|disable_cname_many^$dnsrewrite=new_cname_1 +|disable_cname_many^$dnsrewrite=new_cname_2 +@@||disable_cname_many^$dnsrewrite=new_cname_1 + +|disable_all^$dnsrewrite=127.0.0.1 +|disable_all^$dnsrewrite=127.0.0.2 +@@||disable_all^$dnsrewrite +` + f := NewForTest(nil, []Filter{{ID: 0, Data: []byte(text)}}) + setts := &RequestFilteringSettings{ + FilteringEnabled: true, + } + + ipv4p1 := net.IPv4(127, 0, 0, 1) + ipv4p2 := net.IPv4(127, 0, 0, 2) + ipv6p1 := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + ipv6p2 := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2} + + t.Run("cname", func(t *testing.T) { + dtyp := dns.TypeA + host := path.Base(t.Name()) + + res, err := f.CheckHostRules(host, dtyp, setts) + assert.Nil(t, err) + assert.Equal(t, "new_cname", res.CanonName) + }) + + t.Run("a_record", func(t *testing.T) { + dtyp := dns.TypeA + host := path.Base(t.Name()) + + res, err := f.CheckHostRules(host, dtyp, setts) + assert.Nil(t, err) + + if dnsrr := res.DNSRewriteResult; assert.NotNil(t, dnsrr) { + assert.Equal(t, dns.RcodeSuccess, dnsrr.RCode) + if ipVals := dnsrr.Response[dtyp]; assert.Len(t, ipVals, 1) { + assert.Equal(t, ipv4p1, ipVals[0]) + } + } + }) + + t.Run("aaaa_record", func(t *testing.T) { + dtyp := dns.TypeAAAA + host := path.Base(t.Name()) + + res, err := f.CheckHostRules(host, dtyp, setts) + assert.Nil(t, err) + + if dnsrr := res.DNSRewriteResult; assert.NotNil(t, dnsrr) { + assert.Equal(t, dns.RcodeSuccess, dnsrr.RCode) + if ipVals := dnsrr.Response[dtyp]; assert.Len(t, ipVals, 1) { + assert.Equal(t, ipv6p1, ipVals[0]) + } + } + }) + + t.Run("txt_record", func(t *testing.T) { + dtyp := dns.TypeTXT + host := path.Base(t.Name()) + res, err := f.CheckHostRules(host, dtyp, setts) + assert.Nil(t, err) + + if dnsrr := res.DNSRewriteResult; assert.NotNil(t, dnsrr) { + assert.Equal(t, dns.RcodeSuccess, dnsrr.RCode) + if strVals := dnsrr.Response[dtyp]; assert.Len(t, strVals, 1) { + assert.Equal(t, "hello_world", strVals[0]) + } + } + }) + + t.Run("refused", func(t *testing.T) { + host := path.Base(t.Name()) + res, err := f.CheckHostRules(host, dns.TypeA, setts) + assert.Nil(t, err) + + if dnsrr := res.DNSRewriteResult; assert.NotNil(t, dnsrr) { + assert.Equal(t, dns.RcodeRefused, dnsrr.RCode) + } + }) + + t.Run("a_records", func(t *testing.T) { + dtyp := dns.TypeA + host := path.Base(t.Name()) + + res, err := f.CheckHostRules(host, dtyp, setts) + assert.Nil(t, err) + + if dnsrr := res.DNSRewriteResult; assert.NotNil(t, dnsrr) { + assert.Equal(t, dns.RcodeSuccess, dnsrr.RCode) + if ipVals := dnsrr.Response[dtyp]; assert.Len(t, ipVals, 2) { + assert.Equal(t, ipv4p1, ipVals[0]) + assert.Equal(t, ipv4p2, ipVals[1]) + } + } + }) + + t.Run("aaaa_records", func(t *testing.T) { + dtyp := dns.TypeAAAA + host := path.Base(t.Name()) + + res, err := f.CheckHostRules(host, dtyp, setts) + assert.Nil(t, err) + + if dnsrr := res.DNSRewriteResult; assert.NotNil(t, dnsrr) { + assert.Equal(t, dns.RcodeSuccess, dnsrr.RCode) + if ipVals := dnsrr.Response[dtyp]; assert.Len(t, ipVals, 2) { + assert.Equal(t, ipv6p1, ipVals[0]) + assert.Equal(t, ipv6p2, ipVals[1]) + } + } + }) + + t.Run("disable_one", func(t *testing.T) { + dtyp := dns.TypeA + host := path.Base(t.Name()) + + res, err := f.CheckHostRules(host, dtyp, setts) + assert.Nil(t, err) + + if dnsrr := res.DNSRewriteResult; assert.NotNil(t, dnsrr) { + assert.Equal(t, dns.RcodeSuccess, dnsrr.RCode) + if ipVals := dnsrr.Response[dtyp]; assert.Len(t, ipVals, 1) { + assert.Equal(t, ipv4p2, ipVals[0]) + } + } + }) + + t.Run("disable_cname", func(t *testing.T) { + dtyp := dns.TypeA + host := path.Base(t.Name()) + + res, err := f.CheckHostRules(host, dtyp, setts) + assert.Nil(t, err) + assert.Equal(t, "", res.CanonName) + + if dnsrr := res.DNSRewriteResult; assert.NotNil(t, dnsrr) { + assert.Equal(t, dns.RcodeSuccess, dnsrr.RCode) + if ipVals := dnsrr.Response[dtyp]; assert.Len(t, ipVals, 1) { + assert.Equal(t, ipv4p1, ipVals[0]) + } + } + }) + + t.Run("disable_cname_many", func(t *testing.T) { + dtyp := dns.TypeA + host := path.Base(t.Name()) + + res, err := f.CheckHostRules(host, dtyp, setts) + assert.Nil(t, err) + assert.Equal(t, "new_cname_2", res.CanonName) + assert.Nil(t, res.DNSRewriteResult) + }) + + t.Run("disable_all", func(t *testing.T) { + dtyp := dns.TypeA + host := path.Base(t.Name()) + + res, err := f.CheckHostRules(host, dtyp, setts) + assert.Nil(t, err) + assert.Equal(t, "", res.CanonName) + assert.Len(t, res.Rules, 0) + }) +} diff --git a/internal/dnsfilter/rewrites.go b/internal/dnsfilter/rewrites.go index 9c042228..8db1fd0b 100644 --- a/internal/dnsfilter/rewrites.go +++ b/internal/dnsfilter/rewrites.go @@ -95,7 +95,7 @@ func (r *RewriteEntry) prepare() { } } -func (d *Dnsfilter) prepareRewrites() { +func (d *DNSFilter) prepareRewrites() { for i := range d.Rewrites { d.Rewrites[i].prepare() } @@ -148,8 +148,7 @@ type rewriteEntryJSON struct { Answer string `json:"answer"` } -func (d *Dnsfilter) handleRewriteList(w http.ResponseWriter, r *http.Request) { - +func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) { arr := []*rewriteEntryJSON{} d.confLock.Lock() @@ -170,8 +169,7 @@ func (d *Dnsfilter) handleRewriteList(w http.ResponseWriter, r *http.Request) { } } -func (d *Dnsfilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) { - +func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) { jsent := rewriteEntryJSON{} err := json.NewDecoder(r.Body).Decode(&jsent) if err != nil { @@ -193,8 +191,7 @@ func (d *Dnsfilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) { d.Config.ConfigModified() } -func (d *Dnsfilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) { - +func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) { jsent := rewriteEntryJSON{} err := json.NewDecoder(r.Body).Decode(&jsent) if err != nil { @@ -221,7 +218,7 @@ func (d *Dnsfilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) d.Config.ConfigModified() } -func (d *Dnsfilter) registerRewritesHandlers() { +func (d *DNSFilter) registerRewritesHandlers() { d.Config.HTTPRegister("GET", "/control/rewrite/list", d.handleRewriteList) d.Config.HTTPRegister("POST", "/control/rewrite/add", d.handleRewriteAdd) d.Config.HTTPRegister("POST", "/control/rewrite/delete", d.handleRewriteDelete) diff --git a/internal/dnsfilter/rewrites_test.go b/internal/dnsfilter/rewrites_test.go index 2ed8210e..4304b6de 100644 --- a/internal/dnsfilter/rewrites_test.go +++ b/internal/dnsfilter/rewrites_test.go @@ -9,16 +9,16 @@ import ( ) func TestRewrites(t *testing.T) { - d := Dnsfilter{} + d := DNSFilter{} // CNAME, A, AAAA d.Rewrites = []RewriteEntry{ - RewriteEntry{"somecname", "somehost.com", 0, nil}, - RewriteEntry{"somehost.com", "0.0.0.0", 0, nil}, + {"somecname", "somehost.com", 0, nil}, + {"somehost.com", "0.0.0.0", 0, nil}, - RewriteEntry{"host.com", "1.2.3.4", 0, nil}, - RewriteEntry{"host.com", "1.2.3.5", 0, nil}, - RewriteEntry{"host.com", "1:2:3::4", 0, nil}, - RewriteEntry{"www.host.com", "host.com", 0, nil}, + {"host.com", "1.2.3.4", 0, nil}, + {"host.com", "1.2.3.5", 0, nil}, + {"host.com", "1:2:3::4", 0, nil}, + {"www.host.com", "host.com", 0, nil}, } d.prepareRewrites() r := d.processRewrites("host2.com", dns.TypeA) @@ -39,8 +39,8 @@ func TestRewrites(t *testing.T) { // wildcard d.Rewrites = []RewriteEntry{ - RewriteEntry{"host.com", "1.2.3.4", 0, nil}, - RewriteEntry{"*.host.com", "1.2.3.5", 0, nil}, + {"host.com", "1.2.3.4", 0, nil}, + {"*.host.com", "1.2.3.5", 0, nil}, } d.prepareRewrites() r = d.processRewrites("host.com", dns.TypeA) @@ -56,8 +56,8 @@ func TestRewrites(t *testing.T) { // override a wildcard d.Rewrites = []RewriteEntry{ - RewriteEntry{"a.host.com", "1.2.3.4", 0, nil}, - RewriteEntry{"*.host.com", "1.2.3.5", 0, nil}, + {"a.host.com", "1.2.3.4", 0, nil}, + {"*.host.com", "1.2.3.5", 0, nil}, } d.prepareRewrites() r = d.processRewrites("a.host.com", dns.TypeA) @@ -67,8 +67,8 @@ func TestRewrites(t *testing.T) { // wildcard + CNAME d.Rewrites = []RewriteEntry{ - RewriteEntry{"host.com", "1.2.3.4", 0, nil}, - RewriteEntry{"*.host.com", "host.com", 0, nil}, + {"host.com", "1.2.3.4", 0, nil}, + {"*.host.com", "host.com", 0, nil}, } d.prepareRewrites() r = d.processRewrites("www.host.com", dns.TypeA) @@ -78,9 +78,9 @@ func TestRewrites(t *testing.T) { // 2 CNAMEs d.Rewrites = []RewriteEntry{ - RewriteEntry{"b.host.com", "a.host.com", 0, nil}, - RewriteEntry{"a.host.com", "host.com", 0, nil}, - RewriteEntry{"host.com", "1.2.3.4", 0, nil}, + {"b.host.com", "a.host.com", 0, nil}, + {"a.host.com", "host.com", 0, nil}, + {"host.com", "1.2.3.4", 0, nil}, } d.prepareRewrites() r = d.processRewrites("b.host.com", dns.TypeA) @@ -91,9 +91,9 @@ func TestRewrites(t *testing.T) { // 2 CNAMEs + wildcard d.Rewrites = []RewriteEntry{ - RewriteEntry{"b.host.com", "a.host.com", 0, nil}, - RewriteEntry{"a.host.com", "x.somehost.com", 0, nil}, - RewriteEntry{"*.somehost.com", "1.2.3.4", 0, nil}, + {"b.host.com", "a.host.com", 0, nil}, + {"a.host.com", "x.somehost.com", 0, nil}, + {"*.somehost.com", "1.2.3.4", 0, nil}, } d.prepareRewrites() r = d.processRewrites("b.host.com", dns.TypeA) @@ -104,12 +104,12 @@ func TestRewrites(t *testing.T) { } func TestRewritesLevels(t *testing.T) { - d := Dnsfilter{} + d := DNSFilter{} // exact host, wildcard L2, wildcard L3 d.Rewrites = []RewriteEntry{ - RewriteEntry{"host.com", "1.1.1.1", 0, nil}, - RewriteEntry{"*.host.com", "2.2.2.2", 0, nil}, - RewriteEntry{"*.sub.host.com", "3.3.3.3", 0, nil}, + {"host.com", "1.1.1.1", 0, nil}, + {"*.host.com", "2.2.2.2", 0, nil}, + {"*.sub.host.com", "3.3.3.3", 0, nil}, } d.prepareRewrites() @@ -133,11 +133,11 @@ func TestRewritesLevels(t *testing.T) { } func TestRewritesExceptionCNAME(t *testing.T) { - d := Dnsfilter{} + d := DNSFilter{} // wildcard; exception for a sub-domain d.Rewrites = []RewriteEntry{ - RewriteEntry{"*.host.com", "2.2.2.2", 0, nil}, - RewriteEntry{"sub.host.com", "sub.host.com", 0, nil}, + {"*.host.com", "2.2.2.2", 0, nil}, + {"sub.host.com", "sub.host.com", 0, nil}, } d.prepareRewrites() @@ -153,11 +153,11 @@ func TestRewritesExceptionCNAME(t *testing.T) { } func TestRewritesExceptionWC(t *testing.T) { - d := Dnsfilter{} + d := DNSFilter{} // wildcard; exception for a sub-wildcard d.Rewrites = []RewriteEntry{ - RewriteEntry{"*.host.com", "2.2.2.2", 0, nil}, - RewriteEntry{"*.sub.host.com", "*.sub.host.com", 0, nil}, + {"*.host.com", "2.2.2.2", 0, nil}, + {"*.sub.host.com", "*.sub.host.com", 0, nil}, } d.prepareRewrites() @@ -173,14 +173,14 @@ func TestRewritesExceptionWC(t *testing.T) { } func TestRewritesExceptionIP(t *testing.T) { - d := Dnsfilter{} + d := DNSFilter{} // exception for AAAA record d.Rewrites = []RewriteEntry{ - RewriteEntry{"host.com", "1.2.3.4", 0, nil}, - RewriteEntry{"host.com", "AAAA", 0, nil}, - RewriteEntry{"host2.com", "::1", 0, nil}, - RewriteEntry{"host2.com", "A", 0, nil}, - RewriteEntry{"host3.com", "A", 0, nil}, + {"host.com", "1.2.3.4", 0, nil}, + {"host.com", "AAAA", 0, nil}, + {"host2.com", "::1", 0, nil}, + {"host2.com", "A", 0, nil}, + {"host3.com", "A", 0, nil}, } d.prepareRewrites() diff --git a/internal/dnsfilter/safe_search.go b/internal/dnsfilter/safe_search.go deleted file mode 100644 index 6bd6bc94..00000000 --- a/internal/dnsfilter/safe_search.go +++ /dev/null @@ -1,148 +0,0 @@ -package dnsfilter - -import ( - "bytes" - "encoding/binary" - "encoding/gob" - "encoding/json" - "fmt" - "net" - "net/http" - "time" - - "github.com/AdguardTeam/golibs/cache" - "github.com/AdguardTeam/golibs/log" -) - -/* -expire byte[4] -res Result -*/ -func (d *Dnsfilter) setCacheResult(cache cache.Cache, host string, res Result) int { - var buf bytes.Buffer - - expire := uint(time.Now().Unix()) + d.Config.CacheTime*60 - exp := make([]byte, 4) - binary.BigEndian.PutUint32(exp, uint32(expire)) - _, _ = buf.Write(exp) - - enc := gob.NewEncoder(&buf) - err := enc.Encode(res) - if err != nil { - log.Error("gob.Encode(): %s", err) - return 0 - } - val := buf.Bytes() - _ = cache.Set([]byte(host), val) - return len(val) -} - -func getCachedResult(cache cache.Cache, host string) (Result, bool) { - data := cache.Get([]byte(host)) - if data == nil { - return Result{}, false - } - - exp := int(binary.BigEndian.Uint32(data[:4])) - if exp <= int(time.Now().Unix()) { - cache.Del([]byte(host)) - return Result{}, false - } - - var buf bytes.Buffer - buf.Write(data[4:]) - dec := gob.NewDecoder(&buf) - r := Result{} - err := dec.Decode(&r) - if err != nil { - log.Debug("gob.Decode(): %s", err) - return Result{}, false - } - - return r, true -} - -// SafeSearchDomain returns replacement address for search engine -func (d *Dnsfilter) SafeSearchDomain(host string) (string, bool) { - val, ok := safeSearchDomains[host] - return val, ok -} - -func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) { - if log.GetLevel() >= log.DEBUG { - timer := log.StartTimer() - defer timer.LogElapsed("SafeSearch: lookup for %s", host) - } - - // Check cache. Return cached result if it was found - cachedValue, isFound := getCachedResult(gctx.safeSearchCache, host) - if isFound { - // atomic.AddUint64(&gctx.stats.Safesearch.CacheHits, 1) - log.Tracef("SafeSearch: found in cache: %s", host) - return cachedValue, nil - } - - safeHost, ok := d.SafeSearchDomain(host) - if !ok { - return Result{}, nil - } - - res := Result{IsFiltered: true, Reason: FilteredSafeSearch} - if ip := net.ParseIP(safeHost); ip != nil { - res.IP = ip - valLen := d.setCacheResult(gctx.safeSearchCache, host, res) - log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, valLen) - return res, nil - } - - // TODO this address should be resolved with upstream that was configured in dnsforward - addrs, err := net.LookupIP(safeHost) - if err != nil { - log.Tracef("SafeSearchDomain for %s was found but failed to lookup for %s cause %s", host, safeHost, err) - return Result{}, err - } - - for _, i := range addrs { - if ipv4 := i.To4(); ipv4 != nil { - res.IP = ipv4 - break - } - } - - if len(res.IP) == 0 { - return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost) - } - - // Cache result - valLen := d.setCacheResult(gctx.safeSearchCache, host, res) - log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, valLen) - return res, nil -} - -func (d *Dnsfilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) { - d.Config.SafeSearchEnabled = true - d.Config.ConfigModified() -} - -func (d *Dnsfilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) { - d.Config.SafeSearchEnabled = false - d.Config.ConfigModified() -} - -func (d *Dnsfilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "enabled": d.Config.SafeSearchEnabled, - } - jsonVal, err := json.Marshal(data) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) - return - } - - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(jsonVal) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err) - return - } -} diff --git a/internal/dnsfilter/sb_pc.go b/internal/dnsfilter/safebrowsing.go similarity index 71% rename from internal/dnsfilter/sb_pc.go rename to internal/dnsfilter/safebrowsing.go index eb5457a6..f5aaca9f 100644 --- a/internal/dnsfilter/sb_pc.go +++ b/internal/dnsfilter/safebrowsing.go @@ -1,5 +1,3 @@ -// Safe Browsing, Parental Control - package dnsfilter import ( @@ -22,6 +20,8 @@ import ( "golang.org/x/net/publicsuffix" ) +// Safe browsing and parental control methods. + const ( dnsTimeout = 3 * time.Second defaultSafebrowsingServer = `https://dns-family.adguard.com/dns-query` @@ -30,7 +30,7 @@ const ( pcTXTSuffix = `pc.dns.adguard.com.` ) -func (d *Dnsfilter) initSecurityServices() error { +func (d *DNSFilter) initSecurityServices() error { var err error d.safeBrowsingServer = defaultSafebrowsingServer d.parentalServer = defaultParentalServer @@ -71,31 +71,35 @@ func (c *sbCtx) setCache(prefix, hashes []byte) { log.Debug("%s: stored in cache: %v", c.svc, prefix) } +// findInHash returns 32-byte hash if it's found in hashToHost. +func (c *sbCtx) findInHash(val []byte) (hash32 [32]byte, found bool) { + for i := 4; i < len(val); i += 32 { + hash := val[i : i+32] + + copy(hash32[:], hash[0:32]) + + _, found = c.hashToHost[hash32] + if found { + return hash32, found + } + } + + return [32]byte{}, false +} + func (c *sbCtx) getCached() int { now := time.Now().Unix() hashesToRequest := map[[32]byte]string{} for k, v := range c.hashToHost { key := k[0:2] val := c.cache.Get(key) - if val != nil { - expire := binary.BigEndian.Uint32(val) - if now >= int64(expire) { - val = nil - } else { - for i := 4; i < len(val); i += 32 { - hash := val[i : i+32] - var hash32 [32]byte - copy(hash32[:], hash[0:32]) - _, found := c.hashToHost[hash32] - if found { - log.Debug("%s: found in cache: %s: blocked by %v", c.svc, c.host, hash32) - return 1 - } - } - } - } - if val == nil { + if val == nil || now >= int64(binary.BigEndian.Uint32(val)) { hashesToRequest[k] = v + continue + } + if hash32, found := c.findInHash(val); found { + log.Debug("%s: found in cache: %s: blocked by %v", c.svc, c.host, hash32) + return 1 } } @@ -254,106 +258,75 @@ func (c *sbCtx) storeCache(hashes [][]byte) { } } -// Disabling "dupl": the algorithm of SB/PC is similar, but it uses different data -// nolint:dupl -func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) { +func check(c *sbCtx, r Result, u upstream.Upstream) (Result, error) { + c.hashToHost = hostnameToHashes(c.host) + switch c.getCached() { + case -1: + return Result{}, nil + case 1: + return r, nil + } + + question := c.getQuestion() + + log.Tracef("%s: checking %s: %s", c.svc, c.host, question) + req := (&dns.Msg{}).SetQuestion(question, dns.TypeTXT) + + resp, err := u.Exchange(req) + if err != nil { + return Result{}, err + } + + matched, receivedHashes := c.processTXT(resp) + + c.storeCache(receivedHashes) + if matched { + return r, nil + } + + return Result{}, nil +} + +func (d *DNSFilter) checkSafeBrowsing(host string) (Result, error) { if log.GetLevel() >= log.DEBUG { timer := log.StartTimer() defer timer.LogElapsed("SafeBrowsing lookup for %s", host) } - - result := Result{} - hashes := hostnameToHashes(host) - - c := &sbCtx{ - host: host, - svc: "SafeBrowsing", - hashToHost: hashes, - cache: gctx.safebrowsingCache, - cacheTime: d.Config.CacheTime, + ctx := &sbCtx{ + host: host, + svc: "SafeBrowsing", + cache: gctx.safebrowsingCache, + cacheTime: d.Config.CacheTime, } - - // check cache - match := c.getCached() - if match < 0 { - return result, nil - } else if match > 0 { - result.IsFiltered = true - result.Reason = FilteredSafeBrowsing - result.Rule = "adguard-malware-shavar" - return result, nil + res := Result{ + IsFiltered: true, + Reason: FilteredSafeBrowsing, + Rules: []*ResultRule{{ + Text: "adguard-malware-shavar", + }}, } - - question := c.getQuestion() - log.Tracef("SafeBrowsing: checking %s: %s", host, question) - - req := dns.Msg{} - req.SetQuestion(question, dns.TypeTXT) - resp, err := d.safeBrowsingUpstream.Exchange(&req) - if err != nil { - return result, err - } - - matched, receivedHashes := c.processTXT(resp) - if matched { - result.IsFiltered = true - result.Reason = FilteredSafeBrowsing - result.Rule = "adguard-malware-shavar" - } - c.storeCache(receivedHashes) - - return result, nil + return check(ctx, res, d.safeBrowsingUpstream) } -// Disabling "dupl": the algorithm of SB/PC is similar, but it uses different data -// nolint:dupl -func (d *Dnsfilter) checkParental(host string) (Result, error) { +func (d *DNSFilter) checkParental(host string) (Result, error) { if log.GetLevel() >= log.DEBUG { timer := log.StartTimer() defer timer.LogElapsed("Parental lookup for %s", host) } - - result := Result{} - hashes := hostnameToHashes(host) - - c := &sbCtx{ - host: host, - svc: "Parental", - hashToHost: hashes, - cache: gctx.parentalCache, - cacheTime: d.Config.CacheTime, + ctx := &sbCtx{ + host: host, + svc: "Parental", + cache: gctx.parentalCache, + cacheTime: d.Config.CacheTime, } - - // check cache - match := c.getCached() - if match < 0 { - return result, nil - } else if match > 0 { - result.IsFiltered = true - result.Reason = FilteredParental - result.Rule = "parental CATEGORY_BLACKLISTED" - return result, nil + res := Result{ + IsFiltered: true, + Reason: FilteredParental, + Rules: []*ResultRule{{ + Text: "parental CATEGORY_BLACKLISTED", + }}, } - - question := c.getQuestion() - log.Tracef("Parental: checking %s: %s", host, question) - - req := dns.Msg{} - req.SetQuestion(question, dns.TypeTXT) - resp, err := d.parentalUpstream.Exchange(&req) - if err != nil { - return result, err - } - - matched, receivedHashes := c.processTXT(resp) - if matched { - result.IsFiltered = true - result.Reason = FilteredParental - result.Rule = "parental CATEGORY_BLACKLISTED" - } - c.storeCache(receivedHashes) - - return result, err + return check(ctx, res, d.parentalUpstream) } func httpError(r *http.Request, w http.ResponseWriter, code int, format string, args ...interface{}) { @@ -362,17 +335,17 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string, http.Error(w, text, code) } -func (d *Dnsfilter) handleSafeBrowsingEnable(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleSafeBrowsingEnable(w http.ResponseWriter, r *http.Request) { d.Config.SafeBrowsingEnabled = true d.Config.ConfigModified() } -func (d *Dnsfilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Request) { d.Config.SafeBrowsingEnabled = false d.Config.ConfigModified() } -func (d *Dnsfilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "enabled": d.Config.SafeBrowsingEnabled, } @@ -389,17 +362,17 @@ func (d *Dnsfilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Requ } } -func (d *Dnsfilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) { d.Config.ParentalEnabled = true d.Config.ConfigModified() } -func (d *Dnsfilter) handleParentalDisable(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request) { d.Config.ParentalEnabled = false d.Config.ConfigModified() } -func (d *Dnsfilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) { +func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "enabled": d.Config.ParentalEnabled, } @@ -417,7 +390,7 @@ func (d *Dnsfilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) } } -func (d *Dnsfilter) registerSecurityHandlers() { +func (d *DNSFilter) registerSecurityHandlers() { d.Config.HTTPRegister("POST", "/control/safebrowsing/enable", d.handleSafeBrowsingEnable) d.Config.HTTPRegister("POST", "/control/safebrowsing/disable", d.handleSafeBrowsingDisable) d.Config.HTTPRegister("GET", "/control/safebrowsing/status", d.handleSafeBrowsingStatus) diff --git a/internal/dnsfilter/sb_pc_test.go b/internal/dnsfilter/safebrowsing_test.go similarity index 100% rename from internal/dnsfilter/sb_pc_test.go rename to internal/dnsfilter/safebrowsing_test.go diff --git a/internal/dnsfilter/safesearch.go b/internal/dnsfilter/safesearch.go index db6554d0..4aefa5e1 100644 --- a/internal/dnsfilter/safesearch.go +++ b/internal/dnsfilter/safesearch.go @@ -1,5 +1,155 @@ package dnsfilter +import ( + "bytes" + "encoding/binary" + "encoding/gob" + "encoding/json" + "fmt" + "net" + "net/http" + "time" + + "github.com/AdguardTeam/golibs/cache" + "github.com/AdguardTeam/golibs/log" +) + +/* +expire byte[4] +res Result +*/ +func (d *DNSFilter) setCacheResult(cache cache.Cache, host string, res Result) int { + var buf bytes.Buffer + + expire := uint(time.Now().Unix()) + d.Config.CacheTime*60 + exp := make([]byte, 4) + binary.BigEndian.PutUint32(exp, uint32(expire)) + _, _ = buf.Write(exp) + + enc := gob.NewEncoder(&buf) + err := enc.Encode(res) + if err != nil { + log.Error("gob.Encode(): %s", err) + return 0 + } + val := buf.Bytes() + _ = cache.Set([]byte(host), val) + return len(val) +} + +func getCachedResult(cache cache.Cache, host string) (Result, bool) { + data := cache.Get([]byte(host)) + if data == nil { + return Result{}, false + } + + exp := int(binary.BigEndian.Uint32(data[:4])) + if exp <= int(time.Now().Unix()) { + cache.Del([]byte(host)) + return Result{}, false + } + + var buf bytes.Buffer + buf.Write(data[4:]) + dec := gob.NewDecoder(&buf) + r := Result{} + err := dec.Decode(&r) + if err != nil { + log.Debug("gob.Decode(): %s", err) + return Result{}, false + } + + return r, true +} + +// SafeSearchDomain returns replacement address for search engine +func (d *DNSFilter) SafeSearchDomain(host string) (string, bool) { + val, ok := safeSearchDomains[host] + return val, ok +} + +func (d *DNSFilter) checkSafeSearch(host string) (Result, error) { + if log.GetLevel() >= log.DEBUG { + timer := log.StartTimer() + defer timer.LogElapsed("SafeSearch: lookup for %s", host) + } + + // Check cache. Return cached result if it was found + cachedValue, isFound := getCachedResult(gctx.safeSearchCache, host) + if isFound { + // atomic.AddUint64(&gctx.stats.Safesearch.CacheHits, 1) + log.Tracef("SafeSearch: found in cache: %s", host) + return cachedValue, nil + } + + safeHost, ok := d.SafeSearchDomain(host) + if !ok { + return Result{}, nil + } + + res := Result{ + IsFiltered: true, + Reason: FilteredSafeSearch, + Rules: []*ResultRule{{}}, + } + + if ip := net.ParseIP(safeHost); ip != nil { + res.Rules[0].IP = ip + valLen := d.setCacheResult(gctx.safeSearchCache, host, res) + log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, valLen) + + return res, nil + } + + // TODO this address should be resolved with upstream that was configured in dnsforward + ips, err := net.LookupIP(safeHost) + if err != nil { + log.Tracef("SafeSearchDomain for %s was found but failed to lookup for %s cause %s", host, safeHost, err) + return Result{}, err + } + + for _, ip := range ips { + if ipv4 := ip.To4(); ipv4 != nil { + res.Rules[0].IP = ipv4 + + l := d.setCacheResult(gctx.safeSearchCache, host, res) + log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, l) + + return res, nil + } + } + + return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost) +} + +func (d *DNSFilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) { + d.Config.SafeSearchEnabled = true + d.Config.ConfigModified() +} + +func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) { + d.Config.SafeSearchEnabled = false + d.Config.ConfigModified() +} + +func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) { + data := map[string]interface{}{ + "enabled": d.Config.SafeSearchEnabled, + } + jsonVal, err := json.Marshal(data) + if err != nil { + httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) + return + } + + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(jsonVal) + if err != nil { + httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err) + return + } +} + var safeSearchDomains = map[string]string{ "yandex.com": "213.180.193.56", "yandex.ru": "213.180.193.56", diff --git a/internal/dnsforward/access_test.go b/internal/dnsforward/access_test.go index b760d554..250b4931 100644 --- a/internal/dnsforward/access_test.go +++ b/internal/dnsforward/access_test.go @@ -50,7 +50,8 @@ func TestIsBlockedIPDisallowed(t *testing.T) { func TestIsBlockedIPBlockedDomain(t *testing.T) { a := &accessCtx{} - assert.True(t, a.Init(nil, nil, []string{"host1", + assert.True(t, a.Init(nil, nil, []string{ + "host1", "host2", "*.host.com", "||host3.com^", diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go index 93aa312b..881174d1 100644 --- a/internal/dnsforward/config.go +++ b/internal/dnsforward/config.go @@ -15,6 +15,7 @@ import ( "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/log" + "github.com/ameshkov/dnscrypt/v2" ) // FilteringConfig represents the DNS filtering configuration of AdGuard Home @@ -94,19 +95,33 @@ type FilteringConfig struct { type TLSConfig struct { TLSListenAddr *net.TCPAddr `yaml:"-" json:"-"` QUICListenAddr *net.UDPAddr `yaml:"-" json:"-"` - StrictSNICheck bool `yaml:"strict_sni_check" json:"-"` // Reject connection if the client uses server name (in SNI) that doesn't match the certificate - CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"` // PEM-encoded certificates chain - PrivateKey string `yaml:"private_key" json:"private_key"` // PEM-encoded private key + // Reject connection if the client uses server name (in SNI) that doesn't match the certificate + StrictSNICheck bool `yaml:"strict_sni_check" json:"-"` - CertificatePath string `yaml:"certificate_path" json:"certificate_path"` // certificate file name - PrivateKeyPath string `yaml:"private_key_path" json:"private_key_path"` // private key file name + // PEM-encoded certificates chain + CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"` + // PEM-encoded private key + PrivateKey string `yaml:"private_key" json:"private_key"` + + CertificatePath string `yaml:"certificate_path" json:"certificate_path"` + PrivateKeyPath string `yaml:"private_key_path" json:"private_key_path"` CertificateChainData []byte `yaml:"-" json:"-"` PrivateKeyData []byte `yaml:"-" json:"-"` - cert tls.Certificate // nolint(structcheck) - linter thinks that this field is unused, while TLSConfig is directly included into ServerConfig - dnsNames []string // nolint(structcheck) // DNS names from certificate (SAN) or CN value from Subject + cert tls.Certificate + // DNS names from certificate (SAN) or CN value from Subject + dnsNames []string +} + +// DNSCryptConfig is the DNSCrypt server configuration struct. +type DNSCryptConfig struct { + UDPListenAddr *net.UDPAddr + TCPListenAddr *net.TCPAddr + ProviderName string + ResolverCert *dnscrypt.Cert + Enabled bool } // ServerConfig represents server configuration. @@ -119,6 +134,7 @@ type ServerConfig struct { FilteringConfig TLSConfig + DNSCryptConfig TLSAllowUnencryptedDOH bool TLSv12Roots *x509.CertPool // list of root CAs for TLSv1.2 @@ -184,6 +200,13 @@ func (s *Server) createProxyConfig() (proxy.Config, error) { return proxyConfig, err } + if s.conf.DNSCryptConfig.Enabled { + proxyConfig.DNSCryptUDPListenAddr = []*net.UDPAddr{s.conf.DNSCryptConfig.UDPListenAddr} + proxyConfig.DNSCryptTCPListenAddr = []*net.TCPAddr{s.conf.DNSCryptConfig.TCPListenAddr} + proxyConfig.DNSCryptProviderName = s.conf.DNSCryptConfig.ProviderName + proxyConfig.DNSCryptResolverCert = s.conf.DNSCryptConfig.ResolverCert + } + // Validate proxy config if proxyConfig.UpstreamConfig == nil || len(proxyConfig.UpstreamConfig.Upstreams) == 0 { return proxyConfig, errors.New("no default upstream servers configured") diff --git a/internal/dnsforward/handle_dns.go b/internal/dnsforward/dns.go similarity index 98% rename from internal/dnsforward/handle_dns.go rename to internal/dnsforward/dns.go index 0f9c764b..d7208d1c 100644 --- a/internal/dnsforward/handle_dns.go +++ b/internal/dnsforward/dns.go @@ -366,7 +366,9 @@ func processFilteringAfterResponse(ctx *dnsContext) int { var err error switch res.Reason { - case dnsfilter.ReasonRewrite: + case dnsfilter.ReasonRewrite, + dnsfilter.DNSRewriteRule: + if len(ctx.origQuestion.Name) == 0 { // origQuestion is set in case we get only CNAME without IP from rewrites table break @@ -378,11 +380,11 @@ func processFilteringAfterResponse(ctx *dnsContext) int { if len(d.Res.Answer) != 0 { answer := []dns.RR{} answer = append(answer, s.genCNAMEAnswer(d.Req, res.CanonName)) - answer = append(answer, d.Res.Answer...) // host -> IP + answer = append(answer, d.Res.Answer...) d.Res.Answer = answer } - case dnsfilter.NotFilteredWhiteList: + case dnsfilter.NotFilteredAllowList: // nothing default: diff --git a/internal/dnsforward/dnsforward.go b/internal/dnsforward/dnsforward.go index 3640101c..f1b7e7d2 100644 --- a/internal/dnsforward/dnsforward.go +++ b/internal/dnsforward/dnsforward.go @@ -48,7 +48,7 @@ var webRegistered bool // The zero Server is empty and ready for use. type Server struct { dnsProxy *proxy.Proxy // DNS proxy instance - dnsFilter *dnsfilter.Dnsfilter // DNS filter instance + dnsFilter *dnsfilter.DNSFilter // DNS filter instance dhcpServer dhcpd.ServerInterface // DHCP server instance (optional) queryLog querylog.QueryLog // Query log instance stats stats.Stats @@ -74,7 +74,7 @@ type Server struct { // DNSCreateParams - parameters for NewServer() type DNSCreateParams struct { - DNSFilter *dnsfilter.Dnsfilter + DNSFilter *dnsfilter.DNSFilter Stats stats.Stats QueryLog querylog.QueryLog DHCPServer dhcpd.ServerInterface diff --git a/internal/dnsforward/dnsforward_test.go b/internal/dnsforward/dnsforward_test.go index 327fbe82..fdee8648 100644 --- a/internal/dnsforward/dnsforward_test.go +++ b/internal/dnsforward/dnsforward_test.go @@ -296,7 +296,7 @@ func TestBlockedRequest(t *testing.T) { func TestServerCustomClientUpstream(t *testing.T) { s := createTestServer(t) - s.conf.GetCustomUpstreamByClient = func(clientAddr string) *proxy.UpstreamConfig { + s.conf.GetCustomUpstreamByClient = func(_ string) *proxy.UpstreamConfig { uc := &proxy.UpstreamConfig{} u := &testUpstream{} u.ipv4 = map[string][]net.IP{} @@ -473,7 +473,7 @@ func TestBlockCNAME(t *testing.T) { func TestClientRulesForCNAMEMatching(t *testing.T) { s := createTestServer(t) testUpstm := &testUpstream{testCNAMEs, testIPv4, nil} - s.conf.FilterHandler = func(clientAddr string, settings *dnsfilter.RequestFilteringSettings) { + s.conf.FilterHandler = func(_ string, settings *dnsfilter.RequestFilteringSettings) { settings.FilteringEnabled = false } err := s.startWithUpstream(testUpstm) @@ -863,6 +863,8 @@ func sendTestMessages(t *testing.T, conn *dns.Conn) { } func exchangeAndAssertResponse(t *testing.T, client *dns.Client, addr net.Addr, host, ip string) { + t.Helper() + req := createTestMessage(host) reply, _, err := client.Exchange(req, addr.String()) if err != nil { @@ -900,6 +902,8 @@ func assertGoogleAResponse(t *testing.T, reply *dns.Msg) { } func assertResponse(t *testing.T, reply *dns.Msg, ip string) { + t.Helper() + if len(reply.Answer) != 1 { t.Fatalf("DNS server returned reply with wrong number of answers - %d", len(reply.Answer)) } diff --git a/internal/dnsforward/dnsrewrite.go b/internal/dnsforward/dnsrewrite.go new file mode 100644 index 00000000..01895323 --- /dev/null +++ b/internal/dnsforward/dnsrewrite.go @@ -0,0 +1,79 @@ +package dnsforward + +import ( + "fmt" + "net" + + "github.com/AdguardTeam/AdGuardHome/internal/agherr" + "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" + "github.com/AdguardTeam/dnsproxy/proxy" + "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/urlfilter/rules" + "github.com/miekg/dns" +) + +// filterDNSRewriteResponse handles a single DNS rewrite response entry. +// It returns the constructed answer resource record. +func (s *Server) filterDNSRewriteResponse(req *dns.Msg, rr rules.RRType, v rules.RRValue) (ans dns.RR, err error) { + switch rr { + case dns.TypeA, dns.TypeAAAA: + ip, ok := v.(net.IP) + if !ok { + return nil, fmt.Errorf("value has type %T, not net.IP", v) + } + + if rr == dns.TypeA { + return s.genAAnswer(req, ip.To4()), nil + } + + return s.genAAAAAnswer(req, ip), nil + case dns.TypeTXT: + str, ok := v.(string) + if !ok { + return nil, fmt.Errorf("value has type %T, not string", v) + } + + return s.genTXTAnswer(req, []string{str}), nil + default: + log.Debug("don't know how to handle dns rr type %d, skipping", rr) + + return nil, nil + } +} + +// filterDNSRewrite handles dnsrewrite filters. It constructs a DNS +// response and sets it into d.Res. +func (s *Server) filterDNSRewrite(req *dns.Msg, res dnsfilter.Result, d *proxy.DNSContext) (err error) { + resp := s.makeResponse(req) + dnsrr := res.DNSRewriteResult + if dnsrr == nil { + return agherr.Error("no dns rewrite rule content") + } + + resp.Rcode = dnsrr.RCode + if resp.Rcode != dns.RcodeSuccess { + d.Res = resp + + return nil + } + + if dnsrr.Response == nil { + return agherr.Error("no dns rewrite rule responses") + } + + rr := req.Question[0].Qtype + values := dnsrr.Response[rr] + for i, v := range values { + var ans dns.RR + ans, err = s.filterDNSRewriteResponse(req, rr, v) + if err != nil { + return fmt.Errorf("dns rewrite response for %d[%d]: %w", rr, i, err) + } + + resp.Answer = append(resp.Answer, ans) + } + + d.Res = resp + + return nil +} diff --git a/internal/dnsforward/filter.go b/internal/dnsforward/filter.go index 11267adf..5cd0090a 100644 --- a/internal/dnsforward/filter.go +++ b/internal/dnsforward/filter.go @@ -42,7 +42,8 @@ func (s *Server) getClientRequestFilteringSettings(d *proxy.DNSContext) *dnsfilt return &setts } -// filterDNSRequest applies the dnsFilter and sets d.Res if the request was filtered +// filterDNSRequest applies the dnsFilter and sets d.Res if the request +// was filtered. func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) { d := ctx.proxyCtx req := d.Req @@ -52,13 +53,17 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) { // Return immediately if there's an error return nil, fmt.Errorf("dnsfilter failed to check host %q: %w", host, err) } else if res.IsFiltered { - log.Tracef("Host %s is filtered, reason - %q, matched rule: %q", host, res.Reason, res.Rule) + log.Tracef("Host %s is filtered, reason - %q, matched rule: %q", host, res.Reason, res.Rules[0].Text) d.Res = s.genDNSFilterMessage(d, &res) - } else if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 && len(res.IPList) == 0 { + } else if res.Reason.In(dnsfilter.ReasonRewrite, dnsfilter.DNSRewriteRule) && + res.CanonName != "" && + len(res.IPList) == 0 { + // Resolve the new canonical name, not the original host + // name. The original question is readded in + // processFilteringAfterResponse. ctx.origQuestion = d.Req.Question[0] - // resolve canonical name, not the original host name d.Req.Question[0].Name = dns.Fqdn(res.CanonName) - } else if res.Reason == dnsfilter.RewriteEtcHosts && len(res.ReverseHosts) != 0 { + } else if res.Reason == dnsfilter.RewriteAutoHosts && len(res.ReverseHosts) != 0 { resp := s.makeResponse(req) for _, h := range res.ReverseHosts { hdr := dns.RR_Header{ @@ -77,7 +82,7 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) { } d.Res = resp - } else if res.Reason == dnsfilter.ReasonRewrite || res.Reason == dnsfilter.RewriteEtcHosts { + } else if res.Reason == dnsfilter.ReasonRewrite || res.Reason == dnsfilter.RewriteAutoHosts { resp := s.makeResponse(req) name := host @@ -99,6 +104,11 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) { } d.Res = resp + } else if res.Reason == dnsfilter.DNSRewriteRule { + err = s.filterDNSRewrite(req, res, d) + if err != nil { + return nil, err + } } return &res, err diff --git a/internal/dnsforward/dnsforward_http.go b/internal/dnsforward/http.go similarity index 59% rename from internal/dnsforward/dnsforward_http.go rename to internal/dnsforward/http.go index 1e156c50..e24ba89e 100644 --- a/internal/dnsforward/dnsforward_http.go +++ b/internal/dnsforward/http.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/AdguardTeam/dnsproxy/upstream" - "github.com/AdguardTeam/golibs/jsonutil" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/utils" "github.com/miekg/dns" @@ -21,232 +20,293 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string, http.Error(w, text, code) } -type dnsConfigJSON struct { - Upstreams []string `json:"upstream_dns"` - UpstreamsFile string `json:"upstream_dns_file"` - Bootstraps []string `json:"bootstrap_dns"` +type dnsConfig struct { + Upstreams *[]string `json:"upstream_dns"` + UpstreamsFile *string `json:"upstream_dns_file"` + Bootstraps *[]string `json:"bootstrap_dns"` - ProtectionEnabled bool `json:"protection_enabled"` - RateLimit uint32 `json:"ratelimit"` - BlockingMode string `json:"blocking_mode"` - BlockingIPv4 string `json:"blocking_ipv4"` - BlockingIPv6 string `json:"blocking_ipv6"` - EDNSCSEnabled bool `json:"edns_cs_enabled"` - DNSSECEnabled bool `json:"dnssec_enabled"` - DisableIPv6 bool `json:"disable_ipv6"` - UpstreamMode string `json:"upstream_mode"` - CacheSize uint32 `json:"cache_size"` - CacheMinTTL uint32 `json:"cache_ttl_min"` - CacheMaxTTL uint32 `json:"cache_ttl_max"` + ProtectionEnabled *bool `json:"protection_enabled"` + RateLimit *uint32 `json:"ratelimit"` + BlockingMode *string `json:"blocking_mode"` + BlockingIPv4 *string `json:"blocking_ipv4"` + BlockingIPv6 *string `json:"blocking_ipv6"` + EDNSCSEnabled *bool `json:"edns_cs_enabled"` + DNSSECEnabled *bool `json:"dnssec_enabled"` + DisableIPv6 *bool `json:"disable_ipv6"` + UpstreamMode *string `json:"upstream_mode"` + CacheSize *uint32 `json:"cache_size"` + CacheMinTTL *uint32 `json:"cache_ttl_min"` + CacheMaxTTL *uint32 `json:"cache_ttl_max"` +} + +func (s *Server) getDNSConfig() dnsConfig { + s.RLock() + upstreams := stringArrayDup(s.conf.UpstreamDNS) + upstreamFile := s.conf.UpstreamDNSFileName + bootstraps := stringArrayDup(s.conf.BootstrapDNS) + protectionEnabled := s.conf.ProtectionEnabled + blockingMode := s.conf.BlockingMode + BlockingIPv4 := s.conf.BlockingIPv4 + BlockingIPv6 := s.conf.BlockingIPv6 + Ratelimit := s.conf.Ratelimit + EnableEDNSClientSubnet := s.conf.EnableEDNSClientSubnet + EnableDNSSEC := s.conf.EnableDNSSEC + AAAADisabled := s.conf.AAAADisabled + CacheSize := s.conf.CacheSize + CacheMinTTL := s.conf.CacheMinTTL + CacheMaxTTL := s.conf.CacheMaxTTL + var upstreamMode string + if s.conf.FastestAddr { + upstreamMode = "fastest_addr" + } else if s.conf.AllServers { + upstreamMode = "parallel" + } + s.RUnlock() + return dnsConfig{ + Upstreams: &upstreams, + UpstreamsFile: &upstreamFile, + Bootstraps: &bootstraps, + ProtectionEnabled: &protectionEnabled, + BlockingMode: &blockingMode, + BlockingIPv4: &BlockingIPv4, + BlockingIPv6: &BlockingIPv6, + RateLimit: &Ratelimit, + EDNSCSEnabled: &EnableEDNSClientSubnet, + DNSSECEnabled: &EnableDNSSEC, + DisableIPv6: &AAAADisabled, + CacheSize: &CacheSize, + CacheMinTTL: &CacheMinTTL, + CacheMaxTTL: &CacheMaxTTL, + UpstreamMode: &upstreamMode, + } } func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { - resp := dnsConfigJSON{} - s.RLock() - resp.Upstreams = stringArrayDup(s.conf.UpstreamDNS) - resp.UpstreamsFile = s.conf.UpstreamDNSFileName - resp.Bootstraps = stringArrayDup(s.conf.BootstrapDNS) + resp := s.getDNSConfig() - resp.ProtectionEnabled = s.conf.ProtectionEnabled - resp.BlockingMode = s.conf.BlockingMode - resp.BlockingIPv4 = s.conf.BlockingIPv4 - resp.BlockingIPv6 = s.conf.BlockingIPv6 - resp.RateLimit = s.conf.Ratelimit - resp.EDNSCSEnabled = s.conf.EnableEDNSClientSubnet - resp.DNSSECEnabled = s.conf.EnableDNSSEC - resp.DisableIPv6 = s.conf.AAAADisabled - resp.CacheSize = s.conf.CacheSize - resp.CacheMinTTL = s.conf.CacheMinTTL - resp.CacheMaxTTL = s.conf.CacheMaxTTL - if s.conf.FastestAddr { - resp.UpstreamMode = "fastest_addr" - } else if s.conf.AllServers { - resp.UpstreamMode = "parallel" - } - s.RUnlock() - - js, err := json.Marshal(resp) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "json.Marshal: %s", err) - return - } w.Header().Set("Content-Type", "application/json") - _, _ = w.Write(js) + + enc := json.NewEncoder(w) + if err := enc.Encode(resp); err != nil { + httpError(r, w, http.StatusInternalServerError, "json.Encoder: %s", err) + return + } } -func checkBlockingMode(req dnsConfigJSON) bool { - bm := req.BlockingMode - if !(bm == "default" || bm == "refused" || bm == "nxdomain" || bm == "null_ip" || bm == "custom_ip") { - return false +func (req *dnsConfig) checkBlockingMode() bool { + if req.BlockingMode == nil { + return true } + bm := *req.BlockingMode if bm == "custom_ip" { - ip := net.ParseIP(req.BlockingIPv4) - if ip == nil || ip.To4() == nil { + if req.BlockingIPv4 == nil || req.BlockingIPv6 == nil { return false } - ip = net.ParseIP(req.BlockingIPv6) - if ip == nil { + ip4 := net.ParseIP(*req.BlockingIPv4) + if ip4 == nil || ip4.To4() == nil { return false } + + ip6 := net.ParseIP(*req.BlockingIPv6) + return ip6 != nil + } + + for _, valid := range []string{ + "default", + "refused", + "nxdomain", + "null_ip", + } { + if bm == valid { + return true + } } - return true + return false } -// Validate bootstrap server address -func checkBootstrap(addr string) error { - if addr == "" { // additional check is required because NewResolver() allows empty address - return fmt.Errorf("invalid bootstrap server address: empty") +func (req *dnsConfig) checkUpstreamsMode() bool { + if req.UpstreamMode == nil { + return true } - _, err := upstream.NewResolver(addr, 0) - if err != nil { - return fmt.Errorf("invalid bootstrap server address: %w", err) + + for _, valid := range []string{ + "", + "fastest_addr", + "parallel", + } { + if *req.UpstreamMode == valid { + return true + } } - return nil + + return false +} + +func (req *dnsConfig) checkBootstrap() (string, error) { + if req.Bootstraps == nil { + return "", nil + } + + for _, boot := range *req.Bootstraps { + if boot == "" { + return boot, fmt.Errorf("invalid bootstrap server address: empty") + } + + if _, err := upstream.NewResolver(boot, 0); err != nil { + return boot, fmt.Errorf("invalid bootstrap server address: %w", err) + } + } + + return "", nil +} + +func (req *dnsConfig) checkCacheTTL() bool { + if req.CacheMinTTL == nil && req.CacheMaxTTL == nil { + return true + } + + var min, max uint32 + if req.CacheMinTTL != nil { + min = *req.CacheMinTTL + } + if req.CacheMaxTTL != nil { + max = *req.CacheMaxTTL + } + + return min <= max } -// nolint(gocyclo) - we need to check each JSON field separately func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) { - req := dnsConfigJSON{} - js, err := jsonutil.DecodeObject(&req, r.Body) - if err != nil { - httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err) + req := dnsConfig{} + dec := json.NewDecoder(r.Body) + if err := dec.Decode(&req); err != nil { + httpError(r, w, http.StatusBadRequest, "json Encode: %s", err) return } - if js.Exists("upstream_dns") { - err = ValidateUpstreams(req.Upstreams) - if err != nil { + if req.Upstreams != nil { + if err := ValidateUpstreams(*req.Upstreams); err != nil { httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err) return } } - if js.Exists("bootstrap_dns") { - for _, boot := range req.Bootstraps { - if err := checkBootstrap(boot); err != nil { - httpError(r, w, http.StatusBadRequest, "%s can not be used as bootstrap dns cause: %s", boot, err) - return - } - } + if errBoot, err := req.checkBootstrap(); err != nil { + httpError(r, w, http.StatusBadRequest, "%s can not be used as bootstrap dns cause: %s", errBoot, err) + return } - if js.Exists("blocking_mode") && !checkBlockingMode(req) { + if !req.checkBlockingMode() { httpError(r, w, http.StatusBadRequest, "blocking_mode: incorrect value") return } - if js.Exists("upstream_mode") && - !(req.UpstreamMode == "" || req.UpstreamMode == "fastest_addr" || req.UpstreamMode == "parallel") { + if !req.checkUpstreamsMode() { httpError(r, w, http.StatusBadRequest, "upstream_mode: incorrect value") return } - if req.CacheMinTTL > req.CacheMaxTTL { + if !req.checkCacheTTL() { httpError(r, w, http.StatusBadRequest, "cache_ttl_min must be less or equal than cache_ttl_max") return } - restart := false - s.Lock() - - if js.Exists("upstream_dns") { - s.conf.UpstreamDNS = req.Upstreams - restart = true - } - - if js.Exists("upstream_dns_file") { - s.conf.UpstreamDNSFileName = req.UpstreamsFile - restart = true - } - - if js.Exists("bootstrap_dns") { - s.conf.BootstrapDNS = req.Bootstraps - restart = true - } - - if js.Exists("protection_enabled") { - s.conf.ProtectionEnabled = req.ProtectionEnabled - } - - if js.Exists("blocking_mode") { - s.conf.BlockingMode = req.BlockingMode - if req.BlockingMode == "custom_ip" { - if js.Exists("blocking_ipv4") { - s.conf.BlockingIPv4 = req.BlockingIPv4 - s.conf.BlockingIPAddrv4 = net.ParseIP(req.BlockingIPv4) - } - if js.Exists("blocking_ipv6") { - s.conf.BlockingIPv6 = req.BlockingIPv6 - s.conf.BlockingIPAddrv6 = net.ParseIP(req.BlockingIPv6) - } - } - } - - if js.Exists("ratelimit") { - if s.conf.Ratelimit != req.RateLimit { - restart = true - } - s.conf.Ratelimit = req.RateLimit - } - - if js.Exists("edns_cs_enabled") { - s.conf.EnableEDNSClientSubnet = req.EDNSCSEnabled - restart = true - } - - if js.Exists("dnssec_enabled") { - s.conf.EnableDNSSEC = req.DNSSECEnabled - } - - if js.Exists("disable_ipv6") { - s.conf.AAAADisabled = req.DisableIPv6 - } - - if js.Exists("cache_size") { - s.conf.CacheSize = req.CacheSize - restart = true - } - - if js.Exists("cache_ttl_min") { - s.conf.CacheMinTTL = req.CacheMinTTL - restart = true - } - - if js.Exists("cache_ttl_max") { - s.conf.CacheMaxTTL = req.CacheMaxTTL - restart = true - } - - if js.Exists("upstream_mode") { - s.conf.FastestAddr = false - s.conf.AllServers = false - switch req.UpstreamMode { - case "": - // - - case "parallel": - s.conf.AllServers = true - - case "fastest_addr": - s.conf.FastestAddr = true - } - } - - s.Unlock() - s.conf.ConfigModified() - - if restart { - err = s.Reconfigure(nil) - if err != nil { + if s.setConfig(req) { + if err := s.Reconfigure(nil); err != nil { httpError(r, w, http.StatusInternalServerError, "%s", err) return } } } +func (s *Server) setConfig(dc dnsConfig) (restart bool) { + s.Lock() + + if dc.Upstreams != nil { + s.conf.UpstreamDNS = *dc.Upstreams + restart = true + } + + if dc.UpstreamsFile != nil { + s.conf.UpstreamDNSFileName = *dc.UpstreamsFile + restart = true + } + + if dc.Bootstraps != nil { + s.conf.BootstrapDNS = *dc.Bootstraps + restart = true + } + + if dc.ProtectionEnabled != nil { + s.conf.ProtectionEnabled = *dc.ProtectionEnabled + } + + if dc.BlockingMode != nil { + s.conf.BlockingMode = *dc.BlockingMode + if *dc.BlockingMode == "custom_ip" { + s.conf.BlockingIPv4 = *dc.BlockingIPv4 + s.conf.BlockingIPAddrv4 = net.ParseIP(*dc.BlockingIPv4) + s.conf.BlockingIPv6 = *dc.BlockingIPv6 + s.conf.BlockingIPAddrv6 = net.ParseIP(*dc.BlockingIPv6) + } + } + + if dc.RateLimit != nil { + if s.conf.Ratelimit != *dc.RateLimit { + restart = true + } + s.conf.Ratelimit = *dc.RateLimit + } + + if dc.EDNSCSEnabled != nil { + s.conf.EnableEDNSClientSubnet = *dc.EDNSCSEnabled + restart = true + } + + if dc.DNSSECEnabled != nil { + s.conf.EnableDNSSEC = *dc.DNSSECEnabled + } + + if dc.DisableIPv6 != nil { + s.conf.AAAADisabled = *dc.DisableIPv6 + } + + if dc.CacheSize != nil { + s.conf.CacheSize = *dc.CacheSize + restart = true + } + + if dc.CacheMinTTL != nil { + s.conf.CacheMinTTL = *dc.CacheMinTTL + restart = true + } + + if dc.CacheMaxTTL != nil { + s.conf.CacheMaxTTL = *dc.CacheMaxTTL + restart = true + } + + if dc.UpstreamMode != nil { + switch *dc.UpstreamMode { + case "parallel": + s.conf.AllServers = true + s.conf.FastestAddr = false + case "fastest_addr": + s.conf.AllServers = false + s.conf.FastestAddr = true + default: + s.conf.AllServers = false + s.conf.FastestAddr = false + } + } + s.Unlock() + s.conf.ConfigModified() + return restart +} + type upstreamJSON struct { Upstreams []string `json:"upstream_dns"` // Upstreams BootstrapDNS []string `json:"bootstrap_dns"` // Bootstrap DNS diff --git a/internal/dnsforward/dnsforward_http_test.go b/internal/dnsforward/http_test.go similarity index 94% rename from internal/dnsforward/dnsforward_http_test.go rename to internal/dnsforward/http_test.go index 19c4a0de..c8e2f9c5 100644 --- a/internal/dnsforward/dnsforward_http_test.go +++ b/internal/dnsforward/http_test.go @@ -29,7 +29,7 @@ func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) { conf: func() ServerConfig { return defaultConf }, - want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "fastest_addr", conf: func() ServerConfig { @@ -37,7 +37,7 @@ func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) { conf.FastestAddr = true return conf }, - want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"fastest_addr\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"fastest_addr\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "parallel", conf: func() ServerConfig { @@ -45,7 +45,7 @@ func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) { conf.AllServers = true return conf }, - want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"parallel\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"parallel\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }} for _, tc := range testCases { @@ -73,7 +73,7 @@ func TestDNSForwardHTTTP_handleSetConfig(t *testing.T) { w := httptest.NewRecorder() - const defaultConfJSON = "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}" + const defaultConfJSON = "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n" testCases := []struct { name string req string @@ -83,52 +83,52 @@ func TestDNSForwardHTTTP_handleSetConfig(t *testing.T) { name: "upstream_dns", req: "{\"upstream_dns\":[\"8.8.8.8:77\",\"8.8.4.4:77\"]}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:77\",\"8.8.4.4:77\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:77\",\"8.8.4.4:77\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "bootstraps", req: "{\"bootstrap_dns\":[\"9.9.9.10\"]}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "blocking_mode_good", req: "{\"blocking_mode\":\"refused\"}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"refused\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"refused\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "blocking_mode_bad", req: "{\"blocking_mode\":\"custom_ip\"}", wantSet: "blocking_mode: incorrect value\n", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "ratelimit", req: "{\"ratelimit\":6}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":6,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":6,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "edns_cs_enabled", req: "{\"edns_cs_enabled\":true}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":true,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":true,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "dnssec_enabled", req: "{\"dnssec_enabled\":true}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":true,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":true,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "cache_size", req: "{\"cache_size\":1024}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":1024,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":1024,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "upstream_mode_parallel", req: "{\"upstream_mode\":\"parallel\"}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"parallel\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"parallel\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "upstream_mode_fastest_addr", req: "{\"upstream_mode\":\"fastest_addr\"}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"fastest_addr\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"fastest_addr\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", }, { name: "upstream_dns_bad", req: "{\"upstream_dns\":[\"\"]}", diff --git a/internal/dnsforward/msg.go b/internal/dnsforward/msg.go index d0811df4..f8200056 100644 --- a/internal/dnsforward/msg.go +++ b/internal/dnsforward/msg.go @@ -1,22 +1,27 @@ package dnsforward import ( - "log" "net" "time" "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" "github.com/AdguardTeam/dnsproxy/proxy" + "github.com/AdguardTeam/golibs/log" "github.com/miekg/dns" ) // Create a DNS response by DNS request and set necessary flags -func (s *Server) makeResponse(req *dns.Msg) *dns.Msg { - resp := dns.Msg{} +func (s *Server) makeResponse(req *dns.Msg) (resp *dns.Msg) { + resp = &dns.Msg{ + MsgHdr: dns.MsgHdr{ + RecursionAvailable: true, + }, + Compress: true, + } + resp.SetReply(req) - resp.RecursionAvailable = true - resp.Compress = true - return &resp + + return resp } // genDNSFilterMessage generates a DNS message corresponding to the filtering result @@ -39,8 +44,10 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Resu // If the query was filtered by "Safe search", dnsfilter also must return // the IP address that must be used in response. // In this case regardless of the filtering method, we should return it - if result.Reason == dnsfilter.FilteredSafeSearch && result.IP != nil { - return s.genResponseWithIP(m, result.IP) + if result.Reason == dnsfilter.FilteredSafeSearch && + len(result.Rules) > 0 && + result.Rules[0].IP != nil { + return s.genResponseWithIP(m, result.Rules[0].IP) } if s.conf.BlockingMode == "null_ip" { @@ -68,8 +75,8 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Resu // Default blocking mode // If there's an IP specified in the rule, return it // For host-type rules, return null IP - if result.IP != nil { - return s.genResponseWithIP(m, result.IP) + if len(result.Rules) > 0 && result.Rules[0].IP != nil { + return s.genResponseWithIP(m, result.Rules[0].IP) } return s.makeResponseNullIP(m) @@ -119,6 +126,18 @@ func (s *Server) genAAAAAnswer(req *dns.Msg, ip net.IP) *dns.AAAA { return answer } +func (s *Server) genTXTAnswer(req *dns.Msg, strs []string) (answer *dns.TXT) { + return &dns.TXT{ + Hdr: dns.RR_Header{ + Name: req.Question[0].Name, + Rrtype: dns.TypeTXT, + Ttl: s.conf.BlockedResponseTTL, + Class: dns.ClassINET, + }, + Txt: strs, + } +} + // generate DNS response message with an IP address func (s *Server) genResponseWithIP(req *dns.Msg, ip net.IP) *dns.Msg { if req.Question[0].Qtype == dns.TypeA && ip.To4() != nil { diff --git a/internal/dnsforward/stats.go b/internal/dnsforward/stats.go index c2b8921f..c447be05 100644 --- a/internal/dnsforward/stats.go +++ b/internal/dnsforward/stats.go @@ -91,7 +91,7 @@ func (s *Server) updateStats(d *proxy.DNSContext, elapsed time.Duration, res dns case dnsfilter.FilteredSafeSearch: e.Result = stats.RSafeSearch - case dnsfilter.FilteredBlackList: + case dnsfilter.FilteredBlockList: fallthrough case dnsfilter.FilteredInvalid: fallthrough diff --git a/internal/home/auth.go b/internal/home/auth.go index ca3f653a..00407fa0 100644 --- a/internal/home/auth.go +++ b/internal/home/auth.go @@ -1,12 +1,14 @@ package home import ( + "crypto/rand" "crypto/sha256" "encoding/binary" "encoding/hex" "encoding/json" "fmt" - "math/rand" + "math" + "math/big" "net/http" "strings" "sync" @@ -76,7 +78,6 @@ func InitAuth(dbFilename string, users []User, sessionTTL uint32) *Auth { a := Auth{} a.sessionTTL = sessionTTL a.sessions = make(map[string]*session) - rand.Seed(time.Now().UTC().Unix()) var err error a.db, err = bbolt.Open(dbFilename, 0o644, nil) if err != nil { @@ -275,23 +276,28 @@ type loginJSON struct { Password string `json:"password"` } -func getSession(u *User) []byte { - // the developers don't currently believe that using a - // non-cryptographic RNG for the session hash salt is - // insecure - salt := rand.Uint32() //nolint:gosec - d := []byte(fmt.Sprintf("%d%s%s", salt, u.Name, u.PasswordHash)) - hash := sha256.Sum256(d) - return hash[:] -} - -func (a *Auth) httpCookie(req loginJSON) string { - u := a.UserFind(req.Name, req.Password) - if len(u.Name) == 0 { - return "" +func getSession(u *User) ([]byte, error) { + maxSalt := big.NewInt(math.MaxUint32) + salt, err := rand.Int(rand.Reader, maxSalt) + if err != nil { + return nil, err } - sess := getSession(&u) + d := []byte(fmt.Sprintf("%s%s%s", salt, u.Name, u.PasswordHash)) + hash := sha256.Sum256(d) + return hash[:], nil +} + +func (a *Auth) httpCookie(req loginJSON) (string, error) { + u := a.UserFind(req.Name, req.Password) + if len(u.Name) == 0 { + return "", nil + } + + sess, err := getSession(&u) + if err != nil { + return "", err + } now := time.Now().UTC() expire := now.Add(cookieTTL * time.Hour) @@ -305,7 +311,7 @@ func (a *Auth) httpCookie(req loginJSON) string { a.addSession(sess, &s) return fmt.Sprintf("%s=%s; Path=/; HttpOnly; Expires=%s", - sessionCookieName, hex.EncodeToString(sess), expstr) + sessionCookieName, hex.EncodeToString(sess), expstr), nil } func handleLogin(w http.ResponseWriter, r *http.Request) { @@ -316,7 +322,11 @@ func handleLogin(w http.ResponseWriter, r *http.Request) { return } - cookie := Context.auth.httpCookie(req) + cookie, err := Context.auth.httpCookie(req) + if err != nil { + httpError(w, http.StatusBadRequest, "crypto rand reader: %s", err) + return + } if len(cookie) == 0 { log.Info("Auth: invalid user name or password: name=%q", req.Name) time.Sleep(1 * time.Second) @@ -350,7 +360,7 @@ func handleLogout(w http.ResponseWriter, r *http.Request) { // RegisterAuthHandlers - register handlers func RegisterAuthHandlers() { - http.Handle("/control/login", postInstallHandler(ensureHandler("POST", handleLogin))) + Context.mux.Handle("/control/login", postInstallHandler(ensureHandler("POST", handleLogin))) httpRegister("GET", "/control/logout", handleLogout) } @@ -369,7 +379,54 @@ func parseCookie(cookie string) string { return "" } -// nolint(gocyclo) +// optionalAuthThird return true if user should authenticate first. +func optionalAuthThird(w http.ResponseWriter, r *http.Request) (authFirst bool) { + authFirst = false + + // redirect to login page if not authenticated + ok := false + cookie, err := r.Cookie(sessionCookieName) + + if glProcessCookie(r) { + log.Debug("Auth: authentification was handled by GL-Inet submodule") + ok = true + + } else if err == nil { + r := Context.auth.CheckSession(cookie.Value) + if r == 0 { + ok = true + } else if r < 0 { + log.Debug("Auth: invalid cookie value: %s", cookie) + } + } else { + // there's no Cookie, check Basic authentication + user, pass, ok2 := r.BasicAuth() + if ok2 { + u := Context.auth.UserFind(user, pass) + if len(u.Name) != 0 { + ok = true + } else { + log.Info("Auth: invalid Basic Authorization value") + } + } + } + if !ok { + if r.URL.Path == "/" || r.URL.Path == "/index.html" { + if glProcessRedirect(w, r) { + log.Debug("Auth: redirected to login page by GL-Inet submodule") + } else { + w.Header().Set("Location", "/login.html") + w.WriteHeader(http.StatusFound) + } + } else { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte("Forbidden")) + } + authFirst = true + } + return authFirst +} + func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/login.html" { @@ -392,45 +449,7 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re // process as usual // no additional auth requirements } else if Context.auth != nil && Context.auth.AuthRequired() { - // redirect to login page if not authenticated - ok := false - cookie, err := r.Cookie(sessionCookieName) - - if glProcessCookie(r) { - log.Debug("Auth: authentification was handled by GL-Inet submodule") - ok = true - - } else if err == nil { - r := Context.auth.CheckSession(cookie.Value) - if r == 0 { - ok = true - } else if r < 0 { - log.Debug("Auth: invalid cookie value: %s", cookie) - } - } else { - // there's no Cookie, check Basic authentication - user, pass, ok2 := r.BasicAuth() - if ok2 { - u := Context.auth.UserFind(user, pass) - if len(u.Name) != 0 { - ok = true - } else { - log.Info("Auth: invalid Basic Authorization value") - } - } - } - if !ok { - if r.URL.Path == "/" || r.URL.Path == "/index.html" { - if glProcessRedirect(w, r) { - log.Debug("Auth: redirected to login page by GL-Inet submodule") - } else { - w.Header().Set("Location", "/login.html") - w.WriteHeader(http.StatusFound) - } - } else { - w.WriteHeader(http.StatusForbidden) - _, _ = w.Write([]byte("Forbidden")) - } + if optionalAuthThird(w, r) { return } } diff --git a/internal/home/auth_test.go b/internal/home/auth_test.go index dd2b68b3..25db2dd6 100644 --- a/internal/home/auth_test.go +++ b/internal/home/auth_test.go @@ -20,7 +20,7 @@ func TestMain(m *testing.M) { func prepareTestDir() string { const dir = "./agh-test" _ = os.RemoveAll(dir) - _ = os.MkdirAll(dir, 0755) + _ = os.MkdirAll(dir, 0o755) return dir } @@ -30,7 +30,7 @@ func TestAuth(t *testing.T) { fn := filepath.Join(dir, "sessions.db") users := []User{ - User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, + {Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, } a := InitAuth(fn, nil, 60) s := session{} @@ -41,7 +41,8 @@ func TestAuth(t *testing.T) { assert.True(t, a.CheckSession("notfound") == -1) a.RemoveSession("notfound") - sess := getSession(&users[0]) + sess, err := getSession(&users[0]) + assert.Nil(t, err) sessStr := hex.EncodeToString(sess) now := time.Now().UTC().Unix() @@ -105,7 +106,7 @@ func TestAuthHTTP(t *testing.T) { fn := filepath.Join(dir, "sessions.db") users := []User{ - User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, + {Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, } Context.auth = InitAuth(fn, users, 60) @@ -136,7 +137,8 @@ func TestAuthHTTP(t *testing.T) { assert.True(t, handlerCalled) // perform login - cookie := Context.auth.httpCookie(loginJSON{Name: "name", Password: "password"}) + cookie, err := Context.auth.httpCookie(loginJSON{Name: "name", Password: "password"}) + assert.Nil(t, err) assert.True(t, cookie != "") // get / diff --git a/internal/home/auth_glinet.go b/internal/home/authglinet.go similarity index 78% rename from internal/home/auth_glinet.go rename to internal/home/authglinet.go index 7dd2790d..228843fa 100644 --- a/internal/home/auth_glinet.go +++ b/internal/home/authglinet.go @@ -10,6 +10,7 @@ import ( "time" "unsafe" + "github.com/AdguardTeam/AdGuardHome/internal/aghio" "github.com/AdguardTeam/golibs/log" ) @@ -18,8 +19,10 @@ var GLMode bool var glFilePrefix = "/tmp/gl_token_" -const glTokenTimeoutSeconds = 3600 -const glCookieName = "Admin-Token" +const ( + glTokenTimeoutSeconds = 3600 + glCookieName = "Admin-Token" +) func glProcessRedirect(w http.ResponseWriter, r *http.Request) bool { if !GLMode { @@ -71,14 +74,28 @@ func archIsLittleEndian() bool { return (b == 0x04) } +// MaxFileSize is a maximum file length in bytes. +const MaxFileSize = 1024 * 1024 + func glGetTokenDate(file string) uint32 { f, err := os.Open(file) if err != nil { log.Error("os.Open: %s", err) return 0 } + defer f.Close() + + fileReadCloser, err := aghio.LimitReadCloser(f, MaxFileSize) + if err != nil { + log.Error("LimitReadCloser: %s", err) + return 0 + } + defer fileReadCloser.Close() + var dateToken uint32 - bs, err := ioutil.ReadAll(f) + + // This use of ReadAll is now safe, because we limited reader. + bs, err := ioutil.ReadAll(fileReadCloser) if err != nil { log.Error("ioutil.ReadAll: %s", err) return 0 diff --git a/internal/home/auth_glinet_test.go b/internal/home/authglinet_test.go similarity index 86% rename from internal/home/auth_glinet_test.go rename to internal/home/authglinet_test.go index 171bb84e..df5e3342 100644 --- a/internal/home/auth_glinet_test.go +++ b/internal/home/authglinet_test.go @@ -25,7 +25,7 @@ func TestAuthGL(t *testing.T) { } else { binary.BigEndian.PutUint32(data, tval) } - assert.Nil(t, ioutil.WriteFile(glFilePrefix+"test", data, 0644)) + assert.Nil(t, ioutil.WriteFile(glFilePrefix+"test", data, 0o644)) assert.False(t, glCheckToken("test")) tval = uint32(time.Now().UTC().Unix() + 60) @@ -35,7 +35,7 @@ func TestAuthGL(t *testing.T) { } else { binary.BigEndian.PutUint32(data, tval) } - assert.Nil(t, ioutil.WriteFile(glFilePrefix+"test", data, 0644)) + assert.Nil(t, ioutil.WriteFile(glFilePrefix+"test", data, 0o644)) r, _ := http.NewRequest("GET", "http://localhost/", nil) r.AddCookie(&http.Cookie{Name: glCookieName, Value: "test"}) assert.True(t, glProcessCookie(r)) diff --git a/internal/home/clients.go b/internal/home/clients.go index 538bfe7e..3c6bfa48 100644 --- a/internal/home/clients.go +++ b/internal/home/clients.go @@ -570,31 +570,35 @@ func (clients *clientsContainer) SetWhoisInfo(ip string, info [][]string) { // so we overwrite existing entries with an equal or higher priority func (clients *clientsContainer) AddHost(ip, host string, source clientSource) (bool, error) { clients.lock.Lock() - b, e := clients.addHost(ip, host, source) + b := clients.addHost(ip, host, source) clients.lock.Unlock() - return b, e + return b, nil } -func (clients *clientsContainer) addHost(ip, host string, source clientSource) (bool, error) { - // check auto-clients index +func (clients *clientsContainer) addHost(ip, host string, source clientSource) (addedNew bool) { ch, ok := clients.ipHost[ip] - if ok && ch.Source > source { - return false, nil - } else if ok { + if ok { + if ch.Source > source { + return false + } + ch.Source = source } else { ch = &ClientHost{ Host: host, Source: source, } + clients.ipHost[ip] = ch } - log.Debug("Clients: added %q -> %q [%d]", ip, host, len(clients.ipHost)) - return true, nil + + log.Debug("clients: added %q -> %q [%d]", ip, host, len(clients.ipHost)) + + return true } // Remove all entries that match the specified source -func (clients *clientsContainer) rmHosts(source clientSource) int { +func (clients *clientsContainer) rmHosts(source clientSource) { n := 0 for k, v := range clients.ipHost { if v.Source == source { @@ -602,8 +606,8 @@ func (clients *clientsContainer) rmHosts(source clientSource) int { n++ } } - log.Debug("Clients: removed %d client aliases", n) - return n + + log.Debug("clients: removed %d client aliases", n) } // addFromHostsFile fills the clients hosts list from the system's hosts files. @@ -613,15 +617,12 @@ func (clients *clientsContainer) addFromHostsFile() { clients.lock.Lock() defer clients.lock.Unlock() - _ = clients.rmHosts(ClientSourceHostsFile) + clients.rmHosts(ClientSourceHostsFile) n := 0 for ip, names := range hosts { for _, name := range names { - ok, err := clients.addHost(ip, name, ClientSourceHostsFile) - if err != nil { - log.Debug("Clients: %s", err) - } + ok := clients.addHost(ip, name, ClientSourceHostsFile) if ok { n++ } @@ -650,7 +651,7 @@ func (clients *clientsContainer) addFromSystemARP() { clients.lock.Lock() defer clients.lock.Unlock() - _ = clients.rmHosts(ClientSourceARP) + clients.rmHosts(ClientSourceARP) n := 0 lines := strings.Split(string(data), "\n") @@ -668,10 +669,7 @@ func (clients *clientsContainer) addFromSystemARP() { continue } - ok, e := clients.addHost(ip, host, ClientSourceARP) - if e != nil { - log.Tracef("%s", e) - } + ok := clients.addHost(ip, host, ClientSourceARP) if ok { n++ } @@ -689,7 +687,7 @@ func (clients *clientsContainer) addFromDHCP() { clients.lock.Lock() defer clients.lock.Unlock() - _ = clients.rmHosts(ClientSourceDHCP) + clients.rmHosts(ClientSourceDHCP) leases := clients.dhcpServer.Leases(dhcpd.LeasesAll) n := 0 @@ -697,7 +695,7 @@ func (clients *clientsContainer) addFromDHCP() { if len(l.Hostname) == 0 { continue } - ok, _ := clients.addHost(l.IP.String(), l.Hostname, ClientSourceDHCP) + ok := clients.addHost(l.IP.String(), l.Hostname, ClientSourceDHCP) if ok { n++ } diff --git a/internal/home/clients_test.go b/internal/home/clients_test.go index 5d5a3c81..9268c08f 100644 --- a/internal/home/clients_test.go +++ b/internal/home/clients_test.go @@ -12,144 +12,155 @@ import ( ) func TestClients(t *testing.T) { - var c Client - var e error - var b bool clients := clientsContainer{} clients.testing = true clients.Init(nil, nil, nil) - // add - c = Client{ - IDs: []string{"1.1.1.1", "1:2:3::4", "aa:aa:aa:aa:aa:aa"}, - Name: "client1", - } - b, e = clients.Add(c) - if !b || e != nil { - t.Fatalf("Add #1") - } + t.Run("add_success", func(t *testing.T) { + c := Client{ + IDs: []string{"1.1.1.1", "1:2:3::4", "aa:aa:aa:aa:aa:aa"}, + Name: "client1", + } - // add #2 - c = Client{ - IDs: []string{"2.2.2.2"}, - Name: "client2", - } - b, e = clients.Add(c) - if !b || e != nil { - t.Fatalf("Add #2") - } + b, err := clients.Add(c) + assert.True(t, b) + assert.Nil(t, err) - c, b = clients.Find("1.1.1.1") - assert.True(t, b && c.Name == "client1") + c = Client{ + IDs: []string{"2.2.2.2"}, + Name: "client2", + } - c, b = clients.Find("1:2:3::4") - assert.True(t, b && c.Name == "client1") + b, err = clients.Add(c) + assert.True(t, b) + assert.Nil(t, err) - c, b = clients.Find("2.2.2.2") - assert.True(t, b && c.Name == "client2") + c, b = clients.Find("1.1.1.1") + assert.True(t, b && c.Name == "client1") - // failed add - name in use - c = Client{ - IDs: []string{"1.2.3.5"}, - Name: "client1", - } - b, _ = clients.Add(c) - if b { - t.Fatalf("Add - name in use") - } + c, b = clients.Find("1:2:3::4") + assert.True(t, b && c.Name == "client1") - // failed add - ip in use - c = Client{ - IDs: []string{"2.2.2.2"}, - Name: "client3", - } - b, e = clients.Add(c) - if b || e == nil { - t.Fatalf("Add - ip in use") - } + c, b = clients.Find("2.2.2.2") + assert.True(t, b && c.Name == "client2") - // get - assert.True(t, !clients.Exists("1.2.3.4", ClientSourceHostsFile)) - assert.True(t, clients.Exists("1.1.1.1", ClientSourceHostsFile)) - assert.True(t, clients.Exists("2.2.2.2", ClientSourceHostsFile)) + assert.True(t, !clients.Exists("1.2.3.4", ClientSourceHostsFile)) + assert.True(t, clients.Exists("1.1.1.1", ClientSourceHostsFile)) + assert.True(t, clients.Exists("2.2.2.2", ClientSourceHostsFile)) + }) - // failed update - no such name - c.IDs = []string{"1.2.3.0"} - c.Name = "client3" - if clients.Update("client3", c) == nil { - t.Fatalf("Update") - } + t.Run("add_fail_name", func(t *testing.T) { + c := Client{ + IDs: []string{"1.2.3.5"}, + Name: "client1", + } - // failed update - name in use - c.IDs = []string{"1.2.3.0"} - c.Name = "client2" - if clients.Update("client1", c) == nil { - t.Fatalf("Update - name in use") - } + b, err := clients.Add(c) + assert.False(t, b) + assert.Nil(t, err) + }) - // failed update - ip in use - c.IDs = []string{"2.2.2.2"} - c.Name = "client1" - if clients.Update("client1", c) == nil { - t.Fatalf("Update - ip in use") - } + t.Run("add_fail_ip", func(t *testing.T) { + c := Client{ + IDs: []string{"2.2.2.2"}, + Name: "client3", + } - // update - c.IDs = []string{"1.1.1.2"} - c.Name = "client1" - if clients.Update("client1", c) != nil { - t.Fatalf("Update") - } + b, err := clients.Add(c) + assert.False(t, b) + assert.NotNil(t, err) + }) - // get after update - assert.True(t, !clients.Exists("1.1.1.1", ClientSourceHostsFile)) - assert.True(t, clients.Exists("1.1.1.2", ClientSourceHostsFile)) + t.Run("update_fail_name", func(t *testing.T) { + c := Client{ + IDs: []string{"1.2.3.0"}, + Name: "client3", + } - // update - rename - c.IDs = []string{"1.1.1.2"} - c.Name = "client1-renamed" - c.UseOwnSettings = true - assert.True(t, clients.Update("client1", c) == nil) - c = Client{} - c, b = clients.Find("1.1.1.2") - assert.True(t, b && c.Name == "client1-renamed" && c.IDs[0] == "1.1.1.2" && c.UseOwnSettings) - assert.True(t, clients.list["client1"] == nil) + err := clients.Update("client3", c) + assert.NotNil(t, err) - // failed remove - no such name - if clients.Del("client3") { - t.Fatalf("Del - no such name") - } + c = Client{ + IDs: []string{"1.2.3.0"}, + Name: "client2", + } - // remove - assert.True(t, !(!clients.Del("client1-renamed") || clients.Exists("1.1.1.2", ClientSourceHostsFile))) + err = clients.Update("client3", c) + assert.NotNil(t, err) + }) - // add host client - b, e = clients.AddHost("1.1.1.1", "host", ClientSourceARP) - if !b || e != nil { - t.Fatalf("clientAddHost") - } + t.Run("update_fail_ip", func(t *testing.T) { + c := Client{ + IDs: []string{"2.2.2.2"}, + Name: "client1", + } - // failed add - ip exists - b, e = clients.AddHost("1.1.1.1", "host1", ClientSourceRDNS) - if b || e != nil { - t.Fatalf("clientAddHost - ip exists") - } + err := clients.Update("client1", c) + assert.NotNil(t, err) + }) - // overwrite with new data - b, e = clients.AddHost("1.1.1.1", "host2", ClientSourceARP) - if !b || e != nil { - t.Fatalf("clientAddHost - overwrite with new data") - } + t.Run("update_success", func(t *testing.T) { + c := Client{ + IDs: []string{"1.1.1.2"}, + Name: "client1", + } - // overwrite with new data (higher priority) - b, e = clients.AddHost("1.1.1.1", "host3", ClientSourceHostsFile) - if !b || e != nil { - t.Fatalf("clientAddHost - overwrite with new data (higher priority)") - } + err := clients.Update("client1", c) + assert.Nil(t, err) - // get - assert.True(t, clients.Exists("1.1.1.1", ClientSourceHostsFile)) + assert.True(t, !clients.Exists("1.1.1.1", ClientSourceHostsFile)) + assert.True(t, clients.Exists("1.1.1.2", ClientSourceHostsFile)) + + c = Client{ + IDs: []string{"1.1.1.2"}, + Name: "client1-renamed", + UseOwnSettings: true, + } + + err = clients.Update("client1", c) + assert.Nil(t, err) + + c, b := clients.Find("1.1.1.2") + assert.True(t, b) + assert.True(t, c.Name == "client1-renamed") + assert.True(t, c.IDs[0] == "1.1.1.2") + assert.True(t, c.UseOwnSettings) + assert.Nil(t, clients.list["client1"]) + }) + + t.Run("del_success", func(t *testing.T) { + b := clients.Del("client1-renamed") + assert.True(t, b) + assert.False(t, clients.Exists("1.1.1.2", ClientSourceHostsFile)) + }) + + t.Run("del_fail", func(t *testing.T) { + b := clients.Del("client3") + assert.False(t, b) + }) + + t.Run("addhost_success", func(t *testing.T) { + b, err := clients.AddHost("1.1.1.1", "host", ClientSourceARP) + assert.True(t, b) + assert.Nil(t, err) + + b, err = clients.AddHost("1.1.1.1", "host2", ClientSourceARP) + assert.True(t, b) + assert.Nil(t, err) + + b, err = clients.AddHost("1.1.1.1", "host3", ClientSourceHostsFile) + assert.True(t, b) + assert.Nil(t, err) + + assert.True(t, clients.Exists("1.1.1.1", ClientSourceHostsFile)) + }) + + t.Run("addhost_fail", func(t *testing.T) { + b, err := clients.AddHost("1.1.1.1", "host1", ClientSourceRDNS) + assert.False(t, b) + assert.Nil(t, err) + }) } func TestClientsWhois(t *testing.T) { diff --git a/internal/home/clients_http.go b/internal/home/clientshttp.go similarity index 86% rename from internal/home/clients_http.go rename to internal/home/clientshttp.go index 42fa6f2a..d8cc3ee3 100644 --- a/internal/home/clients_http.go +++ b/internal/home/clientshttp.go @@ -3,7 +3,6 @@ package home import ( "encoding/json" "fmt" - "io/ioutil" "net/http" ) @@ -95,8 +94,8 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, _ *http } // Convert JSON object to Client object -func jsonToClient(cj clientJSON) (*Client, error) { - c := Client{ +func jsonToClient(cj clientJSON) (c *Client) { + return &Client{ Name: cj.Name, IDs: cj.IDs, Tags: cj.Tags, @@ -111,7 +110,6 @@ func jsonToClient(cj clientJSON) (*Client, error) { Upstreams: cj.Upstreams, } - return &c, nil } // Convert Client object to JSON @@ -150,24 +148,15 @@ func clientHostToJSON(ip string, ch ClientHost) clientJSON { // Add a new client func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - httpError(w, http.StatusBadRequest, "failed to read request body: %s", err) - return - } - cj := clientJSON{} - err = json.Unmarshal(body, &cj) + err := json.NewDecoder(r.Body).Decode(&cj) if err != nil { - httpError(w, http.StatusBadRequest, "JSON parse: %s", err) + httpError(w, http.StatusBadRequest, "failed to process request body: %s", err) + return } - c, err := jsonToClient(cj) - if err != nil { - httpError(w, http.StatusBadRequest, "%s", err) - return - } + c := jsonToClient(cj) ok, err := clients.Add(*c) if err != nil { httpError(w, http.StatusBadRequest, "%s", err) @@ -183,16 +172,17 @@ func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http. // Remove client func (clients *clientsContainer) handleDelClient(w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(r.Body) + cj := clientJSON{} + err := json.NewDecoder(r.Body).Decode(&cj) if err != nil { - httpError(w, http.StatusBadRequest, "failed to read request body: %s", err) + httpError(w, http.StatusBadRequest, "failed to process request body: %s", err) + return } - cj := clientJSON{} - err = json.Unmarshal(body, &cj) - if err != nil || len(cj.Name) == 0 { - httpError(w, http.StatusBadRequest, "JSON parse: %s", err) + if len(cj.Name) == 0 { + httpError(w, http.StatusBadRequest, "client's name must be non-empty") + return } @@ -211,29 +201,20 @@ type updateJSON struct { // Update client's properties func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(r.Body) + dj := updateJSON{} + err := json.NewDecoder(r.Body).Decode(&dj) if err != nil { - httpError(w, http.StatusBadRequest, "failed to read request body: %s", err) + httpError(w, http.StatusBadRequest, "failed to process request body: %s", err) + return } - var dj updateJSON - err = json.Unmarshal(body, &dj) - if err != nil { - httpError(w, http.StatusBadRequest, "JSON parse: %s", err) - return - } if len(dj.Name) == 0 { httpError(w, http.StatusBadRequest, "Invalid request") return } - c, err := jsonToClient(dj.Data) - if err != nil { - httpError(w, http.StatusBadRequest, "%s", err) - return - } - + c := jsonToClient(dj.Data) err = clients.Update(dj.Name, *c) if err != nil { httpError(w, http.StatusBadRequest, "%s", err) diff --git a/internal/home/clients_tags.go b/internal/home/clientstags.go similarity index 100% rename from internal/home/clients_tags.go rename to internal/home/clientstags.go diff --git a/internal/home/config.go b/internal/home/config.go index 2f1e7da7..ed81c56e 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -99,6 +99,16 @@ type tlsConfigSettings struct { PortDNSOverTLS int `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"` // DNS-over-TLS port. If 0, DOT will be disabled PortDNSOverQUIC uint16 `yaml:"port_dns_over_quic" json:"port_dns_over_quic,omitempty"` // DNS-over-QUIC port. If 0, DoQ will be disabled + // PortDNSCrypt is the port for DNSCrypt requests. If it's zero, + // DNSCrypt is disabled. + PortDNSCrypt int `yaml:"port_dnscrypt" json:"port_dnscrypt"` + // DNSCryptConfigFile is the path to the DNSCrypt config file. Must be + // set if PortDNSCrypt is not zero. + // + // See https://github.com/AdguardTeam/dnsproxy and + // https://github.com/ameshkov/dnscrypt. + DNSCryptConfigFile string `yaml:"dnscrypt_config_file" json:"dnscrypt_config_file"` + // Allow DOH queries via unencrypted HTTP (e.g. for reverse proxying) AllowUnencryptedDOH bool `yaml:"allow_unencrypted_doh" json:"allow_unencrypted_doh"` diff --git a/internal/home/control.go b/internal/home/control.go index 00334b62..3443515a 100644 --- a/internal/home/control.go +++ b/internal/home/control.go @@ -107,24 +107,24 @@ func registerControlHandlers() { httpRegister(http.MethodGet, "/control/status", handleStatus) httpRegister(http.MethodPost, "/control/i18n/change_language", handleI18nChangeLanguage) httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage) - http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON))) + Context.mux.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON))) httpRegister(http.MethodPost, "/control/update", handleUpdate) httpRegister(http.MethodGet, "/control/profile", handleGetProfile) // No auth is necessary for DOH/DOT configurations - http.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDoh)) - http.HandleFunc("/apple/dot.mobileconfig", postInstall(handleMobileConfigDot)) + Context.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDOH)) + Context.mux.HandleFunc("/apple/dot.mobileconfig", postInstall(handleMobileConfigDOT)) RegisterAuthHandlers() } -func httpRegister(method string, url string, handler func(http.ResponseWriter, *http.Request)) { +func httpRegister(method, url string, handler func(http.ResponseWriter, *http.Request)) { if len(method) == 0 { // "/dns-query" handler doesn't need auth, gzip and isn't restricted by 1 HTTP method - http.HandleFunc(url, postInstall(handler)) + Context.mux.HandleFunc(url, postInstall(handler)) return } - http.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler))))) + Context.mux.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler))))) } // ---------------------------------- @@ -201,7 +201,6 @@ func preInstallHandler(handler http.Handler) http.Handler { // it also enforces HTTPS if it is enabled and configured func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if Context.firstRun && !strings.HasPrefix(r.URL.Path, "/install.") && !strings.HasPrefix(r.URL.Path, "/assets/") { diff --git a/internal/home/control_filtering.go b/internal/home/controlfiltering.go similarity index 90% rename from internal/home/control_filtering.go rename to internal/home/controlfiltering.go index 37f9af81..1d0172e8 100644 --- a/internal/home/control_filtering.go +++ b/internal/home/controlfiltering.go @@ -196,9 +196,9 @@ func (f *Filtering) handleFilteringSetURL(w http.ResponseWriter, r *http.Request } if (status&statusUpdateRequired) != 0 && fj.Data.Enabled { // download new filter and apply its rules - flags := FilterRefreshBlocklists + flags := filterRefreshBlocklists if fj.Whitelist { - flags = FilterRefreshAllowlists + flags = filterRefreshAllowlists } nUpdated, _ := f.refreshFilters(flags, true) // if at least 1 filter has been updated, refreshFilters() restarts the filtering automatically @@ -214,6 +214,7 @@ func (f *Filtering) handleFilteringSetURL(w http.ResponseWriter, r *http.Request } func (f *Filtering) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) { + // This use of ReadAll is safe, because request's body is now limited. body, err := ioutil.ReadAll(r.Body) if err != nil { httpError(w, http.StatusBadRequest, "Failed to read request body: %s", err) @@ -243,11 +244,11 @@ func (f *Filtering) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques } Context.controlLock.Unlock() - flags := FilterRefreshBlocklists + flags := filterRefreshBlocklists if req.White { - flags = FilterRefreshAllowlists + flags = filterRefreshAllowlists } - resp.Updated, err = f.refreshFilters(flags|FilterRefreshForce, false) + resp.Updated, err = f.refreshFilters(flags|filterRefreshForce, false) Context.controlLock.Lock() if err != nil { httpError(w, http.StatusInternalServerError, "%s", err) @@ -345,10 +346,25 @@ func (f *Filtering) handleFilteringConfig(w http.ResponseWriter, r *http.Request enableFilters(true) } +type checkHostRespRule struct { + FilterListID int64 `json:"filter_list_id"` + Text string `json:"text"` +} + type checkHostResp struct { - Reason string `json:"reason"` - FilterID int64 `json:"filter_id"` - Rule string `json:"rule"` + Reason string `json:"reason"` + + // FilterID is the ID of the rule's filter list. + // + // Deprecated: Use Rules[*].FilterListID. + FilterID int64 `json:"filter_id"` + + // Rule is the text of the matched rule. + // + // Deprecated: Use Rules[*].Text. + Rule string `json:"rule"` + + Rules []*checkHostRespRule `json:"rules"` // for FilteredBlockedService: SvcName string `json:"service_name"` @@ -373,11 +389,23 @@ func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) { resp := checkHostResp{} resp.Reason = result.Reason.String() - resp.FilterID = result.FilterID - resp.Rule = result.Rule resp.SvcName = result.ServiceName resp.CanonName = result.CanonName resp.IPList = result.IPList + + if len(result.Rules) > 0 { + resp.FilterID = result.Rules[0].FilterListID + resp.Rule = result.Rules[0].Text + } + + resp.Rules = make([]*checkHostRespRule, len(result.Rules)) + for i, r := range result.Rules { + resp.Rules[i] = &checkHostRespRule{ + FilterListID: r.FilterListID, + Text: r.Text, + } + } + js, err := json.Marshal(resp) if err != nil { httpError(w, http.StatusInternalServerError, "json encode: %s", err) diff --git a/internal/home/control_install.go b/internal/home/controlinstall.go similarity index 94% rename from internal/home/control_install.go rename to internal/home/controlinstall.go index fcb8fcea..ed3cdd38 100644 --- a/internal/home/control_install.go +++ b/internal/home/controlinstall.go @@ -15,7 +15,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/util" - "github.com/AdguardTeam/AdGuardHome/internal/dhcpd" + "github.com/AdguardTeam/AdGuardHome/internal/sysutil" "github.com/AdguardTeam/golibs/log" ) @@ -167,7 +167,7 @@ func handleStaticIP(ip string, set bool) staticIPJSON { if set { // Try to set static IP for the specified interface - err := dhcpd.SetStaticIP(interfaceName) + err := sysutil.IfaceSetStaticIP(interfaceName) if err != nil { resp.Static = "error" resp.Error = err.Error() @@ -177,7 +177,7 @@ func handleStaticIP(ip string, set bool) staticIPJSON { // Fallthrough here even if we set static IP // Check if we have a static IP and return the details - isStaticIP, err := dhcpd.HasStaticIP(interfaceName) + isStaticIP, err := sysutil.IfaceHasStaticIP(interfaceName) if err != nil { resp.Static = "error" resp.Error = err.Error() @@ -273,7 +273,7 @@ type applyConfigReq struct { } // Copy installation parameters between two configuration objects -func copyInstallSettings(dst *configuration, src *configuration) { +func copyInstallSettings(dst, src *configuration) { dst.BindHost = src.BindHost dst.BindPort = src.BindPort dst.DNS.BindHost = src.DNS.BindHost @@ -372,7 +372,7 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { } func (web *Web) registerInstallHandlers() { - http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(web.handleInstallGetAddresses))) - http.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig))) - http.HandleFunc("/control/install/configure", preInstall(ensurePOST(web.handleInstallConfigure))) + Context.mux.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(web.handleInstallGetAddresses))) + Context.mux.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig))) + Context.mux.HandleFunc("/control/install/configure", preInstall(ensurePOST(web.handleInstallConfigure))) } diff --git a/internal/home/control_update.go b/internal/home/controlupdate.go similarity index 91% rename from internal/home/control_update.go rename to internal/home/controlupdate.go index 2ab5d6a2..dcf428b9 100644 --- a/internal/home/control_update.go +++ b/internal/home/controlupdate.go @@ -10,8 +10,8 @@ import ( "strings" "syscall" + "github.com/AdguardTeam/AdGuardHome/internal/sysutil" "github.com/AdguardTeam/AdGuardHome/internal/update" - "github.com/AdguardTeam/AdGuardHome/internal/util" "github.com/AdguardTeam/golibs/log" ) @@ -104,12 +104,7 @@ func getVersionResp(info update.VersionInfo) []byte { tlsConf.PortDNSOverQUIC < 1024)) || config.BindPort < 1024 || config.DNS.Port < 1024) { - // On UNIX, if we're running under a regular user, - // but with CAP_NET_BIND_SERVICE set on a binary file, - // and we're listening on ports <1024, - // we won't be able to restart after we replace the binary file, - // because we'll lose CAP_NET_BIND_SERVICE capability. - canUpdate, _ = util.HaveAdminRights() + canUpdate, _ = sysutil.CanBindPrivilegedPorts() } ret["can_autoupdate"] = canUpdate } diff --git a/internal/home/control_update_test.go b/internal/home/controlupdate_test.go similarity index 98% rename from internal/home/control_update_test.go rename to internal/home/controlupdate_test.go index 82101249..45112f50 100644 --- a/internal/home/control_update_test.go +++ b/internal/home/controlupdate_test.go @@ -81,7 +81,7 @@ func TestTargzFileUnpack(t *testing.T) { fn := "../dist/AdGuardHome_linux_amd64.tar.gz" outdir := "../test-unpack" defer os.RemoveAll(outdir) - _ = os.Mkdir(outdir, 0755) + _ = os.Mkdir(outdir, 0o755) files, e := targzFileUnpack(fn, outdir) if e != nil { t.Fatalf("FAILED: %s", e) @@ -92,7 +92,7 @@ func TestTargzFileUnpack(t *testing.T) { func TestZipFileUnpack(t *testing.T) { fn := "../dist/AdGuardHome_windows_amd64.zip" outdir := "../test-unpack" - _ = os.Mkdir(outdir, 0755) + _ = os.Mkdir(outdir, 0o755) files, e := zipFileUnpack(fn, outdir) if e != nil { t.Fatalf("FAILED: %s", e) diff --git a/internal/home/dns.go b/internal/home/dns.go index a93bc307..1090d9be 100644 --- a/internal/home/dns.go +++ b/internal/home/dns.go @@ -3,8 +3,10 @@ package home import ( "fmt" "net" + "os" "path/filepath" + "github.com/AdguardTeam/AdGuardHome/internal/agherr" "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/querylog" @@ -12,6 +14,8 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/util" "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/golibs/log" + "github.com/ameshkov/dnscrypt/v2" + yaml "gopkg.in/yaml.v2" ) // Called by other modules when configuration is changed @@ -70,7 +74,12 @@ func initDNSServer() error { } Context.dnsServer = dnsforward.NewServer(p) Context.clients.dnsServer = Context.dnsServer - dnsConfig := generateServerConfig() + dnsConfig, err := generateServerConfig() + if err != nil { + closeDNSServer() + return fmt.Errorf("generateServerConfig: %w", err) + } + err = Context.dnsServer.Prepare(&dnsConfig) if err != nil { closeDNSServer() @@ -88,60 +97,6 @@ func isRunning() bool { return Context.dnsServer != nil && Context.dnsServer.IsRunning() } -// nolint (gocyclo) -// Return TRUE if IP is within public Internet IP range -func isPublicIP(ip net.IP) bool { - ip4 := ip.To4() - if ip4 != nil { - switch ip4[0] { - case 0: - return false // software - case 10: - return false // private network - case 127: - return false // loopback - case 169: - if ip4[1] == 254 { - return false // link-local - } - case 172: - if ip4[1] >= 16 && ip4[1] <= 31 { - return false // private network - } - case 192: - if (ip4[1] == 0 && ip4[2] == 0) || // private network - (ip4[1] == 0 && ip4[2] == 2) || // documentation - (ip4[1] == 88 && ip4[2] == 99) || // reserved - (ip4[1] == 168) { // private network - return false - } - case 198: - if (ip4[1] == 18 || ip4[2] == 19) || // private network - (ip4[1] == 51 || ip4[2] == 100) { // documentation - return false - } - case 203: - if ip4[1] == 0 && ip4[2] == 113 { // documentation - return false - } - case 224: - if ip4[1] == 0 && ip4[2] == 0 { // multicast - return false - } - case 255: - if ip4[1] == 255 && ip4[2] == 255 && ip4[3] == 255 { // subnet - return false - } - } - } else { - if ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() { - return false - } - } - - return true -} - func onDNSRequest(d *proxy.DNSContext) { ip := dnsforward.GetIPString(d.Addr) if ip == "" { @@ -153,15 +108,16 @@ func onDNSRequest(d *proxy.DNSContext) { if !ipAddr.IsLoopback() { Context.rdns.Begin(ip) } - if isPublicIP(ipAddr) { + if !Context.ipDetector.detectSpecialNetwork(ipAddr) { Context.whois.Begin(ip) } } -func generateServerConfig() dnsforward.ServerConfig { - newconfig := dnsforward.ServerConfig{ - UDPListenAddr: &net.UDPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port}, - TCPListenAddr: &net.TCPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port}, +func generateServerConfig() (newconfig dnsforward.ServerConfig, err error) { + bindHost := net.ParseIP(config.DNS.BindHost) + newconfig = dnsforward.ServerConfig{ + UDPListenAddr: &net.UDPAddr{IP: bindHost, Port: config.DNS.Port}, + TCPListenAddr: &net.TCPAddr{IP: bindHost, Port: config.DNS.Port}, FilteringConfig: config.DNS.FilteringConfig, ConfigModified: onConfigModified, HTTPRegister: httpRegister, @@ -175,35 +131,86 @@ func generateServerConfig() dnsforward.ServerConfig { if tlsConf.PortDNSOverTLS != 0 { newconfig.TLSListenAddr = &net.TCPAddr{ - IP: net.ParseIP(config.DNS.BindHost), + IP: bindHost, Port: tlsConf.PortDNSOverTLS, } } if tlsConf.PortDNSOverQUIC != 0 { newconfig.QUICListenAddr = &net.UDPAddr{ - IP: net.ParseIP(config.DNS.BindHost), + IP: bindHost, Port: int(tlsConf.PortDNSOverQUIC), } } + + if tlsConf.PortDNSCrypt != 0 { + newconfig.DNSCryptConfig, err = newDNSCrypt(bindHost, tlsConf) + if err != nil { + // Don't wrap the error, because it's already + // wrapped by newDNSCrypt. + return dnsforward.ServerConfig{}, err + } + } } + newconfig.TLSv12Roots = Context.tlsRoots newconfig.TLSCiphers = Context.tlsCiphers newconfig.TLSAllowUnencryptedDOH = tlsConf.AllowUnencryptedDOH newconfig.FilterHandler = applyAdditionalFiltering newconfig.GetCustomUpstreamByClient = Context.clients.FindUpstreams - return newconfig + + return newconfig, nil } -type DNSEncryption struct { +func newDNSCrypt(bindHost net.IP, tlsConf tlsConfigSettings) (dnscc dnsforward.DNSCryptConfig, err error) { + if tlsConf.DNSCryptConfigFile == "" { + return dnscc, agherr.Error("no dnscrypt_config_file") + } + + f, err := os.Open(tlsConf.DNSCryptConfigFile) + if err != nil { + return dnscc, fmt.Errorf("opening dnscrypt config: %w", err) + } + defer f.Close() + + rc := &dnscrypt.ResolverConfig{} + err = yaml.NewDecoder(f).Decode(rc) + if err != nil { + return dnscc, fmt.Errorf("decoding dnscrypt config: %w", err) + } + + cert, err := rc.CreateCert() + if err != nil { + return dnscc, fmt.Errorf("creating dnscrypt cert: %w", err) + } + + udpAddr := &net.UDPAddr{ + IP: bindHost, + Port: tlsConf.PortDNSCrypt, + } + tcpAddr := &net.TCPAddr{ + IP: bindHost, + Port: tlsConf.PortDNSCrypt, + } + + return dnsforward.DNSCryptConfig{ + UDPListenAddr: udpAddr, + TCPListenAddr: tcpAddr, + ResolverCert: cert, + ProviderName: rc.ProviderName, + Enabled: true, + }, nil +} + +type dnsEncryption struct { https string tls string quic string } -func getDNSEncryption() DNSEncryption { - dnsEncryption := DNSEncryption{} +func getDNSEncryption() dnsEncryption { + dnsEncryption := dnsEncryption{} tlsConf := tlsConfigSettings{} @@ -327,7 +334,7 @@ func startDNSServer() error { if !ipAddr.IsLoopback() { Context.rdns.Begin(ip) } - if isPublicIP(ipAddr) { + if !Context.ipDetector.detectSpecialNetwork(ipAddr) { Context.whois.Begin(ip) } } @@ -335,11 +342,16 @@ func startDNSServer() error { return nil } -func reconfigureDNSServer() error { - newconfig := generateServerConfig() - err := Context.dnsServer.Reconfigure(&newconfig) +func reconfigureDNSServer() (err error) { + var newconfig dnsforward.ServerConfig + newconfig, err = generateServerConfig() if err != nil { - return fmt.Errorf("couldn't start forwarding DNS server: %w", err) + return fmt.Errorf("generating forwarding dns server config: %w", err) + } + + err = Context.dnsServer.Reconfigure(&newconfig) + if err != nil { + return fmt.Errorf("starting forwarding dns server: %w", err) } return nil diff --git a/internal/home/filter.go b/internal/home/filter.go index 5c6b0fa6..b85336d9 100644 --- a/internal/home/filter.go +++ b/internal/home/filter.go @@ -6,6 +6,7 @@ import ( "hash/crc32" "io" "io/ioutil" + "net/http" "os" "path/filepath" "regexp" @@ -254,7 +255,7 @@ func (f *Filtering) periodicallyRefreshFilters() { isNetworkErr := false if config.DNS.FiltersUpdateIntervalHours != 0 && atomic.CompareAndSwapUint32(&f.refreshStatus, 0, 1) { f.refreshLock.Lock() - _, isNetworkErr = f.refreshFiltersIfNecessary(FilterRefreshBlocklists | FilterRefreshAllowlists) + _, isNetworkErr = f.refreshFiltersIfNecessary(filterRefreshBlocklists | filterRefreshAllowlists) f.refreshLock.Unlock() f.refreshStatus = 0 if !isNetworkErr { @@ -274,7 +275,7 @@ func (f *Filtering) periodicallyRefreshFilters() { } // Refresh filters -// flags: FilterRefresh* +// flags: filterRefresh* // important: // TRUE: ignore the fact that we're currently updating the filters func (f *Filtering) refreshFilters(flags int, important bool) (int, error) { @@ -367,14 +368,14 @@ func (f *Filtering) refreshFiltersArray(filters *[]filter, force bool) (int, []f } const ( - FilterRefreshForce = 1 // ignore last file modification date - FilterRefreshAllowlists = 2 // update allow-lists - FilterRefreshBlocklists = 4 // update block-lists + filterRefreshForce = 1 // ignore last file modification date + filterRefreshAllowlists = 2 // update allow-lists + filterRefreshBlocklists = 4 // update block-lists ) // Checks filters updates if necessary // If force is true, it ignores the filter.LastUpdated field value -// flags: FilterRefresh* +// flags: filterRefresh* // // Algorithm: // . Get the list of filters to be updated @@ -400,13 +401,13 @@ func (f *Filtering) refreshFiltersIfNecessary(flags int) (int, bool) { netError := false netErrorW := false force := false - if (flags & FilterRefreshForce) != 0 { + if (flags & filterRefreshForce) != 0 { force = true } - if (flags & FilterRefreshBlocklists) != 0 { + if (flags & filterRefreshBlocklists) != 0 { updateCount, updateFilters, updateFlags, netError = f.refreshFiltersArray(&config.Filters, force) } - if (flags & FilterRefreshAllowlists) != 0 { + if (flags & filterRefreshAllowlists) != 0 { updateCountW := 0 var updateFiltersW []filter var updateFlagsW []bool @@ -497,46 +498,7 @@ func (f *Filtering) update(filter *filter) (bool, error) { return b, err } -// nolint(gocyclo) -func (f *Filtering) updateIntl(filter *filter) (bool, error) { - log.Tracef("Downloading update for filter %d from %s", filter.ID, filter.URL) - - tmpFile, err := ioutil.TempFile(filepath.Join(Context.getDataDir(), filterDir), "") - if err != nil { - return false, err - } - defer func() { - if tmpFile != nil { - _ = tmpFile.Close() - _ = os.Remove(tmpFile.Name()) - } - }() - - var reader io.Reader - if filepath.IsAbs(filter.URL) { - f, err := os.Open(filter.URL) - if err != nil { - return false, fmt.Errorf("open file: %w", err) - } - defer f.Close() - reader = f - } else { - resp, err := Context.client.Get(filter.URL) - if resp != nil && resp.Body != nil { - defer resp.Body.Close() - } - if err != nil { - log.Printf("Couldn't request filter from URL %s, skipping: %s", filter.URL, err) - return false, err - } - - if resp.StatusCode != 200 { - log.Printf("Got status code %d from URL %s, skipping", resp.StatusCode, filter.URL) - return false, fmt.Errorf("got status code != 200: %d", resp.StatusCode) - } - reader = resp.Body - } - +func (f *Filtering) read(reader io.Reader, tmpFile *os.File, filter *filter) (int, error) { htmlTest := true firstChunk := make([]byte, 4*1024) firstChunkLen := 0 @@ -556,12 +518,12 @@ func (f *Filtering) updateIntl(filter *filter) (bool, error) { if firstChunkLen == len(firstChunk) || err == io.EOF { if !isPrintableText(firstChunk, firstChunkLen) { - return false, fmt.Errorf("data contains non-printable characters") + return total, fmt.Errorf("data contains non-printable characters") } s := strings.ToLower(string(firstChunk)) if strings.Contains(s, "= '0' && s[1] <= '9') { - t = jsonTNum - } - s = s[1+sep+1:] - } - - *ps = s - return k, v, t -} diff --git a/internal/querylog/decode_test.go b/internal/querylog/decode_test.go index 35863890..ffcf94dc 100644 --- a/internal/querylog/decode_test.go +++ b/internal/querylog/decode_test.go @@ -2,76 +2,183 @@ package querylog import ( "bytes" + "encoding/base64" + "net" "strings" "testing" + "time" + "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" "github.com/AdguardTeam/AdGuardHome/internal/testutil" "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/urlfilter/rules" + "github.com/miekg/dns" "github.com/stretchr/testify/assert" ) -func TestDecode_decodeQueryLog(t *testing.T) { +func TestDecodeLogEntry(t *testing.T) { logOutput := &bytes.Buffer{} testutil.ReplaceLogWriter(t, logOutput) testutil.ReplaceLogLevel(t, log.DEBUG) + t.Run("success", func(t *testing.T) { + const ansStr = `Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==` + const data = `{"IP":"127.0.0.1",` + + `"T":"2020-11-25T18:55:56.519796+03:00",` + + `"QH":"an.yandex.ru",` + + `"QT":"A",` + + `"QC":"IN",` + + `"CP":"",` + + `"Answer":"` + ansStr + `",` + + `"Result":{` + + `"IsFiltered":true,` + + `"Reason":3,` + + `"ReverseHosts":["example.net"],` + + `"IPList":["127.0.0.2"],` + + `"Rules":[{"FilterListID":42,"Text":"||an.yandex.ru","IP":"127.0.0.2"},` + + `{"FilterListID":43,"Text":"||an2.yandex.ru","IP":"127.0.0.3"}],` + + `"CanonName":"example.com",` + + `"ServiceName":"example.org",` + + `"DNSRewriteResult":{"RCode":0,"Response":{"1":["127.0.0.2"]}}},` + + `"Elapsed":837429}` + + ans, err := base64.StdEncoding.DecodeString(ansStr) + assert.Nil(t, err) + + want := &logEntry{ + IP: "127.0.0.1", + Time: time.Date(2020, 11, 25, 15, 55, 56, 519796000, time.UTC), + QHost: "an.yandex.ru", + QType: "A", + QClass: "IN", + ClientProto: "", + Answer: ans, + Result: dnsfilter.Result{ + IsFiltered: true, + Reason: dnsfilter.FilteredBlockList, + ReverseHosts: []string{"example.net"}, + IPList: []net.IP{net.IPv4(127, 0, 0, 2)}, + Rules: []*dnsfilter.ResultRule{{ + FilterListID: 42, + Text: "||an.yandex.ru", + IP: net.IPv4(127, 0, 0, 2), + }, { + FilterListID: 43, + Text: "||an2.yandex.ru", + IP: net.IPv4(127, 0, 0, 3), + }}, + CanonName: "example.com", + ServiceName: "example.org", + DNSRewriteResult: &dnsfilter.DNSRewriteResult{ + RCode: dns.RcodeSuccess, + Response: dnsfilter.DNSRewriteResultResponse{ + dns.TypeA: []rules.RRValue{net.IPv4(127, 0, 0, 2)}, + }, + }, + }, + Elapsed: 837429, + } + + got := &logEntry{} + decodeLogEntry(got, data) + + s := logOutput.String() + assert.Equal(t, "", s) + + // Correct for time zones. + got.Time = got.Time.UTC() + assert.Equal(t, want, got) + }) + testCases := []struct { name string log string want string }{{ - name: "back_compatibility_all_right", - log: `{"Question":"ULgBAAABAAAAAAAAC2FkZ3VhcmR0ZWFtBmdpdGh1YgJpbwAAHAAB","Answer":"ULiBgAABAAAAAQAAC2FkZ3VhcmR0ZWFtBmdpdGh1YgJpbwAAHAABwBgABgABAAADQgBLB25zLTE2MjIJYXdzZG5zLTEwAmNvAnVrABFhd3NkbnMtaG9zdG1hc3RlcgZhbWF6b24DY29tAAAAAAEAABwgAAADhAASdQAAAVGA","Result":{},"Time":"2020-11-13T12:41:25.970861+03:00","Elapsed":244066501,"IP":"127.0.0.1","Upstream":"https://1.1.1.1:443/dns-query"}`, - want: "default", + name: "all_right_old_rule", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":"||an.yandex.","FilterID":1,"ReverseHosts":["example.com"],"IPList":["127.0.0.1"]},"Elapsed":837429}`, + want: "", }, { - name: "back_compatibility_bad_msg", - log: `{"Question":"","Answer":"ULiBgAABAAAAAQAAC2FkZ3VhcmR0ZWFtBmdpdGh1YgJpbwAAHAABwBgABgABAAADQgBLB25zLTE2MjIJYXdzZG5zLTEwAmNvAnVrABFhd3NkbnMtaG9zdG1hc3RlcgZhbWF6b24DY29tAAAAAAEAABwgAAADhAASdQAAAVGA","Result":{},"Time":"2020-11-13T12:41:25.970861+03:00","Elapsed":244066501,"IP":"127.0.0.1","Upstream":"https://1.1.1.1:443/dns-query"}`, - want: "decodeLogEntry err: dns: overflow unpacking uint16\n", + name: "bad_filter_id_old_rule", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"FilterID":1.5},"Elapsed":837429}`, + want: "decodeResult handler err: strconv.ParseInt: parsing \"1.5\": invalid syntax\n", }, { - name: "back_compatibility_bad_decoding", - log: `{"Question":"LgBAAABAAAAAAAAC2FkZ3VhcmR0ZWFtBmdpdGh1YgJpbwAAHAAB","Answer":"ULiBgAABAAAAAQAAC2FkZ3VhcmR0ZWFtBmdpdGh1YgJpbwAAHAABwBgABgABAAADQgBLB25zLTE2MjIJYXdzZG5zLTEwAmNvAnVrABFhd3NkbnMtaG9zdG1hc3RlcgZhbWF6b24DY29tAAAAAAEAABwgAAADhAASdQAAAVGA","Result":{},"Time":"2020-11-13T12:41:25.970861+03:00","Elapsed":244066501,"IP":"127.0.0.1","Upstream":"https://1.1.1.1:443/dns-query"}`, - want: "decodeLogEntry err: illegal base64 data at input byte 48\n", + name: "bad_is_filtered", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":trooe,"Reason":3},"Elapsed":837429}`, + want: "decodeLogEntry err: invalid character 'o' in literal true (expecting 'u')\n", + }, { + name: "bad_elapsed", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":-1}`, + want: "", + }, { + name: "bad_ip", + log: `{"IP":127001,"T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`, + want: "", + }, { + name: "bad_time", + log: `{"IP":"127.0.0.1","T":"12/09/1998T15:00:00.000000+05:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`, + want: "decodeLogEntry handler err: parsing time \"12/09/1998T15:00:00.000000+05:00\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"9/1998T15:00:00.000000+05:00\" as \"2006\"\n", + }, { + name: "bad_host", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":6,"QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`, + want: "", + }, { + name: "bad_type", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":true,"QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`, + want: "", + }, { + name: "bad_class", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":false,"CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`, + want: "", + }, { + name: "bad_client_proto", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":8,"Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`, + want: "", + }, { + name: "very_bad_client_proto", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"dog","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`, + want: "decodeLogEntry handler err: invalid client proto: \"dog\"\n", + }, { + name: "bad_answer", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":0.9,"Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`, + want: "", + }, { + name: "very_bad_answer", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`, + want: "decodeLogEntry handler err: illegal base64 data at input byte 61\n", + }, { + name: "bad_rule", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":false},"Elapsed":837429}`, + want: "", + }, { + name: "bad_reason", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":true},"Elapsed":837429}`, + want: "", + }, { + name: "bad_reverse_hosts", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"ReverseHosts":[{}]},"Elapsed":837429}`, + want: "decodeResultReverseHosts: unexpected delim \"{\"\n", + }, { + name: "bad_ip_list", + log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"ReverseHosts":["example.net"],"IPList":[{}]},"Elapsed":837429}`, + want: "decodeResultIPList: unexpected delim \"{\"\n", }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - _, err := logOutput.Write([]byte("default")) - assert.Nil(t, err) - l := &logEntry{} decodeLogEntry(l, tc.log) - assert.True(t, strings.HasSuffix(logOutput.String(), tc.want), logOutput.String()) + s := logOutput.String() + if tc.want == "" { + assert.Equal(t, "", s) + } else { + assert.True(t, strings.HasSuffix(s, tc.want), + "got %q", s) + } logOutput.Reset() }) } } - -func TestJSON(t *testing.T) { - s := ` - {"keystr":"val","obj":{"keybool":true,"keyint":123456}} - ` - k, v, jtype := readJSON(&s) - assert.Equal(t, jtype, int32(jsonTStr)) - assert.Equal(t, "keystr", k) - assert.Equal(t, "val", v) - - k, _, jtype = readJSON(&s) - assert.Equal(t, jtype, int32(jsonTObj)) - assert.Equal(t, "obj", k) - - k, v, jtype = readJSON(&s) - assert.Equal(t, jtype, int32(jsonTBool)) - assert.Equal(t, "keybool", k) - assert.Equal(t, "true", v) - - k, v, jtype = readJSON(&s) - assert.Equal(t, jtype, int32(jsonTNum)) - assert.Equal(t, "keyint", k) - assert.Equal(t, "123456", v) - - _, _, jtype = readJSON(&s) - assert.True(t, jtype == jsonTErr) -} diff --git a/internal/querylog/qlog_http.go b/internal/querylog/http.go similarity index 99% rename from internal/querylog/qlog_http.go rename to internal/querylog/http.go index 473df2a4..9bc63b7e 100644 --- a/internal/querylog/qlog_http.go +++ b/internal/querylog/http.go @@ -47,7 +47,7 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) { entries, oldest := l.search(params) // convert log entries to JSON - var data = l.entriesToJSON(entries, oldest) + data := l.entriesToJSON(entries, oldest) jsonVal, err := json.Marshal(data) if err != nil { diff --git a/internal/querylog/json.go b/internal/querylog/json.go index 46f3ea0b..3beeb0f1 100644 --- a/internal/querylog/json.go +++ b/internal/querylog/json.go @@ -6,10 +6,13 @@ import ( "strconv" "time" + "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" "github.com/AdguardTeam/golibs/log" "github.com/miekg/dns" ) +// TODO(a.garipov): Use a proper structured approach here. + // Get Client IP address func (l *queryLog) getClientIP(clientIP string) string { if l.conf.AnonymizeClientIP { @@ -29,10 +32,12 @@ func (l *queryLog) getClientIP(clientIP string) string { return clientIP } -// entriesToJSON - converts log entries to JSON -func (l *queryLog) entriesToJSON(entries []*logEntry, oldest time.Time) map[string]interface{} { - // init the response object - var data = []map[string]interface{}{} +// jobject is a JSON object alias. +type jobject = map[string]interface{} + +// entriesToJSON converts query log entries to JSON. +func (l *queryLog) entriesToJSON(entries []*logEntry, oldest time.Time) (res jobject) { + data := []jobject{} // the elements order is already reversed (from newer to older) for i := 0; i < len(entries); i++ { @@ -41,17 +46,18 @@ func (l *queryLog) entriesToJSON(entries []*logEntry, oldest time.Time) map[stri data = append(data, jsonEntry) } - var result = map[string]interface{}{} - result["oldest"] = "" - if !oldest.IsZero() { - result["oldest"] = oldest.Format(time.RFC3339Nano) + res = jobject{ + "data": data, + "oldest": "", + } + if !oldest.IsZero() { + res["oldest"] = oldest.Format(time.RFC3339Nano) } - result["data"] = data - return result + return res } -func (l *queryLog) logEntryToJSONEntry(entry *logEntry) map[string]interface{} { +func (l *queryLog) logEntryToJSONEntry(entry *logEntry) (jsonEntry jobject) { var msg *dns.Msg if len(entry.Answer) > 0 { @@ -62,17 +68,18 @@ func (l *queryLog) logEntryToJSONEntry(entry *logEntry) map[string]interface{} { } } - jsonEntry := map[string]interface{}{ + jsonEntry = jobject{ "reason": entry.Result.Reason.String(), "elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64), "time": entry.Time.Format(time.RFC3339Nano), "client": l.getClientIP(entry.IP), "client_proto": entry.ClientProto, - } - jsonEntry["question"] = map[string]interface{}{ - "host": entry.QHost, - "type": entry.QType, - "class": entry.QClass, + "upstream": entry.Upstream, + "question": jobject{ + "host": entry.QHost, + "type": entry.QType, + "class": entry.QClass, + }, } if msg != nil { @@ -83,12 +90,15 @@ func (l *queryLog) logEntryToJSONEntry(entry *logEntry) map[string]interface{} { if opt != nil { dnssecOk = opt.Do() } + jsonEntry["answer_dnssec"] = dnssecOk } - if len(entry.Result.Rule) > 0 { - jsonEntry["rule"] = entry.Result.Rule - jsonEntry["filterId"] = entry.Result.FilterID + jsonEntry["rules"] = resultRulesToJSONRules(entry.Result.Rules) + + if len(entry.Result.Rules) > 0 && len(entry.Result.Rules[0].Text) > 0 { + jsonEntry["rule"] = entry.Result.Rules[0].Text + jsonEntry["filterId"] = entry.Result.Rules[0].FilterListID } if len(entry.Result.ServiceName) != 0 { @@ -113,20 +123,30 @@ func (l *queryLog) logEntryToJSONEntry(entry *logEntry) map[string]interface{} { } } - jsonEntry["upstream"] = entry.Upstream - return jsonEntry } -func answerToMap(a *dns.Msg) []map[string]interface{} { +func resultRulesToJSONRules(rules []*dnsfilter.ResultRule) (jsonRules []jobject) { + jsonRules = make([]jobject, len(rules)) + for i, r := range rules { + jsonRules[i] = jobject{ + "filter_list_id": r.FilterListID, + "text": r.Text, + } + } + + return jsonRules +} + +func answerToMap(a *dns.Msg) (answers []jobject) { if a == nil || len(a.Answer) == 0 { return nil } - var answers = []map[string]interface{}{} + answers = []jobject{} for _, k := range a.Answer { header := k.Header() - answer := map[string]interface{}{ + answer := jobject{ "type": dns.TypeToString[header.Rrtype], "ttl": header.Ttl, } diff --git a/internal/querylog/qlog_test.go b/internal/querylog/qlog_test.go index 4dec5175..fd37a1de 100644 --- a/internal/querylog/qlog_test.go +++ b/internal/querylog/qlog_test.go @@ -1,9 +1,12 @@ package querylog import ( + "math/rand" "net" "os" + "sort" "testing" + "time" "github.com/AdguardTeam/dnsproxy/proxyutil" @@ -20,7 +23,7 @@ func TestMain(m *testing.M) { func prepareTestDir() string { const dir = "./agh-test" _ = os.RemoveAll(dir) - _ = os.MkdirAll(dir, 0755) + _ = os.MkdirAll(dir, 0o755) return dir } @@ -233,10 +236,12 @@ func addEntry(l *queryLog, host, answerStr, client string) { a.Answer = append(a.Answer, answer) res := dnsfilter.Result{ IsFiltered: true, - Rule: "SomeRule", Reason: dnsfilter.ReasonRewrite, ServiceName: "SomeService", - FilterID: 1, + Rules: []*dnsfilter.ResultRule{{ + FilterListID: 1, + Text: "SomeRule", + }}, } params := AddParams{ Question: &q, @@ -263,3 +268,72 @@ func assertLogEntry(t *testing.T, entry *logEntry, host, answer, client string) assert.Equal(t, answer, ip.String()) return true } + +func testEntries() (entries []*logEntry) { + rsrc := rand.NewSource(time.Now().UnixNano()) + rgen := rand.New(rsrc) + + entries = make([]*logEntry, 1000) + for i := range entries { + min := rgen.Intn(60) + sec := rgen.Intn(60) + entries[i] = &logEntry{ + Time: time.Date(2020, 1, 1, 0, min, sec, 0, time.UTC), + } + } + + return entries +} + +// logEntriesByTimeDesc is a wrapper over []*logEntry for sorting. +// +// NOTE(a.garipov): Weirdly enough, on my machine this gets consistently +// outperformed by sort.Slice, see the benchmark below. I'm leaving this +// implementation here, in tests, in case we want to make sure it outperforms on +// most machines, but for now this is unused in the actual code. +type logEntriesByTimeDesc []*logEntry + +// Len implements the sort.Interface interface for logEntriesByTimeDesc. +func (les logEntriesByTimeDesc) Len() (n int) { return len(les) } + +// Less implements the sort.Interface interface for logEntriesByTimeDesc. +func (les logEntriesByTimeDesc) Less(i, j int) (less bool) { + return les[i].Time.After(les[j].Time) +} + +// Swap implements the sort.Interface interface for logEntriesByTimeDesc. +func (les logEntriesByTimeDesc) Swap(i, j int) { les[i], les[j] = les[j], les[i] } + +func BenchmarkLogEntry_sort(b *testing.B) { + b.Run("methods", func(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + entries := testEntries() + b.StartTimer() + + sort.Stable(logEntriesByTimeDesc(entries)) + } + }) + + b.Run("reflect", func(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + entries := testEntries() + b.StartTimer() + + sort.SliceStable(entries, func(i, j int) (less bool) { + return entries[i].Time.After(entries[j].Time) + }) + } + }) +} + +func TestLogEntriesByTime_sort(t *testing.T) { + entries := testEntries() + sort.Sort(logEntriesByTimeDesc(entries)) + + for i := 1; i < len(entries); i++ { + assert.False(t, entries[i].Time.After(entries[i-1].Time), + "%s %s", entries[i].Time, entries[i-1].Time) + } +} diff --git a/internal/querylog/qlog_file.go b/internal/querylog/qlogfile.go similarity index 89% rename from internal/querylog/qlog_file.go rename to internal/querylog/qlogfile.go index 5c5b8fc9..69a42ed2 100644 --- a/internal/querylog/qlog_file.go +++ b/internal/querylog/qlogfile.go @@ -1,6 +1,7 @@ package querylog import ( + "fmt" "io" "os" "sync" @@ -10,12 +11,12 @@ import ( "github.com/AdguardTeam/golibs/log" ) -// ErrSeekNotFound is returned from Seek if when it fails to find the requested -// record. -const ErrSeekNotFound agherr.Error = "seek: record not found" - -// ErrEndOfLog is returned from Seek when the end of the current log is reached. -const ErrEndOfLog agherr.Error = "seek: end of log" +// Timestamp not found errors. +const ( + ErrTSNotFound agherr.Error = "ts not found" + ErrTSTooLate agherr.Error = "ts too late" + ErrTSTooEarly agherr.Error = "ts too early" +) // TODO: Find a way to grow buffer instead of relying on this value when reading strings const maxEntrySize = 16 * 1024 @@ -52,7 +53,7 @@ func NewQLogFile(path string) (*QLogFile, error) { }, nil } -// Seek performs binary search in the query log file looking for a record +// SeekTS performs binary search in the query log file looking for a record // with the specified timestamp. Once the record is found, it sets // "position" so that the next ReadNext call returned that record. // @@ -68,8 +69,8 @@ func NewQLogFile(path string) (*QLogFile, error) { // * It returns the position of the the line with the timestamp we were looking for // so that when we call "ReadNext" this line was returned. // * Depth of the search (how many times we compared timestamps). -// * If we could not find it, it returns ErrSeekNotFound -func (q *QLogFile) Seek(timestamp int64) (int64, int, error) { +// * If we could not find it, it returns one of the errors described above. +func (q *QLogFile) SeekTS(timestamp int64) (int64, int, error) { q.lock.Lock() defer q.lock.Unlock() @@ -103,13 +104,17 @@ func (q *QLogFile) Seek(timestamp int64) (int64, int, error) { return 0, depth, err } - if lineIdx < start || lineEndIdx > end || lineIdx == lastProbeLineIdx { + if lineIdx == lastProbeLineIdx { + if lineIdx == 0 { + return 0, depth, ErrTSTooEarly + } + // If we're testing the same line twice then most likely - // the scope is too narrow and we won't find anything anymore - log.Error("querylog: didn't find timestamp:%v", timestamp) - return 0, depth, ErrSeekNotFound - } else if lineIdx == end && lineEndIdx == end { - return 0, depth, ErrEndOfLog + // the scope is too narrow and we won't find anything + // anymore in any other file. + return 0, depth, fmt.Errorf("looking up timestamp %d in %q: %w", timestamp, q.file.Name(), ErrTSNotFound) + } else if lineIdx == fileInfo.Size() { + return 0, depth, ErrTSTooLate } // Save the last found idx @@ -117,9 +122,8 @@ func (q *QLogFile) Seek(timestamp int64) (int64, int, error) { // Get the timestamp from the query log record ts := readQLogTimestamp(line) - if ts == 0 { - return 0, depth, ErrSeekNotFound + return 0, depth, fmt.Errorf("looking up timestamp %d in %q: record %q has empty timestamp", timestamp, q.file.Name(), line) } if ts == timestamp { @@ -141,8 +145,7 @@ func (q *QLogFile) Seek(timestamp int64) (int64, int, error) { depth++ if depth >= 100 { - log.Error("Seek depth is too high, aborting. File %s, ts %v", q.file.Name(), timestamp) - return 0, depth, ErrSeekNotFound + return 0, depth, fmt.Errorf("looking up timestamp %d in %q: depth %d too high: %w", timestamp, q.file.Name(), depth, ErrTSNotFound) } } diff --git a/internal/querylog/qlog_file_test.go b/internal/querylog/qlogfile_test.go similarity index 84% rename from internal/querylog/qlog_file_test.go rename to internal/querylog/qlogfile_test.go index de402e8b..950eaaf3 100644 --- a/internal/querylog/qlog_file_test.go +++ b/internal/querylog/qlogfile_test.go @@ -61,7 +61,7 @@ func TestQLogFileLarge(t *testing.T) { line, err = q.ReadNext() if err == nil { assert.True(t, len(line) > 0) - read += 1 + read++ } } @@ -96,12 +96,12 @@ func TestQLogFileSeekLargeFile(t *testing.T) { testSeekLineQLogFile(t, q, count) // CASE 5: Seek non-existent (too low) - _, _, err = q.Seek(123) + _, _, err = q.SeekTS(123) assert.NotNil(t, err) // CASE 6: Seek non-existent (too high) ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00") - _, _, err = q.Seek(ts.UnixNano()) + _, _, err = q.SeekTS(ts.UnixNano()) assert.NotNil(t, err) // CASE 7: "Almost" found @@ -110,7 +110,7 @@ func TestQLogFileSeekLargeFile(t *testing.T) { // ALMOST the record we need timestamp := readQLogTimestamp(line) - 1 assert.NotEqual(t, uint64(0), timestamp) - _, depth, err := q.Seek(timestamp) + _, depth, err := q.SeekTS(timestamp) assert.NotNil(t, err) assert.True(t, depth <= int(math.Log2(float64(count))+3)) } @@ -142,12 +142,12 @@ func TestQLogFileSeekSmallFile(t *testing.T) { testSeekLineQLogFile(t, q, count) // CASE 5: Seek non-existent (too low) - _, _, err = q.Seek(123) + _, _, err = q.SeekTS(123) assert.NotNil(t, err) // CASE 6: Seek non-existent (too high) ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00") - _, _, err = q.Seek(ts.UnixNano()) + _, _, err = q.SeekTS(ts.UnixNano()) assert.NotNil(t, err) // CASE 7: "Almost" found @@ -156,7 +156,7 @@ func TestQLogFileSeekSmallFile(t *testing.T) { // ALMOST the record we need timestamp := readQLogTimestamp(line) - 1 assert.NotEqual(t, uint64(0), timestamp) - _, depth, err := q.Seek(timestamp) + _, depth, err := q.SeekTS(timestamp) assert.NotNil(t, err) assert.True(t, depth <= int(math.Log2(float64(count))+3)) } @@ -168,7 +168,7 @@ func testSeekLineQLogFile(t *testing.T, q *QLogFile, lineNumber int) { assert.NotEqual(t, uint64(0), ts) // try seeking to that line now - pos, _, err := q.Seek(ts) + pos, _, err := q.SeekTS(ts) assert.Nil(t, err) assert.NotEqual(t, int64(0), pos) @@ -243,13 +243,13 @@ func prepareTestFiles(dir string, filesCount, linesCount int) []string { lineTime, _ := time.Parse(time.RFC3339Nano, "2020-02-18T22:36:35.920973+03:00") lineIP := uint32(0) - files := make([]string, 0) + files := make([]string, filesCount) for j := 0; j < filesCount; j++ { f, _ := ioutil.TempFile(dir, "*.txt") - files = append(files, f.Name()) + files[filesCount-j-1] = f.Name() for i := 0; i < linesCount; i++ { - lineIP += 1 + lineIP++ lineTime = lineTime.Add(time.Second) ip := make(net.IP, 4) @@ -284,12 +284,12 @@ func TestQLogSeek(t *testing.T) { target, _ := time.Parse(time.RFC3339, "2020-08-31T18:44:25.376690873+03:00") - _, depth, err := q.Seek(target.UnixNano()) + _, depth, err := q.SeekTS(target.UnixNano()) assert.Nil(t, err) assert.Equal(t, 1, depth) } -func TestQLogSeek_ErrEndOfLog(t *testing.T) { +func TestQLogSeek_ErrTSTooLate(t *testing.T) { testDir := prepareTestDir() t.Cleanup(func() { _ = os.RemoveAll(testDir) @@ -313,7 +313,36 @@ func TestQLogSeek_ErrEndOfLog(t *testing.T) { target, err := time.Parse(time.RFC3339, "2020-08-31T18:44:25.382540454+03:00") assert.Nil(t, err) - _, depth, err := q.Seek(target.UnixNano() + int64(time.Second)) - assert.Equal(t, ErrEndOfLog, err) + _, depth, err := q.SeekTS(target.UnixNano() + int64(time.Second)) + assert.Equal(t, ErrTSTooLate, err) assert.Equal(t, 2, depth) } + +func TestQLogSeek_ErrTSTooEarly(t *testing.T) { + testDir := prepareTestDir() + t.Cleanup(func() { + _ = os.RemoveAll(testDir) + }) + + d := `{"T":"2020-08-31T18:44:23.911246629+03:00","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"} +{"T":"2020-08-31T18:44:25.376690873+03:00"} +{"T":"2020-08-31T18:44:25.382540454+03:00"} +` + f, err := ioutil.TempFile(testDir, "*.txt") + assert.Nil(t, err) + defer f.Close() + + _, err = f.WriteString(d) + assert.Nil(t, err) + + q, err := NewQLogFile(f.Name()) + assert.Nil(t, err) + defer q.Close() + + target, err := time.Parse(time.RFC3339, "2020-08-31T18:44:23.911246629+03:00") + assert.Nil(t, err) + + _, depth, err := q.SeekTS(target.UnixNano() - int64(time.Second)) + assert.Equal(t, ErrTSTooEarly, err) + assert.Equal(t, 1, depth) +} diff --git a/internal/querylog/qlog_reader.go b/internal/querylog/qlogreader.go similarity index 75% rename from internal/querylog/qlog_reader.go rename to internal/querylog/qlogreader.go index 41677c3e..19909110 100644 --- a/internal/querylog/qlog_reader.go +++ b/internal/querylog/qlogreader.go @@ -2,6 +2,7 @@ package querylog import ( "errors" + "fmt" "io" "github.com/AdguardTeam/AdGuardHome/internal/agherr" @@ -43,28 +44,35 @@ func NewQLogReader(files []string) (*QLogReader, error) { }, nil } -// Seek performs binary search of a query log record with the specified timestamp. -// If the record is found, it sets QLogReader's position to point to that line, -// so that the next ReadNext call returned this line. -// -// Returns nil if the record is successfully found. -// Returns an error if for some reason we could not find a record with the specified timestamp. -func (r *QLogReader) Seek(timestamp int64) error { +// SeekTS performs binary search of a query log record with the specified +// timestamp. If the record is found, it sets QLogReader's position to point to +// that line, so that the next ReadNext call returned this line. +func (r *QLogReader) SeekTS(timestamp int64) (err error) { for i := len(r.qFiles) - 1; i >= 0; i-- { q := r.qFiles[i] - _, _, err := q.Seek(timestamp) - if err == nil || errors.Is(err, ErrEndOfLog) { - // Our search is finished, and we either found the - // element we were looking for or reached the end of the - // log. Update currentFile only, position is already - // set properly in QLogFile. + _, _, err = q.SeekTS(timestamp) + if err == nil { + // Search is finished, and the searched element have + // been found. Update currentFile only, position is + // already set properly in QLogFile. r.currentFile = i - return err + return nil + } else if errors.Is(err, ErrTSTooEarly) { + // Look at the next file, since we've reached the end of + // this one. + continue + } else if errors.Is(err, ErrTSTooLate) { + // Just seek to the start then. timestamp is probably + // between the end of the previous one and the start of + // this one. + return r.SeekStart() + } else if errors.Is(err, ErrTSNotFound) { + break } } - return ErrSeekNotFound + return fmt.Errorf("querylog: %w", err) } // SeekStart changes the current position to the end of the newest file diff --git a/internal/querylog/qlog_reader_test.go b/internal/querylog/qlogreader_test.go similarity index 57% rename from internal/querylog/qlog_reader_test.go rename to internal/querylog/qlogreader_test.go index 357b4f9d..d9dfb3ea 100644 --- a/internal/querylog/qlog_reader_test.go +++ b/internal/querylog/qlogreader_test.go @@ -1,6 +1,7 @@ package querylog import ( + "errors" "io" "os" "testing" @@ -49,7 +50,7 @@ func TestQLogReaderOneFile(t *testing.T) { line, err = r.ReadNext() if err == nil { assert.True(t, len(line) > 0) - read += 1 + read++ } } @@ -82,7 +83,7 @@ func TestQLogReaderMultipleFiles(t *testing.T) { line, err = r.ReadNext() if err == nil { assert.True(t, len(line) > 0) - read += 1 + read++ } } @@ -90,6 +91,116 @@ func TestQLogReaderMultipleFiles(t *testing.T) { assert.Equal(t, io.EOF, err) } +func TestQLogReader_Seek(t *testing.T) { + count := 10000 + filesCount := 2 + + testDir := prepareTestDir() + t.Cleanup(func() { + _ = os.RemoveAll(testDir) + }) + testFiles := prepareTestFiles(testDir, filesCount, count) + + r, err := NewQLogReader(testFiles) + assert.Nil(t, err) + assert.NotNil(t, r) + t.Cleanup(func() { + _ = r.Close() + }) + + testCases := []struct { + name string + time string + want error + }{{ + name: "not_too_old", + time: "2020-02-19T04:04:56.920973+03:00", + want: nil, + }, { + name: "old", + time: "2020-02-19T01:28:16.920973+03:00", + want: nil, + }, { + name: "first", + time: "2020-02-19T04:09:55.920973+03:00", + want: nil, + }, { + name: "last", + time: "2020-02-19T01:23:16.920973+03:00", + want: nil, + }, { + name: "non-existent_long_ago", + time: "2000-02-19T01:23:16.920973+03:00", + want: ErrTSTooEarly, + }, { + name: "non-existent_far_ahead", + time: "2100-02-19T01:23:16.920973+03:00", + want: nil, + }, { + name: "non-existent_but_could", + time: "2020-02-18T22:36:37.000000+03:00", + want: ErrTSNotFound, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + timestamp, err := time.Parse(time.RFC3339Nano, tc.time) + assert.Nil(t, err) + + err = r.SeekTS(timestamp.UnixNano()) + assert.True(t, errors.Is(err, tc.want), err) + }) + } +} + +func TestQLogReader_ReadNext(t *testing.T) { + count := 10 + filesCount := 1 + + testDir := prepareTestDir() + t.Cleanup(func() { + _ = os.RemoveAll(testDir) + }) + testFiles := prepareTestFiles(testDir, filesCount, count) + + r, err := NewQLogReader(testFiles) + assert.Nil(t, err) + assert.NotNil(t, r) + t.Cleanup(func() { + _ = r.Close() + }) + + testCases := []struct { + name string + start int + want error + }{{ + name: "ok", + start: 0, + want: nil, + }, { + name: "too_big", + start: count + 1, + want: io.EOF, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := r.SeekStart() + assert.Nil(t, err, err) + + for i := 1; i < tc.start; i++ { + _, err := r.ReadNext() + assert.Nil(t, err) + } + + _, err = r.ReadNext() + assert.Equal(t, tc.want, err) + }) + } +} + +// TODO(e.burkov): Remove the tests below. Make tests above more compelling. func TestQLogReaderSeek(t *testing.T) { // more or less big file count := 10000 @@ -117,12 +228,12 @@ func TestQLogReaderSeek(t *testing.T) { testSeekLineQLogReader(t, r, count) // CASE 5: Seek non-existent (too low) - err = r.Seek(123) + err = r.SeekTS(123) assert.NotNil(t, err) // CASE 6: Seek non-existent (too high) ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00") - err = r.Seek(ts.UnixNano()) + err = r.SeekTS(ts.UnixNano()) assert.NotNil(t, err) } @@ -133,7 +244,7 @@ func testSeekLineQLogReader(t *testing.T, r *QLogReader, lineNumber int) { assert.NotEqual(t, uint64(0), ts) // try seeking to that line now - err = r.Seek(ts) + err = r.SeekTS(ts) assert.Nil(t, err) testLine, err := r.ReadNext() diff --git a/internal/querylog/querylog_file.go b/internal/querylog/querylogfile.go similarity index 99% rename from internal/querylog/querylog_file.go rename to internal/querylog/querylogfile.go index 42e1ddf9..c6d48235 100644 --- a/internal/querylog/querylog_file.go +++ b/internal/querylog/querylogfile.go @@ -65,7 +65,7 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error { l.fileWriteLock.Lock() defer l.fileWriteLock.Unlock() - f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) if err != nil { log.Error("failed to create file \"%s\": %s", filename, err) return err diff --git a/internal/querylog/querylog_search.go b/internal/querylog/search.go similarity index 92% rename from internal/querylog/querylog_search.go rename to internal/querylog/search.go index 133a1dba..f23a42b6 100644 --- a/internal/querylog/querylog_search.go +++ b/internal/querylog/search.go @@ -1,8 +1,8 @@ package querylog import ( - "errors" "io" + "sort" "time" "github.com/AdguardTeam/AdGuardHome/internal/util" @@ -47,6 +47,14 @@ func (l *queryLog) search(params *searchParams) ([]*logEntry, time.Time) { entries = entries[:totalLimit] } + // Resort entries on start time to partially mitigate query log looking + // weird on the frontend. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/2293. + sort.SliceStable(entries, func(i, j int) (less bool) { + return entries[i].Time.After(entries[j].Time) + }) + if params.offset > 0 { if len(entries) > params.offset { entries = entries[params.offset:] @@ -89,20 +97,17 @@ func (l *queryLog) searchFiles(params *searchParams) ([]*logEntry, time.Time, in if params.olderThan.IsZero() { err = r.SeekStart() } else { - err = r.Seek(params.olderThan.UnixNano()) + err = r.SeekTS(params.olderThan.UnixNano()) if err == nil { // Read to the next record right away // The one that was specified in the "oldest" param is not needed, // we need only the one next to it _, err = r.ReadNext() - } else if errors.Is(err, ErrEndOfLog) { - // We've reached the end of the log. - return entries, time.Time{}, 0 } } if err != nil { - log.Debug("Cannot Seek() to %v: %v", params.olderThan, err) + log.Debug("Cannot SeekTS() to %v: %v", params.olderThan, err) return entries, oldest, 0 } diff --git a/internal/querylog/search_criteria.go b/internal/querylog/searchcriteria.go similarity index 60% rename from internal/querylog/search_criteria.go rename to internal/querylog/searchcriteria.go index a4213408..b98e0838 100644 --- a/internal/querylog/search_criteria.go +++ b/internal/querylog/searchcriteria.go @@ -58,7 +58,7 @@ func (c *searchCriteria) quickMatch(line string) bool { } // quickMatchJSONValue - helper used by quickMatch -func (c *searchCriteria) quickMatchJSONValue(line string, propertyName string) bool { +func (c *searchCriteria) quickMatchJSONValue(line, propertyName string) bool { val := readJSONValue(line, propertyName) if len(val) == 0 { return false @@ -77,66 +77,79 @@ func (c *searchCriteria) quickMatchJSONValue(line string, propertyName string) b } // match - checks if the log entry matches this search criteria -// nolint (gocyclo) func (c *searchCriteria) match(entry *logEntry) bool { switch c.criteriaType { case ctDomainOrClient: - qhost := strings.ToLower(entry.QHost) - searchVal := strings.ToLower(c.value) - if c.strict && qhost == searchVal { - return true - } - if !c.strict && strings.Contains(qhost, searchVal) { - return true - } - - if c.strict && entry.IP == c.value { - return true - } - if !c.strict && strings.Contains(entry.IP, c.value) { - return true - } - - return false - + return c.ctDomainOrClientCase(entry) case ctFilteringStatus: - res := entry.Result - - switch c.value { - case filteringStatusAll: - return true - case filteringStatusFiltered: - return res.IsFiltered || - res.Reason == dnsfilter.NotFilteredWhiteList || - res.Reason == dnsfilter.ReasonRewrite || - res.Reason == dnsfilter.RewriteEtcHosts - case filteringStatusBlocked: - return res.IsFiltered && - (res.Reason == dnsfilter.FilteredBlackList || - res.Reason == dnsfilter.FilteredBlockedService) - case filteringStatusBlockedService: - return res.IsFiltered && res.Reason == dnsfilter.FilteredBlockedService - case filteringStatusBlockedParental: - return res.IsFiltered && res.Reason == dnsfilter.FilteredParental - case filteringStatusBlockedSafebrowsing: - return res.IsFiltered && res.Reason == dnsfilter.FilteredSafeBrowsing - case filteringStatusWhitelisted: - return res.Reason == dnsfilter.NotFilteredWhiteList - case filteringStatusRewritten: - return res.Reason == dnsfilter.ReasonRewrite || - res.Reason == dnsfilter.RewriteEtcHosts - case filteringStatusSafeSearch: - return res.IsFiltered && res.Reason == dnsfilter.FilteredSafeSearch - - case filteringStatusProcessed: - return !(res.Reason == dnsfilter.FilteredBlackList || - res.Reason == dnsfilter.FilteredBlockedService || - res.Reason == dnsfilter.NotFilteredWhiteList) - - default: - return false - } + return c.ctFilteringStatusCase(entry.Result) } return false } + +func (c *searchCriteria) ctDomainOrClientCase(entry *logEntry) bool { + qhost := strings.ToLower(entry.QHost) + searchVal := strings.ToLower(c.value) + if c.strict && qhost == searchVal { + return true + } + if !c.strict && strings.Contains(qhost, searchVal) { + return true + } + + if c.strict && entry.IP == c.value { + return true + } + if !c.strict && strings.Contains(entry.IP, c.value) { + return true + } + return false +} + +func (c *searchCriteria) ctFilteringStatusCase(res dnsfilter.Result) bool { + switch c.value { + case filteringStatusAll: + return true + + case filteringStatusFiltered: + return res.IsFiltered || + res.Reason.In( + dnsfilter.NotFilteredAllowList, + dnsfilter.ReasonRewrite, + dnsfilter.RewriteAutoHosts, + ) + + case filteringStatusBlocked: + return res.IsFiltered && + res.Reason.In(dnsfilter.FilteredBlockList, dnsfilter.FilteredBlockedService) + + case filteringStatusBlockedService: + return res.IsFiltered && res.Reason == dnsfilter.FilteredBlockedService + + case filteringStatusBlockedParental: + return res.IsFiltered && res.Reason == dnsfilter.FilteredParental + + case filteringStatusBlockedSafebrowsing: + return res.IsFiltered && res.Reason == dnsfilter.FilteredSafeBrowsing + + case filteringStatusWhitelisted: + return res.Reason == dnsfilter.NotFilteredAllowList + + case filteringStatusRewritten: + return res.Reason.In(dnsfilter.ReasonRewrite, dnsfilter.RewriteAutoHosts) + + case filteringStatusSafeSearch: + return res.IsFiltered && res.Reason == dnsfilter.FilteredSafeSearch + + case filteringStatusProcessed: + return !res.Reason.In( + dnsfilter.FilteredBlockList, + dnsfilter.FilteredBlockedService, + dnsfilter.NotFilteredAllowList, + ) + + default: + return false + } +} diff --git a/internal/querylog/search_params.go b/internal/querylog/searchparams.go similarity index 100% rename from internal/querylog/search_params.go rename to internal/querylog/searchparams.go diff --git a/internal/stats/stats_http.go b/internal/stats/http.go similarity index 94% rename from internal/stats/stats_http.go rename to internal/stats/http.go index ab49a6ae..794bedf1 100644 --- a/internal/stats/stats_http.go +++ b/internal/stats/http.go @@ -37,7 +37,13 @@ func (s *statsCtx) handleStats(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - w.Write(data) + + _, err = w.Write(data) + if err != nil { + httpError(r, w, http.StatusInternalServerError, "json encode: %s", err) + + return + } } type config struct { diff --git a/internal/stats/stats.go b/internal/stats/stats.go index 8f77425f..5cd5910d 100644 --- a/internal/stats/stats.go +++ b/internal/stats/stats.go @@ -1,5 +1,5 @@ -// Module for managing statistics for DNS filtering server - +// Package stats provides units for managing statistics of the filtering DNS +// server. package stats import ( diff --git a/internal/stats/stats_unit.go b/internal/stats/unit.go similarity index 84% rename from internal/stats/stats_unit.go rename to internal/stats/unit.go index 2c406972..a8bd224c 100644 --- a/internal/stats/stats_unit.go +++ b/internal/stats/unit.go @@ -548,7 +548,6 @@ func (s *statsCtx) loadUnits(limit uint32) ([]*unitDB, uint32) { * parental-blocked These values are just the sum of data for all units. */ -// nolint (gocyclo) func (s *statsCtx) getData() map[string]interface{} { limit := s.conf.limit @@ -564,137 +563,63 @@ func (s *statsCtx) getData() map[string]interface{} { } // per time unit counters: - // 720 hours may span 31 days, so we skip data for the first day in this case firstDayID := (firstID + 24 - 1) / 24 * 24 // align_ceil(24) - a := []uint64{} - if timeUnit == Hours { - for _, u := range units { - a = append(a, u.NTotal) - } - } else { - var sum uint64 - id := firstDayID - nextDayID := firstDayID + 24 - for i := firstDayID - firstID; int(i) != len(units); i++ { - sum += units[i].NTotal - if id == nextDayID { - a = append(a, sum) - sum = 0 - nextDayID += 24 + statsCollector := func(numsGetter func(u *unitDB) (num uint64)) (nums []uint64) { + if timeUnit == Hours { + for _, u := range units { + nums = append(nums, numsGetter(u)) } - id++ - } - if id <= nextDayID { - a = append(a, sum) - } - if len(a) != int(limit/24) { - log.Fatalf("len(a) != limit: %d %d", len(a), limit) - } - } - d["dns_queries"] = a - - a = []uint64{} - if timeUnit == Hours { - for _, u := range units { - a = append(a, u.NResult[RFiltered]) - } - } else { - var sum uint64 - id := firstDayID - nextDayID := firstDayID + 24 - for i := firstDayID - firstID; int(i) != len(units); i++ { - sum += units[i].NResult[RFiltered] - if id == nextDayID { - a = append(a, sum) - sum = 0 - nextDayID += 24 + } else { + var sum uint64 + id := firstDayID + nextDayID := firstDayID + 24 + for i := int(firstDayID - firstID); i != len(units); i++ { + sum += numsGetter(units[i]) + if id == nextDayID { + nums = append(nums, sum) + sum = 0 + nextDayID += 24 + } + id++ } - id++ - } - if id <= nextDayID { - a = append(a, sum) - } - } - d["blocked_filtering"] = a - - a = []uint64{} - if timeUnit == Hours { - for _, u := range units { - a = append(a, u.NResult[RSafeBrowsing]) - } - } else { - var sum uint64 - id := firstDayID - nextDayID := firstDayID + 24 - for i := firstDayID - firstID; int(i) != len(units); i++ { - sum += units[i].NResult[RSafeBrowsing] - if id == nextDayID { - a = append(a, sum) - sum = 0 - nextDayID += 24 + if id <= nextDayID { + nums = append(nums, sum) } - id++ - } - if id <= nextDayID { - a = append(a, sum) } + return nums } - d["replaced_safebrowsing"] = a - a = []uint64{} - if timeUnit == Hours { + topsCollector := func(max int, pairsGetter func(u *unitDB) (pairs []countPair)) []map[string]uint64 { + m := map[string]uint64{} for _, u := range units { - a = append(a, u.NResult[RParental]) - } - } else { - var sum uint64 - id := firstDayID - nextDayID := firstDayID + 24 - for i := firstDayID - firstID; int(i) != len(units); i++ { - sum += units[i].NResult[RParental] - if id == nextDayID { - a = append(a, sum) - sum = 0 - nextDayID += 24 + for _, it := range pairsGetter(u) { + m[it.Name] += it.Count } - id++ - } - if id <= nextDayID { - a = append(a, sum) } + a2 := convertMapToArray(m, max) + return convertTopArray(a2) } - d["replaced_parental"] = a - // top counters: - - m := map[string]uint64{} - for _, u := range units { - for _, it := range u.Domains { - m[it.Name] += it.Count - } + dnsQueries := statsCollector(func(u *unitDB) (num uint64) { return u.NTotal }) + if timeUnit != Hours && len(dnsQueries) != int(limit/24) { + log.Fatalf("len(dnsQueries) != limit: %d %d", len(dnsQueries), limit) } - a2 := convertMapToArray(m, maxDomains) - d["top_queried_domains"] = convertTopArray(a2) - m = map[string]uint64{} - for _, u := range units { - for _, it := range u.BlockedDomains { - m[it.Name] += it.Count - } + statsData := map[string]interface{}{ + "dns_queries": dnsQueries, + "blocked_filtering": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }), + "replaced_safebrowsing": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }), + "replaced_parental": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RParental] }), + "top_queried_domains": topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.Domains }), + "top_blocked_domains": topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }), + "top_clients": topsCollector(maxClients, func(u *unitDB) (pairs []countPair) { return u.Clients }), } - a2 = convertMapToArray(m, maxDomains) - d["top_blocked_domains"] = convertTopArray(a2) - m = map[string]uint64{} - for _, u := range units { - for _, it := range u.Clients { - m[it.Name] += it.Count - } + for dataKey, dataValue := range statsData { + d[dataKey] = dataValue } - a2 = convertMapToArray(m, maxClients) - d["top_clients"] = convertTopArray(a2) // total counters: diff --git a/internal/sysutil/net.go b/internal/sysutil/net.go new file mode 100644 index 00000000..557dd8d7 --- /dev/null +++ b/internal/sysutil/net.go @@ -0,0 +1,44 @@ +package sysutil + +import ( + "net" + "os/exec" + "strings" + + "github.com/AdguardTeam/golibs/log" +) + +// IfaceHasStaticIP checks if interface is configured to have static IP address. +func IfaceHasStaticIP(ifaceName string) (has bool, err error) { + return ifaceHasStaticIP(ifaceName) +} + +// IfaceSetStaticIP sets static IP address for network interface. +func IfaceSetStaticIP(ifaceName string) (err error) { + return ifaceSetStaticIP(ifaceName) +} + +// GatewayIP returns IP address of interface's gateway. +func GatewayIP(ifaceName string) string { + cmd := exec.Command("ip", "route", "show", "dev", ifaceName) + log.Tracef("executing %s %v", cmd.Path, cmd.Args) + d, err := cmd.Output() + if err != nil || cmd.ProcessState.ExitCode() != 0 { + return "" + } + + fields := strings.Fields(string(d)) + // The meaningful "ip route" command output should contain the word + // "default" at first field and default gateway IP address at third + // field. + if len(fields) < 3 || fields[0] != "default" { + return "" + } + + ip := net.ParseIP(fields[2]) + if ip == nil { + return "" + } + + return fields[2] +} diff --git a/internal/sysutil/net_darwin.go b/internal/sysutil/net_darwin.go new file mode 100644 index 00000000..1bc1ac61 --- /dev/null +++ b/internal/sysutil/net_darwin.go @@ -0,0 +1,161 @@ +// +build darwin + +package sysutil + +import ( + "errors" + "fmt" + "io/ioutil" + "regexp" + "strings" + + "github.com/AdguardTeam/AdGuardHome/internal/util" +) + +// hardwarePortInfo - information obtained using MacOS networksetup +// about the current state of the internet connection +type hardwarePortInfo struct { + name string + ip string + subnet string + gatewayIP string + static bool +} + +func ifaceHasStaticIP(ifaceName string) (bool, error) { + portInfo, err := getCurrentHardwarePortInfo(ifaceName) + if err != nil { + return false, err + } + + return portInfo.static, nil +} + +// getCurrentHardwarePortInfo gets information the specified network interface. +func getCurrentHardwarePortInfo(ifaceName string) (hardwarePortInfo, error) { + // First of all we should find hardware port name + m := getNetworkSetupHardwareReports() + hardwarePort, ok := m[ifaceName] + if !ok { + return hardwarePortInfo{}, fmt.Errorf("could not find hardware port for %s", ifaceName) + } + + return getHardwarePortInfo(hardwarePort) +} + +// getNetworkSetupHardwareReports parses the output of the `networksetup -listallhardwareports` command +// it returns a map where the key is the interface name, and the value is the "hardware port" +// returns nil if it fails to parse the output +func getNetworkSetupHardwareReports() map[string]string { + _, out, err := util.RunCommand("networksetup", "-listallhardwareports") + if err != nil { + return nil + } + + re, err := regexp.Compile("Hardware Port: (.*?)\nDevice: (.*?)\n") + if err != nil { + return nil + } + + m := make(map[string]string) + + matches := re.FindAllStringSubmatch(out, -1) + for i := range matches { + port := matches[i][1] + device := matches[i][2] + m[device] = port + } + + return m +} + +func getHardwarePortInfo(hardwarePort string) (hardwarePortInfo, error) { + h := hardwarePortInfo{} + + _, out, err := util.RunCommand("networksetup", "-getinfo", hardwarePort) + if err != nil { + return h, err + } + + re := regexp.MustCompile("IP address: (.*?)\nSubnet mask: (.*?)\nRouter: (.*?)\n") + + match := re.FindStringSubmatch(out) + if len(match) == 0 { + return h, errors.New("could not find hardware port info") + } + + h.name = hardwarePort + h.ip = match[1] + h.subnet = match[2] + h.gatewayIP = match[3] + + if strings.Index(out, "Manual Configuration") == 0 { + h.static = true + } + + return h, nil +} + +func ifaceSetStaticIP(ifaceName string) (err error) { + portInfo, err := getCurrentHardwarePortInfo(ifaceName) + if err != nil { + return err + } + + if portInfo.static { + return errors.New("IP address is already static") + } + + dnsAddrs, err := getEtcResolvConfServers() + if err != nil { + return err + } + + args := make([]string, 0) + args = append(args, "-setdnsservers", portInfo.name) + args = append(args, dnsAddrs...) + + // Setting DNS servers is necessary when configuring a static IP + code, _, err := util.RunCommand("networksetup", args...) + if err != nil { + return err + } + if code != 0 { + return fmt.Errorf("failed to set DNS servers, code=%d", code) + } + + // Actually configures hardware port to have static IP + code, _, err = util.RunCommand("networksetup", "-setmanual", + portInfo.name, portInfo.ip, portInfo.subnet, portInfo.gatewayIP) + if err != nil { + return err + } + if code != 0 { + return fmt.Errorf("failed to set DNS servers, code=%d", code) + } + + return nil +} + +// getEtcResolvConfServers returns a list of nameservers configured in +// /etc/resolv.conf. +func getEtcResolvConfServers() ([]string, error) { + body, err := ioutil.ReadFile("/etc/resolv.conf") + if err != nil { + return nil, err + } + + re := regexp.MustCompile("nameserver ([a-zA-Z0-9.:]+)") + + matches := re.FindAllStringSubmatch(string(body), -1) + if len(matches) == 0 { + return nil, errors.New("found no DNS servers in /etc/resolv.conf") + } + + addrs := make([]string, 0) + for i := range matches { + addrs = append(addrs, matches[i][1]) + } + + return addrs, nil +} diff --git a/internal/sysutil/net_linux.go b/internal/sysutil/net_linux.go new file mode 100644 index 00000000..5206f9fd --- /dev/null +++ b/internal/sysutil/net_linux.go @@ -0,0 +1,168 @@ +// +build linux + +package sysutil + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "strings" + + "github.com/AdguardTeam/AdGuardHome/internal/aghio" + "github.com/AdguardTeam/AdGuardHome/internal/util" + "github.com/AdguardTeam/golibs/file" +) + +// maxConfigFileSize is the maximum length of interfaces configuration file. +const maxConfigFileSize = 1024 * 1024 + +func ifaceHasStaticIP(ifaceName string) (has bool, err error) { + var f *os.File + for _, check := range []struct { + checker func(io.Reader, string) (bool, error) + filePath string + }{{ + checker: dhcpcdStaticConfig, + filePath: "/etc/dhcpcd.conf", + }, { + checker: ifacesStaticConfig, + filePath: "/etc/network/interfaces", + }} { + f, err = os.Open(check.filePath) + if errors.Is(err, os.ErrNotExist) { + continue + } + if err != nil { + return false, err + } + defer f.Close() + + fileReadCloser, err := aghio.LimitReadCloser(f, maxConfigFileSize) + if err != nil { + return false, err + } + defer fileReadCloser.Close() + + has, err = check.checker(fileReadCloser, ifaceName) + if has || err != nil { + break + } + } + + return has, err +} + +// dhcpcdStaticConfig checks if interface is configured by /etc/dhcpcd.conf to +// have a static IP. +func dhcpcdStaticConfig(r io.Reader, ifaceName string) (has bool, err error) { + s := bufio.NewScanner(r) + var withinInterfaceCtx bool + + for s.Scan() { + line := strings.TrimSpace(s.Text()) + + if withinInterfaceCtx && len(line) == 0 { + // An empty line resets our state. + withinInterfaceCtx = false + } + + if len(line) == 0 || line[0] == '#' { + continue + } + + fields := strings.Fields(line) + + if withinInterfaceCtx { + if len(fields) >= 2 && fields[0] == "static" && strings.HasPrefix(fields[1], "ip_address=") { + return true, nil + } + if len(fields) > 0 && fields[0] == "interface" { + // Another interface found. + withinInterfaceCtx = false + } + continue + } + + if len(fields) == 2 && fields[0] == "interface" && fields[1] == ifaceName { + // The interface found. + withinInterfaceCtx = true + } + } + + return false, s.Err() +} + +// ifacesStaticConfig checks if interface is configured by +// /etc/network/interfaces to have a static IP. +func ifacesStaticConfig(r io.Reader, ifaceName string) (has bool, err error) { + s := bufio.NewScanner(r) + for s.Scan() { + line := strings.TrimSpace(s.Text()) + + if len(line) == 0 || line[0] == '#' { + continue + } + + fields := strings.Fields(line) + // Man page interfaces(5) declares that interface definition + // should consist of the key word "iface" followed by interface + // name, and method at fourth field. + if len(fields) >= 4 && fields[0] == "iface" && fields[1] == ifaceName && fields[3] == "static" { + return true, nil + } + } + return false, s.Err() +} + +func ifaceSetStaticIP(ifaceName string) (err error) { + ip := util.GetSubnet(ifaceName) + if len(ip) == 0 { + return errors.New("can't get IP address") + } + + ip4, _, err := net.ParseCIDR(ip) + if err != nil { + return err + } + gatewayIP := GatewayIP(ifaceName) + add := updateStaticIPdhcpcdConf(ifaceName, ip, gatewayIP, ip4.String()) + + body, err := ioutil.ReadFile("/etc/dhcpcd.conf") + if err != nil { + return err + } + + body = append(body, []byte(add)...) + err = file.SafeWrite("/etc/dhcpcd.conf", body) + if err != nil { + return err + } + + return nil +} + +// updateStaticIPdhcpcdConf sets static IP address for the interface by writing +// into dhcpd.conf. +func updateStaticIPdhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string { + var body []byte + + add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n", + ifaceName, ip) + body = append(body, []byte(add)...) + + if len(gatewayIP) != 0 { + add = fmt.Sprintf("static routers=%s\n", + gatewayIP) + body = append(body, []byte(add)...) + } + + add = fmt.Sprintf("static domain_name_servers=%s\n\n", + dnsIP) + body = append(body, []byte(add)...) + + return string(body) +} diff --git a/internal/sysutil/net_linux_test.go b/internal/sysutil/net_linux_test.go new file mode 100644 index 00000000..8cadbbb7 --- /dev/null +++ b/internal/sysutil/net_linux_test.go @@ -0,0 +1,109 @@ +// +build linux + +package sysutil + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +const nl = "\n" + +func TestDHCPCDStaticConfig(t *testing.T) { + testCases := []struct { + name string + data []byte + want bool + }{{ + name: "has_not", + data: []byte(`#comment` + nl + + `# comment` + nl + + `interface eth0` + nl + + `static ip_address=192.168.0.1/24` + nl + + `# interface wlan0` + nl + + `static ip_address=192.168.1.1/24` + nl + + `# comment` + nl, + ), + want: false, + }, { + name: "has", + data: []byte(`#comment` + nl + + `# comment` + nl + + `interface eth0` + nl + + `static ip_address=192.168.0.1/24` + nl + + `# interface wlan0` + nl + + `static ip_address=192.168.1.1/24` + nl + + `# comment` + nl + + `interface wlan0` + nl + + `# comment` + nl + + `static ip_address=192.168.2.1/24` + nl, + ), + want: true, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := bytes.NewReader(tc.data) + has, err := dhcpcdStaticConfig(r, "wlan0") + assert.Nil(t, err) + assert.Equal(t, tc.want, has) + }) + } +} + +func TestIfacesStaticConfig(t *testing.T) { + testCases := []struct { + name string + data []byte + want bool + }{{ + name: "has_not", + data: []byte(`allow-hotplug enp0s3` + nl + + `#iface enp0s3 inet static` + nl + + `# address 192.168.0.200` + nl + + `# netmask 255.255.255.0` + nl + + `# gateway 192.168.0.1` + nl + + `iface enp0s3 inet dhcp` + nl, + ), + want: false, + }, { + name: "has", + data: []byte(`allow-hotplug enp0s3` + nl + + `iface enp0s3 inet static` + nl + + ` address 192.168.0.200` + nl + + ` netmask 255.255.255.0` + nl + + ` gateway 192.168.0.1` + nl + + `#iface enp0s3 inet dhcp` + nl, + ), + want: true, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := bytes.NewReader(tc.data) + has, err := ifacesStaticConfig(r, "enp0s3") + assert.Nil(t, err) + assert.Equal(t, tc.want, has) + }) + } +} + +func TestSetStaticIPdhcpcdConf(t *testing.T) { + dhcpcdConf := nl + `interface wlan0` + nl + + `static ip_address=192.168.0.2/24` + nl + + `static routers=192.168.0.1` + nl + + `static domain_name_servers=192.168.0.2` + nl + nl + + s := updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", "192.168.0.1", "192.168.0.2") + assert.Equal(t, dhcpcdConf, s) + + // without gateway + dhcpcdConf = nl + `interface wlan0` + nl + + `static ip_address=192.168.0.2/24` + nl + + `static domain_name_servers=192.168.0.2` + nl + nl + + s = updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", "", "192.168.0.2") + assert.Equal(t, dhcpcdConf, s) +} diff --git a/internal/sysutil/net_others.go b/internal/sysutil/net_others.go new file mode 100644 index 00000000..fd28c1a1 --- /dev/null +++ b/internal/sysutil/net_others.go @@ -0,0 +1,16 @@ +// +build !linux,!darwin + +package sysutil + +import ( + "fmt" + "runtime" +) + +func ifaceHasStaticIP(string) (bool, error) { + return false, fmt.Errorf("cannot check if IP is static: not supported on %s", runtime.GOOS) +} + +func ifaceSetStaticIP(string) error { + return fmt.Errorf("cannot set static IP on %s", runtime.GOOS) +} diff --git a/internal/sysutil/os.go b/internal/sysutil/os.go new file mode 100644 index 00000000..63d2bf7e --- /dev/null +++ b/internal/sysutil/os.go @@ -0,0 +1,26 @@ +// Package sysutil contains utilities for functions requiring system calls. +package sysutil + +import "syscall" + +// CanBindPrivilegedPorts checks if current process can bind to privileged +// ports. +func CanBindPrivilegedPorts() (can bool, err error) { + return canBindPrivilegedPorts() +} + +// SetRlimit sets user-specified limit of how many fd's we can use +// https://github.com/AdguardTeam/AdGuardHome/internal/issues/659. +func SetRlimit(val uint) { + setRlimit(val) +} + +// HaveAdminRights checks if the current user has root (administrator) rights. +func HaveAdminRights() (bool, error) { + return haveAdminRights() +} + +// SendProcessSignal sends signal to a process. +func SendProcessSignal(pid int, sig syscall.Signal) error { + return sendProcessSignal(pid, sig) +} diff --git a/internal/sysutil/os_freebsd.go b/internal/sysutil/os_freebsd.go new file mode 100644 index 00000000..12321aa7 --- /dev/null +++ b/internal/sysutil/os_freebsd.go @@ -0,0 +1,32 @@ +// +build freebsd + +package sysutil + +import ( + "os" + "syscall" + + "github.com/AdguardTeam/golibs/log" +) + +func canBindPrivilegedPorts() (can bool, err error) { + return HaveAdminRights() +} + +func setRlimit(val uint) { + var rlim syscall.Rlimit + rlim.Max = int64(val) + rlim.Cur = int64(val) + err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim) + if err != nil { + log.Error("Setrlimit() failed: %v", err) + } +} + +func haveAdminRights() (bool, error) { + return os.Getuid() == 0, nil +} + +func sendProcessSignal(pid int, sig syscall.Signal) error { + return syscall.Kill(pid, sig) +} diff --git a/internal/sysutil/os_linux.go b/internal/sysutil/os_linux.go new file mode 100644 index 00000000..2bab01d4 --- /dev/null +++ b/internal/sysutil/os_linux.go @@ -0,0 +1,39 @@ +// +build linux + +package sysutil + +import ( + "os" + "syscall" + + "github.com/AdguardTeam/golibs/log" + "golang.org/x/sys/unix" +) + +func canBindPrivilegedPorts() (can bool, err error) { + cnbs, err := unix.PrctlRetInt(unix.PR_CAP_AMBIENT, unix.PR_CAP_AMBIENT_IS_SET, unix.CAP_NET_BIND_SERVICE, 0, 0) + // Don't check the error because it's always nil on Linux. + adm, _ := haveAdminRights() + + return cnbs == 1 || adm, err +} + +func setRlimit(val uint) { + var rlim syscall.Rlimit + rlim.Max = uint64(val) + rlim.Cur = uint64(val) + err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim) + if err != nil { + log.Error("Setrlimit() failed: %v", err) + } +} + +func haveAdminRights() (bool, error) { + // The error is nil because the platform-independent function signature + // requires returning an error. + return os.Getuid() == 0, nil +} + +func sendProcessSignal(pid int, sig syscall.Signal) error { + return syscall.Kill(pid, sig) +} diff --git a/internal/sysutil/os_unix.go b/internal/sysutil/os_unix.go new file mode 100644 index 00000000..a77e9da1 --- /dev/null +++ b/internal/sysutil/os_unix.go @@ -0,0 +1,32 @@ +// +build aix darwin dragonfly netbsd openbsd solaris + +package sysutil + +import ( + "os" + "syscall" + + "github.com/AdguardTeam/golibs/log" +) + +func canBindPrivilegedPorts() (can bool, err error) { + return HaveAdminRights() +} + +func setRlimit(val uint) { + var rlim syscall.Rlimit + rlim.Max = uint64(val) + rlim.Cur = uint64(val) + err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim) + if err != nil { + log.Error("Setrlimit() failed: %v", err) + } +} + +func haveAdminRights() (bool, error) { + return os.Getuid() == 0, nil +} + +func sendProcessSignal(pid int, sig syscall.Signal) error { + return syscall.Kill(pid, sig) +} diff --git a/internal/util/os_windows.go b/internal/sysutil/os_windows.go similarity index 69% rename from internal/util/os_windows.go rename to internal/sysutil/os_windows.go index 2a3742fa..3d6745c7 100644 --- a/internal/util/os_windows.go +++ b/internal/sysutil/os_windows.go @@ -1,4 +1,6 @@ -package util +// +build windows + +package sysutil import ( "fmt" @@ -7,11 +9,14 @@ import ( "golang.org/x/sys/windows" ) -// Set user-specified limit of how many fd's we can use -func SetRlimit(val uint) { +func canBindPrivilegedPorts() (can bool, err error) { + return HaveAdminRights() } -func HaveAdminRights() (bool, error) { +func setRlimit(val uint) { +} + +func haveAdminRights() (bool, error) { var token windows.Token h := windows.CurrentProcess() err := windows.OpenProcessToken(h, windows.TOKEN_QUERY, &token) @@ -32,6 +37,6 @@ func HaveAdminRights() (bool, error) { return true, nil } -func SendProcessSignal(pid int, sig syscall.Signal) error { +func sendProcessSignal(pid int, sig syscall.Signal) error { return fmt.Errorf("not supported on Windows") } diff --git a/internal/sysutil/syslog.go b/internal/sysutil/syslog.go new file mode 100644 index 00000000..7bd505fc --- /dev/null +++ b/internal/sysutil/syslog.go @@ -0,0 +1,6 @@ +package sysutil + +// ConfigureSyslog reroutes standard logger output to syslog. +func ConfigureSyslog(serviceName string) error { + return configureSyslog(serviceName) +} diff --git a/internal/util/syslog_others.go b/internal/sysutil/syslog_others.go similarity index 60% rename from internal/util/syslog_others.go rename to internal/sysutil/syslog_others.go index f4ad9119..0e0e1c3f 100644 --- a/internal/util/syslog_others.go +++ b/internal/sysutil/syslog_others.go @@ -1,14 +1,14 @@ // +build !windows,!nacl,!plan9 -package util +package sysutil import ( - "log" "log/syslog" + + "github.com/AdguardTeam/golibs/log" ) -// ConfigureSyslog reroutes standard logger output to syslog -func ConfigureSyslog(serviceName string) error { +func configureSyslog(serviceName string) error { w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName) if err != nil { return err diff --git a/internal/util/syslog_windows.go b/internal/sysutil/syslog_windows.go similarity index 83% rename from internal/util/syslog_windows.go rename to internal/sysutil/syslog_windows.go index 30ee7815..2160ea43 100644 --- a/internal/util/syslog_windows.go +++ b/internal/sysutil/syslog_windows.go @@ -1,9 +1,11 @@ -package util +// +build windows nacl plan9 + +package sysutil import ( - "log" "strings" + "github.com/AdguardTeam/golibs/log" "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc/eventlog" ) @@ -12,12 +14,12 @@ type eventLogWriter struct { el *eventlog.Log } -// Write sends a log message to the Event Log. +// Write implements io.Writer interface for eventLogWriter. func (w *eventLogWriter) Write(b []byte) (int, error) { return len(b), w.el.Info(1, string(b)) } -func ConfigureSyslog(serviceName string) error { +func configureSyslog(serviceName string) error { // Note that the eventlog src is the same as the service name // Otherwise, we will get "the description for event id cannot be found" warning in every log record diff --git a/internal/sysutil/sysutil_test.go b/internal/sysutil/sysutil_test.go new file mode 100644 index 00000000..0cddbf42 --- /dev/null +++ b/internal/sysutil/sysutil_test.go @@ -0,0 +1,11 @@ +package sysutil + +import ( + "testing" + + "github.com/AdguardTeam/AdGuardHome/internal/testutil" +) + +func TestMain(m *testing.M) { + testutil.DiscardLogOutput(m) +} diff --git a/internal/tools/go.mod b/internal/tools/go.mod new file mode 100644 index 00000000..0707d47f --- /dev/null +++ b/internal/tools/go.mod @@ -0,0 +1,24 @@ +module github.com/AdguardTeam/AdGuardHome/internal/tools + +go 1.15 + +require ( + dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363 // indirect + github.com/client9/misspell v0.3.4 // indirect + github.com/fzipp/gocyclo v0.3.1 + github.com/golangci/misspell v0.3.5 + github.com/google/go-cmp v0.5.4 // indirect + github.com/gookit/color v1.3.3 // indirect + github.com/gordonklaus/ineffassign v0.0.0-20201107091007-3b93a8888063 + github.com/kisielk/errcheck v1.4.0 + github.com/kyoh86/looppointer v0.1.7 + github.com/kyoh86/nolint v0.0.1 // indirect + github.com/securego/gosec/v2 v2.5.0 + golang.org/x/lint v0.0.0-20200302205851-738671d3881b + golang.org/x/mod v0.4.0 // indirect + golang.org/x/tools v0.0.0-20201208062317-e652b2f42cc7 + gopkg.in/yaml.v2 v2.4.0 // indirect + honnef.co/go/tools v0.1.0 + mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475 + mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 +) diff --git a/internal/tools/go.sum b/internal/tools/go.sum new file mode 100644 index 00000000..a8b20e9e --- /dev/null +++ b/internal/tools/go.sum @@ -0,0 +1,167 @@ +dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363 h1:o4lAkfETerCnr1kF9/qwkwjICnU+YLHNDCM8h2xj7as= +dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363/go.mod h1:WG7q7swWsS2f9PYpt5DoEP/EBYWx8We5UoRltn9vJl8= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fzipp/gocyclo v0.3.1 h1:A9UeX3HJSXTBzvHzhqoYVuE0eAhe+aM8XBCCwsPMZOc= +github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golangci/misspell v0.3.5 h1:pLzmVdl3VxTOncgzHcvLOKirdvcx/TydsClUQXTehjo= +github.com/golangci/misspell v0.3.5/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/gookit/color v1.3.1 h1:PPD/C7sf8u2L8XQPdPgsWRoAiLQGZEZOzU3cf5IYYUk= +github.com/gookit/color v1.3.1/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ= +github.com/gookit/color v1.3.3 h1:6IAwxWCdABGNc4gI+YFBMYw0Hz8g5+lpYeKRAaALQoQ= +github.com/gookit/color v1.3.3/go.mod h1:GqqLKF1le3EfrbHbYsYa5WdLqfc/PHMdMRbt6tMnqIc= +github.com/gordonklaus/ineffassign v0.0.0-20201107091007-3b93a8888063 h1:dKprcOvlsvqfWn/iGvz+oYuC2axESeSMuF8dDrWMNsE= +github.com/gordonklaus/ineffassign v0.0.0-20201107091007-3b93a8888063/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kisielk/errcheck v1.4.0 h1:ueN6QYA+c7eDQo7ebpNdYR8mUJZThiGz9PEoJEMGPzA= +github.com/kisielk/errcheck v1.4.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kyoh86/looppointer v0.1.7 h1:q5sZOhFvmvQ6ZoZxvPB/Mjj2croWX7L49BBuI4XQWCM= +github.com/kyoh86/looppointer v0.1.7/go.mod h1:l0cRF49N6xDPx8IuBGC/imZo8Yn1BBLJY0vzI+4fepc= +github.com/kyoh86/nolint v0.0.0-20200711045849-7a7b0d649b7a h1:WKwgzTn8xp2JuhOUsTbi+h+QygLZSfVGwaYZUejGMMw= +github.com/kyoh86/nolint v0.0.0-20200711045849-7a7b0d649b7a/go.mod h1:hPeUNhNOZ22wXzQKMzeYKXVFTBjJ9czxjeIDyI1ueVM= +github.com/kyoh86/nolint v0.0.1 h1:GjNxDEkVn2wAxKHtP7iNTrRxytRZ1wXxLV5j4XzGfRU= +github.com/kyoh86/nolint v0.0.1/go.mod h1:1ZiZZ7qqrZ9dZegU96phwVcdQOMKIqRzFJL3ewq9gtI= +github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= +github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= +github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0= +github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/securego/gosec/v2 v2.5.0 h1:kjfXLeKdk98gBe2+eYRFMpC4+mxmQQtbidpiiOQ69Qc= +github.com/securego/gosec/v2 v2.5.0/go.mod h1:L/CDXVntIff5ypVHIkqPXbtRpJiNCh6c6Amn68jXDjo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200710042808-f1c4188a97a1/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201121010211-780cb80bd7fb/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208062317-e652b2f42cc7 h1:2OSu5vYyX4LVqZAtqZXnFEcN26SDKIJYlEVIRl1tj8U= +golang.org/x/tools v0.0.0-20201208062317-e652b2f42cc7/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +honnef.co/go/tools v0.0.1-2020.1.6 h1:W18jzjh8mfPez+AwGLxmOImucz/IFjpNlrKVnaj2YVc= +honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzEYLhQB2YY= +honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c= +honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM= +mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475 h1:5ZmJGYyuTlhdlIpRxSFhdJqkXQweXETFCEaLhRAX3e8= +mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475/go.mod h1:E4LOcu9JQEtnYXtB1Y51drqh2Qr2Ngk9J3YrRCwcbd0= +mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 h1:kAREL6MPwpsk1/PQPFD3Eg7WAQR5mPTWZJaBiG5LDbY= +mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc= diff --git a/internal/tools/tools.go b/internal/tools/tools.go new file mode 100644 index 00000000..b2f83783 --- /dev/null +++ b/internal/tools/tools.go @@ -0,0 +1,22 @@ +// +build tools + +// Package tools and its main module are a nested internal module containing our +// development tool dependencies. +// +// See https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module. +package tools + +import ( + _ "github.com/fzipp/gocyclo/cmd/gocyclo" + _ "github.com/golangci/misspell/cmd/misspell" + _ "github.com/gordonklaus/ineffassign" + _ "github.com/kisielk/errcheck" + _ "github.com/kyoh86/looppointer" + _ "github.com/securego/gosec/v2/cmd/gosec" + _ "golang.org/x/lint/golint" + _ "golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness" + _ "golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow" + _ "honnef.co/go/tools/cmd/staticcheck" + _ "mvdan.cc/gofumpt/gofumports" + _ "mvdan.cc/unparam" +) diff --git a/internal/update/check.go b/internal/update/check.go index b10cec73..e83ab5c2 100644 --- a/internal/update/check.go +++ b/internal/update/check.go @@ -6,6 +6,8 @@ import ( "io/ioutil" "strings" "time" + + "github.com/AdguardTeam/AdGuardHome/internal/aghio" ) const versionCheckPeriod = 8 * 60 * 60 @@ -19,6 +21,9 @@ type VersionInfo struct { CanAutoUpdate bool // If true - we can auto-update } +// MaxResponseSize is responses on server's requests maximum length in bytes. +const MaxResponseSize = 64 * 1024 + // GetVersionResponse - downloads version.json (if needed) and deserializes it func (u *Updater) GetVersionResponse(forceRecheck bool) (VersionInfo, error) { if !forceRecheck && @@ -27,14 +32,19 @@ func (u *Updater) GetVersionResponse(forceRecheck bool) (VersionInfo, error) { } resp, err := u.Client.Get(u.VersionURL) - if resp != nil && resp.Body != nil { - defer resp.Body.Close() - } - if err != nil { return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", u.VersionURL, err) } + defer resp.Body.Close() + resp.Body, err = aghio.LimitReadCloser(resp.Body, MaxResponseSize) + if err != nil { + return VersionInfo{}, fmt.Errorf("updater: LimitReadCloser: %w", err) + } + defer resp.Body.Close() + + // This use of ReadAll is safe, because we just limited the appropriate + // ReadCloser. body, err := ioutil.ReadAll(resp.Body) if err != nil { return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", u.VersionURL, err) diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 5616104a..ceca2b9d 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -88,16 +88,16 @@ func TestUpdateGetVersion(t *testing.T) { } func TestUpdate(t *testing.T) { - _ = os.Mkdir("aghtest", 0755) + _ = os.Mkdir("aghtest", 0o755) defer func() { _ = os.RemoveAll("aghtest") }() // create "current" files - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome", []byte("AdGuardHome"), 0755)) - assert.Nil(t, ioutil.WriteFile("aghtest/README.md", []byte("README.md"), 0644)) - assert.Nil(t, ioutil.WriteFile("aghtest/LICENSE.txt", []byte("LICENSE.txt"), 0644)) - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.yaml", []byte("AdGuardHome.yaml"), 0644)) + assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome", []byte("AdGuardHome"), 0o755)) + assert.Nil(t, ioutil.WriteFile("aghtest/README.md", []byte("README.md"), 0o644)) + assert.Nil(t, ioutil.WriteFile("aghtest/LICENSE.txt", []byte("LICENSE.txt"), 0o644)) + assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.yaml", []byte("AdGuardHome.yaml"), 0o644)) // start server for returning package file pkgData, err := ioutil.ReadFile("test/AdGuardHome.tar.gz") @@ -151,16 +151,16 @@ func TestUpdate(t *testing.T) { } func TestUpdateWindows(t *testing.T) { - _ = os.Mkdir("aghtest", 0755) + _ = os.Mkdir("aghtest", 0o755) defer func() { _ = os.RemoveAll("aghtest") }() // create "current" files - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.exe", []byte("AdGuardHome.exe"), 0755)) - assert.Nil(t, ioutil.WriteFile("aghtest/README.md", []byte("README.md"), 0644)) - assert.Nil(t, ioutil.WriteFile("aghtest/LICENSE.txt", []byte("LICENSE.txt"), 0644)) - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.yaml", []byte("AdGuardHome.yaml"), 0644)) + assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.exe", []byte("AdGuardHome.exe"), 0o755)) + assert.Nil(t, ioutil.WriteFile("aghtest/README.md", []byte("README.md"), 0o644)) + assert.Nil(t, ioutil.WriteFile("aghtest/LICENSE.txt", []byte("LICENSE.txt"), 0o644)) + assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.yaml", []byte("AdGuardHome.yaml"), 0o644)) // start server for returning package file pkgData, err := ioutil.ReadFile("test/AdGuardHome.zip") diff --git a/internal/update/updater.go b/internal/update/updater.go index f78f85c5..b6901889 100644 --- a/internal/update/updater.go +++ b/internal/update/updater.go @@ -1,3 +1,4 @@ +// Package update provides an updater for AdGuardHome. package update import ( @@ -14,6 +15,7 @@ import ( "strings" "time" + "github.com/AdguardTeam/AdGuardHome/internal/aghio" "github.com/AdguardTeam/AdGuardHome/internal/util" "github.com/AdguardTeam/golibs/log" ) @@ -217,17 +219,27 @@ func (u *Updater) clean() { _ = os.RemoveAll(u.updateDir) } +// MaxPackageFileSize is a maximum package file length in bytes. The largest +// package whose size is limited by this constant currently has the size of +// approximately 9 MiB. +const MaxPackageFileSize = 32 * 1024 * 1024 + // Download package file and save it to disk -func (u *Updater) downloadPackageFile(url string, filename string) error { +func (u *Updater) downloadPackageFile(url, filename string) error { resp, err := u.Client.Get(url) if err != nil { return fmt.Errorf("http request failed: %w", err) } - if resp != nil && resp.Body != nil { - defer resp.Body.Close() + defer resp.Body.Close() + + resp.Body, err = aghio.LimitReadCloser(resp.Body, MaxPackageFileSize) + if err != nil { + return fmt.Errorf("http request failed: %w", err) } + defer resp.Body.Close() log.Debug("updater: reading HTTP body") + // This use of ReadAll is now safe, because we limited body's Reader. body, err := ioutil.ReadAll(resp.Body) if err != nil { return fmt.Errorf("ioutil.ReadAll() failed: %w", err) diff --git a/internal/util/auto_hosts.go b/internal/util/autohosts.go similarity index 100% rename from internal/util/auto_hosts.go rename to internal/util/autohosts.go diff --git a/internal/util/auto_hosts_test.go b/internal/util/autohosts_test.go similarity index 100% rename from internal/util/auto_hosts_test.go rename to internal/util/autohosts_test.go diff --git a/internal/util/helpers.go b/internal/util/helpers.go index e023da08..2770fa44 100644 --- a/internal/util/helpers.go +++ b/internal/util/helpers.go @@ -9,7 +9,6 @@ import ( "io/ioutil" "os" "os/exec" - "path" "runtime" "strings" ) @@ -41,13 +40,6 @@ func RunCommand(command string, arguments ...string) (int, string, error) { return cmd.ProcessState.ExitCode(), string(out), nil } -func FuncName() string { - pc := make([]uintptr, 10) // at least 1 entry needed - runtime.Callers(2, pc) - f := runtime.FuncForPC(pc[0]) - return path.Base(f.Name()) -} - // SplitNext - split string by a byte and return the first chunk // Skip empty chunks // Whitespace is trimmed diff --git a/internal/util/network_utils.go b/internal/util/network.go similarity index 98% rename from internal/util/network_utils.go rename to internal/util/network.go index 3aafecff..1731ed08 100644 --- a/internal/util/network_utils.go +++ b/internal/util/network.go @@ -33,10 +33,7 @@ func GetValidNetInterfaces() ([]net.Interface, error) { netIfaces := []net.Interface{} - for i := range ifaces { - iface := ifaces[i] - netIfaces = append(netIfaces, iface) - } + netIfaces = append(netIfaces, ifaces...) return netIfaces, nil } diff --git a/internal/util/network_utils_test.go b/internal/util/network_test.go similarity index 92% rename from internal/util/network_utils_test.go rename to internal/util/network_test.go index 7feac0f2..9b2a9554 100644 --- a/internal/util/network_utils_test.go +++ b/internal/util/network_test.go @@ -1,7 +1,6 @@ package util import ( - "log" "testing" ) @@ -19,6 +18,6 @@ func TestGetValidNetInterfacesForWeb(t *testing.T) { t.Fatalf("No addresses found for %s", iface.Name) } - log.Printf("%v", iface) + t.Logf("%v", iface) } } diff --git a/internal/util/os_freebsd.go b/internal/util/os_freebsd.go deleted file mode 100644 index 042e83fe..00000000 --- a/internal/util/os_freebsd.go +++ /dev/null @@ -1,32 +0,0 @@ -// +build freebsd - -package util - -import ( - "os" - "syscall" - - "github.com/AdguardTeam/golibs/log" -) - -// Set user-specified limit of how many fd's we can use -// https://github.com/AdguardTeam/AdGuardHome/internal/issues/659 -func SetRlimit(val uint) { - var rlim syscall.Rlimit - rlim.Max = int64(val) - rlim.Cur = int64(val) - err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim) - if err != nil { - log.Error("Setrlimit() failed: %v", err) - } -} - -// Check if the current user has root (administrator) rights -func HaveAdminRights() (bool, error) { - return os.Getuid() == 0, nil -} - -// SendProcessSignal - send signal to a process -func SendProcessSignal(pid int, sig syscall.Signal) error { - return syscall.Kill(pid, sig) -} diff --git a/internal/util/os_unix.go b/internal/util/os_unix.go deleted file mode 100644 index 53557566..00000000 --- a/internal/util/os_unix.go +++ /dev/null @@ -1,32 +0,0 @@ -// +build aix darwin dragonfly linux netbsd openbsd solaris - -package util - -import ( - "os" - "syscall" - - "github.com/AdguardTeam/golibs/log" -) - -// Set user-specified limit of how many fd's we can use -// https://github.com/AdguardTeam/AdGuardHome/internal/issues/659 -func SetRlimit(val uint) { - var rlim syscall.Rlimit - rlim.Max = uint64(val) - rlim.Cur = uint64(val) - err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim) - if err != nil { - log.Error("Setrlimit() failed: %v", err) - } -} - -// Check if the current user has root (administrator) rights -func HaveAdminRights() (bool, error) { - return os.Getuid() == 0, nil -} - -// SendProcessSignal - send signal to a process -func SendProcessSignal(pid int, sig syscall.Signal) error { - return syscall.Kill(pid, sig) -} diff --git a/main.go b/main.go index a040da7d..ecbb3d3b 100644 --- a/main.go +++ b/main.go @@ -7,15 +7,20 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/home" ) -// version will be set through ldflags, contains current version +// version is the release version. It is set by the linker. var version = "undefined" -// channel can be set via ldflags +// channel is the release channel. It is set by the linker. var channel = "release" -// GOARM value - set via ldflags +// goarm is the GOARM value. It is set by the linker. var goarm = "" +// gomips is the GOMIPS value. It is set by the linker. +// +// TODO(a.garipov): Implement. +var gomips = "" + func main() { - home.Main(version, channel, goarm) + home.Main(version, channel, goarm, gomips) } diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index c4bbed51..076d9896 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -1,5 +1,67 @@ # AdGuard Home API Change Log + + +## v0.105: API changes + +### New `"reason"` in `GET /filtering/check_host` and `GET /querylog` + +* The new `DNSRewriteRule` reason is added to `GET /filtering/check_host` and + `GET /querylog`. + +* Also, the reason which was incorrectly documented as `"ReasonRewrite"` is now + correctly documented as `"Rewrite"`, and the previously undocumented + `"RewriteEtcHosts"` is now documented as well. + +### Multiple matched rules in `GET /filtering/check_host` and `GET /querylog` + +* The properties `rule` and `filter_id` are now deprecated. API users should + inspect the newly-added `rules` object array instead. For most rules, it's + either empty or contains one object, which contains the same things as the old + two properties did, but under more correct names: + + ```js + { + // … + + // Deprecated. + "rule": "||example.com^", + // Deprecated. + "filter_id": 42, + // Newly-added. + "rules": [{ + "text": "||example.com^", + "filter_list_id": 42 + }] + } + ``` + + For `$dnsrewrite` rules, they contain all rules that contributed to the + result. For example, if you have the following filtering rules: + + ``` + ||example.com^$dnsrewrite=127.0.0.1 + ||example.com^$dnsrewrite=127.0.0.2 + ``` + + The `"rules"` will be something like: + + ```js + { + // … + + "rules": [{ + "text": "||example.com^$dnsrewrite=127.0.0.1", + "filter_list_id": 0 + }, { + "text": "||example.com^$dnsrewrite=127.0.0.2", + "filter_list_id": 0 + }] + } + ``` + + The old fields will be removed in v0.106.0. + ## v0.103: API changes ### API: replace settings in GET /control/dns_info & POST /control/dns_config diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index d42ee24c..61db836e 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1,2021 +1,2140 @@ -openapi: 3.0.3 -info: - title: AdGuard Home - description: AdGuard Home REST API. Admin web interface is built on top of this REST API. - version: "0.104" - contact: - name: "AdGuard Home" - url: "https://github.com/AdguardTeam/AdGuardHome" +'openapi': '3.0.3' +'info': + 'title': 'AdGuard Home' + 'description': > + AdGuard Home REST-ish API. Our admin web interface is built on top of this + REST-ish API. + 'version': '0.105' + 'contact': + 'name': 'AdGuard Home' + 'url': 'https://github.com/AdguardTeam/AdGuardHome' -servers: - - url: /control +'servers': +- 'url': '/control' -tags: - - name: clients - description: Clients list operations - - name: dhcp - description: Built-in DHCP server controls - - name: filtering - description: Rule-based filtering - - name: global - description: AdGuard Home server general settings and controls - - name: i18n - description: Application localization - - name: install - description: First-time install configuration handlers - - name: log - description: AdGuard Home query log - - name: parental - description: Blocking adult and explicit materials - - name: safebrowsing - description: Blocking malware/phishing sites - - name: safesearch - description: Enforce family-friendly results in search engines - - name: stats - description: AdGuard Home statistics - - name: tls - description: AdGuard Home HTTPS/DOH/DOT settings - - name: mobileconfig - description: Apple .mobileconfig +'security': +- 'basicAuth': [] -paths: - /status: - get: - tags: - - global - operationId: status - summary: Get DNS server current status and general settings - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/ServerStatus" - /dns_info: - get: - tags: - - global - operationId: dnsInfo - summary: Get general DNS parameters - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/DNSConfig" - /dns_config: - post: - tags: - - global - operationId: dnsConfig - summary: Set general DNS parameters - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/DNSConfig" - responses: - "200": - description: OK - /test_upstream_dns: - post: - tags: - - global - operationId: testUpstreamDNS - summary: Test upstream configuration - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/UpstreamsConfig" - description: Upstream configuration to be tested - responses: - "200": - description: Status of testing each requested server, with "OK" meaning that - server works, any other text means an error. - content: - application/json: - examples: - response: - value: - 1.1.1.1: OK - 1.0.0.1: OK - 8.8.8.8: OK - 8.8.4.4: OK - 192.168.1.104:53535: Couldn't communicate with DNS server - /version.json: - post: - tags: - - global - operationId: getVersionJson - summary: Gets information about the latest available version of AdGuard - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/GetVersionRequest" - required: true - responses: - "200": - description: Version info. If response message is empty, UI does not show a - version update message. - content: - application/json: - schema: - $ref: "#/components/schemas/VersionInfo" - "500": - description: Cannot write answer - "502": - description: Cannot retrieve the version.json file contents - /update: - post: - tags: - - global - operationId: beginUpdate - summary: Begin auto-upgrade procedure - responses: - "200": - description: OK - "500": - description: Failed - /querylog: - get: - tags: - - log - operationId: queryLog - summary: Get DNS server query log. - parameters: - - name: older_than - in: query - description: Filter by older than - schema: - type: string - - name: offset - in: query - description: - Specify the ranking number of the first item on the page. - Even though it is possible to use "offset" and "older_than", - we recommend choosing one of them and sticking to it. - schema: - type: integer - - name: limit - in: query - description: Limit the number of records to be returned - schema: - type: integer - - name: search - in: query - description: Filter by domain name or client IP - schema: - type: string - - name: response_status - in: query - description: Filter by response status - schema: - type: string - enum: - - all - - filtered - - blocked - - blocked_safebrowsing - - blocked_parental - - whitelisted - - rewritten - - safe_search - - processed - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/QueryLog" - /querylog_info: - get: - tags: - - log - operationId: queryLogInfo - summary: Get query log parameters - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/QueryLogConfig" - /querylog_config: - post: - tags: - - log - operationId: queryLogConfig - summary: Set query log parameters - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/QueryLogConfig" - responses: - "200": - description: OK - /querylog_clear: - post: - tags: - - log - operationId: querylogClear - summary: Clear query log - responses: - "200": - description: OK - /stats: - get: - tags: - - stats - operationId: stats - summary: Get DNS server statistics - responses: - "200": - description: Returns statistics data - content: - application/json: - schema: - $ref: "#/components/schemas/Stats" - /stats_reset: - post: - tags: - - stats - operationId: statsReset - summary: Reset all statistics to zeroes - responses: - "200": - description: OK - /stats_info: - get: - tags: - - stats - operationId: statsInfo - summary: Get statistics parameters - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/StatsConfig" - /stats_config: - post: - tags: - - stats - operationId: statsConfig - summary: Set statistics parameters - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/StatsConfig" - responses: - "200": - description: OK - /tls/status: - get: - tags: - - tls - operationId: tlsStatus - summary: Returns TLS configuration and its status - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/TlsConfig" - /tls/configure: - post: - tags: - - tls - operationId: tlsConfigure - summary: Updates current TLS configuration - requestBody: - $ref: "#/components/requestBodies/TlsConfig" - responses: - "200": - description: TLS configuration and its status - content: - application/json: - schema: - $ref: "#/components/schemas/TlsConfig" - "400": - description: Invalid configuration or unavailable port - "500": - description: Error occurred while applying configuration - /tls/validate: - post: - tags: - - tls - operationId: tlsValidate - summary: Checks if the current TLS configuration is valid - requestBody: - $ref: "#/components/requestBodies/TlsConfig" - responses: - "200": - description: TLS configuration and its status - content: - application/json: - schema: - $ref: "#/components/schemas/TlsConfig" - "400": - description: Invalid configuration or unavailable port - /dhcp/status: - get: - tags: - - dhcp - operationId: dhcpStatus - summary: Gets the current DHCP settings and status - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/DhcpStatus" - "501": - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - description: Not implemented (for example, on Windows). - /dhcp/set_config: - post: - tags: - - dhcp - operationId: dhcpSetConfig - summary: Updates the current DHCP server configuration - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/DhcpConfig" - responses: - "200": - description: OK - "501": - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - description: Not implemented (for example, on Windows). - /dhcp/find_active_dhcp: - post: - tags: - - dhcp - operationId: checkActiveDhcp - summary: Searches for an active DHCP server on the network - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/DhcpSearchResult" - "501": - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - description: Not implemented (for example, on Windows). - /dhcp/add_static_lease: - post: - tags: - - dhcp - operationId: dhcpAddStaticLease - summary: Adds a static lease - requestBody: - $ref: "#/components/requestBodies/DhcpStaticLease" - responses: - "200": - description: OK - "501": - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - description: Not implemented (for example, on Windows). - /dhcp/remove_static_lease: - post: - tags: - - dhcp - operationId: dhcpRemoveStaticLease - summary: Removes a static lease - requestBody: - $ref: "#/components/requestBodies/DhcpStaticLease" - responses: - "200": - description: OK - "501": - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - description: Not implemented (for example, on Windows). - /dhcp/reset: - post: - tags: - - dhcp - operationId: dhcpReset - summary: Reset DHCP configuration - responses: - "200": - description: OK - "501": - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - description: Not implemented (for example, on Windows). - /filtering/status: - get: - tags: - - filtering - operationId: filteringStatus - summary: Get filtering parameters - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/FilterStatus" - /filtering/config: - post: - tags: - - filtering - operationId: filteringConfig - summary: Set filtering parameters - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/FilterConfig" - required: true - responses: - "200": - description: OK - /filtering/add_url: - post: - tags: - - filtering - operationId: filteringAddURL - summary: Add filter URL or an absolute file path - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/AddUrlRequest" - required: true - responses: - "200": - description: OK - /filtering/remove_url: - post: - tags: - - filtering - operationId: filteringRemoveURL - summary: Remove filter URL - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/RemoveUrlRequest" - required: true - responses: - "200": - description: OK - /filtering/set_url: - post: - tags: - - filtering - operationId: filteringSetURL - summary: Set URL parameters - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/FilterSetUrl" - responses: - "200": - description: OK - /filtering/refresh: - post: - tags: - - filtering - operationId: filteringRefresh - summary: > - Reload filtering rules from URLs +'tags': +- 'name': 'clients' + 'description': 'Clients list operations' +- 'name': 'dhcp' + 'description': 'Built-in DHCP server controls' +- 'name': 'filtering' + 'description': 'Rule-based filtering' +- 'name': 'global' + 'description': 'AdGuard Home server general settings and controls' +- 'name': 'i18n' + 'description': 'Application localization' +- 'name': 'install' + 'description': 'First-time install configuration handlers' +- 'name': 'log' + 'description': 'AdGuard Home query log' +- 'name': 'mobileconfig' + 'description': 'Apple .mobileconfig' +- 'name': 'parental' + 'description': 'Blocking adult and explicit materials' +- 'name': 'safebrowsing' + 'description': 'Blocking malware/phishing sites' +- 'name': 'safesearch' + 'description': 'Enforce family-friendly results in search engines' +- 'name': 'stats' + 'description': 'AdGuard Home statistics' +- 'name': 'tls' + 'description': 'AdGuard Home HTTPS/DOH/DOT settings' +'paths': + '/status': + 'get': + 'tags': + - 'global' + 'operationId': 'status' + 'summary': 'Get DNS server current status and general settings' + 'responses': + '200': + 'description': 'OK' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ServerStatus' + '/dns_info': + 'get': + 'tags': + - 'global' + 'operationId': 'dnsInfo' + 'summary': 'Get general DNS parameters' + 'responses': + '200': + 'description': 'OK' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DNSConfig' + '/dns_config': + 'post': + 'tags': + - 'global' + 'operationId': 'dnsConfig' + 'summary': 'Set general DNS parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DNSConfig' + 'responses': + '200': + 'description': 'OK' + '/test_upstream_dns': + 'post': + 'tags': + - 'global' + 'operationId': 'testUpstreamDNS' + 'summary': 'Test upstream configuration' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/UpstreamsConfig' + 'description': 'Upstream configuration to be tested' + 'responses': + '200': + 'description': > + Status of testing each requested server, with "OK" meaning that + server works, any other text means an error. + 'content': + 'application/json': + 'examples': + 'response': + 'value': + '1.1.1.1': 'OK' + '1.0.0.1': 'OK' + '8.8.8.8': 'OK' + '8.8.4.4': 'OK' + '192.168.1.104:53535': > + Couldn't communicate with DNS server + '/version.json': + 'post': + 'tags': + - 'global' + 'operationId': 'getVersionJson' + 'summary': > + Gets information about the latest available version of AdGuard + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/GetVersionRequest' + 'required': true + 'responses': + '200': + 'description': > + Version info. If response message is empty, UI does not show + a version update message. + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/VersionInfo' + '500': + 'description': 'Cannot write answer' + '502': + 'description': 'Cannot retrieve the version.json file contents' + '/update': + 'post': + 'tags': + - 'global' + 'operationId': 'beginUpdate' + 'summary': 'Begin auto-upgrade procedure' + 'responses': + '200': + 'description': 'OK.' + '500': + 'description': 'Failed' + '/querylog': + 'get': + 'tags': + - 'log' + 'operationId': 'queryLog' + 'summary': 'Get DNS server query log.' + 'parameters': + - 'name': 'older_than' + 'in': 'query' + 'description': 'Filter by older than' + 'schema': + 'type': 'string' + - 'name': 'offset' + 'in': 'query' + 'description': > + Specify the ranking number of the first item on the page. Even + though it is possible to use "offset" and "older_than", we recommend + choosing one of them and sticking to it. + 'schema': + 'type': 'integer' + - 'name': 'limit' + 'in': 'query' + 'description': 'Limit the number of records to be returned' + 'schema': + 'type': 'integer' + - 'name': 'search' + 'in': 'query' + 'description': 'Filter by domain name or client IP' + 'schema': + 'type': 'string' + - 'name': 'response_status' + 'in': 'query' + 'description': 'Filter by response status' + 'schema': + 'type': 'string' + 'enum': + - 'all' + - 'filtered' + - 'blocked' + - 'blocked_safebrowsing' + - 'blocked_parental' + - 'whitelisted' + - 'rewritten' + - 'safe_search' + - 'processed' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/QueryLog' + '/querylog_info': + 'get': + 'tags': + - 'log' + 'operationId': 'queryLogInfo' + 'summary': 'Get query log parameters' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/QueryLogConfig' + '/querylog_config': + 'post': + 'tags': + - 'log' + 'operationId': 'queryLogConfig' + 'summary': 'Set query log parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/QueryLogConfig' + 'responses': + '200': + 'description': 'OK.' + '/querylog_clear': + 'post': + 'tags': + - 'log' + 'operationId': 'querylogClear' + 'summary': 'Clear query log' + 'responses': + '200': + 'description': 'OK.' + '/stats': + 'get': + 'tags': + - 'stats' + 'operationId': 'stats' + 'summary': 'Get DNS server statistics' + 'responses': + '200': + 'description': 'Returns statistics data' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Stats' + '/stats_reset': + 'post': + 'tags': + - 'stats' + 'operationId': 'statsReset' + 'summary': 'Reset all statistics to zeroes' + 'responses': + '200': + 'description': 'OK.' + '/stats_info': + 'get': + 'tags': + - 'stats' + 'operationId': 'statsInfo' + 'summary': 'Get statistics parameters' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/StatsConfig' + '/stats_config': + 'post': + 'tags': + - 'stats' + 'operationId': 'statsConfig' + 'summary': 'Set statistics parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/StatsConfig' + 'responses': + '200': + 'description': 'OK.' + '/tls/status': + 'get': + 'tags': + - 'tls' + 'operationId': 'tlsStatus' + 'summary': 'Returns TLS configuration and its status' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/TlsConfig' + '/tls/configure': + 'post': + 'tags': + - 'tls' + 'operationId': 'tlsConfigure' + 'summary': 'Updates current TLS configuration' + 'requestBody': + '$ref': '#/components/requestBodies/TlsConfig' + 'responses': + '200': + 'description': 'TLS configuration and its status' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/TlsConfig' + '400': + 'description': 'Invalid configuration or unavailable port' + '500': + 'description': 'Error occurred while applying configuration' + '/tls/validate': + 'post': + 'tags': + - 'tls' + 'operationId': 'tlsValidate' + 'summary': 'Checks if the current TLS configuration is valid' + 'requestBody': + '$ref': '#/components/requestBodies/TlsConfig' + 'responses': + '200': + 'description': 'TLS configuration and its status' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/TlsConfig' + '400': + 'description': 'Invalid configuration or unavailable port' + '/dhcp/status': + 'get': + 'tags': + - 'dhcp' + 'operationId': 'dhcpStatus' + 'summary': 'Gets the current DHCP settings and status' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DhcpStatus' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/set_config': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'dhcpSetConfig' + 'summary': 'Updates the current DHCP server configuration' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DhcpConfig' + 'responses': + '200': + 'description': 'OK.' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/find_active_dhcp': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'checkActiveDhcp' + 'summary': 'Searches for an active DHCP server on the network' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DhcpSearchResult' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/add_static_lease': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'dhcpAddStaticLease' + 'summary': 'Adds a static lease' + 'requestBody': + '$ref': '#/components/requestBodies/DhcpStaticLease' + 'responses': + '200': + 'description': 'OK.' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/remove_static_lease': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'dhcpRemoveStaticLease' + 'summary': 'Removes a static lease' + 'requestBody': + '$ref': '#/components/requestBodies/DhcpStaticLease' + 'responses': + '200': + 'description': 'OK.' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/reset': + 'post': + 'tags': + - 'dhcp' + 'operationId': 'dhcpReset' + 'summary': 'Reset DHCP configuration' + 'responses': + '200': + 'description': 'OK.' + '501': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/filtering/status': + 'get': + 'tags': + - 'filtering' + 'operationId': 'filteringStatus' + 'summary': 'Get filtering parameters' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterStatus' + '/filtering/config': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringConfig' + 'summary': 'Set filtering parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterConfig' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/filtering/add_url': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringAddURL' + 'summary': 'Add filter URL or an absolute file path' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/AddUrlRequest' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/filtering/remove_url': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringRemoveURL' + 'summary': 'Remove filter URL' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/RemoveUrlRequest' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/filtering/set_url': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringSetURL' + 'summary': 'Set URL parameters' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterSetUrl' + 'responses': + '200': + 'description': 'OK.' + '/filtering/refresh': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringRefresh' + 'summary': > + Reload filtering rules from URLs. This might be needed if new URL was + just added and you dont want to wait for automatic refresh to kick in. + This API request is ratelimited, so you can call it freely as often as + you like, it wont create unnecessary burden on servers that host the + URL. This should work as intended, a `force` parameter is offered as + last-resort attempt to make filter lists fresh. If you ever find + yourself using `force` to make something work that otherwise wont, this + is a bug and report it accordingly. + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterRefreshRequest' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterRefreshResponse' + '/filtering/set_rules': + 'post': + 'tags': + - 'filtering' + 'operationId': 'filteringSetRules' + 'summary': 'Set user-defined filter rules' + 'requestBody': + 'content': + 'text/plain': + 'schema': + 'type': 'string' + 'example': '@@||yandex.ru^|' + 'description': 'All filtering rules, one line per rule' + 'responses': + '200': + 'description': 'OK.' + '/filtering/check_host': + 'get': + 'tags': + - 'filtering' + 'operationId': 'filteringCheckHost' + 'summary': 'Check if host name is filtered' + 'parameters': + - 'name': 'name' + 'in': 'query' + 'description': 'Filter by host name' + 'schema': + 'type': 'string' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/FilterCheckHostResponse' + '/safebrowsing/enable': + 'post': + 'tags': + - 'safebrowsing' + 'operationId': 'safebrowsingEnable' + 'summary': 'Enable safebrowsing' + 'responses': + '200': + 'description': 'OK.' + '/safebrowsing/disable': + 'post': + 'tags': + - 'safebrowsing' + 'operationId': 'safebrowsingDisable' + 'summary': 'Disable safebrowsing' + 'responses': + '200': + 'description': 'OK.' + '/safebrowsing/status': + 'get': + 'tags': + - 'safebrowsing' + 'operationId': 'safebrowsingStatus' + 'summary': 'Get safebrowsing status' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'examples': + 'response': + 'value': + 'enabled': false + '/parental/enable': + 'post': + 'tags': + - 'parental' + 'operationId': 'parentalEnable' + 'summary': 'Enable parental filtering' + 'requestBody': + 'content': + 'text/plain': + 'schema': + 'type': 'string' + 'enum': + - 'EARLY_CHILDHOOD' + - 'YOUNG' + - 'TEEN' + - 'MATURE' + 'example': 'sensitivity=TEEN' + 'description': | + Age sensitivity for parental filtering, + EARLY_CHILDHOOD is 3 + YOUNG is 10 + TEEN is 13 + MATURE is 17 + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/parental/disable': + 'post': + 'tags': + - 'parental' + 'operationId': 'parentalDisable' + 'summary': 'Disable parental filtering' + 'responses': + '200': + 'description': 'OK.' + '/parental/status': + 'get': + 'tags': + - 'parental' + 'operationId': 'parentalStatus' + 'summary': 'Get parental filtering status' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'examples': + 'response': + 'value': + 'enabled': true + 'sensitivity': 13 + '/safesearch/enable': + 'post': + 'tags': + - 'safesearch' + 'operationId': 'safesearchEnable' + 'summary': 'Enable safesearch' + 'responses': + '200': + 'description': 'OK.' + '/safesearch/disable': + 'post': + 'tags': + - 'safesearch' + 'operationId': 'safesearchDisable' + 'summary': 'Disable safesearch' + 'responses': + '200': + 'description': 'OK.' + '/safesearch/status': + 'get': + 'tags': + - 'safesearch' + 'operationId': 'safesearchStatus' + 'summary': 'Get safesearch status' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'examples': + 'response': + 'value': + 'enabled': false + '/clients': + 'get': + 'tags': + - 'clients' + 'operationId': 'clientsStatus' + 'summary': 'Get information about configured clients' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Clients' + '/clients/add': + 'post': + 'tags': + - 'clients' + 'operationId': 'clientsAdd' + 'summary': 'Add a new client' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Client' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/clients/delete': + 'post': + 'tags': + - 'clients' + 'operationId': 'clientsDelete' + 'summary': 'Remove a client' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ClientDelete' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/clients/update': + 'post': + 'tags': + - 'clients' + 'operationId': 'clientsUpdate' + 'summary': 'Update client information' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ClientUpdate' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/clients/find': + 'get': + 'tags': + - 'clients' + 'operationId': 'clientsFind' + 'summary': 'Get information about selected clients by their IP address' + 'parameters': + - 'name': 'ip0' + 'in': 'query' + 'description': 'Filter by IP address' + 'schema': + 'type': 'string' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ClientsFindResponse' + '/blocked_services/list': + 'get': + 'tags': + - 'blocked_services' + 'operationId': 'blockedServicesList' + 'summary': 'Get blocked services list' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/BlockedServicesArray' + '/blocked_services/set': + 'post': + 'tags': + - 'blocked_services' + 'operationId': 'blockedServicesSet' + 'summary': 'Set blocked services list' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/BlockedServicesArray' + 'responses': + '200': + 'description': 'OK.' + '/rewrite/list': + 'get': + 'tags': + - 'rewrite' + 'operationId': 'rewriteList' + 'summary': 'Get list of Rewrite rules' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/RewriteList' + '/rewrite/add': + 'post': + 'tags': + - 'rewrite' + 'operationId': 'rewriteAdd' + 'summary': 'Add a new Rewrite rule' + 'requestBody': + '$ref': '#/components/requestBodies/RewriteEntry' + 'responses': + '200': + 'description': 'OK.' + '/rewrite/delete': + 'post': + 'tags': + - 'rewrite' + 'operationId': 'rewriteDelete' + 'summary': 'Remove a Rewrite rule' + 'requestBody': + '$ref': '#/components/requestBodies/RewriteEntry' + 'responses': + '200': + 'description': 'OK.' + '/i18n/change_language': + 'post': + 'tags': + - 'i18n' + 'operationId': 'changeLanguage' + 'summary': > + Change current language. Argument must be an ISO 639-1 two-letter code. + 'requestBody': + 'content': + 'text/plain': + 'schema': + 'type': 'string' + 'example': 'en' + 'description': > + New language. It must be known to the server and must be an ISO 639-1 + two-letter code. + 'responses': + '200': + 'description': 'OK.' + '/i18n/current_language': + 'get': + 'tags': + - 'i18n' + 'operationId': 'currentLanguage' + 'summary': > + Get currently set language. Result is ISO 639-1 two-letter code. Empty + result means default language. + 'responses': + '200': + 'description': 'OK.' + 'content': + 'text/plain': + 'examples': + 'response': + 'value': 'en' + '/install/get_addresses': + 'get': + 'tags': + - 'install' + 'operationId': 'installGetAddresses' + 'summary': 'Gets the network interfaces information.' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/AddressesInfo' + '/install/check_config': + 'post': + 'tags': + - 'install' + 'operationId': 'installCheckConfig' + 'summary': 'Checks configuration' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/CheckConfigRequest' + 'description': 'Configuration to be checked' + 'required': true + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/CheckConfigResponse' + '400': + 'description': > + Failed to parse JSON or cannot listen on the specified address. + '/install/configure': + 'post': + 'tags': + - 'install' + 'operationId': 'installConfigure' + 'summary': 'Applies the initial configuration.' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/InitialConfiguration' + 'description': 'Initial configuration JSON' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '400': + 'description': > + Failed to parse initial configuration or cannot listen to the + specified addresses. + '500': + 'description': 'Cannot start the DNS server' + '/login': + 'post': + 'tags': + - 'global' + 'operationId': 'login' + 'summary': 'Perform administrator log-in' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Login' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '/logout': + 'get': + 'tags': + - 'global' + 'operationId': 'logout' + 'summary': 'Perform administrator log-out' + 'responses': + '302': + 'description': 'OK.' + '/profile': + 'get': + 'tags': + - 'global' + 'operationId': 'getProfile' + 'summary': '' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ProfileInfo' - This might be needed if new URL was just added and you dont want to wait for automatic refresh to kick in. + '/apple/doh.mobileconfig': + 'get': + 'operationId': 'mobileConfigDoH' + 'parameters': + - 'description': > + Host for which the config is generated. If no host is provided, + `tls.server_name` from the configuration file is used. If + `tls.server_name` is not set, the API returns an error with a 500 + status. + 'example': 'example.org' + 'in': 'query' + 'name': 'host' + 'schema': + 'type': 'string' + 'responses': + '200': + 'description': 'DNS over HTTPS plist file.' + '500': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Server configuration error.' + 'summary': 'Get DNS over HTTPS .mobileconfig.' + 'tags': + - 'mobileconfig' + - 'global' + '/apple/dot.mobileconfig': + 'get': + 'operationId': 'mobileConfigDoT' + 'parameters': + - 'description': > + Host for which the config is generated. If no host is provided, + `tls.server_name` from the configuration file is used. If + `tls.server_name` is not set, the API returns an error with a 500 + status. + 'example': 'example.org' + 'in': 'query' + 'name': 'host' + 'schema': + 'type': 'string' + 'responses': + '200': + 'description': 'DNS over TLS plist file' + '500': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Server configuration error.' + 'summary': 'Get DNS over TLS .mobileconfig.' + 'tags': + - 'mobileconfig' + - 'global' +'components': + 'requestBodies': + 'TlsConfig': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/TlsConfig' + 'description': 'TLS configuration JSON' + 'required': true + 'DhcpStaticLease': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/DhcpStaticLease' + 'required': true + 'RewriteEntry': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/RewriteEntry' + 'required': true + 'schemas': + 'ServerStatus': + 'type': 'object' + 'description': 'AdGuard Home server status and configuration' + 'required': + - 'dns_address' + - 'dns_port' + - 'protection_enabled' + - 'querylog_enabled' + - 'running' + - 'bootstrap_dns' + - 'upstream_dns' + - 'version' + - 'language' + 'properties': + 'dns_address': + 'type': 'string' + 'example': '127.0.0.1' + 'dns_port': + 'type': 'integer' + 'format': 'int32' + 'example': 53 + 'minimum': 1 + 'maximum': 65535 + 'protection_enabled': + 'type': 'boolean' + 'dhcp_available': + 'type': 'boolean' + 'querylog_enabled': + 'type': 'boolean' + 'running': + 'type': 'boolean' + 'version': + 'type': 'string' + 'example': '0.1' + 'language': + 'type': 'string' + 'example': 'en' + 'DNSConfig': + 'type': 'object' + 'description': 'Query log configuration' + 'properties': + 'bootstrap_dns': + 'type': 'array' + 'description': > + Bootstrap servers, port is optional after colon. Empty value will + reset it to default values. + 'items': + 'type': 'string' + 'example': + - '8.8.8.8:53' + - '1.1.1.1:53' + 'upstream_dns': + 'type': 'array' + 'description': > + Upstream servers, port is optional after colon. Empty value will + reset it to default values. + 'items': + 'type': 'string' + 'example': + - 'tls://1.1.1.1' + - 'tls://1.0.0.1' + 'upstream_dns_file': + 'type': 'string' + 'protection_enabled': + 'type': 'boolean' + 'dhcp_available': + 'type': 'boolean' + 'ratelimit': + 'type': 'integer' + 'blocking_mode': + 'type': 'string' + 'enum': + - 'default' + - 'refused' + - 'nxdomain' + - 'null_ip' + - 'custom_ip' + 'blocking_ipv4': + 'type': 'string' + 'blocking_ipv6': + 'type': 'string' + 'edns_cs_enabled': + 'type': 'boolean' + 'dnssec_enabled': + 'type': 'boolean' + 'cache_size': + 'type': 'integer' + 'cache_ttl_min': + 'type': 'integer' + 'cache_ttl_max': + 'type': 'integer' + 'upstream_mode': + 'enum': + - '' + - 'parallel' + - 'fastest_addr' + 'UpstreamsConfig': + 'type': 'object' + 'description': 'Upstreams configuration' + 'required': + - 'bootstrap_dns' + - 'upstream_dns' + 'properties': + 'bootstrap_dns': + 'type': 'array' + 'description': > + Bootstrap servers, port is optional after colon. Empty value will + reset it to default values. + 'items': + 'type': 'string' + 'example': + - '8.8.8.8:53' + - '1.1.1.1:53' + 'upstream_dns': + 'type': 'array' + 'description': > + Upstream servers, port is optional after colon. Empty value will + reset it to default values. + 'items': + 'type': 'string' + 'example': + - 'tls://1.1.1.1' + - 'tls://1.0.0.1' + 'Filter': + 'type': 'object' + 'description': 'Filter subscription info' + 'required': + - 'enabled' + - 'id' + - 'lastUpdated' + - 'name' + - 'rulesCount' + - 'url' + 'properties': + 'enabled': + 'type': 'boolean' + 'id': + 'type': 'integer' + 'example': 1234 + 'lastUpdated': + 'type': 'string' + 'format': 'date-time' + 'example': '2018-10-30T12:18:57+03:00' + 'name': + 'type': 'string' + 'example': 'AdGuard Simplified Domain Names filter' + 'rulesCount': + 'type': 'integer' + 'example': 5912 + 'url': + 'type': 'string' + 'example': > + https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt + 'FilterStatus': + 'type': 'object' + 'description': 'Filtering settings' + 'properties': + 'enabled': + 'type': 'boolean' + 'interval': + 'type': 'integer' + 'filters': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/Filter' + 'user_rules': + 'type': 'array' + 'items': + 'type': 'string' + 'FilterConfig': + 'type': 'object' + 'description': 'Filtering settings' + 'properties': + 'enabled': + 'type': 'boolean' + 'interval': + 'type': 'integer' + 'FilterSetUrl': + 'type': 'object' + 'description': 'Filtering URL settings' + 'properties': + 'data': + 'properties': + 'enabled': + 'type': 'boolean' + 'name': + 'type': 'string' + 'url': + 'type': 'string' + 'type': 'object' + 'url': + 'type': 'string' + 'whitelist': + 'type': 'boolean' + 'FilterRefreshRequest': + 'type': 'object' + 'description': 'Refresh Filters request data' + 'properties': + 'whitelist': + 'type': 'boolean' + 'FilterCheckHostResponse': + 'type': 'object' + 'description': 'Check Host Result' + 'properties': + 'reason': + 'type': 'string' + 'description': 'Request filtering status.' + 'enum': + - 'NotFilteredNotFound' + - 'NotFilteredWhiteList' + - 'NotFilteredError' + - 'FilteredBlackList' + - 'FilteredSafeBrowsing' + - 'FilteredParental' + - 'FilteredInvalid' + - 'FilteredSafeSearch' + - 'FilteredBlockedService' + - 'Rewrite' + - 'RewriteEtcHosts' + - 'DNSRewriteRule' + 'filter_id': + 'deprecated': true + 'description': > + In case if there's a rule applied to this DNS request, this is ID of + the filter list that the rule belongs to. - This API request is ratelimited, so you can call it freely as often as you like, it wont create unneccessary burden on servers that host the URL. + Deprecated: use `rules[*].filter_list_id` instead. + 'type': 'integer' + 'rule': + 'deprecated': true + 'type': 'string' + 'example': '||example.org^' + 'description': > + Filtering rule applied to the request (if any). + Deprecated: use `rules[*].text` instead. + 'rules': + 'description': 'Applied rules.' + 'type': 'array' + 'items': + '$ref': '#/components/schemas/ResultRule' + 'service_name': + 'type': 'string' + 'description': 'Set if reason=FilteredBlockedService' + 'cname': + 'type': 'string' + 'description': 'Set if reason=Rewrite' + 'ip_addrs': + 'type': 'array' + 'items': + 'type': 'string' + 'description': 'Set if reason=Rewrite' + 'FilterRefreshResponse': + 'type': 'object' + 'description': '/filtering/refresh response data' + 'properties': + 'updated': + 'type': 'integer' + 'GetVersionRequest': + 'type': 'object' + 'description': '/version.json request data' + 'properties': + 'recheck_now': + 'description': > + If false, server will check for a new version data only once in + several hours. + 'type': 'boolean' + 'VersionInfo': + 'type': 'object' + 'description': > + Information about the latest available version of AdGuard Home. + 'properties': + 'new_version': + 'type': 'string' + 'example': 'v0.9' + 'announcement': + 'type': 'string' + 'example': 'AdGuard Home v0.9 is now available!' + 'announcement_url': + 'type': 'string' + 'example': > + https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.9 + 'can_autoupdate': + 'type': 'boolean' + 'Stats': + 'type': 'object' + 'description': 'Server statistics data' + 'properties': + 'time_units': + 'type': 'string' + 'description': 'Time units (hours | days)' + 'example': 'hours' + 'num_dns_queries': + 'type': 'integer' + 'description': 'Total number of DNS queries' + 'example': 123 + 'num_blocked_filtering': + 'type': 'integer' + 'description': 'Number of requests blocked by filtering rules' + 'example': 50 + 'num_replaced_safebrowsing': + 'type': 'integer' + 'description': 'Number of requests blocked by safebrowsing module' + 'example': 5 + 'num_replaced_safesearch': + 'type': 'integer' + 'description': 'Number of requests blocked by safesearch module' + 'example': 5 + 'num_replaced_parental': + 'type': 'integer' + 'description': 'Number of blocked adult websites' + 'example': 15 + 'avg_processing_time': + 'type': 'number' + 'format': 'float' + 'description': 'Average time in milliseconds on processing a DNS' + 'example': 0.34 + 'top_queried_domains': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/TopArrayEntry' + 'top_clients': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/TopArrayEntry' + 'top_blocked_domains': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/TopArrayEntry' + 'dns_queries': + 'type': 'array' + 'items': + 'type': 'integer' + 'blocked_filtering': + 'type': 'array' + 'items': + 'type': 'integer' + 'replaced_safebrowsing': + 'type': 'array' + 'items': + 'type': 'integer' + 'replaced_parental': + 'type': 'array' + 'items': + 'type': 'integer' + 'TopArrayEntry': + 'type': 'object' + 'description': > + Represent the number of hits per key (domain or client IP). + 'properties': + 'domain_or_ip': + 'type': 'integer' + 'StatsConfig': + 'type': 'object' + 'description': 'Statistics configuration' + 'properties': + 'interval': + 'type': 'integer' + 'description': 'Time period to keep data (1 | 7 | 30 | 90)' + 'DhcpConfig': + 'type': 'object' + 'properties': + 'enabled': + 'type': 'boolean' + 'interface_name': + 'type': 'string' + 'v4': + '$ref': '#/components/schemas/DhcpConfigV4' + 'v6': + '$ref': '#/components/schemas/DhcpConfigV6' + 'DhcpConfigV4': + 'type': 'object' + 'properties': + 'gateway_ip': + 'type': 'string' + 'example': '192.168.1.1' + 'subnet_mask': + 'type': 'string' + 'example': '255.255.255.0' + 'range_start': + 'type': 'string' + 'example': '192.168.1.2' + 'range_end': + 'type': 'string' + 'example': '192.168.10.50' + 'lease_duration': + 'type': 'integer' + 'DhcpConfigV6': + 'type': 'object' + 'properties': + 'range_start': + 'type': 'string' + 'lease_duration': + 'type': 'integer' + 'DhcpLease': + 'type': 'object' + 'description': 'DHCP lease information' + 'required': + - 'mac' + - 'ip' + - 'hostname' + - 'expires' + 'properties': + 'mac': + 'type': 'string' + 'example': '00:11:09:b3:b3:b8' + 'ip': + 'type': 'string' + 'example': '192.168.1.22' + 'hostname': + 'type': 'string' + 'example': 'dell' + 'expires': + 'type': 'string' + 'example': '2017-07-21T17:32:28Z' + 'DhcpStaticLease': + 'type': 'object' + 'description': 'DHCP static lease information' + 'required': + - 'mac' + - 'ip' + - 'hostname' + - 'expires' + 'properties': + 'mac': + 'type': 'string' + 'example': '00:11:09:b3:b3:b8' + 'ip': + 'type': 'string' + 'example': '192.168.1.22' + 'hostname': + 'type': 'string' + 'example': 'dell' + 'DhcpStatus': + 'type': 'object' + 'description': 'Built-in DHCP server configuration and status' + 'required': + - 'config' + - 'leases' + 'properties': + 'enabled': + 'type': 'boolean' + 'interface_name': + 'type': 'string' + 'v4': + '$ref': '#/components/schemas/DhcpConfigV4' + 'v6': + '$ref': '#/components/schemas/DhcpConfigV6' + 'leases': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/DhcpLease' + 'static_leases': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/DhcpStaticLease' - This should work as intended, a `force` parameter is offered as last-resort attempt to make filter lists fresh. + 'DhcpSearchResult': + 'type': 'object' + 'description': > + Information about a DHCP server discovered in the current network. + 'properties': + 'v4': + '$ref': '#/components/schemas/DhcpSearchV4' + 'v6': + '$ref': '#/components/schemas/DhcpSearchV6' + 'DhcpSearchV4': + 'type': 'object' + 'properties': + 'other_server': + '$ref': '#/components/schemas/DhcpSearchResultOtherServer' + 'static_ip': + '$ref': '#/components/schemas/DhcpSearchResultStaticIP' - If you ever find yourself using `force` to make something work that otherwise wont, this is a bug and report it accordingly. - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/FilterRefreshRequest" - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/FilterRefreshResponse" - /filtering/set_rules: - post: - tags: - - filtering - operationId: filteringSetRules - summary: Set user-defined filter rules - requestBody: - content: - text/plain: - schema: - type: string - example: "@@||yandex.ru^|" - description: All filtering rules, one line per rule - responses: - "200": - description: OK - /filtering/check_host: - get: - tags: - - filtering - operationId: filteringCheckHost - summary: Check if host name is filtered - parameters: - - name: name - in: query - description: Filter by host name - schema: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/FilterCheckHostResponse" - /safebrowsing/enable: - post: - tags: - - safebrowsing - operationId: safebrowsingEnable - summary: Enable safebrowsing - responses: - "200": - description: OK - /safebrowsing/disable: - post: - tags: - - safebrowsing - operationId: safebrowsingDisable - summary: Disable safebrowsing - responses: - "200": - description: OK - /safebrowsing/status: - get: - tags: - - safebrowsing - operationId: safebrowsingStatus - summary: Get safebrowsing status - responses: - "200": - description: OK - content: - application/json: - examples: - response: - value: - enabled: false - /parental/enable: - post: - tags: - - parental - operationId: parentalEnable - summary: Enable parental filtering - requestBody: - content: - text/plain: - schema: - type: string - enum: - - EARLY_CHILDHOOD - - YOUNG - - TEEN - - MATURE - example: sensitivity=TEEN - description: | - Age sensitivity for parental filtering, - EARLY_CHILDHOOD is 3 - YOUNG is 10 - TEEN is 13 - MATURE is 17 - required: true - responses: - "200": - description: OK - /parental/disable: - post: - tags: - - parental - operationId: parentalDisable - summary: Disable parental filtering - responses: - "200": - description: OK - /parental/status: - get: - tags: - - parental - operationId: parentalStatus - summary: Get parental filtering status - responses: - "200": - description: OK - content: - application/json: - examples: - response: - value: - enabled: true - sensitivity: 13 - /safesearch/enable: - post: - tags: - - safesearch - operationId: safesearchEnable - summary: Enable safesearch - responses: - "200": - description: OK - /safesearch/disable: - post: - tags: - - safesearch - operationId: safesearchDisable - summary: Disable safesearch - responses: - "200": - description: OK - /safesearch/status: - get: - tags: - - safesearch - operationId: safesearchStatus - summary: Get safesearch status - responses: - "200": - description: OK - content: - application/json: - examples: - response: - value: - enabled: false - /clients: - get: - tags: - - clients - operationId: clientsStatus - summary: Get information about configured clients - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/Clients" - /clients/add: - post: - tags: - - clients - operationId: clientsAdd - summary: Add a new client - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/Client" - required: true - responses: - "200": - description: OK - /clients/delete: - post: - tags: - - clients - operationId: clientsDelete - summary: Remove a client - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ClientDelete" - required: true - responses: - "200": - description: OK - /clients/update: - post: - tags: - - clients - operationId: clientsUpdate - summary: Update client information - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ClientUpdate" - required: true - responses: - "200": - description: OK - /clients/find: - get: - tags: - - clients - operationId: clientsFind - summary: Get information about selected clients by their IP address - parameters: - - name: ip0 - in: query - description: Filter by IP address - schema: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/ClientsFindResponse" - /blocked_services/list: - get: - tags: - - blocked_services - operationId: blockedServicesList - summary: Get blocked services list - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/BlockedServicesArray" - /blocked_services/set: - post: - tags: - - blocked_services - operationId: blockedServicesSet - summary: Set blocked services list - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/BlockedServicesArray" - responses: - "200": - description: OK - /rewrite/list: - get: - tags: - - rewrite - operationId: rewriteList - summary: Get list of Rewrite rules - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/RewriteList" - /rewrite/add: - post: - tags: - - rewrite - operationId: rewriteAdd - summary: Add a new Rewrite rule - requestBody: - $ref: "#/components/requestBodies/RewriteEntry" - responses: - "200": - description: OK - /rewrite/delete: - post: - tags: - - rewrite - operationId: rewriteDelete - summary: Remove a Rewrite rule - requestBody: - $ref: "#/components/requestBodies/RewriteEntry" - responses: - "200": - description: OK - /i18n/change_language: - post: - tags: - - i18n - operationId: changeLanguage - summary: Change current language. Argument must be an ISO 639-1 two-letter code - requestBody: - content: - text/plain: - schema: - type: string - example: en - description: New language. It must be known to the server and must be an ISO 639-1 - two-letter code - responses: - "200": - description: OK - /i18n/current_language: - get: - tags: - - i18n - operationId: currentLanguage - summary: Get currently set language. Result is ISO 639-1 two-letter code. Empty - result means default language. - responses: - "200": - description: OK - content: - text/plain: - examples: - response: - value: en - /install/get_addresses: - get: - tags: - - install - operationId: installGetAddresses - summary: Gets the network interfaces information. - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/AddressesInfo" - /install/check_config: - post: - tags: - - install - operationId: installCheckConfig - summary: Checks configuration - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CheckConfigRequest" - description: Configuration to be checked - required: true - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/CheckConfigResponse" - "400": - description: Failed to parse JSON or cannot listen on the specified address - /install/configure: - post: - tags: - - install - operationId: installConfigure - summary: Applies the initial configuration. - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/InitialConfiguration" - description: Initial configuration JSON - required: true - responses: - "200": - description: OK - "400": - description: Failed to parse initial configuration or cannot listen to the - specified addresses - "500": - description: Cannot start the DNS server - /login: - post: - tags: - - global - operationId: login - summary: Perform administrator log-in - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/Login" - required: true - responses: - "200": - description: OK - /logout: - get: - tags: - - global - operationId: logout - summary: Perform administrator log-out - responses: - "302": - description: OK - /profile: - get: - tags: - - global - operationId: getProfile - summary: "" - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/ProfileInfo" - /apple/doh.mobileconfig: - get: - tags: - - mobileconfig - - global - operationId: mobileConfigDoH - summary: Get DNS over HTTPS .mobileconfig - responses: - "200": - description: DNS over HTTPS plist file + 'DhcpSearchV6': + 'type': 'object' + 'properties': + 'other_server': + '$ref': '#/components/schemas/DhcpSearchResultOtherServer' - /apple/dot.mobileconfig: - get: - tags: - - mobileconfig - - global - operationId: mobileConfigDoT - summary: Get TLS over TLS .mobileconfig - responses: - "200": - description: DNS over TLS plist file + 'DhcpSearchResultOtherServer': + 'type': 'object' + 'properties': + 'found': + 'type': 'string' + 'description': 'yes|no|error' + 'example': 'no' + 'error': + 'type': 'string' + 'description': 'Set if found=error' + 'example': '' -components: - requestBodies: - TlsConfig: - content: - application/json: - schema: - $ref: "#/components/schemas/TlsConfig" - description: TLS configuration JSON - required: true - DhcpStaticLease: - content: - application/json: - schema: - $ref: "#/components/schemas/DhcpStaticLease" - required: true - RewriteEntry: - content: - application/json: - schema: - $ref: "#/components/schemas/RewriteEntry" - required: true - schemas: - ServerStatus: - type: object - description: AdGuard Home server status and configuration - required: - - dns_address - - dns_port - - protection_enabled - - querylog_enabled - - running - - bootstrap_dns - - upstream_dns - - version - - language - properties: - dns_address: - type: string - example: 127.0.0.1 - dns_port: - type: integer - format: int32 - example: 53 - minimum: 1 - maximum: 65535 - protection_enabled: - type: boolean - dhcp_available: - type: boolean - querylog_enabled: - type: boolean - running: - type: boolean - version: - type: string - example: "0.1" - language: - type: string - example: en - DNSConfig: - type: object - description: Query log configuration - properties: - bootstrap_dns: - type: array - description: Bootstrap servers, port is optional after colon. Empty value will - reset it to default values - items: - type: string - example: - - 8.8.8.8:53 - - 1.1.1.1:53 - upstream_dns: - type: array - description: Upstream servers, port is optional after colon. Empty value will - reset it to default values - items: - type: string - example: - - tls://1.1.1.1 - - tls://1.0.0.1 - upstream_dns_file: - type: string - protection_enabled: - type: boolean - dhcp_available: - type: boolean - ratelimit: - type: integer - blocking_mode: - type: string - enum: - - default - - refused - - nxdomain - - null_ip - - custom_ip - blocking_ipv4: - type: string - blocking_ipv6: - type: string - edns_cs_enabled: - type: boolean - dnssec_enabled: - type: boolean - cache_size: - type: integer - cache_ttl_min: - type: integer - cache_ttl_max: - type: integer - upstream_mode: - enum: - - "" - - parallel - - fastest_addr - UpstreamsConfig: - type: object - description: Upstreams configuration - required: - - bootstrap_dns - - upstream_dns - properties: - bootstrap_dns: - type: array - description: Bootstrap servers, port is optional after colon. Empty value will - reset it to default values - items: - type: string - example: - - 8.8.8.8:53 - - 1.1.1.1:53 - upstream_dns: - type: array - description: Upstream servers, port is optional after colon. Empty value will - reset it to default values - items: - type: string - example: - - tls://1.1.1.1 - - tls://1.0.0.1 - Filter: - type: object - description: Filter subscription info - required: - - enabled - - id - - lastUpdated - - name - - rulesCount - - url - properties: - enabled: - type: boolean - id: - type: integer - example: 1234 - lastUpdated: - type: string - format: date-time - example: 2018-10-30T12:18:57+03:00 - name: - type: string - example: AdGuard Simplified Domain Names filter - rulesCount: - type: integer - example: 5912 - url: - type: string - example: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt - FilterStatus: - type: object - description: Filtering settings - properties: - enabled: - type: boolean - interval: - type: integer - filters: - type: array - items: - $ref: "#/components/schemas/Filter" - user_rules: - type: array - items: - type: string - FilterConfig: - type: object - description: Filtering settings - properties: - enabled: - type: boolean - interval: - type: integer - FilterSetUrl: - type: object - description: Filtering URL settings - properties: - data: - properties: - enabled: - type: boolean - name: - type: string - url: - type: string - type: object - url: - type: string - whitelist: - type: boolean - FilterRefreshRequest: - type: object - description: Refresh Filters request data - properties: - whitelist: - type: boolean - FilterCheckHostResponse: - type: object - description: Check Host Result - properties: - reason: - type: string - description: DNS filter status - enum: - - NotFilteredNotFound - - NotFilteredWhiteList - - NotFilteredError - - FilteredBlackList - - FilteredSafeBrowsing - - FilteredParental - - FilteredInvalid - - FilteredSafeSearch - - FilteredBlockedService - - ReasonRewrite - filter_id: - type: integer - rule: - type: string - example: "||example.org^" - description: Filtering rule applied to the request (if any) - service_name: - type: string - description: Set if reason=FilteredBlockedService - cname: - type: string - description: Set if reason=ReasonRewrite - ip_addrs: - type: array - items: - type: string - description: Set if reason=ReasonRewrite - FilterRefreshResponse: - type: object - description: /filtering/refresh response data - properties: - updated: - type: integer - GetVersionRequest: - type: object - description: /version.json request data - properties: - recheck_now: - description: If false, server will check for a new version data only once in - several hours - type: boolean - VersionInfo: - type: object - description: Information about the latest available version of AdGuard Home - properties: - new_version: - type: string - example: v0.9 - announcement: - type: string - example: AdGuard Home v0.9 is now available! - announcement_url: - type: string - example: https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.9 - can_autoupdate: - type: boolean - Stats: - type: object - description: Server statistics data - properties: - time_units: - type: string - description: Time units (hours | days) - example: hours - num_dns_queries: - type: integer - description: Total number of DNS queries - example: 123 - num_blocked_filtering: - type: integer - description: Number of requests blocked by filtering rules - example: 50 - num_replaced_safebrowsing: - type: integer - description: Number of requests blocked by safebrowsing module - example: 5 - num_replaced_safesearch: - type: integer - description: Number of requests blocked by safesearch module - example: 5 - num_replaced_parental: - type: integer - description: Number of blocked adult websites - example: 15 - avg_processing_time: - type: number - format: float - description: Average time in milliseconds on processing a DNS - example: 0.34 - top_queried_domains: - type: array - items: - $ref: "#/components/schemas/TopArrayEntry" - top_clients: - type: array - items: - $ref: "#/components/schemas/TopArrayEntry" - top_blocked_domains: - type: array - items: - $ref: "#/components/schemas/TopArrayEntry" - dns_queries: - type: array - items: - type: integer - blocked_filtering: - type: array - items: - type: integer - replaced_safebrowsing: - type: array - items: - type: integer - replaced_parental: - type: array - items: - type: integer - TopArrayEntry: - type: object - description: Represent the number of hits per key (domain or client IP) - properties: - domain_or_ip: - type: integer - StatsConfig: - type: object - description: Statistics configuration - properties: - interval: - type: integer - description: Time period to keep data (1 | 7 | 30 | 90) - DhcpConfig: - type: object - properties: - enabled: - type: boolean - interface_name: - type: string - v4: - $ref: "#/components/schemas/DhcpConfigV4" - v6: - $ref: "#/components/schemas/DhcpConfigV6" - DhcpConfigV4: - type: object - properties: - gateway_ip: - type: string - example: 192.168.1.1 - subnet_mask: - type: string - example: 255.255.255.0 - range_start: - type: string - example: 192.168.1.2 - range_end: - type: string - example: 192.168.10.50 - lease_duration: - type: integer - DhcpConfigV6: - type: object - properties: - range_start: - type: string - lease_duration: - type: integer - DhcpLease: - type: object - description: DHCP lease information - required: - - mac - - ip - - hostname - - expires - properties: - mac: - type: string - example: 00:11:09:b3:b3:b8 - ip: - type: string - example: 192.168.1.22 - hostname: - type: string - example: dell - expires: - type: string - example: "2017-07-21T17:32:28Z" - DhcpStaticLease: - type: object - description: DHCP static lease information - required: - - mac - - ip - - hostname - - expires - properties: - mac: - type: string - example: 00:11:09:b3:b3:b8 - ip: - type: string - example: 192.168.1.22 - hostname: - type: string - example: dell - DhcpStatus: - type: object - description: Built-in DHCP server configuration and status - required: - - config - - leases - properties: - enabled: - type: boolean - interface_name: - type: string - v4: - $ref: "#/components/schemas/DhcpConfigV4" - v6: - $ref: "#/components/schemas/DhcpConfigV6" - leases: - type: array - items: - $ref: "#/components/schemas/DhcpLease" - static_leases: - type: array - items: - $ref: "#/components/schemas/DhcpStaticLease" + 'DhcpSearchResultStaticIP': + 'type': 'object' + 'properties': + 'static': + 'type': 'string' + 'description': 'yes|no|error' + 'example': 'yes' + 'ip': + 'type': 'string' + 'description': 'Set if static=no' + 'example': '' - DhcpSearchResult: - type: object - description: Information about a DHCP server discovered in the current network - properties: - v4: - $ref: "#/components/schemas/DhcpSearchV4" - v6: - $ref: "#/components/schemas/DhcpSearchV6" + 'DnsAnswer': + 'type': 'object' + 'description': 'DNS answer section' + 'properties': + 'ttl': + 'type': 'integer' + 'example': 55 + 'type': + 'type': 'string' + 'example': 'A' + 'value': + 'type': 'string' + 'example': '217.69.139.201' + 'DnsQuestion': + 'type': 'object' + 'description': 'DNS question section' + 'properties': + 'class': + 'type': 'string' + 'example': 'IN' + 'host': + 'type': 'string' + 'example': 'example.org' + 'type': + 'type': 'string' + 'example': 'A' + 'AddUrlRequest': + 'type': 'object' + 'description': '/add_url request data' + 'properties': + 'name': + 'type': 'string' + 'url': + 'description': > + URL or an absolute path to the file containing filtering rules. + 'type': 'string' + 'example': 'https://filters.adtidy.org/windows/filters/15.txt' + 'whitelist': + 'type': 'boolean' + 'RemoveUrlRequest': + 'type': 'object' + 'description': '/remove_url request data' + 'properties': + 'url': + 'description': 'Previously added URL containing filtering rules' + 'type': 'string' + 'example': 'https://filters.adtidy.org/windows/filters/15.txt' + 'QueryLogItem': + 'type': 'object' + 'description': 'Query log item' + 'properties': + 'answer': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/DnsAnswer' + 'original_answer': + 'type': 'array' + 'description': 'Answer from upstream server (optional)' + 'items': + '$ref': '#/components/schemas/DnsAnswer' + 'upstream': + 'type': 'string' + 'description': > + Upstream URL starting with tcp://, tls://, https://, or with an IP + address. + 'answer_dnssec': + 'type': 'boolean' + 'client': + 'type': 'string' + 'example': '192.168.0.1' + 'client_proto': + 'enum': + - 'dot' + - 'doh' + - 'doq' + - '' + 'elapsedMs': + 'type': 'string' + 'example': '54.023928' + 'question': + '$ref': '#/components/schemas/DnsQuestion' + 'filterId': + 'deprecated': true + 'type': 'integer' + 'example': 123123 + 'description': > + In case if there's a rule applied to this DNS request, this is ID of + the filter list that the rule belongs to. - DhcpSearchV4: - type: object - properties: - other_server: - $ref: "#/components/schemas/DhcpSearchResultOtherServer" - static_ip: - $ref: "#/components/schemas/DhcpSearchResultStaticIP" + Deprecated: use `rules[*].filter_list_id` instead. + 'rule': + 'deprecated': true + 'type': 'string' + 'example': '||example.org^' + 'description': > + Filtering rule applied to the request (if any). - DhcpSearchV6: - type: object - properties: - other_server: - $ref: "#/components/schemas/DhcpSearchResultOtherServer" + Deprecated: use `rules[*].text` instead. + 'rules': + 'description': 'Applied rules.' + 'type': 'array' + 'items': + '$ref': '#/components/schemas/ResultRule' + 'reason': + 'type': 'string' + 'description': 'Request filtering status.' + 'enum': + - 'NotFilteredNotFound' + - 'NotFilteredWhiteList' + - 'NotFilteredError' + - 'FilteredBlackList' + - 'FilteredSafeBrowsing' + - 'FilteredParental' + - 'FilteredInvalid' + - 'FilteredSafeSearch' + - 'FilteredBlockedService' + - 'Rewrite' + - 'RewriteEtcHosts' + - 'DNSRewriteRule' + 'service_name': + 'type': 'string' + 'description': 'Set if reason=FilteredBlockedService' + 'status': + 'type': 'string' + 'description': 'DNS response status' + 'example': 'NOERROR' + 'time': + 'type': 'string' + 'description': 'DNS request processing start time' + 'example': '2018-11-26T00:02:41+03:00' + 'QueryLog': + 'type': 'object' + 'description': 'Query log' + 'properties': + 'oldest': + 'type': 'string' + 'example': '2018-11-26T00:02:41+03:00' + 'data': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/QueryLogItem' + 'QueryLogConfig': + 'type': 'object' + 'description': 'Query log configuration' + 'properties': + 'enabled': + 'type': 'boolean' + 'description': 'Is query log enabled' + 'interval': + 'type': 'integer' + 'description': 'Time period to keep data (1 | 7 | 30 | 90)' + 'anonymize_client_ip': + 'type': 'boolean' + 'description': "Anonymize clients' IP addresses" + 'ResultRule': + 'description': 'Applied rule.' + 'properties': + 'filter_list_id': + 'description': > + In case if there's a rule applied to this DNS request, this is ID of + the filter list that the rule belongs to. + 'example': 123123 + 'format': 'int64' + 'type': 'integer' + 'text': + 'description': > + The text of the filtering rule applied to the request (if any). + 'example': '||example.org^' + 'type': 'string' + 'type': 'object' + 'TlsConfig': + 'type': 'object' + 'description': 'TLS configuration settings and status' + 'properties': + 'enabled': + 'type': 'boolean' + 'example': true + 'description': 'enabled is the encryption (DOT/DOH/HTTPS) status' + 'server_name': + 'type': 'string' + 'example': 'example.org' + 'description': 'server_name is the hostname of your HTTPS/TLS server' + 'force_https': + 'type': 'boolean' + 'example': true + 'description': 'if true, forces HTTP->HTTPS redirect' + 'port_https': + 'type': 'integer' + 'format': 'int32' + 'example': 443 + 'description': 'HTTPS port. If 0, HTTPS will be disabled.' + 'port_dns_over_tls': + 'type': 'integer' + 'format': 'int32' + 'example': 853 + 'description': 'DNS-over-TLS port. If 0, DOT will be disabled.' + 'port_dns_over_quic': + 'type': 'integer' + 'format': 'int32' + 'example': 784 + 'description': 'DNS-over-QUIC port. If 0, DOQ will be disabled.' + 'certificate_chain': + 'type': 'string' + 'description': 'Base64 string with PEM-encoded certificates chain' + 'private_key': + 'type': 'string' + 'description': 'Base64 string with PEM-encoded private key' + 'certificate_path': + 'type': 'string' + 'description': 'Path to certificate file' + 'private_key_path': + 'type': 'string' + 'description': 'Path to private key file' + 'valid_cert': + 'type': 'boolean' + 'example': true + 'description': > + Set to true if the specified certificates chain is a valid chain of + X509 certificates. + 'valid_chain': + 'type': 'boolean' + 'example': true + 'description': > + Set to true if the specified certificates chain is verified and + issued by a known CA. + 'subject': + 'type': 'string' + 'example': 'CN=example.org' + 'description': 'The subject of the first certificate in the chain.' + 'issuer': + 'type': 'string' + 'example': "CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US" + 'description': 'The issuer of the first certificate in the chain.' + 'not_before': + 'type': 'string' + 'example': '2019-01-31T10:47:32Z' + 'description': > + The NotBefore field of the first certificate in the chain. + 'not_after': + 'type': 'string' + 'example': '2019-05-01T10:47:32Z' + 'description': > + The NotAfter field of the first certificate in the chain. + 'dns_names': + 'type': 'array' + 'items': + 'type': 'string' + 'description': > + The value of SubjectAltNames field of the first certificate in the + chain. + 'example': + - '*.example.org' + 'valid_key': + 'type': 'boolean' + 'example': true + 'description': 'Set to true if the key is a valid private key.' + 'key_type': + 'type': 'string' + 'example': 'RSA' + 'enum': + - 'RSA' + - 'ECDSA' + 'description': 'Key type.' + 'warning_validation': + 'type': 'string' + 'example': 'You have specified an empty certificate' + 'description': > + A validation warning message with the issue description. + 'valid_pair': + 'type': 'boolean' + 'example': true + 'description': > + Set to true if both certificate and private key are correct. + 'NetInterface': + 'type': 'object' + 'description': 'Network interface info' + 'properties': + 'flags': + 'type': 'string' + 'example': 'up|broadcast|multicast' + 'hardware_address': + 'type': 'string' + 'example': '52:54:00:11:09:ba' + 'name': + 'type': 'string' + 'example': 'eth0' + 'ipv4_addresses': + 'type': 'array' + 'items': + 'type': 'string' + 'ipv6_addresses': + 'type': 'array' + 'items': + 'type': 'string' + 'gateway_ip': + 'type': 'string' + 'AddressInfo': + 'type': 'object' + 'description': 'Port information' + 'properties': + 'ip': + 'type': 'string' + 'example': '127.0.0.1' + 'port': + 'type': 'integer' + 'format': 'int32' + 'example': 53 + 'AddressesInfo': + 'type': 'object' + 'description': 'AdGuard Home addresses configuration' + 'properties': + 'dns_port': + 'type': 'integer' + 'format': 'int32' + 'example': 53 + 'web_port': + 'type': 'integer' + 'format': 'int32' + 'example': 80 + 'interfaces': + 'type': 'object' + 'description': > + Network interfaces dictionary, keys are interface names. + 'additionalProperties': + '$ref': '#/components/schemas/NetInterface' + 'ProfileInfo': + 'type': 'object' + 'description': 'Information about the current user' + 'properties': + 'name': + 'type': 'string' + 'Client': + 'type': 'object' + 'description': 'Client information' + 'properties': + 'name': + 'type': 'string' + 'description': 'Name' + 'example': 'localhost' + 'ids': + 'type': 'array' + 'description': 'IP, CIDR or MAC address' + 'items': + 'type': 'string' + 'use_global_settings': + 'type': 'boolean' + 'filtering_enabled': + 'type': 'boolean' + 'parental_enabled': + 'type': 'boolean' + 'safebrowsing_enabled': + 'type': 'boolean' + 'safesearch_enabled': + 'type': 'boolean' + 'use_global_blocked_services': + 'type': 'boolean' + 'blocked_services': + 'type': 'array' + 'items': + 'type': 'string' + 'upstreams': + 'type': 'array' + 'items': + 'type': 'string' + 'ClientAuto': + 'type': 'object' + 'description': 'Auto-Client information' + 'properties': + 'ip': + 'type': 'string' + 'description': 'IP address' + 'example': '127.0.0.1' + 'name': + 'type': 'string' + 'description': 'Name' + 'example': 'localhost' + 'source': + 'type': 'string' + 'description': 'The source of this information' + 'example': 'etc/hosts' + 'ClientUpdate': + 'type': 'object' + 'description': 'Client update request' + 'properties': + 'name': + 'type': 'string' + 'data': + '$ref': '#/components/schemas/Client' + 'ClientDelete': + 'type': 'object' + 'description': 'Client delete request' + 'properties': + 'name': + 'type': 'string' + 'ClientsFindResponse': + 'type': 'array' + 'description': 'Response to clients find operation' + 'items': + '$ref': '#/components/schemas/ClientsFindEntry' + 'ClientsFindEntry': + 'type': 'object' + 'properties': + '1.2.3.4': + 'items': + '$ref': '#/components/schemas/ClientFindSubEntry' - DhcpSearchResultOtherServer: - type: object - properties: - found: - type: string - description: yes|no|error - example: no - error: - type: string - description: Set if found=error - example: "" + 'ClientFindSubEntry': + 'type': 'object' + 'properties': + 'name': + 'type': 'string' + 'description': 'Name' + 'example': 'localhost' + 'ids': + 'type': 'array' + 'description': 'IP, CIDR or MAC address' + 'items': + 'type': 'string' + 'use_global_settings': + 'type': 'boolean' + 'filtering_enabled': + 'type': 'boolean' + 'parental_enabled': + 'type': 'boolean' + 'safebrowsing_enabled': + 'type': 'boolean' + 'safesearch_enabled': + 'type': 'boolean' + 'use_global_blocked_services': + 'type': 'boolean' + 'blocked_services': + 'type': 'array' + 'items': + 'type': 'string' + 'upstreams': + 'type': 'array' + 'items': + 'type': 'string' + 'whois_info': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/WhoisInfo' + 'disallowed': + 'type': 'boolean' + 'description': > + Whether the client's IP is blocked or not. + 'disallowed_rule': + 'type': 'string' + 'description': > + The rule due to which the client is disallowed. If disallowed is + set to true, and this string is empty, then the client IP is + disallowed by the "allowed IP list", that is it is not included in + the allowed list. - DhcpSearchResultStaticIP: - type: object - properties: - static: - type: string - description: yes|no|error - example: yes - ip: - type: string - description: Set if static=no - example: "" + 'WhoisInfo': + 'type': 'object' + 'properties': + 'key': + 'type': 'string' - DnsAnswer: - type: object - description: DNS answer section - properties: - ttl: - type: integer - example: 55 - type: - type: string - example: A - value: - type: string - example: 217.69.139.201 - DnsQuestion: - type: object - description: DNS question section - properties: - class: - type: string - example: IN - host: - type: string - example: example.org - type: - type: string - example: A - AddUrlRequest: - type: object - description: /add_url request data - properties: - name: - type: string - url: - description: URL or an absolute path to the file containing filtering rules - type: string - example: https://filters.adtidy.org/windows/filters/15.txt - whitelist: - type: boolean - RemoveUrlRequest: - type: object - description: /remove_url request data - properties: - url: - description: Previously added URL containing filtering rules - type: string - example: https://filters.adtidy.org/windows/filters/15.txt - QueryLogItem: - type: object - description: Query log item - properties: - answer: - type: array - items: - $ref: "#/components/schemas/DnsAnswer" - original_answer: - type: array - description: Answer from upstream server (optional) - items: - $ref: "#/components/schemas/DnsAnswer" - upstream: - type: string - description: Upstream URL starting with tcp://, tls://, https://, or with an IP address - answer_dnssec: - type: boolean - client: - type: string - example: 192.168.0.1 - client_proto: - enum: - - dot - - doh - - doq - - "" - elapsedMs: - type: string - example: "54.023928" - question: - $ref: "#/components/schemas/DnsQuestion" - filterId: - type: integer - example: 123123 - description: In case if there's a rule applied to this DNS request, this is ID of - the filter that rule belongs to. - rule: - type: string - example: "||example.org^" - description: Filtering rule applied to the request (if any) - reason: - type: string - description: DNS filter status - enum: - - NotFilteredNotFound - - NotFilteredWhiteList - - NotFilteredError - - FilteredBlackList - - FilteredSafeBrowsing - - FilteredParental - - FilteredInvalid - - FilteredSafeSearch - - FilteredBlockedService - - ReasonRewrite - service_name: - type: string - description: Set if reason=FilteredBlockedService - status: - type: string - description: DNS response status - example: NOERROR - time: - type: string - description: DNS request processing start time - example: 2018-11-26T00:02:41+03:00 - QueryLog: - type: object - description: Query log - properties: - oldest: - type: string - example: 2018-11-26T00:02:41+03:00 - data: - type: array - items: - $ref: "#/components/schemas/QueryLogItem" - QueryLogConfig: - type: object - description: Query log configuration - properties: - enabled: - type: boolean - description: Is query log enabled - interval: - type: integer - description: Time period to keep data (1 | 7 | 30 | 90) - anonymize_client_ip: - type: boolean - description: Anonymize clients' IP addresses - TlsConfig: - type: object - description: TLS configuration settings and status - properties: - enabled: - type: boolean - example: "true" - description: enabled is the encryption (DOT/DOH/HTTPS) status - server_name: - type: string - example: example.org - description: server_name is the hostname of your HTTPS/TLS server - force_https: - type: boolean - example: "true" - description: if true, forces HTTP->HTTPS redirect - port_https: - type: integer - format: int32 - example: 443 - description: HTTPS port. If 0, HTTPS will be disabled. - port_dns_over_tls: - type: integer - format: int32 - example: 853 - description: DNS-over-TLS port. If 0, DOT will be disabled. - port_dns_over_quic: - type: integer - format: int32 - example: 784 - description: DNS-over-QUIC port. If 0, DOQ will be disabled. - certificate_chain: - type: string - description: Base64 string with PEM-encoded certificates chain - private_key: - type: string - description: Base64 string with PEM-encoded private key - certificate_path: - type: string - description: Path to certificate file - private_key_path: - type: string - description: Path to private key file - valid_cert: - type: boolean - example: "true" - description: valid_cert is true if the specified certificates chain is a valid - chain of X509 certificates - valid_chain: - type: boolean - example: "true" - description: valid_chain is true if the specified certificates chain is verified - and issued by a known CA - subject: - type: string - example: CN=example.org - description: subject is the subject of the first certificate in the chain - issuer: - type: string - example: CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US - description: issuer is the issuer of the first certificate in the chain - not_before: - type: string - example: 2019-01-31T10:47:32Z - description: not_before is the NotBefore field of the first certificate in the - chain - not_after: - type: string - example: 2019-05-01T10:47:32Z - description: not_after is the NotAfter field of the first certificate in the chain - dns_names: - type: array - items: - type: string - description: dns_names is the value of SubjectAltNames field of the first - certificate in the chain - example: - - "*.example.org" - valid_key: - type: boolean - example: "true" - description: valid_key is true if the key is a valid private key - key_type: - type: string - example: RSA - description: key_type is either RSA or ECDSA - warning_validation: - type: string - example: You have specified an empty certificate - description: warning_validation is a validation warning message with the issue - description - valid_pair: - type: boolean - example: "true" - description: valid_pair is true if both certificate and private key are correct - NetInterface: - type: object - description: Network interface info - properties: - flags: - type: string - example: up|broadcast|multicast - hardware_address: - type: string - example: 52:54:00:11:09:ba - name: - type: string - example: eth0 - ipv4_addresses: - type: array - items: - type: string - ipv6_addresses: - type: array - items: - type: string - gateway_ip: - type: string - AddressInfo: - type: object - description: Port information - properties: - ip: - type: string - example: 127.0.0.1 - port: - type: integer - format: int32 - example: 53 - AddressesInfo: - type: object - description: AdGuard Home addresses configuration - properties: - dns_port: - type: integer - format: int32 - example: 53 - web_port: - type: integer - format: int32 - example: 80 - interfaces: - type: object - description: Network interfaces dictionary (key is the interface name) - additionalProperties: - $ref: "#/components/schemas/NetInterface" - ProfileInfo: - type: object - description: Information about the current user - properties: - name: - type: string - Client: - type: object - description: Client information - properties: - name: - type: string - description: Name - example: localhost - ids: - type: array - description: IP, CIDR or MAC address - items: - type: string - use_global_settings: - type: boolean - filtering_enabled: - type: boolean - parental_enabled: - type: boolean - safebrowsing_enabled: - type: boolean - safesearch_enabled: - type: boolean - use_global_blocked_services: - type: boolean - blocked_services: - type: array - items: - type: string - upstreams: - type: array - items: - type: string - ClientAuto: - type: object - description: Auto-Client information - properties: - ip: - type: string - description: IP address - example: 127.0.0.1 - name: - type: string - description: Name - example: localhost - source: - type: string - description: The source of this information - example: etc/hosts - ClientUpdate: - type: object - description: Client update request - properties: - name: - type: string - data: - $ref: "#/components/schemas/Client" - ClientDelete: - type: object - description: Client delete request - properties: - name: - type: string - ClientsFindResponse: - type: array - description: Response to clients find operation - items: - $ref: "#/components/schemas/ClientsFindEntry" - ClientsFindEntry: - type: object - properties: - 1.2.3.4: - items: - $ref: "#/components/schemas/ClientFindSubEntry" - - ClientFindSubEntry: - type: object - properties: - name: - type: string - description: Name - example: localhost - ids: - type: array - description: IP, CIDR or MAC address - items: - type: string - use_global_settings: - type: boolean - filtering_enabled: - type: boolean - parental_enabled: - type: boolean - safebrowsing_enabled: - type: boolean - safesearch_enabled: - type: boolean - use_global_blocked_services: - type: boolean - blocked_services: - type: array - items: - type: string - upstreams: - type: array - items: - type: string - whois_info: - type: array - items: - $ref: "#/components/schemas/WhoisInfo" - disallowed: - type: boolean - description: > - Whether the client's IP is blocked or not. - disallowed_rule: - type: string - description: > - The rule due to which the client is disallowed. - If `disallowed` is `true`, and this string is empty - it means that - the client IP is disallowed by the "allowed IP list", i.e. it is not included in allowed list. - - WhoisInfo: - type: object - properties: - key: - type: string - - Clients: - type: object - properties: - clients: - $ref: "#/components/schemas/ClientsArray" - auto_clients: - $ref: "#/components/schemas/ClientsAutoArray" - ClientsArray: - type: array - items: - $ref: "#/components/schemas/Client" - description: Clients array - ClientsAutoArray: - type: array - items: - $ref: "#/components/schemas/ClientAuto" - description: Auto-Clients array - RewriteList: - type: array - items: - $ref: "#/components/schemas/RewriteEntry" - description: Rewrite rules array - RewriteEntry: - type: object - description: Rewrite rule - properties: - domain: - type: string - description: Domain name - example: example.org - answer: - type: string - description: value of A, AAAA or CNAME DNS record - example: 127.0.0.1 - BlockedServicesArray: - type: array - items: - type: string - CheckConfigRequest: - type: object - description: Configuration to be checked - properties: - dns: - $ref: "#/components/schemas/CheckConfigRequestInfo" - web: - $ref: "#/components/schemas/CheckConfigRequestInfo" - set_static_ip: - type: boolean - example: false - CheckConfigRequestInfo: - type: object - properties: - ip: - type: string - example: 127.0.0.1 - port: - type: integer - format: int32 - example: 53 - autofix: - type: boolean - example: false - CheckConfigResponse: - type: object - properties: - dns: - $ref: "#/components/schemas/CheckConfigResponseInfo" - web: - $ref: "#/components/schemas/CheckConfigResponseInfo" - static_ip: - $ref: "#/components/schemas/CheckConfigStaticIpInfo" - CheckConfigResponseInfo: - type: object - properties: - status: - type: string - example: "" - can_autofix: - type: boolean - example: false - CheckConfigStaticIpInfo: - type: object - properties: - static: - type: string - example: no - description: "Can be: yes, no, error" - ip: - type: string - example: 192.168.1.1 - description: Current dynamic IP address. Set if static=no - error: - type: string - example: "" - description: Error text. Set if static=error - InitialConfiguration: - type: object - description: AdGuard Home initial configuration (for the first-install wizard) - properties: - dns: - $ref: "#/components/schemas/AddressInfo" - web: - $ref: "#/components/schemas/AddressInfo" - username: - type: string - description: Basic auth username - example: admin - password: - type: string - description: Basic auth password - example: password - Login: - type: object - description: Login request data - properties: - username: - type: string - description: User name - password: - type: string - description: Password - Error: - description: A generic JSON error response. - properties: - message: - type: string - description: The error message, an opaque string. - type: object + 'Clients': + 'type': 'object' + 'properties': + 'clients': + '$ref': '#/components/schemas/ClientsArray' + 'auto_clients': + '$ref': '#/components/schemas/ClientsAutoArray' + 'ClientsArray': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/Client' + 'description': 'Clients array' + 'ClientsAutoArray': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/ClientAuto' + 'description': 'Auto-Clients array' + 'RewriteList': + 'type': 'array' + 'items': + '$ref': '#/components/schemas/RewriteEntry' + 'description': 'Rewrite rules array' + 'RewriteEntry': + 'type': 'object' + 'description': 'Rewrite rule' + 'properties': + 'domain': + 'type': 'string' + 'description': 'Domain name' + 'example': 'example.org' + 'answer': + 'type': 'string' + 'description': 'value of A, AAAA or CNAME DNS record' + 'example': '127.0.0.1' + 'BlockedServicesArray': + 'type': 'array' + 'items': + 'type': 'string' + 'CheckConfigRequest': + 'type': 'object' + 'description': 'Configuration to be checked' + 'properties': + 'dns': + '$ref': '#/components/schemas/CheckConfigRequestInfo' + 'web': + '$ref': '#/components/schemas/CheckConfigRequestInfo' + 'set_static_ip': + 'type': 'boolean' + 'example': false + 'CheckConfigRequestInfo': + 'type': 'object' + 'properties': + 'ip': + 'type': 'string' + 'example': '127.0.0.1' + 'port': + 'type': 'integer' + 'format': 'int32' + 'example': 53 + 'autofix': + 'type': 'boolean' + 'example': false + 'CheckConfigResponse': + 'type': 'object' + 'properties': + 'dns': + '$ref': '#/components/schemas/CheckConfigResponseInfo' + 'web': + '$ref': '#/components/schemas/CheckConfigResponseInfo' + 'static_ip': + '$ref': '#/components/schemas/CheckConfigStaticIpInfo' + 'CheckConfigResponseInfo': + 'type': 'object' + 'properties': + 'status': + 'type': 'string' + 'example': '' + 'can_autofix': + 'type': 'boolean' + 'example': false + 'CheckConfigStaticIpInfo': + 'type': 'object' + 'properties': + 'static': + 'type': 'string' + 'example': 'no' + 'description': 'Can be: yes, no, error' + 'ip': + 'type': 'string' + 'example': '192.168.1.1' + 'description': 'Current dynamic IP address. Set if static=no' + 'error': + 'type': 'string' + 'example': '' + 'description': 'Error text. Set if static=error' + 'InitialConfiguration': + 'type': 'object' + 'description': > + AdGuard Home initial configuration for the first-install wizard. + 'properties': + 'dns': + '$ref': '#/components/schemas/AddressInfo' + 'web': + '$ref': '#/components/schemas/AddressInfo' + 'username': + 'type': 'string' + 'description': 'Basic auth username' + 'example': 'admin' + 'password': + 'type': 'string' + 'description': 'Basic auth password' + 'example': 'password' + 'Login': + 'type': 'object' + 'description': 'Login request data' + 'properties': + 'username': + 'type': 'string' + 'description': 'User name' + 'password': + 'type': 'string' + 'description': 'Password' + 'Error': + 'description': 'A generic JSON error response.' + 'properties': + 'message': + 'description': 'The error message, an opaque string.' + 'type': 'string' + 'type': 'object' + 'securitySchemes': + 'basicAuth': + 'type': 'http' + 'scheme': 'basic' diff --git a/scripts/go-install-tools.sh b/scripts/go-install-tools.sh new file mode 100644 index 00000000..e75a54e4 --- /dev/null +++ b/scripts/go-install-tools.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +test "$VERBOSE" = '1' && set -x +set -e -f -u + +# TODO(a.garipov): Add goconst? + +env GOBIN="${PWD}/bin" "$GO" install --modfile=./internal/tools/go.mod\ + github.com/fzipp/gocyclo/cmd/gocyclo\ + github.com/golangci/misspell/cmd/misspell\ + github.com/gordonklaus/ineffassign\ + github.com/kisielk/errcheck\ + github.com/kyoh86/looppointer/cmd/looppointer\ + github.com/securego/gosec/v2/cmd/gosec\ + golang.org/x/lint/golint\ + golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness\ + golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow\ + honnef.co/go/tools/cmd/staticcheck\ + mvdan.cc/gofumpt\ + mvdan.cc/unparam\ + ; diff --git a/scripts/go-lint.sh b/scripts/go-lint.sh new file mode 100644 index 00000000..ca30e1e7 --- /dev/null +++ b/scripts/go-lint.sh @@ -0,0 +1,115 @@ +#!/bin/sh + +# Verbosity levels: +# 0 = Don't print anything except for errors. +# 1 = Print commands, but not nested commands. +# 2 = Print everything. +test "${VERBOSE:=0}" -gt '0' && set -x + +# Set $EXITONERROR to zero to see all errors. +test "${EXITONERROR:=1}" = '0' && set +e || set -e + +# We don't need glob expansions and we want to see errors about unset +# variables. +set -f -u + +not_found_msg=' +looks like a binary not found error. +make sure you have installed the linter binaries using: + + $ make go-install-tools +' + +not_found() { + if [ "$?" = '127' ] + then + # Code 127 is the exit status a shell uses when + # a command or a file is not found, according to the + # Bash Hackers wiki. + # + # See https://wiki.bash-hackers.org/dict/terms/exit_status. + echo "$not_found_msg" 1>&2 + fi +} +trap not_found EXIT + +# blocklist_imports is a simple check against unwanted packages. +# Currently it only looks for package log which is replaced by our own +# package github.com/AdguardTeam/golibs/log. +blocklist_imports() { + git grep -F -e '"log"' -- '*.go' || exit 0; +} + +# underscores is a simple check against Go filenames with underscores. +underscores() { + git ls-files '*_*.go' | { grep -F -e '_darwin.go' \ + -e '_freebsd.go' -e '_linux.go' -e '_others.go' \ + -e '_test.go' -e '_unix.go' -e '_windows.go' \ + -v || exit 0; } +} + +# exit_on_output exits with a nonzero exit code if there is anything in +# the command's combined output. +exit_on_output() { + test "$VERBOSE" -lt '2' && set +x + + cmd="$1" + shift + + exitcode='0' + output="$("$cmd" "$@" 2>&1)" + if [ "$output" != '' ] + then + if [ "$*" != '' ] + then + echo "combined output of '$cmd $@':" + else + echo "combined output of '$cmd':" + fi + + echo "$output" + + exitcode='1' + fi + + test "$VERBOSE" -gt '0' && set -x + + return "$exitcode" +} + +exit_on_output blocklist_imports + +exit_on_output underscores + +exit_on_output gofumpt --extra -l -s . + +golint --set_exit_status ./... + +"$GO" vet ./... + +gocyclo --over 20 . + +gosec --quiet . + +ineffassign . + +unparam ./... + +git ls-files -- '*.go' '*.md' '*.yaml' '*.yml' | xargs misspell --error + +looppointer ./... + +nilness ./... + +# TODO(a.garipov): Enable shadow after fixing all of the shadowing. +# shadow --strict ./... + +# TODO(a.garipov): Enable errcheck fully after handling all errors, +# including the deferred ones, properly. Also, perhaps, enable --blank. +# errcheck ./... +exit_on_output sh -c ' + errcheck --asserts ./... |\ + { grep -e "defer" -e "_test\.go:" -v || exit 0; } +' + +staticcheck ./... diff --git a/scripts/install.sh b/scripts/install.sh index b983322c..6f1fa85f 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -190,6 +190,7 @@ main() { SCRIPT_URL="https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh" URL="https://static.adguard.com/adguardhome/${CHANNEL}/${PKG_NAME}" OUT_DIR=/opt + AGH_DIR="${OUT_DIR}/AdGuardHome" # Root check if [ "$(id -u)" -eq 0 ]; then @@ -208,22 +209,22 @@ main() { fi fi - log_info "AdGuard Home will be installed to ${OUT_DIR}/AdGuardHome" + log_info "AdGuard Home will be installed to ${AGH_DIR}" - [ -d "${OUT_DIR}/AdGuardHome" ] && error_exit "Directory ${OUT_DIR}/AdGuardHome already exists, abort installation" + [ -d "${AGH_DIR}" ] && [ -n "$(ls -1 -A -q ${AGH_DIR})" ] && error_exit "Directory ${AGH_DIR} is not empty, abort installation" download "${URL}" "${PKG_NAME}" || error_exit "Cannot download the package" unpack "${PKG_NAME}" "${OUT_DIR}" "${PKG_EXT}" || error_exit "Cannot unpack the package" # Install AdGuard Home service and run it - ${OUT_DIR}/AdGuardHome/AdGuardHome -s install || error_exit "Cannot install AdGuardHome as a service" + ${AGH_DIR}/AdGuardHome -s install || error_exit "Cannot install AdGuardHome as a service" rm "${PKG_NAME}" log_info "AdGuard Home is now installed and running." log_info "You can control the service status with the following commands:" - log_info " sudo ${OUT_DIR}/AdGuardHome/AdGuardHome -s start|stop|restart|status|install|uninstall" + log_info " sudo ${AGH_DIR}/AdGuardHome -s start|stop|restart|status|install|uninstall" } main "$@" \ No newline at end of file diff --git a/scripts/querylog/anonymize.js b/scripts/querylog/anonymize.js index 3aed2877..ea2a3d39 100644 --- a/scripts/querylog/anonymize.js +++ b/scripts/querylog/anonymize.js @@ -2,11 +2,6 @@ const fs = require('fs'); const readline = require('readline'); const dnsPacket = require('dns-packet') -const decodeBase64 = (data) => { - let buff = new Buffer(data, 'base64'); - return buff.toString('ascii'); -} - const processLineByLine = async (source, callback) => { const fileStream = fs.createReadStream(source); diff --git a/scripts/translations/README.md b/scripts/translations/README.md index 2c8e5636..3a5e336c 100644 --- a/scripts/translations/README.md +++ b/scripts/translations/README.md @@ -1,4 +1,4 @@ -## Twosky intergration script +## Twosky integration script ### Usage diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 00000000..43639bf6 --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1,14 @@ +checks = ["all"] +initialisms = [ + # See https://github.com/dominikh/go-tools/blob/master/config/config.go. + "inherit" +, "DHCP" +, "DOH" +, "DOQ" +, "DOT" +, "EDNS" +, "QUIC" +, "SDNS" +] +dot_import_whitelist = [] +http_status_code_whitelist = []