Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home

This commit is contained in:
Andrey Meshkov 2020-12-21 19:34:23 +03:00
commit c62dd03921
155 changed files with 8637 additions and 5202 deletions

View File

@ -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

View File

@ -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;

32
.github/stale.yml vendored
View File

@ -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

View File

@ -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 }}'

View File

@ -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 }}'

50
.gitignore vendored
View File

@ -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/

View File

@ -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

View File

@ -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 }}'

View File

@ -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

View File

@ -9,17 +9,115 @@ and this project adheres to
## [Unreleased]
<!--
## [v0.105.0] - 2020-12-28
-->
### 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.105.0...HEAD
[v0.105.0]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.3...v0.105.0
-->
[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

View File

@ -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`.
* <https://github.com/golang/go/wiki/CodeReviewComments>.
## *Go*
* <https://github.com/golang/go/wiki/TestComments>.
> Not Golang, not GO, not GOLANG, not GoLang. It is Go in natural language,
> golang for others.
* <https://go-proverbs.github.io/>
— [@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
* <https://github.com/golang/go/wiki/CodeReviewComments>.
* <https://github.com/golang/go/wiki/TestComments>.
* <https://go-proverbs.github.io/>
## *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

View File

@ -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))

View File

@ -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)

View File

@ -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', () => {

View File

@ -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,
},
{

View File

@ -259,7 +259,7 @@ let Form = (props) => {
</div>
<div className="form__desc mt-0 mb-2">
<Trans components={[
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists#ctag"
<a target="_blank" rel="noopener noreferrer" href="https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists#ctag"
key="0">link</a>,
]}>
tags_desc

View File

@ -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 <div className="row dhcp__interfaces">
<div className="col col__dhcp">
<Field

View File

@ -113,9 +113,6 @@ const Dhcp = () => {
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))}
>
<Trans>{enabled ? 'dhcp_disable' : 'dhcp_enable'}</Trans>
</button>;

View File

@ -232,7 +232,7 @@ let Form = (props) => {
<Trans
values={{ link: 'letsencrypt.org' }}
components={[
<a href="https://letsencrypt.org/" key="0">
<a target="_blank" rel="noopener noreferrer" href="https://letsencrypt.org/" key="0">
link
</a>,
]}

View File

@ -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 href={FILTERS_LINK} rel="noopener noreferrer" />,
a: <a href={FILTERS_RELATIVE_LINK} rel="noopener noreferrer" />,
};
return (

View File

@ -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 (
<Topline type="danger">
<Trans components={[<a href="#encryption" key="0">link</a>]}>
topline_expired_certificate
</Trans>
</Topline>
);
if (expirationStateKey === EXPIRATION_ENUM.VALID) {
return null;
}
if (isAboutExpire) {
return (
<Topline type="warning">
const { toplineType, i18nKey } = EXPIRATION_STATE[expirationStateKey];
return (
<Topline type={toplineType}>
<Trans components={[<a href="#encryption" key="0">link</a>]}>
topline_expiring_certificate
{i18nKey}
</Trans>
</Topline>
);
}
return false;
);
};
export default EncryptionTopline;

View File

@ -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 }) => <li key={label}>
const renderMobileconfigInfo = ({ label, components, server_name }) => <li key={label}>
<Trans components={components}>{label}</Trans>
<ul>
<li>
<a href={MOBILE_CONFIG_LINKS.DOT} download>{i18next.t('download_mobileconfig_dot')}</a>
<a href={getPathWithQueryString(MOBILE_CONFIG_LINKS.DOT, { host: server_name })}
download>{i18next.t('download_mobileconfig_dot')}</a>
</li>
<li>
<a href={MOBILE_CONFIG_LINKS.DOH} download>{i18next.t('download_mobileconfig_doh')}</a>
<a href={getPathWithQueryString(MOBILE_CONFIG_LINKS.DOH, { host: server_name })}
download>{i18next.t('download_mobileconfig_doh')}</a>
</li>
</ul>
</li>;
@ -38,37 +41,8 @@ const renderLi = ({ label, components }) => <li key={label}>
</Trans>
</li>;
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',
},
<code key="1">text</code>,
],
},
{
label: 'setup_dns_privacy_android_3',
components: [
{
key: 0,
href: 'https://getintra.org/',
},
<code key="1">text</code>,
],
},
],
},
{
title: 'iOS',
list: [
const getDnsPrivacyList = (server_name) => {
const iosList = [
{
label: 'setup_dns_privacy_ios_2',
components: [
@ -79,13 +53,6 @@ const dnsPrivacyList = [{
<code key="1">text</code>,
],
},
{
label: 'setup_dns_privacy_4',
components: {
highlight: <code />,
},
renderComponent: renderMobileconfigInfo,
},
{
label: 'setup_dns_privacy_ios_1',
components: [
@ -93,68 +60,114 @@ const dnsPrivacyList = [{
key: 0,
href: 'https://itunes.apple.com/app/id1452162351',
},
<code key="1">text</code>,
{
key: 2,
href: 'https://dnscrypt.info/stamps',
},
<code key="1">text</code>,
{
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: <code />,
},
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',
},
<code key="1">text</code>,
],
},
{
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/',
},
<code key="1">text</code>,
],
},
{
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',
},
<code key="1">text</code>,
],
},
{
label: 'setup_dns_privacy_other_4',
components: [
{
key: 0,
href: 'https://support.mozilla.org/kb/firefox-dns-over-https',
},
<code key="1">text</code>,
],
},
{
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 }) => <div className="tab__paragraph" key={title}>
<strong><Trans>{title}</Trans></strong>
@ -172,6 +185,7 @@ const getTabs = ({
tlsAddress,
httpsAddress,
showDnsPrivacyNotice,
server_name,
t,
}) => ({
Router: {
@ -277,7 +291,7 @@ const getTabs = ({
setup_dns_privacy_3
</Trans>
</div>
{dnsPrivacyList.map(renderDnsPrivacyList)}
{getDnsPrivacyList(server_name).map(renderDnsPrivacyList)}
</>}
</div>
</div>;
@ -299,6 +313,7 @@ const renderContent = ({ title, list, getTitle }) => <div key={title} label={i18
const Guide = ({ dnsAddresses }) => {
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,
});

View File

@ -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;

View File

@ -687,7 +687,7 @@ export const sortIp = (a, b) => {
return 0;
} catch (e) {
console.error(e);
console.warn(e);
return 0;
}
};

View File

@ -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)

View File

@ -12,7 +12,7 @@ const renderItem = ({
return <li key={ip}>{isDns
? <strong>{dnsAddress}</strong>
: <a href={webAddress}>{webAddress}</a>
: <a href={webAddress} target="_blank" rel="noopener noreferrer">{webAddress}</a>
}
</li>;
};

View File

@ -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;

37
go.mod
View File

@ -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
)

328
go.sum
View File

@ -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=

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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())
})
}
}

View File

@ -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
}
}

View File

@ -74,7 +74,6 @@ func (s *Server) dbLoad() {
} else {
v6DynLeases = append(v6DynLeases, &lease)
}
} else {
if obj[i].Expiry == leaseExpireStatic {
staticLeases = append(staticLeases, &lease)

View File

@ -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{}

View File

@ -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"`

View File

@ -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) {

View File

@ -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}),
},
},

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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

123
internal/dhcpd/v46.go Normal file
View File

@ -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
}

189
internal/dhcpd/v46_test.go Normal file
View File

@ -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))
})
}
}

View File

@ -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 }

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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,
}
}

View File

@ -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)
})
}

View File

@ -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)

View File

@ -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()

View File

@ -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
}
}

View File

@ -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)

View File

@ -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",

View File

@ -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^",

View File

@ -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")

View File

@ -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:

View File

@ -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

View File

@ -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))
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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\":[\"\"]}",

View File

@ -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 {

View File

@ -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

View File

@ -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
}
}

View File

@ -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 /

View File

@ -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

View File

@ -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))

View File

@ -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++
}

View File

@ -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) {

View File

@ -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)

View File

@ -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"`

View File

@ -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/") {

View File

@ -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)

View File

@ -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)))
}

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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, "<html") || strings.Contains(s, "<!doctype") {
return false, fmt.Errorf("data is HTML, not plain text")
return total, fmt.Errorf("data is HTML, not plain text")
}
htmlTest = false
@ -571,17 +533,70 @@ func (f *Filtering) updateIntl(filter *filter) (bool, error) {
_, err2 := tmpFile.Write(buf[:n])
if err2 != nil {
return false, err2
return total, err2
}
if err == io.EOF {
break
return total, nil
}
if err != nil {
log.Printf("Couldn't fetch filter contents from URL %s, skipping: %s", filter.URL, err)
return false, err
return total, err
}
}
}
// updateIntl returns true if filter update performed successfully.
func (f *Filtering) updateIntl(filter *filter) (updated bool, err error) {
updated = false
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 updated, err
}
defer func() {
if tmpFile != nil {
if err := tmpFile.Close(); err != nil {
log.Printf("Couldn't close temporary file: %s", err)
}
tmpFileName := tmpFile.Name()
if err := os.Remove(tmpFileName); err != nil {
log.Printf("Couldn't delete temporary file %s: %s", tmpFileName, err)
}
}
}()
var reader io.Reader
if filepath.IsAbs(filter.URL) {
f, err := os.Open(filter.URL)
if err != nil {
return updated, 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 updated, err
}
if resp.StatusCode != http.StatusOK {
log.Printf("Got status code %d from URL %s, skipping", resp.StatusCode, filter.URL)
return updated, fmt.Errorf("got status code != 200: %d", resp.StatusCode)
}
reader = resp.Body
}
total, err := f.read(reader, tmpFile, filter)
if err != nil {
return updated, err
}
// Extract filter name and count number of rules
_, _ = tmpFile.Seek(0, io.SeekStart)
@ -589,7 +604,7 @@ func (f *Filtering) updateIntl(filter *filter) (bool, error) {
// Check if the filter has been really changed
if filter.checksum == checksum {
log.Tracef("Filter #%d at URL %s hasn't changed, not updating it", filter.ID, filter.URL)
return false, nil
return updated, nil
}
log.Printf("Filter %d has been updated: %d bytes, %d rules",
@ -606,11 +621,12 @@ func (f *Filtering) updateIntl(filter *filter) (bool, error) {
_ = tmpFile.Close()
err = os.Rename(tmpFile.Name(), filterFilePath)
if err != nil {
return false, err
return updated, err
}
tmpFile = nil
updated = true
return true, nil
return updated, nil
}
// loads filter contents from the file in dataDir

View File

@ -12,7 +12,8 @@ import (
)
func testStartFilterListener() net.Listener {
http.HandleFunc("/filters/1.txt", func(w http.ResponseWriter, r *http.Request) {
mux := http.NewServeMux()
mux.HandleFunc("/filters/1.txt", func(w http.ResponseWriter, r *http.Request) {
content := `||example.org^$third-party
# Inline comment example
||example.com^$third-party
@ -26,7 +27,7 @@ func testStartFilterListener() net.Listener {
panic(err)
}
go func() { _ = http.Serve(listener, nil) }()
go func() { _ = http.Serve(listener, mux) }()
return listener
}

View File

@ -26,6 +26,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/AdGuardHome/internal/sysutil"
"github.com/AdguardTeam/AdGuardHome/internal/update"
"github.com/AdguardTeam/AdGuardHome/internal/util"
"github.com/AdguardTeam/golibs/log"
@ -43,6 +44,7 @@ var (
updateChannel = "none"
versionCheckURL = ""
ARMVersion = ""
MIPSVersion = ""
)
// Global context
@ -56,7 +58,7 @@ type homeContext struct {
dnsServer *dnsforward.Server // DNS module
rdns *RDNS // rDNS module
whois *Whois // WHOIS module
dnsFilter *dnsfilter.Dnsfilter // DNS filtering module
dnsFilter *dnsfilter.DNSFilter // DNS filtering module
dhcpServer *dhcpd.Server // DHCP module
auth *Auth // HTTP authentication module
filters Filtering // DNS filtering module
@ -65,6 +67,11 @@ type homeContext struct {
autoHosts util.AutoHosts // IP-hostname pairs taken from system configuration (e.g. /etc/hosts) files
updater *update.Updater
ipDetector *ipDetector
// mux is our custom http.ServeMux.
mux *http.ServeMux
// Runtime properties
// --
@ -92,11 +99,12 @@ func (c *homeContext) getDataDir() string {
var Context homeContext
// Main is the entry point
func Main(version, channel, armVer string) {
func Main(version, channel, armVer, mipsVer string) {
// Init update-related global variables
versionString = version
updateChannel = channel
ARMVersion = armVer
MIPSVersion = mipsVer
versionCheckURL = "https://static.adguard.com/adguardhome/" + updateChannel + "/version.json"
// config can be specified, which reads options from there, but other command line flags have to override config values
@ -133,35 +141,19 @@ func Main(version, channel, armVer string) {
// version - returns the current version string
func version() string {
// TODO(a.garipov): I'm pretty sure we can extract some of this stuff
// from the build info.
msg := "AdGuard Home, version %s, channel %s, arch %s %s"
if ARMVersion != "" {
msg = msg + " v" + ARMVersion
} else if MIPSVersion != "" {
msg = msg + " " + MIPSVersion
}
return fmt.Sprintf(msg, versionString, updateChannel, runtime.GOOS, runtime.GOARCH)
}
// run initializes configuration and runs the AdGuard Home
// run is a blocking method!
// nolint
func run(args options) {
// configure config filename
initConfigFilename(args)
// configure working dir and config path
initWorkingDir(args)
// configure log level and output
configureLogger(args)
// Go memory hacks
memoryUsage(args)
// print the first message after logger is configured
log.Println(version())
log.Debug("Current working directory is %s", Context.workDir)
if args.runningAsService {
log.Info("AdGuard Home is running as a service")
}
func setupContext(args options) {
Context.runningAsService = args.runningAsService
Context.disableUpdate = args.disableUpdate
@ -179,7 +171,8 @@ func run(args options) {
DialContext: customDialContext,
Proxy: getHTTPProxy,
TLSClientConfig: &tls.Config{
RootCAs: Context.tlsRoots,
RootCAs: Context.tlsRoots,
MinVersion: tls.VersionTLS12,
},
}
Context.client = &http.Client{
@ -206,11 +199,10 @@ func run(args options) {
}
}
// 'clients' module uses 'dnsfilter' module's static data (dnsfilter.BlockedSvcKnown()),
// so we have to initialize dnsfilter's static data first,
// but also avoid relying on automatic Go init() function
dnsfilter.InitModule()
Context.mux = http.NewServeMux()
}
func setupConfig(args options) {
config.DHCP.WorkDir = Context.workDir
config.DHCP.HTTPRegister = httpRegister
config.DHCP.ConfigModified = onConfigModified
@ -238,7 +230,7 @@ func run(args options) {
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
config.RlimitNoFile != 0 {
util.SetRlimit(config.RlimitNoFile)
sysutil.SetRlimit(config.RlimitNoFile)
}
// override bind host/port from the console
@ -251,6 +243,37 @@ func run(args options) {
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
Context.pidFileName = args.pidFile
}
}
// run performs configurating and starts AdGuard Home.
func run(args options) {
// configure config filename
initConfigFilename(args)
// configure working dir and config path
initWorkingDir(args)
// configure log level and output
configureLogger(args)
// Go memory hacks
memoryUsage(args)
// print the first message after logger is configured
log.Println(version())
log.Debug("Current working directory is %s", Context.workDir)
if args.runningAsService {
log.Info("AdGuard Home is running as a service")
}
setupContext(args)
// clients package uses dnsfilter package's static data (dnsfilter.BlockedSvcKnown()),
// so we have to initialize dnsfilter's static data first,
// but also avoid relying on automatic Go init() function
dnsfilter.InitModule()
setupConfig(args)
if !Context.firstRun {
// Save the updated config
@ -292,10 +315,14 @@ func run(args options) {
log.Fatalf("Can't initialize TLS module")
}
webConf := WebConfig{
webConf := webConfig{
firstRun: Context.firstRun,
BindHost: config.BindHost,
BindPort: config.BindPort,
ReadTimeout: ReadTimeout,
ReadHeaderTimeout: ReadHeaderTimeout,
WriteTimeout: WriteTimeout,
}
Context.web = CreateWeb(&webConf)
if Context.web == nil {
@ -322,6 +349,11 @@ func run(args options) {
}
}
Context.ipDetector, err = newIPDetector()
if err != nil {
log.Fatal(err)
}
Context.web.Start()
// wait indefinitely for other go-routines to complete their job
@ -352,7 +384,7 @@ func checkPermissions() {
if runtime.GOOS == "windows" {
// On Windows we need to have admin rights to run properly
admin, _ := util.HaveAdminRights()
admin, _ := sysutil.HaveAdminRights()
if admin {
return
}
@ -469,7 +501,7 @@ func configureLogger(args options) {
if ls.LogFile == configSyslog {
// Use syslog where it is possible and eventlog on Windows
err := util.ConfigureSyslog(serviceName)
err := sysutil.ConfigureSyslog(serviceName)
if err != nil {
log.Fatalf("cannot initialize syslog: %s", err)
}
@ -659,3 +691,12 @@ func getHTTPProxy(req *http.Request) (*url.URL, error) {
}
return url.Parse(config.ProxyURL)
}
// jsonError is a generic JSON error response.
//
// TODO(a.garipov): Merge together with the implementations in .../dhcpd and
// other packages after refactoring the web handler registering.
type jsonError struct {
// Message is the error message, an opaque string.
Message string `json:"message"`
}

View File

@ -119,7 +119,7 @@ func TestHome(t *testing.T) {
fn := filepath.Join(dir, "AdGuardHome.yaml")
// Prepare the test config
assert.True(t, ioutil.WriteFile(fn, []byte(yamlConf), 0644) == nil)
assert.True(t, ioutil.WriteFile(fn, []byte(yamlConf), 0o644) == nil)
fn, _ = filepath.Abs(fn)
config = configuration{} // the global variable is dirty because of the previous tests run

View File

@ -66,6 +66,7 @@ func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
}
func handleI18nChangeLanguage(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)

View File

@ -0,0 +1,72 @@
package home
import "net"
// ipDetector describes IP address properties.
type ipDetector struct {
nets []*net.IPNet
}
// newIPDetector returns a new IP detector.
func newIPDetector() (ipd *ipDetector, err error) {
specialNetworks := []string{
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.0.0/29",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"240.0.0.0/4",
"255.255.255.255/32",
"::1/128",
"::/128",
"64:ff9b::/96",
// Since this network is used for mapping IPv4 addresses, we
// don't include it.
// "::ffff:0:0/96",
"100::/64",
"2001::/23",
"2001::/32",
"2001:2::/48",
"2001:db8::/32",
"2001:10::/28",
"2002::/16",
"fc00::/7",
"fe80::/10",
}
ipd = &ipDetector{
nets: make([]*net.IPNet, len(specialNetworks)),
}
for i, ipnetStr := range specialNetworks {
_, ipnet, err := net.ParseCIDR(ipnetStr)
if err != nil {
return nil, err
}
ipd.nets[i] = ipnet
}
return ipd, nil
}
// detectSpecialNetwork returns true if IP address is contained by any of
// special-purpose IP address registries according to RFC-6890
// (https://tools.ietf.org/html/rfc6890).
func (ipd *ipDetector) detectSpecialNetwork(ip net.IP) bool {
for _, ipnet := range ipd.nets {
if ipnet.Contains(ip) {
return true
}
}
return false
}

View File

@ -0,0 +1,146 @@
package home
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIPDetector_detectSpecialNetwork(t *testing.T) {
var ipd *ipDetector
t.Run("newIPDetector", func(t *testing.T) {
var err error
ipd, err = newIPDetector()
assert.Nil(t, err)
})
testCases := []struct {
name string
ip net.IP
want bool
}{{
name: "not_specific",
ip: net.ParseIP("8.8.8.8"),
want: false,
}, {
name: "this_host_on_this_network",
ip: net.ParseIP("0.0.0.0"),
want: true,
}, {
name: "private-Use",
ip: net.ParseIP("10.0.0.0"),
want: true,
}, {
name: "shared_address_space",
ip: net.ParseIP("100.64.0.0"),
want: true,
}, {
name: "loopback",
ip: net.ParseIP("127.0.0.0"),
want: true,
}, {
name: "link_local",
ip: net.ParseIP("169.254.0.0"),
want: true,
}, {
name: "private-use",
ip: net.ParseIP("172.16.0.0"),
want: true,
}, {
name: "ietf_protocol_assignments",
ip: net.ParseIP("192.0.0.0"),
want: true,
}, {
name: "ds-lite",
ip: net.ParseIP("192.0.0.0"),
want: true,
}, {
name: "documentation_(test-net-1)",
ip: net.ParseIP("192.0.2.0"),
want: true,
}, {
name: "6to4_relay_anycast",
ip: net.ParseIP("192.88.99.0"),
want: true,
}, {
name: "private-use",
ip: net.ParseIP("192.168.0.0"),
want: true,
}, {
name: "benchmarking",
ip: net.ParseIP("198.18.0.0"),
want: true,
}, {
name: "documentation_(test-net-2)",
ip: net.ParseIP("198.51.100.0"),
want: true,
}, {
name: "documentation_(test-net-3)",
ip: net.ParseIP("203.0.113.0"),
want: true,
}, {
name: "reserved",
ip: net.ParseIP("240.0.0.0"),
want: true,
}, {
name: "limited_broadcast",
ip: net.ParseIP("255.255.255.255"),
want: true,
}, {
name: "loopback_address",
ip: net.ParseIP("::1"),
want: true,
}, {
name: "unspecified_address",
ip: net.ParseIP("::"),
want: true,
}, {
name: "ipv4-ipv6_translation",
ip: net.ParseIP("64:ff9b::"),
want: true,
}, {
name: "discard-only_address_block",
ip: net.ParseIP("100::"),
want: true,
}, {
name: "ietf_protocol_assignments",
ip: net.ParseIP("2001::"),
want: true,
}, {
name: "teredo",
ip: net.ParseIP("2001::"),
want: true,
}, {
name: "benchmarking",
ip: net.ParseIP("2001:2::"),
want: true,
}, {
name: "documentation",
ip: net.ParseIP("2001:db8::"),
want: true,
}, {
name: "orchid",
ip: net.ParseIP("2001:10::"),
want: true,
}, {
name: "6to4",
ip: net.ParseIP("2002::"),
want: true,
}, {
name: "unique-local",
ip: net.ParseIP("fc00::"),
want: true,
}, {
name: "linked-scoped_unicast",
ip: net.ParseIP("fe80::"),
want: true,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, ipd.detectSpecialNetwork(tc.ip))
})
}
}

View File

@ -0,0 +1,42 @@
package home
import (
"net/http"
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/AdguardTeam/golibs/log"
)
// middlerware is a wrapper function signature.
type middleware func(http.Handler) http.Handler
// withMiddlewares consequently wraps h with all the middlewares.
func withMiddlewares(h http.Handler, middlewares ...middleware) (wrapped http.Handler) {
wrapped = h
for _, mw := range middlewares {
wrapped = mw(wrapped)
}
return wrapped
}
// RequestBodySizeLimit is maximum request body length in bytes.
const RequestBodySizeLimit = 64 * 1024
// limitRequestBody wraps underlying handler h, making it's request's body Read
// method limited.
func limitRequestBody(h http.Handler) (limited http.Handler) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
r.Body, err = aghio.LimitReadCloser(r.Body, RequestBodySizeLimit)
if err != nil {
log.Error("limitRequestBody: %s", err)
return
}
h.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,64 @@
package home
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/stretchr/testify/assert"
)
func TestLimitRequestBody(t *testing.T) {
errReqLimitReached := &aghio.LimitReachedError{
Limit: RequestBodySizeLimit,
}
testCases := []struct {
name string
body string
want []byte
wantErr error
}{{
name: "not_so_big",
body: "somestr",
want: []byte("somestr"),
wantErr: nil,
}, {
name: "so_big",
body: string(make([]byte, RequestBodySizeLimit+1)),
want: make([]byte, RequestBodySizeLimit),
wantErr: errReqLimitReached,
}, {
name: "empty",
body: "",
want: []byte(nil),
wantErr: nil,
}}
makeHandler := func(err *error) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var b []byte
b, *err = ioutil.ReadAll(r.Body)
w.Write(b)
})
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var err error
handler := makeHandler(&err)
lim := limitRequestBody(handler)
req := httptest.NewRequest(http.MethodPost, "https://www.example.com", strings.NewReader(tc.body))
res := httptest.NewRecorder()
lim.ServeHTTP(res, req)
assert.Equal(t, tc.want, res.Body.Bytes())
assert.Equal(t, tc.wantErr, err)
})
}
}

View File

@ -1,21 +1,22 @@
package home
import (
"encoding/json"
"fmt"
"net"
"net/http"
"github.com/AdguardTeam/golibs/log"
uuid "github.com/satori/go.uuid"
"howett.net/plist"
)
type DNSSettings struct {
type dnsSettings struct {
DNSProtocol string
ServerURL string `plist:",omitempty"`
ServerName string `plist:",omitempty"`
}
type PayloadContent struct {
type payloadContent struct {
Name string
PayloadDescription string
PayloadDisplayName string
@ -23,11 +24,11 @@ type PayloadContent struct {
PayloadType string
PayloadUUID string
PayloadVersion int
DNSSettings DNSSettings
DNSSettings dnsSettings
}
type MobileConfig struct {
PayloadContent []PayloadContent
type mobileConfig struct {
PayloadContent []payloadContent
PayloadDescription string
PayloadDisplayName string
PayloadIdentifier string
@ -46,19 +47,20 @@ const (
dnsProtoTLS = "TLS"
)
func getMobileConfig(d DNSSettings) ([]byte, error) {
func getMobileConfig(d dnsSettings) ([]byte, error) {
var name string
switch d.DNSProtocol {
case dnsProtoHTTPS:
name = fmt.Sprintf("%s DoH", d.ServerName)
d.ServerURL = fmt.Sprintf("https://%s/dns-query", d.ServerName)
case dnsProtoTLS:
name = fmt.Sprintf("%s DoT", d.ServerName)
default:
return nil, fmt.Errorf("bad dns protocol %q", d.DNSProtocol)
}
data := MobileConfig{
PayloadContent: []PayloadContent{{
data := mobileConfig{
PayloadContent: []payloadContent{{
Name: name,
PayloadDescription: "Configures device to use AdGuard Home",
PayloadDisplayName: name,
@ -80,34 +82,46 @@ func getMobileConfig(d DNSSettings) ([]byte, error) {
return plist.MarshalIndent(data, plist.XMLFormat, "\t")
}
func handleMobileConfig(w http.ResponseWriter, d DNSSettings) {
func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
host := r.URL.Query().Get("host")
if host == "" {
host = Context.tls.conf.ServerName
}
if host == "" {
w.WriteHeader(http.StatusInternalServerError)
const msg = "no host in query parameters and no server_name"
err := json.NewEncoder(w).Encode(&jsonError{
Message: msg,
})
if err != nil {
log.Debug("writing 500 json response: %s", err)
}
return
}
d := dnsSettings{
DNSProtocol: dnsp,
ServerName: host,
}
mobileconfig, err := getMobileConfig(d)
if err != nil {
httpError(w, http.StatusInternalServerError, "plist.MarshalIndent: %s", err)
return
}
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write(mobileconfig)
}
func handleMobileConfigDoh(w http.ResponseWriter, r *http.Request) {
handleMobileConfig(w, DNSSettings{
DNSProtocol: dnsProtoHTTPS,
ServerURL: fmt.Sprintf("https://%s/dns-query", r.Host),
})
func handleMobileConfigDOH(w http.ResponseWriter, r *http.Request) {
handleMobileConfig(w, r, dnsProtoHTTPS)
}
func handleMobileConfigDot(w http.ResponseWriter, r *http.Request) {
var err error
var host string
host, _, err = net.SplitHostPort(r.Host)
if err != nil {
httpError(w, http.StatusBadRequest, "getting host: %s", err)
}
handleMobileConfig(w, DNSSettings{
DNSProtocol: dnsProtoTLS,
ServerName: host,
})
func handleMobileConfigDOT(w http.ResponseWriter, r *http.Request) {
handleMobileConfig(w, r, dnsProtoTLS)
}

View File

@ -9,25 +9,132 @@ import (
"howett.net/plist"
)
func TestHandleMobileConfigDot(t *testing.T) {
var err error
func TestHandleMobileConfigDOH(t *testing.T) {
t.Run("success", func(t *testing.T) {
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig?host=example.org", nil)
assert.Nil(t, err)
var r *http.Request
r, err = http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig", nil)
assert.Nil(t, err)
w := httptest.NewRecorder()
w := httptest.NewRecorder()
handleMobileConfigDOH(w, r)
assert.Equal(t, http.StatusOK, w.Code)
handleMobileConfigDot(w, r)
assert.Equal(t, http.StatusOK, w.Code)
var mc mobileConfig
_, err = plist.Unmarshal(w.Body.Bytes(), &mc)
assert.Nil(t, err)
var mc MobileConfig
_, err = plist.Unmarshal(w.Body.Bytes(), &mc)
assert.Nil(t, err)
if assert.Equal(t, 1, len(mc.PayloadContent)) {
assert.Equal(t, "example.org DoH", mc.PayloadContent[0].Name)
assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName)
assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName)
assert.Equal(t, "https://example.org/dns-query", mc.PayloadContent[0].DNSSettings.ServerURL)
}
})
if assert.Equal(t, 1, len(mc.PayloadContent)) {
assert.Equal(t, "example.com DoT", mc.PayloadContent[0].Name)
assert.Equal(t, "example.com DoT", mc.PayloadContent[0].PayloadDisplayName)
assert.Equal(t, "example.com", mc.PayloadContent[0].DNSSettings.ServerName)
}
t.Run("success_no_host", func(t *testing.T) {
oldTLSConf := Context.tls
t.Cleanup(func() { Context.tls = oldTLSConf })
Context.tls = &TLSMod{
conf: tlsConfigSettings{ServerName: "example.org"},
}
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig", nil)
assert.Nil(t, err)
w := httptest.NewRecorder()
handleMobileConfigDOH(w, r)
assert.Equal(t, http.StatusOK, w.Code)
var mc mobileConfig
_, err = plist.Unmarshal(w.Body.Bytes(), &mc)
assert.Nil(t, err)
if assert.Equal(t, 1, len(mc.PayloadContent)) {
assert.Equal(t, "example.org DoH", mc.PayloadContent[0].Name)
assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName)
assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName)
assert.Equal(t, "https://example.org/dns-query", mc.PayloadContent[0].DNSSettings.ServerURL)
}
})
t.Run("error_no_host", func(t *testing.T) {
oldTLSConf := Context.tls
t.Cleanup(func() { Context.tls = oldTLSConf })
Context.tls = &TLSMod{conf: tlsConfigSettings{}}
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig", nil)
assert.Nil(t, err)
w := httptest.NewRecorder()
handleMobileConfigDOH(w, r)
assert.Equal(t, http.StatusInternalServerError, w.Code)
})
}
func TestHandleMobileConfigDOT(t *testing.T) {
t.Run("success", func(t *testing.T) {
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig?host=example.org", nil)
assert.Nil(t, err)
w := httptest.NewRecorder()
handleMobileConfigDOT(w, r)
assert.Equal(t, http.StatusOK, w.Code)
var mc mobileConfig
_, err = plist.Unmarshal(w.Body.Bytes(), &mc)
assert.Nil(t, err)
if assert.Equal(t, 1, len(mc.PayloadContent)) {
assert.Equal(t, "example.org DoT", mc.PayloadContent[0].Name)
assert.Equal(t, "example.org DoT", mc.PayloadContent[0].PayloadDisplayName)
assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName)
}
})
t.Run("success_no_host", func(t *testing.T) {
oldTLSConf := Context.tls
t.Cleanup(func() { Context.tls = oldTLSConf })
Context.tls = &TLSMod{
conf: tlsConfigSettings{ServerName: "example.org"},
}
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig", nil)
assert.Nil(t, err)
w := httptest.NewRecorder()
handleMobileConfigDOT(w, r)
assert.Equal(t, http.StatusOK, w.Code)
var mc mobileConfig
_, err = plist.Unmarshal(w.Body.Bytes(), &mc)
assert.Nil(t, err)
if assert.Equal(t, 1, len(mc.PayloadContent)) {
assert.Equal(t, "example.org DoT", mc.PayloadContent[0].Name)
assert.Equal(t, "example.org DoT", mc.PayloadContent[0].PayloadDisplayName)
assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName)
}
})
t.Run("error_no_host", func(t *testing.T) {
oldTLSConf := Context.tls
t.Cleanup(func() { Context.tls = oldTLSConf })
Context.tls = &TLSMod{conf: tlsConfigSettings{}}
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig", nil)
assert.Nil(t, err)
w := httptest.NewRecorder()
handleMobileConfigDOT(w, r)
assert.Equal(t, http.StatusInternalServerError, w.Code)
})
}

View File

@ -9,6 +9,7 @@ import (
"strings"
"syscall"
"github.com/AdguardTeam/AdGuardHome/internal/sysutil"
"github.com/AdguardTeam/AdGuardHome/internal/util"
"github.com/AdguardTeam/golibs/log"
"github.com/kardianos/service"
@ -109,7 +110,7 @@ func sendSigReload() {
log.Error("Can't read PID file %s: %s", pidfile, err)
return
}
err = util.SendProcessSignal(pid, syscall.SIGHUP)
err = sysutil.SendProcessSignal(pid, syscall.SIGHUP)
if err != nil {
log.Error("Can't send signal to PID %d: %s", pid, err)
return

Some files were not shown because too many files have changed in this diff Show More