diff --git a/.circleci/config.yml b/.circleci/config.yml index 71793621c5..117ac3f412 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,16 +13,16 @@ initWorkingDir: &initWorkingDir GOROOT=$(go env GOROOT) sudo rm -r $(go env GOROOT) sudo mkdir $GOROOT - LATEST=$(curl -s https://golang.org/VERSION?m=text) + LATEST=$(curl -s https://go.dev/VERSION?m=text | grep -v time) curl https://dl.google.com/go/${LATEST}.linux-amd64.tar.gz | sudo tar xz -C $GOROOT --strip-components=1 integrationDefaults: &integrationDefaults machine: - image: ubuntu-1604:201903-01 + image: default working_directory: ~/go/src/${CIRCLE_PROJECT_USERNAME}/coredns environment: - - K8S_VERSION: v1.19.1 - - KIND_VERSION: v0.9.0 + - K8S_VERSION: v1.29.4 + - KIND_VERSION: v0.25.0 - KUBECONFIG: /home/circleci/.kube/kind-config-kind setupKubernetes: &setupKubernetes @@ -36,7 +36,7 @@ buildCoreDNSImage: &buildCoreDNSImage command: | cd ~/go/src/${CIRCLE_PROJECT_USERNAME}/coredns make coredns SYSTEM="GOOS=linux" && \ - docker build -t coredns . && \ + DOCKER_BUILDKIT=1 docker build -t coredns . && \ kind load docker-image coredns jobs: @@ -47,7 +47,7 @@ jobs: - checkout - run: name: Get CI repo - command : | + command: | mkdir -p ~/go/src/${CIRCLE_PROJECT_USERNAME}/ci git clone https://github.com/${CIRCLE_PROJECT_USERNAME}/ci ~/go/src/${CIRCLE_PROJECT_USERNAME}/ci - <<: *setupKubernetes @@ -56,7 +56,7 @@ jobs: name: Run Kubernetes tests command: | cd ~/go/src/${CIRCLE_PROJECT_USERNAME}/ci/test/kubernetes - go mod download + go mod tidy go test -v ./... workflows: diff --git a/.dreck.yaml b/.dreck.yaml index 12c8a4c4be..3528962391 100644 --- a/.dreck.yaml +++ b/.dreck.yaml @@ -9,3 +9,5 @@ aliases: /wai -> /label works as intended - | /release (.*) -> /exec /opt/bin/release-coredns $1 + - | + /docker (.*) -> /exec /opt/bin/docker-coredns $1 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 09ad9ede8b..97edaebb4d 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -40,7 +40,7 @@ issue first and "claim" it and get feedback before you invest a lot of time. **If someone already opened a pull request, but you think the pull request has stalled and you would like to open another pull request for the same or similar feature, get some of the maintainers (see -[CODEOWNERS](CODEOWNERS)) involved to resolve the situation and move things forward.** +[CODEOWNERS](../CODEOWNERS)) involved to resolve the situation and move things forward.** If possible make a pull request as small as possible, or submit multiple pull request to complete a feature. Smaller means: easier to understand and review. This in turn means things can be merged diff --git a/.github/SECURITY.md b/.github/SECURITY.md index fd8cc5bfa7..9a09a537db 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -24,7 +24,7 @@ The initial Product Security Team will consist of the set of maintainers that vo ### Private Disclosure Processes If you find a security vulnerability or any security related issues, -please DO NOT file a public issue. Do not create a Github issue. +please DO NOT file a public issue. Do not create a GitHub issue. Instead, send your report privately to security@coredns.io. Security reports are greatly appreciated and we will publicly thank you for it. @@ -94,7 +94,7 @@ so that a realistic timeline can be communicated to users. **Disclosure of Forthcoming Fix to Users** (Completed within 1-7 days of Disclosure) -- The Fix Lead will create a github issue in CoreDNS project to inform users that a security vulnerability +- The Fix Lead will create a GitHub issue in CoreDNS project to inform users that a security vulnerability has been disclosed and that a fix will be made available, with an estimation of the Release Date. It will include any mitigating steps users can take until a fix is available. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..a13a068d51 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 + +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + groups: + k8s.io: + patterns: + - "k8s.io/*" + go.etcd.io: + patterns: + - "go.etcd.io/etcd/*" + open-pull-requests-limit: 20 + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 20 diff --git a/.github/fixup_file_mtime.sh b/.github/fixup_file_mtime.sh index 662c16dbd4..af401a510a 100755 --- a/.github/fixup_file_mtime.sh +++ b/.github/fixup_file_mtime.sh @@ -9,7 +9,7 @@ if [[ ! -f 'coredns.1.md' ]]; then exit 1 fi -for file in coredns.1.md corefile.5.md plugin/*/README.md; do +for file in coredns.1.md corefile.5.md plugin/*/README.md man/*.1 man/*.5 man/*.7; do time=$(git log --pretty=format:%cd -n 1 --date='format:%Y%m%d%H%M.%S' "${file}") touch -m -t "${time}" "${file}" done diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml new file mode 100644 index 0000000000..c9397fc8ae --- /dev/null +++ b/.github/workflows/cifuzz.yml @@ -0,0 +1,32 @@ +name: CIFuzz + +on: + pull_request: + branches: + - master + +permissions: + contents: read + +jobs: + Fuzzing: + runs-on: ubuntu-latest + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master # cifuzz can't be pinned https://github.com/google/oss-fuzz/issues/6836 + with: + oss-fuzz-project-name: "go-coredns" + dry-run: false + - name: Run Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master # cifuzz can't be pinned + with: + oss-fuzz-project-name: "go-coredns" + fuzz-seconds: 600 + dry-run: false + - name: Upload Crash + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 524161be86..40fb9d991c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -8,8 +8,15 @@ on: schedule: - cron: '22 10 * * 4' +permissions: + contents: read + jobs: analyze: + permissions: + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/autobuild to send a status report name: Analyze runs-on: ubuntu-latest @@ -19,16 +26,16 @@ jobs: language: [ 'go' ] steps: - - name: Checkout repository - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} + - name: Initialize CodeQL + uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 + with: + languages: ${{ matrix.language }} - - name: Autobuild - uses: github/codeql-action/autobuild@v1 + - name: Autobuild + uses: github/codeql-action/autobuild@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 diff --git a/.github/workflows/depsreview.yml b/.github/workflows/depsreview.yml new file mode 100644 index 0000000000..68853791da --- /dev/null +++ b/.github/workflows/depsreview.yml @@ -0,0 +1,14 @@ +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + # - name: 'Dependency Review' + # uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..0c1089b9ff --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,34 @@ +name: Docker Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + release: + description: "Release (e.g., v1.9.0)" + required: true + +permissions: + contents: read + +jobs: + docker-release: + runs-on: ubuntu-latest + env: + DOCKER_LOGIN: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} + RELEASE: ${{ github.event.inputs.release || github.event.release.tag_name }} + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Build Docker Images + run: make VERSION=${RELEASE:1} DOCKER=coredns -f Makefile.docker release + - name: Show Docker Images + run: docker images + - name: Docker login + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Publish Docker Images + run: make VERSION=${RELEASE:1} DOCKER=coredns -f Makefile.docker docker-push diff --git a/.github/workflows/go.coverage.yml b/.github/workflows/go.coverage.yml index 66149d35f1..3ca781feb1 100644 --- a/.github/workflows/go.coverage.yml +++ b/.github/workflows/go.coverage.yml @@ -1,26 +1,34 @@ name: Go Coverage -on: [push, pull_request] +on: [pull_request] +permissions: + contents: read + jobs: test: name: Coverage runs-on: ubuntu-latest steps: + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Install Go - uses: actions/setup-go@v2 - id: go + - name: Setup Go Version + run: echo "GO_VERSION=$(cat .go-version)" >> $GITHUB_ENV - - name: Check out code - uses: actions/checkout@v2 + - name: Install Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: ${{ env.GO_VERSION }} + id: go - - name: Build - run: go build -v ./... + - name: Build + run: go build -v ./... - - name: Test With Coverage - run: | - for d in request core coremain plugin test; do \ - ( cd $d; go test -coverprofile=cover.out -covermode=atomic -race ./...; [ -f cover.out ] && cat cover.out >> ../coverage.txt ); \ - done + - name: Test With Coverage + run: | + go install github.com/fatih/faillint@c56e3ec6dbfc933bbeb884fd31f2bcd41f712657 # v1.15.0 + for d in request core coremain plugin test; do \ + ( cd $d; go test -coverprofile=cover.out -covermode=atomic -race ./...; [ -f cover.out ] && cat cover.out >> ../coverage.txt ); \ + done - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + - name: Upload coverage to Codecov + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 diff --git a/.github/workflows/go.fmt.yml b/.github/workflows/go.fmt.yml deleted file mode 100644 index 288657fdd4..0000000000 --- a/.github/workflows/go.fmt.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Go Fmt - -on: - push: - branches: - - 'master' - paths: - - '**.go' - -jobs: - fix: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Fmt - run: | - find . -not -path '*/\.git/*' -type f -name '*.go' -exec gofmt -s -w {} \+ - - - name: Set up Git - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git config user.name "coredns-auto-go-fmt[bot]" - git config user.email "coredns-auto-go-fmt[bot]@users.noreply.github.com" - git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git - - - name: Commit and push changes - run: | - git add . - if output=$(git status --porcelain) && [ ! -z "$output" ]; then - git commit -m 'auto go fmt' - git push - fi diff --git a/.github/workflows/go.test.yml b/.github/workflows/go.test.yml index 8e43315b22..77023ebae6 100644 --- a/.github/workflows/go.test.yml +++ b/.github/workflows/go.test.yml @@ -1,76 +1,95 @@ name: Go Tests on: [push, pull_request] +permissions: + contents: read + jobs: test: name: Test runs-on: ubuntu-latest steps: + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Install Go - uses: actions/setup-go@v2 - id: go + - name: Setup Go Version + run: echo "GO_VERSION=$(cat .go-version)" >> $GITHUB_ENV - - name: Check out code - uses: actions/checkout@v2 + - name: Install Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: ${{ env.GO_VERSION }} + id: go - - name: Build - run: go build -v ./... + - name: Build + run: go build -v ./... - - name: Test - run: | - ( cd request; go test -race ./... ) - ( cd core; go test -race ./... ) - ( cd coremain; go test -race ./... ) + - name: Test + run: | + ( cd request; go test -race ./... ) + ( cd core; go test -race ./... ) + ( cd coremain; go test -race ./... ) test-plugins: name: Test Plugins runs-on: ubuntu-latest steps: + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Install Go - uses: actions/setup-go@v2 - id: go + - name: Setup Go Version + run: echo "GO_VERSION=$(cat .go-version)" >> $GITHUB_ENV - - name: Check out code - uses: actions/checkout@v2 + - name: Install Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: ${{ env.GO_VERSION }} + id: go - - name: Build - run: go build -v ./... + - name: Build + run: go build -v ./... - - name: Test - run: ( cd plugin; go test -race ./... ) + - name: Test + run: ( cd plugin; go test -race ./... ) test-e2e: name: Test e2e runs-on: ubuntu-latest steps: + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Install Go - uses: actions/setup-go@v2 - id: go + - name: Setup Go Version + run: echo "GO_VERSION=$(cat .go-version)" >> $GITHUB_ENV - - name: Check out code - uses: actions/checkout@v2 + - name: Install Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: ${{ env.GO_VERSION }} + id: go - - name: Build - run: go build -v ./... + - name: Build + run: go build -v ./... - - name: Test - run: | - go install github.com/fatih/faillint || true - ( cd test; go test -race ./... ) + - name: Test + run: | + go install github.com/fatih/faillint@c56e3ec6dbfc933bbeb884fd31f2bcd41f712657 # v1.15.0 + ( cd test; go test -race ./... ) test-makefile-release: name: Test Makefile.release runs-on: ubuntu-latest steps: + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Install dependencies + run: sudo apt-get install make curl - - name: Install dependencies - run: | - sudo apt-get install make curl + - name: Test Makefile.release release + run: make GITHUB_ACCESS_TOKEN=x release -f Makefile.release - - name: Check out code - uses: actions/checkout@v2 + - name: Test Makefile.release release/github-push (dry-run) + run: make GITHUB_ACCESS_TOKEN=x -n release github-push -f Makefile.release - - name: Test Makefile - run: make DOCKER=bla GITHUB_ACCESS_TOKEN=bla -n build docker github-push docker-push -f Makefile.release + - name: Test Makefile.docker release/github-push (dry-run) + run: make VERSION=x DOCKER=x -n release docker-push -f Makefile.docker diff --git a/.github/workflows/go.tidy.yml b/.github/workflows/go.tidy.yml deleted file mode 100644 index a0175f5a02..0000000000 --- a/.github/workflows/go.tidy.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Go Tidy - -on: - push: - branches: - - 'master' - paths: - - '.github/workflows/go.tidy.yml' - - 'go.mod' - - 'go.sum' - -jobs: - fix: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Tidy - run: | - rm -f go.sum - go mod tidy - - - name: Set up Git - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git config user.name "coredns-auto-go-mod-tidy[bot]" - git config user.email "coredns-auto-go-mod-tidy[bot]@users.noreply.github.com" - git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git - - - name: Commit and push changes - run: | - git add . - if output=$(git status --porcelain) && [ ! -z "$output" ]; then - git commit -m 'auto go mod tidy' - git push - fi diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000000..930edf7ae8 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,25 @@ +name: golangci-lint + +on: + pull_request: + +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Setup Go Version + run: echo "GO_VERSION=$(cat .go-version)" >> $GITHUB_ENV + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: ${{ env.GO_VERSION }} + - name: golangci-lint + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 + with: + version: v2.4.0 + # - name: modernize # Commented out modernize step temporarily during CoreDNS upgrade. + # run: go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@2e31135b736b96cd609904370c71563ce5447826 -diff -test ./... # v0.20.0 diff --git a/.github/workflows/make.doc.yml b/.github/workflows/make.doc.yml index 8fef7f0a3f..b9ea1eda5b 100644 --- a/.github/workflows/make.doc.yml +++ b/.github/workflows/make.doc.yml @@ -1,43 +1,46 @@ name: Make Doc on: - push: - branches: - - 'master' - paths: - - '.github/workflows/make.doc.yml' - - 'coredns.1.md' - - 'corefile.5.md' - - 'plugin/*/README.md' + schedule: + - cron: '22 10 * * 0' + +permissions: + contents: read jobs: fix: runs-on: ubuntu-latest + permissions: + contents: write steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Go - uses: actions/setup-go@v2 - - - name: Update Docs + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Setup Go Version + run: echo "GO_VERSION=$(cat .go-version)" >> $GITHUB_ENV + + - name: Setup Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Update Docs run: | - ./.github/fixup_file_mtime.sh + bash -x -e ./.github/fixup_file_mtime.sh make -f Makefile.doc - - - name: Set up Git + + - name: Set up Git env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - git config user.name "coredns-auto-go-mod-tidy[bot]" - git config user.email "coredns-auto-go-mod-tidy[bot]@users.noreply.github.com" + git config user.name "coredns[bot]" + git config user.email "bot@bot.coredns.io" git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git - - - name: Commit and push changes + + - name: Commit and push changes run: | git add . if output=$(git status --porcelain) && [ ! -z "$output" ]; then - git commit -m 'auto make -f Makefile.doc' + git commit -s -m 'auto make -f Makefile.doc' git push fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..5af5fa91a4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,60 @@ +name: Release + +on: + workflow_dispatch: + inputs: + commit: + description: "Commit (e.g., 52f0348)" + default: "master" + +permissions: + contents: read + +jobs: + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + ref: ${{ github.event.inputs.commit }} + - name: Set up info + run: | + set -x -e + VERSION=$(make -f Makefile.release version) + COMMIT=$(git rev-parse --quiet --verify ${{ github.event.inputs.commit }}) + if [[ "$(git tag -l v${VERSION})" == "v${VERSION}" ]]; then + echo "v${VERSION} already released" + exit 1 + fi + echo "commit=${COMMIT}" >> $GITHUB_OUTPUT + echo "version=${VERSION}" >> $GITHUB_OUTPUT + id: info + - name: Build release binary + run: make -f Makefile.release release + - name: Build release binary sha256 + run: (cd release; for asset in `ls -A *tgz`; do sha256sum $asset > $asset.sha256; done) + - name: Remove hidden section + run: sed '/+++/,//d' notes/coredns-${{ steps.info.outputs.version}}.md > release.md + - name: Log release info + run: | + set -x -e + git log -1 + echo ${{ steps.info.outputs.commit }} + echo ${{ steps.info.outputs.version }} + cat release.md + sha256sum release/*.tgz + - name: Draft release + uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 + with: + body_path: release.md + name: v${{ steps.info.outputs.version }} + tag_name: v${{ steps.info.outputs.version }} + target_commitish: ${{ steps.info.outputs.commit }} + files: | + release/*.tgz + release/*.tgz.sha256 + draft: true diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml new file mode 100644 index 0000000000..ad933ddc67 --- /dev/null +++ b/.github/workflows/scorecards.yml @@ -0,0 +1,56 @@ +name: Scorecards supply-chain security +on: + # Only the default branch is supported. + branch_protection_rule: + schedule: + - cron: '36 10 * * 3' + push: + branches: [ master ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + actions: read + contents: read + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + # Read-only PAT token. To create it, + # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. + repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} + # Publish the results to enable scorecard badges. For more details, see + # https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories, `publish_results` will automatically be set to `false`, + # regardless of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). + - name: "Upload artifact" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 + with: + sarif_file: results.sarif diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..aba566ba76 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,26 @@ +name: 'Close Stale Issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +permissions: + contents: read + +jobs: + stale: + permissions: + issues: write # for actions/stale to close stale issues + pull-requests: write # for actions/stale to close stale PRs + runs-on: ubuntu-latest + steps: + - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + with: + stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days' + stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days' + days-before-stale: 30 + days-before-close: 7 + exempt-issue-labels: 'enhancement' + exempt-all-milestones: true + labels-to-remove-when-unstale: 'answered,needs info,needs update' + any-of-issue-labels: 'answered,needs info' + any-of-pr-labels: 'needs update,needs info' diff --git a/.github/workflows/trivy-scan.yaml b/.github/workflows/trivy-scan.yaml new file mode 100644 index 0000000000..8cf1c6e406 --- /dev/null +++ b/.github/workflows/trivy-scan.yaml @@ -0,0 +1,33 @@ +name: Trivy Nightly Scan +on: + schedule: + - cron: '0 2 * * 5' # Run at 2AM UTC on every Friday + +permissions: read-all +jobs: + nightly-scan: + name: Trivy Scan nightly + strategy: + fail-fast: false + matrix: + # It will test for only the latest version as older version is not maintained + versions: [latest] + permissions: + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # master + with: + image-ref: 'docker.io/coredns/coredns:${{ matrix.versions }}' + severity: 'CRITICAL,HIGH' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 + with: + sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/whitespace.yml b/.github/workflows/whitespace.yml deleted file mode 100644 index 3f27966fdb..0000000000 --- a/.github/workflows/whitespace.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Remove Trailing Whitespaces - -on: - push: - branches: - - 'master' - paths-ignore: - - '**.go' - -jobs: - fix: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Remove Trailing Whitespaces - run: | - find . -not -path '*/\.git/*' -type f -not -name '*.go' -exec sed -i 's/[[:space:]]\{1,\}$//' {} \+ - - - name: Set up Git - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git config user.name "coredns-auto-trailing-whitespaces[bot]" - git config user.email "coredns-auto-trailing-whitespaces[bot]@users.noreply.github.com" - git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git - - - name: Commit and push changes - run: | - git add . - if output=$(git status --porcelain) && [ ! -z "$output" ]; then - git commit -m 'auto remove trailing whitespaces' - git push - fi diff --git a/.github/workflows/yamllint.yml b/.github/workflows/yamllint.yml new file mode 100644 index 0000000000..7f05d86ee3 --- /dev/null +++ b/.github/workflows/yamllint.yml @@ -0,0 +1,19 @@ +name: 'Yamllint GitHub Actions' +on: + - pull_request +permissions: read-all +jobs: + yamllint: + name: 'Yamllint' + runs-on: ubuntu-latest + steps: + - name: 'Checkout' + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: 'Yamllint' + uses: karancode/yamllint-github-action@4052d365f09b8d34eb552c363d1141fd60e2aeb2 + with: + yamllint_file_or_dir: '.' + yamllint_strict: false + yamllint_comment: true + env: + GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 67780a4715..5a6dd1245e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # only add build artifacts concerning coredns - no editor related files coredns coredns.exe +Corefile build/ release/ vendor/ diff --git a/.go-version b/.go-version new file mode 100644 index 0000000000..61b813d5e6 --- /dev/null +++ b/.go-version @@ -0,0 +1 @@ +1.25.2 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000000..26b005c169 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,119 @@ +version: "2" + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + +linters: + default: none + enable: + - canonicalheader + - copyloopvar + - durationcheck + - govet + - ineffassign + - intrange + - nakedret + - nolintlint + - perfsprint + - prealloc + - protogetter + - staticcheck + - thelper + - unconvert + - unused + - usetesting + - wastedassign + - whitespace + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - perfsprint + settings: + govet: + enable: + - nilness + perfsprint: + error-format: false + revive: + rules: + - name: blank-imports + + - name: context-as-argument + arguments: + - allowTypesBefore: "*testing.T" + + - name: context-keys-type + + - name: dot-imports + + - name: early-return + arguments: + - "preserveScope" + + - name: empty-block + disabled: true + + - name: error-naming + + - name: error-return + + - name: error-strings + + - name: errorf + + - name: increment-decrement + + - name: indent-error-flow + arguments: + - "preserveScope" + + - name: range + + - name: receiver-naming + + - name: redefines-builtin-id + disabled: true + + - name: superfluous-else + arguments: + - "preserveScope" + + - name: time-naming + disabled: true + + - name: unexported-return + disabled: true + + - name: unnecessary-stmt + + - name: unreachable-code + + - name: unused-parameter + arguments: + - allowRegex: "^_" + + - name: use-any + + - name: var-declaration + + - name: var-naming + disabled: true + arguments: + - ["ID"] + - ["VM"] + - - upperCaseConst: true + +formatters: + enable: + - gofmt + exclusions: + generated: lax diff --git a/.stickler.yml b/.stickler.yml index c2a68ce41c..cf662ed94f 100644 --- a/.stickler.yml +++ b/.stickler.yml @@ -1,10 +1,10 @@ --- linters: - golint: - min_confidence: 0.85 - fixer: true + golint: + min_confidence: 0.85 + fixer: true files: - ignore: - - 'vendor/*' - - 'pb/*' + ignore: + - 'vendor/*' + - 'pb/*' diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000000..1fd8ea1d85 --- /dev/null +++ b/.yamllint @@ -0,0 +1,17 @@ +--- +extends: default + +rules: + braces: + max-spaces-inside: 1 + level: error + brackets: + max-spaces-inside: 1 + level: error + document-start: disable + hyphens: + max-spaces-after: 1 + indentation: + spaces: 2 + line-length: disable + truthy: disable diff --git a/ADOPTERS.md b/ADOPTERS.md index 652259ae7a..639fd5f727 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,3 +1,6 @@ +> Note: Adopters list is kept in chronological order. + +* [Qwilt](https://www.qwilt.com) uses CoreDNS in production, implementing a global DNS service allowing access to our ISP edge and regional locations. * [Infoblox](https://www.infoblox.com) uses CoreDNS in its Active Trust Cloud SaaS service, as well as for Kubernetes cluster DNS. * [Sky Betting & Gaming](https://engineering.skybettingandgaming.com) uses CoreDNS for Kubernetes cluster DNS. * [Kismia](https://kismia.com) uses CoreDNS for Kubernetes cluster DNS. @@ -28,3 +31,9 @@ * [Hualala](https://www.hualala.com) uses CoreDNS in Kubernetes using default configuration, in its Lab. Expected to be in production soon. * [Hellofresh](https://www.hellofresh.com/) uses CoreDNS in multiple Kubernetes clusters, with Forward plugin. * [Render](https://render.com) uses CoreDNS in production across all its Kubernetes clusters. +* [BackMarket](https://www.backmarket.com) uses CoreDNS within Kubernetes in production, with standard configuration. +* [Absa Group](https://www.absa.africa) uses CoreDNS as an integral part of Kubernetes Global Balancer project - [k8gb](https://www.k8gb.io/). +* [Northflank](https://northflank.com/) uses CoreDNS on all of our Kubernetes clusters across GCP, AWS, and bare-metal. +* [PITS Global Data Recovery Services](https://www.pitsdatarecovery.net) uses CoreDNS on K8s in its highly-loaded internal infrastructure. +* [plusserver](https://www.plusserver.com) uses CoreDNS on K8s in its plusserver Kubernetes Engine. +* [Sophotech](https://sopho.tech) uses CoreDNS with a tuned configuration for Kubernetes deployment in production environments. \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index 894d9afb48..16b7e845e0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,6 +1,11 @@ -# @miekg, miek@miek.nl, project lead: 11/11/2021 +# 5 steering committee members +# @chrisohaver, term ends 2025-11-21 +# @johnbelamaric, term ends 2025-11-21 +# @stp-ip, term ends 2025-11-21 +# @superq, term ends 2025-11-21 +# @tantalor93, term ends 2025-11-21 -* @bradbeam @chrisohaver @dilyevsky @fastest963 @greenpau @isolus @johnbelamaric @miekg @pmoroney @rajansandeep @stp-ip @superq @yongtang +* @bradbeam @chrisohaver @dilyevsky @jameshartig @greenpau @isolus @johnbelamaric @miekg @pmoroney @rajansandeep @stp-ip @superq @yongtang @Tantalor93 @thevilledev /.circleci/ @miekg @chrisohaver @rajansandeep /plugin/pkg/ @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip @@ -12,13 +17,13 @@ go.sum @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip go.mod @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip /plugin/acl/ @miekg @ihac -/plugin/any/ @miekg +/plugin/any/ @miekg @yongtang /plugin/auto/ @miekg @stp-ip /plugin/autopath/ @chrisohaver @miekg /plugin/azure/ @miekg @yongtang @darshanime /plugin/bind/ @miekg /plugin/bufsize/ @ykhr53 -/plugin/cache/ @miekg +/plugin/cache/ @miekg @chrisohaver /plugin/cancel/ @miekg /plugin/chaos/ @miekg @zouyee /plugin/clouddns/ @miekg @yongtang @@ -26,29 +31,32 @@ go.mod @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip /plugin/dnssec/ @isolus @miekg /plugin/dnstap/ @varyoo @yongtang /plugin/erratic/ @miekg -/plugin/errors/ @miekg +/plugin/errors/ @miekg @Tantalor93 /plugin/etcd/ @miekg @nitisht /plugin/file/ @miekg @yongtang @stp-ip -/plugin/forward/ @johnbelamaric @miekg @rdrozhdzh +/plugin/forward/ @johnbelamaric @miekg @rdrozhdzh @Tantalor93 @chrisohaver +/plugin/geoip/ @miekg @snebel29 /plugin/grpc/ @inigohu @miekg @zouyee -/plugin/health/ @fastest963 @miekg @zouyee +/plugin/health/ @jameshartig @miekg @zouyee +/plugin/header/ @miekg @mqasimsarfraz /plugin/hosts/ @johnbelamaric @pmoroney -/plugin/k8s_external/ @miekg +/plugin/k8s_external/ @miekg @chrisohaver /plugin/kubernetes/ @bradbeam @chrisohaver @johnbelamaric @miekg @rajansandeep @yongtang @zouyee /plugin/loadbalance/ @miekg -/plugin/log/ @miekg @nchrisdk +/plugin/log/ @miekg @nchrisdk @Tantalor93 /plugin/loop/ @miekg @chrisohaver -/plugin/metadata/ @ekleiner @miekg -/plugin/metrics/ @fastest963 @miekg @superq @greenpau +/plugin/metadata/ @ekleiner @miekg @Tantalor93 +/plugin/metrics/ @jameshartig @miekg @superq @greenpau @Tantalor93 /plugin/nsid/ @yongtang /plugin/pprof/ @miekg @zouyee /plugin/reload/ @johnbelamaric /plugin/rewrite/ @greenpau @johnbelamaric -/plugin/root/ @miekg +/plugin/root/ @miekg @yongtang /plugin/route53/ @yongtang @dilyevsky /plugin/secondary/ @bradbeam @miekg /plugin/template/ @rtreffer /plugin/tls/ @johnbelamaric -/plugin/trace/ @johnbelamaric @zouyee +/plugin/trace/ @johnbelamaric @zouyee @Tantalor93 /plugin/transfer/ @miekg @chrisohaver -/plugin/whoami/ @miekg @chrisohaver +/plugin/tsig/ @chrisohaver +/plugin/whoami/ @miekg @chrisohaver @yongtang diff --git a/Dockerfile b/Dockerfile index 9c46085891..5202e8b7b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,25 @@ -FROM debian:stable-slim +ARG DEBIAN_IMAGE=debian:stable-slim +ARG BASE=gcr.io/distroless/static-debian12:nonroot +FROM --platform=$BUILDPLATFORM ${DEBIAN_IMAGE} AS build +SHELL [ "/bin/sh", "-ec" ] -RUN apt-get update && apt-get -uy upgrade -RUN apt-get -y install ca-certificates && update-ca-certificates - -FROM scratch - -COPY --from=0 /etc/ssl/certs /etc/ssl/certs -ADD coredns /coredns +RUN export DEBCONF_NONINTERACTIVE_SEEN=true \ + DEBIAN_FRONTEND=noninteractive \ + DEBIAN_PRIORITY=critical \ + TERM=linux ; \ + apt-get -qq update ; \ + apt-get -qq upgrade ; \ + apt-get -qq --no-install-recommends install ca-certificates libcap2-bin; \ + apt-get clean +COPY coredns /coredns +RUN setcap cap_net_bind_service=+ep /coredns +FROM --platform=$TARGETPLATFORM ${BASE} +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=build /coredns /coredns +USER nonroot:nonroot +# Reset the working directory inherited from the base image back to the expected default: +# https://github.com/coredns/coredns/issues/7009#issuecomment-3124851608 +WORKDIR / EXPOSE 53 53/udp ENTRYPOINT ["/coredns"] diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 36c531b582..51b6e9c036 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -5,26 +5,25 @@ The CoreDNS community adheres to the following principles: - Open: CoreDNS is open source, advertised on [our website](https://coredns.io/community). -- Welcoming and respectful: See [Code of Conduct](CODE-OF-CONDUCT.md). +- Welcoming and respectful: See [Code of Conduct](.github/CODE_OF_CONDUCT.md). - Transparent and accessible: Changes to the CoreDNS organization, CoreDNS code repositories, and CNCF related activities (e.g. level, involvement, etc) are done in public. - Merit: Ideas and contributions are accepted according to their technical merit and alignment with project objectives, scope, and design principles. -## Project Lead +## Project Steering Committee -The CoreDNS project has a project lead. +The CoreDNS project has a project steering committee consisting of 5 members, with a maximum of 1 member from any single organization. +The steering committee in CoreDNS has a final say in any decision concerning the CoreDNS project, with the exceptions of +deciding steering committee membership, and changes to project governance. See `Changes in Project Steering Committee Membership` +and `Changes in Project Governance`. -A project lead in CoreDNS is -a single person that has a final say in any decision concerning the CoreDNS project. +Any decision made must not conflict with CNCF policy. -The term of the project lead is one year, with no term limit restriction. +The maximum term length of each steering committee member is one year, with no term limit restriction. -The project lead is elected by CoreDNS maintainers -according to an individual's technical merit to CoreDNS project. - -The current project lead is identified in the [CODEOWNERS](CODEOWNERS) file with the string -`project lead` and the term behind the name in a comment at the top of the file. +Steering committee member are elected by CoreDNS maintainers. +The steering committee members are identified in the [CODEOWNERS](CODEOWNERS) file. ## Expectations from Maintainers @@ -37,7 +36,7 @@ maintainers. Every Maintainer is listed in the [CODEOWNERS](https://github.com/coredns/coredns/blob/master/CODEOWNERS) -file, with their Github handle. +file, with their GitHub handle. A Maintainer should be a member of `maintainers@coredns.io`, although this is not a hard requirement. @@ -54,53 +53,61 @@ If a Maintainer feels she/he can not fulfill the "Expectations from Maintainers" step down. The CoreDNS organization will never forcefully remove a current Maintainer, unless a maintainer -fails to meet the principles of CoreDNS community, -or adhere to the [Code of Conduct](CODE-OF-CONDUCT.md). +fails to meet the principles of CoreDNS community, or adhere to the [Code of Conduct](.github/CODE_OF_CONDUCT.md). -## Changes in Project Lead +## Changes in Project Steering Committee Membership -Changes in project lead or term is initiated by opening a github PR. +Changes to the project steering committee membership are initiated by opening a separate GitHub PR updating +the [CODEOWNERS](CODEOWNERS) file for each steering committee member candidate. -Anyone from CoreDNS community can vote on the PR with either +1 or -1. +Anyone from the CoreDNS community can vote on the PR with either +1 or -1. Only the following votes are binding: 1) Any maintainer that has been listed in the [CODEOWNERS](CODEOWNERS) file before the PR is opened. 2) Any maintainer from an organization may cast the vote for that organization. However, no organization should have more binding votes than 1/5 of the total number of maintainers defined in 1). -The PR should only be opened no earlier than 6 weeks before the end of the project lead's term. +The PR should be opened no earlier than 6 weeks before the end of affected committee member's term. The PR should be kept open for no less than 4 weeks. The PR can only be merged after the end of the -last project lead's term, with more +1 than -1 in the binding votes. +replaced committee member's term, with more +1 than -1 in the binding votes. + +When there are conflicting PRs for changes to a project committee member, the PR with the most +binding +1 votes is merged. -When there are conflicting PRs about changes in project lead, the PR with the most binding +1 votes is merged. +During a vote there may be several candidates running for multiple committee seat vacancies. Maintainers and +community members should cast a single vote per vacancy (although this does not need to be enforced). At the end of the +voting period, candidates with the most binding votes will fill the vacancies. In the event of a +multi-way tie for a set of remaining vacancies, the candidates who have been maintainers longest have precedence. -The project lead can volunteer to step down. +A project steering committee member may volunteer to step down, ending their term early. ## Changes in Project Governance -Changes in project governance (GOVERNANCE.md) could be initiated by opening a github PR. -The PR should only be opened no earlier than 6 weeks before the end of the project lead's term. -The PR should be kept open for no less than 4 weeks. The PR can only be merged follow the same -voting process as in `Changes in Project Lead`. +Changes in project governance (GOVERNANCE.md) can be initiated by opening a GitHub PR. +The PR should only be opened no earlier than 6 weeks before the end of a committee member's term. +The PR should be kept open for no less than 4 weeks. The PR can only be merged following the same +voting process as in `Changes in Project Steering Committee Membership`. -## Decision making process +## Decision-making process Decisions are build on consensus between maintainers. -Proposals and ideas can either be submitted for agreement via a github issue or PR, +Proposals and ideas can either be submitted for agreement via a GitHub issue or PR, or by sending an email to `maintainers@coredns.io`. In general, we prefer that technical issues and maintainer membership are amicably worked out between the persons involved. -If a dispute cannot be decided independently, get a third-party maintainer (e.g. a mutual contact with some background +If a dispute cannot be resolved independently, get a third-party maintainer (e.g. a mutual contact with some background on the issue, but not involved in the conflict) to intercede. -If a dispute still cannot be decided, the project lead has the final say to decide an issue. +If a dispute still cannot be resolved, the project steering committee has the final say to decide an issue. +The project steering committee may reach this decision by consensus or else by a simple majority vote among committee +members if necessary. The steering should committee endeavor to make this decision within a reasonable amount of time, +not to extend longer than two weeks. -Decision making process should be transparent to adhere to -the principles of CoreDNS project. +The decision-making process should be transparent to adhere to the CoreDNS Code of Conduct. -All proposals, ideas, and decisions by maintainers or the project lead -should either be part of a github issue or PR, or be sent to `maintainers@coredns.io`. +All proposals, ideas, and decisions by maintainers or the steering committee +should either be part of a GitHub issue or PR, or be sent to `maintainers@coredns.io`. -## Github Project Administration +## GitHub Project Administration The __coredns__ GitHub project maintainers team reflects the list of Maintainers. @@ -131,14 +138,14 @@ plugin. ## CoreDNS and CNCF CoreDNS is a CNCF project. As such, CoreDNS might be involved in CNCF (or other CNCF projects) related -marketing, events, or activities. Any maintainer could help driving the CoreDNS involvement, as long as +marketing, events, or activities. Any maintainer may participate in these activities, as long as she/he sends email to `maintainers@coredns.io` (or create a GitHub Pull Request) to call for participation from other maintainers. The `Call for Participation` should be kept open for no less than a week if time permits, or a _reasonable_ time frame to allow maintainers to have a chance to volunteer. ## Code of Conduct -The [CoreDNS Code of Conduct](CODE-OF-CONDUCT.md) is aligned with the CNCF Code of Conduct. +The [CoreDNS Code of Conduct](.github/CODE_OF_CONDUCT.md) is aligned with the CNCF Code of Conduct. ## Credits diff --git a/Makefile b/Makefile index b57e6312cb..8ee633c363 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,16 @@ # Makefile for building CoreDNS -GITCOMMIT:=$(shell git describe --dirty --always) +GITCOMMIT?=$(shell git describe --dirty --always) BINARY:=coredns SYSTEM:= CHECKS:=check -BUILDOPTS:=-v +BUILDOPTS?=-v GOPATH?=$(HOME)/go MAKEPWD:=$(dir $(realpath $(firstword $(MAKEFILE_LIST)))) -CGO_ENABLED:=0 +CGO_ENABLED?=0 +GOLANG_VERSION ?= $(shell cat .go-version) + +export GOSUMDB = sum.golang.org +export GOTOOLCHAIN = go$(GOLANG_VERSION) .PHONY: all all: coredns @@ -20,10 +24,12 @@ check: core/plugin/zplugin.go core/dnsserver/zdirectives.go core/plugin/zplugin.go core/dnsserver/zdirectives.go: plugin.cfg go generate coredns.go + go get .PHONY: gen gen: go generate coredns.go + go get .PHONY: pb pb: diff --git a/Makefile.docker b/Makefile.docker new file mode 100644 index 0000000000..eb7e8ae07b --- /dev/null +++ b/Makefile.docker @@ -0,0 +1,117 @@ +# Makefile for creating and uploading CoreDNS docker image. +# +# First you should do a release and then call this Makefile to create and upload +# the image. +# +# 1. Reuse the issue for this release +# 2. In an issue give the command: /docker VERSION +# Where VERSION is the version of the release. +# 3. (to test as release /docker -t VERSION can be used. +# +# To release we run, these target from the this Makefile.docker ordered like: +# * make release +# * make docker-push +# +# Testing docker is done e.g. via: +# +# export DOCKER_PASSWORD= +# export DOCKER_LOGIN=miek +# make VERSION=x.y.z DOCKER=miek -f Makefile.docker release docker-push + +ifeq (, $(shell which curl)) + $(error "No curl in $$PATH, please install") +endif +ifeq (, $(shell which jq)) + $(error "No jq in $$PATH, please install") +endif + +# VERSION is the version we should download and use. +VERSION:= +# DOCKER is the docker image repo we need to push to. +DOCKER:= +NAME:=coredns +GITHUB:=https://github.com/coredns/coredns/releases/download +# mips is not in LINUX_ARCH because it's not supported by docker manifest. Keep this list in sync with the one in Makefile.release +LINUX_ARCH:=amd64 arm arm64 mips64le ppc64le s390x riscv64 +DOCKER_IMAGE_NAME:=$(DOCKER)/$(NAME) +DOCKER_IMAGE_LIST_VERSIONED:=$(shell echo $(LINUX_ARCH) | sed -e "s~mips64le ~~g" | sed -e "s~[^ ]*~$(DOCKER_IMAGE_NAME):&\-$(VERSION)~g") + +all: + @echo Use the 'release' target to download released binaries and build containers per arch, 'docker-push' to build and push a multi arch manifest. + echo $(DOCKER_IMAGE_LIST_VERSIONED) + echo $(DOCKER_IMAGE_LIST_LATEST) + +release: image-download docker-build + +.PHONY: image-download +image-download: +ifeq ($(VERSION),) + $(error "Please specify a version use. Use VERSION=") +endif + + @# 0. Check until all asset are alive, up to 10 min (asset may not be alive immediately after upload) + try_max=20; try_sleep=30; \ + for arch in $(LINUX_ARCH); do \ + asset=coredns_$(VERSION)_linux_$${arch}.tgz; \ + for i in $$(seq 1 $$try_max ); do \ + if [ $$(curl -I -L -s -o /dev/null -w "%{http_code}" $(GITHUB)/v$(VERSION)/$$asset) -eq 200 ]; then \ + echo "$$asset is live" ; break; \ + else \ + echo "$$asset is not live yet..." ; sleep $$try_sleep ; \ + fi ; \ + done ; \ + if [ $$i -eq $$try_max ]; then \ + echo "$$asset is not live after $$try_max tries" ; exit 1; \ + fi ; \ + done + @rm -rf build/docker + @mkdir -p build/docker + @# 1. Copy appropriate coredns binary to build/docker/ + @# 2. Copy Dockerfile into the correct dir as well. + @# 3. Unpack the tgz from GitHub into 'coredns' binary. + for arch in $(LINUX_ARCH); do \ + mkdir build/docker/$${arch}; \ + curl -L $(GITHUB)/v$(VERSION)/coredns_$(VERSION)_linux_$${arch}.tgz > build/docker/$${arch}/coredns.tgz && \ + ( cd build/docker/$${arch}; tar xf coredns.tgz && rm coredns.tgz ); \ + done + +.PHONY: docker-build +docker-build: +ifeq ($(DOCKER),) + $(error "Please specify Docker registry to use. Use DOCKER=coredns for releases") +else + docker version + for arch in $(LINUX_ARCH); do \ + cp Dockerfile build/docker/$${arch} ; \ + DOCKER_ARGS=""; \ + if [ "$${arch}" = "riscv64" ]; then \ + DOCKER_ARGS="--build-arg=DEBIAN_IMAGE=debian:unstable-slim --build-arg=BASE=ghcr.io/go-riscv/distroless/static-unstable:nonroot"; \ + fi; \ + DOCKER_BUILDKIT=1 docker build --platform=$${arch} -t $(DOCKER_IMAGE_NAME):$${arch}-$(VERSION) $${DOCKER_ARGS} build/docker/$${arch} ;\ + done +endif + +.PHONY: docker-push +docker-push: +ifeq ($(VERSION),) + $(error "Please specify a version use. Use VERSION=") +endif +ifeq ($(DOCKER),) + $(error "Please specify Docker registry to use. Use DOCKER=coredns for releases") +else + @# Pushes coredns/coredns-$arch:$version images + @# Creates manifest for multi-arch image + @# Pushes multi-arch image to coredns/coredns:$version + @echo Pushing: $(VERSION) to $(DOCKER_IMAGE_NAME) + for arch in $(LINUX_ARCH); do \ + docker push $(DOCKER_IMAGE_NAME):$${arch}-$(VERSION) ;\ + done + docker manifest create --amend $(DOCKER_IMAGE_NAME):$(VERSION) $(DOCKER_IMAGE_LIST_VERSIONED) + docker manifest create --amend $(DOCKER_IMAGE_NAME):latest $(DOCKER_IMAGE_LIST_VERSIONED) + docker manifest push --purge $(DOCKER_IMAGE_NAME):$(VERSION) + docker manifest push --purge $(DOCKER_IMAGE_NAME):latest + TOKEN=$$(curl -s -H "Content-Type: application/json" -X POST -d "{\"username\":\"$${DOCKER_LOGIN}\",\"password\":\"$${DOCKER_PASSWORD}\"}" "https://hub.docker.com/v2/users/login/" | jq -r .token) ; \ + for arch in $(LINUX_ARCH); do \ + curl -X DELETE -H "Authorization: JWT $${TOKEN}" "https://hub.docker.com/v2/repositories/$(DOCKER_IMAGE_NAME)/tags/$${arch}-$(VERSION)/" ;\ + done +endif diff --git a/Makefile.fuzz b/Makefile.fuzz deleted file mode 100644 index 5f4c1be2cd..0000000000 --- a/Makefile.fuzz +++ /dev/null @@ -1,58 +0,0 @@ -# Makefile for fuzzing -# -# With https://app.fuzzit.dev/ we are continuously fuzzing CoreDNS. -# -# Use go-fuzz and needs the tools installed. For each fuzz.go in a plugin's directory -# you can start the fuzzing with: make -f Makefile.fuzz -# e.g. -# -# make -f Makefile.fuzz forward -# -# Each plugin that wants to join the fuzzing fray only needs to add a fuzz.go that calls -# the plugin's ServeDNS and used the plugin/pkg/fuzz for the Do function. -# -# Installing go-fuzz is very tricky because it does not support Go modules, see the `Makefile` -# for the current trickery. The following may do the trick: -# -# GO111MODULE=off go get github.com/dvyukov/go-fuzz/go-fuzz-build - -REPO:="github.com/coredns/coredns" -FUZZIT:=v2.4.35 -# set LIBFUZZER=YES to build libfuzzer compatible targets - -FUZZ:=$(dir $(wildcard plugin/*/fuzz.go)) # plugin/cache/ -PLUGINS:=$(foreach f,$(FUZZ),$(subst plugin, ,$(f:/=))) # > /cache -PLUGINS:=$(foreach f,$(PLUGINS),$(subst /, ,$(f))) # > cache - -.PHONY: echo -echo: - @echo $(PLUGINS) corefile - -all: $(PLUGINS) corefile - -.PHONY: $(PLUGINS) -$(PLUGINS): echo -ifeq ($(LIBFUZZER), YES) - go-fuzz-build -libfuzzer -o $(@).a ./plugin/$(@) - clang -fsanitize=fuzzer $(@).a -o $(@) -else - go-fuzz-build $(REPO)/plugin/$(@) - go-fuzz -bin=./$(@)-fuzz.zip -workdir=fuzz/$(@) -endif - -.PHONY: corefile -corefile: -ifeq ($(LIBFUZZER), YES) - go-fuzz-build -libfuzzer -o $(@).a ./test - clang -fsanitize=fuzzer $(@).a -o $(@) -else - go-fuzz-build $(REPO)/test - go-fuzz -bin=./test-fuzz.zip -workdir=fuzz/$(@) -endif - -fuzzit: - wget --quiet -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/$(FUZZIT)/fuzzit_Linux_x86_64 && chmod +x fuzzit - -.PHONY: clean -clean: - rm *-fuzz.zip diff --git a/Makefile.release b/Makefile.release index 8f5534a387..bc4108fd50 100644 --- a/Makefile.release +++ b/Makefile.release @@ -7,6 +7,7 @@ # These are published on coredns.io. For example see: notes/coredns-1.5.1.md # Use make -f Makefile.release notes to create a skeleton notes document. # +# For this to work properly you must fetch the tag of the previous release. # Be sure to prune the PR list a bit, not everything is worthy! # # As seen in notes/coredns-1.5.1.md we want to style the notes in the following manner: @@ -36,45 +37,31 @@ # # To release we run, these target from the this Makefile.release ordered like: # * make release -# * make docker # * make github-push -# * make docker-push # -# Testing this is hard-ish as you don't want to accidentially release a coredns. If not executing the github-push target -# and using a non-coredns docker repo you should be fine. -# Testing docker is done e.g. via: -# -# export DOCKER_PASSWORD= -# export DOCKER_LOGIN=miek -# make DOCKER=miek -f Makefile.release build docker-build docker-push - -EMPTY:= -SPACE:=$(EMPTY) $(EMPTY) -COMMA:=$(EMPTY),$(EMPTY) +# Testing this is hard-ish as you don't want to accidentally release a coredns. If not executing the github-push target +# you should be fine. +# Docker image creation and upload are now separate steps, because it often failed before. See the Makefile.docker for +# details. ifeq (, $(shell which curl)) $(error "No curl in $$PATH, please install") endif -# DOCKER is the docker image repo we need to push to. -DOCKER:= NAME:=coredns VERSION:=$(shell grep 'CoreVersion' coremain/version.go | awk '{ print $$3 }' | tr -d '"') GITHUB:=coredns -# mips is not in LINUX_ARCH because it's not supported by docker manifest -LINUX_ARCH:=amd64 arm arm64 mips64le ppc64le s390x -DOCKER_IMAGE_NAME:=$(DOCKER)/$(NAME) -PLATFORMS:=$(subst $(SPACE),$(COMMA),$(foreach arch,$(LINUX_ARCH),linux/$(arch))) -DOCKER_IMAGE_LIST_VERSIONED:=$(shell echo $(LINUX_ARCH) | sed -e "s~[^ ]*~$(DOCKER_IMAGE_NAME)\-&:$(VERSION)~g") -DOCKER_IMAGE_LIST_LATEST:=$(shell echo $(LINUX_ARCH) | sed -e "s~[^ ]*~$(DOCKER_IMAGE_NAME)\-&:latest~g") +LINUX_ARCH:=amd64 arm arm64 mips64le ppc64le s390x mips riscv64 +GOLANG_VERSION ?= $(shell cat .go-version) + +export GOSUMDB = sum.golang.org +export GOTOOLCHAIN = go$(GOLANG_VERSION) all: - @echo Use the 'release' target to build a release, 'docker' for docker build. + @echo Use the 'release' target to build a release release: build tar -docker: docker-build - .PHONY: build build: @go version @@ -82,10 +69,10 @@ build: @rm -rf build && mkdir build @echo Building: darwin/amd64 - $(VERSION) mkdir -p build/darwin/amd64 && $(MAKE) coredns BINARY=build/darwin/amd64/$(NAME) SYSTEM="GOOS=darwin GOARCH=amd64" CHECKS="" BUILDOPTS="" + @echo Building: darwin/arm64 - $(VERSION) + mkdir -p build/darwin/arm64 && $(MAKE) coredns BINARY=build/darwin/arm64/$(NAME) SYSTEM="GOOS=darwin GOARCH=arm64" CHECKS="" BUILDOPTS="" @echo Building: windows/amd64 - $(VERSION) mkdir -p build/windows/amd64 && $(MAKE) coredns BINARY=build/windows/amd64/$(NAME).exe SYSTEM="GOOS=windows GOARCH=amd64" CHECKS="" BUILDOPTS="" - @echo Building: linux/mips - $(VERSION) - mkdir -p build/linux/mips && $(MAKE) coredns BINARY=build/linux/mips/$(NAME) SYSTEM="GOOS=linux GOARCH=mips" CHECKS="" BUILDOPTS="" @echo Building: linux/$(LINUX_ARCH) - $(VERSION) ;\ for arch in $(LINUX_ARCH); do \ mkdir -p build/linux/$$arch && $(MAKE) coredns BINARY=build/linux/$$arch/$(NAME) SYSTEM="GOOS=linux GOARCH=$$arch" CHECKS="" BUILDOPTS="" ;\ @@ -96,9 +83,8 @@ tar: @echo Cleaning old releases @rm -rf release && mkdir release tar -zcf release/$(NAME)_$(VERSION)_darwin_amd64.tgz -C build/darwin/amd64 $(NAME) + tar -zcf release/$(NAME)_$(VERSION)_darwin_arm64.tgz -C build/darwin/arm64 $(NAME) tar -zcf release/$(NAME)_$(VERSION)_windows_amd64.tgz -C build/windows/amd64 $(NAME).exe - tar -zcf release/$(NAME)_$(VERSION)_linux_mips.tgz -C build/linux/mips $(NAME) - tar -zcf release/$(NAME)_$(VERSION)_linux_mips64le.tgz -C build/linux/mips64le $(NAME) for arch in $(LINUX_ARCH); do \ tar -zcf release/$(NAME)_$(VERSION)_linux_$$arch.tgz -C build/linux/$$arch $(NAME) ;\ done @@ -132,50 +118,6 @@ else done ) endif -.PHONY: docker-build -docker-build: tar -ifeq ($(DOCKER),) - $(error "Please specify Docker registry to use. Use DOCKER=coredns for releases") -else - @# Steps: - @# 1. Copy appropriate coredns binary to build/docker/linux/ - @# 2. Copy Dockerfile to build/docker/linux/ - @rm -rf build/docker - for arch in $(LINUX_ARCH); do \ - mkdir -p build/docker/linux/$${arch} ;\ - tar -xzf release/$(NAME)_$(VERSION)_linux_$${arch}.tgz -C build/docker/linux/$${arch} ;\ - cp Dockerfile build/docker/linux/$${arch} ;\ - docker build -t $(DOCKER_IMAGE_NAME)-$${arch}:$(VERSION) build/docker/linux/$${arch} ;\ - docker tag $(DOCKER_IMAGE_NAME)-$${arch}:$(VERSION) $(DOCKER_IMAGE_NAME)-$${arch}:latest ;\ - done -endif - -.PHONY: docker-push -docker-push: -ifeq ($(DOCKER),) - $(error "Please specify Docker registry to use. Use DOCKER=coredns for releases") -else - @# Experimental CLI is required for docker manifest to work - @# Pushes coredns/coredns-$arch:$version images - @# Creates manifest for multi-arch image - @# Pushes multi-arch image to coredns/coredns:$version - export DOCKER_CLI_EXPERIMENTAL=enabled - @echo $(DOCKER_PASSWORD) | docker login -u $(DOCKER_LOGIN) --password-stdin - @echo Pushing: $(VERSION) to $(DOCKER_IMAGE_NAME) - for arch in $(LINUX_ARCH); do \ - docker push $(DOCKER_IMAGE_NAME)-$${arch}:$(VERSION) ;\ - docker push $(DOCKER_IMAGE_NAME)-$${arch}:latest ;\ - done - docker manifest create --amend $(DOCKER_IMAGE_NAME):$(VERSION) $(DOCKER_IMAGE_LIST_VERSIONED) - docker manifest create --amend $(DOCKER_IMAGE_NAME):latest $(DOCKER_IMAGE_LIST_LATEST) - for arch in $(LINUX_ARCH); do \ - docker manifest annotate --arch $${arch} $(DOCKER_IMAGE_NAME):$(VERSION) $(DOCKER_IMAGE_NAME)-$${arch}:$(VERSION) ;\ - docker manifest annotate --arch $${arch} $(DOCKER_IMAGE_NAME):latest $(DOCKER_IMAGE_NAME)-$${arch}:latest ;\ - done - docker manifest push --purge $(DOCKER_IMAGE_NAME):$(VERSION) - docker manifest push --purge $(DOCKER_IMAGE_NAME):latest -endif - .PHONY: version version: @echo $(VERSION) @@ -203,5 +145,5 @@ prs: authors: @echo "## Brought to You By" @echo - @git log --pretty=format:'%an' $$(git describe --tags --abbrev=0)..master | sort -u | grep -v '^coredns-auto' | grep -v '^dependabot-preview' | \ + @git log --pretty=format:'%an' $$(git describe --tags --abbrev=0)..master | sort -u | grep -v '^coredns-auto' | grep -v '^coredns\[bot\]' | grep -v '^dependabot-preview' | \ tac | cat -n | sed -e 's/^[[:space:]]\+1[[:space:]]\+\(.*\)/\1./' | sed -e 's/^[[:space:]]\+[[:digit:]]\+[[:space:]]\+\(.*\)/\1,/' | tac # comma separate, with dot at the end diff --git a/README.md b/README.md index b71ba7c559..9d60143042 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ [![CoreDNS](https://coredns.io/images/CoreDNS_Colour_Horizontal.png)](https://coredns.io) [![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/coredns/coredns) -[![Build Status](https://img.shields.io/travis/coredns/coredns/master.svg?label=build)](https://travis-ci.org/coredns/coredns) -[![fuzzit](https://app.fuzzit.dev/badge?org_id=coredns&branch=master)](https://fuzzit.dev) +![CodeQL](https://github.com/coredns/coredns/actions/workflows/codeql-analysis.yml/badge.svg) +![Go Tests](https://github.com/coredns/coredns/actions/workflows/go.test.yml/badge.svg) +[![CircleCI](https://circleci.com/gh/coredns/coredns.svg?style=shield)](https://circleci.com/gh/coredns/coredns) [![Code Coverage](https://img.shields.io/codecov/c/github/coredns/coredns/master.svg)](https://codecov.io/github/coredns/coredns?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/coredns/coredns.svg)](https://hub.docker.com/r/coredns/coredns) [![Go Report Card](https://goreportcard.com/badge/github.com/coredns/coredns)](https://goreportcard.com/report/coredns/coredns) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/1250/badge)](https://bestpractices.coreinfrastructure.org/projects/1250) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/coredns/coredns/badge)](https://scorecard.dev/viewer/?uri=github.com/coredns/coredns) CoreDNS is a DNS server/forwarder, written in Go, that chains [plugins](https://coredns.io/plugins). Each plugin performs a (DNS) function. @@ -17,9 +19,12 @@ CoreDNS is a fast and flexible DNS server. The key word here is *flexible*: with are able to do what you want with your DNS data by utilizing plugins. If some functionality is not provided out of the box you can add it by [writing a plugin](https://coredns.io/explugins). -CoreDNS can listen for DNS requests coming in over UDP/TCP (go'old DNS), TLS ([RFC -7858](https://tools.ietf.org/html/rfc7858)), also called DoT, DNS over HTTP/2 - DoH - -([RFC 8484](https://tools.ietf.org/html/rfc8484)) and [gRPC](https://grpc.io) (not a standard). +CoreDNS can listen for DNS requests coming in over: +* UDP/TCP (go'old DNS). +* TLS - DoT ([RFC 7858](https://tools.ietf.org/html/rfc7858)). +* DNS over HTTP/2 - DoH ([RFC 8484](https://tools.ietf.org/html/rfc8484)). +* DNS over QUIC - DoQ ([RFC 9250](https://tools.ietf.org/html/rfc9250)). +* [gRPC](https://grpc.io) (not a standard). Currently CoreDNS is able to: @@ -52,7 +57,7 @@ out-of-tree plugins. To compile CoreDNS, we assume you have a working Go setup. See various tutorials if you don’t have that already configured. -First, make sure your golang version is 1.12 or higher as `go mod` support is needed. +First, make sure your golang version is 1.24.0 or higher as `go mod` support and other api is needed. See [here](https://github.com/golang/go/wiki/Modules) for `go mod` details. Then, check out the project and run `make` to compile the binary: @@ -62,6 +67,8 @@ $ cd coredns $ make ~~~ +> **_NOTE:_** extra plugins may be enabled when building by setting the `COREDNS_PLUGINS` environment variable with comma separate list of plugins in the same format as plugin.cfg + This should yield a `coredns` binary. ## Compilation with Docker @@ -70,7 +77,9 @@ CoreDNS requires Go to compile. However, if you already have docker installed an setup a Go environment, you could build CoreDNS easily: ``` -$ docker run --rm -i -t -v $PWD:/v -w /v golang:1.14 make +docker run --rm -i -t \ + -v $PWD:/go/src/github.com/coredns/coredns -w /go/src/github.com/coredns/coredns \ + golang:1.24 sh -c 'GOFLAGS="-buildvcs=false" make gen && GOFLAGS="-buildvcs=false" make' ``` The above command alone will have `coredns` binary generated. @@ -84,7 +93,7 @@ and starts listening on port 53 (override with `-dns.port`), it should show the ~~~ txt .:53 CoreDNS-1.6.6 -linux/amd64, go1.13.5, aa8c32 +linux/amd64, go1.16.10, aa8c32 ~~~ The following could be used to query the CoreDNS server that is running now: @@ -107,7 +116,7 @@ on port `53` and enables `whoami` plugin is: ~~~ Sometimes port number 53 is occupied by system processes. In that case you can start the CoreDNS server -while modifying the Corefile as given below so that the CoreDNS server starts on port 1053. +while modifying the `Corefile` as given below so that the CoreDNS server starts on port 1053. ~~~ corefile .:1053 { @@ -115,9 +124,29 @@ while modifying the Corefile as given below so that the CoreDNS server starts on } ~~~ -If you have a Corefile without a port number specified it will, by default, use port 53, but you can +If you have a `Corefile` without a port number specified it will, by default, use port 53, but you can override the port with the `-dns.port` flag: `coredns -dns.port 1053`, runs the server on port 1053. +You may import other text files into the `Corefile` using the _import_ directive. You can use globs to match multiple +files with a single _import_ directive. + +~~~ txt +.:53 { + import example1.txt +} +import example2.txt +~~~ + +You can use environment variables in the `Corefile` with `{$VARIABLE}`. Note that each environment variable is inserted +into the `Corefile` as a single token. For example, an environment variable with a space in it will be treated as a single +token, not as two separate tokens. + +~~~ txt +.:53 { + {$ENV_VAR} +} +~~~ + A Corefile for a CoreDNS server that forward any queries to an upstream DNS (e.g., `8.8.8.8`) is as follows: ~~~ corefile @@ -190,6 +219,15 @@ tls://example.org grpc://example.org { } ~~~ +Similarly, for QUIC (DoQ): + +~~~ corefile +quic://example.org { + whoami + tls mycert mykey +} +~~~ + And for DNS over HTTP/2 (DoH) use: ~~~ corefile @@ -198,8 +236,15 @@ https://example.org { tls mycert mykey } ~~~ +in this setup, the CoreDNS will be responsible for TLS termination -Note that you must have the *tls* plugin configured as DoH requires that to be setup. +you can also start DNS server serving DoH without TLS termination (plain HTTP), but beware that in such scenario there has to be some kind +of TLS termination proxy before CoreDNS instance, which forwards DNS requests otherwise clients will not be able to communicate via DoH with the server +~~~ corefile +https://example.org { + whoami +} +~~~ Specifying ports works in the same way: @@ -213,22 +258,22 @@ When no transport protocol is specified the default `dns://` is assumed. ## Community -We're most active on Github (and Slack): +We're most active on GitHub (and Slack): -- Github: +- GitHub: - Slack: #coredns on More resources can be found: - Website: -- Blog: +- Blog: - Twitter: [@corednsio](https://twitter.com/corednsio) - Mailing list/group: (not very active) ## Contribution guidelines If you want to contribute to CoreDNS, be sure to review the [contribution -guidelines](CONTRIBUTING.md). +guidelines](./.github/CONTRIBUTING.md). ## Deployment @@ -251,9 +296,11 @@ And finally 1.4.1 that removes the config workarounds. ## Security -### Security Audit -A third party security audit was performed by Cure53, you can see the full report -[here](https://coredns.io/assets/DNS-01-report.pdf). +### Security Audits + +Third party security audits have been performed by: +* [Cure53](https://cure53.de) in March 2018. [Full Report](https://coredns.io/assets/DNS-01-report.pdf) +* [Trail of Bits](https://www.trailofbits.com) in March 2022. [Full Report](https://github.com/trailofbits/publications/blob/master/reviews/CoreDNS.pdf) ### Reporting security vulnerabilities @@ -262,4 +309,4 @@ issue, instead send your report privately to `security@coredns.io`. Security rep appreciated and we will publicly thank you for it. Please consult [security vulnerability disclosures and security fix and release process -document](https://github.com/coredns/coredns/blob/master/SECURITY.md) +document](https://github.com/coredns/coredns/blob/master/.github/SECURITY.md) diff --git a/core/dnsserver/address.go b/core/dnsserver/address.go index 1a69c33b85..872e44cb7c 100644 --- a/core/dnsserver/address.go +++ b/core/dnsserver/address.go @@ -4,20 +4,13 @@ import ( "fmt" "net" "strings" - - "github.com/coredns/coredns/plugin" - "github.com/coredns/coredns/plugin/pkg/parse" - "github.com/coredns/coredns/plugin/pkg/transport" - - "github.com/miekg/dns" ) type zoneAddr struct { Zone string Port string - Transport string // dns, tls or grpc - IPNet *net.IPNet // if reverse zone this hold the IPNet - Address string // used for bound zoneAddr - validation of overlapping + Transport string // dns, tls or grpc + Address string // used for bound zoneAddr - validation of overlapping } // String returns the string representation of z. @@ -29,32 +22,6 @@ func (z zoneAddr) String() string { return s } -// normalizeZone parses a zone string into a structured format with separate -// host, and port portions, as well as the original input string. -func normalizeZone(str string) (zoneAddr, error) { - trans, str := parse.Transport(str) - - host, port, ipnet, err := plugin.SplitHostPort(str) - if err != nil { - return zoneAddr{}, err - } - - if port == "" { - switch trans { - case transport.DNS: - port = Port - case transport.TLS: - port = transport.TLSPort - case transport.GRPC: - port = transport.GRPCPort - case transport.HTTPS: - port = transport.HTTPSPort - } - } - - return zoneAddr{Zone: dns.Fqdn(host), Port: port, Transport: trans, IPNet: ipnet}, nil -} - // SplitProtocolHostPort splits a full formed address like "dns://[::1]:53" into parts. func SplitProtocolHostPort(address string) (protocol string, ip string, port string, err error) { parts := strings.Split(address, "://") @@ -82,12 +49,23 @@ func newOverlapZone() *zoneOverlap { // registerAndCheck adds a new zoneAddr for validation, it returns information about existing or overlapping with already registered // we consider that an unbound address is overlapping all bound addresses for same zone, same port func (zo *zoneOverlap) registerAndCheck(z zoneAddr) (existingZone *zoneAddr, overlappingZone *zoneAddr) { + existingZone, overlappingZone = zo.check(z) + if existingZone != nil || overlappingZone != nil { + return existingZone, overlappingZone + } + // there is no overlap, keep the current zoneAddr for future checks + zo.registeredAddr[z] = z + zo.unboundOverlap[z.unbound()] = z + return nil, nil +} +// check validates a zoneAddr for overlap without registering it +func (zo *zoneOverlap) check(z zoneAddr) (existingZone *zoneAddr, overlappingZone *zoneAddr) { if exist, ok := zo.registeredAddr[z]; ok { // exact same zone already registered return &exist, nil } - uz := zoneAddr{Zone: z.Zone, Address: "", Port: z.Port, Transport: z.Transport} + uz := z.unbound() if already, ok := zo.unboundOverlap[uz]; ok { if z.Address == "" { // current is not bound to an address, but there is already another zone with a bind address registered @@ -98,8 +76,11 @@ func (zo *zoneOverlap) registerAndCheck(z zoneAddr) (existingZone *zoneAddr, ove return nil, &uz } } - // there is no overlap, keep the current zoneAddr for future checks - zo.registeredAddr[z] = z - zo.unboundOverlap[uz] = z + // there is no overlap return nil, nil } + +// unbound returns an unbound version of the zoneAddr +func (z zoneAddr) unbound() zoneAddr { + return zoneAddr{Zone: z.Zone, Address: "", Port: z.Port, Transport: z.Transport} +} diff --git a/core/dnsserver/address_test.go b/core/dnsserver/address_test.go index 6d4d0beab2..05b601313b 100644 --- a/core/dnsserver/address_test.go +++ b/core/dnsserver/address_test.go @@ -2,83 +2,6 @@ package dnsserver import "testing" -func TestNormalizeZone(t *testing.T) { - for i, test := range []struct { - input string - expected string - shouldErr bool - }{ - {".", "dns://.:53", false}, - {".:54", "dns://.:54", false}, - {"..", "://:", true}, - {".:", "://:", true}, - {"dns://.", "dns://.:53", false}, - {"dns://.:5353", "dns://.:5353", false}, - {"dns://..", "://:", true}, - {"dns://.:", "://:", true}, - {"tls://.", "tls://.:853", false}, - {"tls://.:8953", "tls://.:8953", false}, - {"tls://..", "://:", true}, - {"tls://.:", "://:", true}, - {"grpc://.", "grpc://.:443", false}, - {"grpc://.:8443", "grpc://.:8443", false}, - {"grpc://..", "://:", true}, - {"grpc://.:", "://:", true}, - {"https://.", "https://.:443", false}, - {"https://.:8443", "https://.:8443", false}, - {"https://..", "://:", true}, - {"https://.:", "://:", true}, - } { - addr, err := normalizeZone(test.input) - actual := addr.String() - if test.shouldErr && err == nil { - t.Errorf("Test %d: Expected error, but there wasn't any", i) - } - if !test.shouldErr && err != nil { - t.Errorf("Test %d: Expected no error, but there was one: %v", i, err) - } - if actual != test.expected { - t.Errorf("Test %d: Expected %s but got %s", i, test.expected, actual) - } - } -} - -func TestNormalizeZoneReverse(t *testing.T) { - for i, test := range []struct { - input string - expected string - shouldErr bool - }{ - {"2003::1/64", "dns://0.0.0.0.0.0.0.0.0.0.0.0.3.0.0.2.ip6.arpa.:53", false}, - {"2003::1/64.", "dns://2003::1/64.:53", false}, // OK, with closing dot the parse will fail. - {"2003::1/64:53", "dns://0.0.0.0.0.0.0.0.0.0.0.0.3.0.0.2.ip6.arpa.:53", false}, - {"2003::1/64.:53", "dns://2003::1/64.:53", false}, - - {"10.0.0.0/24", "dns://0.0.10.in-addr.arpa.:53", false}, - {"10.0.0.0/24.", "dns://10.0.0.0/24.:53", false}, - {"10.0.0.0/24:53", "dns://0.0.10.in-addr.arpa.:53", false}, - {"10.0.0.0/24.:53", "dns://10.0.0.0/24.:53", false}, - - // non %8==0 netmasks - {"2003::53/67", "dns://0.0.0.0.0.0.0.0.0.0.0.0.0.3.0.0.2.ip6.arpa.:53", false}, - {"10.0.0.0/25.", "dns://10.0.0.0/25.:53", false}, // has dot - {"10.0.0.0/25", "dns://0.0.10.in-addr.arpa.:53", false}, - {"fd00:77:30::0/110", "dns://0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.3.0.0.7.7.0.0.0.0.d.f.ip6.arpa.:53", false}, - } { - addr, err := normalizeZone(test.input) - actual := addr.String() - if test.shouldErr && err == nil { - t.Errorf("Test %d: Expected error, but there wasn't any", i) - } - if !test.shouldErr && err != nil { - t.Errorf("Test %d: Expected no error, but there was one: %v", i, err) - } - if actual != test.expected { - t.Errorf("Test %d: Expected %s but got %s", i, test.expected, actual) - } - } -} - func TestSplitProtocolHostPort(t *testing.T) { for i, test := range []struct { input string @@ -120,7 +43,6 @@ func TestSplitProtocolHostPort(t *testing.T) { if port != test.port { t.Errorf("Test %d: (address = %s) expected port with value %s but got %s", i, test.input, test.port, port) } - } } @@ -169,7 +91,6 @@ func TestOverlapAddressChecker(t *testing.T) { }, }, } { - checker := newOverlapZone() for _, call := range test.sequence { same, overlap := checker.registerAndCheck(call.zone) @@ -185,10 +106,8 @@ func TestOverlapAddressChecker(t *testing.T) { if overlap.String() != call.overlapKey { t.Errorf("Test %d: error, for zone %s, 'overlap Key' (%v) has not the expected value (%v)", i, sZone, overlap.String(), call.overlapKey) } - } } - } } } diff --git a/core/dnsserver/config.go b/core/dnsserver/config.go index 4ff2ecda18..168120795d 100644 --- a/core/dnsserver/config.go +++ b/core/dnsserver/config.go @@ -1,12 +1,15 @@ package dnsserver import ( + "context" "crypto/tls" "fmt" "net/http" + "time" "github.com/coredns/caddy" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" ) // Config configuration for a single server. @@ -21,6 +24,10 @@ type Config struct { // The port to listen on. Port string + // The number of servers that will listen on one port. + // By default, one server will be running. + NumSockets int + // Root points to a base directory we find user defined "things". // First consumer is the file plugin to looks for zone files in this place. Root string @@ -28,6 +35,9 @@ type Config struct { // Debug controls the panic/recover mechanism that is enabled by default. Debug bool + // Stacktrace controls including stacktrace as part of log from recover mechanism, it is disabled by default. + Stacktrace bool + // The transport we implement, normally just "dns" over TCP/UDP, but could be // DNS-over-TLS or DNS-over-gRPC. Transport string @@ -37,14 +47,33 @@ type Config struct { // may depend on it. HTTPRequestValidateFunc func(*http.Request) bool - // If this function is not nil it will be used to further filter access - // to this handler. The primary use is to limit access to a reverse zone + // FilterFuncs is used to further filter access + // to this handler. E.g. to limit access to a reverse zone // on a non-octet boundary, i.e. /17 - FilterFunc func(string) bool + FilterFuncs []FilterFunc + + // ViewName is the name of the Viewer PLugin defined in the Config + ViewName string // TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS). TLSConfig *tls.Config + // MaxQUICStreams defines the maximum number of concurrent QUIC streams for a QUIC server. + // This is nil if not specified, allowing for a default to be used. + MaxQUICStreams *int + + // MaxQUICWorkerPoolSize defines the size of the worker pool for processing QUIC streams. + // This is nil if not specified, allowing for a default to be used. + MaxQUICWorkerPoolSize *int + + // Timeouts for TCP, TLS and HTTPS servers. + ReadTimeout time.Duration + WriteTimeout time.Duration + IdleTimeout time.Duration + + // TSIG secrets, [name]key. + TsigSecret map[string]string + // Plugin stack. Plugin []plugin.Plugin @@ -55,8 +84,18 @@ type Config struct { // on them should register themselves here. The name should be the name as return by the // Handler's Name method. registry map[string]plugin.Handler + + // firstConfigInBlock is used to reference the first config in a server block, for the + // purpose of sharing single instance of each plugin among all zones in a server block. + firstConfigInBlock *Config + + // metaCollector references the first MetadataCollector plugin, if one exists + metaCollector MetadataCollector } +// FilterFunc is a function that filters requests from the Config +type FilterFunc func(context.Context, *request.Request) bool + // keyForConfig builds a key for identifying the configs during setup time func keyForConfig(blocIndex int, blocKeyIndex int) string { return fmt.Sprintf("%d:%d", blocIndex, blocKeyIndex) diff --git a/core/dnsserver/config_test.go b/core/dnsserver/config_test.go new file mode 100644 index 0000000000..a335455654 --- /dev/null +++ b/core/dnsserver/config_test.go @@ -0,0 +1,67 @@ +package dnsserver + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestKeyForConfig(t *testing.T) { + tests := []struct { + name string + blockIndex int + blockKeyIndex int + expected string + }{ + {"zero_indices", 0, 0, "0:0"}, + {"positive_indices", 1, 2, "1:2"}, + {"larger_indices", 10, 5, "10:5"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := keyForConfig(tc.blockIndex, tc.blockKeyIndex) + if result != tc.expected { + t.Errorf("Expected %s, got %s for blockIndex %d and blockKeyIndex %d", + tc.expected, result, tc.blockIndex, tc.blockKeyIndex) + } + }) + } +} + +func TestGetConfig(t *testing.T) { + controller := caddy.NewTestController("dns", "") + initialCtx := controller.Context() + dnsCtx, ok := initialCtx.(*dnsContext) + if !ok { + t.Fatalf("controller.Context() did not return a *dnsContext, got %T", initialCtx) + } + if dnsCtx.keysToConfigs == nil { + t.Fatal("dnsCtx.keysToConfigs is nil; it should have been initialized by newContext") + } + + t.Run("returns and saves default config when config missing", func(t *testing.T) { + controller.ServerBlockIndex = 0 + controller.ServerBlockKeyIndex = 0 + key := keyForConfig(controller.ServerBlockIndex, controller.ServerBlockKeyIndex) + + // Ensure config doesn't exist initially for this specific key + delete(dnsCtx.keysToConfigs, key) + + cfg := GetConfig(controller) + if cfg == nil { + t.Fatal("GetConfig returned nil (should create and return a default)") + } + if len(cfg.ListenHosts) != 1 || cfg.ListenHosts[0] != "" { + t.Errorf("Expected default ListenHosts [\"\"] for auto-created config, got %v", cfg.ListenHosts) + } + + savedCfg, found := dnsCtx.keysToConfigs[key] + if !found { + t.Fatal("fallback did not save the default config into the context") + } + if savedCfg != cfg { + t.Fatal("config is not the same instance as the one saved in the context") + } + }) +} diff --git a/core/dnsserver/https.go b/core/dnsserver/https.go index 382e06efea..437c561f2b 100644 --- a/core/dnsserver/https.go +++ b/core/dnsserver/https.go @@ -4,13 +4,11 @@ import ( "net" "net/http" - "github.com/coredns/coredns/plugin/pkg/nonwriter" + "github.com/miekg/dns" ) -// DoHWriter is a nonwriter.Writer that adds more specific LocalAddr and RemoteAddr methods. +// DoHWriter is a dns.ResponseWriter that adds more specific LocalAddr and RemoteAddr methods. type DoHWriter struct { - nonwriter.Writer - // raddr is the remote's address. This can be optionally set. raddr net.Addr // laddr is our address. This can be optionally set. @@ -18,13 +16,55 @@ type DoHWriter struct { // request is the HTTP request we're currently handling. request *http.Request + + // Msg is a response to be written to the client. + Msg *dns.Msg +} + +// WriteMsg stores the message to be written to the client. +func (d *DoHWriter) WriteMsg(m *dns.Msg) error { + d.Msg = m + return nil +} + +// Write stores the message to be written to the client. +func (d *DoHWriter) Write(b []byte) (int, error) { + d.Msg = new(dns.Msg) + return len(b), d.Msg.Unpack(b) } // RemoteAddr returns the remote address. -func (d *DoHWriter) RemoteAddr() net.Addr { return d.raddr } +func (d *DoHWriter) RemoteAddr() net.Addr { + return d.raddr +} // LocalAddr returns the local address. -func (d *DoHWriter) LocalAddr() net.Addr { return d.laddr } +func (d *DoHWriter) LocalAddr() net.Addr { + return d.laddr +} + +// Network no-op implementation. +func (d *DoHWriter) Network() string { + return "" +} + +// Request returns the HTTP request. +func (d *DoHWriter) Request() *http.Request { + return d.request +} + +// Close no-op implementation. +func (d *DoHWriter) Close() error { + return nil +} + +// TsigStatus no-op implementation. +func (d *DoHWriter) TsigStatus() error { + return nil +} + +// TsigTimersOnly no-op implementation. +func (d *DoHWriter) TsigTimersOnly(_ bool) {} -// Request returns the HTTP request -func (d *DoHWriter) Request() *http.Request { return d.request } +// Hijack no-op implementation. +func (d *DoHWriter) Hijack() {} diff --git a/core/dnsserver/https_test.go b/core/dnsserver/https_test.go new file mode 100644 index 0000000000..3d50cda641 --- /dev/null +++ b/core/dnsserver/https_test.go @@ -0,0 +1,159 @@ +package dnsserver + +import ( + "net" + "net/http" + "reflect" + "testing" +) + +func TestDoHWriter_LocalAddr(t *testing.T) { + tests := []struct { + name string + laddr net.Addr + want net.Addr + }{ + { + name: "LocalAddr", + laddr: &net.TCPAddr{}, + want: &net.TCPAddr{}, + }, + { + name: "LocalAddr", + laddr: &net.UDPAddr{}, + want: &net.UDPAddr{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DoHWriter{ + laddr: tt.laddr, + } + if got := d.LocalAddr(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("LocalAddr() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDoHWriter_RemoteAddr(t *testing.T) { + tests := []struct { + name string + want net.Addr + raddr net.Addr + }{ + { + name: "RemoteAddr", + want: &net.TCPAddr{}, + raddr: &net.TCPAddr{}, + }, + { + name: "RemoteAddr", + want: &net.UDPAddr{}, + raddr: &net.UDPAddr{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DoHWriter{ + raddr: tt.raddr, + } + if got := d.RemoteAddr(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("RemoteAddr() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDoHWriter_Request(t *testing.T) { + tests := []struct { + name string + request *http.Request + want *http.Request + }{ + { + name: "Request", + request: &http.Request{}, + want: &http.Request{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DoHWriter{ + request: tt.request, + } + if got := d.Request(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Request() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDoHWriter_Write(t *testing.T) { + tests := []struct { + name string + input []byte + wantErr bool + }{ + { + name: "valid DNS message", + // A minimal valid DNS query message + input: []byte{ + 0x00, 0x01, /* ID */ + 0x01, 0x00, /* Flags: query, recursion desired */ + 0x00, 0x01, /* Questions: 1 */ + 0x00, 0x00, /* Answer RRs: 0 */ + 0x00, 0x00, /* Authority RRs: 0 */ + 0x00, 0x00, /* Additional RRs: 0 */ + 0x03, 'w', 'w', 'w', + 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 0x03, 'c', 'o', 'm', + 0x00, /* Null terminator for domain name */ + 0x00, 0x01, /* Type: A */ + 0x00, 0x01, /* Class: IN */ + }, + wantErr: false, + }, + { + name: "empty message", + input: []byte{}, + wantErr: true, // Expect an error because unpacking an empty message will fail + }, + { + name: "invalid DNS message", + input: []byte{0x00, 0x01, 0x02}, // Truncated message + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &DoHWriter{} + n, err := d.Write(tt.input) + + if (err != nil) != tt.wantErr { + t.Errorf("Write() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && n != len(tt.input) { + t.Errorf("Write() bytes written = %v, want %v", n, len(tt.input)) + } + if !tt.wantErr && d.Msg == nil { + t.Errorf("Write() d.Msg is nil, expected a parsed message") + } + }) + } +} + +func TestDoHWriter_Close(t *testing.T) { + d := &DoHWriter{} + if err := d.Close(); err != nil { + t.Errorf("Close() error = %v, want nil", err) + } +} + +func TestDoHWriter_TsigStatus(t *testing.T) { + d := &DoHWriter{} + if err := d.TsigStatus(); err != nil { + t.Errorf("TsigStatus() error = %v, want nil", err) + } +} diff --git a/core/dnsserver/onstartup.go b/core/dnsserver/onstartup.go index 7aa519e98d..90a270badc 100644 --- a/core/dnsserver/onstartup.go +++ b/core/dnsserver/onstartup.go @@ -1,29 +1,57 @@ package dnsserver -import "fmt" +import ( + "fmt" + "regexp" + "sort" + "strings" + + "github.com/coredns/coredns/plugin/pkg/dnsutil" +) + +// checkZoneSyntax() checks whether the given string match 1035 Preferred Syntax or not. +// The root zone, and all reverse zones always return true even though they technically don't meet 1035 Preferred Syntax +func checkZoneSyntax(zone string) bool { + if zone == "." || dnsutil.IsReverse(zone) != 0 { + return true + } + regex1035PreferredSyntax, _ := regexp.MatchString(`^(([A-Za-z]([A-Za-z0-9-]*[A-Za-z0-9])?)\.)+$`, zone) + return regex1035PreferredSyntax +} // startUpZones creates the text that we show when starting up: // grpc://example.com.:1055 // example.com.:1053 on 127.0.0.1 -func startUpZones(protocol, addr string, zones map[string]*Config) string { - s := "" +func startUpZones(protocol, addr string, zones map[string][]*Config) string { + keys := make([]string, len(zones)) + i := 0 - for zone := range zones { + for k := range zones { + keys[i] = k + i++ + } + sort.Strings(keys) + + var sb strings.Builder + for _, zone := range keys { + if !checkZoneSyntax(zone) { + sb.WriteString(fmt.Sprintf("Warning: Domain %q does not follow RFC1035 preferred syntax\n", zone)) + } // split addr into protocol, IP and Port _, ip, port, err := SplitProtocolHostPort(addr) if err != nil { // this should not happen, but we need to take care of it anyway - s += fmt.Sprintln(protocol + zone + ":" + addr) + sb.WriteString(fmt.Sprintln(protocol + zone + ":" + addr)) continue } if ip == "" { - s += fmt.Sprintln(protocol + zone + ":" + port) + sb.WriteString(fmt.Sprintln(protocol + zone + ":" + port)) continue } // if the server is listening on a specific address let's make it visible in the log, // so one can differentiate between all active listeners - s += fmt.Sprintln(protocol + zone + ":" + port + " on " + ip) + sb.WriteString(fmt.Sprintln(protocol + zone + ":" + port + " on " + ip)) } - return s + return sb.String() } diff --git a/core/dnsserver/onstartup_test.go b/core/dnsserver/onstartup_test.go new file mode 100644 index 0000000000..4af376e011 --- /dev/null +++ b/core/dnsserver/onstartup_test.go @@ -0,0 +1,146 @@ +package dnsserver + +import ( + "testing" +) + +func TestRegex1035PrefSyntax(t *testing.T) { + testCases := []struct { + zone string + expected bool + }{ + {zone: ".", expected: true}, + {zone: "example.com.", expected: true}, + {zone: "example.", expected: true}, + {zone: "example123.", expected: true}, + {zone: "example123.com.", expected: true}, + {zone: "abc-123.com.", expected: true}, + {zone: "an-example.com.", expected: true}, + {zone: "a.example.com.", expected: true}, + {zone: "1.0.0.2.ip6.arpa.", expected: true}, + {zone: "0.10.in-addr.arpa.", expected: true}, + {zone: "example", expected: false}, + {zone: "example:.", expected: false}, + {zone: "-example.com.", expected: false}, + {zone: ".example.com.", expected: false}, + {zone: "1.example.com", expected: false}, + {zone: "abc.123-xyz.", expected: false}, + {zone: "example-?&^%$.com.", expected: false}, + {zone: "abc-.example.com.", expected: false}, + {zone: "abc-%$.example.com.", expected: false}, + {zone: "123-abc.example.com.", expected: false}, + } + + for _, testCase := range testCases { + if checkZoneSyntax(testCase.zone) != testCase.expected { + t.Errorf("Expected %v for %q", testCase.expected, testCase.zone) + } + } +} + +func TestStartUpZones(t *testing.T) { + tests := []struct { + name string + protocol string + addr string + zones map[string][]*Config + expectedOutput string + }{ + { + name: "no zones", + protocol: "dns://", + addr: "127.0.0.1:53", + zones: map[string][]*Config{}, + expectedOutput: "", + }, + { + name: "single zone valid syntax ip and port", + protocol: "dns://", + addr: "127.0.0.1:53", + zones: map[string][]*Config{"example.com.": nil}, + expectedOutput: "dns://example.com.:53 on 127.0.0.1\n", + }, + { + name: "single zone valid syntax port only", + protocol: "http://", + addr: ":8080", + zones: map[string][]*Config{"example.org.": nil}, + expectedOutput: "http://example.org.:8080\n", + }, + { + name: "single zone invalid syntax", + protocol: "tls://", + addr: "10.0.0.1:853", + zones: map[string][]*Config{"invalid-zone": nil}, + expectedOutput: "Warning: Domain \"invalid-zone\" does not follow RFC1035 preferred syntax\n" + + "tls://invalid-zone:853 on 10.0.0.1\n", + }, + { + name: "multiple zones sorted order", + protocol: "dns://", + addr: "localhost:5353", + zones: map[string][]*Config{ + "c-zone.com.": nil, + "a-zone.org.": nil, + "b-zone.net.": nil, + }, + expectedOutput: "dns://a-zone.org.:5353 on localhost\n" + + "dns://b-zone.net.:5353 on localhost\n" + + "dns://c-zone.com.:5353 on localhost\n", + }, + { + name: "addr parse error", + protocol: "grpc://", + addr: "[::1]:8080:extra", // Malformed, should cause SplitProtocolHostPort to error + zones: map[string][]*Config{"error.example.": nil}, + expectedOutput: "grpc://error.example.:[::1]:8080:extra\n", + }, + { + name: "root zone", + protocol: "dns://", + addr: "192.168.1.1:53", + zones: map[string][]*Config{".": nil}, + expectedOutput: "dns://.:53 on 192.168.1.1\n", + }, + { + name: "reverse zone", + protocol: "dns://", + addr: ":53", + zones: map[string][]*Config{"1.0.168.192.in-addr.arpa.": nil}, + expectedOutput: "dns://1.0.168.192.in-addr.arpa.:53\n", + }, + { + name: "multiple zones mixed syntax and addr handling", + protocol: "quic://", + addr: "coolserver.local:784", + zones: map[string][]*Config{ + "valid.net.": nil, + "_tcp.service.": nil, // Invalid syntax + "another.valid.com.": nil, + }, + expectedOutput: "Warning: Domain \"_tcp.service.\" does not follow RFC1035 preferred syntax\n" + + "quic://_tcp.service.:784 on coolserver.local\n" + + "quic://another.valid.com.:784 on coolserver.local\n" + + "quic://valid.net.:784 on coolserver.local\n", + }, + { + name: "zone with leading dash invalid", + protocol: "dns://", + addr: "127.0.0.1:53", + zones: map[string][]*Config{"-leadingdash.com.": nil}, + expectedOutput: "Warning: Domain \"-leadingdash.com.\" does not follow RFC1035 preferred syntax\n" + + "dns://-leadingdash.com.:53 on 127.0.0.1\n", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := startUpZones(tc.protocol, tc.addr, tc.zones) + if got != tc.expectedOutput { + // Use %q for expected and got to make differences in whitespace/newlines visible. + t.Errorf("startUpZones(%q, %q, ...) mismatch for test '%s':\nGot:\n%q\nExpected:\n%q", + tc.protocol, tc.addr, tc.name, got, tc.expectedOutput) + } + }) + } +} diff --git a/core/dnsserver/quic.go b/core/dnsserver/quic.go new file mode 100644 index 0000000000..f7cde74a81 --- /dev/null +++ b/core/dnsserver/quic.go @@ -0,0 +1,68 @@ +package dnsserver + +import ( + "encoding/binary" + "errors" + "net" + + "github.com/miekg/dns" + "github.com/quic-go/quic-go" +) + +type DoQWriter struct { + localAddr net.Addr + remoteAddr net.Addr + stream *quic.Stream + Msg *dns.Msg +} + +func (w *DoQWriter) Write(b []byte) (int, error) { + if w.stream == nil { + return 0, errors.New("stream is nil") + } + b = AddPrefix(b) + return w.stream.Write(b) +} + +func (w *DoQWriter) WriteMsg(m *dns.Msg) error { + bytes, err := m.Pack() + if err != nil { + return err + } + + _, err = w.Write(bytes) + if err != nil { + return err + } + + return w.Close() +} + +// Close sends the STREAM FIN signal. +// The server MUST send the response(s) on the same stream and MUST +// indicate, after the last response, through the STREAM FIN +// mechanism that no further data will be sent on that stream. +// See https://www.rfc-editor.org/rfc/rfc9250#section-4.2-7 +func (w *DoQWriter) Close() error { + if w.stream == nil { + return errors.New("stream is nil") + } + return w.stream.Close() +} + +// AddPrefix adds a 2-byte prefix with the DNS message length. +func AddPrefix(b []byte) (m []byte) { + m = make([]byte, 2+len(b)) + binary.BigEndian.PutUint16(m, uint16(len(b))) + copy(m[2:], b) + + return m +} + +// These methods implement the dns.ResponseWriter interface from Go DNS. +func (w *DoQWriter) TsigStatus() error { return nil } +func (w *DoQWriter) TsigTimersOnly(b bool) {} +func (w *DoQWriter) Hijack() {} +func (w *DoQWriter) LocalAddr() net.Addr { return w.localAddr } +func (w *DoQWriter) RemoteAddr() net.Addr { return w.remoteAddr } +func (w *DoQWriter) Network() string { return "" } diff --git a/core/dnsserver/quic_test.go b/core/dnsserver/quic_test.go new file mode 100644 index 0000000000..7e73019066 --- /dev/null +++ b/core/dnsserver/quic_test.go @@ -0,0 +1,50 @@ +package dnsserver + +import ( + "net" + "testing" +) + +func TestDoQWriterAddPrefix(t *testing.T) { + byteArray := []byte{0x1, 0x2, 0x3} + + byteArrayWithPrefix := AddPrefix(byteArray) + + if len(byteArrayWithPrefix) != 5 { + t.Error("Expected byte array with prefix to have length of 5") + } + + size := int16(byteArrayWithPrefix[0])<<8 | int16(byteArrayWithPrefix[1]) + if size != 3 { + t.Errorf("Expected prefixed size to be 3, got: %d", size) + } +} + +func TestDoQWriter_ResponseWriterMethods(t *testing.T) { + localAddr := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + remoteAddr := &net.UDPAddr{IP: net.ParseIP("8.8.8.8"), Port: 53} + + writer := &DoQWriter{ + localAddr: localAddr, + remoteAddr: remoteAddr, + } + + if err := writer.TsigStatus(); err != nil { + t.Errorf("TsigStatus() returned an error: %v", err) + } + + // this is a no-op, just call it + writer.TsigTimersOnly(true) + writer.TsigTimersOnly(false) + + // this is a no-op, just call it + writer.Hijack() + + if addr := writer.LocalAddr(); addr != localAddr { + t.Errorf("LocalAddr() = %v, want %v", addr, localAddr) + } + + if addr := writer.RemoteAddr(); addr != remoteAddr { + t.Errorf("RemoteAddr() = %v, want %v", addr, remoteAddr) + } +} diff --git a/core/dnsserver/register.go b/core/dnsserver/register.go index b4e9f4fde9..f289ceaa51 100644 --- a/core/dnsserver/register.go +++ b/core/dnsserver/register.go @@ -1,27 +1,22 @@ package dnsserver import ( - "flag" "fmt" "net" - "strings" "time" "github.com/coredns/caddy" "github.com/coredns/caddy/caddyfile" "github.com/coredns/coredns/plugin" - "github.com/coredns/coredns/plugin/pkg/dnsutil" "github.com/coredns/coredns/plugin/pkg/parse" "github.com/coredns/coredns/plugin/pkg/transport" + + "github.com/miekg/dns" ) const serverType = "dns" -// Any flags defined here, need to be namespaced to the serverType other -// wise they potentially clash with other server types. func init() { - flag.StringVar(&Port, serverType+".port", DefaultPort, "Default port") - caddy.RegisterServerType(serverType, caddy.ServerType{ Directives: func() []string { return Directives }, DefaultInput: func() caddy.Input { @@ -60,11 +55,59 @@ var _ caddy.Context = &dnsContext{} func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) { // Normalize and check all the zone names and check for duplicates for ib, s := range serverBlocks { + // Walk the s.Keys and expand any reverse address in their proper DNS in-addr zones. If the expansions leads for + // more than one reverse zone, replace the current value and add the rest to s.Keys. + zoneAddrs := []zoneAddr{} for ik, k := range s.Keys { - za, err := normalizeZone(k) + trans, k1 := parse.Transport(k) // get rid of any dns:// or other scheme. + hosts, port, err := plugin.SplitHostPort(k1) + // We need to make this a fully qualified domain name to catch all errors here and not later when + // plugin.Normalize is called again on these strings, with the prime difference being that the domain + // name is fully qualified. This was found by fuzzing where "ȶ" is deemed OK, but "ȶ." is not (might be a + // bug in miekg/dns actually). But here we were checking ȶ, which is OK, and later we barf in ȶ. leading to + // "index out of range". + for ih := range hosts { + _, _, err := plugin.SplitHostPort(dns.Fqdn(hosts[ih])) + if err != nil { + return nil, err + } + } if err != nil { return nil, err } + + if port == "" { + switch trans { + case transport.DNS: + port = Port + case transport.TLS: + port = transport.TLSPort + case transport.QUIC: + port = transport.QUICPort + case transport.GRPC: + port = transport.GRPCPort + case transport.HTTPS: + port = transport.HTTPSPort + } + } + + if len(hosts) > 1 { + s.Keys[ik] = hosts[0] + ":" + port // replace for the first + for _, h := range hosts[1:] { // add the rest + s.Keys = append(s.Keys, h+":"+port) + } + } + for i := range hosts { + zoneAddrs = append(zoneAddrs, zoneAddr{Zone: dns.Fqdn(hosts[i]), Port: port, Transport: trans}) + } + } + + serverBlocks[ib].Keys = s.Keys // important to save back the new keys that are potentially created here. + + var firstConfigInBlock *Config + + for ik := range s.Keys { + za := zoneAddrs[ik] s.Keys[ik] = za.String() // Save the config to our master list, and key it for lookups. cfg := &Config{ @@ -73,23 +116,16 @@ func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddy Port: za.Port, Transport: za.Transport, } - keyConfig := keyForConfig(ib, ik) - if za.IPNet == nil { - h.saveConfig(keyConfig, cfg) - continue - } - - ones, bits := za.IPNet.Mask.Size() - if (bits-ones)%8 != 0 { // only do this for non-octet boundaries - cfg.FilterFunc = func(s string) bool { - // TODO(miek): strings.ToLower! Slow and allocates new string. - addr := dnsutil.ExtractAddressFromReverse(strings.ToLower(s)) - if addr == "" { - return true - } - return za.IPNet.Contains(net.ParseIP(addr)) - } + + // Set reference to the first config in the current block. + // This is used later by MakeServers to share a single plugin list + // for all zones in a server block. + if ik == 0 { + firstConfigInBlock = cfg } + cfg.firstConfigInBlock = firstConfigInBlock + + keyConfig := keyForConfig(ib, ik) h.saveConfig(keyConfig, cfg) } } @@ -98,53 +134,44 @@ func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddy // MakeServers uses the newly-created siteConfigs to create and return a list of server instances. func (h *dnsContext) MakeServers() ([]caddy.Server, error) { - - // Now that all Keys and Directives are parsed and initialized - // lets verify that there is no overlap on the zones and addresses to listen for - errValid := h.validateZonesAndListeningAddresses() - if errValid != nil { - return nil, errValid - } + // Copy parameters from first config in the block to all other config in the same block + propagateConfigParams(h.configs) // we must map (group) each config to a bind address groups, err := groupConfigsByListenAddr(h.configs) if err != nil { return nil, err } + // then we create a server for each group var servers []caddy.Server for addr, group := range groups { - // switch on addr - switch tr, _ := parse.Transport(addr); tr { - case transport.DNS: - s, err := NewServer(addr, group) - if err != nil { - return nil, err - } - servers = append(servers, s) - - case transport.TLS: - s, err := NewServerTLS(addr, group) - if err != nil { - return nil, err - } - servers = append(servers, s) - - case transport.GRPC: - s, err := NewServergRPC(addr, group) - if err != nil { - return nil, err - } - servers = append(servers, s) + serversForGroup, err := makeServersForGroup(addr, group) + if err != nil { + return nil, err + } + servers = append(servers, serversForGroup...) + } - case transport.HTTPS: - s, err := NewServerHTTPS(addr, group) - if err != nil { - return nil, err + // For each server config, check for View Filter plugins + for _, c := range h.configs { + // Add filters in the plugin.cfg order for consistent filter func evaluation order. + for _, d := range Directives { + if vf, ok := c.registry[d].(Viewer); ok { + if c.ViewName != "" { + return nil, fmt.Errorf("multiple views defined in server block") + } + c.ViewName = vf.ViewName() + c.FilterFuncs = append(c.FilterFuncs, vf.Filter) } - servers = append(servers, s) } + } + // Verify that there is no overlap on the zones and listen addresses + // for unfiltered server configs + errValid := h.validateZonesAndListeningAddresses() + if errValid != nil { + return nil, errValid } return servers, nil @@ -156,7 +183,8 @@ func (c *Config) AddPlugin(m plugin.Plugin) { } // registerHandler adds a handler to a site's handler registration. Handlers -// use this to announce that they exist to other plugin. +// +// use this to announce that they exist to other plugin. func (c *Config) registerHandler(h plugin.Handler) { if c.registry == nil { c.registry = make(map[string]plugin.Handler) @@ -189,8 +217,11 @@ func (c *Config) Handlers() []plugin.Handler { return nil } hs := make([]plugin.Handler, 0, len(c.registry)) - for k := range c.registry { - hs = append(hs, c.registry[k]) + for _, k := range Directives { + registry := c.Handler(k) + if registry != nil { + hs = append(hs, registry) + } } return hs } @@ -202,27 +233,53 @@ func (h *dnsContext) validateZonesAndListeningAddresses() error { for _, h := range conf.ListenHosts { // Validate the overlapping of ZoneAddr akey := zoneAddr{Transport: conf.Transport, Zone: conf.Zone, Address: h, Port: conf.Port} - existZone, overlapZone := checker.registerAndCheck(akey) + var existZone, overlapZone *zoneAddr + if len(conf.FilterFuncs) > 0 { + // This config has filters. Check for overlap with other (unfiltered) configs. + existZone, overlapZone = checker.check(akey) + } else { + // This config has no filters. Check for overlap with other (unfiltered) configs, + // and register the zone to prevent subsequent zones from overlapping with it. + existZone, overlapZone = checker.registerAndCheck(akey) + } if existZone != nil { return fmt.Errorf("cannot serve %s - it is already defined", akey.String()) } if overlapZone != nil { return fmt.Errorf("cannot serve %s - zone overlap listener capacity with %v", akey.String(), overlapZone.String()) } - } } return nil +} +// propagateConfigParams copies the necessary parameters from first config in the block +// to all other config in the same block. Doing this results in zones +// sharing the same plugin instances and settings as other zones in +// the same block. +func propagateConfigParams(configs []*Config) { + for _, c := range configs { + c.Plugin = c.firstConfigInBlock.Plugin + c.ListenHosts = c.firstConfigInBlock.ListenHosts + c.Debug = c.firstConfigInBlock.Debug + c.Stacktrace = c.firstConfigInBlock.Stacktrace + c.NumSockets = c.firstConfigInBlock.NumSockets + + // Fork TLSConfig for each encrypted connection + c.TLSConfig = c.firstConfigInBlock.TLSConfig.Clone() + c.ReadTimeout = c.firstConfigInBlock.ReadTimeout + c.WriteTimeout = c.firstConfigInBlock.WriteTimeout + c.IdleTimeout = c.firstConfigInBlock.IdleTimeout + c.TsigSecret = c.firstConfigInBlock.TsigSecret + } } -// groupSiteConfigsByListenAddr groups site configs by their listen +// groupConfigsByListenAddr groups site configs by their listen // (bind) address, so sites that use the same listener can be served // on the same server instance. The return value maps the listen // address (what you pass into net.Listen) to the list of site configs. // This function does NOT vet the configs to ensure they are compatible. func groupConfigsByListenAddr(configs []*Config) (map[string][]*Config, error) { - groups := make(map[string][]*Config) for _, conf := range configs { for _, h := range conf.ListenHosts { @@ -238,6 +295,63 @@ func groupConfigsByListenAddr(configs []*Config) (map[string][]*Config, error) { return groups, nil } +// makeServersForGroup creates servers for a specific transport and group. +// It creates as many servers as specified in the NumSockets configuration. +// If the NumSockets param is not specified, one server is created by default. +func makeServersForGroup(addr string, group []*Config) ([]caddy.Server, error) { + // that is impossible, but better to check + if len(group) == 0 { + return nil, fmt.Errorf("no configs for group defined") + } + // create one server by default if no NumSockets specified + numSockets := 1 + if group[0].NumSockets > 0 { + numSockets = group[0].NumSockets + } + + var servers []caddy.Server + for range numSockets { + // switch on addr + switch tr, _ := parse.Transport(addr); tr { + case transport.DNS: + s, err := NewServer(addr, group) + if err != nil { + return nil, err + } + servers = append(servers, s) + + case transport.TLS: + s, err := NewServerTLS(addr, group) + if err != nil { + return nil, err + } + servers = append(servers, s) + + case transport.QUIC: + s, err := NewServerQUIC(addr, group) + if err != nil { + return nil, err + } + servers = append(servers, s) + + case transport.GRPC: + s, err := NewServergRPC(addr, group) + if err != nil { + return nil, err + } + servers = append(servers, s) + + case transport.HTTPS: + s, err := NewServerHTTPS(addr, group) + if err != nil { + return nil, err + } + servers = append(servers, s) + } + } + return servers, nil +} + // DefaultPort is the default port. const DefaultPort = transport.Port @@ -250,5 +364,3 @@ var ( // GracefulTimeout is the maximum duration of a graceful shutdown. GracefulTimeout time.Duration ) - -var _ caddy.GracefulServer = new(Server) diff --git a/core/dnsserver/register_test.go b/core/dnsserver/register_test.go index a2d24e1dd7..b8d594f80d 100644 --- a/core/dnsserver/register_test.go +++ b/core/dnsserver/register_test.go @@ -2,6 +2,8 @@ package dnsserver import ( "testing" + + "github.com/coredns/caddy/caddyfile" ) func TestHandler(t *testing.T) { @@ -10,7 +12,7 @@ func TestHandler(t *testing.T) { if _, err := NewServer("127.0.0.1:53", []*Config{c}); err != nil { t.Errorf("Expected no error for NewServer, got %s", err) } - if h := c.Handler("testplugin"); h != tp { + if h := c.Handler("local"); h != tp { t.Errorf("Expected testPlugin from Handler, got %T", h) } if h := c.Handler("nothing"); h != nil { @@ -114,8 +116,219 @@ func TestGroupingServers(t *testing.T) { for _, v := range test.expectedGroups { if _, ok := groups[v]; !ok { t.Errorf("Test %d : expected value %v to be in the group, was not", i, v) - } } } } + +func TestInspectServerBlocks(t *testing.T) { + tests := []struct { + name string + serverBlocks []caddyfile.ServerBlock + expectedServerBlocks []caddyfile.ServerBlock + expectedConfigsLen int + expectedZoneAddrs map[string]zoneAddr + wantErr bool + }{ + { + name: "simple dns", + serverBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"example.org"}}, + }, + expectedServerBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"dns://example.org.:53"}}, + }, + expectedConfigsLen: 1, + expectedZoneAddrs: map[string]zoneAddr{ + "dns://example.org.:53": {Zone: "example.org.", Port: "53", Transport: "dns"}, + }, + }, + { + name: "dns with port", + serverBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"example.org:1053"}}, + }, + expectedServerBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"dns://example.org.:1053"}}, + }, + expectedConfigsLen: 1, + expectedZoneAddrs: map[string]zoneAddr{ + "dns://example.org.:1053": {Zone: "example.org.", Port: "1053", Transport: "dns"}, + }, + }, + { + name: "tls", + serverBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"tls://example.org"}}, + }, + expectedServerBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"tls://example.org.:853"}}, + }, + expectedConfigsLen: 1, + expectedZoneAddrs: map[string]zoneAddr{ + "tls://example.org.:853": {Zone: "example.org.", Port: "853", Transport: "tls"}, + }, + }, + { + name: "quic", + serverBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"quic://example.org"}}, + }, + expectedServerBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"quic://example.org.:853"}}, + }, + expectedConfigsLen: 1, + expectedZoneAddrs: map[string]zoneAddr{ + "quic://example.org.:853": {Zone: "example.org.", Port: "853", Transport: "quic"}, + }, + }, + { + name: "grpc", + serverBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"grpc://example.org"}}, + }, + expectedServerBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"grpc://example.org.:443"}}, + }, + expectedConfigsLen: 1, + expectedZoneAddrs: map[string]zoneAddr{ + "grpc://example.org.:443": {Zone: "example.org.", Port: "443", Transport: "grpc"}, + }, + }, + { + name: "https", + serverBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"https://example.org."}}, + }, + expectedServerBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"https://example.org.:443"}}, + }, + expectedConfigsLen: 1, + expectedZoneAddrs: map[string]zoneAddr{ + "https://example.org.:443": {Zone: "example.org.", Port: "443", Transport: "https"}, + }, + }, + { + name: "multiple hosts same key", + serverBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"example.org,example.com:1053"}}, + }, + expectedServerBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"dns://example.org,example.com.:1053"}}, + }, + expectedConfigsLen: 1, + expectedZoneAddrs: map[string]zoneAddr{ + "dns://example.org,example.com.:1053": {Zone: "example.org,example.com.", Port: "1053", Transport: "dns"}, + }, + }, + { + name: "multiple keys", + serverBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"example.org", "example.com:1053"}}, + }, + expectedServerBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"dns://example.org.:53", "dns://example.com.:1053"}}, + }, + expectedConfigsLen: 2, + expectedZoneAddrs: map[string]zoneAddr{ + "dns://example.org.:53": {Zone: "example.org.", Port: "53", Transport: "dns"}, + "dns://example.com.:1053": {Zone: "example.com.", Port: "1053", Transport: "dns"}, + }, + }, + { + name: "fqdn input", + serverBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"example.org."}}, + }, + expectedServerBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"dns://example.org.:53"}}, + }, + expectedConfigsLen: 1, + expectedZoneAddrs: map[string]zoneAddr{ + "dns://example.org.:53": {Zone: "example.org.", Port: "53", Transport: "dns"}, + }, + }, + { + name: "multiple server blocks", + serverBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"example.org"}}, + {Keys: []string{"sub.example.org:1054"}}, + }, + expectedServerBlocks: []caddyfile.ServerBlock{ + {Keys: []string{"dns://example.org.:53"}}, + {Keys: []string{"dns://sub.example.org.:1054"}}, + }, + expectedConfigsLen: 2, + expectedZoneAddrs: map[string]zoneAddr{ + "dns://example.org.:53": {Zone: "example.org.", Port: "53", Transport: "dns"}, + "dns://sub.example.org.:1054": {Zone: "sub.example.org.", Port: "1054", Transport: "dns"}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := newContext(nil).(*dnsContext) + processedBlocks, err := ctx.InspectServerBlocks("TestInspectServerBlocks", tc.serverBlocks) + + if (err != nil) != tc.wantErr { + t.Fatalf("InspectServerBlocks() error = %v, wantErr %v", err, tc.wantErr) + } + if tc.wantErr { + return + } + + if len(processedBlocks) != len(tc.expectedServerBlocks) { + t.Fatalf("Expected %d processed blocks, got %d", len(tc.expectedServerBlocks), len(processedBlocks)) + } + + for i, block := range processedBlocks { + expectedBlock := tc.expectedServerBlocks[i] + if len(block.Keys) != len(expectedBlock.Keys) { + t.Errorf("Block %d: expected %d keys, got %d. Expected: %v, Got: %v", i, len(expectedBlock.Keys), len(block.Keys), expectedBlock.Keys, block.Keys) + continue + } + for j, key := range block.Keys { + if key != expectedBlock.Keys[j] { + t.Errorf("Block %d, Key %d: expected key '%s', got '%s'", i, j, expectedBlock.Keys[j], key) + } + } + } + + if len(ctx.configs) != tc.expectedConfigsLen { + t.Errorf("Expected %d configs to be created, got %d", tc.expectedConfigsLen, len(ctx.configs)) + } + + if tc.expectedZoneAddrs != nil { + configIndex := 0 + for ib := range processedBlocks { + for ik, key := range processedBlocks[ib].Keys { + if configIndex >= len(ctx.configs) { + t.Fatalf("Not enough configs stored, expected at least %d, processed block %d key %d", configIndex+1, ib, ik) + } + cfg := ctx.configs[configIndex] + expectedZa, ok := tc.expectedZoneAddrs[key] + if !ok { + t.Errorf("No expected zoneAddr for processed key '%s'", key) + continue + } + + if cfg.Zone != expectedZa.Zone { + t.Errorf("Config for key '%s': expected Zone '%s', got '%s'", key, expectedZa.Zone, cfg.Zone) + } + if cfg.Port != expectedZa.Port { + t.Errorf("Config for key '%s': expected Port '%s', got '%s'", key, expectedZa.Port, cfg.Port) + } + if cfg.Transport != expectedZa.Transport { + t.Errorf("Config for key '%s': expected Transport '%s', got '%s'", key, expectedZa.Transport, cfg.Transport) + } + if len(cfg.ListenHosts) != 1 || cfg.ListenHosts[0] != "" { + t.Errorf("Config for key '%s': expected ListenHosts [''], got %v", key, cfg.ListenHosts) + } + configIndex++ + } + } + } + }) + } +} diff --git a/core/dnsserver/server.go b/core/dnsserver/server.go index c7304d7630..3f7441dfcc 100644 --- a/core/dnsserver/server.go +++ b/core/dnsserver/server.go @@ -4,8 +4,9 @@ package dnsserver import ( "context" "fmt" + "maps" "net" - "runtime" + "runtime/debug" "strings" "sync" "time" @@ -31,44 +32,69 @@ import ( // the same address and the listener may be stopped for // graceful termination (POSIX only). type Server struct { - Addr string // Address we listen on + Addr string // Address we listen on + IdleTimeout time.Duration // Idle timeout for TCP + ReadTimeout time.Duration // Read timeout for TCP + WriteTimeout time.Duration // Write timeout for TCP server [2]*dns.Server // 0 is a net.Listener, 1 is a net.PacketConn (a *UDPConn) in our case. m sync.Mutex // protects the servers - zones map[string]*Config // zones keyed by their address - dnsWg sync.WaitGroup // used to wait on outstanding connections - graceTimeout time.Duration // the maximum duration of a graceful shutdown - trace trace.Trace // the trace plugin for the server - debug bool // disable recover() - classChaos bool // allow non-INET class queries + zones map[string][]*Config // zones keyed by their address + graceTimeout time.Duration // the maximum duration of a graceful shutdown + trace trace.Trace // the trace plugin for the server + debug bool // disable recover() + stacktrace bool // enable stacktrace in recover error log + classChaos bool // allow non-INET class queries + + tsigSecret map[string]string + + // Ensure Stop is idempotent when invoked concurrently (e.g., during reload and SIGTERM). + stopOnce sync.Once + stopErr error +} + +// MetadataCollector is a plugin that can retrieve metadata functions from all metadata providing plugins +type MetadataCollector interface { + Collect(context.Context, request.Request) context.Context } // NewServer returns a new CoreDNS server and compiles all plugins in to it. By default CH class // queries are blocked unless queries from enableChaos are loaded. func NewServer(addr string, group []*Config) (*Server, error) { - s := &Server{ Addr: addr, - zones: make(map[string]*Config), + zones: make(map[string][]*Config), graceTimeout: 5 * time.Second, + IdleTimeout: 10 * time.Second, + ReadTimeout: 3 * time.Second, + WriteTimeout: 5 * time.Second, + tsigSecret: make(map[string]string), } - // We have to bound our wg with one increment - // to prevent a "race condition" that is hard-coded - // into sync.WaitGroup.Wait() - basically, an add - // with a positive delta must be guaranteed to - // occur before Wait() is called on the wg. - // In a way, this kind of acts as a safety barrier. - s.dnsWg.Add(1) - for _, site := range group { if site.Debug { s.debug = true log.D.Set() } - // set the config per zone - s.zones[site.Zone] = site + s.stacktrace = site.Stacktrace + + // append the config to the zone's configs + s.zones[site.Zone] = append(s.zones[site.Zone], site) + + // set timeouts + if site.ReadTimeout != 0 { + s.ReadTimeout = site.ReadTimeout + } + if site.WriteTimeout != 0 { + s.WriteTimeout = site.WriteTimeout + } + if site.IdleTimeout != 0 { + s.IdleTimeout = site.IdleTimeout + } + + // copy tsig secrets + maps.Copy(s.tsigSecret, site.TsigSecret) // compile custom plugin for everything var stack plugin.Handler @@ -78,6 +104,12 @@ func NewServer(addr string, group []*Config) (*Server, error) { // register the *handler* also site.registerHandler(stack) + // If the current plugin is a MetadataCollector, bookmark it for later use. This loop traverses the plugin + // list backwards, so the first MetadataCollector plugin wins. + if mdc, ok := stack.(MetadataCollector); ok { + site.metaCollector = mdc + } + if s.trace == nil && stack.Name() == "trace" { // we have to stash away the plugin, not the // Tracer object, because the Tracer won't be initialized yet @@ -108,11 +140,22 @@ var _ caddy.GracefulServer = &Server{} // This implements caddy.TCPServer interface. func (s *Server) Serve(l net.Listener) error { s.m.Lock() - s.server[tcp] = &dns.Server{Listener: l, Net: "tcp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) { - ctx := context.WithValue(context.Background(), Key{}, s) - ctx = context.WithValue(ctx, LoopKey{}, 0) - s.ServeDNS(ctx, w, r) - })} + + s.server[tcp] = &dns.Server{Listener: l, + Net: "tcp", + TsigSecret: s.tsigSecret, + MaxTCPQueries: tcpMaxQueries, + ReadTimeout: s.ReadTimeout, + WriteTimeout: s.WriteTimeout, + IdleTimeout: func() time.Duration { + return s.IdleTimeout + }, + Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) { + ctx := context.WithValue(context.Background(), Key{}, s) + ctx = context.WithValue(ctx, LoopKey{}, 0) + s.ServeDNS(ctx, w, r) + })} + s.m.Unlock() return s.server[tcp].ActivateAndServe() @@ -126,7 +169,7 @@ func (s *Server) ServePacket(p net.PacketConn) error { ctx := context.WithValue(context.Background(), Key{}, s) ctx = context.WithValue(ctx, LoopKey{}, 0) s.ServeDNS(ctx, w, r) - })} + }), TsigSecret: s.tsigSecret} s.m.Unlock() return s.server[udp].ActivateAndServe() @@ -156,41 +199,36 @@ func (s *Server) ListenPacket() (net.PacketConn, error) { return p, nil } -// Stop stops the server. It blocks until the server is -// totally stopped. On POSIX systems, it will wait for -// connections to close (up to a max timeout of a few -// seconds); on Windows it will close the listener -// immediately. +// Stop attempts to gracefully stop the server. +// It waits until the server is stopped and its connections are closed, +// up to a max timeout of a few seconds. If unsuccessful, an error is returned. +// // This implements Caddy.Stopper interface. -func (s *Server) Stop() (err error) { - - if runtime.GOOS != "windows" { - // force connections to close after timeout - done := make(chan struct{}) - go func() { - s.dnsWg.Done() // decrement our initial increment used as a barrier - s.dnsWg.Wait() - close(done) - }() +func (s *Server) Stop() error { + s.stopOnce.Do(func() { + ctx, cancelCtx := context.WithTimeout(context.Background(), s.graceTimeout) + defer cancelCtx() + + var wg sync.WaitGroup + s.m.Lock() + for _, s1 := range s.server { + // We might not have started and initialized the full set of servers + if s1 == nil { + continue + } - // Wait for remaining connections to finish or - // force them all to close after timeout - select { - case <-time.After(s.graceTimeout): - case <-done: + wg.Add(1) + go func() { + s1.ShutdownContext(ctx) + wg.Done() + }() } - } + s.m.Unlock() + wg.Wait() - // Close the listener now; this stops the server without delay - s.m.Lock() - for _, s1 := range s.server { - // We might not have started and initialized the full set of servers - if s1 != nil { - err = s1.Shutdown() - } - } - s.m.Unlock() - return + s.stopErr = ctx.Err() + }) + return s.stopErr } // Address together with Stop() implement caddy.GracefulServer. @@ -213,7 +251,11 @@ func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) // In case the user doesn't enable error plugin, we still // need to make sure that we stay alive up here if rec := recover(); rec != nil { - log.Errorf("Recovered from panic in server: %q", s.Addr) + if s.stacktrace { + log.Errorf("Recovered from panic in server: %q %v\n%s", s.Addr, rec, string(debug.Stack())) + } else { + log.Errorf("Recovered from panic in server: %q %v", s.Addr, rec) + } vars.Panic.Inc() errorAndMetricsFunc(s.Addr, w, r, dns.RcodeServerFailure) } @@ -241,35 +283,39 @@ func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ) for { - if h, ok := s.zones[q[off:]]; ok { - if h.pluginChain == nil { // zone defined, but has not got any plugins - errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused) - return - } - if r.Question[0].Qtype != dns.TypeDS { - if h.FilterFunc == nil { - rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) - if !plugin.ClientWrite(rcode) { - errorFunc(s.Addr, w, r, rcode) - } + if z, ok := s.zones[q[off:]]; ok { + for _, h := range z { + if h.pluginChain == nil { // zone defined, but has not got any plugins + errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused) return } - // FilterFunc is set, call it to see if we should use this handler. - // This is given to full query name. - if h.FilterFunc(q) { - rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) - if !plugin.ClientWrite(rcode) { - errorFunc(s.Addr, w, r, rcode) + + if h.metaCollector != nil { + // Collect metadata now, so it can be used before we send a request down the plugin chain. + ctx = h.metaCollector.Collect(ctx, request.Request{Req: r, W: w}) + } + + // If all filter funcs pass, use this config. + if passAllFilterFuncs(ctx, h.FilterFuncs, &request.Request{Req: r, W: w}) { + if h.ViewName != "" { + // if there was a view defined for this Config, set the view name in the context + ctx = context.WithValue(ctx, ViewKey{}, h.ViewName) } - return + if r.Question[0].Qtype != dns.TypeDS { + rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) + if !plugin.ClientWrite(rcode) { + errorFunc(s.Addr, w, r, rcode) + } + return + } + // The type is DS, keep the handler, but keep on searching as maybe we are serving + // the parent as well and the DS should be routed to it - this will probably *misroute* DS + // queries to a possibly grand parent, but there is no way for us to know at this point + // if there is an actual delegation from grandparent -> parent -> zone. + // In all fairness: direct DS queries should not be needed. + dshandler = h } } - // The type is DS, keep the handler, but keep on searching as maybe we are serving - // the parent as well and the DS should be routed to it - this will probably *misroute* DS - // queries to a possibly grand parent, but there is no way for us to know at this point - // if there is an actual delegation from grandparent -> parent -> zone. - // In all fairness: direct DS queries should not be needed. - dshandler = h } off, end = dns.NextLabel(q, off) if end { @@ -287,18 +333,46 @@ func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) } // Wildcard match, if we have found nothing try the root zone as a last resort. - if h, ok := s.zones["."]; ok && h.pluginChain != nil { - rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) - if !plugin.ClientWrite(rcode) { - errorFunc(s.Addr, w, r, rcode) + if z, ok := s.zones["."]; ok { + for _, h := range z { + if h.pluginChain == nil { + continue + } + + if h.metaCollector != nil { + // Collect metadata now, so it can be used before we send a request down the plugin chain. + ctx = h.metaCollector.Collect(ctx, request.Request{Req: r, W: w}) + } + + // If all filter funcs pass, use this config. + if passAllFilterFuncs(ctx, h.FilterFuncs, &request.Request{Req: r, W: w}) { + if h.ViewName != "" { + // if there was a view defined for this Config, set the view name in the context + ctx = context.WithValue(ctx, ViewKey{}, h.ViewName) + } + rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) + if !plugin.ClientWrite(rcode) { + errorFunc(s.Addr, w, r, rcode) + } + return + } } - return } // Still here? Error out with REFUSED. errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused) } +// passAllFilterFuncs returns true if all filter funcs evaluate to true for the given request +func passAllFilterFuncs(ctx context.Context, filterFuncs []FilterFunc, req *request.Request) bool { + for _, ff := range filterFuncs { + if !ff(ctx, req) { + return false + } + } + return true +} + // OnStartupComplete lists the sites served by this server // and any relevant information, assuming Quiet is false. func (s *Server) OnStartupComplete() { @@ -339,7 +413,7 @@ func errorAndMetricsFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int answer.SetRcode(r, rc) state.SizeAndDo(answer) - vars.Report(server, state, vars.Dropped, rcode.ToString(rc), answer.Len(), time.Now()) + vars.Report(server, state, vars.Dropped, "", rcode.ToString(rc), "" /* plugin */, answer.Len(), time.Now()) w.WriteMsg(answer) } @@ -347,6 +421,8 @@ func errorAndMetricsFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int const ( tcp = 0 udp = 1 + + tcpMaxQueries = -1 ) type ( @@ -355,6 +431,9 @@ type ( // LoopKey is the context key to detect server wide loops. LoopKey struct{} + + // ViewKey is the context key for the current view, if defined + ViewKey struct{} ) // EnableChaos is a map with plugin names for which we should open CH class queries as we block these by default. diff --git a/core/dnsserver/server_grpc.go b/core/dnsserver/server_grpc.go index 37cc237b72..3a4da752d2 100644 --- a/core/dnsserver/server_grpc.go +++ b/core/dnsserver/server_grpc.go @@ -22,6 +22,7 @@ import ( // ServergRPC represents an instance of a DNS-over-gRPC server. type ServergRPC struct { *Server + *pb.UnimplementedDnsServiceServer grpcServer *grpc.Server listenAddr net.Addr tlsConfig *tls.Config @@ -36,16 +37,23 @@ func NewServergRPC(addr string, group []*Config) (*ServergRPC, error) { // The *tls* plugin must make sure that multiple conflicting // TLS configuration returns an error: it can only be specified once. var tlsConfig *tls.Config - for _, conf := range s.zones { - // Should we error if some configs *don't* have TLS? - tlsConfig = conf.TLSConfig + for _, z := range s.zones { + for _, conf := range z { + // Should we error if some configs *don't* have TLS? + tlsConfig = conf.TLSConfig + } + } + // http/2 is required when using gRPC. We need to specify it in next protos + // or the upgrade won't happen. + if tlsConfig != nil { + tlsConfig.NextProtos = []string{"h2"} } return &ServergRPC{Server: s, tlsConfig: tlsConfig}, nil } -// Compile-time check to ensure Server implements the caddy.GracefulServer interface -var _ caddy.GracefulServer = &Server{} +// Compile-time check to ensure ServergRPC implements the caddy.GracefulServer interface +var _ caddy.GracefulServer = &ServergRPC{} // Serve implements caddy.TCPServer interface. func (s *ServergRPC) Serve(l net.Listener) error { @@ -54,7 +62,7 @@ func (s *ServergRPC) Serve(l net.Listener) error { s.m.Unlock() if s.Tracer() != nil { - onlyIfParent := func(parentSpanCtx opentracing.SpanContext, method string, req, resp interface{}) bool { + onlyIfParent := func(parentSpanCtx opentracing.SpanContext, method string, req, resp any) bool { return parentSpanCtx != nil } intercept := otgrpc.OpenTracingServerInterceptor(s.Tracer(), otgrpc.IncludingSpans(onlyIfParent)) @@ -76,7 +84,6 @@ func (s *ServergRPC) ServePacket(p net.PacketConn) error { return nil } // Listen implements caddy.TCPServer interface. func (s *ServergRPC) Listen() (net.Listener, error) { - l, err := reuseport.Listen("tcp", s.Addr[len(transport.GRPC+"://"):]) if err != nil { return nil, err @@ -116,7 +123,7 @@ func (s *ServergRPC) Stop() (err error) { // back to the client as a protobuf. func (s *ServergRPC) Query(ctx context.Context, in *pb.DnsPacket) (*pb.DnsPacket, error) { msg := new(dns.Msg) - err := msg.Unpack(in.Msg) + err := msg.Unpack(in.GetMsg()) if err != nil { return nil, err } @@ -174,4 +181,5 @@ func (r *gRPCresponse) TsigTimersOnly(b bool) {} func (r *gRPCresponse) Hijack() {} func (r *gRPCresponse) LocalAddr() net.Addr { return r.localAddr } func (r *gRPCresponse) RemoteAddr() net.Addr { return r.remoteAddr } +func (r *gRPCresponse) Network() string { return "" } func (r *gRPCresponse) WriteMsg(m *dns.Msg) error { r.Msg = m; return nil } diff --git a/core/dnsserver/server_grpc_test.go b/core/dnsserver/server_grpc_test.go new file mode 100644 index 0000000000..5dc72b55bd --- /dev/null +++ b/core/dnsserver/server_grpc_test.go @@ -0,0 +1,330 @@ +package dnsserver + +import ( + "context" + "crypto/tls" + "net" + "testing" + + "github.com/coredns/coredns/pb" + "github.com/coredns/coredns/plugin/pkg/transport" + + "github.com/miekg/dns" + "google.golang.org/grpc" + "google.golang.org/grpc/peer" +) + +func TestNewServergRPC(t *testing.T) { + tests := []struct { + name string + addr string + configs []*Config + wantErr bool + }{ + { + name: "valid grpc server", + addr: "127.0.0.1:0", + configs: []*Config{testConfig("grpc", testPlugin{})}, + wantErr: false, + }, + { + name: "empty configs", + addr: "127.0.0.1:0", + configs: []*Config{}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server, err := NewServergRPC(tt.addr, tt.configs) + if (err != nil) != tt.wantErr { + t.Errorf("NewServergRPC() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && server == nil { + t.Error("NewServergRPC() returned nil server without error") + } + }) + } +} + +func TestNewServergRPCWithTLS(t *testing.T) { + config := testConfig("grpc", testPlugin{}) + config.TLSConfig = &tls.Config{ + ServerName: "example.com", + } + + server, err := NewServergRPC("127.0.0.1:0", []*Config{config}) + if err != nil { + t.Fatalf("NewServergRPC() with TLS failed: %v", err) + } + + if server.tlsConfig == nil { + t.Error("Expected TLS config to be set") + } + + if len(server.tlsConfig.NextProtos) == 0 || server.tlsConfig.NextProtos[0] != "h2" { + t.Error("Expected NextProtos to include h2 for gRPC") + } +} + +func TestServergRPC_Listen(t *testing.T) { + server, err := NewServergRPC(transport.GRPC+"://127.0.0.1:0", []*Config{testConfig("grpc", testPlugin{})}) + if err != nil { + t.Fatalf("NewServergRPC() failed: %v", err) + } + + listener, err := server.Listen() + if err != nil { + t.Fatalf("Listen() failed: %v", err) + } + defer listener.Close() + + if listener == nil { + t.Error("Listen() returned nil listener") + } + + // Verify it's a TCP listener + if _, ok := listener.Addr().(*net.TCPAddr); !ok { + t.Errorf("Expected TCP listener, got %T", listener.Addr()) + } +} + +func TestServergRPC_Listen_InvalidAddress(t *testing.T) { + server, err := NewServergRPC(transport.GRPC+"://invalid:99999", []*Config{testConfig("grpc", testPlugin{})}) + if err != nil { + t.Fatalf("NewServergRPC() failed: %v", err) + } + + _, err = server.Listen() + if err == nil { + t.Error("Listen() should fail with invalid address") + } +} + +func TestServergRPC_ListenPacket(t *testing.T) { + server, err := NewServergRPC("127.0.0.1:0", []*Config{testConfig("grpc", testPlugin{})}) + if err != nil { + t.Fatalf("NewServergRPC() failed: %v", err) + } + + conn, err := server.ListenPacket() + if err != nil { + t.Errorf("ListenPacket() failed: %v", err) + } + if conn != nil { + t.Error("ListenPacket() should return nil for gRPC server") + } +} + +func TestServergRPC_ServePacket(t *testing.T) { + server, err := NewServergRPC("127.0.0.1:0", []*Config{testConfig("grpc", testPlugin{})}) + if err != nil { + t.Fatalf("NewServergRPC() failed: %v", err) + } + + err = server.ServePacket(nil) + if err != nil { + t.Errorf("ServePacket() should not return error, got: %v", err) + } +} + +func TestServergRPC_Stop(t *testing.T) { + server, err := NewServergRPC("127.0.0.1:0", []*Config{testConfig("grpc", testPlugin{})}) + if err != nil { + t.Fatalf("NewServergRPC() failed: %v", err) + } + + // Test stopping server without grpcServer initialized + err = server.Stop() + if err != nil { + t.Errorf("Stop() failed: %v", err) + } + + // Test stopping after initializing grpcServer + server.grpcServer = grpc.NewServer() + err = server.Stop() + if err != nil { + t.Errorf("Stop() with grpcServer failed: %v", err) + } +} + +func TestServergRPC_Shutdown(t *testing.T) { + server, err := NewServergRPC("127.0.0.1:0", []*Config{testConfig("grpc", testPlugin{})}) + if err != nil { + t.Fatalf("NewServergRPC() failed: %v", err) + } + + // Test shutdown without grpcServer + err = server.Shutdown() + if err != nil { + t.Errorf("Shutdown() failed: %v", err) + } + + // Test shutdown with grpcServer + server.grpcServer = grpc.NewServer() + err = server.Shutdown() + if err != nil { + t.Errorf("Shutdown() with grpcServer failed: %v", err) + } +} + +func TestServergRPC_OnStartupComplete(t *testing.T) { + server, err := NewServergRPC("127.0.0.1:53", []*Config{testConfig("grpc", testPlugin{})}) + if err != nil { + t.Fatalf("NewServergRPC() failed: %v", err) + } + + Quiet = true + server.OnStartupComplete() + + Quiet = false + server.OnStartupComplete() +} + +func TestServergRPC_Query(t *testing.T) { + server, err := NewServergRPC("127.0.0.1:0", []*Config{testConfig("grpc", testPlugin{})}) + if err != nil { + t.Fatalf("NewServergRPC() failed: %v", err) + } + + msg := new(dns.Msg) + msg.SetQuestion("example.com.", dns.TypeA) + packed, err := msg.Pack() + if err != nil { + t.Fatalf("Failed to pack DNS message: %v", err) + } + + dnsPacket := &pb.DnsPacket{Msg: packed} + + tcpAddr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:12345") + p := &peer.Peer{Addr: tcpAddr} + ctx := peer.NewContext(context.Background(), p) + + server.listenAddr = tcpAddr + + response, err := server.Query(ctx, dnsPacket) + if err != nil { + t.Errorf("Query() failed: %v", err) + } + + if len(response.GetMsg()) == 0 { + t.Error("Query() returned empty message") + } + + // Verify the response can be unpacked + respMsg := new(dns.Msg) + err = respMsg.Unpack(response.GetMsg()) + if err != nil { + t.Errorf("Failed to unpack response message: %v", err) + } +} + +func TestServergRPC_Query_ErrorCases(t *testing.T) { + server, err := NewServergRPC("127.0.0.1:0", []*Config{testConfig("grpc", testPlugin{})}) + if err != nil { + t.Fatalf("NewServergRPC() failed: %v", err) + } + + tests := []struct { + name string + ctx context.Context + packet *pb.DnsPacket + wantErr bool + }{ + { + name: "invalid DNS message", + ctx: peer.NewContext(context.Background(), &peer.Peer{Addr: &net.TCPAddr{}}), + packet: &pb.DnsPacket{Msg: []byte("invalid")}, + wantErr: true, + }, + { + name: "no peer in context", + ctx: context.Background(), + packet: &pb.DnsPacket{Msg: []byte{}}, + wantErr: true, + }, + { + name: "non-TCP peer", + ctx: peer.NewContext(context.Background(), &peer.Peer{Addr: &net.UDPAddr{}}), + packet: &pb.DnsPacket{Msg: []byte{}}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := server.Query(tt.ctx, tt.packet) + if (err != nil) != tt.wantErr { + t.Errorf("Query() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGRPCResponse(t *testing.T) { + localAddr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:53") + remoteAddr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:12345") + + r := &gRPCresponse{ + localAddr: localAddr, + remoteAddr: remoteAddr, + } + + if r.LocalAddr() != localAddr { + t.Errorf("LocalAddr() = %v, want %v", r.LocalAddr(), localAddr) + } + + if r.RemoteAddr() != remoteAddr { + t.Errorf("RemoteAddr() = %v, want %v", r.RemoteAddr(), remoteAddr) + } + + msg := new(dns.Msg) + msg.SetQuestion("example.com.", dns.TypeA) + packed, err := msg.Pack() + if err != nil { + t.Fatalf("Failed to pack DNS message: %v", err) + } + + n, err := r.Write(packed) + if err != nil { + t.Errorf("Write() failed: %v", err) + } + + if n != len(packed) { + t.Errorf("Write() returned %d, want %d", n, len(packed)) + } + + if r.Msg == nil { + t.Error("Write() did not set Msg") + } + + newMsg := new(dns.Msg) + newMsg.SetQuestion("test.com.", dns.TypeAAAA) + + err = r.WriteMsg(newMsg) + if err != nil { + t.Errorf("WriteMsg() failed: %v", err) + } + + if r.Msg != newMsg { + t.Error("WriteMsg() did not set correct message") + } + if err := r.Close(); err != nil { + t.Errorf("Close() returned error: %v", err) + } + + if err := r.TsigStatus(); err != nil { + t.Errorf("TsigStatus() returned error: %v", err) + } +} + +func TestGRPCResponse_WriteInvalidMessage(t *testing.T) { + r := &gRPCresponse{} + + _, err := r.Write([]byte("invalid dns message")) + if err == nil { + t.Error("Write() should return error for invalid DNS message") + } +} diff --git a/core/dnsserver/server_https.go b/core/dnsserver/server_https.go index 5962a5f091..cf84e8c35b 100644 --- a/core/dnsserver/server_https.go +++ b/core/dnsserver/server_https.go @@ -4,14 +4,17 @@ import ( "context" "crypto/tls" "fmt" + stdlog "log" "net" "net/http" "strconv" "time" "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/metrics/vars" "github.com/coredns/coredns/plugin/pkg/dnsutil" "github.com/coredns/coredns/plugin/pkg/doh" + clog "github.com/coredns/coredns/plugin/pkg/log" "github.com/coredns/coredns/plugin/pkg/response" "github.com/coredns/coredns/plugin/pkg/reuseport" "github.com/coredns/coredns/plugin/pkg/transport" @@ -26,6 +29,19 @@ type ServerHTTPS struct { validRequest func(*http.Request) bool } +// loggerAdapter is a simple adapter around CoreDNS logger made to implement io.Writer in order to log errors from HTTP server +type loggerAdapter struct { +} + +func (l *loggerAdapter) Write(p []byte) (n int, err error) { + clog.Debug(string(p)) + return len(p), nil +} + +// HTTPRequestKey is the context key for the HTTP request when processing DNS-over-HTTPS. +// Plugins can access the original HTTP request to retrieve headers, client IP, and metadata. +type HTTPRequestKey struct{} + // NewServerHTTPS returns a new CoreDNS HTTPS server and compiles all plugins in to it. func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, error) { s, err := NewServer(addr, group) @@ -35,30 +51,35 @@ func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, error) { // The *tls* plugin must make sure that multiple conflicting // TLS configuration returns an error: it can only be specified once. var tlsConfig *tls.Config - for _, conf := range s.zones { - // Should we error if some configs *don't* have TLS? - tlsConfig = conf.TLSConfig - } - if tlsConfig == nil { - return nil, fmt.Errorf("DoH requires TLS to be configured, see the tls plugin") + for _, z := range s.zones { + for _, conf := range z { + // Should we error if some configs *don't* have TLS? + tlsConfig = conf.TLSConfig + } } + // http/2 is recommended when using DoH. We need to specify it in next protos // or the upgrade won't happen. - tlsConfig.NextProtos = []string{"h2", "http/1.1"} + if tlsConfig != nil { + tlsConfig.NextProtos = []string{"h2", "http/1.1"} + } // Use a custom request validation func or use the standard DoH path check. var validator func(*http.Request) bool - for _, conf := range s.zones { - validator = conf.HTTPRequestValidateFunc + for _, z := range s.zones { + for _, conf := range z { + validator = conf.HTTPRequestValidateFunc + } } if validator == nil { validator = func(r *http.Request) bool { return r.URL.Path == doh.Path } } srv := &http.Server{ - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - IdleTimeout: 120 * time.Second, + ReadTimeout: s.ReadTimeout, + WriteTimeout: s.WriteTimeout, + IdleTimeout: s.IdleTimeout, + ErrorLog: stdlog.New(&loggerAdapter{}, "", 0), } sh := &ServerHTTPS{ Server: s, tlsConfig: tlsConfig, httpsServer: srv, validRequest: validator, @@ -68,8 +89,8 @@ func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, error) { return sh, nil } -// Compile-time check to ensure Server implements the caddy.GracefulServer interface -var _ caddy.GracefulServer = &Server{} +// Compile-time check to ensure ServerHTTPS implements the caddy.GracefulServer interface +var _ caddy.GracefulServer = &ServerHTTPS{} // Serve implements caddy.TCPServer interface. func (s *ServerHTTPS) Serve(l net.Listener) error { @@ -88,7 +109,6 @@ func (s *ServerHTTPS) ServePacket(p net.PacketConn) error { return nil } // Listen implements caddy.TCPServer interface. func (s *ServerHTTPS) Listen() (net.Listener, error) { - l, err := reuseport.Listen("tcp", s.Addr[len(transport.HTTPS+"://"):]) if err != nil { return nil, err @@ -125,15 +145,16 @@ func (s *ServerHTTPS) Stop() error { // ServeHTTP is the handler that gets the HTTP request and converts to the dns format, calls the plugin // chain, converts it back and write it to the client. func (s *ServerHTTPS) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if !s.validRequest(r) { http.Error(w, "", http.StatusNotFound) + s.countResponse(http.StatusNotFound) return } msg, err := doh.RequestToMsg(r) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) + s.countResponse(http.StatusBadRequest) return } @@ -148,8 +169,13 @@ func (s *ServerHTTPS) ServeHTTP(w http.ResponseWriter, r *http.Request) { // We just call the normal chain handler - all error handling is done there. // We should expect a packet to be returned that we can send to the client. - ctx := context.WithValue(context.Background(), Key{}, s.Server) + + // Propagate HTTP request context to DNS processing chain. This ensures that + // HTTP request timeouts, cancellations, and other context values are properly + // inherited by the DNS processing pipeline. + ctx := context.WithValue(r.Context(), Key{}, s.Server) ctx = context.WithValue(ctx, LoopKey{}, 0) + ctx = context.WithValue(ctx, HTTPRequestKey{}, r) s.ServeDNS(ctx, dw, msg) // See section 4.2.1 of RFC 8484. @@ -157,6 +183,7 @@ func (s *ServerHTTPS) ServeHTTP(w http.ResponseWriter, r *http.Request) { // handler has not provided any response message. if dw.Msg == nil { http.Error(w, "No response", http.StatusInternalServerError) + s.countResponse(http.StatusInternalServerError) return } @@ -166,13 +193,18 @@ func (s *ServerHTTPS) ServeHTTP(w http.ResponseWriter, r *http.Request) { age := dnsutil.MinimalTTL(dw.Msg, mt) w.Header().Set("Content-Type", doh.MimeType) - w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%f", age.Seconds())) + w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", uint32(age.Seconds()))) w.Header().Set("Content-Length", strconv.Itoa(len(buf))) w.WriteHeader(http.StatusOK) + s.countResponse(http.StatusOK) w.Write(buf) } +func (s *ServerHTTPS) countResponse(status int) { + vars.HTTPSResponsesCount.WithLabelValues(s.Addr, strconv.Itoa(status)).Inc() +} + // Shutdown stops the server (non gracefully). func (s *ServerHTTPS) Shutdown() error { if s.httpsServer != nil { diff --git a/core/dnsserver/server_https_test.go b/core/dnsserver/server_https_test.go index 6663c10758..5d062e168e 100644 --- a/core/dnsserver/server_https_test.go +++ b/core/dnsserver/server_https_test.go @@ -2,11 +2,16 @@ package dnsserver import ( "bytes" + "context" "crypto/tls" + "io" "net/http" "net/http/httptest" "regexp" "testing" + "time" + + "github.com/coredns/coredns/plugin" "github.com/miekg/dns" ) @@ -17,6 +22,7 @@ var ( ) func testServerHTTPS(t *testing.T, path string, validator func(*http.Request) bool) *http.Response { + t.Helper() c := Config{ Zone: "example.com.", Transport: "https", @@ -61,6 +67,167 @@ func TestCustomHTTPRequestValidator(t *testing.T) { if res.StatusCode != tc.expected { t.Error("unexpected HTTP code", res.StatusCode) } + res.Body.Close() }) } } + +type contextCapturingPlugin struct { + capturedContext context.Context + contextCancelled bool +} + +func (p *contextCapturingPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + p.capturedContext = ctx + select { + case <-ctx.Done(): + p.contextCancelled = true + default: + } + + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative = true + w.WriteMsg(m) + return dns.RcodeSuccess, nil +} + +func (p *contextCapturingPlugin) Name() string { return "context_capturing" } + +func testConfigWithPlugin(p *contextCapturingPlugin) *Config { + c := &Config{ + Zone: "example.com.", + Transport: "https", + TLSConfig: &tls.Config{}, + ListenHosts: []string{"127.0.0.1"}, + Port: "443", + } + c.AddPlugin(func(next plugin.Handler) plugin.Handler { return p }) + return c +} + +func TestHTTPRequestContextPropagation(t *testing.T) { + plugin := &contextCapturingPlugin{} + + s, err := NewServerHTTPS("127.0.0.1:443", []*Config{testConfigWithPlugin(plugin)}) + if err != nil { + t.Fatal("could not create HTTPS server:", err) + } + + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + buf, err := m.Pack() + if err != nil { + t.Fatal(err) + } + t.Run("context values propagation", func(t *testing.T) { + contextValue := "test-request-id" + + r := httptest.NewRequest(http.MethodPost, "/dns-query", io.NopCloser(bytes.NewReader(buf))) + ctx := context.WithValue(r.Context(), Key{}, contextValue) + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + s.ServeHTTP(w, r) + + if plugin.capturedContext == nil { + t.Fatal("No context received in plugin") + } + + if val := plugin.capturedContext.Value(Key{}); val != s.Server { + t.Error("Server key not properly set in context") + } + + if httpReq, ok := plugin.capturedContext.Value(HTTPRequestKey{}).(*http.Request); !ok { + t.Error("HTTPRequestKey not found in context") + } else if httpReq != r { + t.Error("HTTPRequestKey contains different request than expected") + } + }) + + t.Run("plugins can access HTTP request details", func(t *testing.T) { + r := httptest.NewRequest(http.MethodPost, "/dns-query", io.NopCloser(bytes.NewReader(buf))) + r.Header.Set("User-Agent", "my-doh-client/2.1") + r.Header.Set("X-Forwarded-For", "10.10.10.10") + r.Header.Set("Accept", "application/dns-message") + r.RemoteAddr = "10.10.10.100:45678" + w := httptest.NewRecorder() + + s.ServeHTTP(w, r) + + if plugin.capturedContext == nil { + t.Fatal("No context received in plugin") + } + + httpReq, ok := plugin.capturedContext.Value(HTTPRequestKey{}).(*http.Request) + if !ok { + t.Fatal("HTTPRequestKey not found in context") + } + + if httpReq.Method != "POST" { + t.Errorf("Plugin expected POST method, got %s", httpReq.Method) + } + + if ua := httpReq.Header.Get("User-Agent"); ua != "my-doh-client/2.1" { + t.Errorf("Plugin expected User-Agent 'my-doh-client/2.1', got %s", ua) + } + + if xff := httpReq.Header.Get("X-Forwarded-For"); xff != "10.10.10.10" { + t.Errorf("Plugin expected X-Forwarded-For '10.10.10.10', got %s", xff) + } + + if accept := httpReq.Header.Get("Accept"); accept != "application/dns-message" { + t.Errorf("Plugin expected Accept 'application/dns-message', got %s", accept) + } + + if httpReq.RemoteAddr != "10.10.10.100:45678" { + t.Errorf("Plugin expected RemoteAddr '10.10.10.100:45678', got %s", httpReq.RemoteAddr) + } + + if loopValue := plugin.capturedContext.Value(LoopKey{}); loopValue != 0 { + t.Errorf("Expected LoopKey value 0, got %v", loopValue) + } + }) + + t.Run("context cancellation propagation", func(t *testing.T) { + r := httptest.NewRequest(http.MethodPost, "/dns-query", io.NopCloser(bytes.NewReader(buf))) + ctx, cancel := context.WithCancel(r.Context()) + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + cancel() + s.ServeHTTP(w, r) + + if plugin.capturedContext == nil { + t.Fatal("No context received in plugin") + } + + if !plugin.contextCancelled { + t.Error("Context cancellation was not detected in plugin") + } + + if err := plugin.capturedContext.Err(); err == nil { + t.Error("Expected context to be cancelled, but it wasn't") + } + }) + + t.Run("context timeout propagation", func(t *testing.T) { + r := httptest.NewRequest(http.MethodPost, "/dns-query", io.NopCloser(bytes.NewReader(buf))) + ctx, cancel := context.WithTimeout(r.Context(), time.Millisecond) + defer cancel() + r = r.WithContext(ctx) + w := httptest.NewRecorder() + + s.ServeHTTP(w, r) + + if plugin.capturedContext == nil { + t.Fatal("No context received in plugin") + } + + if deadline, ok := plugin.capturedContext.Deadline(); !ok { + t.Error("Expected context to have a deadline") + } else if deadline.IsZero() { + t.Error("Context deadline is zero") + } + }) +} diff --git a/core/dnsserver/server_quic.go b/core/dnsserver/server_quic.go new file mode 100644 index 0000000000..531cbd82d9 --- /dev/null +++ b/core/dnsserver/server_quic.go @@ -0,0 +1,386 @@ +package dnsserver + +import ( + "context" + "crypto/tls" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + + "github.com/coredns/coredns/plugin/metrics/vars" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/reuseport" + "github.com/coredns/coredns/plugin/pkg/transport" + + "github.com/miekg/dns" + "github.com/quic-go/quic-go" +) + +const ( + // DoQCodeNoError is used when the connection or stream needs to be + // closed, but there is no error to signal. + DoQCodeNoError quic.ApplicationErrorCode = 0 + + // DoQCodeInternalError signals that the DoQ implementation encountered + // an internal error and is incapable of pursuing the transaction or the + // connection. + DoQCodeInternalError quic.ApplicationErrorCode = 1 + + // DoQCodeProtocolError signals that the DoQ implementation encountered + // a protocol error and is forcibly aborting the connection. + DoQCodeProtocolError quic.ApplicationErrorCode = 2 + + // DefaultMaxQUICStreams is the default maximum number of concurrent QUIC streams + // on a per-connection basis. RFC 9250 (DNS-over-QUIC) does not require a high + // concurrent-stream limit; normal stub or recursive resolvers open only a handful + // of streams in parallel. This default (256) is a safe upper bound. + DefaultMaxQUICStreams = 256 + + // DefaultQUICStreamWorkers is the default number of workers for processing QUIC streams. + DefaultQUICStreamWorkers = 1024 +) + +// ServerQUIC represents an instance of a DNS-over-QUIC server. +type ServerQUIC struct { + *Server + listenAddr net.Addr + tlsConfig *tls.Config + quicConfig *quic.Config + quicListener *quic.Listener + maxStreams int + streamProcessPool chan struct{} +} + +// NewServerQUIC returns a new CoreDNS QUIC server and compiles all plugin in to it. +func NewServerQUIC(addr string, group []*Config) (*ServerQUIC, error) { + s, err := NewServer(addr, group) + if err != nil { + return nil, err + } + // The *tls* plugin must make sure that multiple conflicting + // TLS configuration returns an error: it can only be specified once. + var tlsConfig *tls.Config + for _, z := range s.zones { + for _, conf := range z { + // Should we error if some configs *don't* have TLS? + tlsConfig = conf.TLSConfig + } + } + + if tlsConfig != nil { + tlsConfig.NextProtos = []string{"doq"} + } + + maxStreams := DefaultMaxQUICStreams + if len(group) > 0 && group[0] != nil && group[0].MaxQUICStreams != nil { + maxStreams = *group[0].MaxQUICStreams + } + + streamProcessPoolSize := DefaultQUICStreamWorkers + if len(group) > 0 && group[0] != nil && group[0].MaxQUICWorkerPoolSize != nil { + streamProcessPoolSize = *group[0].MaxQUICWorkerPoolSize + } + + var quicConfig = &quic.Config{ + MaxIdleTimeout: s.IdleTimeout, + MaxIncomingStreams: int64(maxStreams), + MaxIncomingUniStreams: int64(maxStreams), + // Enable 0-RTT by default for all connections on the server-side. + Allow0RTT: true, + } + + return &ServerQUIC{ + Server: s, + tlsConfig: tlsConfig, + quicConfig: quicConfig, + maxStreams: maxStreams, + streamProcessPool: make(chan struct{}, streamProcessPoolSize), + }, nil +} + +// ServePacket implements caddy.UDPServer interface. +func (s *ServerQUIC) ServePacket(p net.PacketConn) error { + s.m.Lock() + s.listenAddr = s.quicListener.Addr() + s.m.Unlock() + + return s.ServeQUIC() +} + +// ServeQUIC listens for incoming QUIC packets. +func (s *ServerQUIC) ServeQUIC() error { + for { + conn, err := s.quicListener.Accept(context.Background()) + if err != nil { + if s.isExpectedErr(err) { + s.closeQUICConn(conn, DoQCodeNoError) + return err + } + + s.closeQUICConn(conn, DoQCodeInternalError) + return err + } + + go s.serveQUICConnection(conn) + } +} + +// serveQUICConnection handles a new QUIC connection. It waits for new streams +// and passes them to serveQUICStream. +func (s *ServerQUIC) serveQUICConnection(conn *quic.Conn) { + if conn == nil { + return + } + for { + // In DoQ, one query consumes one stream. + // The client MUST select the next available client-initiated bidirectional + // stream for each subsequent query on a QUIC connection. + stream, err := conn.AcceptStream(context.Background()) + if err != nil { + if s.isExpectedErr(err) { + s.closeQUICConn(conn, DoQCodeNoError) + return + } + + s.closeQUICConn(conn, DoQCodeInternalError) + return + } + + // Use a bounded worker pool + s.streamProcessPool <- struct{}{} // Acquire a worker slot, may block + go func(st *quic.Stream, cn *quic.Conn) { + defer func() { <-s.streamProcessPool }() // Release worker slot + s.serveQUICStream(st, cn) + }(stream, conn) + } +} + +func (s *ServerQUIC) serveQUICStream(stream *quic.Stream, conn *quic.Conn) { + if conn == nil { + return + } + if stream == nil { + s.closeQUICConn(conn, DoQCodeInternalError) + return + } + buf, err := readDOQMessage(stream) + + // io.EOF does not really mean that there's any error, it is just + // the STREAM FIN indicating that there will be no data to read + // anymore from this stream. + if err != nil && err != io.EOF { + s.closeQUICConn(conn, DoQCodeProtocolError) + + return + } + + req := &dns.Msg{} + err = req.Unpack(buf) + if err != nil { + clog.Debugf("unpacking quic packet: %s", err) + s.closeQUICConn(conn, DoQCodeProtocolError) + + return + } + + if !validRequest(req) { + // If a peer encounters such an error condition, it is considered a + // fatal error. It SHOULD forcibly abort the connection using QUIC's + // CONNECTION_CLOSE mechanism and SHOULD use the DoQ error code + // DOQ_PROTOCOL_ERROR. + // See https://www.rfc-editor.org/rfc/rfc9250#section-4.3.3-3 + s.closeQUICConn(conn, DoQCodeProtocolError) + + return + } + + w := &DoQWriter{ + localAddr: conn.LocalAddr(), + remoteAddr: conn.RemoteAddr(), + stream: stream, + Msg: req, + } + + dnsCtx := context.WithValue(stream.Context(), Key{}, s.Server) + dnsCtx = context.WithValue(dnsCtx, LoopKey{}, 0) + s.ServeDNS(dnsCtx, w, req) + s.countResponse(DoQCodeNoError) +} + +// ListenPacket implements caddy.UDPServer interface. +func (s *ServerQUIC) ListenPacket() (net.PacketConn, error) { + p, err := reuseport.ListenPacket("udp", s.Addr[len(transport.QUIC+"://"):]) + if err != nil { + return nil, err + } + + s.m.Lock() + defer s.m.Unlock() + + s.quicListener, err = quic.Listen(p, s.tlsConfig, s.quicConfig) + if err != nil { + return nil, err + } + + return p, nil +} + +// OnStartupComplete lists the sites served by this server +// and any relevant information, assuming Quiet is false. +func (s *ServerQUIC) OnStartupComplete() { + if Quiet { + return + } + + out := startUpZones(transport.QUIC+"://", s.Addr, s.zones) + if out != "" { + fmt.Print(out) + } +} + +// Stop stops the server non-gracefully. It blocks until the server is totally stopped. +func (s *ServerQUIC) Stop() error { + s.m.Lock() + defer s.m.Unlock() + + if s.quicListener != nil { + return s.quicListener.Close() + } + + return nil +} + +// Serve implements caddy.TCPServer interface. +func (s *ServerQUIC) Serve(l net.Listener) error { return nil } + +// Listen implements caddy.TCPServer interface. +func (s *ServerQUIC) Listen() (net.Listener, error) { return nil, nil } + +// closeQUICConn quietly closes the QUIC connection. +func (s *ServerQUIC) closeQUICConn(conn *quic.Conn, code quic.ApplicationErrorCode) { + if conn == nil { + return + } + + clog.Debugf("closing quic conn %s with code %d", conn.LocalAddr(), code) + err := conn.CloseWithError(code, "") + if err != nil { + clog.Debugf("failed to close quic connection with code %d: %s", code, err) + } + + // DoQCodeNoError metrics are already registered after s.ServeDNS() + if code != DoQCodeNoError { + s.countResponse(code) + } +} + +// validRequest checks for protocol errors in the unpacked DNS message. +// See https://www.rfc-editor.org/rfc/rfc9250.html#name-protocol-errors +func validRequest(req *dns.Msg) (ok bool) { + // 1. a client or server receives a message with a non-zero Message ID. + if req.Id != 0 { + return false + } + + // 2. an implementation receives a message containing the edns-tcp-keepalive + // EDNS(0) Option [RFC7828]. + if opt := req.IsEdns0(); opt != nil { + for _, option := range opt.Option { + if option.Option() == dns.EDNS0TCPKEEPALIVE { + clog.Debug("client sent EDNS0 TCP keepalive option") + + return false + } + } + } + + // 3. the client or server does not indicate the expected STREAM FIN after + // sending requests or responses. + // + // This is quite problematic to validate this case since this would imply + // we have to wait until STREAM FIN is arrived before we start processing + // the message. So we're consciously ignoring this case in this + // implementation. + + // 4. a server receives a "replayable" transaction in 0-RTT data + // + // The information necessary to validate this is not exposed by quic-go. + + return true +} + +// readDOQMessage reads a DNS over QUIC (DOQ) message from the given stream +// and returns the message bytes. +// Drafts of the RFC9250 did not require the 2-byte prefixed message length. +// Thus, we are only supporting the official version (DoQ v1). +func readDOQMessage(r io.Reader) ([]byte, error) { + // All DNS messages (queries and responses) sent over DoQ connections MUST + // be encoded as a 2-octet length field followed by the message content as + // specified in [RFC1035]. + // See https://www.rfc-editor.org/rfc/rfc9250.html#section-4.2-4 + sizeBuf := make([]byte, 2) + _, err := io.ReadFull(r, sizeBuf) + if err != nil { + return nil, err + } + + size := binary.BigEndian.Uint16(sizeBuf) + + if size == 0 { + return nil, fmt.Errorf("message size is 0: probably unsupported DoQ version") + } + + buf := make([]byte, size) + _, err = io.ReadFull(r, buf) + + // A client or server receives a STREAM FIN before receiving all the bytes + // for a message indicated in the 2-octet length field. + // See https://www.rfc-editor.org/rfc/rfc9250#section-4.3.3-2.2 + if size != uint16(len(buf)) { + return nil, fmt.Errorf("message size does not match 2-byte prefix") + } + + return buf, err +} + +// isExpectedErr returns true if err is an expected error, likely related to +// the current implementation. +func (s *ServerQUIC) isExpectedErr(err error) bool { + if err == nil { + return false + } + + // This error is returned when the QUIC listener was closed by us. As + // graceful shutdown is not implemented, the connection will be abruptly + // closed but there is no error to signal. + if errors.Is(err, quic.ErrServerClosed) { + return true + } + + // This error happens when the connection was closed due to a DoQ + // protocol error but there's still something to read in the closed stream. + // For example, when the message was sent without the prefixed length. + var qAppErr *quic.ApplicationError + if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 2 { + return true + } + + // When a connection hits the idle timeout, quic.AcceptStream() returns + // an IdleTimeoutError. In this, case, we should just drop the connection + // with DoQCodeNoError. + var qIdleErr *quic.IdleTimeoutError + return errors.As(err, &qIdleErr) +} + +func (s *ServerQUIC) countResponse(code quic.ApplicationErrorCode) { + switch code { + case DoQCodeNoError: + vars.QUICResponsesCount.WithLabelValues(s.Addr, "0x0").Inc() + case DoQCodeInternalError: + vars.QUICResponsesCount.WithLabelValues(s.Addr, "0x1").Inc() + case DoQCodeProtocolError: + vars.QUICResponsesCount.WithLabelValues(s.Addr, "0x2").Inc() + } +} diff --git a/core/dnsserver/server_quic_test.go b/core/dnsserver/server_quic_test.go new file mode 100644 index 0000000000..8deb11c7c2 --- /dev/null +++ b/core/dnsserver/server_quic_test.go @@ -0,0 +1,402 @@ +package dnsserver + +import ( + "bytes" + "crypto/tls" + "errors" + "testing" + + "github.com/miekg/dns" + "github.com/quic-go/quic-go" +) + +func TestNewServerQUIC(t *testing.T) { + tests := []struct { + name string + addr string + configs []*Config + wantErr bool + }{ + { + name: "valid quic server", + addr: "127.0.0.1:0", + configs: []*Config{testConfig("quic", testPlugin{})}, + wantErr: false, + }, + { + name: "empty configs", + addr: "127.0.0.1:0", + configs: []*Config{}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server, err := NewServerQUIC(tt.addr, tt.configs) + if (err != nil) != tt.wantErr { + t.Errorf("NewServerQUIC() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && server == nil { + t.Error("NewServerQUIC() returned nil server without error") + } + }) + } +} + +func TestNewServerQUICWithTLS(t *testing.T) { + config := testConfig("quic", testPlugin{}) + config.TLSConfig = &tls.Config{ + ServerName: "example.com", + } + + server, err := NewServerQUIC("127.0.0.1:0", []*Config{config}) + if err != nil { + t.Fatalf("NewServerQUIC() with TLS failed: %v", err) + } + + if server.tlsConfig == nil { + t.Error("Expected TLS config to be set") + } + + if len(server.tlsConfig.NextProtos) == 0 || server.tlsConfig.NextProtos[0] != "doq" { + t.Error("Expected NextProtos to include doq for QUIC") + } +} + +func TestNewServerQUICWithCustomLimits(t *testing.T) { + config := testConfig("quic", testPlugin{}) + maxStreams := 100 + workerPoolSize := 50 + config.MaxQUICStreams = &maxStreams + config.MaxQUICWorkerPoolSize = &workerPoolSize + + server, err := NewServerQUIC("127.0.0.1:0", []*Config{config}) + if err != nil { + t.Fatalf("NewServerQUIC() with custom limits failed: %v", err) + } + + if server.maxStreams != maxStreams { + t.Errorf("Expected maxStreams = %d, got %d", maxStreams, server.maxStreams) + } + + if cap(server.streamProcessPool) != workerPoolSize { + t.Errorf("Expected streamProcessPool capacity = %d, got %d", workerPoolSize, cap(server.streamProcessPool)) + } + + expectedMaxStreams := int64(maxStreams) + if server.quicConfig.MaxIncomingStreams != expectedMaxStreams { + t.Errorf("Expected quicConfig.MaxIncomingStreams = %d, got %d", expectedMaxStreams, server.quicConfig.MaxIncomingStreams) + } + + if server.quicConfig.MaxIncomingUniStreams != expectedMaxStreams { + t.Errorf("Expected quicConfig.MaxIncomingUniStreams = %d, got %d", expectedMaxStreams, server.quicConfig.MaxIncomingUniStreams) + } +} + +func TestNewServerQUICDefaults(t *testing.T) { + server, err := NewServerQUIC("127.0.0.1:0", []*Config{testConfig("quic", testPlugin{})}) + if err != nil { + t.Fatalf("NewServerQUIC() failed: %v", err) + } + + if server.maxStreams != DefaultMaxQUICStreams { + t.Errorf("Expected default maxStreams = %d, got %d", DefaultMaxQUICStreams, server.maxStreams) + } + + if cap(server.streamProcessPool) != DefaultQUICStreamWorkers { + t.Errorf("Expected default streamProcessPool capacity = %d, got %d", DefaultQUICStreamWorkers, cap(server.streamProcessPool)) + } + + if !server.quicConfig.Allow0RTT { + t.Error("Expected Allow0RTT to be true by default") + } +} + +func TestServerQUIC_ServeAndListen(t *testing.T) { + server, err := NewServerQUIC("127.0.0.1:0", []*Config{testConfig("quic", testPlugin{})}) + if err != nil { + t.Fatalf("NewServerQUIC() failed: %v", err) + } + + // Test Serve - should return nil for QUIC (not used) + err = server.Serve(nil) + if err != nil { + t.Errorf("Serve() should return nil for QUIC server, got: %v", err) + } + + // Test Listen - should return nil for QUIC (not used) + listener, err := server.Listen() + if err != nil { + t.Errorf("Listen() should return nil error for QUIC server, got: %v", err) + } + if listener != nil { + t.Error("Listen() should return nil listener for QUIC server") + } +} + +func TestServerQUIC_OnStartupComplete(t *testing.T) { + server, err := NewServerQUIC("127.0.0.1:53", []*Config{testConfig("quic", testPlugin{})}) + if err != nil { + t.Fatalf("NewServerQUIC() failed: %v", err) + } + + Quiet = true + server.OnStartupComplete() + + Quiet = false + server.OnStartupComplete() +} + +func TestServerQUIC_Stop(t *testing.T) { + server, err := NewServerQUIC("127.0.0.1:0", []*Config{testConfig("quic", testPlugin{})}) + if err != nil { + t.Fatalf("NewServerQUIC() failed: %v", err) + } + + err = server.Stop() + if err != nil { + t.Errorf("Stop() without listener should not error, got: %v", err) + } +} + +func TestServerQUIC_CloseQUICConn(t *testing.T) { + server, err := NewServerQUIC("127.0.0.1:0", []*Config{testConfig("quic", testPlugin{})}) + if err != nil { + t.Fatalf("NewServerQUIC() failed: %v", err) + } + + server.closeQUICConn(nil, DoQCodeNoError) +} + +func TestServerQUIC_IsExpectedErr(t *testing.T) { + server, err := NewServerQUIC("127.0.0.1:0", []*Config{testConfig("quic", testPlugin{})}) + if err != nil { + t.Fatalf("NewServerQUIC() failed: %v", err) + } + + tests := []struct { + name string + err error + expected bool + }{ + { + name: "nil error", + err: nil, + expected: false, + }, + { + name: "server closed error", + err: quic.ErrServerClosed, + expected: true, + }, + { + name: "application error code 2", + err: &quic.ApplicationError{ErrorCode: 2}, + expected: true, + }, + { + name: "application error code 1", + err: &quic.ApplicationError{ErrorCode: 1}, + expected: false, + }, + { + name: "idle timeout error", + err: &quic.IdleTimeoutError{}, + expected: true, + }, + { + name: "other error", + err: errors.New("some other error"), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := server.isExpectedErr(tt.err) + if result != tt.expected { + t.Errorf("isExpectedErr(%v) = %v, want %v", tt.err, result, tt.expected) + } + }) + } +} + +func TestValidRequest(t *testing.T) { + tests := []struct { + name string + setupMsg func() *dns.Msg + valid bool + }{ + { + name: "valid request", + setupMsg: func() *dns.Msg { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + m.Id = 0 + return m + }, + valid: true, + }, + { + name: "non-zero message ID", + setupMsg: func() *dns.Msg { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + m.Id = 1234 + return m + }, + valid: false, + }, + { + name: "with EDNS TCP keepalive", + setupMsg: func() *dns.Msg { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + m.Id = 0 + opt := &dns.OPT{ + Hdr: dns.RR_Header{ + Name: ".", + Rrtype: dns.TypeOPT, + Class: 4096, + Ttl: 0, + }, + Option: []dns.EDNS0{ + &dns.EDNS0_TCP_KEEPALIVE{ + Code: dns.EDNS0TCPKEEPALIVE, + Timeout: 300, + }, + }, + } + m.Extra = append(m.Extra, opt) + return m + }, + valid: false, + }, + { + name: "with other EDNS options", + setupMsg: func() *dns.Msg { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + m.Id = 0 + opt := &dns.OPT{ + Hdr: dns.RR_Header{ + Name: ".", + Rrtype: dns.TypeOPT, + Class: 4096, + Ttl: 0, + }, + Option: []dns.EDNS0{ + &dns.EDNS0_NSID{ + Code: dns.EDNS0NSID, + Nsid: "test", + }, + }, + } + m.Extra = append(m.Extra, opt) + return m + }, + valid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := tt.setupMsg() + result := validRequest(msg) + if result != tt.valid { + t.Errorf("validRequest() = %v, want %v", result, tt.valid) + } + }) + } +} + +func TestReadDOQMessage(t *testing.T) { + tests := []struct { + name string + input []byte + wantMsg []byte + wantErr bool + }{ + { + name: "valid message", + input: []byte{0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05}, + wantMsg: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, + wantErr: false, + }, + { + name: "zero length message", + input: []byte{0x00, 0x00}, + wantMsg: nil, + wantErr: true, + }, + { + name: "incomplete length prefix", + input: []byte{0x00}, + wantMsg: nil, + wantErr: true, + }, + { + name: "incomplete message", + input: []byte{0x00, 0x05, 0x01, 0x02}, + wantMsg: []byte{0x01, 0x02, 0x00, 0x00, 0x00}, + wantErr: true, + }, + { + name: "empty input", + input: []byte{}, + wantMsg: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := bytes.NewReader(tt.input) + msg, err := readDOQMessage(reader) + + if (err != nil) != tt.wantErr { + t.Errorf("readDOQMessage() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !bytes.Equal(msg, tt.wantMsg) { + t.Errorf("readDOQMessage() msg = %v, want %v", msg, tt.wantMsg) + } + }) + } +} + +func TestAddPrefix(t *testing.T) { + tests := []struct { + name string + input []byte + expected []byte + }{ + { + name: "empty message", + input: []byte{}, + expected: []byte{0x00, 0x00}, + }, + { + name: "short message", + input: []byte{0x01, 0x02}, + expected: []byte{0x00, 0x02, 0x01, 0x02}, + }, + { + name: "longer message", + input: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, + expected: []byte{0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := AddPrefix(tt.input) + if !bytes.Equal(result, tt.expected) { + t.Errorf("AddPrefix() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/core/dnsserver/server_test.go b/core/dnsserver/server_test.go index d7289474b6..fef01faed4 100644 --- a/core/dnsserver/server_test.go +++ b/core/dnsserver/server_test.go @@ -2,7 +2,11 @@ package dnsserver import ( "context" + "errors" + "net" + "sync" "testing" + "time" "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/pkg/log" @@ -17,7 +21,25 @@ func (tp testPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns. return 0, nil } -func (tp testPlugin) Name() string { return "testplugin" } +func (tp testPlugin) Name() string { return "local" } + +// blockingPlugin uses sync.Mutex to simulate extended processing. +type blockingPlugin struct { + sync.Mutex +} + +func (b *blockingPlugin) Name() string { return "blocking" } + +func (b *blockingPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + // Respond immediately to avoid waiting in dns.Exchange + m := new(dns.Msg) + m.SetRcodeFormatError(r) + w.WriteMsg(m) + + b.Lock() + defer b.Unlock() + return dns.RcodeSuccess, nil +} func testConfig(transport string, p plugin.Handler) *Config { c := &Config{ @@ -26,6 +48,7 @@ func testConfig(transport string, p plugin.Handler) *Config { ListenHosts: []string{"127.0.0.1"}, Port: "53", Debug: false, + Stacktrace: false, } c.AddPlugin(func(next plugin.Handler) plugin.Handler { return p }) @@ -47,6 +70,11 @@ func TestNewServer(t *testing.T) { if err != nil { t.Errorf("Expected no error for NewServerTLS, got %s", err) } + + _, err = NewServerQUIC("127.0.0.1:53", []*Config{testConfig("quic", testPlugin{})}) + if err != nil { + t.Errorf("Expected no error for NewServerQUIC, got %s", err) + } } func TestDebug(t *testing.T) { @@ -76,6 +104,65 @@ func TestDebug(t *testing.T) { } } +func TestStacktrace(t *testing.T) { + configNoStacktrace, configStacktrace := testConfig("dns", testPlugin{}), testConfig("dns", testPlugin{}) + configStacktrace.Stacktrace = true + + s1, err := NewServer("127.0.0.1:53", []*Config{configStacktrace, configStacktrace}) + if err != nil { + t.Errorf("Expected no error for NewServer, got %s", err) + } + if !s1.stacktrace { + t.Errorf("Expected stacktrace mode enabled for server s1") + } + + s2, err := NewServer("127.0.0.1:53", []*Config{configNoStacktrace}) + if err != nil { + t.Errorf("Expected no error for NewServer, got %s", err) + } + if s2.stacktrace { + t.Errorf("Expected stacktrace disabled for server s2") + } +} + +func TestGracefulStopTimeout_Internal(t *testing.T) { + p := new(blockingPlugin) + cfg := testConfig("dns", p) + + s, err := NewServer("127.0.0.1:0", []*Config{cfg}) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + // Shorten the graceful timeout + s.graceTimeout = 500 * time.Millisecond + + pc, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatalf("ListenPacket failed: %v", err) + } + defer pc.Close() + + go s.ServePacket(pc) + udp := pc.LocalAddr().String() + + // Block the handler + p.Lock() + defer p.Unlock() + + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + _, err = dns.Exchange(m, udp) + if err != nil { + t.Fatalf("dns.Exchange failed: %v", err) + } + + err = s.Stop() + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("expected context.DeadlineExceeded, got %v", err) + } +} + func BenchmarkCoreServeDNS(b *testing.B) { s, err := NewServer("127.0.0.1:53", []*Config{testConfig("dns", testPlugin{})}) if err != nil { @@ -88,8 +175,8 @@ func BenchmarkCoreServeDNS(b *testing.B) { m.SetQuestion("aaa.example.com.", dns.TypeTXT) b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { + + for b.Loop() { s.ServeDNS(ctx, w, m) } } diff --git a/core/dnsserver/server_tls.go b/core/dnsserver/server_tls.go index 1c53c4e3c5..83c560e690 100644 --- a/core/dnsserver/server_tls.go +++ b/core/dnsserver/server_tls.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "fmt" "net" + "time" "github.com/coredns/caddy" "github.com/coredns/coredns/plugin/pkg/reuseport" @@ -28,16 +29,18 @@ func NewServerTLS(addr string, group []*Config) (*ServerTLS, error) { // The *tls* plugin must make sure that multiple conflicting // TLS configuration returns an error: it can only be specified once. var tlsConfig *tls.Config - for _, conf := range s.zones { - // Should we error if some configs *don't* have TLS? - tlsConfig = conf.TLSConfig + for _, z := range s.zones { + for _, conf := range z { + // Should we error if some configs *don't* have TLS? + tlsConfig = conf.TLSConfig + } } return &ServerTLS{Server: s, tlsConfig: tlsConfig}, nil } -// Compile-time check to ensure Server implements the caddy.GracefulServer interface -var _ caddy.GracefulServer = &Server{} +// Compile-time check to ensure ServerTLS implements the caddy.GracefulServer interface +var _ caddy.GracefulServer = &ServerTLS{} // Serve implements caddy.TCPServer interface. func (s *ServerTLS) Serve(l net.Listener) error { @@ -48,11 +51,20 @@ func (s *ServerTLS) Serve(l net.Listener) error { } // Only fill out the TCP server for this one. - s.server[tcp] = &dns.Server{Listener: l, Net: "tcp-tls", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) { - ctx := context.WithValue(context.Background(), Key{}, s.Server) - ctx = context.WithValue(ctx, LoopKey{}, 0) - s.ServeDNS(ctx, w, r) - })} + s.server[tcp] = &dns.Server{Listener: l, + Net: "tcp-tls", + MaxTCPQueries: tlsMaxQueries, + ReadTimeout: s.ReadTimeout, + WriteTimeout: s.WriteTimeout, + IdleTimeout: func() time.Duration { + return s.IdleTimeout + }, + Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) { + ctx := context.WithValue(context.Background(), Key{}, s.Server) + ctx = context.WithValue(ctx, LoopKey{}, 0) + s.ServeDNS(ctx, w, r) + })} + s.m.Unlock() return s.server[tcp].ActivateAndServe() @@ -85,3 +97,7 @@ func (s *ServerTLS) OnStartupComplete() { fmt.Print(out) } } + +const ( + tlsMaxQueries = -1 +) diff --git a/core/dnsserver/view.go b/core/dnsserver/view.go new file mode 100644 index 0000000000..ac797839db --- /dev/null +++ b/core/dnsserver/view.go @@ -0,0 +1,20 @@ +package dnsserver + +import ( + "context" + + "github.com/coredns/coredns/request" +) + +// Viewer - If Viewer is implemented by a plugin in a server block, its Filter() +// is added to the server block's filter functions when starting the server. When a running server +// serves a DNS request, it will route the request to the first Config (server block) that passes +// all its filter functions. +type Viewer interface { + // Filter returns true if the server should use the server block in which the implementing plugin resides, and the + // name of the view for metrics logging. + Filter(ctx context.Context, req *request.Request) bool + + // ViewName returns the name of the view + ViewName() string +} diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go index 4726345e31..a237cbf342 100644 --- a/core/dnsserver/zdirectives.go +++ b/core/dnsserver/zdirectives.go @@ -10,13 +10,17 @@ package dnsserver // (after) them during a request, but they must not // care what plugin above them are doing. var Directives = []string{ + "root", "metadata", + "geoip", "cancel", "tls", + "quic", + "timeouts", + "multisocket", "reload", "nsid", "bufsize", - "root", "bind", "debug", "trace", @@ -33,8 +37,10 @@ var Directives = []string{ "any", "chaos", "loadbalance", + "tsig", "cache", "rewrite", + "header", "dnssec", "autopath", "minimal", @@ -57,4 +63,6 @@ var Directives = []string{ "whoami", "on", "sign", + "view", + "nomad", } diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go index eee813910c..025c044742 100644 --- a/core/plugin/zplugin.go +++ b/core/plugin/zplugin.go @@ -25,7 +25,9 @@ import ( _ "github.com/coredns/coredns/plugin/etcd" _ "github.com/coredns/coredns/plugin/file" _ "github.com/coredns/coredns/plugin/forward" + _ "github.com/coredns/coredns/plugin/geoip" _ "github.com/coredns/coredns/plugin/grpc" + _ "github.com/coredns/coredns/plugin/header" _ "github.com/coredns/coredns/plugin/health" _ "github.com/coredns/coredns/plugin/hosts" _ "github.com/coredns/coredns/plugin/k8s_external" @@ -37,8 +39,11 @@ import ( _ "github.com/coredns/coredns/plugin/metadata" _ "github.com/coredns/coredns/plugin/metrics" _ "github.com/coredns/coredns/plugin/minimal" + _ "github.com/coredns/coredns/plugin/multisocket" + _ "github.com/coredns/coredns/plugin/nomad" _ "github.com/coredns/coredns/plugin/nsid" _ "github.com/coredns/coredns/plugin/pprof" + _ "github.com/coredns/coredns/plugin/quic" _ "github.com/coredns/coredns/plugin/ready" _ "github.com/coredns/coredns/plugin/reload" _ "github.com/coredns/coredns/plugin/rewrite" @@ -47,8 +52,11 @@ import ( _ "github.com/coredns/coredns/plugin/secondary" _ "github.com/coredns/coredns/plugin/sign" _ "github.com/coredns/coredns/plugin/template" + _ "github.com/coredns/coredns/plugin/timeouts" _ "github.com/coredns/coredns/plugin/tls" _ "github.com/coredns/coredns/plugin/trace" _ "github.com/coredns/coredns/plugin/transfer" + _ "github.com/coredns/coredns/plugin/tsig" + _ "github.com/coredns/coredns/plugin/view" _ "github.com/coredns/coredns/plugin/whoami" ) diff --git a/coredns.1.md b/coredns.1.md index 1b4237dccb..64daaca484 100644 --- a/coredns.1.md +++ b/coredns.1.md @@ -24,7 +24,7 @@ Available options: : specify Corefile to load, if not given CoreDNS will look for a `Corefile` in the current directory. -**-dns.port** **PORT** +**-dns.port** **PORT** or **-p** **PORT** : override default port (53) to listen on. **-pidfile** **FILE** diff --git a/coremain/run.go b/coremain/run.go index f4172fee66..41e91b0508 100644 --- a/coremain/run.go +++ b/coremain/run.go @@ -4,14 +4,16 @@ package coremain import ( "flag" "fmt" - "io/ioutil" "log" "os" + "path/filepath" "runtime" "strings" "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" + + "go.uber.org/automaxprocs/maxprocs" ) func init() { @@ -28,7 +30,10 @@ func init() { caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader)) caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader)) - caddy.AppName = coreName + flag.StringVar(&dnsserver.Port, serverType+".port", dnsserver.DefaultPort, "Default port") + flag.StringVar(&dnsserver.Port, "p", dnsserver.DefaultPort, "Default port") + + caddy.AppName = CoreName caddy.AppVersion = CoreVersion } @@ -42,7 +47,7 @@ func Run() { } log.SetOutput(os.Stdout) - log.SetFlags(0) // Set to 0 because we're doing our own time, with timezone + log.SetFlags(LogFlags) if version { showVersion() @@ -53,6 +58,11 @@ func Run() { os.Exit(0) } + _, err := maxprocs.Set(maxprocs.Logger(log.Printf)) + if err != nil { + log.Println("[WARNING] Failed to set GOMAXPROCS:", err) + } + // Get Corefile input corefile, err := caddy.LoadCaddyfile(serverType) if err != nil { @@ -79,7 +89,7 @@ func Run() { // enabled. If this process is an upgrade, however, and the user // might not be there anymore, this just logs to the process // log and exits. -func mustLogFatal(args ...interface{}) { +func mustLogFatal(args ...any) { if !caddy.IsUpgrade() { log.SetOutput(os.Stderr) } @@ -96,7 +106,7 @@ func confLoader(serverType string) (caddy.Input, error) { return caddy.CaddyfileFromPipe(os.Stdin, serverType) } - contents, err := ioutil.ReadFile(conf) + contents, err := os.ReadFile(filepath.Clean(conf)) if err != nil { return nil, err } @@ -109,7 +119,7 @@ func confLoader(serverType string) (caddy.Input, error) { // defaultLoader loads the Corefile from the current working directory. func defaultLoader(serverType string) (caddy.Input, error) { - contents, err := ioutil.ReadFile(caddy.DefaultConfigFile) + contents, err := os.ReadFile(caddy.DefaultConfigFile) if err != nil { if os.IsNotExist(err) { return nil, nil @@ -166,6 +176,9 @@ var ( conf string version bool plugins bool + + // LogFlags are initially set to 0 for no extra output + LogFlags int ) // Build information obtained with the help of -ldflags diff --git a/coremain/run_test.go b/coremain/run_test.go new file mode 100644 index 0000000000..84ca7dfa5a --- /dev/null +++ b/coremain/run_test.go @@ -0,0 +1,300 @@ +package coremain + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/coredns/caddy" +) + +func TestConfLoader(t *testing.T) { + tests := []struct { + name string + conf string + contents []byte + serverType string + expectedError bool + expectedInput bool + checkContents bool + }{ + { + name: "empty conf", + conf: "", + serverType: "dns", + expectedError: false, + expectedInput: false, + }, + { + name: "non-existent file", + conf: "non-existent-file", + serverType: "dns", + expectedError: true, + expectedInput: false, + }, + { + name: "stdin input", + conf: "stdin", + serverType: "dns", + expectedError: false, + expectedInput: false, + }, + { + name: "valid config file", + contents: []byte("example.org:53 {\n whoami\n}\n"), + serverType: "dns", + expectedError: false, + expectedInput: true, + checkContents: true, + }, + { + name: "empty config file", + conf: "", + contents: []byte(""), + serverType: "dns", + expectedError: false, + expectedInput: false, + checkContents: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if len(tc.contents) > 0 { + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "corefile-test") + if err := os.WriteFile(tmpFile, tc.contents, 0o644); err != nil { + t.Fatalf("Failed to write to temp file: %v", err) + } + conf = tmpFile + } else { + conf = tc.conf + } + + input, err := confLoader(tc.serverType) + + // Check error + if tc.expectedError && err == nil { + t.Errorf("Expected error but got none") + } + if !tc.expectedError && err != nil { + t.Errorf("Expected no error but got: %v", err) + } + + // Check input + if !tc.expectedInput && input != nil { + t.Errorf("Expected nil input but got: %v", input) + } + if tc.expectedInput && input == nil { + t.Errorf("Expected non-nil input but got nil") + } + + // Check contents if needed + if tc.checkContents && input != nil { + caddyInput, ok := input.(caddy.CaddyfileInput) + if !ok { + t.Errorf("Expected input to be caddy.CaddyfileInput") + } + if string(caddyInput.Contents) != string(tc.contents) { + t.Errorf("Expected contents %q, got %q", tc.contents, caddyInput.Contents) + } + if caddyInput.ServerTypeName != tc.serverType { + t.Errorf("Expected ServerTypeName to be %q, got %q", tc.serverType, caddyInput.ServerTypeName) + } + } + }) + } +} + +func TestDefaultLoader(t *testing.T) { + // The working directory matters because defaultLoader() looks for "Corefile" in the current directory + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + // Test without Corefile + input, err := defaultLoader("dns") + if err != nil { + t.Errorf("Expected no error for missing Corefile, got: %v", err) + } + if input != nil { + t.Errorf("Expected nil input for missing Corefile, got: %v", input) + } + + // Test with Corefile + testContents := []byte("example.org:53 {\n whoami\n}\n") + if err := os.WriteFile(filepath.Join(tmpDir, "Corefile"), testContents, 0o644); err != nil { + t.Fatalf("Failed to write Corefile: %v", err) + } + + input, err = defaultLoader("dns") + if err != nil { + t.Errorf("Expected no error for valid Corefile, got: %v", err) + } + if input == nil { + t.Errorf("Expected non-nil input for valid Corefile") + } else { + caddyInput, ok := input.(caddy.CaddyfileInput) + if !ok { + t.Errorf("Expected input to be caddy.CaddyfileInput") + } + if string(caddyInput.Contents) != string(testContents) { + t.Errorf("Expected contents %q, got %q", testContents, caddyInput.Contents) + } + if caddyInput.ServerTypeName != "dns" { + t.Errorf("Expected ServerTypeName to be %q, got %q", "dns", caddyInput.ServerTypeName) + } + } + + // Create a file but make it unreadable + tmpFile := filepath.Join(tmpDir, "Corefile") + if err := os.WriteFile(tmpFile, []byte("test"), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + if err := os.Chmod(tmpFile, 0000); err != nil { + t.Fatalf("Failed to change permissions: %v", err) + } + + input, err = defaultLoader("dns") + if err == nil { + t.Error("Expected error for unreadable Corefile but got none") + } + if input != nil { + t.Error("Expected nil input for unreadable Corefile") + } +} + +func TestVersionString(t *testing.T) { + caddy.AppName = "TestApp" + caddy.AppVersion = "1.0.0" + + expected := "TestApp-1.0.0\n" + result := versionString() + + if result != expected { + t.Errorf("Expected version string %q, got %q", expected, result) + } +} + +func TestReleaseString(t *testing.T) { + GitCommit = "a6d2d7b5" + + expected := runtime.GOOS + "/" + runtime.GOARCH + ", " + runtime.Version() + ", " + GitCommit + "\n" + result := releaseString() + + if result != expected { + t.Errorf("Expected release string %q, got %q", expected, result) + } +} + +func TestSetVersion(t *testing.T) { + // Test case 1: Development build with nearest tag + gitTag = "" + gitNearestTag = "v1.2.3" + gitShortStat = "1 file changed" + GitCommit = "abcdef" + buildDate = "2023-05-01" + + setVersion() + + if !devBuild { + t.Errorf("Expected devBuild to be true with empty gitTag and non-empty gitShortStat") + } + + expectedAppVersion := "1.2.3 (+abcdef 2023-05-01)" + if appVersion != expectedAppVersion { + t.Errorf("Expected appVersion to be %q, got %q", expectedAppVersion, appVersion) + } + + // Test case 2: Release build with tag + gitTag = "v2.0.0" + gitNearestTag = "v1.9.0" + gitShortStat = "" + + setVersion() + + if devBuild { + t.Errorf("Expected devBuild to be false with non-empty gitTag and empty gitShortStat") + } + + expectedAppVersion = "2.0.0" + if appVersion != expectedAppVersion { + t.Errorf("Expected appVersion to be %q, got %q", expectedAppVersion, appVersion) + } + + // Test case 3: No tags available + gitTag = "" + gitNearestTag = "" + gitShortStat = "" + appVersion = "(untracked dev build)" // Reset to default + + setVersion() + + if !devBuild { + t.Errorf("Expected devBuild to be true with empty gitTag") + } + + expectedAppVersion = "(untracked dev build)" + if appVersion != expectedAppVersion { + t.Errorf("Expected appVersion to be %q, got %q", expectedAppVersion, appVersion) + } +} + +func TestShowVersion(t *testing.T) { + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Set test values + caddy.AppName = "TestApp" + caddy.AppVersion = "1.0.0" + GitCommit = "abc123" + + // Test case 1: Non-dev build + devBuild = false + gitShortStat = "" + gitFilesModified = "" + + showVersion() + + // Restore stdout + w.Close() + os.Stdout = oldStdout + + var buf bytes.Buffer + io.Copy(&buf, r) + result := buf.String() + + expected := fmt.Sprintf("TestApp-1.0.0\n%s/%s, %s, abc123\n", runtime.GOOS, runtime.GOARCH, runtime.Version()) + if result != expected { + t.Errorf("Expected version output %q, got %q", expected, result) + } + + // Test case 2: Dev build with modified files + oldStdout = os.Stdout + r, w, _ = os.Pipe() + os.Stdout = w + + devBuild = true + gitShortStat = "1 file changed, 10 insertions(+), 5 deletions(-)" + gitFilesModified = "path/to/modified/file.go" + + showVersion() + + w.Close() + os.Stdout = oldStdout + + buf.Reset() + io.Copy(&buf, r) + result = buf.String() + + expected = fmt.Sprintf("TestApp-1.0.0\n%s/%s, %s, abc123\n1 file changed, 10 insertions(+), 5 deletions(-)\npath/to/modified/file.go\n", + runtime.GOOS, runtime.GOARCH, runtime.Version()) + if result != expected { + t.Errorf("Expected version output with dev build info:\n%q\ngot:\n%q", expected, result) + } +} diff --git a/coremain/version.go b/coremain/version.go index 0fc9f2e3f6..77931d8ece 100644 --- a/coremain/version.go +++ b/coremain/version.go @@ -2,7 +2,7 @@ package coremain // Various CoreDNS constants. const ( - CoreVersion = "1.8.3" - coreName = "CoreDNS" + CoreVersion = "1.13.1" + CoreName = "CoreDNS" serverType = "dns" ) diff --git a/directives_generate.go b/directives_generate.go index 3c1c0214b9..ba3b12fc03 100644 --- a/directives_generate.go +++ b/directives_generate.go @@ -1,11 +1,10 @@ -//+build ignore +//go:build ignore package main import ( "bufio" "go/format" - "io/ioutil" "log" "os" "strings" @@ -15,24 +14,11 @@ func main() { mi := make(map[string]string, 0) md := []string{} - file, err := os.Open(pluginFile) - if err != nil { - log.Fatalf("Failed to open %s: %q", pluginFile, err) - } - - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "#") { - continue - } - - items := strings.Split(line, ":") + parsePlugin := func(element string) { + items := strings.Split(element, ":") if len(items) != 2 { // ignore empty lines - continue + return } name, repo := items[0], items[1] @@ -48,6 +34,27 @@ func main() { } } + file, err := os.Open(pluginFile) + if err != nil { + log.Fatalf("Failed to open %s: %q", pluginFile, err) + } + + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "#") { + continue + } + + parsePlugin(line) + } + + for _, element := range strings.Split(os.Getenv("COREDNS_PLUGINS"), ",") { + parsePlugin(element) + } + genImports("core/plugin/zplugin.go", "plugin", mi) genDirectives("core/dnsserver/zdirectives.go", "dnsserver", md) } @@ -59,10 +66,22 @@ func genImports(file, pack string, mi map[string]string) { outs += "\n" } + coreDnsImports := "" + thirdPartyImports := "" + outs += "// Include all plugins.\n" for _, v := range mi { - outs += `_ "` + v + `"` + "\n" + if strings.HasPrefix(v, githubOrg) { + coreDnsImports += `_ "` + v + `"` + "\n" + } else { + thirdPartyImports += `_ "` + v + `"` + "\n" + } } + outs += coreDnsImports + if thirdPartyImports != "" { + outs += "\n" + thirdPartyImports + } + outs += ")\n" if err := formatAndWrite(file, outs); err != nil { @@ -101,14 +120,15 @@ func formatAndWrite(file string, data string) error { return err } - if err = ioutil.WriteFile(file, res, 0644); err != nil { + if err = os.WriteFile(file, res, 0644); err != nil { return err } return nil } const ( - pluginPath = "github.com/coredns/coredns/plugin/" + githubOrg = "github.com/coredns" + pluginPath = githubOrg + "/coredns/plugin/" pluginFile = "plugin.cfg" pluginFSPath = "plugin/" // Where the plugins are located on the file system header = "// generated by directives_generate.go; DO NOT EDIT\n\n" diff --git a/go.mod b/go.mod index 9fa9e5ae80..4b6b2b6eeb 100644 --- a/go.mod +++ b/go.mod @@ -1,42 +1,199 @@ module github.com/coredns/coredns -go 1.16 +// Note this minimum version requirement. CoreDNS supports the last two +// Go versions. This follows the upstream Go project support. +go 1.24.0 require ( - github.com/Azure/azure-sdk-for-go v40.6.0+incompatible - github.com/Azure/go-autorest/autorest v0.11.18 - github.com/Azure/go-autorest/autorest/azure/auth v0.5.7 - github.com/Azure/go-autorest/autorest/to v0.2.0 // indirect - github.com/DataDog/datadog-go v3.5.0+incompatible // indirect - github.com/aws/aws-sdk-go v1.37.10 - github.com/coredns/caddy v1.1.0 + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible + github.com/Azure/go-autorest/autorest v0.11.30 + github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 + github.com/DataDog/dd-trace-go/v2 v2.2.3 + github.com/apparentlymart/go-cidr v1.1.0 + github.com/aws/aws-sdk-go-v2 v1.39.2 + github.com/aws/aws-sdk-go-v2/config v1.31.12 + github.com/aws/aws-sdk-go-v2/credentials v1.18.16 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 + github.com/aws/aws-sdk-go-v2/service/route53 v1.58.4 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.6 + github.com/coredns/caddy v1.1.4-0.20250930002214-15135a999495 github.com/dnstap/golang-dnstap v0.4.0 - github.com/dustin/go-humanize v1.0.0 // indirect + github.com/expr-lang/expr v1.17.6 github.com/farsightsec/golang-framestream v0.3.0 - github.com/golang/protobuf v1.4.3 - github.com/gorilla/websocket v1.4.0 // indirect + github.com/go-logr/logr v1.4.3 github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 - github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/imdario/mergo v0.3.9 // indirect - github.com/infobloxopen/go-trees v0.0.0-20190313150506-2af4e13f9062 - github.com/matttproud/golang_protobuf_extensions v1.0.1 - github.com/miekg/dns v1.1.40 + github.com/hashicorp/nomad/api v0.0.0-20250909143645-a3b86c697f38 // v1.10.5 + github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9 + github.com/matttproud/golang_protobuf_extensions v1.0.4 + github.com/miekg/dns v1.1.68 github.com/opentracing/opentracing-go v1.2.0 - github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 - github.com/openzipkin/zipkin-go v0.2.2 - github.com/philhofer/fwd v1.0.0 // indirect - github.com/prometheus/client_golang v1.9.0 - github.com/prometheus/client_model v0.2.0 - github.com/prometheus/common v0.15.0 - go.etcd.io/etcd v0.5.0-alpha.5.0.20200306183522-221f0cc107cb - go.uber.org/zap v1.14.1 // indirect - golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad - golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e - google.golang.org/api v0.29.0 - google.golang.org/grpc v1.29.1 - gopkg.in/DataDog/dd-trace-go.v1 v1.28.0 - k8s.io/api v0.20.2 - k8s.io/apimachinery v0.20.2 - k8s.io/client-go v0.20.2 - k8s.io/klog v1.0.0 + github.com/openzipkin-contrib/zipkin-go-opentracing v0.5.0 + github.com/openzipkin/zipkin-go v0.4.3 + github.com/oschwald/geoip2-golang v1.13.0 + github.com/prometheus/client_golang v1.23.0 + github.com/prometheus/client_model v0.6.2 + github.com/prometheus/common v0.66.1 + github.com/quic-go/quic-go v0.55.0 + go.etcd.io/etcd/api/v3 v3.6.5 + go.etcd.io/etcd/client/v3 v3.6.5 + go.uber.org/automaxprocs v1.6.0 + golang.org/x/crypto v0.42.0 + golang.org/x/sys v0.36.0 + google.golang.org/api v0.251.0 + google.golang.org/grpc v1.75.1 + google.golang.org/protobuf v1.36.10 + k8s.io/api v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 + k8s.io/klog/v2 v2.130.1 + sigs.k8s.io/mcs-api v0.3.0 +) + +require ( + cloud.google.com/go/auth v0.16.5 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/autorest/to v0.2.0 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/DataDog/appsec-internal-go v1.13.0 // indirect + github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.67.0 // indirect + github.com/DataDog/datadog-agent/pkg/obfuscate v0.67.0 // indirect + github.com/DataDog/datadog-agent/pkg/proto v0.67.0 // indirect + github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.69.0 // indirect + github.com/DataDog/datadog-agent/pkg/trace v0.67.0 // indirect + github.com/DataDog/datadog-agent/pkg/util/log v0.67.0 // indirect + github.com/DataDog/datadog-agent/pkg/util/scrubber v0.67.0 // indirect + github.com/DataDog/datadog-agent/pkg/version v0.67.0 // indirect + github.com/DataDog/datadog-go/v5 v5.6.0 // indirect + github.com/DataDog/go-libddwaf/v4 v4.3.2 // indirect + github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20250721125240-fdf1ef85b633 // indirect + github.com/DataDog/go-sqllexer v0.1.6 // indirect + github.com/DataDog/go-tuf v1.1.0-0.5.2 // indirect + github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.27.0 // indirect + github.com/DataDog/sketches-go v1.4.7 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 // indirect + github.com/aws/smithy-go v1.23.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect + github.com/ebitengine/purego v0.8.3 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/hashicorp/cronexpr v1.1.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/ginkgo/v2 v2.22.1 // indirect + github.com/onsi/gomega v1.36.2 // indirect + github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 // indirect + github.com/oschwald/maxminddb-golang v1.13.0 // indirect + github.com/outcaste-io/ristretto v0.2.3 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.3 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/tinylib/msgp v1.2.5 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/component v1.31.0 // indirect + go.opentelemetry.io/collector/featuregate v1.31.0 // indirect + go.opentelemetry.io/collector/internal/telemetry v0.125.0 // indirect + go.opentelemetry.io/collector/pdata v1.31.0 // indirect + go.opentelemetry.io/collector/semconv v0.125.0 // indirect + go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/log v0.11.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.45.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/term v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/time v0.13.0 // indirect + golang.org/x/tools v0.36.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 93bb1df191..5dabf04968 100644 --- a/go.sum +++ b/go.sum @@ -1,825 +1,595 @@ -cloud.google.com/go v0.26.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.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 v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0 h1:WRz29PgAsVEyPSDHyk+0fpEkwEFyfhHn+JbksT6gIL4= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go v40.6.0+incompatible h1:ULjp/a/UsBfnZcl45jjywhcBKex/k/A1cG9s9NapLFw= -github.com/Azure/azure-sdk-for-go v40.6.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= -github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= -github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/adal v0.9.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk= -github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= -github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.7 h1:8DQB8yl7aLQuP+nuR5e2RO6454OvFlSTXXaNHshc16s= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.7/go.mod h1:AkzUsqkrdmNhfP2i54HqINVQopw0CLDnvHpJ88Zz1eI= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= +github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= +github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= +github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= github.com/Azure/go-autorest/autorest/to v0.2.0 h1:nQOZzFCudTh+TvquAtCRjM01VEYx85e9qbwt5ncW4L8= github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -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/DataDog/datadog-go v3.5.0+incompatible h1:AShr9cqkF+taHjyQgcBcQUt/ZNK+iPq4ROaZwSX5c/U= -github.com/DataDog/datadog-go v3.5.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -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/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= -github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.37.10 h1:LRwl+97B4D69Z7tz+eRUxJ1C7baBaIYhgrn5eLtua+Q= -github.com/aws/aws-sdk-go v1.37.10/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -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/DataDog/appsec-internal-go v1.13.0 h1:aO6DmHYsAU8BNFuvYJByhMKGgcQT3WAbj9J/sgAJxtA= +github.com/DataDog/appsec-internal-go v1.13.0/go.mod h1:9YppRCpElfGX+emXOKruShFYsdPq7WEPq/Fen4tYYpk= +github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.67.0 h1:2mEwRWvhIPHMPK4CMD8iKbsrYBxeMBSuuCXumQAwShU= +github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.67.0/go.mod h1:ejJHsyJTG7NU6c6TDbF7dmckD3g+AUGSdiSXy+ZyaCE= +github.com/DataDog/datadog-agent/pkg/obfuscate v0.67.0 h1:NcvyDVIUA0NbBDbp7QJnsYhoBv548g8bXq886795mCQ= +github.com/DataDog/datadog-agent/pkg/obfuscate v0.67.0/go.mod h1:1oPcs3BUTQhiTkmk789rb7ob105MxNV6OuBa28BdukQ= +github.com/DataDog/datadog-agent/pkg/proto v0.67.0 h1:7dO6mKYRb7qSiXEu7Q2mfeKbhp4hykCAULy4BfMPmsQ= +github.com/DataDog/datadog-agent/pkg/proto v0.67.0/go.mod h1:bKVXB7pxBg0wqXF6YSJ+KU6PeCWKDyJj83kUH1ab+7o= +github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.69.0 h1:/DsN4R+IkC6t1+4cHSfkxzLtDl84rBbPC5Wa9srBAoM= +github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.69.0/go.mod h1:Th2LD/IGid5Rza55pzqGu6nUdOv/Rts6wPwLjTyOSTs= +github.com/DataDog/datadog-agent/pkg/trace v0.67.0 h1:dqt+/nObo0JKyaEqIMZgfqGZbx9TfEHpCkrjQ/zzH7k= +github.com/DataDog/datadog-agent/pkg/trace v0.67.0/go.mod h1:zmZoEtKvOnaKHbJGBKH3a4xuyPrSfBaF0ZE3Q3rCoDw= +github.com/DataDog/datadog-agent/pkg/util/log v0.67.0 h1:xrH15QNqeJZkYoXYi44VCIvGvTwlQ3z2iT2QVTGiT7s= +github.com/DataDog/datadog-agent/pkg/util/log v0.67.0/go.mod h1:dfVLR+euzEyg1CeiExgJQq1c1dod42S6IeiRPj8H7Yk= +github.com/DataDog/datadog-agent/pkg/util/scrubber v0.67.0 h1:aIWF85OKxXGo7rVyqJ7jm7lm2qCQrgyXzYyFuw0T2EQ= +github.com/DataDog/datadog-agent/pkg/util/scrubber v0.67.0/go.mod h1:Lfap5FuM4b/Pw9IrTuAvWBWZEmXOvZhCya3dYv4G8O0= +github.com/DataDog/datadog-agent/pkg/version v0.67.0 h1:TB8H8r+laB1Qdttvvc6XJVyLGxp8E6j2f2Mh5IPbYmQ= +github.com/DataDog/datadog-agent/pkg/version v0.67.0/go.mod h1:kvAw/WbI7qLAsDI2wHabZfM7Cv2zraD3JA3323GEB+8= +github.com/DataDog/datadog-go/v5 v5.6.0 h1:2oCLxjF/4htd55piM75baflj/KoE6VYS7alEUqFvRDw= +github.com/DataDog/datadog-go/v5 v5.6.0/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= +github.com/DataDog/dd-trace-go/v2 v2.2.3 h1:6RvVdY9suR/rYYYZHjx4txrtSYcRZ5u5Cs2sXMsIBf4= +github.com/DataDog/dd-trace-go/v2 v2.2.3/go.mod h1:1LcqWELgQwgk6x7sO0MXUgsvxcAVjxSA423cUjvUqR0= +github.com/DataDog/go-libddwaf/v4 v4.3.2 h1:YGvW2Of1C4e1yU+p7iibmhN2zEOgi9XEchbhQjBxb/A= +github.com/DataDog/go-libddwaf/v4 v4.3.2/go.mod h1:/AZqP6zw3qGJK5mLrA0PkfK3UQDk1zCI2fUNCt4xftE= +github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20250721125240-fdf1ef85b633 h1:ZRLR9Lbym748e8RznWzmSoK+OfV+8qW6SdNYA4/IqdA= +github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20250721125240-fdf1ef85b633/go.mod h1:YFoTl1xsMzdSRFIu33oCSPS/3+HZAPGpO3oOM96wXCM= +github.com/DataDog/go-sqllexer v0.1.6 h1:skEXpWEVCpeZFIiydoIa2f2rf+ymNpjiIMqpW4w3YAk= +github.com/DataDog/go-sqllexer v0.1.6/go.mod h1:GGpo1h9/BVSN+6NJKaEcJ9Jn44Hqc63Rakeb+24Mjgo= +github.com/DataDog/go-tuf v1.1.0-0.5.2 h1:4CagiIekonLSfL8GMHRHcHudo1fQnxELS9g4tiAupQ4= +github.com/DataDog/go-tuf v1.1.0-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0= +github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= +github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= +github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.27.0 h1:5US5SqqhfkZkg/E64uvn7YmeTwnudJHtlPEH/LOT99w= +github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.27.0/go.mod h1:VRo4D6rj92AExpVBlq3Gcuol9Nm1bber12KyxRjKGWw= +github.com/DataDog/sketches-go v1.4.7 h1:eHs5/0i2Sdf20Zkj0udVFWuCrXGRFig2Dcfm5rtcTxc= +github.com/DataDog/sketches-go v1.4.7/go.mod h1:eAmQ/EBmtSO+nQp7IZMZVRPT4BQTmIc5RZQ+deGlTPM= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= +github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2/config v1.31.12 h1:pYM1Qgy0dKZLHX2cXslNacbcEFMkDMl+Bcj5ROuS6p8= +github.com/aws/aws-sdk-go-v2/config v1.31.12/go.mod h1:/MM0dyD7KSDPR+39p9ZNVKaHDLb9qnfDurvVS2KAhN8= +github.com/aws/aws-sdk-go-v2/credentials v1.18.16 h1:4JHirI4zp958zC026Sm+V4pSDwW4pwLefKrc0bF2lwI= +github.com/aws/aws-sdk-go-v2/credentials v1.18.16/go.mod h1:qQMtGx9OSw7ty1yLclzLxXCRbrkjWAM7JnObZjmCB7I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 h1:Mv4Bc0mWmv6oDuSWTKnk+wgeqPL5DRFu5bQL9BGPQ8Y= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9/go.mod h1:IKlKfRppK2a1y0gy1yH6zD+yX5uplJ6UuPlgd48dJiQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 h1:5r34CgVOD4WZudeEKZ9/iKpiT6cM1JyEROpXjOcdWv8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9/go.mod h1:dB12CEbNWPbzO2uC6QSWHteqOg4JfBVJOojbAoAUb5I= +github.com/aws/aws-sdk-go-v2/service/route53 v1.58.4 h1:KycXrohD5OxAZ5h02YechO2gevvoHfAPAaJM5l8zqb0= +github.com/aws/aws-sdk-go-v2/service/route53 v1.58.4/go.mod h1:xNLZLn4SusktBQ5moqUOgiDKGz3a7vHwF4W0KD+WBPc= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.6 h1:9PWl450XOG+m5lKv+qg5BXso1eLxpsZLqq7VPug5km0= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.6/go.mod h1:hwt7auGsDcaNQ8pzLgE2kCNyIWouYlAKSjuUu5Dqr7I= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 h1:A1oRkiSQOWstGh61y4Wc/yQ04sqrQZr1Si/oAXj20/s= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.6/go.mod h1:5PfYspyCU5Vw1wNPsxi15LZovOnULudOQuVxphSflQA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 h1:5fm5RTONng73/QA73LhCNR7UT9RpFH3hR6HWL6bIgVY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1/go.mod h1:xBEjWD13h+6nq+z4AkqSfSvqRKFgDIQeaMguAJndOWo= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 h1:p3jIvqYwUZgu/XYeI48bJxOhvm47hZb5HUQ0tn6Q9kA= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6/go.mod h1:WtKK+ppze5yKPkZ0XwqIVWD4beCwv056ZbPQNoeHqM8= +github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= +github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/coredns/caddy v1.1.0 h1:ezvsPrT/tA/7pYDBZxu0cT0VmWk75AfIaf6GSYCNMf0= -github.com/coredns/caddy v1.1.0/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= -github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.0.0 h1:XJIw/+VlJ+87J+doOxznsAWIdmWuViOVhkQamW5YV28= -github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/license-bill-of-materials v0.0.0-20190913234955-13baff47494e/go.mod h1:4xMOusJ7xxc84WclVxKT8+lNfGYDwojOUC2OQNCwcj4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= +github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= +github.com/coredns/caddy v1.1.4-0.20250930002214-15135a999495 h1:JFeOmbjLnVRhvmLHyuO3M1pfXWlPWpwkdM8UqXZRtBg= +github.com/coredns/caddy v1.1.4-0.20250930002214-15135a999495/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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 h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dnstap/golang-dnstap v0.4.0 h1:KRHBoURygdGtBjDI2w4HifJfMAhhOqDuktAokaSa234= github.com/dnstap/golang-dnstap v0.4.0/go.mod h1:FqsSdH58NAmkAvKcpyxht7i4FoBjKu8E4JUPt8ipSUs= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 h1:8EXxF+tCLqaVk8AOC29zl2mnhQjwyLxxOTuhUazWRsg= +github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4/go.mod h1:I5sHm0Y0T1u5YjlyqC5GVArM7aNZRUYtTjmJ8mPJFds= +github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc= +github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec= +github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/farsightsec/golang-framestream v0.3.0 h1:/spFQHucTle/ZIPkYqrfshQqPe2VQEzesH243TjIwqA= github.com/farsightsec/golang-framestream v0.3.0/go.mod h1:eNde4IQyEiA5br02AouhEHCu3p3UzrCdFR4LuQHklMI= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= -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-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/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-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -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.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= -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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.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-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -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/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= -github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= -github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/cronexpr v1.1.3 h1:rl5IkxXN2m681EfivTlccqIryzYJSXRGRNa0xeG7NA4= +github.com/hashicorp/cronexpr v1.1.3/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= 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-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -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/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -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 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= -github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/infobloxopen/go-trees v0.0.0-20190313150506-2af4e13f9062 h1:d3VSuNcgTCn21dNMm8g412Fck/XWFmMj4nJhhHT7ZZ0= -github.com/infobloxopen/go-trees v0.0.0-20190313150506-2af4e13f9062/go.mod h1:PcNJqIlcX/dj3DTG/+QQnRvSgTMG6CLpRMjWcv4+J6w= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -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/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/nomad/api v0.0.0-20250909143645-a3b86c697f38 h1:1LTbcTpGdSdbj0ee7YZHNe4R2XqxfyWwIkSGWRhgkfM= +github.com/hashicorp/nomad/api v0.0.0-20250909143645-a3b86c697f38/go.mod h1:0Tdp+9HbvwrxprXv/LfYZ8P21bOl4oA8Afyet1kUvhI= +github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9 h1:w66aaP3c6SIQ0pi3QH1Tb4AMO3aWoEPxd1CNvLphbkA= +github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9/go.mod h1:BaIJzjD2ZnHmx2acPF6XfGLPzNCMiBbMRqJr+8/8uRI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 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.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/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -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/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= -github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA= -github.com/miekg/dns v1.1.40/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/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 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/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= +github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.125.0 h1:0dOJCEtabevxxDQmxed69oMzSw+gb3ErCnFwFYZFu0M= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.125.0/go.mod h1:QwzQhtxPThXMUDW1XRXNQ+l0GrI2BRsvNhX6ZuKyAds= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.125.0 h1:F68/Nbpcvo3JZpaWlRUDJtG7xs8FHBZ7A8GOMauDkyc= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.125.0/go.mod h1:haO4cJtAk05Y0p7NO9ME660xxtSh54ifCIIT7+PO9C0= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= -github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= -github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb9NAWI= -github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= -github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= -github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -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/openzipkin-contrib/zipkin-go-opentracing v0.5.0 h1:uhcF5Jd7rP9DVEL10Siffyepr6SvlKbUsjH5JpNCRi8= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.5.0/go.mod h1:+oCZ5GXXr7KPI/DNOQORPTq5AWHfALJj9c72b0+YsEY= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= +github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= +github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= +github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= +github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0= +github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 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.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU= -github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/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/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM= -github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -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/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -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/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 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/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/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= +github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 h1:4+LEVOB87y175cLJC/mbsgKmoDOjrBldtXvioEy96WY= +github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3/go.mod h1:vl5+MqJ1nBINuSsUI2mGgH79UweUT/B5Fy8857PqyyI= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= +github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= +github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE= +github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= +github.com/shoenig/test v1.12.1 h1:mLHfnMv7gmhhP44WrvT+nKSxKkPDiNkIuHGdIGI9RLU= +github.com/shoenig/test v1.12.1/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -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/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= -github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200306183522-221f0cc107cb h1:TcJ8iNja1CH/h/3QcsydKL5krb0MIPjMJLYgzClNaSQ= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200306183522-221f0cc107cb/go.mod h1:VZB9Yx4s43MHItytoe8jcvaEFEgF2QzHDZGfQ/XQjvQ= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -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.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo= -go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= -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= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= +github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/vmihailenco/msgpack/v4 v4.3.13 h1:A2wsiTbvp63ilDaWmsk2wjx6xZdxQOvpiNlKBGKKXKI= +github.com/vmihailenco/msgpack/v4 v4.3.13/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA= +go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= +go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8= +go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= +go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U= +go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/component v1.31.0 h1:9LzU8X1RhV3h8/QsAoTX23aFUfoJ3EUc9O/vK+hFpSI= +go.opentelemetry.io/collector/component v1.31.0/go.mod h1:JbZl/KywXJxpUXPbt96qlEXJSym1zQ2hauMxYMuvlxM= +go.opentelemetry.io/collector/component/componentstatus v0.125.0 h1:zlxGQZYd9kknRZSjRpOYW5SBjl0a5zYFYRPbreobXoU= +go.opentelemetry.io/collector/component/componentstatus v0.125.0/go.mod h1:bHXc2W8bqqo9adOvCgvhcO7pYzJOSpyV4cuQ1wiIl04= +go.opentelemetry.io/collector/component/componenttest v0.125.0 h1:E2mpnMQbkMpYoZ3Q8pHx4kod7kedjwRs1xqDpzCe/84= +go.opentelemetry.io/collector/component/componenttest v0.125.0/go.mod h1:pQtsE1u/SPZdTphP5BZP64XbjXSq6wc+mDut5Ws/JDI= +go.opentelemetry.io/collector/consumer v1.31.0 h1:L+y66ywxLHnAxnUxv0JDwUf5bFj53kMxCCyEfRKlM7s= +go.opentelemetry.io/collector/consumer v1.31.0/go.mod h1:rPsqy5ni+c6xNMUkOChleZYO/nInVY6eaBNZ1FmWJVk= +go.opentelemetry.io/collector/consumer/consumertest v0.125.0 h1:TUkxomGS4DAtjBvcWQd2UY4FDLLEKMQD6iOIDUr/5dM= +go.opentelemetry.io/collector/consumer/consumertest v0.125.0/go.mod h1:vkHf3y85cFLDHARO/cTREVjLjOPAV+cQg7lkC44DWOY= +go.opentelemetry.io/collector/consumer/xconsumer v0.125.0 h1:oTreUlk1KpMSWwuHFnstW+orrjGTyvs2xd3o/Dpy+hI= +go.opentelemetry.io/collector/consumer/xconsumer v0.125.0/go.mod h1:FX0G37r0W+wXRgxxFtwEJ4rlsCB+p0cIaxtU3C4hskw= +go.opentelemetry.io/collector/featuregate v1.31.0 h1:20q7plPQZwmAiaYAa6l1m/i2qDITZuWlhjr4EkmeQls= +go.opentelemetry.io/collector/featuregate v1.31.0/go.mod h1:Y/KsHbvREENKvvN9RlpiWk/IGBK+CATBYzIIpU7nccc= +go.opentelemetry.io/collector/internal/telemetry v0.125.0 h1:6lcGOxw3dAg7LfXTKdN8ZjR+l7KvzLdEiPMhhLwG4r4= +go.opentelemetry.io/collector/internal/telemetry v0.125.0/go.mod h1:5GyFslLqjZgq1DZTtFiluxYhhXrCofHgOOOybodDPGE= +go.opentelemetry.io/collector/pdata v1.31.0 h1:P5WuLr1l2JcIvr6Dw2hl01ltp2ZafPnC4Isv+BLTBqU= +go.opentelemetry.io/collector/pdata v1.31.0/go.mod h1:m41io9nWpy7aCm/uD1L9QcKiZwOP0ldj83JEA34dmlk= +go.opentelemetry.io/collector/pdata/pprofile v0.125.0 h1:Qqlx8w1HpiYZ9RQqjmMQIysI0cHNO1nh3E/fCTeFysA= +go.opentelemetry.io/collector/pdata/pprofile v0.125.0/go.mod h1:p/yK023VxAp8hm27/1G5DPTcMIpnJy3cHGAFUQZGyaQ= +go.opentelemetry.io/collector/pdata/testdata v0.125.0 h1:due1Hl0EEVRVwfCkiamRy5E8lS6yalv0lo8Zl/SJtGw= +go.opentelemetry.io/collector/pdata/testdata v0.125.0/go.mod h1:1GpEWlgdMrd+fWsBk37ZC2YmOP5YU3gFQ4rWuCu9g24= +go.opentelemetry.io/collector/pipeline v0.125.0 h1:oitBgcAFqntDB4ihQJUHJSQ8IHqKFpPkaTVbTYdIUzM= +go.opentelemetry.io/collector/pipeline v0.125.0/go.mod h1:TO02zju/K6E+oFIOdi372Wk0MXd+Szy72zcTsFQwXl4= +go.opentelemetry.io/collector/processor v1.31.0 h1:+u7sBUpnCBsHYoALp4hfr9VEjLHHYa4uKENGITe0K9Q= +go.opentelemetry.io/collector/processor v1.31.0/go.mod h1:5hDYJ7/hTdfd2tF2Rj5Hs6+mfyFz2O7CaPzVvW1qHQc= +go.opentelemetry.io/collector/processor/processorhelper v0.125.0 h1:QRpX7oFW88DAZhy+Q93npklRoaQr8ue0GKpeup7C/Fk= +go.opentelemetry.io/collector/processor/processorhelper v0.125.0/go.mod h1:oXRvslUuN62wErcoJrcEJYoTXu5wHyNyJsE+/a9Cc9s= +go.opentelemetry.io/collector/processor/processortest v0.125.0 h1:ZVAN4iZPDcWhpzKqnuok2NIuS5hwGVVQUOWkJFR12tA= +go.opentelemetry.io/collector/processor/processortest v0.125.0/go.mod h1:VAw0IRG35cWTBjBtreXeXJEgqkRegfjrH/EuLhNX2+I= +go.opentelemetry.io/collector/processor/xprocessor v0.125.0 h1:VWYPMW1VmDq6xB7M5SYjBpQCCIq3MhQ3W++wU47QpZM= +go.opentelemetry.io/collector/processor/xprocessor v0.125.0/go.mod h1:bCxUyFVlksANg8wjYZqWVsRB33lkLQ294rTrju/IZiM= +go.opentelemetry.io/collector/semconv v0.125.0 h1:SyRP617YGvNSWRSKMy7Lbk9RaJSR+qFAAfyxJOeZe4s= +go.opentelemetry.io/collector/semconv v0.125.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= +go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU= +go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= +go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/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-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/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/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -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-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/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/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= 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-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-20190125091013-d26f9f9a57f3/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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/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-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/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/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -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= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/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-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-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-20190412213103-97732733099d/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-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 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= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20190614205625-5aca471b1d59/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-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-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4 h1:kDtqNkeBrZb8B+atrj50B5XLHpzXXqcCdZPP/ApQ5NY= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -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/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -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.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/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -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-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -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-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -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.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= -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.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.251.0 h1:6lea5nHRT8RUmpy9kkC2PJYnhnDAB13LqrLSVQlMIE8= +google.golang.org/api v0.251.0/go.mod h1:Rwy0lPf/TD7+T2VhYcffCHhyyInyuxGjICxdfLqT7KI= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -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/DataDog/dd-trace-go.v1 v1.28.0 h1:EmglUJuykRsTwsQDcKaAo3CmOunWU6Dqk7U2lo7Pjss= -gopkg.in/DataDog/dd-trace-go.v1 v1.28.0/go.mod h1:Sp1lku8WJMvNV0kjDI4Ni/T7J/U3BO5ct5kEaoVU8+I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -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/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -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= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -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= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.20.2 h1:y/HR22XDZY3pniu9hIFDLpUCPq2w5eQ6aV/VFQ7uJMw= -k8s.io/api v0.20.2/go.mod h1:d7n6Ehyzx+S+cE3VhTGfVNNqtGc/oL9DCdYYahlurV8= -k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg= -k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/client-go v0.20.2 h1:uuf+iIAbfnCSw8IGAv/Rg0giM+2bOzHLOsbbrwrdhNQ= -k8s.io/client-go v0.20.2/go.mod h1:kH5brqWqp7HDxUFKoEgiI4v8G1xzbe9giaCenUWJzgE= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -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= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/mcs-api v0.3.0 h1:LjRvgzjMrvO1904GP6XBJSnIX221DJMyQlZOYt9LAnM= +sigs.k8s.io/mcs-api v0.3.0/go.mod h1:zZ5CK8uS6HaLkxY4HqsmcBHfzHuNMrY2uJy8T7jffK4= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/man/coredns-bind.7 b/man/coredns-bind.7 index 608268d88f..2306bca15e 100644 --- a/man/coredns-bind.7 +++ b/man/coredns-bind.7 @@ -14,7 +14,10 @@ another IP instead. If several addresses are provided, a listener will be open on each of the IP provided. .PP -Each address has to be an IP of one of the interfaces of the host. +Each address has to be an IP or name of one of the interfaces of the host. Bind by interface name, binds to the IPs on that interface at the time of startup or reload (reload will happen with a SIGHUP or if the config file changes). + +.PP +If the given argument is an interface name, and that interface has serveral IP addresses, CoreDNS will listen on all of the interface IP addresses (including IPv4 and IPv6). .SH "SYNTAX" .PP @@ -75,6 +78,20 @@ The following sample is equivalent to the preceding: .fi .RE +.PP +The following server block, binds on localhost with its interface name (both "127.0.0.1" and "::1"): + +.PP +.RS + +.nf +\&. { + bind lo +} + +.fi +.RE + .SH "BUGS" .PP When defining more than one server block, take care not to bind more than one server to the same diff --git a/man/coredns-dnstap.7 b/man/coredns-dnstap.7 index 3dbb0f9581..6d39902505 100644 --- a/man/coredns-dnstap.7 +++ b/man/coredns-dnstap.7 @@ -71,7 +71,7 @@ dnstap tcp://127.0.0.1:6000 full .SH "COMMAND LINE TOOL" .PP Dnstap has a command line tool that can be used to inspect the logging. The tool can be found -at Github: https://github.com/dnstap/golang-dnstap +at GitHub: https://github.com/dnstap/golang-dnstap \[la]https://github.com/dnstap/golang-dnstap\[ra]. It's written in Go. .PP diff --git a/man/coredns-etcd.7 b/man/coredns-etcd.7 index 371f81f2bd..f3484adde4 100644 --- a/man/coredns-etcd.7 +++ b/man/coredns-etcd.7 @@ -1,5 +1,5 @@ .\" Generated by Mmark Markdown Processer - mmark.miek.nl -.TH "COREDNS-ETCD" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" +.TH "COREDNS-ETCD" 7 "August 2025" "CoreDNS" "CoreDNS Plugins" .SH "NAME" .PP @@ -85,6 +85,10 @@ file - if the server certificate is not signed by a system-installed CA and clie is needed. .RE +.IP \(bu 4 +\fB\fCmin-lease-ttl\fR the minimum TTL for DNS records based on etcd lease duration. Accepts flexible time formats like '30', '30s', '5m', '1h', '2h30m'. Default: 30 seconds. +.IP \(bu 4 +\fB\fCmax-lease-ttl\fR the maximum TTL for DNS records based on etcd lease duration. Accepts flexible time formats like '30', '30s', '5m', '1h', '2h30m'. Default: 24 hours. .SH "SPECIAL BEHAVIOUR" @@ -93,7 +97,7 @@ The \fIetcd\fP plugin leverages directory structure to look for related entries. an entry \fB\fC/skydns/test/skydns/mx\fR would have entries like \fB\fC/skydns/test/skydns/mx/a\fR, \fB\fC/skydns/test/skydns/mx/b\fR and so on. Similarly a directory \fB\fC/skydns/test/skydns/mx1\fR will have all \fB\fCmx1\fR entries. Note this plugin will search through the entire (sub)tree for records. In case of the -first example, a query for \fB\fCmx.skydns.text\fR will return both the contents of the \fB\fCa\fR and \fB\fCb\fR records. +first example, a query for \fB\fCmx.skydns.test\fR will return both the contents of the \fB\fCa\fR and \fB\fCb\fR records. If the directory extends deeper those records are returned as well. .PP @@ -120,6 +124,8 @@ skydns.local { etcd { path /skydns endpoint http://localhost:2379 + min\-lease\-ttl 60 # minimum 1 minute for lease\-based records + max\-lease\-ttl 1h # maximum 1 hour for lease\-based records } prometheus cache @@ -349,6 +355,7 @@ If you would like to use \fB\fCTXT\fR records, you can set the following: .nf % etcdctl put /skydns/local/skydns/x6 '{"ttl":60,"text":"this is a random text message."}' +% etcdctl put /skydns/local/skydns/x7 '{"ttl":60,"text":"this is a another random text message."}' .fi .RE @@ -362,6 +369,7 @@ If you query the zone name for \fB\fCTXT\fR now, you will get the following resp .nf % dig +short skydns.local TXT @localhost "this is a random text message." +"this is a another random text message." .fi .RE diff --git a/man/coredns-file.7 b/man/coredns-file.7 index 9ba8a7e648..972ac3d476 100644 --- a/man/coredns-file.7 +++ b/man/coredns-file.7 @@ -1,5 +1,5 @@ .\" Generated by Mmark Markdown Processer - mmark.miek.nl -.TH "COREDNS-FILE" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" +.TH "COREDNS-FILE" 7 "May 2025" "CoreDNS" "CoreDNS Plugins" .SH "NAME" .PP @@ -39,6 +39,7 @@ If you want to round-robin A and AAAA responses look at the \fIloadbalance\fP pl .nf file DBFILE [ZONES... ] { reload DURATION + fallthrough [ZONES...] } .fi @@ -48,6 +49,11 @@ file DBFILE [ZONES... ] { \fB\fCreload\fR interval to perform a reload of the zone if the SOA version changes. Default is one minute. Value of \fB\fC0\fR means to not scan for changes and reload. For example, \fB\fC30s\fR checks the zonefile every 30 seconds and reloads the zone when serial changes. +.IP \(bu 4 +\fB\fCfallthrough\fR If zone matches and no record can be generated, pass request to the next plugin. +If \fB[ZONES...]\fP is omitted, then fallthrough happens for all zones for which the plugin +is authoritative. If specific zones are listed (for example \fB\fCin-addr.arpa\fR and \fB\fCip6.arpa\fR), then only +queries for those zones will be subject to fallthrough. .PP diff --git a/man/coredns-geoip.7 b/man/coredns-geoip.7 new file mode 100644 index 0000000000..28104259d0 --- /dev/null +++ b/man/coredns-geoip.7 @@ -0,0 +1,118 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-GEOIP" 7 "July 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIgeoip\fP - Lookup maxmind geoip2 databases using the client IP, then add associated geoip data to the context request. + +.SH "DESCRIPTION" +.PP +The \fIgeoip\fP plugin add geo location data associated with the client IP, it allows you to configure a geoIP2 maxmind database +\[la]https://dev.maxmind.com/geoip/docs/databases\[ra] to add the geo location data associated with the IP address. + +.PP +The data is added leveraging the \fImetadata\fP plugin, values can then be retrieved using it as well, for example: + +.PP +.RS + +.nf +import ( + "strconv" + "github.com/coredns/coredns/plugin/metadata" +) +// ... +if getLongitude := metadata.ValueFunc(ctx, "geoip/longitude"); getLongitude != nil { + if longitude, err := strconv.ParseFloat(getLongitude(), 64); err == nil { + // Do something useful with longitude. + } +} else { + // The metadata label geoip/longitude for some reason, was not set. +} +// ... + +.fi +.RE + +.SH "DATABASES" +.PP +The supported databases use city schema such as \fB\fCCity\fR and \fB\fCEnterprise\fR. Other databases types with different schemas are not supported yet. + +.PP +You can download a free and public City database +\[la]https://dev.maxmind.com/geoip/geolite2-free-geolocation-data\[ra]. + +.SH "SYNTAX" +.PP +.RS + +.nf +geoip [DBFILE] + +.fi +.RE + +.IP \(bu 4 +\fBDBFILE\fP the mmdb database file path. + + +.SH "EXAMPLES" +.PP +The following configuration configures the \fB\fCCity\fR database. + +.PP +.RS + +.nf +\&. { + geoip /opt/geoip2/db/GeoLite2\-City.mmdb + metadata # Note that metadata plugin must be enabled as well. +} + +.fi +.RE + +.SH "METADATADA LABELS" +.PP +A limited set of fields will be exported as labels, all values are stored using strings \fBregardless of their underlying value type\fP, and therefore you may have to convert it back to its original type, note that numeric values are always represented in base 10. + +.RS +.TS +allbox; +l l l l +l l l l . +\fBLabel\fP\fB Type\fP\fB Example\fP\fB Description\fP +\fB\fCgeoip/city/name\fR \fB\fCstring\fR \fB\fCCambridge\fR Then city name in English language. +\fB\fCgeoip/country/code\fR \fB\fCstring\fR \fB\fCGB\fR Country ISO 3166-1 +\[la]https://en.wikipedia.org/wiki/ISO_3166-1\[ra] code. +\fB\fCgeoip/country/name\fR \fB\fCstring\fR \fB\fCUnited Kingdom\fR The country name in English language. +\fB\fCgeoip/country/is_in_european_union\fR \fB\fCbool\fR \fB\fCfalse\fR Either \fB\fCtrue\fR or \fB\fCfalse\fR. +\fB\fCgeoip/continent/code\fR \fB\fCstring\fR \fB\fCEU\fR See Continent codes +\[la]#ContinentCodes\[ra]. +\fB\fCgeoip/continent/name\fR \fB\fCstring\fR \fB\fCEurope\fR The continent name in English language. +\fB\fCgeoip/latitude\fR \fB\fCfloat64\fR \fB\fC52.2242\fR Base 10, max available precision. +\fB\fCgeoip/longitude\fR \fB\fCfloat64\fR \fB\fC0.1315\fR Base 10, max available precision. +\fB\fCgeoip/timezone\fR \fB\fCstring\fR \fB\fCEurope/London\fR The timezone. +\fB\fCgeoip/postalcode\fR \fB\fCstring\fR \fB\fCCB4\fR The postal code. +.TE +.RE + + +.SH "CONTINENT CODES" +.RS +.TS +allbox; +l l +l l . +\fBValue\fP\fB Continent (EN)\fP +AF Africa +AN Antarctica +AS Asia +EU Europe +NA North America +OC Oceania +SA South America +.TE +.RE + + diff --git a/man/coredns-header.7 b/man/coredns-header.7 new file mode 100644 index 0000000000..c0e76e46df --- /dev/null +++ b/man/coredns-header.7 @@ -0,0 +1,84 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-HEADER" 7 "July 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIheader\fP - modifies the header for responses. + +.SH "DESCRIPTION" +.PP +\fIheader\fP ensures that the flags are in the desired state for responses. The modifications are made transparently for +the client. + +.SH "SYNTAX" +.PP +.RS + +.nf +header { + ACTION FLAGS... + ACTION FLAGS... +} + +.fi +.RE + +.IP \(bu 4 +\fBACTION\fP defines the state for DNS message header flags. Actions are evaluated in the order they are defined so last one has the +most precedence. Allowed values are: + +.RS +.IP \(en 4 +\fB\fCset\fR +.IP \(en 4 +\fB\fCclear\fR + +.RE +.IP \(bu 4 +\fBFLAGS\fP are the DNS header flags that will be modified. Current supported flags include: + +.RS +.IP \(en 4 +\fB\fCaa\fR - Authoritative(Answer) +.IP \(en 4 +\fB\fCra\fR - RecursionAvailable +.IP \(en 4 +\fB\fCrd\fR - RecursionDesired + +.RE + + +.SH "EXAMPLES" +.PP +Make sure recursive available \fB\fCra\fR flag is set in all the responses: + +.PP +.RS + +.nf +\&. { + header { + set ra + } +} + +.fi +.RE + +.PP +Make sure "recursion available" \fB\fCra\fR and "authoritative answer" \fB\fCaa\fR flags are set and "recursion desired" is cleared in all responses: + +.PP +.RS + +.nf +\&. { + header { + set ra aa + clear rd + } +} + +.fi +.RE + diff --git a/man/coredns-metrics.7 b/man/coredns-metrics.7 index 565af34458..9c9410bc53 100644 --- a/man/coredns-metrics.7 +++ b/man/coredns-metrics.7 @@ -1,5 +1,5 @@ .\" Generated by Mmark Markdown Processer - mmark.miek.nl -.TH "COREDNS-METRICS" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" +.TH "COREDNS-METRICS" 7 "May 2025" "CoreDNS" "CoreDNS Plugins" .SH "NAME" .PP @@ -10,30 +10,38 @@ .PP With \fIprometheus\fP you export metrics from CoreDNS and any plugin that has them. The default location for the metrics is \fB\fClocalhost:9153\fR. The metrics path is fixed to \fB\fC/metrics\fR. -The following metrics are exported: + +.PP +In addition to the default Go metrics exported by the Prometheus Go client +\[la]https://prometheus.io/docs/guides/go-application/\[ra], +the following metrics are exported: .IP \(bu 4 \fB\fCcoredns_build_info{version, revision, goversion}\fR - info about CoreDNS itself. .IP \(bu 4 \fB\fCcoredns_panics_total{}\fR - total number of panics. .IP \(bu 4 -\fB\fCcoredns_dns_requests_total{server, zone, proto, family, type}\fR - total query count. +\fB\fCcoredns_dns_requests_total{server, zone, view, proto, family, type}\fR - total query count. +.IP \(bu 4 +\fB\fCcoredns_dns_request_duration_seconds{server, zone, view, type}\fR - duration to process each query. .IP \(bu 4 -\fB\fCcoredns_dns_request_duration_seconds{server, zone, type}\fR - duration to process each query. +\fB\fCcoredns_dns_request_size_bytes{server, zone, view, proto}\fR - size of the request in bytes. Uses the original size before any plugin rewrites. .IP \(bu 4 -\fB\fCcoredns_dns_request_size_bytes{server, zone, proto}\fR - size of the request in bytes. +\fB\fCcoredns_dns_do_requests_total{server, view, zone}\fR - queries that have the DO bit set .IP \(bu 4 -\fB\fCcoredns_dns_do_requests_total{server, zone}\fR - queries that have the DO bit set +\fB\fCcoredns_dns_response_size_bytes{server, zone, view, proto}\fR - response size in bytes. .IP \(bu 4 -\fB\fCcoredns_dns_response_size_bytes{server, zone, proto}\fR - response size in bytes. +\fB\fCcoredns_dns_responses_total{server, zone, view, rcode, plugin}\fR - response per zone, rcode and plugin. .IP \(bu 4 -\fB\fCcoredns_dns_responses_total{server, zone, rcode}\fR - response per zone and rcode. +\fB\fCcoredns_dns_https_responses_total{server, status}\fR - responses per server and http status code. .IP \(bu 4 -\fB\fCcoredns_plugin_enabled{server, zone, name}\fR - indicates whether a plugin is enabled on per server and zone basis. +\fB\fCcoredns_dns_quic_responses_total{server, status}\fR - responses per server and QUIC application code. +.IP \(bu 4 +\fB\fCcoredns_plugin_enabled{server, zone, view, name}\fR - indicates whether a plugin is enabled on per server, zone and view basis. .PP -Each counter has a label \fB\fCzone\fR which is the zonename used for the request/response. +Almost each counter has a label \fB\fCzone\fR which is the zonename used for the request/response. .PP Extra labels used are: @@ -48,14 +56,35 @@ this is \fB\fCdns://:53\fR. If you are using the \fIbind\fP plugin an IP address The address family (\fB\fCfamily\fR) of the transport (1 = IP (IP version 4), 2 = IP6 (IP version 6)). .IP \(bu 4 \fB\fCtype\fR which holds the query type. It holds most common types (A, AAAA, MX, SOA, CNAME, PTR, TXT, -NS, SRV, DS, DNSKEY, RRSIG, NSEC, NSEC3, IXFR, AXFR and ANY) and "other" which lumps together all +NS, SRV, DS, DNSKEY, RRSIG, NSEC, NSEC3, HTTPS, IXFR, AXFR and ANY) and "other" which lumps together all other types. +.IP \(bu 4 +\fB\fCstatus\fR which holds the https status code. Possible values are: + +.RS +.IP \(en 4 +200 - request is processed, +.IP \(en 4 +404 - request has been rejected on validation, +.IP \(en 4 +400 - request to dns message conversion failed, +.IP \(en 4 +500 - processing ended up with no response. + +.RE +.IP \(bu 4 +the \fB\fCplugin\fR label holds the name of the plugin that made the write to the client. If the server +did the write (on error for instance), the value is empty. .PP If monitoring is enabled, queries that do not enter the plugin chain are exported under the fake name "dropped" (without a closing dot - this is never a valid domain name). +.PP +Other plugins may export additional stats when the \fIprometheus\fP plugin is enabled. Those stats are documented in each +plugin's README. + .PP This plugin can only be used once per Server Block. diff --git a/man/coredns-multisocket.7 b/man/coredns-multisocket.7 new file mode 100644 index 0000000000..f0013a4c53 --- /dev/null +++ b/man/coredns-multisocket.7 @@ -0,0 +1,92 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-MULTISOCKET" 7 "May 2025" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fImultisocket\fP - allows to start multiple servers that will listen on one port. + +.SH "DESCRIPTION" +.PP +With \fImultisocket\fP, you can define the number of servers that will listen on the same port. The SO_REUSEPORT socket +option allows to open multiple listening sockets at the same address and port. In this case, kernel distributes incoming +connections between sockets. + +.PP +Enabling this option allows to start multiple servers, which increases the throughput of CoreDNS in environments with a +large number of CPU cores. + +.SH "SYNTAX" +.PP +.RS + +.nf +multisocket [NUM\_SOCKETS] + +.fi +.RE + +.IP \(bu 4 +\fBNUM_SOCKETS\fP - the number of servers that will listen on one port. Default value is equal to GOMAXPROCS. + + +.SH "EXAMPLES" +.PP +Start 5 TCP/UDP servers on the same port. + +.PP +.RS + +.nf +\&. { + multisocket 5 + forward . /etc/resolv.conf +} + +.fi +.RE + +.PP +Do not define \fB\fCNUM_SOCKETS\fR, in this case it will take a value equal to GOMAXPROCS. + +.PP +.RS + +.nf +\&. { + multisocket + forward . /etc/resolv.conf +} + +.fi +.RE + +.SH "RECOMMENDATIONS" +.PP +The tests of the \fB\fCmultisocket\fR plugin, which were conducted for \fB\fCNUM_SOCKETS\fR from 1 to 10, did not reveal any side +effects or performance degradation. + +.PP +This means that the \fB\fCmultisocket\fR plugin can be used with a default value that is equal to GOMAXPROCS. + +.PP +However, to achieve the best results, it is recommended to consider the specific environment and plugins used in +CoreDNS. To determine the optimal configuration, it is advisable to conduct performance tests with different +\fB\fCNUM_SOCKETS\fR, measuring Queries Per Second (QPS) and system load. + +.PP +If conducting such tests is difficult, follow these recommendations: +1. Determine the maximum CPU consumption of CoreDNS server without \fB\fCmultisocket\fR plugin. Estimate how much CPU CoreDNS + actually consumes in specific environment under maximum load. +2. Align \fB\fCNUM_SOCKETS\fR with the estimated CPU usage and CPU limits or system's available resources. + Examples: + - If CoreDNS consumes 4 CPUs and 8 CPUs are available, set \fB\fCNUM_SOCKETS\fR to 2. + - If CoreDNS consumes 8 CPUs and 64 CPUs are available, set \fB\fCNUM_SOCKETS\fR to 8. + +.SH "LIMITATIONS" +.PP +The SO_REUSEPORT socket option is not available for some operating systems. It is available since Linux Kernel 3.9 and +not available for Windows at all. + +.PP +Using this plugin with a system that does not support SO_REUSEPORT will cause an \fB\fCaddress already in use\fR error. + diff --git a/man/coredns-nomad.7 b/man/coredns-nomad.7 new file mode 100644 index 0000000000..18585d2ed6 --- /dev/null +++ b/man/coredns-nomad.7 @@ -0,0 +1,292 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-NOMAD" 7 "September 2025" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fInomad\fP - enables reading zone data from a Nomad cluster. + +.SH "DESCRIPTION" +.PP +This plugin serves DNS records for services registered with Nomad. Nomad 1.3+ comes with support for discovering services +\[la]https://www.hashicorp.com/en/blog/nomad-service-discovery\[ra] with an in-built service catalogue that is available via the HTTP API. This plugin extends the HTTP API and provides a DNS interface for querying the service catalogue. + +.PP +The query can be looked up with the format \fB\fC[service].[namespace].service.nomad\fR. The plugin currently handles A, AAAA and SRV records. Refer to #Usage Example +\[la]#usage-example\[ra] for more details. + +.SH "EXAMPLE JOB TEMPLATE" +.PP +.RS + +.nf +job "dns" { + type = "service" + + group "dns" { + network { + port "dns" { + static = 1053 + } + } + task "dns" { + driver = "docker" + + config { + image = "coredns/coredns:latest" + ports = ["dns"] + args = ["\-conf", "/secrets/coredns/Corefile", "\-dns.port", "1053"] + } + + service { + name = "hostmaster" + provider = "nomad" + port = "dns" + address\_mode = "driver" + } + + identity { + env = true + } + + template { + data = <> DiG 9.18.1\-1ubuntu1.2\-Ubuntu <<>> redis.default.service.nomad @127.0.0.1 \-p 1053 +;; global options: +cmd +;; Got answer: +;; \->>HEADER<<\- opcode: QUERY, status: NOERROR, id: 54986 +;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 +;; WARNING: recursion requested but not available + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 1232 +; COOKIE: bdc9237f49a1f744 (echoed) +;; QUESTION SECTION: +;redis.default.service.nomad. IN A + +;; ANSWER SECTION: +redis.default.service.nomad. 10 IN A 192.168.29.76 +redis.default.service.nomad. 10 IN A 192.168.29.76 +redis.default.service.nomad. 10 IN A 192.168.29.76 + +;; Query time: 4 msec +;; SERVER: 127.0.0.1#1053(127.0.0.1) (UDP) +;; WHEN: Thu Jan 05 12:12:25 IST 2023 +;; MSG SIZE rcvd: 165 + +.fi +.RE + +.SS "SRV RECORD" +.PP +Since an A record doesn't contain the port number, SRV record can be used to query the port number of a service. + +.PP +.RS + +.nf +dig redis.default.service.nomad @127.0.0.1 \-p 1053 SRV + +; <<>> DiG 9.18.1\-1ubuntu1.2\-Ubuntu <<>> redis.default.service.nomad @127.0.0.1 \-p 1053 SRV +;; global options: +cmd +;; Got answer: +;; \->>HEADER<<\- opcode: QUERY, status: NOERROR, id: 49945 +;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 4 +;; WARNING: recursion requested but not available + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 1232 +; COOKIE: 14572535f3ba6648 (echoed) +;; QUESTION SECTION: +;redis.default.service.nomad. IN SRV + +;; ANSWER SECTION: +redis.default.service.nomad. 8 IN SRV 10 10 25395 redis.default.service.nomad. +redis.default.service.nomad. 8 IN SRV 10 10 20888 redis.default.service.nomad. +redis.default.service.nomad. 8 IN SRV 10 10 26292 redis.default.service.nomad. + +;; ADDITIONAL SECTION: +redis.default.service.nomad. 8 IN A 192.168.29.76 +redis.default.service.nomad. 8 IN A 192.168.29.76 +redis.default.service.nomad. 8 IN A 192.168.29.76 + +;; Query time: 0 msec +;; SERVER: 127.0.0.1#1053(127.0.0.1) (UDP) +;; WHEN: Thu Jan 05 12:12:20 IST 2023 +;; MSG SIZE rcvd: 339 + +.fi +.RE + +.SS "SOA RECORD" +.PP +.RS + +.nf +$ dig @localhost \-p 1053 1dns.default.service.nomad. + +; <<>> DiG 9.18.12\-0ubuntu0.22.04.2\-Ubuntu <<>> @localhost \-p 1053 1dns.default.service.nomad. +; (1 server found) +;; global options: +cmd +;; Got answer: +;; \->>HEADER<<\- opcode: QUERY, status: NXDOMAIN, id: 21012 +;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 +;; WARNING: recursion requested but not available + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 1232 +; COOKIE: 6d146bb140b4d8ca (echoed) +;; QUESTION SECTION: +;1dns.default.service.nomad. IN A + +;; ANSWER SECTION: +1dns.default.service.nomad. 5 IN SOA ns1.1dns.default.service.nomad. ns1.1dns.default.service.nomad. 1 3600 600 604800 3600 + +;; Query time: 0 msec +;; SERVER: 127.0.0.1#1053(localhost) (UDP) +;; WHEN: Wed Aug 23 21:14:41 EEST 2023 +;; MSG SIZE rcvd: 189 + +.fi +.RE + diff --git a/man/coredns-quic.7 b/man/coredns-quic.7 new file mode 100644 index 0000000000..6301ec2295 --- /dev/null +++ b/man/coredns-quic.7 @@ -0,0 +1,69 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-QUIC" 7 "May 2025" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIquic\fP - configures DNS-over-QUIC (DoQ) server options. + +.SH "DESCRIPTION" +.PP +The \fIquic\fP plugin allows you to configure parameters for the DNS-over-QUIC (DoQ) server to fine-tune the security posture and performance of the server. + +.PP +This plugin can only be used once per quic Server Block. + +.SH "SYNTAX" +.PP +.RS + +.nf +quic { + max\_streams POSITIVE\_INTEGER + worker\_pool\_size POSITIVE\_INTEGER +} + +.fi +.RE + +.IP \(bu 4 +\fB\fCmax_streams\fR limits the number of concurrent QUIC streams per connection. This helps prevent DoS attacks where an attacker could open many streams on a single connection, exhausting server resources. The default value is 256 if not specified. +.IP \(bu 4 +\fB\fCworker_pool_size\fR defines the size of the worker pool for processing QUIC streams across all connections. The default value is 512 if not specified. This limits the total number of concurrent streams that can be processed across all connections. + + +.SH "EXAMPLES" +.PP +Enable DNS-over-QUIC with default settings (256 concurrent streams per connection, 512 worker pool size): + +.PP +.RS + +.nf +quic://.:8853 { + tls cert.pem key.pem + quic + whoami +} + +.fi +.RE + +.PP +Set custom limits for maximum QUIC streams per connection and worker pool size: + +.PP +.RS + +.nf +quic://.:8853 { + tls cert.pem key.pem + quic { + max\_streams 16 + worker\_pool\_size 65536 + } + whoami +} + +.fi +.RE + diff --git a/man/coredns-timeouts.7 b/man/coredns-timeouts.7 new file mode 100644 index 0000000000..a283f6cbdb --- /dev/null +++ b/man/coredns-timeouts.7 @@ -0,0 +1,103 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-TIMEOUTS" 7 "July 2023" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fItimeouts\fP - allows you to configure the server read, write and idle timeouts for the TCP, TLS and DoH servers. + +.SH "DESCRIPTION" +.PP +CoreDNS is configured with sensible timeouts for server connections by default. +However in some cases for example where CoreDNS is serving over a slow mobile +data connection the default timeouts are not optimal. + +.PP +Additionally some routers hold open connections when using DNS over TLS or DNS +over HTTPS. Allowing a longer idle timeout helps performance and reduces issues +with such routers. + +.PP +The \fItimeouts\fP "plugin" allows you to configure CoreDNS server read, write and +idle timeouts. + +.SH "SYNTAX" +.PP +.RS + +.nf +timeouts { + read DURATION + write DURATION + idle DURATION +} + +.fi +.RE + +.PP +For any timeouts that are not provided, default values are used which may vary +depending on the server type. At least one timeout must be specified otherwise +the entire timeouts block should be omitted. + +.SH "EXAMPLES" +.PP +Start a DNS-over-TLS server that picks up incoming DNS-over-TLS queries on port +5553 and uses the nameservers defined in \fB\fC/etc/resolv.conf\fR to resolve the +query. This proxy path uses plain old DNS. A 10 second read timeout, 20 +second write timeout and a 60 second idle timeout have been configured. + +.PP +.RS + +.nf +tls://.:5553 { + tls cert.pem key.pem ca.pem + timeouts { + read 10s + write 20s + idle 60s + } + forward . /etc/resolv.conf +} + +.fi +.RE + +.PP +Start a DNS-over-HTTPS server that is similar to the previous example. Only the +read timeout has been configured for 1 minute. + +.PP +.RS + +.nf +https://. { + tls cert.pem key.pem ca.pem + timeouts { + read 1m + } + forward . /etc/resolv.conf +} + +.fi +.RE + +.PP +Start a standard TCP/UDP server on port 1053. A read and write timeout has been +configured. The timeouts are only applied to the TCP side of the server. + +.PP +.RS + +.nf +\&.:1053 { + timeouts { + read 15s + write 30s + } + forward . /etc/resolv.conf +} + +.fi +.RE + diff --git a/man/coredns-tsig.7 b/man/coredns-tsig.7 new file mode 100644 index 0000000000..9716515fac --- /dev/null +++ b/man/coredns-tsig.7 @@ -0,0 +1,150 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-TSIG" 7 "July 2022" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fItsig\fP - validate TSIG requests and sign responses. + +.SH "DESCRIPTION" +.PP +With \fItsig\fP, you can define a set of TSIG secret keys for validating incoming TSIG requests and signing +responses. It can also require TSIG for certain query types, refusing requests that do not comply. + +.SH "SYNTAX" +.PP +.RS + +.nf +tsig [ZONE...] { + secret NAME KEY + secrets FILE + require [QTYPE...] +} + +.fi +.RE + +.IP \(bu 4 +\fBZONE\fP - the zones \fItsig\fP will TSIG. By default, the zones from the server block are used. +.IP \(bu 4 +\fB\fCsecret\fR \fBNAME\fP \fBKEY\fP - specifies a TSIG secret for \fBNAME\fP with \fBKEY\fP. Use this option more than once +to define multiple secrets. Secrets are global to the server instance, not just for the enclosing \fBZONE\fP. +.IP \(bu 4 +\fB\fCsecrets\fR \fBFILE\fP - same as \fB\fCsecret\fR, but load the secrets from a file. The file may define any number + of unique keys, each in the following \fB\fCnamed.conf\fR format: + +.PP +.RS + +.nf + key "example." { + secret "X28hl0BOfAL5G0jsmJWSacrwn7YRm2f6U5brnzwWEus="; + }; + +.fi +.RE + + +Each key may also specify an \fB\fCalgorithm\fR e.g. \fB\fCalgorithm hmac-sha256;\fR, but this is currently ignored by the plugin. + +.RS +.IP \(en 4 +\fB\fCrequire\fR \fBQTYPE...\fP - the query types that must be TSIG'd. Requests of the specified types +will be \fB\fCREFUSED\fR if they are not signed.\fB\fCrequire all\fR will require requests of all types to be +signed. \fB\fCrequire none\fR will not require requests any types to be signed. Default behavior is to not require. + +.RE + + +.SH "EXAMPLES" +.PP +Require TSIG signed transactions for transfer requests to \fB\fCexample.zone\fR. + +.PP +.RS + +.nf +example.zone { + tsig { + secret example.zone.key. NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk= + require AXFR IXFR + } + transfer { + to * + } +} + +.fi +.RE + +.PP +Require TSIG signed transactions for all requests to \fB\fCauth.zone\fR. + +.PP +.RS + +.nf +auth.zone { + tsig { + secret auth.zone.key. NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk= + require all + } + forward . 10.1.0.2 +} + +.fi +.RE + +.SH "BUGS" +.SS "ZONE TRANSFER NOTIFIES" +.PP +With the transfer plugin, zone transfer notifications from CoreDNS are not TSIG signed. + +.SS "SPECIAL CONSIDERATIONS FOR FORWARDING SERVERS (RFC 8945 5.5)" +.PP +https://datatracker.ietf.org/doc/html/rfc8945#section-5.5 +\[la]https://datatracker.ietf.org/doc/html/rfc8945#section-5.5\[ra] + +.PP +CoreDNS does not implement this section as follows ... + +.IP \(bu 4 +RFC requirement: +> If the name on the TSIG is not +of a secret that the server shares with the originator, the server +MUST forward the message unchanged including the TSIG. + + +.PP +CoreDNS behavior: +If ths zone of the request matches the \fItsig\fP plugin zones, then the TSIG record +is always stripped. But even when the \fItsig\fP plugin is not involved, the \fIforward\fP plugin +may alter the message with compression, which would cause validation failure +at the destination. + +.IP \(bu 4 +RFC requirement: +> If the TSIG passes all checks, the forwarding +server MUST, if possible, include a TSIG of its own to the +destination or the next forwarder. + + +.PP +CoreDNS behavior: +If ths zone of the request matches the \fItsig\fP plugin zones, \fIforward\fP plugin will +proxy the request upstream without TSIG. + +.IP \(bu 4 +RFC requirement: +> If no transaction security is +available to the destination and the message is a query, and if the +corresponding response has the AD flag (see RFC4035) set, the +forwarder MUST clear the AD flag before adding the TSIG to the +response and returning the result to the system from which it +received the query. + + +.PP +CoreDNS behavior: +The AD flag is not cleared. + diff --git a/man/coredns-view.7 b/man/coredns-view.7 new file mode 100644 index 0000000000..503576bd9e --- /dev/null +++ b/man/coredns-view.7 @@ -0,0 +1,196 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-VIEW" 7 "June 2025" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIview\fP - defines conditions that must be met for a DNS request to be routed to the server block. + +.SH "DESCRIPTION" +.PP +\fIview\fP defines an expression that must evaluate to true for a DNS request to be routed to the server block. +This enables advanced server block routing functions such as split dns. + +.SH "SYNTAX" +.PP +.RS + +.nf +view NAME { + expr EXPRESSION +} + +.fi +.RE + +.IP \(bu 4 +\fB\fCview\fR \fBNAME\fP - The name of the view used by metrics and exported as metadata for requests that match the +view's expression +.IP \(bu 4 +\fB\fCexpr\fR \fBEXPRESSION\fP - CoreDNS will only route incoming queries to the enclosing server block +if the \fBEXPRESSION\fP evaluates to true. See the \fBExpressions\fP section for available variables and functions. +If multiple instances of view are defined, all \fBEXPRESSION\fP must evaluate to true for CoreDNS will only route +incoming queries to the enclosing server block. + + +.PP +For expression syntax and examples, see the Expressions and Examples sections. + +.SH "EXAMPLES" +.PP +Implement CIDR based split DNS routing. This will return a different +answer for \fB\fCtest.\fR depending on client's IP address. It returns ... +* \fB\fCtest. 3600 IN A 1.1.1.1\fR, for queries with a source address in 127.0.0.0/24 +* \fB\fCtest. 3600 IN A 2.2.2.2\fR, for queries with a source address in 192.168.0.0/16 +* \fB\fCtest. 3600 IN AAAA 2001:0DB8::1\fR, for queries with a source address in 2001:0DB8::/32 +* \fB\fCtest. 3600 IN A 3.3.3.3\fR, for all others + +.PP +.RS + +.nf +\&. { + view example1 { + expr incidr(client\_ip(), '127.0.0.0/24') + } + hosts { + 1.1.1.1 test + } +} + +\&. { + view example2 { + expr incidr(client\_ip(), '192.168.0.0/16') + } + hosts { + 2.2.2.2 test + } +} + +\&. { + view v6\_example1 { + expr incidr(client\_ip(), '2001:0DB8::/32') + } + hosts { + 2001:0DB8::1 test + } +} + +} + +\&. { + hosts { + 3.3.3.3 test + } +} + +.fi +.RE + +.PP +Send all \fB\fCA\fR and \fB\fCAAAA\fR requests to \fB\fC10.0.0.6\fR, and all other requests to \fB\fC10.0.0.1\fR. + +.PP +.RS + +.nf +\&. { + view example { + expr type() in ['A', 'AAAA'] + } + forward . 10.0.0.6 +} + +\&. { + forward . 10.0.0.1 +} + +.fi +.RE + +.PP +Send all requests for \fB\fCabc.*.example.com\fR (where * can be any number of labels), to \fB\fC10.0.0.2\fR, and all other +requests to \fB\fC10.0.0.1\fR. +Note that the regex pattern is enclosed in single quotes, and backslashes are escaped with backslashes. + +.PP +.RS + +.nf +\&. { + view example { + expr name() matches '^abc\\\\..*\\\\.example\\\\.com\\\\.$' + } + forward . 10.0.0.2 +} + +\&. { + forward . 10.0.0.1 +} + +.fi +.RE + +.SH "EXPRESSIONS" +.PP +To evaluate expressions, \fIview\fP uses the expr-lang/expr package ( https://github.com/expr-lang/expr +\[la]https://github.com/expr-lang/expr\[ra] ). +For example, an expression could look like: +\fB\fC(type() == 'A' && name() == 'example.com.') || client_ip() == '1.2.3.4'\fR. + +.PP +All expressions should be written to evaluate to a boolean value. + +.PP +See https://github.com/expr-lang/expr/blob/master/docs/Language-Definition.md +\[la]https://github.com/expr-lang/expr/blob/master/docs/Language-Definition.md\[ra] as a detailed reference for valid syntax. + +.SS "AVAILABLE EXPRESSION FUNCTIONS" +.PP +In the context of the \fIview\fP plugin, expressions can reference DNS query information by using utility +functions defined below. + +.SS "DNS QUERY FUNCTIONS" +.IP \(bu 4 +\fB\fCbufsize() int\fR: the EDNS0 buffer size advertised in the query +.IP \(bu 4 +\fB\fCclass() string\fR: class of the request (IN, CH, ...) +.IP \(bu 4 +\fB\fCclient_ip() string\fR: client's IP address, for IPv6 addresses these are enclosed in brackets: \fB\fC[::1]\fR +.IP \(bu 4 +\fB\fCdo() bool\fR: the EDNS0 DO (DNSSEC OK) bit set in the query +.IP \(bu 4 +\fB\fCid() int\fR: query ID +.IP \(bu 4 +\fB\fCname() string\fR: name of the request (the domain name requested ending with a dot): \fB\fCexample.com.\fR +.IP \(bu 4 +\fB\fCopcode() int\fR: query OPCODE +.IP \(bu 4 +\fB\fCport() string\fR: client's port +.IP \(bu 4 +\fB\fCproto() string\fR: protocol used (tcp or udp) +.IP \(bu 4 +\fB\fCserver_ip() string\fR: server's IP address; for IPv6 addresses these are enclosed in brackets: \fB\fC[::1]\fR +.IP \(bu 4 +\fB\fCserver_port() string\fR : server's port +.IP \(bu 4 +\fB\fCsize() int\fR: request size in bytes +.IP \(bu 4 +\fB\fCtype() string\fR: type of the request (A, AAAA, TXT, ...) + + +.SS "UTILITY FUNCTIONS" +.IP \(bu 4 +\fB\fCincidr(ip string, cidr string) bool\fR: returns true if \fIip\fP is within \fIcidr\fP +.IP \(bu 4 +\fB\fCmetadata(label string)\fR - returns the value for the metadata matching \fIlabel\fP + + +.SH "METADATA" +.PP +The view plugin will publish the following metadata, if the \fImetadata\fP +plugin is also enabled: + +.IP \(bu 4 +\fB\fCview/name\fR: the name of the view handling the current request + + diff --git a/man/coredns.1 b/man/coredns.1 index e73d4780d0..b495009060 100644 --- a/man/coredns.1 +++ b/man/coredns.1 @@ -1,5 +1,5 @@ .\" Generated by Mmark Markdown Processer - mmark.miek.nl -.TH "COREDNS" 1 "March 2021" "CoreDNS" "CoreDNS" +.TH "COREDNS" 1 "May 2021" "CoreDNS" "CoreDNS" .SH "COREDNS" .PP @@ -32,7 +32,7 @@ Available options: specify Corefile to load, if not given CoreDNS will look for a \fB\fCCorefile\fR in the current directory. .TP -\fB-dns.port\fP \fBPORT\fP +\fB-dns.port\fP \fBPORT\fP or \fB-p\fP \fBPORT\fP override default port (53) to listen on. .TP \fB-pidfile\fP \fBFILE\fP @@ -58,5 +58,5 @@ Apache License 2.0 .SH "SEE ALSO" .PP -Corefile(5) coredns-k8s_external(7) coredns-any(7) coredns-hosts(7) coredns-reload(7) coredns-acl(7) coredns-dnssec(7) coredns-health(7) coredns-grpc(7) coredns-sign(7) coredns-log(7) coredns-tls(7) coredns-file(7) coredns-root(7) coredns-loop(7) coredns-chaos(7) coredns-dnstap(7) coredns-pprof(7) coredns-bufsize(7) coredns-clouddns(7) coredns-loadbalance(7) coredns-cache(7) coredns-whoami(7) coredns-minimal(7) coredns-bind(7) coredns-erratic(7) coredns-auto(7) coredns-import(7) coredns-debug(7) coredns-template(7) coredns-azure(7) coredns-autopath(7) coredns-kubernetes(7) coredns-forward(7) coredns-nsid(7) coredns-secondary(7) coredns-route53(7) coredns-local(7) coredns-errors(7) coredns-transfer(7) coredns-ready(7) coredns-metadata(7) coredns-rewrite(7) coredns-metrics(7) coredns-dns64(7) coredns-etcd(7) coredns-cancel(7) coredns-trace(7). +Corefile(5) coredns-k8s_external(7) coredns-any(7) coredns-hosts(7) coredns-acl(7) coredns-dnssec(7) coredns-health(7) coredns-grpc(7) coredns-sign(7) coredns-log(7) coredns-tls(7) coredns-file(7) coredns-root(7) coredns-loop(7) coredns-chaos(7) coredns-dnstap(7) coredns-pprof(7) coredns-bufsize(7) coredns-clouddns(7) coredns-loadbalance(7) coredns-cache(7) coredns-whoami(7) coredns-minimal(7) coredns-dns64(7) coredns-erratic(7) coredns-auto(7) coredns-import(7) coredns-debug(7) coredns-template(7) coredns-azure(7) coredns-autopath(7) coredns-kubernetes(7) coredns-forward(7) coredns-nsid(7) coredns-secondary(7) coredns-route53(7) coredns-local(7) coredns-bind(7) coredns-errors(7) coredns-transfer(7) coredns-ready(7) coredns-reload(7) coredns-rewrite(7) coredns-metrics(7) coredns-metadata(7) coredns-etcd(7) coredns-cancel(7) coredns-trace(7). diff --git a/notes/coredns-1.10.0.md b/notes/coredns-1.10.0.md new file mode 100644 index 0000000000..145f2d4f1d --- /dev/null +++ b/notes/coredns-1.10.0.md @@ -0,0 +1,24 @@ ++++ +title = "CoreDNS-1.10.0 Release" +description = "CoreDNS-1.10.0 Release Notes." +tags = ["Release", "1.10.0", "Notes"] +release = "1.10.0" +date = "2022-09-16T00:00:00+00:00" +author = "coredns" ++++ + +This release adds the new *view* plugin, enabling advanced server-block routing configurations such as split-DNS. + +## Brought to You By + +Ben Kochie +Chris O'Haver +Erik Johansson +John Belamaric +Marius Kimmina +Ondřej Benkovský + +## Noteworthy Changes + +* plugin/view: Advanced routing interface and new 'view' plugin (https://github.com/coredns/coredns/pull/5538) +* plugin/template: Add parseInt template function (https://github.com/coredns/coredns/pull/5609) diff --git a/notes/coredns-1.10.1.md b/notes/coredns-1.10.1.md new file mode 100644 index 0000000000..0ec32ec4ff --- /dev/null +++ b/notes/coredns-1.10.1.md @@ -0,0 +1,50 @@ ++++ +title = "CoreDNS-1.10.1 Release" +description = "CoreDNS-1.10.1 Release Notes." +tags = ["Release", "1.10.1", "Notes"] +release = "1.10.0" +date = "2023-01-20T00:00:00+00:00" +author = "coredns" ++++ + +This release fixes some bugs, and adds some new features including: +* Corrected architecture labels in multi-arch image manifest +* A new plugin *timeouts* that allows configuration of server listener timeout durations +* *acl* can drop queries as an action +* *template* supports creating responses with extended DNS errors +* New weighted policy in *loadbalance* +* Option to serve original record TTLs from *cache* + +## Brought to You By + +Arthur Outhenin-Chalandre, +Ben Kaplan, +Chris O'Haver, +Gabor Dozsa, +Grant Spence, +Kumiko as a Service, +LAMRobinson, +Miciah Dashiel Butler Masters, +Ondřej Benkovský, +Rich, +Stephen Kitt, +Yash Singh, +Yong Tang, +rsclarke, +sanyo0714 + +## Noteworthy Changes + +* plugin/timeouts - Allow ability to configure listening server timeouts (https://github.com/coredns/coredns/pull/5784) +* plugin/acl: adding ability to drop queries (https://github.com/coredns/coredns/pull/5722) +* plugin/template : add support for extended DNS errors (https://github.com/coredns/coredns/pull/5659) +* plugin/kubernetes: error NXDOMAIN for TXT lookups (https://github.com/coredns/coredns/pull/5737) +* plugin/kubernetes: dont match external services when endpoint is specified (https://github.com/coredns/coredns/pull/5734) +* plugin/k8s_external: Fix rcode for headless services (https://github.com/coredns/coredns/pull/5657) +* plugin/edns: remove truncating of question section on bad EDNS version (https://github.com/coredns/coredns/pull/5787) +* plugin/dnstap: Fix behavior when multiple dnstap plugins specified (https://github.com/coredns/coredns/pull/5773) +* plugin/cache: cache now uses source query DNSSEC option for upstream refresh (https://github.com/coredns/coredns/pull/5671) +* Workaround for incorrect architecture (https://github.com/coredns/coredns/pull/5691) +* plugin/loadbalance: Add weighted policy (https://github.com/coredns/coredns/pull/5662) +* plugin/cache: Add keepttl option (https://github.com/coredns/coredns/pull/5879) +* plugin/forward: Fix dnstap for forwarded request/response (https://github.com/coredns/coredns/pull/5890) \ No newline at end of file diff --git a/notes/coredns-1.11.0.md b/notes/coredns-1.11.0.md new file mode 100644 index 0000000000..0f08628e9f --- /dev/null +++ b/notes/coredns-1.11.0.md @@ -0,0 +1,83 @@ ++++ +title = "CoreDNS-1.11.0 Release" +description = "CoreDNS-1.11.0 Release Notes." +tags = ["Release", "1.11.0", "Notes"] +release = "1.11.0" +date = "2023-07-25T00:00:00+00:00" +author = "coredns" ++++ + +## Release Highlights + +* Adds support for accepting DNS connections over QUIC (doq). +* Adds CNAME target rewrites to the _rewrite_ plugin. +* Plus many bug fixes, and some security improvements. + +This release introduces the following backward incompatible changes: +* In the _kubernetes_ plugin, we have dropped support for watching Endpoint and Endpointslice v1beta, since all supported K8s versions now use Endpointslice. +* The _bufsize_ plugin changed its default size limit value to 1232 +* Some changes to _forward_ plugin metrics. +## Brought to You By + +Amila Senadheera, +Antony Chazapis, +Ayato Tokubi, +Ben Kochie, +Catena cyber, +Chris O'Haver, +Dan Salmon, +Dan Wilson, +Denis MACHARD, +Diogenes Pelisson, +Eng Zer Jun, +Fish-pro, +Gabor Dozsa, +Gary McDonald, +João Henri, +Justin, +Lio李歐, +Marcos Mendez, +Marius Kimmina, +Ondřej Benkovský, +Pat Downey, +Petr Menšík, +Rotem Kfir, +Sebastian Dahlgren, +Vancl, +Vinayak Goyal, +W. Trevor King, +Yash Singh, +Yashpal, +Yong Tang, +Yuheng, +cui fliter, +jeremiejig, +junhwong, +rokkiter, +yyzxw + +## Other Noteworthy Changes + +* add support for RISC-V (https://github.com/coredns/coredns/pull/6195) +* doh: allow http as the protocol (https://github.com/coredns/coredns/pull/5762) +* doq: add DNS-Over-QUIC server support (https://github.com/coredns/coredns/pull/6182) +* plugin/bufsize: change default value to 1232 (https://github.com/coredns/coredns/pull/6183) +* plugin/clouddns: fix answers limited to one response (https://github.com/coredns/coredns/pull/5986) +* plugin/dnssec: on delegation, sign DS or NSEC of no DS. (https://github.com/coredns/coredns/pull/5899) +* plugin/dnstap: add tls support (https://github.com/coredns/coredns/pull/5917) +* plugin/forward: continue waiting after receiving malformed responses (https://github.com/coredns/coredns/pull/6014) +* plugin/forward: fix forward metrics for backwards compatibility (https://github.com/coredns/coredns/pull/6178) +* plugin/health: poll localhost by default (https://github.com/coredns/coredns/pull/5934) +* plugin/k8s_external: add fallthrough option (https://github.com/coredns/coredns/pull/5959) +* plugin/kubernetes: expose client-go internal request metrics (https://github.com/coredns/coredns/pull/5991) +* plugin/kubernetes: filter ExternalName service queries for subdomains of subdomains (https://github.com/coredns/coredns/pull/6162) +* plugin/kubernetes: fix headless/endpoint query panics when endpoints are disabled (https://github.com/coredns/coredns/pull/6137) +* plugin/kubernetes: fix ports panic (https://github.com/coredns/coredns/pull/6179) +* plugin/kubernetes: remove Endpoint and EndpointSlice v1beta Support (https://github.com/coredns/coredns/pull/6147) +* plugin/loadbalance: improve weights update (https://github.com/coredns/coredns/pull/5906) +* plugin/rewrite: introduce cname target rewrite rule to rewrite plugin (https://github.com/coredns/coredns/pull/6004) +* plugin/transfer: send notifies after adding zones all zones (https://github.com/coredns/coredns/pull/5774) +* prevent fail counter of a proxy overflows (https://github.com/coredns/coredns/pull/5990) +* prevent panics when using DoHWriter (https://github.com/coredns/coredns/pull/6120) +* run coredns as non root. (https://github.com/coredns/coredns/pull/5969) +* support unix socket for GRPC (https://github.com/coredns/coredns/pull/5943) diff --git a/notes/coredns-1.11.1.md b/notes/coredns-1.11.1.md new file mode 100644 index 0000000000..47fbb72608 --- /dev/null +++ b/notes/coredns-1.11.1.md @@ -0,0 +1,26 @@ ++++ +title = "CoreDNS-1.11.1 Release" +description = "CoreDNS-1.11.1 Release Notes." +tags = ["Release", "1.11.1", "Notes"] +release = "1.11.1" +date = "2023-08-15T00:00:00+00:00" +author = "coredns" ++++ + +This release fixes a major performance regression introduced in 1.11.0 that affected DoT (TLS) forwarded connections. +It also adds a new option to _dnstap_ to add metadata to the dnstap extra field, and fixes a config parsing bug in _cache_. + +## Brought to You By + +Chris O'Haver, +P. Radha Krishna, +Yong Tang, +Yuheng, +Zhizhen He + +## Noteworthy Changes + +* Revert "plugin/forward: Continue waiting after receiving malformed responses (https://github.com/coredns/coredns/pull/6014)" (#6270) +* plugin/dnstap: add support for "extra" field in payload (https://github.com/coredns/coredns/pull/6226) +* plugin/cache: fix keepttl parsing (https://github.com/coredns/coredns/pull/6250) + diff --git a/notes/coredns-1.11.2.md b/notes/coredns-1.11.2.md new file mode 100644 index 0000000000..66ebff63d7 --- /dev/null +++ b/notes/coredns-1.11.2.md @@ -0,0 +1,53 @@ ++++ +title = "CoreDNS-1.11.2 Release" +description = "CoreDNS-1.11.2 Release Notes." +tags = ["Release", "1.11.2", "Notes"] +release = "1.11.2" +date = "2024-01-26T00:00:00+00:00" +author = "coredns" ++++ + +This release contains some new features, bug fixes, and package updates. +New features include: +* When the _forward_ plugin receives a malformed upstream response that overflows, + it will now send an empty response to the client with the truncated (TC) bit set to prompt the client + to retry over TCP. +* The _rewrite_ plugin can now rewrite response codes. +* The _dnstap_ plugin now supports adding metadata to the dnstap `extra` field. + +## Brought to You By + +Amila Senadheera, +Ben Kochie, +Benjamin, +Chris O'Haver, +Grant Spence, +John Belamaric, +Keita Kitamura, +Marius Kimmina, +Michael Grosser, +Ondřej Benkovský, +P. Radha Krishna, +Rahil Bhimjiani, +Sri Harsha, +Tom Thorogood, +Willow (GHOST), +Yong Tang, +Yuheng, +Zhizhen He, +guangwu, +journey-c, +pschou + +## Noteworthy Changes + +* plugin/tls: respect the path specified by root plugin (https://github.com/coredns/coredns/pull/6138) +* plugin/auto: warn when auto is unable to read elements of the directory tree (https://github.com/coredns/coredns/pull/6333) +* plugin/etcd: the etcd client adds the DialKeepAliveTime parameter (https://github.com/coredns/coredns/pull/6351) +* plugin/cache: key cache on Checking Disabled (CD) bit (https://github.com/coredns/coredns/pull/6354) +* plugin/forward: Use the correct root domain name in the forward plugin's health checks (https://github.com/coredns/coredns/pull/6395) +* plugin/forward: Handle UDP responses that overflow with TC bit (https://github.com/coredns/coredns/pull/6277) +* plugin/rewrite: fix multi request concurrency issue in cname rewrite (https://github.com/coredns/coredns/pull/6407) +* plugin/rewrite: add rcode as a rewrite option (https://github.com/coredns/coredns/pull/6204) +* plugin/dnstap: add support for "extra" field in payload (https://github.com/coredns/coredns/pull/6226) +* plugin/cache: fix keepttl parsing (https://github.com/coredns/coredns/pull/6250) diff --git a/notes/coredns-1.11.3.md b/notes/coredns-1.11.3.md new file mode 100644 index 0000000000..e557322ba9 --- /dev/null +++ b/notes/coredns-1.11.3.md @@ -0,0 +1,59 @@ ++++ +title = "CoreDNS-1.11.3 Release" +description = "CoreDNS-1.11.3 Release Notes." +tags = ["Release", "1.11.3", "Notes"] +release = "1.11.3" +date = "2024-04-24T16:57:00-04:00" +author = "coredns" ++++ + +This release contains some new features, bug fixes, and package updates. Because of the deployment issues with the previous release, all changed features from 1.11.2 have been included in this release. +New features include: +* When the _forward_ plugin receives a malformed upstream response that overflows, + it will now send an empty response to the client with the truncated (TC) bit set to prompt the client + to retry over TCP. +* The _rewrite_ plugin can now rewrite response codes. +* The _dnstap_ plugin now supports adding metadata to the dnstap `extra` field. + +## Brought to You By + +Amila Senadheera, +Ben Kochie, +Benjamin, +Chris O'Haver, +Grant Spence, +John Belamaric, +Keita Kitamura, +Marius Kimmina, +Michael Grosser, +Ondřej Benkovský, +P. Radha Krishna, +Rahil Bhimjiani, +Sri Harsha, +Tom Thorogood, +Willow (GHOST), +Yong Tang, +Yuheng, +Zhizhen He, +guangwu, +journey-c, +pschou +Ted Ford + +## Noteworthy Changes + +* plugin/tls: respect the path specified by root plugin (https://github.com/coredns/coredns/pull/6138) +* plugin/auto: warn when auto is unable to read elements of the directory tree (https://github.com/coredns/coredns/pull/6333) +* plugin/etcd: the etcd client adds the DialKeepAliveTime parameter (https://github.com/coredns/coredns/pull/6351) +* plugin/cache: key cache on Checking Disabled (CD) bit (https://github.com/coredns/coredns/pull/6354) +* plugin/forward: Use the correct root domain name in the forward plugin's health checks (https://github.com/coredns/coredns/pull/6395) +* plugin/forward: Handle UDP responses that overflow with TC bit (https://github.com/coredns/coredns/pull/6277) +* plugin/rewrite: fix multi request concurrency issue in cname rewrite (https://github.com/coredns/coredns/pull/6407) +* plugin/rewrite: add rcode as a rewrite option (https://github.com/coredns/coredns/pull/6204) +* plugin/dnstap: add support for "extra" field in payload (https://github.com/coredns/coredns/pull/6226) +* plugin/cache: fix keepttl parsing (https://github.com/coredns/coredns/pull/6250) +* Return RcodeServerFailure when DNS64 has no next plugin (https://github.com/coredns/coredns/pull/6590) +* Change the log flags to be a variable that can be set (https://github.com/coredns/coredns/pull/6546) +* Bump go version to 1.21 (https://github.com/coredns/coredns/pull/6533) +* replace the mutex locks in logging with atomic bool for the "on" flag (https://github.com/coredns/coredns/pull/6525) +* Enable Prometheus native histograms (https://github.com/coredns/coredns/pull/6524) diff --git a/notes/coredns-1.11.4.md b/notes/coredns-1.11.4.md new file mode 100644 index 0000000000..0afad01f8b --- /dev/null +++ b/notes/coredns-1.11.4.md @@ -0,0 +1,66 @@ ++++ +title = "CoreDNS-1.11.4 Release" +description = "CoreDNS-1.11.4 Release Notes." +tags = ["Release", "1.11.4", "Notes"] +release = "1.11.4" +date = "2024-11-13T00:00:00+00:00" +author = "coredns" ++++ + +This release adds some new features and fixes some bugs. New features of note: + * forward plugin: new option `next`, to try alternate upstreams when receiving specified response codes upstreams on (functions like the external plugin _alternate_) + * dnssec plugin: new option to load keys from AWS Secrets Manager + * rewrite plugin: new option to revert EDNS0 option rewrites in responses + +## Brought to You By + +AdamKorcz, +Anifalak, +Ben Kochie, +Chris O'Haver, +Frederic Hemery, +Grant Spence, +Harshita Sao, +Jason Joo, +Jasper Bernhardt, +Johnny Bergström, +Keith Coleman, +Kevin Lyda, +Lan, +Lin-1997, +Manuel Rüger, +Nathan Currier, +Nicolai Søborg, +Nikita Usatov, +Paco Xu, +Reinhard Nägele, +Robbie Ostrow, +TAKAHASHI Shuuji, +Till Riedel, +Tobias Klauser, +YASH JAIN, +cedar-gao, +chenylh, +wmkuipers, +xinbenlv, +zhangguanzhang + +## Noteworthy Changes + +* core: set cache-control max-age as integer, not float (https://github.com/coredns/coredns/pull/6764) +* plugin/metadata: evaluate metadata in plugin order (https://github.com/coredns/coredns/pull/6729) +* plugin/dnssec: dnssec load keys from AWS Secrets Manager (https://github.com/coredns/coredns/pull/6618) +* plugin/rewrite: Add "revert" parameter for EDNS0 options (https://github.com/coredns/coredns/pull/6893) +* container: Restored backwards compatibility of Current Workdir (https://github.com/coredns/coredns/pull/6731) +* plugin/auto: call OnShutdown() for each zone at its own OnShutdown() (https://github.com/coredns/coredns/pull/6705) +* plugin/dnstap: log queue and buffer memory size configuration (https://github.com/coredns/coredns/pull/6591) +* plugin/bind: add zone for link-local IPv6 instead of skipping (https://github.com/coredns/coredns/pull/6547) +* plugin/kubernetes: only create PTR records for endpoints with hostname defined (https://github.com/coredns/coredns/pull/6898) +* plugin/rewrite: execute the reversion in reversed order (https://github.com/coredns/coredns/pull/6872) +* plugin/etcd: fix etcd connection leakage during reload (https://github.com/coredns/coredns/pull/6646) +* plugin/kubernetes: Add useragent (https://github.com/coredns/coredns/pull/6484) +* plugin/hosts: add hostsfile as label for coredns_hosts_entries (https://github.com/coredns/coredns/pull/6801) +* plugin/file: Fix zone parser error handling (https://github.com/coredns/coredns/pull/6680) +* plugin/forward: Add alternate option to forward plugin (https://github.com/coredns/coredns/pull/6681) +* plugin/file: return error when parsing the file fails (https://github.com/coredns/coredns/pull/6699) +* build: Generate zplugin.go correctly with third-party plugins (https://github.com/coredns/coredns/pull/6692) diff --git a/notes/coredns-1.12.0.md b/notes/coredns-1.12.0.md new file mode 100644 index 0000000000..c9c23424c0 --- /dev/null +++ b/notes/coredns-1.12.0.md @@ -0,0 +1,23 @@ ++++ +title = "CoreDNS-1.12.0 Release" +description = "CoreDNS-1.12.0 Release Notes." +tags = ["Release", "1.12.0", "Notes"] +release = "1.12.0" +date = "2024-11-21T00:00:00+00:00" +author = "coredns" ++++ + +This release adds some a feature: +* New multisocket plugin - allows CoreDNS to listen on multiple sockets + +## Brought to You By + +Ben Kochie, +Chris O'Haver, +Emmanuel Ferdman, +Viktor + + +## Noteworthy Changes + +* plugin/multisocket (https://github.com/coredns/coredns/pull/6882) diff --git a/notes/coredns-1.12.1.md b/notes/coredns-1.12.1.md new file mode 100644 index 0000000000..d73841c3ae --- /dev/null +++ b/notes/coredns-1.12.1.md @@ -0,0 +1,33 @@ ++++ +title = "CoreDNS-1.12.1 Release" +description = "CoreDNS-1.12.1 Release Notes." +tags = ["Release", "1.12.1", "Notes"] +release = "1.12.1" +date = "2024-03-24T00:00:00+00:00" +author = "coredns" ++++ + +In this release: +* kubernetes: Revert recent change to only create PTR records for endpoints with hostname defined. +* forward: added option to return SERVFAIL immediately if all upstreams are unhealthy. + +## Brought to You By + +Adrian Moisey, +Arthur Outhenin-Chalandre, +Bartosz Borkowski, +Ben Kochie, +Chris O'Haver, +Min Woo Kim, +Puneet Loya, +Rich, +Viktor, +momantech + + +## Noteworthy Changes + +* core: Increase CNAME lookup limit from 7 to 10 (https://github.com/coredns/coredns/pull/7153) +* plugin/kubernetes: Fix handling of pods having DeletionTimestamp set (https://github.com/coredns/coredns/pull/7119) (#7131) +* plugin/kubernetes: Revert "only create PTR records for endpoints with hostname defined (https://github.com/coredns/coredns/pull/6898)" (#7194) +* plugin/forward: added option `failfast_all_unhealthy_upstreams` to return servfail if all upstreams are down (https://github.com/coredns/coredns/pull/6999) diff --git a/notes/coredns-1.12.2.md b/notes/coredns-1.12.2.md new file mode 100644 index 0000000000..27cd726fd6 --- /dev/null +++ b/notes/coredns-1.12.2.md @@ -0,0 +1,50 @@ ++++ +title = "CoreDNS-1.12.2 Release" +description = "CoreDNS-1.12.2 Release Notes." +tags = ["Release", "1.12.2", "Notes"] +release = "1.12.2" +date = "2024-05-30T00:00:00+00:00" +author = "coredns" ++++ + +This release introduces significant improvements to plugin stability and extensibility. +It adds multicluster support to the Kubernetes plugin, fallthrough support in the file plugin, +and a new SetProxyOptions function for the forward plugin. +Notably, the QUIC (DoQ) plugin now limits concurrent streams, improving performance under load. +Several bug fixes and optimizations improve reliability across plugins, including rewrite, proxy, and metrics. + +## Brought to You By + +Ambrose Chua, +Arthur Outhenin-Chalandre, +Ben Kochie, +Colden Cullen, +Gleb Kogtev, +Hirotaka Tagawa, +Kevin Lyda, +Manuel Rüger, +Mark Mickan, +Parfenov Ivan, +skipper, +vdbe, +Viktor Oreshkin, +Ville Vesilehto, +Yannick Epstein, +Yong Tang + + +## Noteworthy Changes + +* core: Enable plugins via environment during build (https://github.com/coredns/coredns/pull/7310) +* core: Ensure DNS query name reset in plugin.NS error path (https://github.com/coredns/coredns/pull/7142) +* plugin/forward: Added SetProxyOptions function for forward plugin (https://github.com/coredns/coredns/pull/7229) +* plugin/ready: Do not interrupt querying readiness probes for plugins (https://github.com/coredns/coredns/pull/6975) +* plugin/secondary: Make transfer property mandatory (https://github.com/coredns/coredns/pull/7249) +* plugin/rewrite: Truncated upstream response (https://github.com/coredns/coredns/pull/7277) +* plugin/quic: Limit concurrent DoQ streams and goroutines (https://github.com/coredns/coredns/pull/7296) +* plugin/kubernetes: Add multicluster support (https://github.com/coredns/coredns/pull/7266) +* plugin/bind: Remove zone for link-local IPv4 (https://github.com/coredns/coredns/pull/7295) +* plugin/metrics: Preserve request size from plugins (https://github.com/coredns/coredns/pull/7313) +* plugin/proxy: Avoid Dial hang after Transport stopped (https://github.com/coredns/coredns/pull/7321) +* plugin/file: Add fallthrough support (https://github.com/coredns/coredns/pull/7327) +* plugin/kubernetes: Optimize AutoPath slice allocation (https://github.com/coredns/coredns/pull/7323) diff --git a/notes/coredns-1.12.3.md b/notes/coredns-1.12.3.md new file mode 100644 index 0000000000..35bacc8e79 --- /dev/null +++ b/notes/coredns-1.12.3.md @@ -0,0 +1,48 @@ ++++ +title = "CoreDNS-1.12.3 Release" +description = "CoreDNS-1.12.3 Release Notes." +tags = ["Release", "1.12.3", "Notes"] +release = "1.12.3" +date = "2024-08-05T00:00:00+00:00" +author = "coredns" ++++ + +This release improves plugin reliability and standards compliance, adding startup timeout to the Kubernetes +plugin, fallthrough to gRPC, and EDNS0 unset to rewrite. The file plugin now preserves SRV record case per +RFC 6763, route53 is updated to AWS SDK v2, and multiple race conditions in cache and connection handling in +forward are fixed. + +## Brought to You By + +blakebarnett +Brennan Kinney +Cameron Steel +Dave Brown +Dennis Simmons +Guillaume Jacquet +harshith-2411-2002 +houpo-bob +Oleg Guba +Sebastian Mayr +Stephen Kitt +Syed Azeez +Ville Vesilehto +Yong Tang +Yoofi Quansah + + +## Noteworthy Changes + +* plugin/auto: Return REFUSED when no next plugin is available (https://github.com/coredns/coredns/pull/7381) +* plugin/cache: Create a copy of a response to ensure original msg is never modified (https://github.com/coredns/coredns/pull/7357) +* plugin/cache: Fix data race when refreshing cached messages (https://github.com/coredns/coredns/pull/7398) +* plugin/cache: Fix data race when updating the TTL of cached messages (https://github.com/coredns/coredns/pull/7397) +* plugin/file: Return REFUSED when no next plugin is available (https://github.com/coredns/coredns/pull/7381) +* plugin/file: Preserve case in SRV record names and targets per RFC 6763 (https://github.com/coredns/coredns/pull/7402) +* plugin/forward: Handle cached connection closure in forward plugin (https://github.com/coredns/coredns/pull/7427) +* plugin/grpc: Add support for fallthrough to the grpc plugin (https://github.com/coredns/coredns/pull/7359) +* plugin/kubernetes: Add startup_timeout for kubernetes plugin (https://github.com/coredns/coredns/pull/7068) +* plugin/kubernetes: Properly create hostname from IPv6 (https://github.com/coredns/coredns/pull/7431) +* plugin/rewrite: Add EDNS0 unset action (https://github.com/coredns/coredns/pull/7380) +* plugin/route53: Port to AWS Go SDK v2 (https://github.com/coredns/coredns/pull/6588) +* plugin/test: Fix TXT record comparison logic for multi-string vs multi-record scenarios (https://github.com/coredns/coredns/pull/7413) diff --git a/notes/coredns-1.12.4.md b/notes/coredns-1.12.4.md new file mode 100644 index 0000000000..70e08c7314 --- /dev/null +++ b/notes/coredns-1.12.4.md @@ -0,0 +1,39 @@ ++++ +title = "CoreDNS-1.12.4 Release" +description = "CoreDNS-1.12.4 Release Notes." +tags = ["Release", "1.12.4", "Notes"] +release = "1.12.4" +date = "2025-09-08T00:00:00+00:00" +author = "coredns" ++++ + +This release improves stability and security, fixing context propagation in DoH, label offset handling +in the file plugin, and connection leaks in gRPC and transfer. It also adds support for the prefer option +in loadbalance, introduces timeouts to the metrics server, and fixes several security vulnerabilities +(see details in related security advisories). + + +## Brought to You By + +Archy +Ilya Kulakov +Olli Janatuinen +Qasim Sarfraz +Syed Azeez +Ville Vesilehto +wencyu +Yong Tang + + +## Noteworthy Changes + +* core: Improve caddy.GracefulServer conformance checks (https://github.com/coredns/coredns/pull/7416) +* core: Propagate HTTP request context in DoH (https://github.com/coredns/coredns/pull/7491) +* plugin/file: Fix label offset problem in ClosestEncloser (https://github.com/coredns/coredns/pull/7465) +* plugin/grpc: Check proxy list length in policies (https://github.com/coredns/coredns/pull/7512) +* plugin/grpc: Fix span leak and deadline on error attempt (https://github.com/coredns/coredns/pull/7487) +* plugin/header: Remove deprecated syntax (https://github.com/coredns/coredns/pull/7436) +* plugin/loadbalance: Support prefer option (https://github.com/coredns/coredns/pull/7433) +* plugin/metrics: Add timeouts to metrics HTTP server (https://github.com/coredns/coredns/pull/7469) +* plugin/trace: Migrate dd-trace-go v1 to v2 (https://github.com/coredns/coredns/pull/7466) +* plugin/transfer: Fix goroutine leak on axfr err (https://github.com/coredns/coredns/pull/7516) diff --git a/notes/coredns-1.13.0.md b/notes/coredns-1.13.0.md new file mode 100644 index 0000000000..4b69c9dedc --- /dev/null +++ b/notes/coredns-1.13.0.md @@ -0,0 +1,37 @@ ++++ +title = "CoreDNS-1.13.0 Release" +description = "CoreDNS-1.13.0 Release Notes." +tags = ["Release", "1.13.0", "Notes"] +release = "1.13.0" +date = "2025-10-05T00:00:00+00:00" +author = "coredns" ++++ + +This release introduces a new Nomad plugin for integrating CoreDNS with HashiCorp Nomad. +It also fixes major Corefile issues on infinite loops and import cycles, improves shutdown +handling, normalizes core panics, addresses data races in the file plugin, enforces gRPC size +limits, adjusts forward failover behavior, as well as prevents reload deadlocks. + +## Brought to You By + +Fitz_dev +Ilya Kulakov +Olli Janatuinen +Ville Vesilehto +Yong Tang + +## Noteworthy Changes + +* core: Export timeout values in dnsserver.Server (https://github.com/coredns/coredns/pull/7497) +* core: Fix Corefile infinite loop on unclosed braces (https://github.com/coredns/coredns/pull/7571) +* core: Fix Corefile related import cycle issue (https://github.com/coredns/coredns/pull/7567) +* core: Normalize panics on invalid origins (https://github.com/coredns/coredns/pull/7563) +* core: Rely on dns.Server.ShutdownContext to gracefully stop (https://github.com/coredns/coredns/pull/7517) +* plugin/dnstap: Add bounds for plugin args (https://github.com/coredns/coredns/pull/7557) +* plugin/file: Fix data race in tree Elem.Name (https://github.com/coredns/coredns/pull/7574) +* plugin/forward: No failover to next upstream when receiving SERVFAIL or REFUSED response codes (https://github.com/coredns/coredns/pull/7458) +* plugin/grpc: Enforce DNS message size limits (https://github.com/coredns/coredns/pull/7490) +* plugin/loop: Prevent panic when ListenHosts is empty (https://github.com/coredns/coredns/pull/7565) +* plugin/loop: Avoid panic on invalid server block (https://github.com/coredns/coredns/pull/7568) +* plugin/nomad: Add a Nomad plugin (https://github.com/coredns/coredns/pull/7467) +* plugin/reload: Prevent SIGTERM/reload deadlock (https://github.com/coredns/coredns/pull/7562) diff --git a/notes/coredns-1.13.1.md b/notes/coredns-1.13.1.md new file mode 100644 index 0000000000..6e7407b85f --- /dev/null +++ b/notes/coredns-1.13.1.md @@ -0,0 +1,24 @@ ++++ +title = "CoreDNS-1.13.1 Release" +description = "CoreDNS-1.13.1 Release Notes." +tags = ["Release", "1.13.1", "Notes"] +release = "1.13.1" +date = "2025-10-08T00:00:00+00:00" +author = "coredns" ++++ + +This release updates CoreDNS to Go 1.25.2 and golang.org/x/net v0.45.0 to address multiple +high-severity CVEs. It also improves core performance by avoiding string concatenation in +loops, and hardens the sign plugin by rejecting invalid UTF-8 tokens in dbfile. + +## Brought to You By + +Catena cyber +Ville Vesilehto +Yong Tang + +## Noteworthy Changes + +* core: Avoid string concatenation in loops (https://github.com/coredns/coredns/pull/7572) +* core: Update golang to 1.25.2 and golang.org/x/net to v0.45.0 on CVE fixes (https://github.com/coredns/coredns/pull/7598) +* plugin/sign: Reject invalid UTF‑8 dbfile token (https://github.com/coredns/coredns/pull/7589) diff --git a/notes/coredns-1.8.0.md b/notes/coredns-1.8.0.md index ee5aa67ad8..071ce5f80b 100644 --- a/notes/coredns-1.8.0.md +++ b/notes/coredns-1.8.0.md @@ -14,7 +14,7 @@ If you are running 1.7.1 you want to upgrade for the *cache* plugin fixes. This release also adds three backwards incompatible changes. This will only affect you if you have an **external plugin** or use **outgoing zone transfers**. If you're using `dnstap` in your plugin, -you'll need to upgrade to the new API as detailed in it's [documentation](/plugin/dnstap). +you'll need to upgrade to the new API as detailed in it's [documentation](/plugins/dnstap). Two, because Caddy is now developing a version 2 and we are using version 1, we've internalized Caddy into . This means the `caddy` types change and *all* plugins diff --git a/notes/coredns-1.8.1.md b/notes/coredns-1.8.1.md index d02423bb24..4d80c0dbcd 100644 --- a/notes/coredns-1.8.1.md +++ b/notes/coredns-1.8.1.md @@ -13,7 +13,10 @@ The CoreDNS team has released This release fixes a bunch of bugs, and adds a (very) simple new plugin called [local](https://coredns.io/plugins/local/) to answer "local" queries. Bunch of work in the [kubernetes](https://coredns.io/plugins/kubernetes) plugin to add support for new upstream -feautres. +features. + +If using the [kubernetes](https://coredns.io/plugins/kubernetes) plugin for a Kubernetes +cluster >= 1.19, CoreDNS must be granted `list` and `watch` access to `endpointslices`. ## Brought to You By diff --git a/notes/coredns-1.8.4.md b/notes/coredns-1.8.4.md new file mode 100644 index 0000000000..462f8402fd --- /dev/null +++ b/notes/coredns-1.8.4.md @@ -0,0 +1,57 @@ ++++ +title = "CoreDNS-1.8.4 Release" +description = "CoreDNS-1.8.4 Release Notes." +tags = ["Release", "1.8.4", "Notes"] +release = "1.8.4" +date = 2021-05-28T07:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.8.4](https://github.com/coredns/coredns/releases/tag/v1.8.4). This release includes a +bunch of bugfixes and a few enhancements mostly in the *dnssec* and *kubernetes* plugins, and a new +(small) plugin called *minimal*. + +It also include a fix when using the "reverse zone cidr syntax", e.g. 10.0.0.0/15, now return the proper +set of reverse zones. + +## Brought to You By + +Chris O'Haver, +cuirunxing-hub, +Frank Riley, +Keith Coleman, +Miek Gieben, +milgradesec, +Mohammad Yosefpor, +ntoofu, +Paco Xu, +Soumya Ghosh Dastidar, +Steve Greene, +Théotime Lévêque, +Uwe Krueger, +wangchenglong01, +Yong Tang, +Yury Tsarev. + +## Noteworthy Changes + +* core: fix reverse zones expansion (https://github.com/coredns/coredns/pull/4538) +* plugins: fix Normalize (https://github.com/coredns/coredns/pull/4621) +* reverse zone: make Normalize return proper reverse zones (https://github.com/coredns/coredns/pull/4621) +* plugin/bind: Bind by interface name (https://github.com/coredns/coredns/pull/4522) +* plugin/bind: Exclude interface or ip address (https://github.com/coredns/coredns/pull/4543) +* plugin/dnssec: Check for two days of remaining validity (https://github.com/coredns/coredns/pull/4606) +* plugin/dnssec: interface type correction for `periodicClean` sig validity check (https://github.com/coredns/coredns/pull/4608) +* plugin/dnssec: use entire RRset as key input (https://github.com/coredns/coredns/pull/4537) +* plugin/etcd: Bump etcd to v3.5.0-beta.3 (https://github.com/coredns/coredns/pull/4638) +* plugin/forward: Add upstream metadata (https://github.com/coredns/coredns/pull/4521) +* plugin/health: add logging for local health request (https://github.com/coredns/coredns/pull/4533) +* plugin/kubernetes: consider nil ready as ready (https://github.com/coredns/coredns/pull/4632) +* plugin/kubernetes: do endpoint/slice check in retry loop (https://github.com/coredns/coredns/pull/4492) +* plugin/kubernetes: Exclude unready endpoints from endpointslices (https://github.com/coredns/coredns/pull/4580) +* plugin/metrics: remove RR type (https://github.com/coredns/coredns/pull/4534) +* plugin/minimal: Add minimal-responses plugin (https://github.com/coredns/coredns/pull/4417) +* plugin/rewrite: streamline the ResponseRule handling. (https://github.com/coredns/coredns/pull/4473) +* plugin/sign: Revert "plugin/sign: track zone file's mtime (https://github.com/coredns/coredns/pull/4431)" +* plugin/transfer: reply with refused (https://github.com/coredns/coredns/pull/4510) diff --git a/notes/coredns-1.8.5.md b/notes/coredns-1.8.5.md new file mode 100644 index 0000000000..03f2e755af --- /dev/null +++ b/notes/coredns-1.8.5.md @@ -0,0 +1,62 @@ ++++ +title = "CoreDNS-1.8.5 Release" +description = "CoreDNS-1.8.5 Release Notes." +tags = ["Release", "1.8.5", "Notes"] +release = "1.8.5" +date = 2021-09-10T07:00:00+00:00 +author = "coredns" ++++ + +This is a rather big release, we now [share plugins among zones in the same server +block](https://github.com/coredns/coredns/pull/4593), which should save memory. Various bug fixes in +a bunch of plugins and not one, but two new plugins. A *geoip* plugin that can report **where** the +query came from and a *header* plugin that allows you to fiddle with (some of) the header bits in a +DNS message. + +With this release, the `coredns_cache_misses_total` metric is deprecated. It will be removed in a later release. +Users should migrate their promQL to use `coredns_cache_requests_total - coredns_cache_hits_total`. + +## Brought to You By + +Ben Kochie, +Chris O'Haver, +Jeongwook Park, +Kohei Yoshida, +Licht Takeuchi, +Manuel Rüger, +Mat Lowery, +mfleader, +Miek Gieben, +Ondřej Benkovský, +Qasim Sarfraz, +rouzier, +Sascha Grunert, +Sven Nebel, +Yong Tang. + +## Noteworthy Changes + +* core: Add -p for port flag (https://github.com/coredns/coredns/pull/4653) +* core: Fix IPv6 case for CIDR format reverse zones (https://github.com/coredns/coredns/pull/4652) +* core: Share plugins among zones in the same server block (https://github.com/coredns/coredns/pull/4593) +* core: Upstream lookups are done with original EDNS options (https://github.com/coredns/coredns/pull/4826) +* plugin/cache: Unset AD flag when DO is not set for cache miss (https://github.com/coredns/coredns/pull/4736) +* plugin/cache: Update cache metrics and add a total cache request counter to follow Prometheus convention (https://github.com/coredns/coredns/pull/4781) +* plugin/errors: Add configurable log level to errors plugin (https://github.com/coredns/coredns/pull/4718) +* plugin/file: fix wildcard CNAME answer (https://github.com/coredns/coredns/pull/4828) +* plugin/forward: Add proxy address as tag (https://github.com/coredns/coredns/pull/4757) +* plugin/geoip: Create geoip plugin (https://github.com/coredns/coredns/pull/4688) +* plugin/header: Introduce header plugin (https://github.com/coredns/coredns/pull/4752) +* plugin/kubernetes: Add NS+hosts records to xfr response. Add coredns service to test data. (https://github.com/coredns/coredns/pull/4696) +* plugin/kubernetes: Improve namespace usage (https://github.com/coredns/coredns/pull/4767) +* plugins/kubernetes: Switch to klog/v2 (https://github.com/coredns/coredns/pull/4778) +* plugin/kubernetes: Only answer transfer requests for authoritative zones (https://github.com/coredns/coredns/pull/4802) +* plugin/log: Do not log NOERROR in log plugin when response is not available (https://github.com/coredns/coredns/pull/4725) +* plugin/log: Fix closing of codeblock (https://github.com/coredns/coredns/pull/4680) +* plugin/metrics: When no response is written, fallback to status of next plugin in prometheus plugin (https://github.com/coredns/coredns/pull/4727) +* plugin/route53: Fix Route53 plugin cannot retrieve ECS Task Role (https://github.com/coredns/coredns/pull/4669) +* plugin/secondary: Doc updates (https://github.com/coredns/coredns/pull/4686) +* plugin/secondary: Retry initial transfer until successful (https://github.com/coredns/coredns/pull/4663) +* plugin/trace: Fix rcode tag in case of no response (https://github.com/coredns/coredns/pull/4742) +* plugin/trace: Publish trace id as metadata from trace plugin (https://github.com/coredns/coredns/pull/4749) +* plugin/trace: Trace plugin can mark traces with error tag (https://github.com/coredns/coredns/pull/4720) diff --git a/notes/coredns-1.8.6.md b/notes/coredns-1.8.6.md new file mode 100644 index 0000000000..aaf6f59dcd --- /dev/null +++ b/notes/coredns-1.8.6.md @@ -0,0 +1,20 @@ ++++ +title = "CoreDNS-1.8.6 Release" +description = "CoreDNS-1.8.6 Release Notes." +tags = ["Release", "1.8.6", "Notes"] +release = "1.8.6" +date = "2021-10-07T00:00:00+00:00" +author = "coredns" ++++ + +This is a small bug fix release. + +## Brought to You By + +Chris O'Haver, +Miek Gieben. + +## Noteworthy Changes + +* plugin/kubernetes: fix reload panic (https://github.com/coredns/coredns/pull/4881) +* plugin/kubernetes: Don't use pod names longer than 63 characters as dns labels (https://github.com/coredns/coredns/pull/4908) diff --git a/notes/coredns-1.8.7.md b/notes/coredns-1.8.7.md new file mode 100644 index 0000000000..f9e99edc3f --- /dev/null +++ b/notes/coredns-1.8.7.md @@ -0,0 +1,58 @@ ++++ +title = "CoreDNS-1.8.7 Release" +description = "CoreDNS-1.8.7 Release Notes." +tags = ["Release", "1.8.7", "Notes"] +release = "1.8.7" +date = "2021-12-09T00:00:00+00:00" +author = "coredns" ++++ + +This is a release with bug fixes and some new features added. We now enable HTTP/2 in +gRPC service (https://github.com/coredns/coredns/pull/4842). The shuffling algorithm +in loadbalance plugin has also been improved to have a more consistent +behavior (https://github.com/coredns/coredns/pull/4961). This release will also +log deprecation warnings when wildcard queries are received by kubernetes. The +wildcard functionality will be completely removed from kubernetes plugin in +future releases. + + +## Brought to You By + +Chris O'Haver, +Christian Ang, +Cyb3r Jak3, +Denis Tingaikin, +gomakesix, +Hu Shuai, +Humberto Leal, +jayonlau, +Johnny Bergström, +LiuCongran, +Matt Palmer, +Miek Gieben, +OctoHuman, +Ondřej Benkovský, +Pavol Lieskovský, +Vector, +Wu Shuang, +xuweiwei, +xww, +Yong Tang, +ZhangJian He, +Zou Nengren + +## Noteworthy Changes + +* core: Support plain HTTP for DoH (https://github.com/coredns/coredns/pull/4997) +* plugin/auto: Fix panic caused by config invalid reload value (https://github.com/coredns/coredns/pull/4986) +* plugin/cache: fix data race (https://github.com/coredns/coredns/pull/4932) +* plugin/file: Fix print tree error (https://github.com/coredns/coredns/pull/4962) +* plugin/file: Fix issue of multiple file plugin have same reload time (https://github.com/coredns/coredns/pull/5020) +* plugin/forward: Use new msg.Id for upstream queries (https://github.com/coredns/coredns/pull/4841) +* plugin/grpc: Enable HTTP/2 in gRPC service (https://github.com/coredns/coredns/pull/4842) +* plugin/k8s_external: Fix SRV queries doesn't work with AWS ELB/NLB (https://github.com/coredns/coredns/pull/4929) +* plugin/kubernetes: Add wildcard warnings (https://github.com/coredns/coredns/pull/5030) +* plugin/loadbalance: More consistent shuffling (https://github.com/coredns/coredns/pull/4961) +* plugin/metrics: Support HTTPS qType in requests count metric label (https://github.com/coredns/coredns/pull/4934) +* plugin/metrics: Expand coredns_dns_responses_total with plugin label (https://github.com/coredns/coredns/pull/4914) +* plugin/route53: Configurable AWS Endpoint (https://github.com/coredns/coredns/pull/4963) diff --git a/notes/coredns-1.9.0.md b/notes/coredns-1.9.0.md new file mode 100644 index 0000000000..4fb023a912 --- /dev/null +++ b/notes/coredns-1.9.0.md @@ -0,0 +1,28 @@ ++++ +title = "CoreDNS-1.9.0 Release" +description = "CoreDNS-1.9.0 Release Notes." +tags = ["Release", "1.9.0", "Notes"] +release = "1.9.0" +date = "2022-02-01T00:00:00+00:00" +author = "coredns" ++++ + +This is a release with bug fixes and some new features added. Starting with 1.9.0 +the minimal required go version will be 1.17. +Wildcard queries are no longer supported by the _kubernetes_ plugin. + + +## Brought to You By + +Chris O'Haver, +Ondřej Benkovský, +Tomas Hulata, +Yong Tang, +xuweiwei + +## Noteworthy Changes + +* plugin/kubernetes: remove wildcard query functionality (https://github.com/coredns/coredns/pull/5019) +* Health-checks should respect force_tcp (https://github.com/coredns/coredns/pull/5109) +* plugin/prometheus: Write rcode properly to the metrics (https://github.com/coredns/coredns/pull/5126) +* plugin/template: Persist truncated state to client if CNAME lookup response is truncated (https://github.com/coredns/coredns/pull/4713) diff --git a/notes/coredns-1.9.1.md b/notes/coredns-1.9.1.md new file mode 100644 index 0000000000..791285727f --- /dev/null +++ b/notes/coredns-1.9.1.md @@ -0,0 +1,38 @@ ++++ +title = "CoreDNS-1.9.1 Release" +description = "CoreDNS-1.9.1 Release Notes." +tags = ["Release", "1.9.1", "Notes"] +release = "1.9.1" +date = "2022-03-09T00:00:00+00:00" +author = "coredns" ++++ + +This is a release with security and bug fixes and some new features added. 1.9.1 is also built +with golang 1.17.8 that addressed several golang 1.17.6 vulnerabilities (CVE-2022-23772, +CVE-2022-23773, CVE-2022-23806). +Note golang 1.17.6 was used to built coredns 1.9.0. + +## Brought to You By + +Chris O'Haver, +Elijah Andrews, +Rudolf Schönecker, +Yong Tang, +nathannaveen, +xuweiwei + +## Noteworthy Changes + +* plugin/autopath: Don't panic on empty token (https://github.com/coredns/coredns/pull/5169) +* plugin/cache: Add zones label to cache metrics (https://github.com/coredns/coredns/pull/5124) +* plugin/file: Add TXT test case (https://github.com/coredns/coredns/pull/5079) +* plugin/forward: Don't panic when from-zone cannot be normalized (https://github.com/coredns/coredns/pull/5170) +* plugin/grpc: Fix healthy proxy error case (https://github.com/coredns/coredns/pull/5168) +* plugin/grpc: Don't panic when from-zone cannot be normalized (https://github.com/coredns/coredns/pull/5171) +* plugin/k8s_external: Implement zone transfers (https://github.com/coredns/coredns/pull/4977) +* plugin/k8s_external: Fix external nsAddrs when CoreDNS Service has no External IPs (https://github.com/coredns/coredns/pull/4891) +* plugin/kubernetes: Log api connection failures and server start delay (https://github.com/coredns/coredns/pull/5044) +* plugin/log: Expand `{combined}` and `{common}` in log format (https://github.com/coredns/coredns/pull/5230) +* plugin/metrics: Add metric counting DNS-over-HTTPS responses (https://github.com/coredns/coredns/pull/5130) +* plugin/reload: Change hash from md5 to sha512 (https://github.com/coredns/coredns/pull/5226) +* plugin/secondary: Fix startup transfer failure wrong zone logged (https://github.com/coredns/coredns/pull/5085) diff --git a/notes/coredns-1.9.2.md b/notes/coredns-1.9.2.md new file mode 100644 index 0000000000..3feba4bdca --- /dev/null +++ b/notes/coredns-1.9.2.md @@ -0,0 +1,46 @@ ++++ +title = "CoreDNS-1.9.2 Release" +description = "CoreDNS-1.9.2 Release Notes." +tags = ["Release", "1.9.2", "Notes"] +release = "1.9.2" +date = "2022-05-13T00:00:00+00:00" +author = "coredns" ++++ + +This is a release with many added features and security and bug fixes. The most notable one is the +release of 3rd party security audit from Trail of Bits. Security issues discovered by this audit +have all been fixed or covered. + +## Brought to You By + +Antoine Tollenaere, +Balazs Nagy, +Chris O'Haver, +dilyevsky, +hansedong, +Lorenz Brun, +Marius Kimmina, +nathannaveen, +Ondřej Benkovský, +Patrick W. Healy, +Qasim Sarfraz, +xuweiwei, +Yong Tang + +## Noteworthy Changes + +* core: add Trail of Bits to list of 3rd party security auditors (https://github.com/coredns/coredns/pull/5356) +* core: avoid usage of pseudo-random number (https://github.com/coredns/coredns/pull/5228) +* plugin/bufsize: don't add OPT RR to non-EDNS0 queries (https://github.com/coredns/coredns/pull/5368) +* plugin/cache: add refresh mode setting to serve_stale (https://github.com/coredns/coredns/pull/5131) +* plugin/cache: fix cache poisoning exploit (https://github.com/coredns/coredns/pull/5174) +* plugin/etcd: fix multi record TXT lookups (https://github.com/coredns/coredns/pull/5293) +* plugin/forward: configurable domain support for healthcheck (https://github.com/coredns/coredns/pull/5281) +* plugin/geoip: read source IP from EDNS0 subnet if provided (https://github.com/coredns/coredns/pull/5183) +* plugin/health: rework overloaded goroutine to support graceful shutdown (https://github.com/coredns/coredns/pull/5244) +* plugin/k8s_external: persist tc bit from lookup to client response (https://github.com/coredns/coredns/pull/4716) +* plugin/k8s_external: set authoritative bit in responses (https://github.com/coredns/coredns/pull/5284) +* plugin/kubernetes: fix k8s start up timeout ticker (https://github.com/coredns/coredns/pull/5361) +* plugin/route53: deprecate plaintext secret in Corefile for route53 plugin (https://github.com/coredns/coredns/pull/5228) +* plugin/route53: expand AWS config/credentials setup. (https://github.com/coredns/coredns/pull/5370) +* plugin/template: fix rcode option documentation (https://github.com/coredns/coredns/pull/5328) diff --git a/notes/coredns-1.9.3.md b/notes/coredns-1.9.3.md new file mode 100644 index 0000000000..2601532765 --- /dev/null +++ b/notes/coredns-1.9.3.md @@ -0,0 +1,31 @@ ++++ +title = "CoreDNS-1.9.3 Release" +description = "CoreDNS-1.9.3 Release Notes." +tags = ["Release", "1.9.3", "Notes"] +release = "1.9.3" +date = "2022-05-27T00:00:00+00:00" +author = "coredns" ++++ + +This is a release with a focus on security (CVE-2022-27191 and CVE-2022-28948) fixes. Additionally, +several feature enhancements and bug fixes have been added. + +## Brought to You By + +Chris O'Haver, +lobshunter, +Naveen, +Radim Hatlapatka, +RetoHaslerMGB, +Tintin, +Yong Tang + + +## Noteworthy Changes + +* core: update gopkg.in/yaml.v3 to fix CVE-2022-28948 (https://github.com/coredns/coredns/pull/5408) +* core: update golang.org/x/crypto to fix CVE-2022-27191 (https://github.com/coredns/coredns/pull/5407) +* plugin/acl: adding a check to parse out zone info (https://github.com/coredns/coredns/pull/5387) +* plugin/dnstap: support FQDN TCP endpoint (https://github.com/coredns/coredns/pull/5377) +* plugin/errors: add `stacktrace` option to log a stacktrace during panic recovery (https://github.com/coredns/coredns/pull/5392) +* plugin/template: return SERVFAIL for zone-match regex-no-match case (https://github.com/coredns/coredns/pull/5180) diff --git a/notes/coredns-1.9.4.md b/notes/coredns-1.9.4.md new file mode 100644 index 0000000000..3acafd0814 --- /dev/null +++ b/notes/coredns-1.9.4.md @@ -0,0 +1,63 @@ ++++ +title = "CoreDNS-1.9.4 Release" +description = "CoreDNS-1.9.4 Release Notes." +tags = ["Release", "1.9.4", "Notes"] +release = "1.9.4" +date = "2022-09-07T00:00:00+00:00" +author = "coredns" ++++ + +This is a release with many new features. The most notable addition is a new plugin tsig for validating +TSIG requests and signing responses. In header plugin a selector of `query` or `response` (default) is added for +applying the actions. This release also adds lots of enhancements and bug fixes. + +## Brought to You By + +Abirdcfly +Alex +AndreasHuber-CH +Andy Lindeman +Chris Narkiewicz +Chris O'Haver +Christoph Heer +Daniel Jolly +Konstantin Demin +Marius Kimmina +Md Sahil +Ondřej Benkovský +Shane Xie +TomasKohout +Vancl +Yong Tang + + +## Noteworthy Changes + +* core: add log listeners for k8s_event plugin (https://github.com/coredns/coredns/pull/5451) +* core: log DoH HTTP server error logs in CoreDNS format (https://github.com/coredns/coredns/pull/5457) +* core: warn when domain names are not in RFC1035 preferred syntax (https://github.com/coredns/coredns/pull/5414) +* plugin/acl: add support for extended DNS errors (https://github.com/coredns/coredns/pull/5532) +* plugin/bufsize: do not expand query UDP buffer size if already set to a smaller value (https://github.com/coredns/coredns/pull/5602) +* plugin/cache: add cache disable option (https://github.com/coredns/coredns/pull/5540) +* plugin/cache: add metadata for wildcard record responses (https://github.com/coredns/coredns/pull/5308) +* plugin/cache: add option to adjust SERVFAIL response cache TTL (https://github.com/coredns/coredns/pull/5320) +* plugin/cache: correct responses to Authenticated Data requests (https://github.com/coredns/coredns/pull/5191) +* plugin/dnstap: add identity and version support for the dnstap plugin (https://github.com/coredns/coredns/pull/5555) +* plugin/file: add metadata for wildcard record responses (https://github.com/coredns/coredns/pull/5308) +* plugin/forward: enable multiple forward declarations (https://github.com/coredns/coredns/pull/5127) +* plugin/forward: health_check needs to normalize a specified domain name (https://github.com/coredns/coredns/pull/5543) +* plugin/forward: remove unused coredns_forward_sockets_open metric (https://github.com/coredns/coredns/pull/5431) +* plugin/header: add support for query modification (https://github.com/coredns/coredns/pull/5556) +* plugin/health: bypass proxy in self health check (https://github.com/coredns/coredns/pull/5401) +* plugin/health: don't go lameduck when reloading (https://github.com/coredns/coredns/pull/5472) +* plugin/k8s_external: add support for PTR requests (https://github.com/coredns/coredns/pull/5435) +* plugin/k8s_external: resolve headless services (https://github.com/coredns/coredns/pull/5505) +* plugin/kubernetes: make kubernetes client log in CoreDNS format (https://github.com/coredns/coredns/pull/5461) +* plugin/ready: reset list of readiness plugins on startup (https://github.com/coredns/coredns/pull/5492) +* plugin/rewrite: add PTR records to supported types (https://github.com/coredns/coredns/pull/5565) +* plugin/rewrite: fix a crash in rewrite plugin when rule type is missing (https://github.com/coredns/coredns/pull/5459) +* plugin/rewrite: fix out-of-index issue in rewrite plugin (https://github.com/coredns/coredns/pull/5462) +* plugin/rewrite: support min and max TTL values (https://github.com/coredns/coredns/pull/5508) +* plugin/trace : make zipkin HTTP reporter more configurable using Corefile (https://github.com/coredns/coredns/pull/5460) +* plugin/trace: read trace context info from headers for DOH (https://github.com/coredns/coredns/pull/5439) +* plugin/tsig: add new plugin TSIG for validating TSIG requests and signing responses (https://github.com/coredns/coredns/pull/4957) diff --git a/owners_generate.go b/owners_generate.go index cc62082950..ebae010387 100644 --- a/owners_generate.go +++ b/owners_generate.go @@ -1,4 +1,4 @@ -//+build ignore +//go:build ignore // generates plugin/chaos/zowners.go. @@ -7,7 +7,6 @@ package main import ( "bufio" "fmt" - "io/ioutil" "log" "os" "sort" @@ -35,7 +34,7 @@ var Owners = []string{` // to prevent `No newline at end of file` with gofmt golist += "\n" - if err := ioutil.WriteFile("plugin/chaos/zowners.go", []byte(golist), 0644); err != nil { + if err := os.WriteFile("plugin/chaos/zowners.go", []byte(golist), 0644); err != nil { log.Fatal(err) } return diff --git a/pb/Makefile b/pb/Makefile index a666d6c158..7e8cdaf268 100644 --- a/pb/Makefile +++ b/pb/Makefile @@ -2,11 +2,18 @@ # from: https://github.com/golang/protobuf to make this work. # The generate dns.pb.go is checked into git, so for normal builds we don't need # to run this generation step. +# Note: The following has been used when regenerate pb: +# curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-x86_64.zip +# go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1 +# go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0 +# export PATH="$PATH:$(go env GOPATH)/bin" +# rm pb/dns.pb.go pb/dns_grpc.pb.go +# make pb all: dns.pb.go dns.pb.go: dns.proto - protoc --go_out=plugins=grpc:. dns.proto + protoc --go_out=. --go-grpc_out=. dns.proto .PHONY: clean clean: diff --git a/pb/dns.pb.go b/pb/dns.pb.go index 919f0e7d7e..e2a311cc1b 100644 --- a/pb/dns.pb.go +++ b/pb/dns.pb.go @@ -1,156 +1,147 @@ // Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.19.4 // source: dns.proto package pb import ( - context "context" - fmt "fmt" - math "math" - - proto "github.com/golang/protobuf/proto" - grpc "google.golang.org/grpc" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -/* Miek: disabled this manually, because I don't know what the heck */ -/* -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package -*/ +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) type DnsPacket struct { - Msg []byte `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields -func (m *DnsPacket) Reset() { *m = DnsPacket{} } -func (m *DnsPacket) String() string { return proto.CompactTextString(m) } -func (*DnsPacket) ProtoMessage() {} -func (*DnsPacket) Descriptor() ([]byte, []int) { - return fileDescriptor_638ff8d8aaf3d8ae, []int{0} + Msg []byte `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` } -func (m *DnsPacket) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_DnsPacket.Unmarshal(m, b) -} -func (m *DnsPacket) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_DnsPacket.Marshal(b, m, deterministic) -} -func (m *DnsPacket) XXX_Merge(src proto.Message) { - xxx_messageInfo_DnsPacket.Merge(m, src) -} -func (m *DnsPacket) XXX_Size() int { - return xxx_messageInfo_DnsPacket.Size(m) +func (x *DnsPacket) Reset() { + *x = DnsPacket{} + if protoimpl.UnsafeEnabled { + mi := &file_dns_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } -func (m *DnsPacket) XXX_DiscardUnknown() { - xxx_messageInfo_DnsPacket.DiscardUnknown(m) + +func (x *DnsPacket) String() string { + return protoimpl.X.MessageStringOf(x) } -var xxx_messageInfo_DnsPacket proto.InternalMessageInfo +func (*DnsPacket) ProtoMessage() {} -func (m *DnsPacket) GetMsg() []byte { - if m != nil { - return m.Msg +func (x *DnsPacket) ProtoReflect() protoreflect.Message { + mi := &file_dns_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return nil + return mi.MessageOf(x) } -func init() { - proto.RegisterType((*DnsPacket)(nil), "coredns.dns.DnsPacket") +// Deprecated: Use DnsPacket.ProtoReflect.Descriptor instead. +func (*DnsPacket) Descriptor() ([]byte, []int) { + return file_dns_proto_rawDescGZIP(), []int{0} } -func init() { proto.RegisterFile("dns.proto", fileDescriptor_638ff8d8aaf3d8ae) } - -var fileDescriptor_638ff8d8aaf3d8ae = []byte{ - // 120 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4c, 0xc9, 0x2b, 0xd6, - 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x4e, 0xce, 0x2f, 0x4a, 0x05, 0x71, 0x53, 0xf2, 0x8a, - 0x95, 0x64, 0xb9, 0x38, 0x5d, 0xf2, 0x8a, 0x03, 0x12, 0x93, 0xb3, 0x53, 0x4b, 0x84, 0x04, 0xb8, - 0x98, 0x73, 0x8b, 0xd3, 0x25, 0x18, 0x15, 0x18, 0x35, 0x78, 0x82, 0x40, 0x4c, 0x23, 0x57, 0x2e, - 0x2e, 0x97, 0xbc, 0xe2, 0xe0, 0xd4, 0xa2, 0xb2, 0xcc, 0xe4, 0x54, 0x21, 0x73, 0x2e, 0xd6, 0xc0, - 0xd2, 0xd4, 0xa2, 0x4a, 0x21, 0x31, 0x3d, 0x24, 0x33, 0xf4, 0xe0, 0x06, 0x48, 0xe1, 0x10, 0x77, - 0x62, 0x89, 0x62, 0x2a, 0x48, 0x4a, 0x62, 0x03, 0xdb, 0x6f, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, - 0xf5, 0xd1, 0x3f, 0x26, 0x8c, 0x00, 0x00, 0x00, +func (x *DnsPacket) GetMsg() []byte { + if x != nil { + return x.Msg + } + return nil } -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// DnsServiceClient is the client API for DnsService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. -type DnsServiceClient interface { - Query(ctx context.Context, in *DnsPacket, opts ...grpc.CallOption) (*DnsPacket, error) -} +var File_dns_proto protoreflect.FileDescriptor -type dnsServiceClient struct { - cc *grpc.ClientConn +var file_dns_proto_rawDesc = []byte{ + 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x63, 0x6f, 0x72, + 0x65, 0x64, 0x6e, 0x73, 0x2e, 0x64, 0x6e, 0x73, 0x22, 0x1d, 0x0a, 0x09, 0x44, 0x6e, 0x73, 0x50, + 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x32, 0x45, 0x0a, 0x0a, 0x44, 0x6e, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x16, + 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x64, 0x6e, 0x73, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6e, 0x73, + 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x1a, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x64, 0x6e, 0x73, + 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6e, 0x73, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x42, 0x06, + 0x5a, 0x04, 0x2e, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } -func NewDnsServiceClient(cc *grpc.ClientConn) DnsServiceClient { - return &dnsServiceClient{cc} -} +var ( + file_dns_proto_rawDescOnce sync.Once + file_dns_proto_rawDescData = file_dns_proto_rawDesc +) -func (c *dnsServiceClient) Query(ctx context.Context, in *DnsPacket, opts ...grpc.CallOption) (*DnsPacket, error) { - out := new(DnsPacket) - err := c.cc.Invoke(ctx, "/coredns.dns.DnsService/Query", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil +func file_dns_proto_rawDescGZIP() []byte { + file_dns_proto_rawDescOnce.Do(func() { + file_dns_proto_rawDescData = protoimpl.X.CompressGZIP(file_dns_proto_rawDescData) + }) + return file_dns_proto_rawDescData } -// DnsServiceServer is the server API for DnsService service. -type DnsServiceServer interface { - Query(context.Context, *DnsPacket) (*DnsPacket, error) +var file_dns_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_dns_proto_goTypes = []interface{}{ + (*DnsPacket)(nil), // 0: coredns.dns.DnsPacket } - -func RegisterDnsServiceServer(s *grpc.Server, srv DnsServiceServer) { - s.RegisterService(&_DnsService_serviceDesc, srv) +var file_dns_proto_depIdxs = []int32{ + 0, // 0: coredns.dns.DnsService.Query:input_type -> coredns.dns.DnsPacket + 0, // 1: coredns.dns.DnsService.Query:output_type -> coredns.dns.DnsPacket + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name } -func _DnsService_Query_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DnsPacket) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DnsServiceServer).Query(ctx, in) +func init() { file_dns_proto_init() } +func file_dns_proto_init() { + if File_dns_proto != nil { + return } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/coredns.dns.DnsService/Query", + if !protoimpl.UnsafeEnabled { + file_dns_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DnsPacket); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DnsServiceServer).Query(ctx, req.(*DnsPacket)) - } - return interceptor(ctx, in, info, handler) -} - -var _DnsService_serviceDesc = grpc.ServiceDesc{ - ServiceName: "coredns.dns.DnsService", - HandlerType: (*DnsServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Query", - Handler: _DnsService_Query_Handler, + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_dns_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 1, }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "dns.proto", + GoTypes: file_dns_proto_goTypes, + DependencyIndexes: file_dns_proto_depIdxs, + MessageInfos: file_dns_proto_msgTypes, + }.Build() + File_dns_proto = out.File + file_dns_proto_rawDesc = nil + file_dns_proto_goTypes = nil + file_dns_proto_depIdxs = nil } diff --git a/pb/dns.proto b/pb/dns.proto index 8461f01e6d..ee24cb0bb0 100644 --- a/pb/dns.proto +++ b/pb/dns.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package coredns.dns; -option go_package = "pb"; +option go_package = ".;pb"; message DnsPacket { bytes msg = 1; diff --git a/pb/dns_grpc.pb.go b/pb/dns_grpc.pb.go new file mode 100644 index 0000000000..6ff3faf11b --- /dev/null +++ b/pb/dns_grpc.pb.go @@ -0,0 +1,105 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.19.4 +// source: dns.proto + +package pb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// DnsServiceClient is the client API for DnsService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type DnsServiceClient interface { + Query(ctx context.Context, in *DnsPacket, opts ...grpc.CallOption) (*DnsPacket, error) +} + +type dnsServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewDnsServiceClient(cc grpc.ClientConnInterface) DnsServiceClient { + return &dnsServiceClient{cc} +} + +func (c *dnsServiceClient) Query(ctx context.Context, in *DnsPacket, opts ...grpc.CallOption) (*DnsPacket, error) { + out := new(DnsPacket) + err := c.cc.Invoke(ctx, "/coredns.dns.DnsService/Query", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DnsServiceServer is the server API for DnsService service. +// All implementations must embed UnimplementedDnsServiceServer +// for forward compatibility +type DnsServiceServer interface { + Query(context.Context, *DnsPacket) (*DnsPacket, error) + mustEmbedUnimplementedDnsServiceServer() +} + +// UnimplementedDnsServiceServer must be embedded to have forward compatible implementations. +type UnimplementedDnsServiceServer struct { +} + +func (UnimplementedDnsServiceServer) Query(context.Context, *DnsPacket) (*DnsPacket, error) { + return nil, status.Errorf(codes.Unimplemented, "method Query not implemented") +} +func (UnimplementedDnsServiceServer) mustEmbedUnimplementedDnsServiceServer() {} + +// UnsafeDnsServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DnsServiceServer will +// result in compilation errors. +type UnsafeDnsServiceServer interface { + mustEmbedUnimplementedDnsServiceServer() +} + +func RegisterDnsServiceServer(s grpc.ServiceRegistrar, srv DnsServiceServer) { + s.RegisterService(&DnsService_ServiceDesc, srv) +} + +func _DnsService_Query_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DnsPacket) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DnsServiceServer).Query(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/coredns.dns.DnsService/Query", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DnsServiceServer).Query(ctx, req.(*DnsPacket)) + } + return interceptor(ctx, in, info, handler) +} + +// DnsService_ServiceDesc is the grpc.ServiceDesc for DnsService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DnsService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "coredns.dns.DnsService", + HandlerType: (*DnsServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Query", + Handler: _DnsService_Query_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "dns.proto", +} diff --git a/plugin.cfg b/plugin.cfg index 58f1767936..b4d3bae033 100644 --- a/plugin.cfg +++ b/plugin.cfg @@ -19,13 +19,17 @@ # Local plugin example: # log:log +root:root metadata:metadata +geoip:geoip cancel:cancel tls:tls +quic:quic +timeouts:timeouts +multisocket:multisocket reload:reload nsid:nsid bufsize:bufsize -root:root bind:bind debug:debug trace:trace @@ -42,8 +46,10 @@ acl:acl any:any chaos:chaos loadbalance:loadbalance +tsig:tsig cache:cache rewrite:rewrite +header:header dnssec:dnssec autopath:autopath minimal:minimal @@ -66,3 +72,5 @@ erratic:erratic whoami:whoami on:github.com/coredns/caddy/onevent sign:sign +view:view +nomad:nomad diff --git a/plugin.md b/plugin.md index 1973729f2a..b1c68151f0 100644 --- a/plugin.md +++ b/plugin.md @@ -30,8 +30,8 @@ code. See a couple of blog posts on how to write and add plugin to CoreDNS: -* -* , slightly older, but useful. +* +* , slightly older, but useful. ## Logging @@ -71,12 +71,22 @@ your plugin handle reload events better. ## Context -Every request get a context.Context these are pre-filled with 2 values: +Every request gets a `context.Context` with values that provide information about the request and server state. -* `Key`: holds a pointer to the current server, this can be useful for logging or metrics. It is - infact used in the *metrics* plugin to tie a request to a specific (internal) server. -* `LoopKey`: holds an integer to detect loops within the current context. The *file* plugin uses - this to detect loops when resolving CNAMEs. +### Core Context Values + +These values are available for all DNS requests: + +* `Key`: holds a pointer to the current server, useful for logging or metrics. Used by the *metrics* plugin to tie requests to specific (internal) server. +* `LoopKey`: holds an integer to detect loops within the current context. Used by the *file* plugin when resolving CNAMEs. + +### Transport-Specific Context Values + +Depending on the DNS transport protocol, additional context values may be available: + +* **DNS-over-HTTPS**: `HTTPRequestKey` contains the original `*http.Request`, providing access to HTTP headers, client information, and request metadata. +* **DNS-over-gRPC**: Standard gRPC context values are available, including peer information via `peer.FromContext()` and metadata via `metadata.FromIncomingContext()`. +* **DNS-over-QUIC**: QUIC stream context is propagated, including timeouts and cancellation signals. ## Documentation diff --git a/plugin/acl/README.md b/plugin/acl/README.md index 5103018ef0..d957d24ec9 100644 --- a/plugin/acl/README.md +++ b/plugin/acl/README.md @@ -6,7 +6,13 @@ ## Description -With `acl` enabled, users are able to block or filter suspicious DNS queries by configuring IP filter rule sets, i.e. allowing authorized queries to recurse or blocking unauthorized queries. +With `acl` enabled, users are able to block or filter suspicious DNS queries by configuring IP filter rule sets, i.e. allowing authorized queries or blocking unauthorized queries. + + +When evaluating the rule sets, _acl_ uses the source IP of the TCP/UDP headers of the DNS query received by CoreDNS. +This source IP will be different than the IP of the client originating the request in cases where the source IP of the request is changed in transit. For example: +* if the request passes though an intermediate forwarding DNS server or recursive DNS server before reaching CoreDNS +* if the request traverses a Source NAT before reaching CoreDNS This plugin can be used multiple times per Server Block. @@ -19,7 +25,7 @@ acl [ZONES...] { ``` - **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block are used. -- **ACTION** (*allow*, *block*, or *filter*) defines the way to deal with DNS queries matched by this rule. The default action is *allow*, which means a DNS query not matched by any rules will be allowed to recurse. The difference between *block* and *filter* is that block returns status code of *REFUSED* while filter returns an empty set *NOERROR* +- **ACTION** (*allow*, *block*, *filter*, or *drop*) defines the way to deal with DNS queries matched by this rule. The default action is *allow*, which means a DNS query not matched by any rules will be allowed to recurse. The difference between *block* and *filter* is that block returns status code of *REFUSED* while filter returns an empty set *NOERROR*. *drop* however returns no response to the client. - **QTYPE** is the query type to match for the requests to be allowed or blocked. Common resource record types are supported. `*` stands for all record types. The default behavior for an omitted `type QTYPE...` is to match all kinds of DNS queries (same as `type *`). - **SOURCE** is the source IP address to match for the requests to be allowed or blocked. Typical CIDR notation and single IP address are supported. `*` stands for all possible source IP addresses. @@ -79,12 +85,26 @@ example.org { } ~~~ +Drop all DNS queries from 192.0.2.0/24: + +~~~ corefile +. { + acl { + drop net 192.0.2.0/24 + } +} +~~~ + ## Metrics If monitoring is enabled (via the _prometheus_ plugin) then the following metrics are exported: -- `coredns_acl_blocked_requests_total{server, zone}` - counter of DNS requests being blocked. +- `coredns_acl_blocked_requests_total{server, zone, view}` - counter of DNS requests being blocked. + +- `coredns_acl_filtered_requests_total{server, zone, view}` - counter of DNS requests being filtered. + +- `coredns_acl_allowed_requests_total{server, view}` - counter of DNS requests being allowed. -- `coredns_acl_allowed_requests_total{server}` - counter of DNS requests being allowed. +- `coredns_acl_dropped_requests_total{server, zone, view}` - counter of DNS requests being dropped. The `server` and `zone` labels are explained in the _metrics_ plugin documentation. diff --git a/plugin/acl/acl.go b/plugin/acl/acl.go index e684dc42c4..2632326325 100644 --- a/plugin/acl/acl.go +++ b/plugin/acl/acl.go @@ -3,9 +3,11 @@ package acl import ( "context" "net" + "strings" "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/metrics" + clog "github.com/coredns/coredns/plugin/pkg/log" "github.com/coredns/coredns/request" "github.com/infobloxopen/go-trees/iptree" @@ -47,8 +49,12 @@ const ( actionBlock // actionFilter returns empty sets for queries towards protected DNS zones. actionFilter + // actionDrop does not respond for queries towards the protected DNS zones. + actionDrop ) +var log = clog.NewWithPlugin("acl") + // ServeDNS implements the plugin.Handler interface. func (a ACL) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { state := request.Request{W: w, Req: r} @@ -63,12 +69,20 @@ RulesCheckLoop: action := matchWithPolicies(rule.policies, w, r) switch action { + case actionDrop: + { + RequestDropCount.WithLabelValues(metrics.WithServer(ctx), zone, metrics.WithView(ctx)).Inc() + return dns.RcodeSuccess, nil + } case actionBlock: { - m := new(dns.Msg) - m.SetRcode(r, dns.RcodeRefused) + m := new(dns.Msg). + SetRcode(r, dns.RcodeRefused). + SetEdns0(4096, true) + ede := dns.EDNS0_EDE{InfoCode: dns.ExtendedErrorCodeBlocked} + m.IsEdns0().Option = append(m.IsEdns0().Option, &ede) w.WriteMsg(m) - RequestBlockCount.WithLabelValues(metrics.WithServer(ctx), zone).Inc() + RequestBlockCount.WithLabelValues(metrics.WithServer(ctx), zone, metrics.WithView(ctx)).Inc() return dns.RcodeSuccess, nil } case actionAllow: @@ -77,17 +91,19 @@ RulesCheckLoop: } case actionFilter: { - m := new(dns.Msg) - m.SetRcode(r, dns.RcodeSuccess) + m := new(dns.Msg). + SetRcode(r, dns.RcodeSuccess). + SetEdns0(4096, true) + ede := dns.EDNS0_EDE{InfoCode: dns.ExtendedErrorCodeFiltered} + m.IsEdns0().Option = append(m.IsEdns0().Option, &ede) w.WriteMsg(m) - RequestFilterCount.WithLabelValues(metrics.WithServer(ctx), zone).Inc() + RequestFilterCount.WithLabelValues(metrics.WithServer(ctx), zone, metrics.WithView(ctx)).Inc() return dns.RcodeSuccess, nil } } - } - RequestAllowCount.WithLabelValues(metrics.WithServer(ctx)).Inc() + RequestAllowCount.WithLabelValues(metrics.WithServer(ctx), metrics.WithView(ctx)).Inc() return plugin.NextOrFailure(state.Name(), a.Next, ctx, w, r) } @@ -96,7 +112,19 @@ RulesCheckLoop: func matchWithPolicies(policies []policy, w dns.ResponseWriter, r *dns.Msg) action { state := request.Request{W: w, Req: r} - ip := net.ParseIP(state.IP()) + var ip net.IP + if idx := strings.IndexByte(state.IP(), '%'); idx >= 0 { + ip = net.ParseIP(state.IP()[:idx]) + } else { + ip = net.ParseIP(state.IP()) + } + + // if the parsing did not return a proper response then we simply return 'actionBlock' to + // block the query + if ip == nil { + log.Errorf("Blocking request. Unable to parse source address: %v", state.IP()) + return actionBlock + } qtype := state.QType() for _, policy := range policies { // dns.TypeNone matches all query types. diff --git a/plugin/acl/acl_test.go b/plugin/acl/acl_test.go index 4c6df95e53..f867d1f529 100644 --- a/plugin/acl/acl_test.go +++ b/plugin/acl/acl_test.go @@ -13,15 +13,21 @@ import ( type testResponseWriter struct { test.ResponseWriter Rcode int + Msg *dns.Msg } func (t *testResponseWriter) setRemoteIP(ip string) { t.RemoteIP = ip } +func (t *testResponseWriter) setZone(zone string) { + t.Zone = zone +} + // WriteMsg implement dns.ResponseWriter interface. func (t *testResponseWriter) WriteMsg(m *dns.Msg) error { t.Rcode = m.Rcode + t.Msg = m return nil } @@ -38,359 +44,510 @@ func TestACLServeDNS(t *testing.T) { qtype uint16 } tests := []struct { - name string - config string - zones []string - args args - wantRcode int - wantErr bool + name string + config string + zones []string + args args + wantRcode int + wantErr bool + wantExtendedErrorCode uint16 + expectNoResponse bool }{ // IPv4 tests. { - "Blacklist 1 BLOCKED", - `acl example.org { + name: "Blacklist 1 BLOCKED", + config: `acl example.org { block type A net 192.168.0.0/16 }`, - []string{}, - args{ - "www.example.org.", - "192.168.0.2", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.0.2", + qtype: dns.TypeA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Blacklist 1 ALLOWED", - `acl example.org { + name: "Blacklist 1 ALLOWED", + config: `acl example.org { block type A net 192.168.0.0/16 }`, - []string{}, - args{ - "www.example.org.", - "192.167.0.2", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.167.0.2", + qtype: dns.TypeA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, }, { - "Blacklist 2 BLOCKED", - ` + name: "Blacklist 2 BLOCKED", + config: ` acl example.org { block type * net 192.168.0.0/16 }`, - []string{}, - args{ - "www.example.org.", - "192.168.0.2", - dns.TypeAAAA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.0.2", + qtype: dns.TypeAAAA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Blacklist 3 BLOCKED", - `acl example.org { + name: "Blacklist 3 BLOCKED", + config: `acl example.org { block type A }`, - []string{}, - args{ - "www.example.org.", - "10.1.0.2", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "10.1.0.2", + qtype: dns.TypeA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Blacklist 3 ALLOWED", - `acl example.org { + name: "Blacklist 3 ALLOWED", + config: `acl example.org { block type A }`, - []string{}, - args{ - "www.example.org.", - "10.1.0.2", - dns.TypeAAAA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "10.1.0.2", + qtype: dns.TypeAAAA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, }, { - "Blacklist 4 Single IP BLOCKED", - `acl example.org { + name: "Blacklist 4 Single IP BLOCKED", + config: `acl example.org { block type A net 192.168.1.2 }`, - []string{}, - args{ - "www.example.org.", - "192.168.1.2", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Blacklist 4 Single IP ALLOWED", - `acl example.org { + name: "Blacklist 4 Single IP ALLOWED", + config: `acl example.org { block type A net 192.168.1.2 }`, - []string{}, - args{ - "www.example.org.", - "192.168.1.3", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.1.3", + qtype: dns.TypeA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, }, { - "Filter 1 FILTERED", - `acl example.org { + name: "Filter 1 FILTERED", + config: `acl example.org { filter type A net 192.168.0.0/16 }`, - []string{}, - args{ - "www.example.org.", - "192.168.0.2", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.0.2", + qtype: dns.TypeA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, + wantExtendedErrorCode: dns.ExtendedErrorCodeFiltered, }, { - "Filter 1 ALLOWED", - `acl example.org { + name: "Filter 1 ALLOWED", + config: `acl example.org { filter type A net 192.168.0.0/16 }`, - []string{}, - args{ - "www.example.org.", - "192.167.0.2", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.167.0.2", + qtype: dns.TypeA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, }, { - "Whitelist 1 ALLOWED", - `acl example.org { + name: "Whitelist 1 ALLOWED", + config: `acl example.org { allow net 192.168.0.0/16 block }`, - []string{}, - args{ - "www.example.org.", - "192.168.0.2", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.0.2", + qtype: dns.TypeA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, }, { - "Whitelist 1 REFUSED", - `acl example.org { + name: "Whitelist 1 REFUSED", + config: `acl example.org { allow type * net 192.168.0.0/16 block }`, - []string{}, - args{ - "www.example.org.", - "10.1.0.2", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "10.1.0.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Drop 1 DROPPED", + config: `acl example.org { + drop net 192.168.0.0/16 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.0.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + expectNoResponse: true, + }, + { + name: "Subnet-Order 1 REFUSED", + config: `acl example.org { + block net 192.168.1.0/24 + drop net 192.168.0.0/16 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Fine-Grained 1 REFUSED", - `acl a.example.org { + name: "Subnet-Order 2 DROPPED", + config: `acl example.org { + drop net 192.168.0.0/16 + block net 192.168.1.0/24 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.1.1", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + expectNoResponse: true, + }, + { + name: "Drop-Type 1 DROPPED", + config: `acl example.org { + drop type A + allow net 192.168.0.0/16 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.1.1", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + expectNoResponse: true, + }, + { + name: "Drop-Type 2 ALLOWED", + config: `acl example.org { + drop type A + allow net 192.168.0.0/16 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.1.1", + qtype: dns.TypeAAAA, + }, + wantRcode: dns.RcodeSuccess, + }, + { + name: "Fine-Grained 1 REFUSED", + config: `acl a.example.org { block type * net 192.168.1.0/24 }`, - []string{"example.org"}, - args{ - "a.example.org.", - "192.168.1.2", - dns.TypeA, + zones: []string{"example.org"}, + args: args{ + domain: "a.example.org.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Fine-Grained 1 ALLOWED", - `acl a.example.org { + name: "Fine-Grained 1 ALLOWED", + config: `acl a.example.org { block net 192.168.1.0/24 }`, - []string{"example.org"}, - args{ - "www.example.org.", - "192.168.1.2", - dns.TypeA, + zones: []string{"example.org"}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, }, { - "Fine-Grained 2 REFUSED", - `acl { + name: "Fine-Grained 2 REFUSED", + config: `acl example.org { block net 192.168.1.0/24 }`, - []string{"example.org"}, - args{ - "a.example.org.", - "192.168.1.2", - dns.TypeA, + zones: []string{"example.org"}, + args: args{ + domain: "a.example.org.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Fine-Grained 2 ALLOWED", - `acl { + name: "Fine-Grained 2 ALLOWED", + config: `acl { block net 192.168.1.0/24 }`, - []string{"example.org"}, - args{ - "a.example.com.", - "192.168.1.2", - dns.TypeA, + zones: []string{"example.org"}, + args: args{ + domain: "a.example.com.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, }, { - "Fine-Grained 3 REFUSED", - `acl a.example.org { + name: "Fine-Grained 3 REFUSED", + config: `acl a.example.org { block net 192.168.1.0/24 } acl b.example.org { block type * net 192.168.2.0/24 }`, - []string{"example.org"}, - args{ - "b.example.org.", - "192.168.2.2", - dns.TypeA, + zones: []string{"example.org"}, + args: args{ + domain: "b.example.org.", + sourceIP: "192.168.2.2", + qtype: dns.TypeA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Fine-Grained 3 ALLOWED", - `acl a.example.org { + name: "Fine-Grained 3 ALLOWED", + config: `acl a.example.org { block net 192.168.1.0/24 } acl b.example.org { block net 192.168.2.0/24 }`, - []string{"example.org"}, - args{ - "b.example.org.", - "192.168.1.2", - dns.TypeA, + zones: []string{"example.org"}, + args: args{ + domain: "b.example.org.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, }, // IPv6 tests. { - "Blacklist 1 BLOCKED IPv6", - `acl example.org { + name: "Blacklist 1 BLOCKED IPv6", + config: `acl example.org { block type A net 2001:db8:abcd:0012::0/64 }`, - []string{}, - args{ - "www.example.org.", - "2001:db8:abcd:0012::1230", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:db8:abcd:0012::1230", + qtype: dns.TypeA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Blacklist 1 ALLOWED IPv6", - `acl example.org { + name: "Blacklist 1 ALLOWED IPv6", + config: `acl example.org { block type A net 2001:db8:abcd:0012::0/64 }`, - []string{}, - args{ - "www.example.org.", - "2001:db8:abcd:0013::0", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:db8:abcd:0013::0", + qtype: dns.TypeA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, }, { - "Blacklist 2 BLOCKED IPv6", - `acl example.org { + name: "Blacklist 2 BLOCKED IPv6", + config: `acl example.org { block type A }`, - []string{}, - args{ - "www.example.org.", - "2001:0db8:85a3:0000:0000:8a2e:0370:7334", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + qtype: dns.TypeA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Blacklist 3 Single IP BLOCKED IPv6", - `acl example.org { + name: "Blacklist 3 Single IP BLOCKED IPv6", + config: `acl example.org { block type A net 2001:0db8:85a3:0000:0000:8a2e:0370:7334 }`, - []string{}, - args{ - "www.example.org.", - "2001:0db8:85a3:0000:0000:8a2e:0370:7334", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + qtype: dns.TypeA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Blacklist 3 Single IP ALLOWED IPv6", - `acl example.org { + name: "Blacklist 3 Single IP ALLOWED IPv6", + config: `acl example.org { block type A net 2001:0db8:85a3:0000:0000:8a2e:0370:7334 }`, - []string{}, - args{ - "www.example.org.", - "2001:0db8:85a3:0000:0000:8a2e:0370:7335", - dns.TypeA, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:0db8:85a3:0000:0000:8a2e:0370:7335", + qtype: dns.TypeA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, }, { - "Fine-Grained 1 REFUSED IPv6", - `acl a.example.org { + name: "Fine-Grained 1 REFUSED IPv6", + config: `acl a.example.org { block type * net 2001:db8:abcd:0012::0/64 }`, - []string{"example.org"}, - args{ - "a.example.org.", - "2001:db8:abcd:0012:2019::0", - dns.TypeA, + zones: []string{"example.org"}, + args: args{ + domain: "a.example.org.", + sourceIP: "2001:db8:abcd:0012:2019::0", + qtype: dns.TypeA, }, - dns.RcodeRefused, - false, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, }, { - "Fine-Grained 1 ALLOWED IPv6", - `acl a.example.org { + name: "Fine-Grained 1 ALLOWED IPv6", + config: `acl a.example.org { block net 2001:db8:abcd:0012::0/64 }`, - []string{"example.org"}, - args{ - "www.example.org.", - "2001:db8:abcd:0012:2019::0", - dns.TypeA, + zones: []string{"example.org"}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:db8:abcd:0012:2019::0", + qtype: dns.TypeA, }, - dns.RcodeSuccess, - false, + wantRcode: dns.RcodeSuccess, + }, + { + name: "Blacklist Address%ifname", + config: `acl example.org { + block type AAAA net 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + }`, + zones: []string{"eth0"}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + qtype: dns.TypeAAAA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Drop 1 DROPPED IPV6", + config: `acl example.org { + drop net 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + qtype: dns.TypeAAAA, + }, + wantRcode: dns.RcodeSuccess, + expectNoResponse: true, + }, + { + name: "Subnet-Order 1 REFUSED IPv6", + config: `acl example.org { + block net 2001:db8:abcd:0012:8000::/66 + drop net 2001:db8:abcd:0012::0/64 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:db8:abcd:0012:8000::1", + qtype: dns.TypeAAAA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Subnet-Order 2 DROPPED IPv6", + config: `acl example.org { + drop net 2001:db8:abcd:0012::0/64 + block net 2001:db8:abcd:0012:8000::/66 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:db8:abcd:0012:8000::1", + qtype: dns.TypeAAAA, + }, + wantRcode: dns.RcodeSuccess, + expectNoResponse: true, + }, + { + name: "Drop-Type 1 DROPPED IPv6", + config: `acl example.org { + drop type A + allow net 2001:db8:85a3:0000::0/64 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + expectNoResponse: true, + }, + { + name: "Drop-Type 2 ALLOWED IPv6", + config: `acl example.org { + drop type A + allow net 2001:db8:85a3:0000::0/64 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + qtype: dns.TypeAAAA, + }, + wantRcode: dns.RcodeSuccess, }, } @@ -408,6 +565,9 @@ func TestACLServeDNS(t *testing.T) { w := &testResponseWriter{} m := new(dns.Msg) w.setRemoteIP(tt.args.sourceIP) + if len(tt.zones) > 0 { + w.setZone(tt.zones[0]) + } m.SetQuestion(tt.args.domain, tt.args.qtype) _, err = a.ServeDNS(ctx, w, m) if (err != nil) != tt.wantErr { @@ -417,6 +577,23 @@ func TestACLServeDNS(t *testing.T) { if w.Rcode != tt.wantRcode { t.Errorf("Error: acl.ServeDNS() Rcode = %v, want %v", w.Rcode, tt.wantRcode) } + if tt.expectNoResponse && w.Msg != nil { + t.Errorf("Error: acl.ServeDNS() responded to client when not expected") + } + if tt.wantExtendedErrorCode != 0 { + matched := false + for _, opt := range w.Msg.IsEdns0().Option { + if ede, ok := opt.(*dns.EDNS0_EDE); ok { + if ede.InfoCode != tt.wantExtendedErrorCode { + t.Errorf("Error: acl.ServeDNS() Extended DNS Error = %v, want %v", ede.InfoCode, tt.wantExtendedErrorCode) + } + matched = true + } + } + if !matched { + t.Error("Error: acl.ServeDNS() missing Extended DNS Error option") + } + } }) } } diff --git a/plugin/acl/metrics.go b/plugin/acl/metrics.go index 76f30b5a6d..a8d8232546 100644 --- a/plugin/acl/metrics.go +++ b/plugin/acl/metrics.go @@ -14,19 +14,26 @@ var ( Subsystem: pluginName, Name: "blocked_requests_total", Help: "Counter of DNS requests being blocked.", - }, []string{"server", "zone"}) + }, []string{"server", "zone", "view"}) // RequestFilterCount is the number of DNS requests being filtered. RequestFilterCount = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: pluginName, Name: "filtered_requests_total", Help: "Counter of DNS requests being filtered.", - }, []string{"server", "zone"}) + }, []string{"server", "zone", "view"}) // RequestAllowCount is the number of DNS requests being Allowed. RequestAllowCount = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: pluginName, Name: "allowed_requests_total", Help: "Counter of DNS requests being allowed.", - }, []string{"server"}) + }, []string{"server", "view"}) + // RequestDropCount is the number of DNS requests being dropped. + RequestDropCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: pluginName, + Name: "dropped_requests_total", + Help: "Counter of DNS requests being dropped.", + }, []string{"server", "zone", "view"}) ) diff --git a/plugin/acl/setup.go b/plugin/acl/setup.go index 1a688a485e..cd2e222d9e 100644 --- a/plugin/acl/setup.go +++ b/plugin/acl/setup.go @@ -43,28 +43,24 @@ func parse(c *caddy.Controller) (ACL, error) { a := ACL{} for c.Next() { r := rule{} - r.zones = c.RemainingArgs() - if len(r.zones) == 0 { - // if empty, the zones from the configuration block are used. - r.zones = make([]string, len(c.ServerBlockKeys)) - copy(r.zones, c.ServerBlockKeys) - } - for i := range r.zones { - r.zones[i] = plugin.Host(r.zones[i]).Normalize() - } + args := c.RemainingArgs() + r.zones = plugin.OriginsFromArgsOrServerBlock(args, c.ServerBlockKeys) for c.NextBlock() { p := policy{} action := strings.ToLower(c.Val()) - if action == "allow" { + switch action { + case "allow": p.action = actionAllow - } else if action == "block" { + case "block": p.action = actionBlock - } else if action == "filter" { + case "filter": p.action = actionFilter - } else { - return a, c.Errf("unexpected token %q; expect 'allow', 'block', or 'filter'", c.Val()) + case "drop": + p.action = actionDrop + default: + return a, c.Errf("unexpected token %q; expect 'allow', 'block', 'filter' or 'drop'", c.Val()) } p.qtypes = make(map[uint16]struct{}) diff --git a/plugin/acl/setup_test.go b/plugin/acl/setup_test.go index 1d25dd7af3..5cd51bb3a1 100644 --- a/plugin/acl/setup_test.go +++ b/plugin/acl/setup_test.go @@ -57,6 +57,13 @@ func TestSetup(t *testing.T) { }`, false, }, + { + "Drop 1", + `acl { + drop type * net 192.168.0.0/16 + }`, + false, + }, { "fine-grained 1", `acl a.example.org { @@ -175,6 +182,13 @@ func TestSetup(t *testing.T) { }`, false, }, + { + "Drop 1 IPv6", + `acl { + drop net 2001:db8:abcd:0012::0/64 + }`, + false, + }, { "fine-grained 1 IPv6", `acl a.example.org { diff --git a/plugin/any/any_test.go b/plugin/any/any_test.go index 85df7d672d..846a804a0a 100644 --- a/plugin/any/any_test.go +++ b/plugin/any/any_test.go @@ -26,3 +26,42 @@ func TestAny(t *testing.T) { t.Errorf("Expected HINFO, but got %q", rec.Msg.Answer[0].(*dns.HINFO).Cpu) } } + +func TestAnyNonANYQuery(t *testing.T) { + tests := []struct { + name string + qtype uint16 + }{ + {"A query", dns.TypeA}, + {"AAAA query", dns.TypeAAAA}, + {"MX query", dns.TypeMX}, + {"TXT query", dns.TypeTXT}, + {"CNAME query", dns.TypeCNAME}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := new(dns.Msg) + req.SetQuestion("example.org.", tt.qtype) + + nextCalled := false + a := &Any{ + Next: test.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + nextCalled = true + return 0, nil + }), + } + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := a.ServeDNS(context.TODO(), rec, req) + + if err != nil { + t.Errorf("Expected no error, but got %q", err) + } + + if !nextCalled { + t.Error("Expected Next handler to be called for non-ANY query") + } + }) + } +} diff --git a/plugin/any/setup_test.go b/plugin/any/setup_test.go new file mode 100644 index 0000000000..49c5ae827c --- /dev/null +++ b/plugin/any/setup_test.go @@ -0,0 +1,21 @@ +package any + +import ( + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" +) + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", `any`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + // Check that the plugin was added to the config + cfg := dnsserver.GetConfig(c) + if len(cfg.Plugin) == 0 { + t.Error("Expected plugin to be added to config") + } +} diff --git a/plugin/auto/auto.go b/plugin/auto/auto.go index 6330621be3..fa62d8f14a 100644 --- a/plugin/auto/auto.go +++ b/plugin/auto/auto.go @@ -51,12 +51,16 @@ func (a Auto) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i // Now the real zone. zone = plugin.Zones(a.Zones.Names()).Matches(qname) if zone == "" { + // If no next plugin is configured, it's more correct to return REFUSED as auto acts as an authoritative server + if a.Next == nil { + return dns.RcodeRefused, nil + } return plugin.NextOrFailure(a.Name(), a.Next, ctx, w, r) } - a.Zones.RLock() - z, ok := a.Zones.Z[zone] - a.Zones.RUnlock() + a.RLock() + z, ok := a.Z[zone] + a.RUnlock() if !ok || z == nil { return dns.RcodeServerFailure, nil @@ -82,7 +86,14 @@ func (a Auto) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i case file.Delegation: m.Authoritative = false case file.ServerFailure: - return dns.RcodeServerFailure, nil + // If the result is SERVFAIL and the answer is non-empty, then the SERVFAIL came from an + // external CNAME lookup and the answer contains the CNAME with no target record. We should + // write the CNAME record to the client instead of sending an empty SERVFAIL response. + if len(m.Answer) == 0 { + return dns.RcodeServerFailure, nil + } + // The rcode in the response should be the rcode received from the target lookup. RFC 6604 section 3 + m.Rcode = dns.RcodeServerFailure } w.WriteMsg(m) diff --git a/plugin/auto/auto_test.go b/plugin/auto/auto_test.go new file mode 100644 index 0000000000..9871d342d1 --- /dev/null +++ b/plugin/auto/auto_test.go @@ -0,0 +1,268 @@ +package auto + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestAutoName(t *testing.T) { + t.Parallel() + a := Auto{} + if a.Name() != "auto" { + t.Errorf("Expected 'auto', got %s", a.Name()) + } +} + +func TestAutoServeDNS(t *testing.T) { + t.Parallel() + tests := []struct { + name string + qname string + qtype uint16 + zones []string + expectedCode int + shouldMatch bool + }{ + { + name: "valid A query", + qname: "test.example.org.", + qtype: dns.TypeA, + zones: []string{"example.org."}, + expectedCode: dns.RcodeServerFailure, // Zone exists but no data + shouldMatch: true, + }, + { + name: "AXFR query refused", + qname: "test.example.org.", + qtype: dns.TypeAXFR, + zones: []string{"example.org."}, + expectedCode: dns.RcodeRefused, + shouldMatch: true, + }, + { + name: "IXFR query refused", + qname: "test.example.org.", + qtype: dns.TypeIXFR, + zones: []string{"example.org."}, + expectedCode: dns.RcodeRefused, + shouldMatch: true, + }, + { + name: "no matching zone", + qname: "test.notfound.org.", + qtype: dns.TypeA, + zones: []string{"example.org."}, + shouldMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + a := createTestAuto(tt.zones) + + m := new(dns.Msg) + m.SetQuestion(tt.qname, tt.qtype) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + ctx := context.Background() + + code, err := a.ServeDNS(ctx, rec, m) + + if !tt.shouldMatch { + if err == nil { + t.Errorf("Expected error for non-matching zone, got nil") + } + return + } + + if err != nil { + t.Errorf("ServeDNS returned error: %v", err) + } + + if tt.qtype == dns.TypeAXFR || tt.qtype == dns.TypeIXFR { + if code != dns.RcodeRefused { + t.Errorf("Expected RcodeRefused for %s, got %d", dns.TypeToString[tt.qtype], code) + } + return + } + + if code != tt.expectedCode { + t.Errorf("Expected code %d, got %d", tt.expectedCode, code) + } + }) + } +} + +func TestAutoServeDNSZoneMatching(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + origins []string + names []string + qname string + hasZone bool + shouldRefuse bool + }{ + { + name: "exact zone match", + origins: []string{"example.org."}, + names: []string{"example.org."}, + qname: "test.example.org.", + hasZone: true, + shouldRefuse: false, + }, + { + name: "subdomain zone match", + origins: []string{"example.org."}, + names: []string{"example.org."}, + qname: "sub.test.example.org.", + hasZone: true, + shouldRefuse: false, + }, + { + name: "no origin match", + origins: []string{"other.org."}, + names: []string{"example.org."}, + qname: "test.example.org.", + hasZone: false, + shouldRefuse: false, + }, + { + name: "origin match but no name match", + origins: []string{"example.org."}, + names: []string{"other.org."}, + qname: "test.example.org.", + hasZone: false, + shouldRefuse: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + a := &Auto{ + Zones: &Zones{ + Z: make(map[string]*file.Zone), + origins: tt.origins, + names: tt.names, + }, + Next: nil, + } + + for _, name := range tt.names { + a.Z[name] = &file.Zone{} + } + + m := new(dns.Msg) + m.SetQuestion(tt.qname, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + ctx := context.Background() + + code, err := a.ServeDNS(ctx, rec, m) + + if tt.hasZone { + if err != nil { + t.Errorf("Expected no error for zone match, got: %v", err) + } + } else { + if tt.shouldRefuse { + if code != dns.RcodeRefused { + t.Errorf("Expected code %d, got %d", dns.RcodeRefused, code) + } + } else if err == nil { + t.Errorf("Expected error for no zone match, got nil") + } + } + }) + } +} + +func TestAutoServeDNSNilZone(t *testing.T) { + t.Parallel() + + a := &Auto{ + Zones: &Zones{ + Z: make(map[string]*file.Zone), + origins: []string{"example.org."}, + names: []string{"example.org."}, + }, + Next: nil, + } + + a.Z["example.org."] = nil + + m := new(dns.Msg) + m.SetQuestion("test.example.org.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + ctx := context.Background() + + code, err := a.ServeDNS(ctx, rec, m) + + if code != dns.RcodeServerFailure { + t.Errorf("Expected RcodeServerFailure for nil zone, got %d", code) + } + if err != nil { + t.Errorf("Expected no error for nil zone, got: %v", err) + } +} + +func TestAutoServeDNSMissingZone(t *testing.T) { + t.Parallel() + + a := &Auto{ + Zones: &Zones{ + Z: make(map[string]*file.Zone), + origins: []string{"example.org."}, + names: []string{"example.org."}, + }, + Next: nil, + } + + // Don't add the zone to the map to test the missing zone case + + m := new(dns.Msg) + m.SetQuestion("test.example.org.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + ctx := context.Background() + + code, err := a.ServeDNS(ctx, rec, m) + + if code != dns.RcodeServerFailure { + t.Errorf("Expected RcodeServerFailure for missing zone, got %d", code) + } + if err != nil { + t.Errorf("Expected no error for missing zone, got: %v", err) + } +} + +// Helper functions for testing + +func createTestAuto(zones []string) *Auto { + a := &Auto{ + Zones: &Zones{ + Z: make(map[string]*file.Zone), + origins: zones, + names: zones, + }, + Next: nil, // No next plugin for testing + } + + // Initialize with empty zones for the tests + for _, zone := range zones { + a.Z[zone] = &file.Zone{} + } + + return a +} diff --git a/plugin/auto/regexp.go b/plugin/auto/regexp.go index fa424ec7ec..c203e559ab 100644 --- a/plugin/auto/regexp.go +++ b/plugin/auto/regexp.go @@ -1,5 +1,9 @@ package auto +import ( + "strings" +) + // rewriteToExpand rewrites our template string to one that we can give to regexp.ExpandString. This basically // involves prefixing any '{' with a '$'. func rewriteToExpand(s string) string { @@ -7,14 +11,13 @@ func rewriteToExpand(s string) string { // Also wasteful as we build the string with +=. This is OKish // as we do this during config parsing. - copy := "" - + var copySb strings.Builder for _, c := range s { if c == '{' { - copy += "$" + copySb.WriteString("$") } - copy += string(c) + copySb.WriteString(string(c)) } - return copy + return copySb.String() } diff --git a/plugin/auto/regexp_test.go b/plugin/auto/regexp_test.go index 17c35eb90c..23bf094dea 100644 --- a/plugin/auto/regexp_test.go +++ b/plugin/auto/regexp_test.go @@ -1,8 +1,12 @@ package auto -import "testing" +import ( + "fmt" + "testing" +) func TestRewriteToExpand(t *testing.T) { + t.Parallel() tests := []struct { in string expected string @@ -12,9 +16,12 @@ func TestRewriteToExpand(t *testing.T) { {in: "{1", expected: "${1"}, } for i, tc := range tests { - got := rewriteToExpand(tc.in) - if got != tc.expected { - t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expected, got) - } + t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { + t.Parallel() + got := rewriteToExpand(tc.in) + if got != tc.expected { + t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expected, got) + } + }) } } diff --git a/plugin/auto/setup.go b/plugin/auto/setup.go index bebf3a65b1..a31d5e5bce 100644 --- a/plugin/auto/setup.go +++ b/plugin/auto/setup.go @@ -1,6 +1,7 @@ package auto import ( + "errors" "os" "path/filepath" "regexp" @@ -44,15 +45,24 @@ func setup(c *caddy.Controller) error { if err != nil { return err } - + if err := a.Notify(); err != nil { + log.Warning(err) + } + if a.ReloadInterval == 0 { + return nil + } go func() { - ticker := time.NewTicker(a.loader.ReloadInterval) + ticker := time.NewTicker(a.ReloadInterval) + defer ticker.Stop() for { select { case <-walkChan: return case <-ticker.C: a.Walk() + if err := a.Notify(); err != nil { + log.Warning(err) + } } } }() @@ -61,6 +71,11 @@ func setup(c *caddy.Controller) error { c.OnShutdown(func() error { close(walkChan) + for _, z := range a.Z { + z.Lock() + z.OnShutdown() + z.Unlock() + } return nil }) @@ -87,17 +102,9 @@ func autoParse(c *caddy.Controller) (Auto, error) { for c.Next() { // auto [ZONES...] - a.Zones.origins = make([]string, len(c.ServerBlockKeys)) - copy(a.Zones.origins, c.ServerBlockKeys) - args := c.RemainingArgs() - if len(args) > 0 { - a.Zones.origins = args - } - for i := range a.Zones.origins { - a.Zones.origins[i] = plugin.Host(a.Zones.origins[i]).Normalize() - } - a.loader.upstream = upstream.New() + a.origins = plugin.OriginsFromArgsOrServerBlock(args, c.ServerBlockKeys) + a.upstream = upstream.New() for c.NextBlock() { switch c.Val() { @@ -105,33 +112,32 @@ func autoParse(c *caddy.Controller) (Auto, error) { if !c.NextArg() { return a, c.ArgErr() } - a.loader.directory = c.Val() - if !filepath.IsAbs(a.loader.directory) && config.Root != "" { - a.loader.directory = filepath.Join(config.Root, a.loader.directory) + a.directory = c.Val() + if !filepath.IsAbs(a.directory) && config.Root != "" { + a.directory = filepath.Join(config.Root, a.directory) } - _, err := os.Stat(a.loader.directory) + _, err := os.Stat(a.directory) if err != nil { - if os.IsNotExist(err) { - log.Warningf("Directory does not exist: %s", a.loader.directory) - } else { - return a, c.Errf("Unable to access root path '%s': %v", a.loader.directory, err) + if !os.IsNotExist(err) { + return a, c.Errf("Unable to access root path '%s': %v", a.directory, err) } + log.Warningf("Directory does not exist: %s", a.directory) } // regexp template if c.NextArg() { - a.loader.re, err = regexp.Compile(c.Val()) + a.re, err = regexp.Compile(c.Val()) if err != nil { return a, err } - if a.loader.re.NumSubexp() == 0 { + if a.re.NumSubexp() == 0 { return a, c.Errf("Need at least one sub expression") } if !c.NextArg() { return a, c.ArgErr() } - a.loader.template = rewriteToExpand(c.Val()) + a.template = rewriteToExpand(c.Val()) } if c.NextArg() { @@ -139,11 +145,18 @@ func autoParse(c *caddy.Controller) (Auto, error) { } case "reload": - d, err := time.ParseDuration(c.RemainingArgs()[0]) + t := c.RemainingArgs() + if len(t) < 1 { + return a, errors.New("reload duration value is expected") + } + d, err := time.ParseDuration(t[0]) + if d < 0 { + err = errors.New("invalid duration") + } if err != nil { return a, plugin.Error("file", err) } - a.loader.ReloadInterval = d + a.ReloadInterval = d case "upstream": // remove soon @@ -155,8 +168,8 @@ func autoParse(c *caddy.Controller) (Auto, error) { } } - if a.loader.ReloadInterval == nilInterval { - a.loader.ReloadInterval = 60 * time.Second + if a.ReloadInterval == nilInterval { + a.ReloadInterval = 60 * time.Second } return a, nil diff --git a/plugin/auto/setup_test.go b/plugin/auto/setup_test.go index 987411c079..9f0ede8485 100644 --- a/plugin/auto/setup_test.go +++ b/plugin/auto/setup_test.go @@ -1,6 +1,7 @@ package auto import ( + "fmt" "testing" "time" @@ -8,6 +9,7 @@ import ( ) func TestAutoParse(t *testing.T) { + t.Parallel() tests := []struct { inputFileRules string shouldErr bool @@ -93,6 +95,13 @@ func TestAutoParse(t *testing.T) { }`, true, "/tmp", "${1}", ``, 60 * time.Second, }, + // non-existent directory. + { + `auto example.org { + directory /foobar/coredns * {1} + }`, + true, "/tmp", "${1}", ``, 60 * time.Second, + }, // unexpected argument. { `auto example.org { @@ -100,29 +109,99 @@ func TestAutoParse(t *testing.T) { }`, true, "/tmp", "${1}", ``, 60 * time.Second, }, + // upstream directive should not error and should consume args + { + `auto example.org { + directory /tmp + upstream 8.8.8.8 1.1.1.1 + }`, + false, "/tmp", "${1}", `db\.(.*)`, 60 * time.Second, + }, + // upstream directive with no args should not error + { + `auto example.org { + directory /tmp + upstream + }`, + false, "/tmp", "${1}", `db\.(.*)`, 60 * time.Second, + }, } for i, test := range tests { - c := caddy.NewTestController("dns", test.inputFileRules) - a, err := autoParse(c) + t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { + t.Parallel() + c := caddy.NewTestController("dns", test.inputFileRules) + a, err := autoParse(c) - if err == nil && test.shouldErr { - t.Fatalf("Test %d expected errors, but got no error", i) - } else if err != nil && !test.shouldErr { - t.Fatalf("Test %d expected no errors, but got '%v'", i, err) - } else if !test.shouldErr { - if a.loader.directory != test.expectedDirectory { - t.Fatalf("Test %d expected %v, got %v", i, test.expectedDirectory, a.loader.directory) - } - if a.loader.template != test.expectedTempl { - t.Fatalf("Test %d expected %v, got %v", i, test.expectedTempl, a.loader.template) - } - if a.loader.re.String() != test.expectedRe { - t.Fatalf("Test %d expected %v, got %v", i, test.expectedRe, a.loader.re) + if err == nil && test.shouldErr { + t.Fatalf("Test %d expected errors, but got no error", i) + } else if err != nil && !test.shouldErr { + t.Fatalf("Test %d expected no errors, but got '%v'", i, err) + } else if !test.shouldErr { + if a.directory != test.expectedDirectory { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedDirectory, a.directory) + } + if a.template != test.expectedTempl { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedTempl, a.template) + } + if a.re.String() != test.expectedRe { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedRe, a.re) + } + if a.ReloadInterval != test.expectedReloadInterval { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedReloadInterval, a.ReloadInterval) + } } - if a.loader.ReloadInterval != test.expectedReloadInterval { - t.Fatalf("Test %d expected %v, got %v", i, test.expectedReloadInterval, a.loader.ReloadInterval) + }) + } +} + +func TestSetupReload(t *testing.T) { + t.Parallel() + tests := []struct { + name string + config string + wantErr bool + }{ + { + name: "reload valid", + config: `auto { + directory . + reload 5s + }`, + wantErr: false, + }, + { + name: "reload disable", + config: `auto { + directory . + reload 0 + }`, + wantErr: false, + }, + { + name: "reload invalid", + config: `auto { + directory . + reload -1s + }`, + wantErr: true, + }, + { + name: "reload invalid", + config: `auto { + directory . + reload + }`, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctr := caddy.NewTestController("dns", tt.config) + if err := setup(ctr); (err != nil) != tt.wantErr { + t.Errorf("Error: setup() error = %v, wantErr %v", err, tt.wantErr) } - } + }) } } diff --git a/plugin/auto/walk.go b/plugin/auto/walk.go index 9108fe9411..e1743e232b 100644 --- a/plugin/auto/walk.go +++ b/plugin/auto/walk.go @@ -12,32 +12,34 @@ import ( // Walk will recursively walk of the file under l.directory and adds the one that match l.re. func (a Auto) Walk() error { - // TODO(miek): should add something so that we don't stomp on each other. toDelete := make(map[string]bool) - for _, n := range a.Zones.Names() { + for _, n := range a.Names() { toDelete[n] = true } - filepath.Walk(a.loader.directory, func(path string, info os.FileInfo, _ error) error { + filepath.Walk(a.directory, func(path string, info os.FileInfo, e error) error { + if e != nil { + log.Warningf("error reading %v: %v", path, e) + } if info == nil || info.IsDir() { return nil } - match, origin := matches(a.loader.re, info.Name(), a.loader.template) + match, origin := matches(a.re, info.Name(), a.template) if !match { return nil } - if z, ok := a.Zones.Z[origin]; ok { + if z, ok := a.Z[origin]; ok { // we already have this zone toDelete[origin] = false z.SetFile(path) return nil } - reader, err := os.Open(path) + reader, err := os.Open(filepath.Clean(path)) if err != nil { log.Warningf("Opening %s failed: %s", path, err) return nil @@ -51,17 +53,15 @@ func (a Auto) Walk() error { return nil } - zo.ReloadInterval = a.loader.ReloadInterval - zo.Upstream = a.loader.upstream + zo.ReloadInterval = a.ReloadInterval + zo.Upstream = a.upstream - a.Zones.Add(zo, origin, a.transfer) + a.Add(zo, origin, a.transfer) if a.metrics != nil { a.metrics.AddZone(origin) } - a.transfer.Notify(origin) - log.Infof("Inserting zone `%s' from: %s", origin, path) toDelete[origin] = false @@ -78,7 +78,7 @@ func (a Auto) Walk() error { a.metrics.RemoveZone(origin) } - a.Zones.Remove(origin) + a.Remove(origin) log.Infof("Deleting zone `%s'", origin) } diff --git a/plugin/auto/walk_test.go b/plugin/auto/walk_test.go index d655b9be5f..5517085790 100644 --- a/plugin/auto/walk_test.go +++ b/plugin/auto/walk_test.go @@ -1,7 +1,6 @@ package auto import ( - "io/ioutil" "os" "path/filepath" "regexp" @@ -19,14 +18,11 @@ www IN A 127.0.0.1 ` func TestWalk(t *testing.T) { - tempdir, err := createFiles() + t.Parallel() + tempdir, err := createFiles(t) if err != nil { - if tempdir != "" { - os.RemoveAll(tempdir) - } t.Fatal(err) } - defer os.RemoveAll(tempdir) ldr := loader{ directory: tempdir, @@ -43,13 +39,14 @@ func TestWalk(t *testing.T) { // db.example.org and db.example.com should be here (created in createFiles) for _, name := range []string{"example.com.", "example.org."} { - if _, ok := a.Zones.Z[name]; !ok { + if _, ok := a.Z[name]; !ok { t.Errorf("%s should have been added", name) } } } func TestWalkNonExistent(t *testing.T) { + t.Parallel() nonExistingDir := "highly_unlikely_to_exist_dir" ldr := loader{ @@ -66,22 +63,20 @@ func TestWalkNonExistent(t *testing.T) { a.Walk() } -func createFiles() (string, error) { - dir, err := ioutil.TempDir(os.TempDir(), "coredns") - if err != nil { - return dir, err - } +func createFiles(t *testing.T) (string, error) { + t.Helper() + dir := t.TempDir() for _, name := range dbFiles { - if err := ioutil.WriteFile(filepath.Join(dir, name), []byte(zoneContent), 0644); err != nil { + if err := os.WriteFile(filepath.Join(dir, name), []byte(zoneContent), 0644); err != nil { return dir, err } } // symlinks - if err = os.Symlink(filepath.Join(dir, "db.example.org"), filepath.Join(dir, "db.example.com")); err != nil { + if err := os.Symlink(filepath.Join(dir, "db.example.org"), filepath.Join(dir, "db.example.com")); err != nil { return dir, err } - if err = os.Symlink(filepath.Join(dir, "db.example.org"), filepath.Join(dir, "aa.example.com")); err != nil { + if err := os.Symlink(filepath.Join(dir, "db.example.org"), filepath.Join(dir, "aa.example.com")); err != nil { return dir, err } diff --git a/plugin/auto/watcher_test.go b/plugin/auto/watcher_test.go index 43f1ff374f..eb524678a0 100644 --- a/plugin/auto/watcher_test.go +++ b/plugin/auto/watcher_test.go @@ -8,14 +8,11 @@ import ( ) func TestWatcher(t *testing.T) { - tempdir, err := createFiles() + t.Parallel() + tempdir, err := createFiles(t) if err != nil { - if tempdir != "" { - os.RemoveAll(tempdir) - } t.Fatal(err) } - defer os.RemoveAll(tempdir) ldr := loader{ directory: tempdir, @@ -31,10 +28,10 @@ func TestWatcher(t *testing.T) { a.Walk() // example.org and example.com should exist, we have 3 apex rrs and 1 "real" record. All() returns the non-apex ones. - if x := len(a.Zones.Z["example.org."].All()); x != 1 { + if x := len(a.Z["example.org."].All()); x != 1 { t.Fatalf("Expected 1 RRs, got %d", x) } - if x := len(a.Zones.Z["example.com."].All()); x != 1 { + if x := len(a.Z["example.com."].All()); x != 1 { t.Fatalf("Expected 1 RRs, got %d", x) } @@ -45,23 +42,20 @@ func TestWatcher(t *testing.T) { a.Walk() - if _, ok := a.Zones.Z["example.com."]; ok { + if _, ok := a.Z["example.com."]; ok { t.Errorf("Expected %q to be gone.", "example.com.") } - if _, ok := a.Zones.Z["example.org."]; !ok { + if _, ok := a.Z["example.org."]; !ok { t.Errorf("Expected %q to still be there.", "example.org.") } } func TestSymlinks(t *testing.T) { - tempdir, err := createFiles() + t.Parallel() + tempdir, err := createFiles(t) if err != nil { - if tempdir != "" { - os.RemoveAll(tempdir) - } t.Fatal(err) } - defer os.RemoveAll(tempdir) ldr := loader{ directory: tempdir, @@ -91,7 +85,7 @@ func TestSymlinks(t *testing.T) { a.Walk() - if storedZone, ok := a.Zones.Z["example.com."]; ok { + if storedZone, ok := a.Z["example.com."]; ok { storedFile := storedZone.File() if storedFile != newFile { t.Errorf("Expected %q to reflect new path %q", storedFile, newFile) diff --git a/plugin/auto/xfr.go b/plugin/auto/xfr.go index 6fef8b9e81..93e20bc291 100644 --- a/plugin/auto/xfr.go +++ b/plugin/auto/xfr.go @@ -8,12 +8,24 @@ import ( // Transfer implements the transfer.Transfer interface. func (a Auto) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) { - a.Zones.RLock() - z, ok := a.Zones.Z[zone] - a.Zones.RUnlock() + a.RLock() + z, ok := a.Z[zone] + a.RUnlock() if !ok || z == nil { return nil, transfer.ErrNotAuthoritative } return z.Transfer(serial) } + +// Notify sends notifies for all zones with secondaries configured with the transfer plugin +func (a Auto) Notify() error { + var err error + for _, origin := range a.Names() { + e := a.transfer.Notify(origin) + if e != nil { + err = e + } + } + return err +} diff --git a/plugin/auto/xfr_test.go b/plugin/auto/xfr_test.go new file mode 100644 index 0000000000..51d75fe364 --- /dev/null +++ b/plugin/auto/xfr_test.go @@ -0,0 +1,98 @@ +package auto + +import ( + "testing" + + "github.com/coredns/coredns/plugin/file" +) + +func TestAutoNotify(t *testing.T) { + t.Parallel() + + a := &Auto{ + Zones: &Zones{ + names: []string{"example.org.", "test.org."}, + }, + transfer: nil, + } + + err := a.Notify() + if err != nil { + t.Errorf("Expected no error, got %v", err) + } +} + +func TestAutoTransferZoneCase(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + zone string + expectError bool + errorType string + }{ + { + name: "exact match", + zone: "example.org.", + expectError: true, + errorType: "no SOA", + }, + { + name: "case different", + zone: "EXAMPLE.ORG.", + expectError: true, + errorType: "not authoritative", + }, + { + name: "no match", + zone: "other.org.", + expectError: true, + errorType: "not authoritative", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + a := createTestAutoForTransfer(t, []string{"example.org."}) + + ch, err := a.Transfer(tt.zone, 1234) + + if !tt.expectError { + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if ch == nil { + t.Error("Expected non-nil channel") + } + } else { + if err == nil { + t.Error("Expected error, got nil") + } + if ch != nil { + t.Error("Expected nil channel when error occurs") + } + } + }) + } +} + +// Helper functions + +func createTestAutoForTransfer(t *testing.T, zones []string) *Auto { + t.Helper() + a := &Auto{ + Zones: &Zones{ + Z: make(map[string]*file.Zone), + names: zones, + }, + } + + // Initialize with real empty zones for the tests + for _, zone := range zones { + a.Z[zone] = &file.Zone{} + } + + return a +} diff --git a/plugin/auto/zone.go b/plugin/auto/zone.go index dff376bf9f..bb81186136 100644 --- a/plugin/auto/zone.go +++ b/plugin/auto/zone.go @@ -41,7 +41,7 @@ func (z *Zones) Zones(name string) *file.Zone { return zo } -// Add adds a new zone into z. If zo.NoReload is false, the +// Add adds a new zone into z. If z.ReloadInterval is not zero, the // reload goroutine is started. func (z *Zones) Add(zo *file.Zone, name string, t *transfer.Transfer) { z.Lock() diff --git a/plugin/auto/zone_test.go b/plugin/auto/zone_test.go new file mode 100644 index 0000000000..6fc1da7f97 --- /dev/null +++ b/plugin/auto/zone_test.go @@ -0,0 +1,222 @@ +package auto + +import ( + "testing" + + "github.com/coredns/coredns/plugin/file" +) + +func TestZonesNames(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + zones []string + expected []string + }{ + { + name: "empty zones", + zones: []string{}, + expected: []string{}, + }, + { + name: "single zone", + zones: []string{"example.org."}, + expected: []string{"example.org."}, + }, + { + name: "multiple zones", + zones: []string{"example.org.", "test.org.", "another.com."}, + expected: []string{"example.org.", "test.org.", "another.com."}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + z := &Zones{ + names: tt.zones, + } + + result := z.Names() + + if len(result) != len(tt.expected) { + t.Errorf("Expected %d names, got %d", len(tt.expected), len(result)) + } + + for i, name := range tt.expected { + if i >= len(result) || result[i] != name { + t.Errorf("Expected name %s at index %d, got %s", name, i, result[i]) + } + } + }) + } +} + +func TestZonesOrigins(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + origins []string + expected []string + }{ + { + name: "empty origins", + origins: []string{}, + expected: []string{}, + }, + { + name: "single origin", + origins: []string{"example.org."}, + expected: []string{"example.org."}, + }, + { + name: "multiple origins", + origins: []string{"example.org.", "test.org."}, + expected: []string{"example.org.", "test.org."}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + z := &Zones{ + origins: tt.origins, + } + + result := z.Origins() + + if len(result) != len(tt.expected) { + t.Errorf("Expected %d origins, got %d", len(tt.expected), len(result)) + } + + for i, origin := range tt.expected { + if i >= len(result) || result[i] != origin { + t.Errorf("Expected origin %s at index %d, got %s", origin, i, result[i]) + } + } + }) + } +} + +func TestZonesZones(t *testing.T) { + t.Parallel() + + zone1 := &file.Zone{} + zone2 := &file.Zone{} + + z := &Zones{ + Z: map[string]*file.Zone{ + "example.org.": zone1, + "test.org.": zone2, + }, + } + + tests := []struct { + name string + zoneName string + expected *file.Zone + }{ + { + name: "existing zone", + zoneName: "example.org.", + expected: zone1, + }, + { + name: "another existing zone", + zoneName: "test.org.", + expected: zone2, + }, + { + name: "non-existent zone", + zoneName: "notfound.org.", + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := z.Zones(tt.zoneName) + + if result != tt.expected { + t.Errorf("Expected zone %v, got %v", tt.expected, result) + } + }) + } +} + +func TestZonesAdd(t *testing.T) { + t.Parallel() + + z := &Zones{} + zone := &file.Zone{} + + // Test adding to empty zones + z.Add(zone, "example.org.", nil) + + if z.Z == nil { + t.Error("Expected Z map to be initialized") + } + + if z.Z["example.org."] != zone { + t.Error("Expected zone to be added to map") + } + + if len(z.names) != 1 || z.names[0] != "example.org." { + t.Errorf("Expected names to contain 'example.org.', got %v", z.names) + } + + // Test adding another zone + zone2 := &file.Zone{} + z.Add(zone2, "test.org.", nil) + + if len(z.Z) != 2 { + t.Errorf("Expected 2 zones in map, got %d", len(z.Z)) + } + + if z.Z["test.org."] != zone2 { + t.Error("Expected second zone to be added to map") + } + + if len(z.names) != 2 { + t.Errorf("Expected 2 names, got %d", len(z.names)) + } +} + +func TestZonesEmptyOperations(t *testing.T) { + t.Parallel() + + z := &Zones{} + + names := z.Names() + if len(names) != 0 { + t.Errorf("Expected empty names slice, got %v", names) + } + + origins := z.Origins() + if len(origins) != 0 { + t.Errorf("Expected empty origins slice, got %v", origins) + } + + zone := z.Zones("any.zone.") + if zone != nil { + t.Errorf("Expected nil zone, got %v", zone) + } + + z.Remove("any.zone.") + + testZone := &file.Zone{} + z.Add(testZone, "test.org.", nil) + + if z.Z == nil { + t.Error("Expected Z map to be initialized after Add") + } + if z.Z["test.org."] != testZone { + t.Error("Expected zone to be added") + } +} diff --git a/plugin/autopath/README.md b/plugin/autopath/README.md index eedbf5ee77..64b022e0a6 100644 --- a/plugin/autopath/README.md +++ b/plugin/autopath/README.md @@ -57,7 +57,7 @@ path) in the following case. To properly build the search path of a client *auto the namespace of the a Pod making a DNS request. To do this, it relies on the *kubernetes* plugin's Pod cache to resolve the client's IP address to a Pod. The Pod cache is maintained by an API watch on Pods. When Pod IP assignments change, the Kubernetes API notifies CoreDNS via the API watch. -However, that notification is not instantaneous. In the case that a Pod is deleted, and it's IP is +However, that notification is not instantaneous. In the case that a Pod is deleted, and its IP is immediately provisioned to a Pod in another namespace, and that new Pod make a DNS lookup *before* the API watch can notify CoreDNS of the change, *autopath* will resolve the IP to the previous Pod's namespace. diff --git a/plugin/autopath/autopath.go b/plugin/autopath/autopath.go index b8e153c2ac..f6b3488e8b 100644 --- a/plugin/autopath/autopath.go +++ b/plugin/autopath/autopath.go @@ -25,9 +25,9 @@ autopath.Func. Note the searchpath must be ending with the empty string. I.e: -func (m Plugins ) AutoPath(state request.Request) []string { - return []string{"first", "second", "last", ""} -} + func (m Plugins ) AutoPath(state request.Request) []string { + return []string{"first", "second", "last", ""} + } */ package autopath @@ -135,7 +135,6 @@ func (a *AutoPath) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms w.WriteMsg(msg) autoPathCount.WithLabelValues(metrics.WithServer(ctx)).Add(1) return rcode, err - } if plugin.ClientWrite(firstRcode) { w.WriteMsg(firstReply) diff --git a/plugin/autopath/setup.go b/plugin/autopath/setup.go index 39beef770a..a041e364c6 100644 --- a/plugin/autopath/setup.go +++ b/plugin/autopath/setup.go @@ -2,6 +2,7 @@ package autopath import ( "fmt" + "strings" "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" @@ -50,7 +51,7 @@ func autoPathParse(c *caddy.Controller) (*AutoPath, string, error) { return ap, "", fmt.Errorf("no resolv-conf specified") } resolv := zoneAndresolv[len(zoneAndresolv)-1] - if resolv[0] == '@' { + if strings.HasPrefix(resolv, "@") { mw = resolv[1:] } else { // assume file on disk @@ -62,14 +63,8 @@ func autoPathParse(c *caddy.Controller) (*AutoPath, string, error) { plugin.Zones(ap.search).Normalize() ap.search = append(ap.search, "") // sentinel value as demanded. } - ap.Zones = zoneAndresolv[:len(zoneAndresolv)-1] - if len(ap.Zones) == 0 { - ap.Zones = make([]string, len(c.ServerBlockKeys)) - copy(ap.Zones, c.ServerBlockKeys) - } - for i, str := range ap.Zones { - ap.Zones[i] = plugin.Host(str).Normalize() - } + zones := zoneAndresolv[:len(zoneAndresolv)-1] + ap.Zones = plugin.OriginsFromArgsOrServerBlock(zones, c.ServerBlockKeys) } return ap, mw, nil } diff --git a/plugin/autopath/setup_test.go b/plugin/autopath/setup_test.go index 5847e0de50..4644c7d59b 100644 --- a/plugin/autopath/setup_test.go +++ b/plugin/autopath/setup_test.go @@ -33,6 +33,7 @@ func TestSetupAutoPath(t *testing.T) { // negative {`autopath kubernetes`, true, "", "", nil, "open kubernetes: no such file or directory"}, {`autopath`, true, "", "", nil, "no resolv-conf"}, + {`autopath ""`, true, "", "", nil, "no such file"}, } for i, test := range tests { diff --git a/plugin/azure/azure.go b/plugin/azure/azure.go index 6b4aba0ccd..369d337051 100644 --- a/plugin/azure/azure.go +++ b/plugin/azure/azure.go @@ -84,12 +84,16 @@ func (h *Azure) Run(ctx context.Context) error { return err } go func() { + delay := 1 * time.Minute + timer := time.NewTimer(delay) + defer timer.Stop() for { + timer.Reset(delay) select { case <-ctx.Done(): log.Debugf("Breaking out of Azure update loop for %v: %v", h.zoneNames, ctx.Err()) return - case <-time.After(1 * time.Minute): + case <-timer.C: if err := h.updateZones(ctx); err != nil && ctx.Err() == nil { log.Errorf("Failed to update zones %v: %v", h.zoneNames, err) } @@ -130,31 +134,30 @@ func (h *Azure) updateZones(ctx context.Context) error { return fmt.Errorf("errors updating zones: %v", errs) } return nil - } func updateZoneFromPublicResourceSet(recordSet publicdns.RecordSetListResultPage, newZ *file.Zone) { for _, result := range *(recordSet.Response().Value) { - resultFqdn := *(result.RecordSetProperties.Fqdn) - resultTTL := uint32(*(result.RecordSetProperties.TTL)) - if result.RecordSetProperties.ARecords != nil { - for _, A := range *(result.RecordSetProperties.ARecords) { + resultFqdn := *(result.Fqdn) + resultTTL := uint32(*(result.TTL)) + if result.ARecords != nil { + for _, A := range *(result.ARecords) { a := &dns.A{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: resultTTL}, A: net.ParseIP(*(A.Ipv4Address))} newZ.Insert(a) } } - if result.RecordSetProperties.AaaaRecords != nil { - for _, AAAA := range *(result.RecordSetProperties.AaaaRecords) { + if result.AaaaRecords != nil { + for _, AAAA := range *(result.AaaaRecords) { aaaa := &dns.AAAA{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: resultTTL}, AAAA: net.ParseIP(*(AAAA.Ipv6Address))} newZ.Insert(aaaa) } } - if result.RecordSetProperties.MxRecords != nil { - for _, MX := range *(result.RecordSetProperties.MxRecords) { + if result.MxRecords != nil { + for _, MX := range *(result.MxRecords) { mx := &dns.MX{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: resultTTL}, Preference: uint16(*(MX.Preference)), Mx: dns.Fqdn(*(MX.Exchange))} @@ -162,16 +165,16 @@ func updateZoneFromPublicResourceSet(recordSet publicdns.RecordSetListResultPage } } - if result.RecordSetProperties.PtrRecords != nil { - for _, PTR := range *(result.RecordSetProperties.PtrRecords) { + if result.PtrRecords != nil { + for _, PTR := range *(result.PtrRecords) { ptr := &dns.PTR{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: resultTTL}, Ptr: dns.Fqdn(*(PTR.Ptrdname))} newZ.Insert(ptr) } } - if result.RecordSetProperties.SrvRecords != nil { - for _, SRV := range *(result.RecordSetProperties.SrvRecords) { + if result.SrvRecords != nil { + for _, SRV := range *(result.SrvRecords) { srv := &dns.SRV{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: resultTTL}, Priority: uint16(*(SRV.Priority)), Weight: uint16(*(SRV.Weight)), @@ -181,24 +184,24 @@ func updateZoneFromPublicResourceSet(recordSet publicdns.RecordSetListResultPage } } - if result.RecordSetProperties.TxtRecords != nil { - for _, TXT := range *(result.RecordSetProperties.TxtRecords) { + if result.TxtRecords != nil { + for _, TXT := range *(result.TxtRecords) { txt := &dns.TXT{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: resultTTL}, Txt: *(TXT.Value)} newZ.Insert(txt) } } - if result.RecordSetProperties.NsRecords != nil { - for _, NS := range *(result.RecordSetProperties.NsRecords) { + if result.NsRecords != nil { + for _, NS := range *(result.NsRecords) { ns := &dns.NS{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: resultTTL}, Ns: *(NS.Nsdname)} newZ.Insert(ns) } } - if result.RecordSetProperties.SoaRecord != nil { - SOA := result.RecordSetProperties.SoaRecord + if result.SoaRecord != nil { + SOA := result.SoaRecord soa := &dns.SOA{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: resultTTL}, Minttl: uint32(*(SOA.MinimumTTL)), Expire: uint32(*(SOA.ExpireTime)), @@ -210,8 +213,8 @@ func updateZoneFromPublicResourceSet(recordSet publicdns.RecordSetListResultPage newZ.Insert(soa) } - if result.RecordSetProperties.CnameRecord != nil { - CNAME := result.RecordSetProperties.CnameRecord.Cname + if result.CnameRecord != nil { + CNAME := result.CnameRecord.Cname cname := &dns.CNAME{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: resultTTL}, Target: dns.Fqdn(*CNAME)} newZ.Insert(cname) @@ -221,25 +224,25 @@ func updateZoneFromPublicResourceSet(recordSet publicdns.RecordSetListResultPage func updateZoneFromPrivateResourceSet(recordSet privatedns.RecordSetListResultPage, newZ *file.Zone) { for _, result := range *(recordSet.Response().Value) { - resultFqdn := *(result.RecordSetProperties.Fqdn) - resultTTL := uint32(*(result.RecordSetProperties.TTL)) - if result.RecordSetProperties.ARecords != nil { - for _, A := range *(result.RecordSetProperties.ARecords) { + resultFqdn := *(result.Fqdn) + resultTTL := uint32(*(result.TTL)) + if result.ARecords != nil { + for _, A := range *(result.ARecords) { a := &dns.A{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: resultTTL}, A: net.ParseIP(*(A.Ipv4Address))} newZ.Insert(a) } } - if result.RecordSetProperties.AaaaRecords != nil { - for _, AAAA := range *(result.RecordSetProperties.AaaaRecords) { + if result.AaaaRecords != nil { + for _, AAAA := range *(result.AaaaRecords) { aaaa := &dns.AAAA{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: resultTTL}, AAAA: net.ParseIP(*(AAAA.Ipv6Address))} newZ.Insert(aaaa) } } - if result.RecordSetProperties.MxRecords != nil { - for _, MX := range *(result.RecordSetProperties.MxRecords) { + if result.MxRecords != nil { + for _, MX := range *(result.MxRecords) { mx := &dns.MX{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: resultTTL}, Preference: uint16(*(MX.Preference)), Mx: dns.Fqdn(*(MX.Exchange))} @@ -247,16 +250,16 @@ func updateZoneFromPrivateResourceSet(recordSet privatedns.RecordSetListResultPa } } - if result.RecordSetProperties.PtrRecords != nil { - for _, PTR := range *(result.RecordSetProperties.PtrRecords) { + if result.PtrRecords != nil { + for _, PTR := range *(result.PtrRecords) { ptr := &dns.PTR{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: resultTTL}, Ptr: dns.Fqdn(*(PTR.Ptrdname))} newZ.Insert(ptr) } } - if result.RecordSetProperties.SrvRecords != nil { - for _, SRV := range *(result.RecordSetProperties.SrvRecords) { + if result.SrvRecords != nil { + for _, SRV := range *(result.SrvRecords) { srv := &dns.SRV{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: resultTTL}, Priority: uint16(*(SRV.Priority)), Weight: uint16(*(SRV.Weight)), @@ -266,16 +269,16 @@ func updateZoneFromPrivateResourceSet(recordSet privatedns.RecordSetListResultPa } } - if result.RecordSetProperties.TxtRecords != nil { - for _, TXT := range *(result.RecordSetProperties.TxtRecords) { + if result.TxtRecords != nil { + for _, TXT := range *(result.TxtRecords) { txt := &dns.TXT{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: resultTTL}, Txt: *(TXT.Value)} newZ.Insert(txt) } } - if result.RecordSetProperties.SoaRecord != nil { - SOA := result.RecordSetProperties.SoaRecord + if result.SoaRecord != nil { + SOA := result.SoaRecord soa := &dns.SOA{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: resultTTL}, Minttl: uint32(*(SOA.MinimumTTL)), Expire: uint32(*(SOA.ExpireTime)), @@ -287,13 +290,12 @@ func updateZoneFromPrivateResourceSet(recordSet privatedns.RecordSetListResultPa newZ.Insert(soa) } - if result.RecordSetProperties.CnameRecord != nil { - CNAME := result.RecordSetProperties.CnameRecord.Cname + if result.CnameRecord != nil { + CNAME := result.CnameRecord.Cname cname := &dns.CNAME{Hdr: dns.RR_Header{Name: resultFqdn, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: resultTTL}, Target: dns.Fqdn(*CNAME)} newZ.Insert(cname) } - } } diff --git a/plugin/azure/azure_test.go b/plugin/azure/azure_test.go index d006f196bc..0178300012 100644 --- a/plugin/azure/azure_test.go +++ b/plugin/azure/azure_test.go @@ -77,18 +77,18 @@ func TestAzure(t *testing.T) { expectedErr error }{ { - qname: "example.org.", - qtype: dns.TypeA, + qname: "example.org.", + qtype: dns.TypeA, wantAnswer: []string{"example.org. 300 IN A 1.2.3.4"}, }, { - qname: "example.org", - qtype: dns.TypeAAAA, + qname: "example.org", + qtype: dns.TypeAAAA, wantAnswer: []string{"example.org. 300 IN AAAA 2001:db8:85a3::8a2e:370:7334"}, }, { - qname: "example.org", - qtype: dns.TypeSOA, + qname: "example.org", + qtype: dns.TypeSOA, wantAnswer: []string{"org. 300 IN SOA ns1-06.azure-dns.com. azuredns-hostmaster.microsoft.com. 1 3600 300 2419200 300"}, }, { @@ -98,38 +98,38 @@ func TestAzure(t *testing.T) { wantMsgRCode: dns.RcodeServerFailure, }, { - qname: "example.gov", - qtype: dns.TypeA, + qname: "example.gov", + qtype: dns.TypeA, wantAnswer: []string{"example.gov. 300 IN A 2.4.6.8"}, }, { - qname: "example.org", - qtype: dns.TypeSRV, + qname: "example.org", + qtype: dns.TypeSRV, wantAnswer: []string{"example.org. 300 IN SRV 1 10 5269 srv-1.example.com.", "example.org. 300 IN SRV 1 10 5269 srv-2.example.com."}, }, { - qname: "cname.example.org.", - qtype: dns.TypeCNAME, + qname: "cname.example.org.", + qtype: dns.TypeCNAME, wantAnswer: []string{"cname.example.org. 300 IN CNAME example.org."}, }, { - qname: "cname.example.org.", - qtype: dns.TypeA, + qname: "cname.example.org.", + qtype: dns.TypeA, wantAnswer: []string{"cname.example.org. 300 IN CNAME example.org.", "example.org. 300 IN A 1.2.3.4"}, }, { - qname: "mail.example.org.", - qtype: dns.TypeMX, + qname: "mail.example.org.", + qtype: dns.TypeMX, wantAnswer: []string{"mail.example.org. 300 IN MX 10 mailserver.example.com."}, }, { - qname: "ptr.example.org.", - qtype: dns.TypePTR, + qname: "ptr.example.org.", + qtype: dns.TypePTR, wantAnswer: []string{"ptr.example.org. 300 IN PTR www.ptr-example.com."}, }, { - qname: "txt.example.org.", - qtype: dns.TypeTXT, + qname: "txt.example.org.", + qtype: dns.TypeTXT, wantAnswer: []string{"txt.example.org. 300 IN TXT \"TXT for example.org\""}, }, } @@ -145,7 +145,7 @@ func TestAzure(t *testing.T) { t.Fatalf("Test %d: Expected error %v, but got %v", ti, tc.expectedErr, err) } - if code != int(tc.wantRetCode) { + if code != tc.wantRetCode { t.Fatalf("Test %d: Expected returned status code %s, but got %s", ti, dns.RcodeToString[tc.wantRetCode], dns.RcodeToString[code]) } diff --git a/plugin/azure/setup.go b/plugin/azure/setup.go index 302555f11e..f089e52271 100644 --- a/plugin/azure/setup.go +++ b/plugin/azure/setup.go @@ -29,20 +29,24 @@ func setup(c *caddy.Controller) error { publicDNSClient := publicAzureDNS.NewRecordSetsClient(env.Values[auth.SubscriptionID]) if publicDNSClient.Authorizer, err = env.GetAuthorizer(); err != nil { + cancel() return plugin.Error("azure", err) } privateDNSClient := privateAzureDNS.NewRecordSetsClient(env.Values[auth.SubscriptionID]) if privateDNSClient.Authorizer, err = env.GetAuthorizer(); err != nil { + cancel() return plugin.Error("azure", err) } h, err := New(ctx, publicDNSClient, privateDNSClient, keys, accessMap) if err != nil { + cancel() return plugin.Error("azure", err) } h.Fall = fall if err := h.Run(ctx); err != nil { + cancel() return plugin.Error("azure", err) } @@ -69,7 +73,7 @@ func parse(c *caddy.Controller) (auth.EnvironmentSettings, map[string][]string, for c.Next() { args := c.RemainingArgs() - for i := 0; i < len(args); i++ { + for i := range args { parts := strings.SplitN(args[i], ":", 2) if len(parts) != 2 { return env, resourceGroupMapping, accessMap, fall, c.Errf("invalid resource group/zone: %q", args[i]) diff --git a/plugin/backend_lookup.go b/plugin/backend_lookup.go index 5e08efb78b..b9b1022da8 100644 --- a/plugin/backend_lookup.go +++ b/plugin/backend_lookup.go @@ -13,28 +13,30 @@ import ( "github.com/miekg/dns" ) +const maxCnameChainLength = 10 + // A returns A records from Backend or an error. -func A(ctx context.Context, b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, err error) { +func A(ctx context.Context, b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, truncated bool, err error) { services, err := checkForApex(ctx, b, zone, state, opt) if err != nil { - return nil, err + return nil, false, err } dup := make(map[string]struct{}) for _, serv := range services { - what, ip := serv.HostType() switch what { case dns.TypeCNAME: if Name(state.Name()).Matches(dns.Fqdn(serv.Host)) { // x CNAME x is a direct loop, don't add those + // in etcd/skydns w.x CNAME x is also direct loop due to the "recursive" nature of search results continue } newRecord := serv.NewCNAME(state.QName(), serv.Host) - if len(previousRecords) > 7 { + if len(previousRecords) > maxCnameChainLength { // don't add it, and just continue continue } @@ -44,7 +46,7 @@ func A(ctx context.Context, b ServiceBackend, zone string, state request.Request if dns.IsSubDomain(zone, dns.Fqdn(serv.Host)) { state1 := state.NewWithQuestion(serv.Host, state.QType()) state1.Zone = zone - nextRecords, err := A(ctx, b, zone, state1, append(previousRecords, newRecord), opt) + nextRecords, tc, err := A(ctx, b, zone, state1, append(previousRecords, newRecord), opt) if err == nil { // Not only have we found something we should add the CNAME and the IP addresses. @@ -53,15 +55,21 @@ func A(ctx context.Context, b ServiceBackend, zone string, state request.Request records = append(records, nextRecords...) } } + if tc { + truncated = true + } continue } // This means we can not complete the CNAME, try to look else where. target := newRecord.Target // Lookup m1, e1 := b.Lookup(ctx, state, target, state.QType()) - if e1 != nil { + if e1 != nil || m1 == nil { continue } + if m1.Truncated { + truncated = true + } // Len(m1.Answer) > 0 here is well? records = append(records, newRecord) records = append(records, m1.Answer...) @@ -77,20 +85,19 @@ func A(ctx context.Context, b ServiceBackend, zone string, state request.Request // nada } } - return records, nil + return records, truncated, nil } // AAAA returns AAAA records from Backend or an error. -func AAAA(ctx context.Context, b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, err error) { +func AAAA(ctx context.Context, b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, truncated bool, err error) { services, err := checkForApex(ctx, b, zone, state, opt) if err != nil { - return nil, err + return nil, false, err } dup := make(map[string]struct{}) for _, serv := range services { - what, ip := serv.HostType() switch what { @@ -98,11 +105,12 @@ func AAAA(ctx context.Context, b ServiceBackend, zone string, state request.Requ // Try to resolve as CNAME if it's not an IP, but only if we don't create loops. if Name(state.Name()).Matches(dns.Fqdn(serv.Host)) { // x CNAME x is a direct loop, don't add those + // in etcd/skydns w.x CNAME x is also direct loop due to the "recursive" nature of search results continue } newRecord := serv.NewCNAME(state.QName(), serv.Host) - if len(previousRecords) > 7 { + if len(previousRecords) > maxCnameChainLength { // don't add it, and just continue continue } @@ -112,7 +120,7 @@ func AAAA(ctx context.Context, b ServiceBackend, zone string, state request.Requ if dns.IsSubDomain(zone, dns.Fqdn(serv.Host)) { state1 := state.NewWithQuestion(serv.Host, state.QType()) state1.Zone = zone - nextRecords, err := AAAA(ctx, b, zone, state1, append(previousRecords, newRecord), opt) + nextRecords, tc, err := AAAA(ctx, b, zone, state1, append(previousRecords, newRecord), opt) if err == nil { // Not only have we found something we should add the CNAME and the IP addresses. @@ -121,14 +129,20 @@ func AAAA(ctx context.Context, b ServiceBackend, zone string, state request.Requ records = append(records, nextRecords...) } } + if tc { + truncated = true + } continue } // This means we can not complete the CNAME, try to look else where. target := newRecord.Target m1, e1 := b.Lookup(ctx, state, target, state.QType()) - if e1 != nil { + if e1 != nil || m1 == nil { continue } + if m1.Truncated { + truncated = true + } // Len(m1.Answer) > 0 here is well? records = append(records, newRecord) records = append(records, m1.Answer...) @@ -145,7 +159,7 @@ func AAAA(ctx context.Context, b ServiceBackend, zone string, state request.Requ } } } - return records, nil + return records, truncated, nil } // SRV returns SRV records from the Backend. @@ -205,12 +219,12 @@ func SRV(ctx context.Context, b ServiceBackend, zone string, state request.Reque if !dns.IsSubDomain(zone, srv.Target) { m1, e1 := b.Lookup(ctx, state, srv.Target, dns.TypeA) - if e1 == nil { + if e1 == nil && m1 != nil { extra = append(extra, m1.Answer...) } m1, e1 = b.Lookup(ctx, state, srv.Target, dns.TypeAAAA) - if e1 == nil { + if e1 == nil && m1 != nil { // If we have seen CNAME's we *assume* that they are already added. for _, a := range m1.Answer { if _, ok := a.(*dns.CNAME); !ok { @@ -223,7 +237,7 @@ func SRV(ctx context.Context, b ServiceBackend, zone string, state request.Reque // Internal name, we should have some info on them, either v4 or v6 // Clients expect a complete answer, because we are a recursor in their view. state1 := state.NewWithQuestion(srv.Target, dns.TypeA) - addr, e1 := A(ctx, b, zone, state1, nil, opt) + addr, _, e1 := A(ctx, b, zone, state1, nil, opt) if e1 == nil { extra = append(extra, addr...) } @@ -272,12 +286,12 @@ func MX(ctx context.Context, b ServiceBackend, zone string, state request.Reques if !dns.IsSubDomain(zone, mx.Mx) { m1, e1 := b.Lookup(ctx, state, mx.Mx, dns.TypeA) - if e1 == nil { + if e1 == nil && m1 != nil { extra = append(extra, m1.Answer...) } m1, e1 = b.Lookup(ctx, state, mx.Mx, dns.TypeAAAA) - if e1 == nil { + if e1 == nil && m1 != nil { // If we have seen CNAME's we *assume* that they are already added. for _, a := range m1.Answer { if _, ok := a.(*dns.CNAME); !ok { @@ -289,7 +303,7 @@ func MX(ctx context.Context, b ServiceBackend, zone string, state request.Reques } // Internal name state1 := state.NewWithQuestion(mx.Mx, dns.TypeA) - addr, e1 := A(ctx, b, zone, state1, nil, opt) + addr, _, e1 := A(ctx, b, zone, state1, nil, opt) if e1 == nil { extra = append(extra, addr...) } @@ -329,28 +343,27 @@ func CNAME(ctx context.Context, b ServiceBackend, zone string, state request.Req } // TXT returns TXT records from Backend or an error. -func TXT(ctx context.Context, b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, err error) { - - services, err := b.Services(ctx, state, true, opt) +func TXT(ctx context.Context, b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, truncated bool, err error) { + services, err := b.Services(ctx, state, false, opt) if err != nil { - return nil, err + return nil, false, err } dup := make(map[string]struct{}) for _, serv := range services { - what, _ := serv.HostType() switch what { case dns.TypeCNAME: if Name(state.Name()).Matches(dns.Fqdn(serv.Host)) { // x CNAME x is a direct loop, don't add those + // in etcd/skydns w.x CNAME x is also direct loop due to the "recursive" nature of search results continue } newRecord := serv.NewCNAME(state.QName(), serv.Host) - if len(previousRecords) > 7 { + if len(previousRecords) > maxCnameChainLength { // don't add it, and just continue continue } @@ -360,8 +373,10 @@ func TXT(ctx context.Context, b ServiceBackend, zone string, state request.Reque if dns.IsSubDomain(zone, dns.Fqdn(serv.Host)) { state1 := state.NewWithQuestion(serv.Host, state.QType()) state1.Zone = zone - nextRecords, err := TXT(ctx, b, zone, state1, append(previousRecords, newRecord), opt) - + nextRecords, tc, err := TXT(ctx, b, zone, state1, append(previousRecords, newRecord), opt) + if tc { + truncated = true + } if err == nil { // Not only have we found something we should add the CNAME and the IP addresses. if len(nextRecords) > 0 { @@ -375,7 +390,7 @@ func TXT(ctx context.Context, b ServiceBackend, zone string, state request.Reque target := newRecord.Target // Lookup m1, e1 := b.Lookup(ctx, state, target, state.QType()) - if e1 != nil { + if e1 != nil || m1 == nil { continue } // Len(m1.Answer) > 0 here is well? @@ -384,15 +399,14 @@ func TXT(ctx context.Context, b ServiceBackend, zone string, state request.Reque continue case dns.TypeTXT: - if _, ok := dup[serv.Host]; !ok { - dup[serv.Host] = struct{}{} - return append(records, serv.NewTXT(state.QName())), nil + if _, ok := dup[serv.Text]; !ok { + dup[serv.Text] = struct{}{} + records = append(records, serv.NewTXT(state.QName())) } - } } - return records, nil + return records, truncated, nil } // PTR returns the PTR records from the backend, only services that have a domain name as host are included. @@ -415,20 +429,20 @@ func PTR(ctx context.Context, b ServiceBackend, zone string, state request.Reque return records, nil } -// NS returns NS records from the backend +// NS returns NS records from the backend func NS(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, err error) { - // NS record for this zone live in a special place, ns.dns.. Fake our lookup. - // only a tad bit fishy... + // NS record for this zone lives in a special place, ns.dns.. Fake our lookup. + // Only a tad bit fishy... old := state.QName() state.Clear() state.Req.Question[0].Name = dnsutil.Join("ns.dns.", zone) services, err := b.Services(ctx, state, false, opt) + // reset the query name to the original + state.Req.Question[0].Name = old if err != nil { return nil, nil, err } - // ... and reset - state.Req.Question[0].Name = old seen := map[string]bool{} @@ -440,8 +454,8 @@ func NS(ctx context.Context, b ServiceBackend, zone string, state request.Reques case dns.TypeA, dns.TypeAAAA: serv.Host = msg.Domain(serv.Key) - extra = append(extra, newAddress(serv, serv.Host, ip, what)) ns := serv.NewNS(state.QName()) + extra = append(extra, newAddress(serv, ns.Ns, ip, what)) if _, ok := seen[ns.Ns]; ok { continue } @@ -455,10 +469,7 @@ func NS(ctx context.Context, b ServiceBackend, zone string, state request.Reques // SOA returns a SOA record from the backend. func SOA(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) ([]dns.RR, error) { minTTL := b.MinTTL(state) - ttl := uint32(300) - if minTTL < ttl { - ttl = minTTL - } + ttl := min(minTTL, uint32(300)) header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: ttl, Class: dns.ClassINET} @@ -490,7 +501,6 @@ func BackendError(ctx context.Context, b ServiceBackend, zone string, rcode int, } func newAddress(s msg.Service, name string, ip net.IP, what uint16) dns.RR { - hdr := dns.RR_Header{Name: name, Rrtype: what, Class: dns.ClassINET, Ttl: s.TTL} if what == dns.TypeA { diff --git a/plugin/backend_lookup_test.go b/plugin/backend_lookup_test.go new file mode 100644 index 0000000000..604a610f40 --- /dev/null +++ b/plugin/backend_lookup_test.go @@ -0,0 +1,606 @@ +package plugin + +import ( + "context" + "fmt" + "net" + "testing" + "time" + + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// mockBackend implements ServiceBackend interface for testing +var _ ServiceBackend = &mockBackend{} + +type mockBackend struct { + mockServices func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) + mockReverse func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) + mockLookup func(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) + mockRecords func(ctx context.Context, state request.Request, exact bool) ([]msg.Service, error) + minTTL uint32 + serial uint32 +} + +func (m *mockBackend) Serial(state request.Request) uint32 { + if m.serial == 0 { + return uint32(time.Now().Unix()) + } + return m.serial +} + +func (m *mockBackend) MinTTL(state request.Request) uint32 { + if m.minTTL == 0 { + return 30 + } + return m.minTTL +} + +func (m *mockBackend) Services(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return m.mockServices(ctx, state, exact, opt) +} + +func (m *mockBackend) Reverse(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return m.mockReverse(ctx, state, exact, opt) +} + +func (m *mockBackend) Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) { + return m.mockLookup(ctx, state, name, typ) +} + +func (m *mockBackend) IsNameError(err error) bool { + return false +} + +func (m *mockBackend) Records(ctx context.Context, state request.Request, exact bool) ([]msg.Service, error) { + return m.mockRecords(ctx, state, exact) +} + +func TestNSStateReset(t *testing.T) { + // Create a mock backend that always returns error + mock := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return nil, fmt.Errorf("mock error") + }, + } + // Create a test request + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeNS) + state := request.Request{ + Req: req, + W: &test.ResponseWriter{}, + } + + originalName := state.QName() + ctx := context.TODO() + + // Call NS function which should fail due to mock error + records, extra, err := NS(ctx, mock, "example.org.", state, Options{}) + + // Verify error is returned + if err == nil { + t.Error("Expected error from mock backend, got nil") + } + + // Verify query name is reset even when an error occurs + if state.QName() != originalName { + t.Errorf("Query name not properly reset after error. Expected %s, got %s", originalName, state.QName()) + } + + // Verify no records are returned + if len(records) != 0 || len(extra) != 0 { + t.Error("Expected no records returned on error") + } +} + +func TestARecords_Dedup(t *testing.T) { + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return []msg.Service{ + {Host: "1.2.3.4", TTL: 60}, + {Host: "1.2.3.4", TTL: 60}, + {Host: "::1", TTL: 60}, + }, nil + }, + } + req := new(dns.Msg) + req.SetQuestion("a.example.org.", dns.TypeA) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, tc, err := A(context.Background(), b, "example.org.", state, nil, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tc { + t.Fatal("unexpected truncation") + } + if len(recs) != 1 { + t.Fatalf("expected 1 A record, got %d", len(recs)) + } + if _, ok := recs[0].(*dns.A); !ok { + t.Fatalf("expected A record, got %T", recs[0]) + } +} + +func TestAAAARecords_Dedup(t *testing.T) { + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return []msg.Service{ + {Host: "::1", TTL: 60}, + {Host: "::1", TTL: 60}, + {Host: "1.2.3.4", TTL: 60}, + }, nil + }, + } + req := new(dns.Msg) + req.SetQuestion("aaaa.example.org.", dns.TypeAAAA) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, tc, err := AAAA(context.Background(), b, "example.org.", state, nil, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tc { + t.Fatal("unexpected truncation") + } + if len(recs) != 1 { + t.Fatalf("expected 1 AAAA record, got %d", len(recs)) + } + if _, ok := recs[0].(*dns.AAAA); !ok { + t.Fatalf("expected AAAA record, got %T", recs[0]) + } +} + +func TestTXTWithInternalCNAME(t *testing.T) { + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + switch state.QName() { + case "txt.example.org.": + return []msg.Service{{Host: "target.example.org.", TTL: 50}}, nil + case "target.example.org.": + return []msg.Service{{Text: "v=txt1", TTL: 40}}, nil + default: + return nil, nil + } + }, + } + req := new(dns.Msg) + req.SetQuestion("txt.example.org.", dns.TypeTXT) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, tc, err := TXT(context.Background(), b, "example.org.", state, nil, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tc { + t.Fatal("unexpected truncation") + } + if len(recs) != 2 { + t.Fatalf("expected 2 records (CNAME+TXT), got %d", len(recs)) + } + if _, ok := recs[0].(*dns.CNAME); !ok { + t.Fatalf("expected first record CNAME, got %T", recs[0]) + } + if _, ok := recs[1].(*dns.TXT); !ok { + t.Fatalf("expected second record TXT, got %T", recs[1]) + } +} + +func TestCNAMEHostIsNameAndIpIgnored(t *testing.T) { + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return []msg.Service{ + {Host: "target.example.org.", TTL: 50}, + {Host: "1.2.3.4", TTL: 50}, + }, nil + }, + } + req := new(dns.Msg) + req.SetQuestion("name.example.org.", dns.TypeCNAME) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, err := CNAME(context.Background(), b, "example.org.", state, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(recs) != 1 { + t.Fatalf("expected 1 CNAME, got %d", len(recs)) + } + if _, ok := recs[0].(*dns.CNAME); !ok { + t.Fatalf("expected CNAME, got %T", recs[0]) + } +} + +func TestCNAMEChainLimitAndLoop(t *testing.T) { + // Construct internal CNAME chain longer than maxCnameChainLength and ensure truncation of chain + chainLength := maxCnameChainLength + 2 + names := make([]string, 0, chainLength) + for i := range chainLength { + names = append(names, fmt.Sprintf("c%d.example.org.", i)) + } + chain := map[string]string{} + for i := range len(names) - 1 { + chain[names[i]] = names[i+1] + } + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + if nxt, ok := chain[state.QName()]; ok { + return []msg.Service{{Host: nxt, TTL: 10}}, nil + } + return []msg.Service{}, nil + }, + } + // A query should follow the chain only until limit; since no terminal A, result is 0 or just below limit + req := new(dns.Msg) + req.SetQuestion(names[0], dns.TypeA) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, _, err := A(context.Background(), b, "example.org.", state, nil, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // We cannot exceed limit; if any records exist they must be <= maxCnameChainLength + if len(recs) > maxCnameChainLength { + t.Fatalf("CNAME chain exceeded limit: %d > %d", len(recs), maxCnameChainLength) + } + + // Now create a direct loop: qname CNAME qname should be ignored + b2 := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return []msg.Service{{Host: state.QName(), TTL: 10}}, nil + }, + } + req2 := new(dns.Msg) + req2.SetQuestion("loop.example.org.", dns.TypeA) + state2 := request.Request{Req: req2, W: &test.ResponseWriter{}} + recs2, _, err := A(context.Background(), b2, "example.org.", state2, nil, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(recs2) != 0 { + t.Fatalf("expected 0 records due to CNAME self-loop, got %d", len(recs2)) + } +} + +func TestAWithExternalCNAMELookupTruncated(t *testing.T) { + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return []msg.Service{{Host: "alias.external."}}, nil + }, + mockLookup: func(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) { + if name != "alias.external." || typ != dns.TypeA { + t.Fatalf("unexpected mockLookup: %s %d", name, typ) + } + m := new(dns.Msg) + m.Truncated = true + a := &dns.A{Hdr: dns.RR_Header{Name: "alias.external.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 10}, A: net.ParseIP("1.2.3.4")} + m.Answer = []dns.RR{a} + return m, nil + }, + } + req := new(dns.Msg) + req.SetQuestion("cname.example.org.", dns.TypeA) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, tc, err := A(context.Background(), b, "example.org.", state, nil, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !tc { + t.Fatal("expected truncation true") + } + if len(recs) != 2 { + t.Fatalf("expected 2 records (CNAME+A), got %d", len(recs)) + } + if _, ok := recs[0].(*dns.CNAME); !ok { + t.Fatalf("expected first record CNAME, got %T", recs[0]) + } + if _, ok := recs[1].(*dns.A); !ok { + t.Fatalf("expected second record A, got %T", recs[1]) + } +} + +func TestAAAAWithInternalCNAMEChain(t *testing.T) { + // Build a short internal chain: n0 -> n1 -> final (AAAA) + names := []string{"c0.example.org.", "c1.example.org.", "final.example.org."} + chain := map[string]string{names[0]: names[1], names[1]: names[2]} + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + if nxt, ok := chain[state.QName()]; ok { + return []msg.Service{{Host: nxt, TTL: 10}}, nil + } + if state.QName() == names[2] { + return []msg.Service{{Host: "::1", TTL: 20}}, nil + } + return nil, nil + }, + } + req := new(dns.Msg) + req.SetQuestion(names[0], dns.TypeAAAA) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, tc, err := AAAA(context.Background(), b, "example.org.", state, nil, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tc { + t.Fatal("unexpected truncation") + } + if len(recs) != 3 { + t.Fatalf("expected 2 CNAMEs + 1 AAAA, got %d", len(recs)) + } + if _, ok := recs[0].(*dns.CNAME); !ok { + t.Fatalf("expected first record CNAME, got %T", recs[0]) + } + if _, ok := recs[1].(*dns.CNAME); !ok { + t.Fatalf("expected second record CNAME, got %T", recs[1]) + } + if aaaa, ok := recs[2].(*dns.AAAA); !ok || !aaaa.AAAA.Equal(net.ParseIP("::1")) { + t.Fatalf("expected final AAAA with ::1, got %T %v", recs[2], recs[2]) + } +} + +func TestAAAAWithExternalCNAMELookupTruncated(t *testing.T) { + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return []msg.Service{{Host: "alias.external."}}, nil + }, + mockLookup: func(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) { + if name != "alias.external." || typ != dns.TypeAAAA { + t.Fatalf("unexpected mockLookup: %s %d", name, typ) + } + m := new(dns.Msg) + m.Truncated = true + aaaa := &dns.AAAA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 10}, AAAA: net.ParseIP("::1")} + m.Answer = []dns.RR{aaaa} + return m, nil + }, + } + req := new(dns.Msg) + req.SetQuestion("cname6.example.org.", dns.TypeAAAA) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, tc, err := AAAA(context.Background(), b, "example.org.", state, nil, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !tc { + t.Fatal("expected truncation true") + } + if len(recs) != 2 { + t.Fatalf("expected 2 records (CNAME+AAAA), got %d", len(recs)) + } + if _, ok := recs[0].(*dns.CNAME); !ok { + t.Fatalf("expected first record CNAME, got %T", recs[0]) + } + if _, ok := recs[1].(*dns.AAAA); !ok { + t.Fatalf("expected second record AAAA, got %T", recs[1]) + } +} + +func TestPTRDomainOnly(t *testing.T) { + b := &mockBackend{ + mockReverse: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return []msg.Service{ + {Host: "name.example.org.", TTL: 20}, + {Host: "1.2.3.4", TTL: 20}, + }, nil + }, + } + req := new(dns.Msg) + req.SetQuestion("4.3.2.1.in-addr.arpa.", dns.TypePTR) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, err := PTR(context.Background(), b, "in-addr.arpa.", state, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(recs) != 1 { + t.Fatalf("expected 1 PTR, got %d", len(recs)) + } + if ptr, ok := recs[0].(*dns.PTR); !ok || ptr.Ptr != "name.example.org." { + t.Fatalf("unexpected PTR: %T %v", recs[0], recs[0]) + } +} + +func TestNSSuccess(t *testing.T) { + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return []msg.Service{ + {Host: "1.2.3.4", TTL: 30, Key: "/skydns/org/example/ns1"}, + {Host: "::1", TTL: 30, Key: "/skydns/org/example/ns2"}, + }, nil + }, + } + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeNS) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, extra, err := NS(context.Background(), b, "example.org.", state, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(recs) != 2 { + t.Fatalf("expected 2 NS records, got %d", len(recs)) + } + if len(extra) != 2 { + t.Fatalf("expected 2 extra address records, got %d", len(extra)) + } +} + +func TestSRVAddressesAndExternalLookup(t *testing.T) { + // First service is IP host -> produces SRV + extra address; second is CNAME target -> triggers external lookup + lookedUp := false + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return []msg.Service{ + {Host: "1.2.3.4", Port: 80, Priority: 10, Weight: 5, TTL: 30, Key: "/skydns/org/example/s1"}, + {Host: "alias.external.", Port: 80, Priority: 10, Weight: 5, TTL: 30, Key: "/skydns/org/example/s2"}, + }, nil + }, + mockLookup: func(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) { + if name == "alias.external." && (typ == dns.TypeA || typ == dns.TypeAAAA) { + lookedUp = true + m := new(dns.Msg) + if typ == dns.TypeA { + m.Answer = []dns.RR{&dns.A{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 10}, A: net.ParseIP("5.6.7.8")}} + } else { + m.Answer = []dns.RR{&dns.AAAA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 10}, AAAA: net.ParseIP("::1")}} + } + return m, nil + } + return &dns.Msg{}, nil + }, + } + req := new(dns.Msg) + req.SetQuestion("_sip._tcp.example.org.", dns.TypeSRV) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, extra, err := SRV(context.Background(), b, "example.org.", state, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(recs) != 2 { + t.Fatalf("expected 2 SRV records, got %d", len(recs)) + } + if len(extra) == 0 { + t.Fatalf("expected extra address records") + } + if !lookedUp { + t.Fatalf("expected external lookup for alias.external.") + } +} + +func TestMXInternalAndExternalTargets(t *testing.T) { + lookedUp := false + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + return []msg.Service{ + {Host: "1.2.3.4", Mail: true, Priority: 10, TTL: 60, Key: "/skydns/org/example/mx1"}, + {Host: "alias.external.", Mail: true, Priority: 20, TTL: 60, Key: "/skydns/org/example/mx2"}, + }, nil + }, + mockLookup: func(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) { + if name == "alias.external." && (typ == dns.TypeA || typ == dns.TypeAAAA) { + lookedUp = true + m := new(dns.Msg) + if typ == dns.TypeA { + m.Answer = []dns.RR{&dns.A{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 10}, A: net.ParseIP("9.9.9.9")}} + } else { + m.Answer = []dns.RR{&dns.AAAA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 10}, AAAA: net.ParseIP("::2")}} + } + return m, nil + } + return &dns.Msg{}, nil + }, + } + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeMX) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, extra, err := MX(context.Background(), b, "example.org.", state, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(recs) != 2 { + t.Fatalf("expected 2 MX records, got %d", len(recs)) + } + if len(extra) == 0 { + t.Fatalf("expected extra address records for MX") + } + if !lookedUp { + t.Fatalf("expected external lookup for alias.external.") + } +} + +func TestSOA(t *testing.T) { + b := &mockBackend{minTTL: 30, serial: 1234} + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeSOA) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + recs, err := SOA(context.Background(), b, "example.org.", state, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(recs) != 1 { + t.Fatalf("expected 1 SOA, got %d", len(recs)) + } + soa, ok := recs[0].(*dns.SOA) + if !ok { + t.Fatalf("expected SOA, got %T", recs[0]) + } + if soa.Hdr.Ttl != 30 { + t.Fatalf("expected ttl 30, got %d", soa.Hdr.Ttl) + } + if soa.Ns != "ns.dns.example.org." { + t.Fatalf("unexpected NS in SOA: %s", soa.Ns) + } + if soa.Mbox != "hostmaster.example.org." { + t.Fatalf("unexpected Mbox in SOA: %s", soa.Mbox) + } +} + +func TestBackendError(t *testing.T) { + b := &mockBackend{} + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + w := &test.ResponseWriter{} + state := request.Request{Req: req, W: w} + code, err := BackendError(context.Background(), b, "example.org.", dns.RcodeServerFailure, state, fmt.Errorf("err"), Options{}) + if code != dns.RcodeSuccess { + t.Fatalf("expected RcodeSuccess, got %d", code) + } + if err == nil { + t.Fatal("expected error to be returned") + } +} + +func TestCheckForApex(t *testing.T) { + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + if state.QName() == "apex.dns.example.org." { + return []msg.Service{{Host: "1.2.3.4", TTL: 10}}, nil + } + return []msg.Service{{Host: "::1", TTL: 20}}, nil + }, + } + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + services, err := checkForApex(context.Background(), b, "example.org.", state, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(services) != 1 || services[0].Host != "1.2.3.4" { + t.Fatalf("expected apex services, got %+v", services) + } +} + +func TestCheckForApexFallback(t *testing.T) { + b := &mockBackend{ + mockServices: func(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) { + if state.QName() == "apex.dns.example.org." { + return nil, dns.ErrRcode + } + return []msg.Service{{Host: "::1", TTL: 20}}, nil + }, + } + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + state := request.Request{Req: req, W: &test.ResponseWriter{}} + services, err := checkForApex(context.Background(), b, "example.org.", state, Options{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(services) != 1 || services[0].Host != "::1" { + t.Fatalf("expected fallback services, got %+v", services) + } +} + +func TestIsDuplicate(t *testing.T) { + m := map[item]struct{}{} + if isDuplicate(m, "name.", "", 53) { + t.Fatal("unexpected duplicate on first insert (port)") + } + if !isDuplicate(m, "name.", "", 53) { + t.Fatal("expected duplicate on second insert (port)") + } + if isDuplicate(m, "name.", "1.2.3.4", 0) { + t.Fatal("unexpected duplicate on first insert (addr)") + } + if !isDuplicate(m, "name.", "1.2.3.4", 0) { + t.Fatal("expected duplicate on second insert (addr)") + } +} diff --git a/plugin/bind/README.md b/plugin/bind/README.md index 761aa056a2..fddbff741b 100644 --- a/plugin/bind/README.md +++ b/plugin/bind/README.md @@ -11,17 +11,31 @@ another IP instead. If several addresses are provided, a listener will be open on each of the IP provided. -Each address has to be an IP of one of the interfaces of the host. +Each address has to be an IP or name of one of the interfaces of the host. Bind by interface name, binds to the IPs on that interface at the time of startup or reload (reload will happen with a SIGHUP or if the config file changes). + +If the given argument is an interface name, and that interface has several IP addresses, CoreDNS will listen on all of the interface IP addresses (including IPv4 and IPv6). ## Syntax +In its basic form, a simple bind uses this syntax: + ~~~ txt -bind ADDRESS ... +bind ADDRESS|IFACE ... +~~~ + +You can also exclude some addresses with their IP address or interface name in expanded syntax: + +~~~ +bind ADDRESS|IFACE ... { + except ADDRESS|IFACE ... +} ~~~ -**ADDRESS** is an IP address to bind to. -When several addresses are provided a listener will be opened on each of the addresses. + +* **ADDRESS|IFACE** is an IP address or interface name to bind to. +When several addresses are provided a listener will be opened on each of the addresses. Please read the *Description* for more details. +* `except`, excludes interfaces or IP addresses to bind to. `except` option only excludes addresses for the current `bind` directive if multiple `bind` directives are used in the same server block. ## Examples To make your socket accessible only to that machine, bind to IP 127.0.0.1 (localhost): @@ -50,12 +64,37 @@ The following sample is equivalent to the preceding: } ~~~ +The following server block, binds on localhost with its interface name (both "127.0.0.1" and "::1"): + +~~~ corefile +. { + bind lo +} +~~~ + +You can exclude some addresses by their IP or interface name (The following will only listen on `::1` or whatever addresses have been assigned to the `lo` interface): + +~~~ corefile +. { + bind lo { + except 127.0.0.1 + } +} +~~~ + ## Bugs -When defining more than one server block, take care not to bind more than one server to the same -address and port. Doing so will result in unpredictable behavior (requests may be randomly -served by either server). Keep in mind that *without* the *bind* plugin, a server will bind to all -interfaces, and this will collide with another server if it's using *bind* to listen to an interface +### Avoiding Listener Contention + +TL;DR, When adding the _bind_ plugin to a server block, it must also be added to all other server blocks that listen on the same port. + +When more than one server block is configured to listen to a common port, those server blocks must either +all use the _bind_ plugin, or all use default binding (no _bind_ plugin). Note that "port" here refers the TCP/UDP port that +a server block is configured to serve (default 53) - not a network interface. For two server blocks listening on the same port, +if one uses the bind plugin and the other does not, two separate listeners will be created that will contend for serving +packets destined to the same address. Doing so will result in unpredictable behavior (requests may be randomly +served by either server). This happens because *without* the *bind* plugin, a server will bind to all +interfaces, and this will collide with another server if it's using *bind* to listen to an address on the same port. For example, the following creates two servers that both listen on 127.0.0.1:53, which would result in unpredictable behavior for queries in `a.bad.example.com`: @@ -68,4 +107,4 @@ a.bad.example.com { bad.example.com { forward . 5.6.7.8 } -``` \ No newline at end of file +``` diff --git a/plugin/bind/bind.go b/plugin/bind/bind.go index cfbd36597a..cada8fa987 100644 --- a/plugin/bind/bind.go +++ b/plugin/bind/bind.go @@ -1,6 +1,17 @@ // Package bind allows binding to a specific interface instead of bind to all of them. package bind -import "github.com/coredns/coredns/plugin" +import ( + "github.com/coredns/coredns/plugin" +) func init() { plugin.Register("bind", setup) } + +type bind struct { + Next plugin.Handler + addrs []string + except []string +} + +// Name implements plugin.Handler. +func (b *bind) Name() string { return "bind" } diff --git a/plugin/bind/setup.go b/plugin/bind/setup.go index 44c9b8a6fb..1e3ac53f06 100644 --- a/plugin/bind/setup.go +++ b/plugin/bind/setup.go @@ -1,31 +1,108 @@ package bind import ( + "errors" "fmt" "net" + "slices" "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/log" ) func setup(c *caddy.Controller) error { config := dnsserver.GetConfig(c) - // addresses will be consolidated over all BIND directives available in that BlocServer all := []string{} + ifaces, err := net.Interfaces() + if err != nil { + log.Warning(plugin.Error("bind", fmt.Errorf("failed to get interfaces list, cannot bind by interface name: %s", err))) + } + for c.Next() { - addrs := c.RemainingArgs() - if len(addrs) == 0 { - return plugin.Error("bind", fmt.Errorf("at least one address is expected")) + b, err := parse(c) + if err != nil { + return plugin.Error("bind", err) + } + + ips, err := listIP(b.addrs, ifaces) + if err != nil { + return plugin.Error("bind", err) + } + + except, err := listIP(b.except, ifaces) + if err != nil { + return plugin.Error("bind", err) } - for _, addr := range addrs { - if net.ParseIP(addr) == nil { - return plugin.Error("bind", fmt.Errorf("not a valid IP address: %s", addr)) + + for _, ip := range ips { + if !slices.Contains(except, ip) { + all = append(all, ip) } } - all = append(all, addrs...) } + config.ListenHosts = all return nil } + +func parse(c *caddy.Controller) (*bind, error) { + b := &bind{} + b.addrs = c.RemainingArgs() + if len(b.addrs) == 0 { + return nil, errors.New("at least one address or interface name is expected") + } + for c.NextBlock() { + switch c.Val() { + case "except": + b.except = c.RemainingArgs() + if len(b.except) == 0 { + return nil, errors.New("at least one address or interface must be given to except subdirective") + } + default: + return nil, fmt.Errorf("invalid option %q", c.Val()) + } + } + return b, nil +} + +// listIP returns a list of IP addresses from a list of arguments which can be either IP-Address or Interface-Name. +func listIP(args []string, ifaces []net.Interface) ([]string, error) { + all := []string{} + var isIface bool + for _, a := range args { + isIface = false + for _, iface := range ifaces { + if a == iface.Name { + isIface = true + addrs, err := iface.Addrs() + if err != nil { + return nil, fmt.Errorf("failed to get the IP addresses of the interface: %q", a) + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + ipa, err := net.ResolveIPAddr("ip", ipnet.IP.String()) + if err == nil { + if ipnet.IP.To4() == nil && + (ipnet.IP.IsLinkLocalMulticast() || ipnet.IP.IsLinkLocalUnicast()) { + if ipa.Zone == "" { + ipa.Zone = iface.Name + } + } + all = append(all, ipa.String()) + } + } + } + } + } + if !isIface { + if net.ParseIP(a) == nil { + return nil, fmt.Errorf("not a valid IP address or interface name: %q", a) + } + all = append(all, a) + } + } + return all, nil +} diff --git a/plugin/bind/setup_test.go b/plugin/bind/setup_test.go index 556aa1ea29..858edf2d18 100644 --- a/plugin/bind/setup_test.go +++ b/plugin/bind/setup_test.go @@ -1,6 +1,7 @@ package bind import ( + "runtime" "testing" "github.com/coredns/caddy" @@ -8,6 +9,12 @@ import ( ) func TestSetup(t *testing.T) { + // Skip on non-Linux systems as some tests refer to for e.g. loopback interfaces which + // are not present on all systems. + if runtime.GOOS != "linux" { + t.Skipf("Skipping bind test on %s", runtime.GOOS) + } + for i, test := range []struct { config string expected []string @@ -19,6 +26,8 @@ func TestSetup(t *testing.T) { {`bind 1.2.3.4 ::5`, []string{"1.2.3.4", "::5"}, false}, {`bind ::1 1.2.3.4 ::5 127.9.9.0`, []string{"::1", "1.2.3.4", "::5", "127.9.9.0"}, false}, {`bind ::1 1.2.3.4 ::5 127.9.9.0 noone`, nil, true}, + {`bind 1.2.3.4 lo`, []string{"1.2.3.4", "127.0.0.1", "::1"}, false}, + {"bind lo {\nexcept 127.0.0.1\n}\n", []string{"::1"}, false}, } { c := caddy.NewTestController("dns", test.config) err := setup(c) diff --git a/plugin/bufsize/README.md b/plugin/bufsize/README.md index 6353307a93..0dc96235ca 100644 --- a/plugin/bufsize/README.md +++ b/plugin/bufsize/README.md @@ -1,10 +1,15 @@ # bufsize ## Name -*bufsize* - sizes EDNS0 buffer size to prevent IP fragmentation. +*bufsize* - limits EDNS0 buffer size to prevent IP fragmentation. ## Description -*bufsize* limits a requester's UDP payload size. +*bufsize* limits a requester's UDP payload size to within a maximum value. +If a request with an OPT RR has a bufsize greater than the limit, the bufsize +of the request will be reduced. Otherwise the request is unaffected. It prevents IP fragmentation, mitigating certain DNS vulnerabilities. +It cannot increase UDP size requested by the client, it can be reduced only. +This will only affect queries that have +an OPT RR ([EDNS(0)](https://www.rfc-editor.org/rfc/rfc6891)). ## Syntax ```txt @@ -12,14 +17,14 @@ bufsize [SIZE] ``` **[SIZE]** is an int value for setting the buffer size. -The default value is 512, and the value must be within 512 - 4096. +The default value is 1232, and the value must be within 512 - 4096. Only one argument is acceptable, and it covers both IPv4 and IPv6. ## Examples Enable limiting the buffer size of outgoing query to the resolver (172.31.0.10): ```corefile . { - bufsize 512 + bufsize 1100 forward . 172.31.0.10 log } @@ -28,7 +33,7 @@ Enable limiting the buffer size of outgoing query to the resolver (172.31.0.10): Enable limiting the buffer size as an authoritative nameserver: ```corefile . { - bufsize 512 + bufsize 1220 file db.example.org log } @@ -36,4 +41,3 @@ Enable limiting the buffer size as an authoritative nameserver: ## Considerations - Setting 1232 bytes to bufsize may avoid fragmentation on the majority of networks in use today, but it depends on the MTU of the physical network links. -- For now, if a client does not use EDNS, this plugin adds OPT RR. diff --git a/plugin/bufsize/bufsize.go b/plugin/bufsize/bufsize.go index 1522be8948..00556c2bab 100644 --- a/plugin/bufsize/bufsize.go +++ b/plugin/bufsize/bufsize.go @@ -1,4 +1,4 @@ -// Package bufsize implements a plugin that modifies EDNS0 buffer size. +// Package bufsize implements a plugin that clamps EDNS0 buffer size preventing packet fragmentation. package bufsize import ( @@ -17,13 +17,9 @@ type Bufsize struct { // ServeDNS implements the plugin.Handler interface. func (buf Bufsize) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { - if option := r.IsEdns0(); option != nil { + if option := r.IsEdns0(); option != nil && int(option.UDPSize()) > buf.Size { option.SetUDPSize(uint16(buf.Size)) - } else { - // If a client does not use EDNS, add it - r.SetEdns0(uint16(buf.Size), false) } - return plugin.NextOrFailure(buf.Name(), buf.Next, ctx, w, r) } diff --git a/plugin/bufsize/bufsize_test.go b/plugin/bufsize/bufsize_test.go index 3d714d2f19..eb267ddbc6 100644 --- a/plugin/bufsize/bufsize_test.go +++ b/plugin/bufsize/bufsize_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/whoami" @@ -12,61 +11,92 @@ import ( ) func TestBufsize(t *testing.T) { - em := Bufsize{ - Size: 512, - } + const maxBufSize = 1024 - tests := []struct { - next plugin.Handler - qname string - inputBufsize uint16 - outgoingBufsize uint16 - expectedErr error - }{ - // This plugin is responsible for limiting outgoing query's bufize - { - next: whoami.Whoami{}, - qname: ".", - inputBufsize: 1200, - outgoingBufsize: 512, - expectedErr: nil, - }, - // If EDNS is not enabled, this plugin adds it - { - next: whoami.Whoami{}, - qname: ".", - outgoingBufsize: 512, - expectedErr: nil, - }, + setUpWithRequestBufsz := func(bufferSize uint16) (Bufsize, *dns.Msg) { + p := Bufsize{ + Size: maxBufSize, + Next: whoami.Whoami{}, + } + r := new(dns.Msg) + r.SetQuestion(dns.Fqdn("."), dns.TypeA) + r.Question[0].Qclass = dns.ClassINET + if bufferSize > 0 { + r.SetEdns0(bufferSize, false) + } + return p, r } - for i, tc := range tests { - req := new(dns.Msg) - req.SetQuestion(dns.Fqdn(tc.qname), dns.TypeA) - req.Question[0].Qclass = dns.ClassINET - em.Next = tc.next + t.Run("Limit response buffer size", func(t *testing.T) { + // GIVEN + // plugin initialized with maximum buffer size + // request has larger buffer size than allowed + p, r := setUpWithRequestBufsz(maxBufSize + 128) + + // WHEN + // request is processed + _, err := p.ServeDNS(context.Background(), &test.ResponseWriter{}, r) - if tc.inputBufsize != 0 { - req.SetEdns0(tc.inputBufsize, false) + // THEN + // no error + // OPT RR present + // request buffer size is limited + if err != nil { + t.Errorf("unexpected error %s", err) } + option := r.IsEdns0() + if option == nil { + t.Errorf("OPT RR not present") + } + if option.UDPSize() != maxBufSize { + t.Errorf("buffer size not limited") + } + }) + + t.Run("Do not increase response buffer size", func(t *testing.T) { + // GIVEN + // plugin initialized with maximum buffer size + // request has smaller buffer size than allowed + const smallerBufferSize = maxBufSize - 128 + p, r := setUpWithRequestBufsz(smallerBufferSize) - _, err := em.ServeDNS(context.Background(), &test.ResponseWriter{}, req) + // WHEN + // request is processed + _, err := p.ServeDNS(context.Background(), &test.ResponseWriter{}, r) - if err != tc.expectedErr { - t.Errorf("Test %d: Expected error is %v, but got %v", i, tc.expectedErr, err) + // THEN + // no error + // request buffer size is not expanded + if err != nil { + t.Errorf("unexpected error %s", err) } + option := r.IsEdns0() + if option == nil { + t.Errorf("OPT RR not present") + } + if option.UDPSize() != smallerBufferSize { + t.Errorf("buffer size should not be increased") + } + }) + + t.Run("Buffer size should not be set", func(t *testing.T) { + // GIVEN + // plugin initialized with maximum buffer size + // request has no EDNS0 option set + p, r := setUpWithRequestBufsz(0) - if tc.outgoingBufsize != 0 { - for _, extra := range req.Extra { - if option, ok := extra.(*dns.OPT); ok { - b := option.UDPSize() - if b != tc.outgoingBufsize { - t.Errorf("Test %d: Expected outgoing bufsize is %d, but got %d", i, tc.outgoingBufsize, b) - } - } else { - t.Errorf("Test %d: Not found OPT RR.", i) - } - } + // WHEN + // request is processed + _, err := p.ServeDNS(context.Background(), &test.ResponseWriter{}, r) + + // THEN + // no error + // OPT RR is not appended + if err != nil { + t.Errorf("unexpected error %s", err) } - } + if r.IsEdns0() != nil { + t.Errorf("EDNS0 enabled for incoming request") + } + }) } diff --git a/plugin/bufsize/setup.go b/plugin/bufsize/setup.go index 7ac602d5d9..56113e6330 100644 --- a/plugin/bufsize/setup.go +++ b/plugin/bufsize/setup.go @@ -24,12 +24,13 @@ func setup(c *caddy.Controller) error { } func parse(c *caddy.Controller) (int, error) { - const defaultBufSize = 512 + // value from http://www.dnsflagday.net/2020/ + const defaultBufSize = 1232 for c.Next() { args := c.RemainingArgs() switch len(args) { case 0: - // Nothing specified; use 512 as default + // Nothing specified; use defaultBufSize return defaultBufSize, nil case 1: // Specified value is needed to verify diff --git a/plugin/bufsize/setup_test.go b/plugin/bufsize/setup_test.go index bb103027d5..5bf7b80959 100644 --- a/plugin/bufsize/setup_test.go +++ b/plugin/bufsize/setup_test.go @@ -14,10 +14,11 @@ func TestSetupBufsize(t *testing.T) { expectedData int expectedErrContent string // substring from the expected error. Empty for positive cases. }{ - {`bufsize`, false, 512, ""}, - {`bufsize "1232"`, false, 1232, ""}, + {`bufsize`, false, 1232, ""}, + {`bufsize "1220"`, false, 1220, ""}, {`bufsize "5000"`, true, -1, "plugin"}, {`bufsize "512 512"`, true, -1, "plugin"}, + {`bufsize "511"`, true, -1, "plugin"}, {`bufsize "abc123"`, true, -1, "plugin"}, } diff --git a/plugin/cache/README.md b/plugin/cache/README.md index 28a427371e..d516a91dbf 100644 --- a/plugin/cache/README.md +++ b/plugin/cache/README.md @@ -10,8 +10,7 @@ With *cache* enabled, all records except zone transfers and metadata records wil 3600s. Caching is mostly useful in a scenario when fetching data from the backend (upstream, database, etc.) is expensive. -*Cache* will change the query to enable DNSSEC (DNSSEC OK; DO) if it passes through the plugin. If -the client didn't request any DNSSEC (records), these are filtered out when replying. +*Cache* will pass DNSSEC (DNSSEC OK; DO) options through the plugin for upstream queries. This plugin can only be used once per Server Block. @@ -37,7 +36,10 @@ cache [TTL] [ZONES...] { success CAPACITY [TTL] [MINTTL] denial CAPACITY [TTL] [MINTTL] prefetch AMOUNT [[DURATION] [PERCENTAGE%]] - serve_stale [DURATION] + serve_stale [DURATION] [REFRESH_MODE] + servfail DURATION + disable success|denial [ZONES...] + keepttl } ~~~ @@ -54,10 +56,25 @@ cache [TTL] [ZONES...] { **DURATION** defaults to 1m. Prefetching will happen when the TTL drops below **PERCENTAGE**, which defaults to `10%`, or latest 1 second before TTL expiration. Values should be in the range `[10%, 90%]`. Note the percent sign is mandatory. **PERCENTAGE** is treated as an `int`. -* `serve_stale`, when serve\_stale is set, cache always will serve an expired entry to a client if there is one - available. When this happens, cache will attempt to refresh the cache entry after sending the expired cache - entry to the client. The responses have a TTL of 0. **DURATION** is how far back to consider - stale responses as fresh. The default duration is 1h. +* `serve_stale`, when serve\_stale is set, cache will always serve an expired entry to a client if there is one + available as long as it has not been expired for longer than **DURATION** (default 1 hour). By default, the _cache_ plugin will + attempt to refresh the cache entry after sending the expired cache entry to the client. The + responses have a TTL of 0. **REFRESH_MODE** controls the timing of the expired cache entry refresh. + `verify` will first verify that an entry is still unavailable from the source before sending the expired entry to the client. + `immediate` will immediately send the expired entry to the client before + checking to see if the entry is available from the source. **REFRESH_MODE** defaults to `immediate`. Setting this + value to `verify` can lead to increased latency when serving stale responses, but will prevent stale entries + from ever being served if an updated response can be retrieved from the source. +* `servfail` cache SERVFAIL responses for **DURATION**. Setting **DURATION** to 0 will disable caching of SERVFAIL + responses. If this option is not set, SERVFAIL responses will be cached for 5 seconds. **DURATION** may not be + greater than 5 minutes. +* `disable` disable the success or denial cache for the listed **ZONES**. If no **ZONES** are given, the specified + cache will be disabled for all zones. +* `keepttl` do not age TTL when serving responses from cache. The entry will still be removed from cache + when the TTL expires as normal, but until it expires responses will include the original TTL instead + of the remaining TTL. This can be useful if CoreDNS is used as an authoritative server and you want + to serve a consistent TTL to downstream clients. This is **NOT** recommended when CoreDNS is caching + records it is not authoritative for because it could result in downstream clients using stale answers. ## Capacity and Eviction @@ -73,12 +90,14 @@ Entries with 0 TTL will remain in the cache until randomly evicted when the shar If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported: -* `coredns_cache_entries{server, type}` - Total elements in the cache by cache type. -* `coredns_cache_hits_total{server, type}` - Counter of cache hits by cache type. -* `coredns_cache_misses_total{server}` - Counter of cache misses. -* `coredns_cache_prefetch_total{server}` - Counter of times the cache has prefetched a cached item. -* `coredns_cache_drops_total{server}` - Counter of responses excluded from the cache due to request/response question name mismatch. -* `coredns_cache_served_stale_total{server}` - Counter of requests served from stale cache entries. +* `coredns_cache_entries{server, type, zones, view}` - Total elements in the cache by cache type. +* `coredns_cache_hits_total{server, type, zones, view}` - Counter of cache hits by cache type. +* `coredns_cache_misses_total{server, zones, view}` - Counter of cache misses. - Deprecated, derive misses from cache hits/requests counters. +* `coredns_cache_requests_total{server, zones, view}` - Counter of cache requests. +* `coredns_cache_prefetch_total{server, zones, view}` - Counter of times the cache has prefetched a cached item. +* `coredns_cache_drops_total{server, zones, view}` - Counter of responses excluded from the cache due to request/response question name mismatch. +* `coredns_cache_served_stale_total{server, zones, view}` - Counter of requests served from stale cache entries. +* `coredns_cache_evictions_total{server, type, zones, view}` - Counter of cache evictions. Cache types are either "denial" or "success". `Server` is the server handling the request, see the prometheus plugin for documentation. @@ -113,3 +132,13 @@ example.org { } } ~~~ + +Enable caching for `example.org`, but do not cache denials in `sub.example.org`: + +~~~ corefile +example.org { + cache { + disable denial sub.example.org + } +} +~~~ diff --git a/plugin/cache/cache.go b/plugin/cache/cache.go index 2a56500a3c..5c5e3dbfe4 100644 --- a/plugin/cache/cache.go +++ b/plugin/cache/cache.go @@ -21,6 +21,9 @@ type Cache struct { Next plugin.Handler Zones []string + zonesMetricLabel string + viewMetricLabel string + ncache *cache.Cache ncap int nttl time.Duration @@ -30,13 +33,23 @@ type Cache struct { pcap int pttl time.Duration minpttl time.Duration + failttl time.Duration // TTL for caching SERVFAIL responses // Prefetch. prefetch int duration time.Duration percentage int - staleUpTo time.Duration + // Stale serve + staleUpTo time.Duration + verifyStale bool + + // Positive/negative zone exceptions + pexcept []string + nexcept []string + + // Keep ttl option + keepttl bool // Testing. now func() time.Time @@ -55,6 +68,7 @@ func New() *Cache { ncache: cache.New(defaultCap), nttl: maxNTTL, minnttl: minNTTL, + failttl: minNTTL, prefetch: 0, duration: 1 * time.Minute, percentage: 10, @@ -65,7 +79,7 @@ func New() *Cache { // key returns key under which we store the item, -1 will be returned if we don't store the message. // Currently we do not cache Truncated, errors zone transfers or dynamic update messages. // qname holds the already lowercased qname. -func key(qname string, m *dns.Msg, t response.Type) (bool, uint64) { +func key(qname string, m *dns.Msg, t response.Type, do, cd bool) (bool, uint64) { // We don't store truncated responses. if m.Truncated { return false, 0 @@ -75,11 +89,27 @@ func key(qname string, m *dns.Msg, t response.Type) (bool, uint64) { return false, 0 } - return true, hash(qname, m.Question[0].Qtype) + return true, hash(qname, m.Question[0].Qtype, do, cd) } -func hash(qname string, qtype uint16) uint64 { +var one = []byte("1") +var zero = []byte("0") + +func hash(qname string, qtype uint16, do, cd bool) uint64 { h := fnv.New64() + + if do { + h.Write(one) + } else { + h.Write(zero) + } + + if cd { + h.Write(one) + } else { + h.Write(zero) + } + h.Write([]byte{byte(qtype >> 8)}) h.Write([]byte{byte(qtype)}) h.Write([]byte(qname)) @@ -87,13 +117,7 @@ func hash(qname string, qtype uint16) uint64 { } func computeTTL(msgTTL, minTTL, maxTTL time.Duration) time.Duration { - ttl := msgTTL - if ttl < minTTL { - ttl = minTTL - } - if ttl > maxTTL { - ttl = maxTTL - } + ttl := min(max(msgTTL, minTTL), maxTTL) return ttl } @@ -105,8 +129,15 @@ type ResponseWriter struct { server string // Server handling the request. do bool // When true the original request had the DO bit set. + cd bool // When true the original request had the CD bit set. + ad bool // When true the original request had the AD bit set. prefetch bool // When true write nothing back to the client. remoteAddr net.Addr + + wildcardFunc func() string // function to retrieve wildcard name that synthesized the result. + + pexcept []string // positive zone exceptions + nexcept []string // negative zone exceptions } // newPrefetchResponseWriter returns a Cache ResponseWriter to be used in @@ -128,6 +159,8 @@ func newPrefetchResponseWriter(server string, state request.Request, c *Cache) * Cache: c, state: state, server: server, + do: state.Do(), + cd: state.Req.CheckingDisabled, prefetch: true, remoteAddr: addr, } @@ -143,30 +176,43 @@ func (w *ResponseWriter) RemoteAddr() net.Addr { // WriteMsg implements the dns.ResponseWriter interface. func (w *ResponseWriter) WriteMsg(res *dns.Msg) error { + res = res.Copy() mt, _ := response.Typify(res, w.now().UTC()) // key returns empty string for anything we don't want to cache. - hasKey, key := key(w.state.Name(), res, mt) + hasKey, key := key(w.state.Name(), res, mt, w.do, w.cd) msgTTL := dnsutil.MinimalTTL(res, mt) var duration time.Duration - if mt == response.NameError || mt == response.NoData { + switch mt { + case response.NameError, response.NoData: duration = computeTTL(msgTTL, w.minnttl, w.nttl) - } else if mt == response.ServerError { - // use default ttl which is 5s - duration = minTTL - } else { + case response.ServerError: + duration = w.failttl + default: duration = computeTTL(msgTTL, w.minpttl, w.pttl) } + // Apply capped TTL to this reply to avoid jarring TTL experience 1799 -> 8 (e.g.) + ttl := uint32(duration.Seconds()) + res.Answer = filterRRSlice(res.Answer, ttl, false) + res.Ns = filterRRSlice(res.Ns, ttl, false) + res.Extra = filterRRSlice(res.Extra, ttl, false) + + if !w.do && !w.ad { + // unset AD bit if requester is not OK with DNSSEC + // But retain AD bit if requester set the AD bit in the request, per RFC6840 5.7-5.8 + res.AuthenticatedData = false + } + if hasKey && duration > 0 { if w.state.Match(res) { w.set(res, key, mt, duration) - cacheSize.WithLabelValues(w.server, Success).Set(float64(w.pcache.Len())) - cacheSize.WithLabelValues(w.server, Denial).Set(float64(w.ncache.Len())) + cacheSize.WithLabelValues(w.server, Success, w.zonesMetricLabel, w.viewMetricLabel).Set(float64(w.pcache.Len())) + cacheSize.WithLabelValues(w.server, Denial, w.zonesMetricLabel, w.viewMetricLabel).Set(float64(w.ncache.Len())) } else { // Don't log it, but increment counter - cacheDrops.WithLabelValues(w.server).Inc() + cacheDrops.WithLabelValues(w.server, w.zonesMetricLabel, w.viewMetricLabel).Inc() } } @@ -174,13 +220,6 @@ func (w *ResponseWriter) WriteMsg(res *dns.Msg) error { return nil } - // Apply capped TTL to this reply to avoid jarring TTL experience 1799 -> 8 (e.g.) - // We also may need to filter out DNSSEC records, see toMsg() for similar code. - ttl := uint32(duration.Seconds()) - res.Answer = filterRRSlice(res.Answer, ttl, w.do, false) - res.Ns = filterRRSlice(res.Ns, ttl, w.do, false) - res.Extra = filterRRSlice(res.Extra, ttl, w.do, false) - return w.ResponseWriter.WriteMsg(res) } @@ -189,16 +228,34 @@ func (w *ResponseWriter) set(m *dns.Msg, key uint64, mt response.Type, duration // and key is valid switch mt { case response.NoError, response.Delegation: + if plugin.Zones(w.pexcept).Matches(m.Question[0].Name) != "" { + // zone is in exception list, do not cache + return + } i := newItem(m, w.now(), duration) - w.pcache.Add(key, i) + if w.wildcardFunc != nil { + i.wildcard = w.wildcardFunc() + } + if w.pcache.Add(key, i) { + evictions.WithLabelValues(w.server, Success, w.zonesMetricLabel, w.viewMetricLabel).Inc() + } // when pre-fetching, remove the negative cache entry if it exists if w.prefetch { w.ncache.Remove(key) } case response.NameError, response.NoData, response.ServerError: + if plugin.Zones(w.nexcept).Matches(m.Question[0].Name) != "" { + // zone is in exception list, do not cache + return + } i := newItem(m, w.now(), duration) - w.ncache.Add(key, i) + if w.wildcardFunc != nil { + i.wildcard = w.wildcardFunc() + } + if w.ncache.Add(key, i) { + evictions.WithLabelValues(w.server, Denial, w.zonesMetricLabel, w.viewMetricLabel).Inc() + } case response.OtherError: // don't cache these @@ -217,6 +274,33 @@ func (w *ResponseWriter) Write(buf []byte) (int, error) { return n, err } +// verifyStaleResponseWriter is a response writer that only writes messages if they should replace a +// stale cache entry, and otherwise discards them. +type verifyStaleResponseWriter struct { + *ResponseWriter + refreshed bool // set to true if the last WriteMsg wrote to ResponseWriter, false otherwise. +} + +// newVerifyStaleResponseWriter returns a ResponseWriter to be used when verifying stale cache +// entries. It only forward writes if an entry was successfully refreshed according to RFC8767, +// section 4 (response is NoError or NXDomain), and ignores any other response. +func newVerifyStaleResponseWriter(w *ResponseWriter) *verifyStaleResponseWriter { + return &verifyStaleResponseWriter{ + w, + false, + } +} + +// WriteMsg implements the dns.ResponseWriter interface. +func (w *verifyStaleResponseWriter) WriteMsg(res *dns.Msg) error { + w.refreshed = false + if res.Rcode == dns.RcodeSuccess || res.Rcode == dns.RcodeNameError { + w.refreshed = true + return w.ResponseWriter.WriteMsg(res) // stores to the cache and send to client + } + return nil // else discard +} + const ( maxTTL = dnsutil.MaximumDefaulTTL minTTL = dnsutil.MinimalDefaultTTL diff --git a/plugin/cache/cache_test.go b/plugin/cache/cache_test.go index 9417a59f68..1a4d178f2d 100644 --- a/plugin/cache/cache_test.go +++ b/plugin/cache/cache_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metadata" "github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/pkg/response" "github.com/coredns/coredns/plugin/test" @@ -15,223 +16,330 @@ import ( "github.com/miekg/dns" ) -type cacheTestCase struct { - test.Case - in test.Case - AuthenticatedData bool - RecursionAvailable bool - Truncated bool - shouldCache bool +func cacheMsg(m *dns.Msg, tc test.Case) *dns.Msg { + m.RecursionAvailable = tc.RecursionAvailable + m.AuthenticatedData = tc.AuthenticatedData + m.CheckingDisabled = tc.CheckingDisabled + m.Authoritative = tc.Authoritative + m.Rcode = tc.Rcode + m.Truncated = tc.Truncated + m.Answer = tc.Answer + m.Ns = tc.Ns + // m.Extra = tc.in.Extra don't copy Extra, because we don't care and fake EDNS0 DO with tc.Do. + return m +} + +func newTestCache(ttl time.Duration) (*Cache, *ResponseWriter) { + c := New() + c.pttl = ttl + c.nttl = ttl + + crr := &ResponseWriter{ResponseWriter: nil, Cache: c} + crr.nexcept = []string{"neg-disabled.example.org."} + crr.pexcept = []string{"pos-disabled.example.org."} + + return c, crr } -var cacheTestCases = []cacheTestCase{ - { - RecursionAvailable: true, AuthenticatedData: true, - Case: test.Case{ - Qname: "miek.nl.", Qtype: dns.TypeMX, - Answer: []dns.RR{ - test.MX("miek.nl. 3600 IN MX 1 aspmx.l.google.com."), - test.MX("miek.nl. 3600 IN MX 10 aspmx2.googlemail.com."), +// TestCacheInsertion verifies the insertion of items to the cache. +func TestCacheInsertion(t *testing.T) { + cacheTestCases := []struct { + name string + out test.Case // the expected message coming "out" of cache + in test.Case // the test message going "in" to cache + shouldCache bool + }{ + { + name: "test ad bit cache", + out: test.Case{ + Qname: "miek.nl.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("miek.nl. 3600 IN MX 1 aspmx.l.google.com."), + test.MX("miek.nl. 3600 IN MX 10 aspmx2.googlemail.com."), + }, + RecursionAvailable: true, + AuthenticatedData: true, }, - }, - in: test.Case{ - Qname: "miek.nl.", Qtype: dns.TypeMX, - Answer: []dns.RR{ - test.MX("miek.nl. 3601 IN MX 1 aspmx.l.google.com."), - test.MX("miek.nl. 3601 IN MX 10 aspmx2.googlemail.com."), + in: test.Case{ + Qname: "miek.nl.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("miek.nl. 3601 IN MX 1 aspmx.l.google.com."), + test.MX("miek.nl. 3601 IN MX 10 aspmx2.googlemail.com."), + }, + RecursionAvailable: true, + AuthenticatedData: true, }, + shouldCache: true, }, - shouldCache: true, - }, - { - RecursionAvailable: true, AuthenticatedData: true, - Case: test.Case{ - Qname: "miek.nl.", Qtype: dns.TypeMX, - Answer: []dns.RR{ - test.MX("miek.nl. 3600 IN MX 1 aspmx.l.google.com."), - test.MX("miek.nl. 3600 IN MX 10 aspmx2.googlemail.com."), + { + name: "test case sensitivity cache", + out: test.Case{ + Qname: "miek.nl.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("miek.nl. 3600 IN MX 1 aspmx.l.google.com."), + test.MX("miek.nl. 3600 IN MX 10 aspmx2.googlemail.com."), + }, + RecursionAvailable: true, + AuthenticatedData: true, }, - }, - in: test.Case{ - Qname: "mIEK.nL.", Qtype: dns.TypeMX, - Answer: []dns.RR{ - test.MX("miek.nl. 3601 IN MX 1 aspmx.l.google.com."), - test.MX("miek.nl. 3601 IN MX 10 aspmx2.googlemail.com."), - // RRSIG must be here, because we are always doing DNSSEC lookups, and miek.nl MX is tested later in this list as well. - test.RRSIG("miek.nl. 3600 IN RRSIG MX 8 2 1800 20160521031301 20160421031301 12051 miek.nl. lAaEzB5teQLLKyDenatmyhca7blLRg9DoGNrhe3NReBZN5C5/pMQk8Jc u25hv2fW23/SLm5IC2zaDpp2Fzgm6Jf7e90/yLcwQPuE7JjS55WMF+HE LEh7Z6AEb+Iq4BWmNhUz6gPxD4d9eRMs7EAzk13o1NYi5/JhfL6IlaYy qkc="), + in: test.Case{ + Qname: "mIEK.nL.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("miek.nl. 3601 IN MX 1 aspmx.l.google.com."), + test.MX("miek.nl. 3601 IN MX 10 aspmx2.googlemail.com."), + }, + RecursionAvailable: true, + AuthenticatedData: true, }, + shouldCache: true, }, - shouldCache: true, - }, - { - Truncated: true, - Case: test.Case{ - Qname: "miek.nl.", Qtype: dns.TypeMX, - Answer: []dns.RR{test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com.")}, + { + name: "test truncated responses shouldn't cache", + in: test.Case{ + Qname: "miek.nl.", Qtype: dns.TypeMX, + Answer: []dns.RR{test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com.")}, + Truncated: true, + }, + shouldCache: false, }, - in: test.Case{}, - shouldCache: false, - }, - { - RecursionAvailable: true, - Case: test.Case{ - Rcode: dns.RcodeNameError, - Qname: "example.org.", Qtype: dns.TypeA, - Ns: []dns.RR{ - test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"), + { + name: "test dns.RcodeNameError cache", + out: test.Case{ + Rcode: dns.RcodeNameError, + Qname: "example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"), + }, + RecursionAvailable: true, }, + in: test.Case{ + Rcode: dns.RcodeNameError, + Qname: "example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"), + }, + RecursionAvailable: true, + }, + shouldCache: true, }, - in: test.Case{ - Rcode: dns.RcodeNameError, - Qname: "example.org.", Qtype: dns.TypeA, - Ns: []dns.RR{ - test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"), + { + name: "test dns.RcodeServerFailure cache", + out: test.Case{ + Rcode: dns.RcodeServerFailure, + Qname: "example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{}, + RecursionAvailable: true, + }, + in: test.Case{ + Rcode: dns.RcodeServerFailure, + Qname: "example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{}, + RecursionAvailable: true, }, + shouldCache: true, }, - shouldCache: true, - }, - { - RecursionAvailable: true, - Case: test.Case{ - Rcode: dns.RcodeServerFailure, - Qname: "example.org.", Qtype: dns.TypeA, - Ns: []dns.RR{}, + { + name: "test dns.RcodeNotImplemented cache", + out: test.Case{ + Rcode: dns.RcodeNotImplemented, + Qname: "example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{}, + RecursionAvailable: true, + }, + in: test.Case{ + Rcode: dns.RcodeNotImplemented, + Qname: "example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{}, + RecursionAvailable: true, + }, + shouldCache: true, }, - in: test.Case{ - Rcode: dns.RcodeServerFailure, - Qname: "example.org.", Qtype: dns.TypeA, - Ns: []dns.RR{}, + { + name: "test expired RRSIG doesn't cache", + in: test.Case{ + Qname: "miek.nl.", Qtype: dns.TypeMX, + Do: true, + Answer: []dns.RR{ + test.MX("miek.nl. 3600 IN MX 1 aspmx.l.google.com."), + test.MX("miek.nl. 3600 IN MX 10 aspmx2.googlemail.com."), + test.RRSIG("miek.nl. 1800 IN RRSIG MX 8 2 1800 20160521031301 20160421031301 12051 miek.nl. lAaEzB5teQLLKyDenatmyhca7blLRg9DoGNrhe3NReBZN5C5/pMQk8Jc u25hv2fW23/SLm5IC2zaDpp2Fzgm6Jf7e90/yLcwQPuE7JjS55WMF+HE LEh7Z6AEb+Iq4BWmNhUz6gPxD4d9eRMs7EAzk13o1NYi5/JhfL6IlaYy qkc="), + }, + RecursionAvailable: true, + }, + shouldCache: false, }, - shouldCache: true, - }, - { - RecursionAvailable: true, - Case: test.Case{ - Rcode: dns.RcodeNotImplemented, - Qname: "example.org.", Qtype: dns.TypeA, - Ns: []dns.RR{}, + { + name: "test DO bit with RRSIG not expired cache", + out: test.Case{ + Qname: "example.org.", Qtype: dns.TypeMX, + Do: true, + Answer: []dns.RR{ + test.MX("example.org. 3600 IN MX 1 aspmx.l.google.com."), + test.MX("example.org. 3600 IN MX 10 aspmx2.googlemail.com."), + test.RRSIG("example.org. 3600 IN RRSIG MX 8 2 1800 20170521031301 20170421031301 12051 miek.nl. lAaEzB5teQLLKyDenatmyhca7blLRg9DoGNrhe3NReBZN5C5/pMQk8Jc u25hv2fW23/SLm5IC2zaDpp2Fzgm6Jf7e90/yLcwQPuE7JjS55WMF+HE LEh7Z6AEb+Iq4BWmNhUz6gPxD4d9eRMs7EAzk13o1NYi5/JhfL6IlaYy qkc="), + }, + RecursionAvailable: true, + }, + in: test.Case{ + Qname: "example.org.", Qtype: dns.TypeMX, + Do: true, + Answer: []dns.RR{ + test.MX("example.org. 3600 IN MX 1 aspmx.l.google.com."), + test.MX("example.org. 3600 IN MX 10 aspmx2.googlemail.com."), + test.RRSIG("example.org. 1800 IN RRSIG MX 8 2 1800 20170521031301 20170421031301 12051 miek.nl. lAaEzB5teQLLKyDenatmyhca7blLRg9DoGNrhe3NReBZN5C5/pMQk8Jc u25hv2fW23/SLm5IC2zaDpp2Fzgm6Jf7e90/yLcwQPuE7JjS55WMF+HE LEh7Z6AEb+Iq4BWmNhUz6gPxD4d9eRMs7EAzk13o1NYi5/JhfL6IlaYy qkc="), + }, + RecursionAvailable: true, + }, + shouldCache: true, }, - in: test.Case{ - Rcode: dns.RcodeNotImplemented, - Qname: "example.org.", Qtype: dns.TypeA, - Ns: []dns.RR{}, + { + name: "test CD bit cache", + out: test.Case{ + Rcode: dns.RcodeSuccess, + Qname: "dnssec-failed.org.", + Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("dnssec-failed.org. 3600 IN A 127.0.0.1"), + }, + CheckingDisabled: true, + }, + in: test.Case{ + Rcode: dns.RcodeSuccess, + Qname: "dnssec-failed.org.", + Answer: []dns.RR{ + test.A("dnssec-failed.org. 3600 IN A 127.0.0.1"), + }, + Qtype: dns.TypeA, + CheckingDisabled: true, + }, + shouldCache: true, }, - shouldCache: true, - }, - { - RecursionAvailable: true, - Case: test.Case{ - Qname: "miek.nl.", Qtype: dns.TypeMX, - Do: true, - Answer: []dns.RR{ - test.MX("miek.nl. 3600 IN MX 1 aspmx.l.google.com."), - test.MX("miek.nl. 3600 IN MX 10 aspmx2.googlemail.com."), - test.RRSIG("miek.nl. 3600 IN RRSIG MX 8 2 1800 20160521031301 20160421031301 12051 miek.nl. lAaEzB5teQLLKyDenatmyhca7blLRg9DoGNrhe3NReBZN5C5/pMQk8Jc u25hv2fW23/SLm5IC2zaDpp2Fzgm6Jf7e90/yLcwQPuE7JjS55WMF+HE LEh7Z6AEb+Iq4BWmNhUz6gPxD4d9eRMs7EAzk13o1NYi5/JhfL6IlaYy qkc="), + { + name: "test negative zone exception shouldn't cache", + in: test.Case{ + Rcode: dns.RcodeNameError, + Qname: "neg-disabled.example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"), + }, }, + shouldCache: false, }, - in: test.Case{ - Qname: "miek.nl.", Qtype: dns.TypeMX, - Do: true, - Answer: []dns.RR{ - test.MX("miek.nl. 3600 IN MX 1 aspmx.l.google.com."), - test.MX("miek.nl. 3600 IN MX 10 aspmx2.googlemail.com."), - test.RRSIG("miek.nl. 1800 IN RRSIG MX 8 2 1800 20160521031301 20160421031301 12051 miek.nl. lAaEzB5teQLLKyDenatmyhca7blLRg9DoGNrhe3NReBZN5C5/pMQk8Jc u25hv2fW23/SLm5IC2zaDpp2Fzgm6Jf7e90/yLcwQPuE7JjS55WMF+HE LEh7Z6AEb+Iq4BWmNhUz6gPxD4d9eRMs7EAzk13o1NYi5/JhfL6IlaYy qkc="), + { + name: "test positive zone exception shouldn't cache", + in: test.Case{ + Rcode: dns.RcodeSuccess, + Qname: "pos-disabled.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("pos-disabled.example.org. 3600 IN A 127.0.0.1"), + }, }, + shouldCache: false, }, - shouldCache: true, - }, - { - RecursionAvailable: true, - Case: test.Case{ - Qname: "example.org.", Qtype: dns.TypeMX, - Do: true, - Answer: []dns.RR{ - test.MX("example.org. 3600 IN MX 1 aspmx.l.google.com."), - test.MX("example.org. 3600 IN MX 10 aspmx2.googlemail.com."), - test.RRSIG("example.org. 3600 IN RRSIG MX 8 2 1800 20170521031301 20170421031301 12051 miek.nl. lAaEzB5teQLLKyDenatmyhca7blLRg9DoGNrhe3NReBZN5C5/pMQk8Jc u25hv2fW23/SLm5IC2zaDpp2Fzgm6Jf7e90/yLcwQPuE7JjS55WMF+HE LEh7Z6AEb+Iq4BWmNhUz6gPxD4d9eRMs7EAzk13o1NYi5/JhfL6IlaYy qkc="), + { + name: "test positive zone exception with negative answer cache", + in: test.Case{ + Rcode: dns.RcodeNameError, + Qname: "pos-disabled.example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"), + }, + }, + out: test.Case{ + Rcode: dns.RcodeNameError, + Qname: "pos-disabled.example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"), + }, }, + shouldCache: true, }, - in: test.Case{ - Qname: "example.org.", Qtype: dns.TypeMX, - Do: true, - Answer: []dns.RR{ - test.MX("example.org. 3600 IN MX 1 aspmx.l.google.com."), - test.MX("example.org. 3600 IN MX 10 aspmx2.googlemail.com."), - test.RRSIG("example.org. 1800 IN RRSIG MX 8 2 1800 20170521031301 20170421031301 12051 miek.nl. lAaEzB5teQLLKyDenatmyhca7blLRg9DoGNrhe3NReBZN5C5/pMQk8Jc u25hv2fW23/SLm5IC2zaDpp2Fzgm6Jf7e90/yLcwQPuE7JjS55WMF+HE LEh7Z6AEb+Iq4BWmNhUz6gPxD4d9eRMs7EAzk13o1NYi5/JhfL6IlaYy qkc="), + { + name: "test negative zone exception with positive answer cache", + in: test.Case{ + Rcode: dns.RcodeSuccess, + Qname: "neg-disabled.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("neg-disabled.example.org. 3600 IN A 127.0.0.1"), + }, }, + out: test.Case{ + Rcode: dns.RcodeSuccess, + Qname: "neg-disabled.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("neg-disabled.example.org. 3600 IN A 127.0.0.1"), + }, + }, + shouldCache: true, }, - shouldCache: true, - }, -} - -func cacheMsg(m *dns.Msg, tc cacheTestCase) *dns.Msg { - m.RecursionAvailable = tc.RecursionAvailable - m.AuthenticatedData = tc.AuthenticatedData - m.Authoritative = true - m.Rcode = tc.Rcode - m.Truncated = tc.Truncated - m.Answer = tc.in.Answer - m.Ns = tc.in.Ns - // m.Extra = tc.in.Extra don't copy Extra, because we don't care and fake EDNS0 DO with tc.Do. - return m -} - -func newTestCache(ttl time.Duration) (*Cache, *ResponseWriter) { - c := New() - c.pttl = ttl - c.nttl = ttl - - crr := &ResponseWriter{ResponseWriter: nil, Cache: c} - return c, crr -} - -func TestCache(t *testing.T) { + } now, _ := time.Parse(time.UnixDate, "Fri Apr 21 10:51:21 BST 2017") utc := now.UTC() - c, crr := newTestCache(maxTTL) - for _, tc := range cacheTestCases { - m := tc.in.Msg() - m = cacheMsg(m, tc) - - state := request.Request{W: &test.ResponseWriter{}, Req: m} + t.Run(tc.name, func(t *testing.T) { + // Create a new cache every time to prevent accidental comparison with a previous item. + c, crr := newTestCache(maxTTL) - mt, _ := response.Typify(m, utc) - valid, k := key(state.Name(), m, mt) - - if valid { - crr.set(m, k, mt, c.pttl) - } + m := tc.in.Msg() + m = cacheMsg(m, tc.in) - i, _ := c.get(time.Now().UTC(), state, "dns://:53") - ok := i != nil - - if ok != tc.shouldCache { - t.Errorf("Cached message that should not have been cached: %s", state.Name()) - continue - } + state := request.Request{W: &test.ResponseWriter{}, Req: m} - if ok { - resp := i.toMsg(m, time.Now().UTC(), state.Do()) + mt, _ := response.Typify(m, utc) + valid, k := key(state.Name(), m, mt, state.Do(), state.Req.CheckingDisabled) - if err := test.Header(tc.Case, resp); err != nil { - t.Logf("Cache %v", resp) - t.Error(err) - continue + if valid { + // Insert cache entry + crr.set(m, k, mt, c.pttl) } - if err := test.Section(tc.Case, test.Answer, resp.Answer); err != nil { - t.Logf("Cache %v -- %v", test.Answer, resp.Answer) - t.Error(err) + // Attempt to retrieve cache entry + i := c.getIgnoreTTL(time.Now().UTC(), state, "dns://:53") + found := i != nil + + if !tc.shouldCache && found { + t.Fatalf("Cached message that should not have been cached: %s", state.Name()) } - if err := test.Section(tc.Case, test.Ns, resp.Ns); err != nil { - t.Error(err) + if tc.shouldCache && !found { + t.Fatalf("Did not cache message that should have been cached: %s", state.Name()) } - if err := test.Section(tc.Case, test.Extra, resp.Extra); err != nil { - t.Error(err) + + if found { + resp := i.toMsg(m, time.Now().UTC(), state.Do(), m.AuthenticatedData) + + // TODO: If we incorporate these individual checks into the + // test.Header function, we can eliminate them from here. + // Cache entries are always Authoritative. + if resp.Authoritative != true { + t.Error("Expected Authoritative Answer bit to be true, but was false") + } + if resp.AuthenticatedData != tc.out.AuthenticatedData { + t.Errorf("Expected Authenticated Data bit to be %t, but got %t", tc.out.AuthenticatedData, resp.AuthenticatedData) + } + if resp.RecursionAvailable != tc.out.RecursionAvailable { + t.Errorf("Expected Recursion Available bit to be %t, but got %t", tc.out.RecursionAvailable, resp.RecursionAvailable) + } + if resp.CheckingDisabled != tc.out.CheckingDisabled { + t.Errorf("Expected Checking Disabled bit to be %t, but got %t", tc.out.CheckingDisabled, resp.CheckingDisabled) + } + + if err := test.Header(tc.out, resp); err != nil { + t.Logf("Cache %v", resp) + t.Error(err) + } + if err := test.Section(tc.out, test.Answer, resp.Answer); err != nil { + t.Logf("Cache %v -- %v", test.Answer, resp.Answer) + t.Error(err) + } + if err := test.Section(tc.out, test.Ns, resp.Ns); err != nil { + t.Error(err) + } + if err := test.Section(tc.out, test.Extra, resp.Extra); err != nil { + t.Error(err) + } } - } + }) } } @@ -254,6 +362,23 @@ func TestCacheZeroTTL(t *testing.T) { } } +func TestCacheServfailTTL0(t *testing.T) { + c := New() + c.minpttl = minTTL + c.minnttl = minNTTL + c.failttl = 0 + c.Next = servFailBackend(0) + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + ctx := context.TODO() + + c.ServeDNS(ctx, &test.ResponseWriter{}, req) + if c.ncache.Len() != 0 { + t.Errorf("SERVFAIL response should not have been cached") + } +} + func TestServeFromStaleCache(t *testing.T) { c := New() c.Next = ttlBackend(60) @@ -262,7 +387,7 @@ func TestServeFromStaleCache(t *testing.T) { req.SetQuestion("cached.org.", dns.TypeA) ctx := context.TODO() - // Cache example.org. + // Cache cached.org. with 60s TTL rec := dnstest.NewRecorder(&test.ResponseWriter{}) c.staleUpTo = 1 * time.Hour c.ServeDNS(ctx, rec, req) @@ -300,6 +425,82 @@ func TestServeFromStaleCache(t *testing.T) { } } +func TestServeFromStaleCacheFetchVerify(t *testing.T) { + c := New() + c.Next = ttlBackend(120) + + req := new(dns.Msg) + req.SetQuestion("cached.org.", dns.TypeA) + ctx := context.TODO() + + // Cache cached.org. with 120s TTL + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + c.staleUpTo = 1 * time.Hour + c.verifyStale = true + c.ServeDNS(ctx, rec, req) + if c.pcache.Len() != 1 { + t.Fatalf("Msg with > 0 TTL should have been cached") + } + + tests := []struct { + name string + upstreamRCode int + upstreamTtl int + futureMinutes int + expectedRCode int + expectedTtl int + }{ + // After 1 minutes of initial TTL, we should see a cached response + {"cached.org.", dns.RcodeSuccess, 200, 1, dns.RcodeSuccess, 60}, // ttl = 120 - 60 -- not refreshed + + // After the 2 more minutes, we should see upstream responses because upstream is available + {"cached.org.", dns.RcodeSuccess, 200, 3, dns.RcodeSuccess, 200}, + + // After the TTL expired, if the server fails we should get the cached entry + {"cached.org.", dns.RcodeServerFailure, 200, 7, dns.RcodeSuccess, 0}, + + // After 1 more minutes, if the server serves nxdomain we should see them (despite being within the serve stale period) + {"cached.org.", dns.RcodeNameError, 150, 8, dns.RcodeNameError, 150}, + } + + for i, tt := range tests { + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + c.now = func() time.Time { return time.Now().Add(time.Duration(tt.futureMinutes) * time.Minute) } + + switch tt.upstreamRCode { + case dns.RcodeSuccess: + c.Next = ttlBackend(tt.upstreamTtl) + case dns.RcodeServerFailure: + // Make upstream fail, should now rely on cache during the c.staleUpTo period + c.Next = servFailBackend(tt.upstreamTtl) + case dns.RcodeNameError: + c.Next = nxDomainBackend(tt.upstreamTtl) + default: + t.Fatal("upstream code not implemented") + } + + r := req.Copy() + r.SetQuestion(tt.name, dns.TypeA) + ret, _ := c.ServeDNS(ctx, rec, r) + if ret != tt.expectedRCode { + t.Errorf("Test %d: expected rcode=%v, got rcode=%v", i, tt.expectedRCode, ret) + continue + } + switch ret { + case dns.RcodeSuccess: + recTtl := rec.Msg.Answer[0].Header().Ttl + if tt.expectedTtl != int(recTtl) { + t.Errorf("Test %d: expected TTL=%d, got TTL=%d", i, tt.expectedTtl, recTtl) + } + case dns.RcodeNameError: + soaTtl := rec.Msg.Ns[0].Header().Ttl + if tt.expectedTtl != int(soaTtl) { + t.Errorf("Test %d: expected TTL=%d, got TTL=%d", i, tt.expectedTtl, soaTtl) + } + } + } +} + func TestNegativeStaleMaskingPositiveCache(t *testing.T) { c := New() c.staleUpTo = time.Minute * 10 @@ -399,10 +600,8 @@ func BenchmarkCacheResponse(b *testing.B) { reqs[i].SetQuestion(q+".example.org.", dns.TypeA) } - b.StartTimer() - j := 0 - for i := 0; i < b.N; i++ { + for b.Loop() { req := reqs[j] c.ServeDNS(ctx, &test.ResponseWriter{}, req) j = (j + 1) % 5 @@ -432,7 +631,7 @@ func nxDomainBackend(ttl int) plugin.Handler { m.Ns = []dns.RR{test.SOA(fmt.Sprintf("example.org. %d IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600", ttl))} - m.MsgHdr.Rcode = dns.RcodeNameError + m.Rcode = dns.RcodeNameError w.WriteMsg(m) return dns.RcodeNameError, nil }) @@ -450,6 +649,20 @@ func ttlBackend(ttl int) plugin.Handler { }) } +func servFailBackend(ttl int) plugin.Handler { + return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + m := new(dns.Msg) + m.SetReply(r) + m.Response, m.RecursionAvailable = true, true + + m.Ns = []dns.RR{test.SOA(fmt.Sprintf("example.org. %d IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600", ttl))} + + m.Rcode = dns.RcodeServerFailure + w.WriteMsg(m) + return dns.RcodeServerFailure, nil + }) +} + func TestComputeTTL(t *testing.T) { tests := []struct { msgTTL time.Duration @@ -469,3 +682,220 @@ func TestComputeTTL(t *testing.T) { } } } + +func TestCacheWildcardMetadata(t *testing.T) { + c := New() + qname := "foo.bar.example.org." + wildcard := "*.bar.example.org." + c.Next = wildcardMetadataBackend(qname, wildcard) + + req := new(dns.Msg) + req.SetQuestion(qname, dns.TypeA) + state := request.Request{W: &test.ResponseWriter{}, Req: req} + + // 1. Test writing wildcard metadata retrieved from backend to the cache + + ctx := metadata.ContextWithMetadata(context.TODO()) + w := dnstest.NewRecorder(&test.ResponseWriter{}) + c.ServeDNS(ctx, w, req) + if c.pcache.Len() != 1 { + t.Errorf("Msg should have been cached") + } + _, k := key(qname, w.Msg, response.NoError, state.Do(), state.Req.CheckingDisabled) + i, _ := c.pcache.Get(k) + if i.(*item).wildcard != wildcard { + t.Errorf("expected wildcard response to enter cache with cache item's wildcard = %q, got %q", wildcard, i.(*item).wildcard) + } + + // 2. Test retrieving the cached item from cache and writing its wildcard value to metadata + + // reset context and response writer + ctx = metadata.ContextWithMetadata(context.TODO()) + w = dnstest.NewRecorder(&test.ResponseWriter{}) + + c.ServeDNS(ctx, w, req) + f := metadata.ValueFunc(ctx, "zone/wildcard") + if f == nil { + t.Fatal("expected metadata func for wildcard response retrieved from cache, got nil") + } + if f() != wildcard { + t.Errorf("after retrieving wildcard item from cache, expected \"zone/wildcard\" metadata value to be %q, got %q", wildcard, i.(*item).wildcard) + } +} + +func TestCacheKeepTTL(t *testing.T) { + defaultTtl := 60 + + c := New() + c.Next = ttlBackend(defaultTtl) + + req := new(dns.Msg) + req.SetQuestion("cached.org.", dns.TypeA) + ctx := context.TODO() + + // Cache cached.org. with 60s TTL + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + c.keepttl = true + c.ServeDNS(ctx, rec, req) + + tests := []struct { + name string + futureSeconds int + }{ + {"cached.org.", 0}, + {"cached.org.", 30}, + {"uncached.org.", 60}, + } + + for i, tt := range tests { + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + c.now = func() time.Time { return time.Now().Add(time.Duration(tt.futureSeconds) * time.Second) } + r := req.Copy() + r.SetQuestion(tt.name, dns.TypeA) + c.ServeDNS(ctx, rec, r) + + recTtl := rec.Msg.Answer[0].Header().Ttl + if defaultTtl != int(recTtl) { + t.Errorf("Test %d: expecting TTL=%d, got TTL=%d", i, defaultTtl, recTtl) + } + } +} + +// TestCacheSeparation verifies whether the cache maintains separation for specific DNS query types and options. +func TestCacheSeparation(t *testing.T) { + now, _ := time.Parse(time.UnixDate, "Fri Apr 21 10:51:21 BST 2017") + utc := now.UTC() + + testCases := []struct { + name string + initial test.Case + query test.Case + expectCached bool // if a cache entry should be found before inserting + }{ + { + name: "query type should be unique", + initial: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeA, + }, + query: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeAAAA, + }, + }, + { + name: "DO bit should be unique", + initial: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeA, + }, + query: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeA, + Do: true, + }, + }, + { + name: "CD bit should be unique", + initial: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeA, + }, + query: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeA, + CheckingDisabled: true, + }, + }, + { + name: "CD bit and DO bit should be unique", + initial: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeA, + }, + query: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeA, + CheckingDisabled: true, + Do: true, + }, + }, + { + name: "CD bit, DO bit, and query type should be unique", + initial: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeA, + }, + query: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeMX, + CheckingDisabled: true, + Do: true, + }, + }, + { + name: "authoritative answer bit should NOT be unique", + initial: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeA, + }, + query: test.Case{ + Qname: "example.org.", + Qtype: dns.TypeA, + Authoritative: true, + }, + expectCached: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := New() + crr := &ResponseWriter{ResponseWriter: nil, Cache: c} + + // Insert initial cache entry + m := tc.initial.Msg() + m = cacheMsg(m, tc.initial) + state := request.Request{W: &test.ResponseWriter{}, Req: m} + + mt, _ := response.Typify(m, utc) + valid, k := key(state.Name(), m, mt, state.Do(), state.Req.CheckingDisabled) + + if valid { + // Insert cache entry + crr.set(m, k, mt, c.pttl) + } + + // Attempt to retrieve cache entry + m = tc.query.Msg() + m = cacheMsg(m, tc.query) + state = request.Request{W: &test.ResponseWriter{}, Req: m} + + item := c.getIgnoreTTL(time.Now().UTC(), state, "dns://:53") + found := item != nil + + if !tc.expectCached && found { + t.Fatal("Found cache message should that should not exist prior to inserting") + } + if tc.expectCached && !found { + t.Fatal("Did not find cache message that should exist prior to inserting") + } + }) + } +} + +// wildcardMetadataBackend mocks a backend that responds with a response for qname synthesized by wildcard +// and sets the zone/wildcard metadata value +func wildcardMetadataBackend(qname, wildcard string) plugin.Handler { + return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + m := new(dns.Msg) + m.SetReply(r) + m.Response, m.RecursionAvailable = true, true + m.Answer = []dns.RR{test.A(qname + " 300 IN A 127.0.0.1")} + metadata.SetValueFunc(ctx, "zone/wildcard", func() string { + return wildcard + }) + w.WriteMsg(m) + + return dns.RcodeSuccess, nil + }) +} diff --git a/plugin/cache/dnssec.go b/plugin/cache/dnssec.go index 61a1cc15fe..da7e1e9b3e 100644 --- a/plugin/cache/dnssec.go +++ b/plugin/cache/dnssec.go @@ -2,44 +2,22 @@ package cache import "github.com/miekg/dns" -// isDNSSEC returns true if r is a DNSSEC record. NSEC,NSEC3,DS and RRSIG/SIG -// are DNSSEC records. DNSKEYs is not in this list on the assumption that the -// client explicitly asked for it. -func isDNSSEC(r dns.RR) bool { - switch r.Header().Rrtype { - case dns.TypeNSEC: - return true - case dns.TypeNSEC3: - return true - case dns.TypeDS: - return true - case dns.TypeRRSIG: - return true - case dns.TypeSIG: - return true - } - return false -} - -// filterRRSlice filters rrs and removes DNSSEC RRs when do is false. In the returned slice -// the TTLs are set to ttl. If dup is true the RRs in rrs are _copied_ into the slice that is -// returned. -func filterRRSlice(rrs []dns.RR, ttl uint32, do, dup bool) []dns.RR { +// filterRRSlice filters out OPT RRs, and sets all RR TTLs to ttl. +// If dup is true the RRs in rrs are _copied_ before adjusting their +// TTL and the slice of copied RRs is returned. +func filterRRSlice(rrs []dns.RR, ttl uint32, dup bool) []dns.RR { j := 0 - rs := make([]dns.RR, len(rrs), len(rrs)) + rs := make([]dns.RR, len(rrs)) for _, r := range rrs { - if !do && isDNSSEC(r) { - continue - } if r.Header().Rrtype == dns.TypeOPT { continue } - r.Header().Ttl = ttl if dup { rs[j] = dns.Copy(r) } else { rs[j] = r } + rs[j].Header().Ttl = ttl j++ } return rs[:j] diff --git a/plugin/cache/dnssec_test.go b/plugin/cache/dnssec_test.go index 446718c9fc..b73d52cf7b 100644 --- a/plugin/cache/dnssec_test.go +++ b/plugin/cache/dnssec_test.go @@ -7,6 +7,7 @@ import ( "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" "github.com/miekg/dns" ) @@ -23,7 +24,8 @@ func TestResponseWithDNSSEC(t *testing.T) { }, { Qname: "invent.example.org.", Qtype: dns.TypeA, - Do: true, + Do: true, + AuthenticatedData: true, Answer: []dns.RR{ test.CNAME("invent.example.org. 1781 IN CNAME leptone.example.org."), test.RRSIG("invent.example.org. 1781 IN RRSIG CNAME 8 3 1800 20201012085750 20200912082613 57411 example.org. ijSv5FmsNjFviBcOFwQgqjt073lttxTTNqkno6oMa3DD3kC+"), @@ -40,6 +42,9 @@ func TestResponseWithDNSSEC(t *testing.T) { m := tc.Msg() rec := dnstest.NewRecorder(&test.ResponseWriter{}) c.ServeDNS(context.TODO(), rec, m) + if tc.AuthenticatedData != rec.Msg.AuthenticatedData { + t.Errorf("Test %d, expected AuthenticatedData=%v", i, tc.AuthenticatedData) + } if err := test.Section(tc, test.Answer, rec.Msg.Answer); err != nil { t.Errorf("Test %d, expected no error, got %s", i, err) } @@ -63,18 +68,27 @@ func dnssecHandler() plugin.Handler { return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { m := new(dns.Msg) m.SetQuestion("example.org.", dns.TypeA) + state := request.Request{W: &test.ResponseWriter{}, Req: r} - m.Answer = make([]dns.RR, 4) - m.Answer[0] = test.CNAME("invent.example.org. 1781 IN CNAME leptone.example.org.") - m.Answer[1] = test.RRSIG("invent.example.org. 1781 IN RRSIG CNAME 8 3 1800 20201012085750 20200912082613 57411 example.org. ijSv5FmsNjFviBcOFwQgqjt073lttxTTNqkno6oMa3DD3kC+") - m.Answer[2] = test.A("leptone.example.org. 1781 IN A 195.201.182.103") - m.Answer[3] = test.RRSIG("leptone.example.org. 1781 IN RRSIG A 8 3 1800 20201012093630 20200912083827 57411 example.org. eLuSOkLAzm/WIOpaZD3/4TfvKP1HAFzjkis9LIJSRVpQt307dm9WY9") + m.AuthenticatedData = true + // If query has the DO bit, then send DNSSEC responses (RRSIGs) + if state.Do() { + m.Answer = make([]dns.RR, 4) + m.Answer[0] = test.CNAME("invent.example.org. 1781 IN CNAME leptone.example.org.") + m.Answer[1] = test.RRSIG("invent.example.org. 1781 IN RRSIG CNAME 8 3 1800 20201012085750 20200912082613 57411 example.org. ijSv5FmsNjFviBcOFwQgqjt073lttxTTNqkno6oMa3DD3kC+") + m.Answer[2] = test.A("leptone.example.org. 1781 IN A 195.201.182.103") + m.Answer[3] = test.RRSIG("leptone.example.org. 1781 IN RRSIG A 8 3 1800 20201012093630 20200912083827 57411 example.org. eLuSOkLAzm/WIOpaZD3/4TfvKP1HAFzjkis9LIJSRVpQt307dm9WY9") + } else { + m.Answer = make([]dns.RR, 2) + m.Answer[0] = test.CNAME("invent.example.org. 1781 IN CNAME leptone.example.org.") + m.Answer[1] = test.A("leptone.example.org. 1781 IN A 195.201.182.103") + } w.WriteMsg(m) return dns.RcodeSuccess, nil }) } -func TestFliterRRSlice(t *testing.T) { +func TestFilterRRSlice(t *testing.T) { rrs := []dns.RR{ test.CNAME("invent.example.org. 1781 IN CNAME leptone.example.org."), test.RRSIG("invent.example.org. 1781 IN RRSIG CNAME 8 3 1800 20201012085750 20200912082613 57411 example.org. ijSv5FmsNjFviBcOFwQgqjt073lttxTTNqkno6oMa3DD3kC+"), @@ -82,7 +96,7 @@ func TestFliterRRSlice(t *testing.T) { test.RRSIG("leptone.example.org. 1781 IN RRSIG A 8 3 1800 20201012093630 20200912083827 57411 example.org. eLuSOkLAzm/WIOpaZD3/4TfvKP1HAFzjkis9LIJSRVpQt307dm9WY9"), } - filter1 := filterRRSlice(rrs, 0, true, false) + filter1 := filterRRSlice(rrs, 0, false) if len(filter1) != 4 { t.Errorf("Expected 4 RRs after filtering, got %d", len(filter1)) } @@ -96,9 +110,9 @@ func TestFliterRRSlice(t *testing.T) { t.Errorf("Expected 2 RRSIGs after filtering, got %d", rrsig) } - filter2 := filterRRSlice(rrs, 0, false, false) - if len(filter2) != 2 { - t.Errorf("Expected 2 RRs after filtering, got %d", len(filter2)) + filter2 := filterRRSlice(rrs, 0, false) + if len(filter2) != 4 { + t.Errorf("Expected 4 RRs after filtering, got %d", len(filter2)) } rrsig = 0 for _, f := range filter2 { @@ -106,7 +120,7 @@ func TestFliterRRSlice(t *testing.T) { rrsig++ } } - if rrsig != 0 { - t.Errorf("Expected 0 RRSIGs after filtering, got %d", rrsig) + if rrsig != 2 { + t.Errorf("Expected 2 RRSIGs after filtering, got %d", rrsig) } } diff --git a/plugin/cache/freq/freq_test.go b/plugin/cache/freq/freq_test.go index 740194c86e..fc6042c064 100644 --- a/plugin/cache/freq/freq_test.go +++ b/plugin/cache/freq/freq_test.go @@ -30,6 +30,7 @@ func TestReset(t *testing.T) { } func hitsCheck(t *testing.T, f *Freq, expected int) { + t.Helper() if x := f.Hits(); x != expected { t.Fatalf("Expected hits to be %d, got %d", expected, x) } diff --git a/plugin/cache/fuzz.go b/plugin/cache/fuzz.go index 18e98fa9f2..43f4d26040 100644 --- a/plugin/cache/fuzz.go +++ b/plugin/cache/fuzz.go @@ -1,4 +1,4 @@ -// +build gofuzz +//go:build gofuzz package cache diff --git a/plugin/cache/handler.go b/plugin/cache/handler.go index 2dd3c0646e..4de37ea8b4 100644 --- a/plugin/cache/handler.go +++ b/plugin/cache/handler.go @@ -6,6 +6,7 @@ import ( "time" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metadata" "github.com/coredns/coredns/plugin/metrics" "github.com/coredns/coredns/request" @@ -17,6 +18,8 @@ func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) rc := r.Copy() // We potentially modify r, to prevent other plugins from seeing this (r is a pointer), copy r into rc. state := request.Request{W: w, Req: rc} do := state.Do() + cd := r.CheckingDisabled + ad := r.AuthenticatedData zone := plugin.Zones(c.Zones).Matches(state.Name()) if zone == "" { @@ -26,54 +29,81 @@ func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) now := c.now().UTC() server := metrics.WithServer(ctx) - // On cache miss, if the request has the OPT record and the DO bit set we leave the message as-is. If there isn't a DO bit - // set we will modify the request to _add_ one. This means we will always do DNSSEC lookups on cache misses. - // When writing to cache, any DNSSEC RRs in the response are written to cache with the response. - // When sending a response to a non-DNSSEC client, we remove DNSSEC RRs from the response. We use a 2048 buffer size, which is - // less than 4096 (and older default) and more than 1024 which may be too small. We might need to tweaks this - // value to be smaller still to prevent UDP fragmentation? + // On cache refresh, we will just use the DO bit from the incoming query for the refresh since we key our cache + // with the query DO bit. That means two separate cache items for the query DO bit true or false. In the situation + // in which upstream doesn't support DNSSEC, the two cache items will effectively be the same. Regardless, any + // DNSSEC RRs in the response are written to cache with the response. - ttl := 0 i := c.getIgnoreTTL(now, state, server) - if i != nil { - ttl = i.ttl(now) - } if i == nil { - crr := &ResponseWriter{ResponseWriter: w, Cache: c, state: state, server: server, do: do} + crr := &ResponseWriter{ResponseWriter: w, Cache: c, state: state, server: server, do: do, ad: ad, cd: cd, + nexcept: c.nexcept, pexcept: c.pexcept, wildcardFunc: wildcardFunc(ctx)} return c.doRefresh(ctx, state, crr) } + ttl := i.ttl(now) if ttl < 0 { - servedStale.WithLabelValues(server).Inc() + // serve stale behavior + if c.verifyStale { + crr := &ResponseWriter{ResponseWriter: w, Cache: c, state: state, server: server, do: do, cd: cd} + cw := newVerifyStaleResponseWriter(crr) + ret, err := c.doRefresh(ctx, state, cw) + if cw.refreshed { + return ret, err + } + } + // Adjust the time to get a 0 TTL in the reply built from a stale item. now = now.Add(time.Duration(ttl) * time.Second) - cw := newPrefetchResponseWriter(server, state, c) - go c.doPrefetch(ctx, state, cw, i, now) + if !c.verifyStale { + cw := newPrefetchResponseWriter(server, state, c) + go c.doPrefetch(ctx, state, cw, i, now) + } + servedStale.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc() } else if c.shouldPrefetch(i, now) { cw := newPrefetchResponseWriter(server, state, c) go c.doPrefetch(ctx, state, cw, i, now) } - resp := i.toMsg(r, now, do) - w.WriteMsg(resp) + if i.wildcard != "" { + // Set wildcard source record name to metadata + metadata.SetValueFunc(ctx, "zone/wildcard", func() string { + return i.wildcard + }) + } + + if c.keepttl { + // If keepttl is enabled we fake the current time to the stored + // one so that we always get the original TTL + now = i.stored + } + resp := i.toMsg(r, now, do, ad) + w.WriteMsg(resp) return dns.RcodeSuccess, nil } +func wildcardFunc(ctx context.Context) func() string { + return func() string { + // Get wildcard source record name from metadata + if f := metadata.ValueFunc(ctx, "zone/wildcard"); f != nil { + return f() + } + return "" + } +} + func (c *Cache) doPrefetch(ctx context.Context, state request.Request, cw *ResponseWriter, i *item, now time.Time) { - cachePrefetches.WithLabelValues(cw.server).Inc() + cachePrefetches.WithLabelValues(cw.server, c.zonesMetricLabel, c.viewMetricLabel).Inc() c.doRefresh(ctx, state, cw) // When prefetching we loose the item i, and with it the frequency // that we've gathered sofar. See we copy the frequencies info back // into the new item that was stored in the cache. if i1 := c.exists(state); i1 != nil { - i1.Freq.Reset(now, i.Freq.Hits()) + i1.Reset(now, i.Hits()) } } -func (c *Cache) doRefresh(ctx context.Context, state request.Request, cw *ResponseWriter) (int, error) { - if !state.Do() { - setDo(state.Req) - } +func (c *Cache) doRefresh(ctx context.Context, state request.Request, cw dns.ResponseWriter) (int, error) { return plugin.NextOrFailure(c.Name(), c.Next, ctx, cw, state.Req) } @@ -81,54 +111,41 @@ func (c *Cache) shouldPrefetch(i *item, now time.Time) bool { if c.prefetch <= 0 { return false } - i.Freq.Update(c.duration, now) + i.Update(c.duration, now) threshold := int(math.Ceil(float64(c.percentage) / 100 * float64(i.origTTL))) - return i.Freq.Hits() >= c.prefetch && i.ttl(now) <= threshold + return i.Hits() >= c.prefetch && i.ttl(now) <= threshold } // Name implements the Handler interface. func (c *Cache) Name() string { return "cache" } -func (c *Cache) get(now time.Time, state request.Request, server string) (*item, bool) { - k := hash(state.Name(), state.QType()) - - if i, ok := c.ncache.Get(k); ok && i.(*item).ttl(now) > 0 { - cacheHits.WithLabelValues(server, Denial).Inc() - return i.(*item), true - } - - if i, ok := c.pcache.Get(k); ok && i.(*item).ttl(now) > 0 { - cacheHits.WithLabelValues(server, Success).Inc() - return i.(*item), true - } - cacheMisses.WithLabelValues(server).Inc() - return nil, false -} - // getIgnoreTTL unconditionally returns an item if it exists in the cache. func (c *Cache) getIgnoreTTL(now time.Time, state request.Request, server string) *item { - k := hash(state.Name(), state.QType()) + k := hash(state.Name(), state.QType(), state.Do(), state.Req.CheckingDisabled) + cacheRequests.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc() if i, ok := c.ncache.Get(k); ok { - ttl := i.(*item).ttl(now) - if ttl > 0 || (c.staleUpTo > 0 && -ttl < int(c.staleUpTo.Seconds())) { - cacheHits.WithLabelValues(server, Denial).Inc() + itm := i.(*item) + ttl := itm.ttl(now) + if itm.matches(state) && (ttl > 0 || (c.staleUpTo > 0 && -ttl < int(c.staleUpTo.Seconds()))) { + cacheHits.WithLabelValues(server, Denial, c.zonesMetricLabel, c.viewMetricLabel).Inc() return i.(*item) } } if i, ok := c.pcache.Get(k); ok { - ttl := i.(*item).ttl(now) - if ttl > 0 || (c.staleUpTo > 0 && -ttl < int(c.staleUpTo.Seconds())) { - cacheHits.WithLabelValues(server, Success).Inc() + itm := i.(*item) + ttl := itm.ttl(now) + if itm.matches(state) && (ttl > 0 || (c.staleUpTo > 0 && -ttl < int(c.staleUpTo.Seconds()))) { + cacheHits.WithLabelValues(server, Success, c.zonesMetricLabel, c.viewMetricLabel).Inc() return i.(*item) } } - cacheMisses.WithLabelValues(server).Inc() + cacheMisses.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc() return nil } func (c *Cache) exists(state request.Request) *item { - k := hash(state.Name(), state.QType()) + k := hash(state.Name(), state.QType(), state.Do(), state.Req.CheckingDisabled) if i, ok := c.ncache.Get(k); ok { return i.(*item) } @@ -137,22 +154,3 @@ func (c *Cache) exists(state request.Request) *item { } return nil } - -// setDo sets the DO bit and UDP buffer size in the message m. -func setDo(m *dns.Msg) { - o := m.IsEdns0() - if o != nil { - o.SetDo() - o.SetUDPSize(defaultUDPBufSize) - return - } - - o = &dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}} - o.SetDo() - o.SetUDPSize(defaultUDPBufSize) - m.Extra = append(m.Extra, o) -} - -// defaultUDPBufsize is the bufsize the cache plugin uses on outgoing requests that don't -// have an OPT RR. -const defaultUDPBufSize = 2048 diff --git a/plugin/cache/item.go b/plugin/cache/item.go index 3b47a3b6be..c5aeccdccd 100644 --- a/plugin/cache/item.go +++ b/plugin/cache/item.go @@ -1,20 +1,25 @@ package cache import ( + "strings" "time" "github.com/coredns/coredns/plugin/cache/freq" + "github.com/coredns/coredns/request" "github.com/miekg/dns" ) type item struct { + Name string + QType uint16 Rcode int AuthenticatedData bool RecursionAvailable bool Answer []dns.RR Ns []dns.RR Extra []dns.RR + wildcard string origTTL uint32 stored time.Time @@ -24,6 +29,10 @@ type item struct { func newItem(m *dns.Msg, now time.Time, d time.Duration) *item { i := new(item) + if len(m.Question) != 0 { + i.Name = m.Question[0].Name + i.QType = m.Question[0].Qtype + } i.Rcode = m.Rcode i.AuthenticatedData = m.AuthenticatedData i.RecursionAvailable = m.RecursionAvailable @@ -56,7 +65,7 @@ func newItem(m *dns.Msg, now time.Time, d time.Duration) *item { // So we're forced to always set this to 1; regardless if the answer came from the cache or not. // On newer systems(e.g. ubuntu 16.04 with glib version 2.23), this issue is resolved. // So we may set this bit back to 0 in the future ? -func (i *item) toMsg(m *dns.Msg, now time.Time, do bool) *dns.Msg { +func (i *item) toMsg(m *dns.Msg, now time.Time, do bool, ad bool) *dns.Msg { m1 := new(dns.Msg) m1.SetReply(m) @@ -65,8 +74,10 @@ func (i *item) toMsg(m *dns.Msg, now time.Time, do bool) *dns.Msg { // just set it to true. m1.Authoritative = true m1.AuthenticatedData = i.AuthenticatedData - if !do { - m1.AuthenticatedData = false // when DNSSEC was not wanted, it can't be authenticated data. + if !do && !ad { + // When DNSSEC was not wanted, it can't be authenticated data. + // However, retain the AD bit if the requester set the AD bit, per RFC6840 5.7-5.8 + m1.AuthenticatedData = false } m1.RecursionAvailable = i.RecursionAvailable m1.Rcode = i.Rcode @@ -76,9 +87,9 @@ func (i *item) toMsg(m *dns.Msg, now time.Time, do bool) *dns.Msg { m1.Extra = make([]dns.RR, len(i.Extra)) ttl := uint32(i.ttl(now)) - m1.Answer = filterRRSlice(i.Answer, ttl, do, true) - m1.Ns = filterRRSlice(i.Ns, ttl, do, true) - m1.Extra = filterRRSlice(i.Extra, ttl, do, true) + m1.Answer = filterRRSlice(i.Answer, ttl, true) + m1.Ns = filterRRSlice(i.Ns, ttl, true) + m1.Extra = filterRRSlice(i.Extra, ttl, true) return m1 } @@ -87,3 +98,10 @@ func (i *item) ttl(now time.Time) int { ttl := int(i.origTTL) - int(now.UTC().Sub(i.stored).Seconds()) return ttl } + +func (i *item) matches(state request.Request) bool { + if state.QType() == i.QType && strings.EqualFold(state.QName(), i.Name) { + return true + } + return false +} diff --git a/plugin/cache/metrics.go b/plugin/cache/metrics.go index b4b11ae262..77edb0286b 100644 --- a/plugin/cache/metrics.go +++ b/plugin/cache/metrics.go @@ -14,40 +14,54 @@ var ( Subsystem: "cache", Name: "entries", Help: "The number of elements in the cache.", - }, []string{"server", "type"}) + }, []string{"server", "type", "zones", "view"}) + // cacheRequests is a counter of all requests through the cache. + cacheRequests = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "cache", + Name: "requests_total", + Help: "The count of cache requests.", + }, []string{"server", "zones", "view"}) // cacheHits is counter of cache hits by cache type. cacheHits = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "hits_total", Help: "The count of cache hits.", - }, []string{"server", "type"}) - // cacheMisses is the counter of cache misses. + }, []string{"server", "type", "zones", "view"}) + // cacheMisses is the counter of cache misses. - Deprecated cacheMisses = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "misses_total", - Help: "The count of cache misses.", - }, []string{"server"}) + Help: "The count of cache misses. Deprecated, derive misses from cache hits/requests counters.", + }, []string{"server", "zones", "view"}) // cachePrefetches is the number of time the cache has prefetched a cached item. cachePrefetches = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "prefetch_total", Help: "The number of times the cache has prefetched a cached item.", - }, []string{"server"}) + }, []string{"server", "zones", "view"}) // cacheDrops is the number responses that are not cached, because the reply is malformed. cacheDrops = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "drops_total", Help: "The number responses that are not cached, because the reply is malformed.", - }, []string{"server"}) + }, []string{"server", "zones", "view"}) // servedStale is the number of requests served from stale cache entries. servedStale = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "cache", Name: "served_stale_total", Help: "The number of requests served from stale cache entries.", - }, []string{"server"}) + }, []string{"server", "zones", "view"}) + // evictions is the counter of cache evictions. + evictions = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "cache", + Name: "evictions_total", + Help: "The count of cache evictions.", + }, []string{"server", "type", "zones", "view"}) ) diff --git a/plugin/cache/prefech_test.go b/plugin/cache/prefetch_test.go similarity index 67% rename from plugin/cache/prefech_test.go rename to plugin/cache/prefetch_test.go index 609956ee92..3085fe0f9b 100644 --- a/plugin/cache/prefech_test.go +++ b/plugin/cache/prefetch_test.go @@ -28,12 +28,12 @@ func TestPrefetch(t *testing.T) { { after: 0 * time.Second, answer: "hits.reset.example.org. 80 IN A 127.0.0.1", - fetch: true, + fetch: true, // Initial fetch }, { after: 73 * time.Second, answer: "hits.reset.example.org. 7 IN A 127.0.0.1", - fetch: true, + fetch: true, // Triggers prefetch with 7 TTL (10% of 80 = 8 TTL threshold) }, { after: 80 * time.Second, @@ -91,6 +91,68 @@ func TestPrefetch(t *testing.T) { }, }, }, + { + // tests whether cache prefetches with the do bit + qname: "do.prefetch.example.org.", + ttl: 80, + prefetch: 1, + verifications: []verification{ + { + after: 0 * time.Second, + answer: "do.prefetch.example.org. 80 IN A 127.0.0.1", + do: true, + fetch: true, + }, + { + after: 73 * time.Second, + answer: "do.prefetch.example.org. 7 IN A 127.0.0.1", + do: true, + fetch: true, + }, + { + after: 80 * time.Second, + answer: "do.prefetch.example.org. 73 IN A 127.0.0.2", + do: true, + }, + { + // Should be 127.0.0.3 as 127.0.0.2 was the prefetch WITH do bit + after: 80 * time.Second, + answer: "do.prefetch.example.org. 80 IN A 127.0.0.3", + fetch: true, + }, + }, + }, + { + // tests whether cache prefetches with the cd bit + qname: "cd.prefetch.example.org.", + ttl: 80, + prefetch: 1, + verifications: []verification{ + { + after: 0 * time.Second, + answer: "cd.prefetch.example.org. 80 IN A 127.0.0.1", + cd: true, + fetch: true, + }, + { + after: 73 * time.Second, + answer: "cd.prefetch.example.org. 7 IN A 127.0.0.1", + cd: true, + fetch: true, + }, + { + after: 80 * time.Second, + answer: "cd.prefetch.example.org. 73 IN A 127.0.0.2", + cd: true, + }, + { + // Should be 127.0.0.3 as 127.0.0.2 was the prefetch WITH cd bit + after: 80 * time.Second, + answer: "cd.prefetch.example.org. 80 IN A 127.0.0.3", + fetch: true, + }, + }, + }, } t0, err := time.Parse(time.RFC3339, "2018-01-01T14:00:00+00:00") @@ -102,28 +164,29 @@ func TestPrefetch(t *testing.T) { fetchc := make(chan struct{}, 1) c := New() - c.prefetch = tt.prefetch c.Next = prefetchHandler(tt.qname, tt.ttl, fetchc) + c.prefetch = tt.prefetch - req := new(dns.Msg) - req.SetQuestion(tt.qname, dns.TypeA) rec := dnstest.NewRecorder(&test.ResponseWriter{}) for _, v := range tt.verifications { c.now = func() time.Time { return t0.Add(v.after) } + req := new(dns.Msg) + req.SetQuestion(tt.qname, dns.TypeA) + req.CheckingDisabled = v.cd + req.SetEdns0(512, v.do) + c.ServeDNS(context.TODO(), rec, req) if v.fetch { select { case <-fetchc: - if !v.fetch { - t.Fatalf("After %s: want request to trigger a prefetch", v.after) - } + // Prefetch handler was called. case <-time.After(time.Second): t.Fatalf("After %s: want request to trigger a prefetch", v.after) } } - if want, got := rec.Rcode, dns.RcodeSuccess; want != got { + if want, got := dns.RcodeSuccess, rec.Rcode; want != got { t.Errorf("After %s: want rcode %d, got %d", v.after, want, got) } if want, got := 1, len(rec.Msg.Answer); want != got { @@ -140,6 +203,8 @@ func TestPrefetch(t *testing.T) { type verification struct { after time.Duration answer string + do bool + cd bool // fetch defines whether a request is sent to the next handler. fetch bool } diff --git a/plugin/cache/setup.go b/plugin/cache/setup.go index d936db895d..f8278b872b 100644 --- a/plugin/cache/setup.go +++ b/plugin/cache/setup.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strconv" + "strings" "time" "github.com/coredns/caddy" @@ -22,6 +23,12 @@ func setup(c *caddy.Controller) error { if err != nil { return plugin.Error("cache", err) } + + c.OnStartup(func() error { + ca.viewMetricLabel = dnsserver.GetConfig(c).ViewName + return nil + }) + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { ca.Next = next return ca @@ -41,10 +48,7 @@ func cacheParse(c *caddy.Controller) (*Cache, error) { j++ // cache [ttl] [zones..] - origins := make([]string, len(c.ServerBlockKeys)) - copy(origins, c.ServerBlockKeys) args := c.RemainingArgs() - if len(args) > 0 { // first args may be just a number, then it is the ttl, if not it is a zone ttl, err := strconv.Atoi(args[0]) @@ -57,10 +61,8 @@ func cacheParse(c *caddy.Controller) (*Cache, error) { ca.nttl = time.Duration(ttl) * time.Second args = args[1:] } - if len(args) > 0 { - copy(origins, args) - } } + origins := plugin.OriginsFromArgsOrServerBlock(args, c.ServerBlockKeys) // Refinements? In an extra block. for c.NextBlock() { @@ -170,11 +172,11 @@ func cacheParse(c *caddy.Controller) (*Cache, error) { case "serve_stale": args := c.RemainingArgs() - if len(args) > 1 { + if len(args) > 2 { return nil, c.ArgErr() } ca.staleUpTo = 1 * time.Hour - if len(args) == 1 { + if len(args) > 0 { d, err := time.ParseDuration(args[0]) if err != nil { return nil, err @@ -184,16 +186,73 @@ func cacheParse(c *caddy.Controller) (*Cache, error) { } ca.staleUpTo = d } + ca.verifyStale = false + if len(args) > 1 { + mode := strings.ToLower(args[1]) + if mode != "immediate" && mode != "verify" { + return nil, fmt.Errorf("invalid value for serve_stale refresh mode: %s", mode) + } + ca.verifyStale = mode == "verify" + } + case "servfail": + args := c.RemainingArgs() + if len(args) != 1 { + return nil, c.ArgErr() + } + d, err := time.ParseDuration(args[0]) + if err != nil { + return nil, err + } + if d < 0 { + return nil, errors.New("invalid negative ttl for servfail") + } + if d > 5*time.Minute { + // RFC 2308 prohibits caching SERVFAIL longer than 5 minutes + return nil, errors.New("caching SERVFAIL responses over 5 minutes is not permitted") + } + ca.failttl = d + case "disable": + // disable [success|denial] [zones]... + args := c.RemainingArgs() + if len(args) < 1 { + return nil, c.ArgErr() + } + + var zones []string + if len(args) > 1 { + for _, z := range args[1:] { // args[1:] define the list of zones to disable + nz := plugin.Name(z).Normalize() + if nz == "" { + return nil, fmt.Errorf("invalid disabled zone: %s", z) + } + zones = append(zones, nz) + } + } else { + // if no zones specified, default to root + zones = []string{"."} + } + + switch args[0] { // args[0] defines which cache to disable + case Denial: + ca.nexcept = zones + case Success: + ca.pexcept = zones + default: + return nil, fmt.Errorf("cache type for disable must be %q or %q", Success, Denial) + } + case "keepttl": + args := c.RemainingArgs() + if len(args) != 0 { + return nil, c.ArgErr() + } + ca.keepttl = true default: return nil, c.ArgErr() } } - for i := range origins { - origins[i] = plugin.Host(origins[i]).Normalize() - } ca.Zones = origins - + ca.zonesMetricLabel = strings.Join(origins, ",") ca.pcache = cache.New(ca.pcap) ca.ncache = cache.New(ca.ncap) } diff --git a/plugin/cache/setup_test.go b/plugin/cache/setup_test.go index 875af7d030..46ac5bd9e4 100644 --- a/plugin/cache/setup_test.go +++ b/plugin/cache/setup_test.go @@ -117,20 +117,25 @@ func TestSetup(t *testing.T) { func TestServeStale(t *testing.T) { tests := []struct { - input string - shouldErr bool - staleUpTo time.Duration + input string + shouldErr bool + staleUpTo time.Duration + verifyStale bool }{ - {"serve_stale", false, 1 * time.Hour}, - {"serve_stale 20m", false, 20 * time.Minute}, - {"serve_stale 1h20m", false, 80 * time.Minute}, - {"serve_stale 0m", false, 0}, - {"serve_stale 0", false, 0}, + {"serve_stale", false, 1 * time.Hour, false}, + {"serve_stale 20m", false, 20 * time.Minute, false}, + {"serve_stale 1h20m", false, 80 * time.Minute, false}, + {"serve_stale 0m", false, 0, false}, + {"serve_stale 0", false, 0, false}, + {"serve_stale 0 verify", false, 0, true}, + {"serve_stale 0 immediate", false, 0, false}, + {"serve_stale 0 VERIFY", false, 0, true}, // fails - {"serve_stale 20", true, 0}, - {"serve_stale -20m", true, 0}, - {"serve_stale aa", true, 0}, - {"serve_stale 1m nono", true, 0}, + {"serve_stale 20", true, 0, false}, + {"serve_stale -20m", true, 0, false}, + {"serve_stale aa", true, 0, false}, + {"serve_stale 1m nono", true, 0, false}, + {"serve_stale 0 after nono", true, 0, false}, } for i, test := range tests { c := caddy.NewTestController("dns", fmt.Sprintf("cache {\n%s\n}", test.input)) @@ -150,3 +155,108 @@ func TestServeStale(t *testing.T) { } } } + +func TestServfail(t *testing.T) { + tests := []struct { + input string + shouldErr bool + failttl time.Duration + }{ + {"servfail 1s", false, 1 * time.Second}, + {"servfail 5m", false, 5 * time.Minute}, + {"servfail 0s", false, 0}, + {"servfail 0", false, 0}, + // fails + {"servfail", true, minNTTL}, + {"servfail 6m", true, minNTTL}, + {"servfail 20", true, minNTTL}, + {"servfail -1s", true, minNTTL}, + {"servfail aa", true, minNTTL}, + {"servfail 1m invalid", true, minNTTL}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", fmt.Sprintf("cache {\n%s\n}", test.input)) + ca, err := cacheParse(c) + if test.shouldErr && err == nil { + t.Errorf("Test %v: Expected error but found nil", i) + continue + } else if !test.shouldErr && err != nil { + t.Errorf("Test %v: Expected no error but found error: %v", i, err) + continue + } + if test.shouldErr && err != nil { + continue + } + if ca.failttl != test.failttl { + t.Errorf("Test %v: Expected stale %v but found: %v", i, test.failttl, ca.staleUpTo) + } + } +} + +func TestDisable(t *testing.T) { + tests := []struct { + input string + shouldErr bool + nexcept []string + pexcept []string + }{ + // positive + {"disable denial example.com example.org", false, []string{"example.com.", "example.org."}, nil}, + {"disable success example.com example.org", false, nil, []string{"example.com.", "example.org."}}, + {"disable denial", false, []string{"."}, nil}, + {"disable success", false, nil, []string{"."}}, + {"disable denial example.com example.org\ndisable success example.com example.org", false, + []string{"example.com.", "example.org."}, []string{"example.com.", "example.org."}}, + // negative + {"disable invalid example.com example.org", true, nil, nil}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", fmt.Sprintf("cache {\n%s\n}", test.input)) + ca, err := cacheParse(c) + if test.shouldErr && err == nil { + t.Errorf("Test %v: Expected error but found nil", i) + continue + } else if !test.shouldErr && err != nil { + t.Errorf("Test %v: Expected no error but found error: %v", i, err) + continue + } + if test.shouldErr { + continue + } + if fmt.Sprintf("%v", test.nexcept) != fmt.Sprintf("%v", ca.nexcept) { + t.Errorf("Test %v: Expected %v but got: %v", i, test.nexcept, ca.nexcept) + } + if fmt.Sprintf("%v", test.pexcept) != fmt.Sprintf("%v", ca.pexcept) { + t.Errorf("Test %v: Expected %v but got: %v", i, test.pexcept, ca.pexcept) + } + } +} + +func TestKeepttl(t *testing.T) { + tests := []struct { + input string + shouldErr bool + }{ + // positive + {"keepttl", false}, + // negative + {"keepttl arg1", true}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", fmt.Sprintf("cache {\n%s\n}", test.input)) + ca, err := cacheParse(c) + if test.shouldErr && err == nil { + t.Errorf("Test %v: Expected error but found nil", i) + continue + } else if !test.shouldErr && err != nil { + t.Errorf("Test %v: Expected no error but found error: %v", i, err) + continue + } + if test.shouldErr { + continue + } + if !ca.keepttl { + t.Errorf("Test %v: Expected keepttl enabled but disabled", i) + } + } +} diff --git a/plugin/chaos/chaos_test.go b/plugin/chaos/chaos_test.go index 12cc1697ec..e5d4a55aa0 100644 --- a/plugin/chaos/chaos_test.go +++ b/plugin/chaos/chaos_test.go @@ -65,7 +65,7 @@ func TestChaos(t *testing.T) { if err != tc.expectedErr { t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expectedErr, err) } - if code != int(tc.expectedCode) { + if code != tc.expectedCode { t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code) } if tc.expectedReply != "" { diff --git a/plugin/chaos/fuzz.go b/plugin/chaos/fuzz.go index 53667f2cd8..001cf1dee4 100644 --- a/plugin/chaos/fuzz.go +++ b/plugin/chaos/fuzz.go @@ -1,4 +1,4 @@ -// +build gofuzz +//go:build gofuzz package chaos diff --git a/plugin/chaos/setup.go b/plugin/chaos/setup.go index de3622fe56..ce0eb7a686 100644 --- a/plugin/chaos/setup.go +++ b/plugin/chaos/setup.go @@ -30,7 +30,7 @@ func parse(c *caddy.Controller) (string, []string, error) { chaosVersion = caddy.AppName + "-" + caddy.AppVersion version := "" - for c.Next() { + if c.Next() { args := c.RemainingArgs() if len(args) == 0 { return trim(chaosVersion), Owners, nil diff --git a/plugin/chaos/zowners.go b/plugin/chaos/zowners.go index cfc2d89a01..419ca3cf5a 100644 --- a/plugin/chaos/zowners.go +++ b/plugin/chaos/zowners.go @@ -1,4 +1,4 @@ package chaos // Owners are all GitHub handlers of all maintainers. -var Owners = []string{"bradbeam", "chrisohaver", "darshanime", "dilyevsky", "ekleiner", "fastest963", "greenpau", "ihac", "inigohu", "isolus", "johnbelamaric", "miekg", "nchrisdk", "nitisht", "pmoroney", "rajansandeep", "rdrozhdzh", "rtreffer", "stp-ip", "superq", "varyoo", "ykhr53", "yongtang", "zouyee"} +var Owners = []string{"Tantalor93", "bradbeam", "chrisohaver", "darshanime", "dilyevsky", "ekleiner", "greenpau", "ihac", "inigohu", "isolus", "jameshartig", "johnbelamaric", "miekg", "mqasimsarfraz", "nchrisdk", "nitisht", "pmoroney", "rajansandeep", "rdrozhdzh", "rtreffer", "snebel29", "stp-ip", "superq", "varyoo", "ykhr53", "yongtang", "zouyee"} diff --git a/plugin/clouddns/README.md b/plugin/clouddns/README.md index 6ff4a2b243..1e122813f8 100644 --- a/plugin/clouddns/README.md +++ b/plugin/clouddns/README.md @@ -34,6 +34,9 @@ clouddns [ZONE:PROJECT_ID:HOSTED_ZONE_NAME...] { accessed. * `credentials` is used for reading the credential file from **FILENAME** (normally a .json file). + This field is optional. If this field is not provided then authentication will be done automatically, + e.g., through environmental variable `GOOGLE_APPLICATION_CREDENTIALS`. Please see + Google Cloud's [authentication method](https://cloud.google.com/docs/authentication) for more details. * `fallthrough` If zone matches and no record can be generated, pass request to the next plugin. If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin is diff --git a/plugin/clouddns/clouddns.go b/plugin/clouddns/clouddns.go index 371832440a..6c912a7d60 100644 --- a/plugin/clouddns/clouddns.go +++ b/plugin/clouddns/clouddns.go @@ -82,12 +82,16 @@ func (h *CloudDNS) Run(ctx context.Context) error { return err } go func() { + delay := 1 * time.Minute + timer := time.NewTimer(delay) + defer timer.Stop() for { + timer.Reset(delay) select { case <-ctx.Done(): log.Debugf("Breaking out of CloudDNS update loop for %v: %v", h.zoneNames, ctx.Err()) return - case <-time.After(1 * time.Minute): + case <-timer.C: if err := h.updateZones(ctx); err != nil && ctx.Err() == nil /* Don't log error if ctx expired. */ { log.Errorf("Failed to update zones %v: %v", h.zoneNames, err) } @@ -157,16 +161,18 @@ func updateZoneFromRRS(rrs *gcp.ResourceRecordSetsListResponse, z *file.Zone) er if rr.Type == "CNAME" || rr.Type == "PTR" { value = dns.Fqdn(value) } - // Assemble RFC 1035 conforming record to pass into dns scanner. rfc1035 = fmt.Sprintf("%s %d IN %s %s", dns.Fqdn(rr.Name), rr.Ttl, rr.Type, value) r, err = dns.NewRR(rfc1035) if err != nil { return fmt.Errorf("failed to parse resource record: %v", err) } - } - z.Insert(r) + err = z.Insert(r) + if err != nil { + return fmt.Errorf("failed to insert record: %v", err) + } + } } return nil } @@ -200,13 +206,12 @@ func (h *CloudDNS) updateZones(ctx context.Context) error { (*z[i]).z = newZ h.zMu.Unlock() } - }(zName, z) } // Collect errors (if any). This will also sync on all zones updates // completion. var errs []string - for i := 0; i < len(h.zones); i++ { + for range len(h.zones) { err := <-errc if err != nil { errs = append(errs, err.Error()) diff --git a/plugin/clouddns/clouddns_test.go b/plugin/clouddns/clouddns_test.go index f394f0593c..269be2b042 100644 --- a/plugin/clouddns/clouddns_test.go +++ b/plugin/clouddns/clouddns_test.go @@ -4,8 +4,10 @@ import ( "context" "errors" "reflect" + "sync" "testing" + "github.com/coredns/coredns/plugin/file" "github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/plugin/pkg/upstream" @@ -114,6 +116,15 @@ func (c fakeGCPClient) listRRSets(ctx context.Context, projectName, hostedZoneNa Type: "SOA", Rrdatas: []string{"ns-cloud-e1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"}, }, + { + Name: "_dummy._tcp.example.org.", + Ttl: 300, + Type: "SRV", + Rrdatas: []string{ + "0 0 5269 split-example.org", + "0 0 5269 other-example.org", + }, + }, } } @@ -152,7 +163,6 @@ func TestCloudDNS(t *testing.T) { m.Authoritative = true rcode = dns.RcodeSuccess - } m.SetRcode(r, rcode) @@ -175,20 +185,20 @@ func TestCloudDNS(t *testing.T) { }{ // 0. example.org A found - success. { - qname: "example.org", - qtype: dns.TypeA, + qname: "example.org", + qtype: dns.TypeA, wantAnswer: []string{"example.org. 300 IN A 1.2.3.4"}, }, // 1. example.org AAAA found - success. { - qname: "example.org", - qtype: dns.TypeAAAA, + qname: "example.org", + qtype: dns.TypeAAAA, wantAnswer: []string{"example.org. 300 IN AAAA 2001:db8:85a3::8a2e:370:7334"}, }, // 2. exampled.org PTR found - success. { - qname: "example.org", - qtype: dns.TypePTR, + qname: "example.org", + qtype: dns.TypePTR, wantAnswer: []string{"example.org. 300 IN PTR ptr.example.org."}, }, // 3. sample.example.org points to example.org CNAME. @@ -204,14 +214,14 @@ func TestCloudDNS(t *testing.T) { // 4. Explicit CNAME query for sample.example.org. // Query must return just CNAME. { - qname: "sample.example.org", - qtype: dns.TypeCNAME, + qname: "sample.example.org", + qtype: dns.TypeCNAME, wantAnswer: []string{"sample.example.org. 300 IN CNAME example.org."}, }, // 5. Explicit SOA query for example.org. { - qname: "example.org", - qtype: dns.TypeNS, + qname: "example.org", + qtype: dns.TypeNS, wantNS: []string{"org. 300 IN SOA ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"}, }, // 6. AAAA query for split-example.org must return NODATA. @@ -219,7 +229,7 @@ func TestCloudDNS(t *testing.T) { qname: "split-example.gov", qtype: dns.TypeAAAA, wantRetCode: dns.RcodeSuccess, - wantNS: []string{"org. 300 IN SOA ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"}, + wantNS: []string{"org. 300 IN SOA ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"}, }, // 7. Zone not configured. { @@ -234,24 +244,24 @@ func TestCloudDNS(t *testing.T) { qtype: dns.TypeA, wantRetCode: dns.RcodeSuccess, wantMsgRCode: dns.RcodeNameError, - wantNS: []string{"org. 300 IN SOA ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"}, + wantNS: []string{"org. 300 IN SOA ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"}, }, // 9. No record found. Fallthrough. { - qname: "example.gov", - qtype: dns.TypeA, + qname: "example.gov", + qtype: dns.TypeA, wantAnswer: []string{"example.gov. 300 IN A 2.4.6.8"}, }, // 10. other-zone.example.org is stored in a different hosted zone. success { - qname: "other-example.org", - qtype: dns.TypeA, + qname: "other-example.org", + qtype: dns.TypeA, wantAnswer: []string{"other-example.org. 300 IN A 3.5.7.9"}, }, // 11. split-example.org only has A record. Expect NODATA. { - qname: "split-example.org", - qtype: dns.TypeAAAA, + qname: "split-example.org", + qtype: dns.TypeAAAA, wantNS: []string{"org. 300 IN SOA ns-cloud-e1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"}, }, // 12. *.www.example.org is a wildcard CNAME to www.example.org. @@ -263,6 +273,15 @@ func TestCloudDNS(t *testing.T) { "www.example.org. 300 IN A 1.2.3.4", }, }, + // 13. example.org SRV found with 2 answers - success. + { + qname: "_dummy._tcp.example.org.", + qtype: dns.TypeSRV, + wantAnswer: []string{ + "_dummy._tcp.example.org. 300 IN SRV 0 0 5269 split-example.org.", + "_dummy._tcp.example.org. 300 IN SRV 0 0 5269 other-example.org.", + }, + }, } for ti, tc := range tests { @@ -275,7 +294,7 @@ func TestCloudDNS(t *testing.T) { if err != tc.expectedErr { t.Fatalf("Test %d: Expected error %v, but got %v", ti, tc.expectedErr, err) } - if code != int(tc.wantRetCode) { + if code != tc.wantRetCode { t.Fatalf("Test %d: Expected returned status code %s, but got %s", ti, dns.RcodeToString[tc.wantRetCode], dns.RcodeToString[code]) } @@ -308,3 +327,132 @@ func TestCloudDNS(t *testing.T) { } } } + +// TestCloudDNSConcurrentServeDNS stresses r.ServeDNS directly to trigger +// concurrent Elem.Name() initialization from the file plugin. +func TestCloudDNSConcurrentServeDNS(t *testing.T) { + ctx := context.Background() + + r, err := New(ctx, + fakeGCPClient{}, + map[string][]string{ + "org.": {"sample-project-1:sample-zone-2", "sample-project-1:sample-zone-1"}, + }, + &upstream.Upstream{}, + ) + if err != nil { + t.Fatalf("Failed to create Cloud DNS: %v", err) + } + if err := r.Run(ctx); err != nil { + t.Fatalf("Failed to initialize Cloud DNS: %v", err) + } + + queries := []struct { + qname string + qtype uint16 + }{ + {"example.org", dns.TypeA}, + {"example.org", dns.TypeAAAA}, + {"sample.example.org", dns.TypeA}, + {"a.www.example.org", dns.TypeA}, + {"split-example.org", dns.TypeA}, + {"other-example.org", dns.TypeA}, + {"_dummy._tcp.example.org.", dns.TypeSRV}, + } + + var wg sync.WaitGroup + + // Concurrently refresh zones to race with Lookup reads. + wg.Add(1) + go func() { + defer wg.Done() + for range 50 { + _ = r.updateZones(ctx) + } + }() + + const workers = 32 + const iterations = 200 + for w := range workers { + wg.Add(1) + go func(id int) { + defer wg.Done() + for i := range iterations { + tc := queries[(id+i)%len(queries)] + req := new(dns.Msg) + req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + code, err := r.ServeDNS(ctx, rec, req) + if err != nil { + t.Errorf("ServeDNS error: %v", err) + return + } + if code != dns.RcodeSuccess { + t.Errorf("unexpected return code: %v", code) + return + } + } + }(w) + } + + wg.Wait() +} + +// TestCloudDNSConcurrentLookupNameCache stresses hostedZone.z.Lookup +// directly to trigger concurrent Elem.Name() initialization from the file plugin. +func TestCloudDNSConcurrentLookupNameCache(t *testing.T) { + ctx := context.Background() + + r, err := New(ctx, + fakeGCPClient{}, + map[string][]string{ + "org.": {"sample-project-1:sample-zone-2", "sample-project-1:sample-zone-1"}, + }, + &upstream.Upstream{}) + if err != nil { + t.Fatalf("Failed to create Cloud DNS: %v", err) + } + if err := r.Run(ctx); err != nil { + t.Fatalf("Failed to initialize Cloud DNS: %v", err) + } + + // Use a fixed set of qnames that exist in the zones to maximize reuse of the same Elems. + queries := []struct { + qname string + qtype uint16 + }{ + {"example.org.", dns.TypeA}, + {"example.org.", dns.TypeAAAA}, + {"sample.example.org.", dns.TypeA}, + {"a.www.example.org.", dns.TypeA}, + {"split-example.org.", dns.TypeA}, + {"other-example.org.", dns.TypeA}, + {"_dummy._tcp.example.org.", dns.TypeSRV}, + } + + // Fan-out goroutines that repeatedly call Lookup on the same Zone pointer. + const workers = 32 + const iterations = 300 + + var wg sync.WaitGroup + for _, hostedZones := range r.zones { + for _, hz := range hostedZones { + z := hz.z + wg.Add(workers) + for w := range workers { + go func(id int, zptr *file.Zone) { + defer wg.Done() + for i := range iterations { + tc := queries[(id+i)%len(queries)] + req := new(dns.Msg) + req.SetQuestion(tc.qname, tc.qtype) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + state := request.Request{W: rec, Req: req} + _, _, _, _ = zptr.Lookup(ctx, state, tc.qname) + } + }(w, z) + } + } + } + wg.Wait() +} diff --git a/plugin/clouddns/gcp.go b/plugin/clouddns/gcp.go index 0f5126c874..b02ab2bdaf 100644 --- a/plugin/clouddns/gcp.go +++ b/plugin/clouddns/gcp.go @@ -31,9 +31,7 @@ func (c gcpClient) listRRSets(ctx context.Context, projectName, hostedZoneName s req := c.ResourceRecordSets.List(projectName, hostedZoneName) var rs []*gcp.ResourceRecordSet if err := req.Pages(ctx, func(page *gcp.ResourceRecordSetsListResponse) error { - for _, rr := range page.Rrsets { - rs = append(rs, rr) - } + rs = append(rs, page.Rrsets...) return nil }); err != nil { return nil, err diff --git a/plugin/clouddns/setup.go b/plugin/clouddns/setup.go index 507ae278e1..343f6c7051 100644 --- a/plugin/clouddns/setup.go +++ b/plugin/clouddns/setup.go @@ -43,7 +43,7 @@ func setup(c *caddy.Controller) error { args := c.RemainingArgs() - for i := 0; i < len(args); i++ { + for i := range args { parts := strings.SplitN(args[i], ":", 3) if len(parts) != 3 { return plugin.Error("clouddns", c.Errf("invalid zone %q", args[i])) @@ -81,16 +81,19 @@ func setup(c *caddy.Controller) error { ctx, cancel := context.WithCancel(context.Background()) client, err := f(ctx, opt) if err != nil { + cancel() return err } h, err := New(ctx, client, keys, up) if err != nil { + cancel() return plugin.Error("clouddns", c.Errf("failed to create plugin: %v", err)) } h.Fall = fall if err := h.Run(ctx); err != nil { + cancel() return plugin.Error("clouddns", c.Errf("failed to initialize plugin: %v", err)) } diff --git a/plugin/debug/pcap.go b/plugin/debug/pcap.go index 493478a409..a6c491ee41 100644 --- a/plugin/debug/pcap.go +++ b/plugin/debug/pcap.go @@ -22,7 +22,7 @@ import ( // is prefixed with 'debug: ' so the data can be easily extracted. // // msg will prefix the pcap dump. -func Hexdump(m *dns.Msg, v ...interface{}) { +func Hexdump(m *dns.Msg, v ...any) { if !log.D.Value() { return } @@ -38,7 +38,7 @@ func Hexdump(m *dns.Msg, v ...interface{}) { } // Hexdumpf dumps a DNS message as Hexdump, but allows a format string. -func Hexdumpf(m *dns.Msg, format string, v ...interface{}) { +func Hexdumpf(m *dns.Msg, format string, v ...any) { if !log.D.Value() { return } @@ -57,7 +57,7 @@ func hexdump(data []byte) []byte { b := new(bytes.Buffer) newline := "" - for i := 0; i < len(data); i++ { + for i := range data { if i%16 == 0 { fmt.Fprintf(b, "%s%s%06x", newline, prefix, i) newline = "\n" diff --git a/plugin/debug/pcap_test.go b/plugin/debug/pcap_test.go index b2796e3996..6b263c8839 100644 --- a/plugin/debug/pcap_test.go +++ b/plugin/debug/pcap_test.go @@ -32,7 +32,7 @@ func TestNoDebug(t *testing.T) { } } -func ExampleLogHexdump() { +func ExampleHexdump() { buf, _ := msg().Pack() h := hexdump(buf) fmt.Println(string(h)) diff --git a/plugin/dns64/README.md b/plugin/dns64/README.md index 65a338b638..7975574854 100644 --- a/plugin/dns64/README.md +++ b/plugin/dns64/README.md @@ -27,11 +27,13 @@ Or use this slightly longer form with more options: dns64 [PREFIX] { [translate_all] prefix PREFIX + [allow_ipv4] } ~~~ * `prefix` specifies any local IPv6 prefix to use, instead of the well known prefix (64:ff9b::/96) * `translate_all` translates all queries, including responses that have AAAA results. +* `allow_ipv4` Allow translating queries if they come in over IPv4, default is IPv6 only translation. ## Examples @@ -70,6 +72,19 @@ Enable translation even if an existing AAAA record is present. } ~~~ +Apply translation even to the requests which arrived over IPv4 network. Warning, the `allow_ipv4` feature will apply +translations to requests coming from dual-stack clients. This means that a request for a client that sends an `AAAA` +that would normal result in an `NXDOMAIN` would get a translated result. +This may cause unwanted IPv6 dns64 traffic when a dualstack client would normally use the result of an `A` record request. + +~~~ corefile +. { + dns64 { + allow_ipv4 + } +} +~~~ + ## Metrics If monitoring is enabled (via the _prometheus_ plugin) then the following metrics are exported: diff --git a/plugin/dns64/dns64.go b/plugin/dns64/dns64.go index b06b0bdb9f..01d98923be 100644 --- a/plugin/dns64/dns64.go +++ b/plugin/dns64/dns64.go @@ -28,14 +28,15 @@ type DNS64 struct { Next plugin.Handler Prefix *net.IPNet TranslateAll bool // Not comply with 5.1.1 + AllowIPv4 bool Upstream UpstreamInt } // ServeDNS implements the plugin.Handler interface. func (d *DNS64) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { // Don't proxy if we don't need to. - if !requestShouldIntercept(&request.Request{W: w, Req: r}) { - return d.Next.ServeDNS(ctx, w, r) + if !d.requestShouldIntercept(&request.Request{W: w, Req: r}) { + return plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r) } // Pass the request to the next plugin in the chain, but intercept the response. @@ -61,7 +62,7 @@ func (d *DNS64) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) RequestsTranslatedCount.WithLabelValues(metrics.WithServer(ctx)).Inc() w.WriteMsg(msg) - return msg.MsgHdr.Rcode, nil + return msg.Rcode, nil } // Name implements the Handler interface. @@ -69,13 +70,13 @@ func (d *DNS64) Name() string { return "dns64" } // requestShouldIntercept returns true if the request represents one that is eligible // for DNS64 rewriting: -// 1. The request came in over IPv6 (not in RFC) +// 1. The request came in over IPv6 or the 'allow_ipv4' option is set // 2. The request is of type AAAA // 3. The request is of class INET -func requestShouldIntercept(req *request.Request) bool { - // Only intercept with this when the request came in over IPv6. This is not mentioned in the RFC. - // File an issue if you think we should translate even requests made using IPv4, or have a configuration flag - if req.Family() == 1 { // If it came in over v4, don't do anything. +func (d *DNS64) requestShouldIntercept(req *request.Request) bool { + // Make sure that request came in over IPv4 unless AllowIPv4 option is enabled. + // Translating requests without taking into consideration client (source) IP might be problematic in dual-stack networks. + if !d.AllowIPv4 && req.Family() == 1 { return false } @@ -130,6 +131,9 @@ func (d *DNS64) Synthesize(origReq, origResponse, resp *dns.Msg) *dns.Msg { ret := dns.Msg{} ret.SetReply(origReq) + // persist truncated state of AAAA response + ret.Truncated = resp.Truncated + // 5.3.2: DNS64 MUST pass the additional section unchanged ret.Extra = resp.Extra ret.Ns = resp.Ns @@ -156,10 +160,7 @@ func (d *DNS64) Synthesize(origReq, origResponse, resp *dns.Msg) *dns.Msg { aaaa, _ := to6(d.Prefix, rr.(*dns.A).A) // ttl is min of SOA TTL and A TTL - ttl := SOATtl - if rr.Header().Ttl < ttl { - ttl = rr.Header().Ttl - } + ttl := min(rr.Header().Ttl, SOATtl) // Replace A answer with a DNS64 AAAA answer ret.Answer = append(ret.Answer, &dns.AAAA{ diff --git a/plugin/dns64/dns64_test.go b/plugin/dns64/dns64_test.go index d7f9fdade5..a294721dc6 100644 --- a/plugin/dns64/dns64_test.go +++ b/plugin/dns64/dns64_test.go @@ -21,8 +21,59 @@ func To6(prefix, address string) (net.IP, error) { return to6(pref, addr) } -func TestTo6(t *testing.T) { +func TestRequestShouldIntercept(t *testing.T) { + tests := []struct { + name string + allowIpv4 bool + remoteIP string + msg *dns.Msg + want bool + }{ + { + name: "should intercept request from IPv6 network - AAAA - IN", + allowIpv4: true, + remoteIP: "::1", + msg: new(dns.Msg).SetQuestion("example.com", dns.TypeAAAA), + want: true, + }, + { + name: "should intercept request from IPv4 network - AAAA - IN", + allowIpv4: true, + remoteIP: "127.0.0.1", + msg: new(dns.Msg).SetQuestion("example.com", dns.TypeAAAA), + want: true, + }, + { + name: "should not intercept request from IPv4 network - AAAA - IN", + allowIpv4: false, + remoteIP: "127.0.0.1", + msg: new(dns.Msg).SetQuestion("example.com", dns.TypeAAAA), + want: false, + }, + { + name: "should not intercept request from IPv6 network - A - IN", + allowIpv4: false, + remoteIP: "::1", + msg: new(dns.Msg).SetQuestion("example.com", dns.TypeA), + want: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + h := DNS64{AllowIPv4: tc.allowIpv4} + rec := dnstest.NewRecorder(&test.ResponseWriter{RemoteIP: tc.remoteIP}) + r := request.Request{W: rec, Req: tc.msg} + + actual := h.requestShouldIntercept(&r) + + if actual != tc.want { + t.Fatalf("Expected %v, but got %v", tc.want, actual) + } + }) + } +} +func TestTo6(t *testing.T) { v6, err := To6("64:ff9b::/96", "64.64.64.64") if err != nil { t.Error(err) @@ -164,7 +215,7 @@ func TestDNS64(t *testing.T) { RecursionDesired: true, Opcode: dns.OpcodeQuery, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ //success, no answers MsgHdr: dns.MsgHdr{ @@ -174,7 +225,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeSuccess, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 70 IN SOA foo bar 1 1 1 1 1")}, }, aResp: &dns.Msg{ @@ -185,7 +236,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeSuccess, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.A("example.com. 60 IN A 192.0.2.42"), test.A("example.com. 5000 IN A 192.0.2.43"), @@ -200,7 +251,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeSuccess, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.AAAA("example.com. 60 IN AAAA 64:ff9b::192.0.2.42"), // override RR ttl to SOA ttl, since it's lower @@ -217,7 +268,7 @@ func TestDNS64(t *testing.T) { RecursionDesired: true, Opcode: dns.OpcodeQuery, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ //success, no answers MsgHdr: dns.MsgHdr{ @@ -227,7 +278,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeSuccess, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, aResp: &dns.Msg{ @@ -238,7 +289,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeSuccess, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, @@ -250,7 +301,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeSuccess, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, Answer: []dns.RR{}, // just to make comparison happy }, @@ -264,7 +315,7 @@ func TestDNS64(t *testing.T) { RecursionDesired: true, Opcode: dns.OpcodeQuery, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ // failure MsgHdr: dns.MsgHdr{ @@ -274,7 +325,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeRefused, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, }, aResp: &dns.Msg{ MsgHdr: dns.MsgHdr{ @@ -284,7 +335,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeSuccess, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.A("example.com. 60 IN A 192.0.2.42"), test.A("example.com. 5000 IN A 192.0.2.43"), @@ -299,7 +350,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeSuccess, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.AAAA("example.com. 60 IN AAAA 64:ff9b::192.0.2.42"), test.AAAA("example.com. 600 IN AAAA 64:ff9b::192.0.2.43"), @@ -315,7 +366,7 @@ func TestDNS64(t *testing.T) { RecursionDesired: true, Opcode: dns.OpcodeQuery, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ // failure MsgHdr: dns.MsgHdr{ @@ -325,7 +376,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeNameError, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, resp: &dns.Msg{ @@ -336,7 +387,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeNameError, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Ns: []dns.RR{test.SOA("example.com. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, }, }, @@ -349,7 +400,7 @@ func TestDNS64(t *testing.T) { RecursionDesired: true, Opcode: dns.OpcodeQuery, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, }, initResp: &dns.Msg{ @@ -360,7 +411,7 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeSuccess, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.AAAA("example.com. 60 IN AAAA ::1"), test.AAAA("example.com. 5000 IN AAAA ::2"), @@ -375,13 +426,68 @@ func TestDNS64(t *testing.T) { Rcode: dns.RcodeSuccess, Response: true, }, - Question: []dns.Question{{"example.com.", dns.TypeAAAA, dns.ClassINET}}, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, Answer: []dns.RR{ test.AAAA("example.com. 60 IN AAAA ::1"), test.AAAA("example.com. 5000 IN AAAA ::2"), }, }, }, + { + // no AAAA records, A record response truncated. + name: "truncated A response", + req: &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: 42, + RecursionDesired: true, + Opcode: dns.OpcodeQuery, + }, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, + }, + initResp: &dns.Msg{ //success, no answers + MsgHdr: dns.MsgHdr{ + Id: 42, + Opcode: dns.OpcodeQuery, + RecursionDesired: true, + Rcode: dns.RcodeSuccess, + Response: true, + }, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, + Ns: []dns.RR{test.SOA("example.com. 70 IN SOA foo bar 1 1 1 1 1")}, + }, + aResp: &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: 43, + Opcode: dns.OpcodeQuery, + RecursionDesired: true, + Truncated: true, + Rcode: dns.RcodeSuccess, + Response: true, + }, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, + Answer: []dns.RR{ + test.A("example.com. 60 IN A 192.0.2.42"), + test.A("example.com. 5000 IN A 192.0.2.43"), + }, + }, + + resp: &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: 42, + Opcode: dns.OpcodeQuery, + RecursionDesired: true, + Truncated: true, + Rcode: dns.RcodeSuccess, + Response: true, + }, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, + Answer: []dns.RR{ + test.AAAA("example.com. 60 IN AAAA 64:ff9b::192.0.2.42"), + // override RR ttl to SOA ttl, since it's lower + test.AAAA("example.com. 70 IN AAAA 64:ff9b::192.0.2.43"), + }, + }, + }, } _, pfx, _ := net.ParseCIDR("64:ff9b::/96") diff --git a/plugin/dns64/setup.go b/plugin/dns64/setup.go index 0c53fab4b0..5e061875f3 100644 --- a/plugin/dns64/setup.go +++ b/plugin/dns64/setup.go @@ -6,14 +6,11 @@ import ( "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" - clog "github.com/coredns/coredns/plugin/pkg/log" "github.com/coredns/coredns/plugin/pkg/upstream" ) const pluginName = "dns64" -var log = clog.NewWithPlugin(pluginName) - func init() { plugin.Register(pluginName, setup) } func setup(c *caddy.Controller) error { @@ -66,6 +63,8 @@ func dns64Parse(c *caddy.Controller) (*DNS64, error) { dns64.Prefix = pref case "translate_all": dns64.TranslateAll = true + case "allow_ipv4": + dns64.AllowIPv4 = true default: return nil, c.Errf("unknown property '%s'", c.Val()) } diff --git a/plugin/dns64/setup_test.go b/plugin/dns64/setup_test.go index 3b4a4fe0ec..e7d13f4183 100644 --- a/plugin/dns64/setup_test.go +++ b/plugin/dns64/setup_test.go @@ -10,17 +10,20 @@ func TestSetupDns64(t *testing.T) { tests := []struct { inputUpstreams string shouldErr bool - prefix string + wantPrefix string + wantAllowIpv4 bool }{ { `dns64`, false, "64:ff9b::/96", + false, }, { `dns64 64:dead::/96`, false, "64:dead::/96", + false, }, { `dns64 { @@ -28,11 +31,13 @@ func TestSetupDns64(t *testing.T) { }`, false, "64:ff9b::/96", + false, }, { `dns64`, false, "64:ff9b::/96", + false, }, { `dns64 { @@ -40,6 +45,7 @@ func TestSetupDns64(t *testing.T) { }`, false, "64:ff9b::/96", + false, }, { `dns64 { @@ -47,6 +53,7 @@ func TestSetupDns64(t *testing.T) { }`, false, "64:ff9b::/32", + false, }, { `dns64 { @@ -54,6 +61,7 @@ func TestSetupDns64(t *testing.T) { }`, true, "64:ff9b::/52", + false, }, { `dns64 { @@ -61,6 +69,7 @@ func TestSetupDns64(t *testing.T) { }`, true, "64:ff9b::/104", + false, }, { `dns64 { @@ -68,6 +77,7 @@ func TestSetupDns64(t *testing.T) { }`, true, "8.8.9.9/24", + false, }, { `dns64 { @@ -75,6 +85,7 @@ func TestSetupDns64(t *testing.T) { }`, false, "64:ff9b::/96", + false, }, { `dns64 { @@ -82,6 +93,7 @@ func TestSetupDns64(t *testing.T) { }`, false, "2002:ac12:b083::/96", + false, }, { `dns64 { @@ -89,6 +101,7 @@ func TestSetupDns64(t *testing.T) { }`, false, "2002:c0a8:a88a::/48", + false, }, { `dns64 foobar { @@ -96,11 +109,13 @@ func TestSetupDns64(t *testing.T) { }`, true, "64:ff9b::/96", + false, }, { `dns64 foobar`, true, "64:ff9b::/96", + false, }, { `dns64 { @@ -108,6 +123,15 @@ func TestSetupDns64(t *testing.T) { }`, true, "64:ff9b::/96", + false, + }, + { + `dns64 { + allow_ipv4 + }`, + false, + "64:ff9b::/96", + true, }, } @@ -118,8 +142,11 @@ func TestSetupDns64(t *testing.T) { t.Errorf("Test %d expected %v error, got %v for %s", i+1, test.shouldErr, err, test.inputUpstreams) } if err == nil { - if dns64.Prefix.String() != test.prefix { - t.Errorf("Test %d expected prefix %s, got %v", i+1, test.prefix, dns64.Prefix.String()) + if dns64.Prefix.String() != test.wantPrefix { + t.Errorf("Test %d expected prefix %s, got %v", i+1, test.wantPrefix, dns64.Prefix.String()) + } + if dns64.AllowIPv4 != test.wantAllowIpv4 { + t.Errorf("Test %d expected prefix %v, got %v", i+1, test.wantAllowIpv4, dns64.AllowIPv4) } } } diff --git a/plugin/dnssec/README.md b/plugin/dnssec/README.md index 2a65370b7c..f586ca9ff7 100644 --- a/plugin/dnssec/README.md +++ b/plugin/dnssec/README.md @@ -16,7 +16,7 @@ This plugin can only be used once per Server Block. ~~~ dnssec [ZONES... ] { - key file KEY... + key file|aws_secretsmanager KEY... cache_capacity CAPACITY } ~~~ @@ -31,8 +31,11 @@ ZSK/KSK split. All signing operations are done online. Authenticated denial of existence is implemented with NSEC black lies. Using ECDSA as an algorithm is preferred as this leads to smaller signatures (compared to RSA). NSEC3 is *not* supported. +As the *dnssec* plugin can't see the original TTL of the RRSets it signs, it will always use 3600s +as the value. + If multiple *dnssec* plugins are specified in the same zone, the last one specified will be -used (See [bugs](#bugs)). +used. * **ZONES** zones that should be signed. If empty, the zones from the configuration block are used. @@ -46,6 +49,26 @@ used (See [bugs](#bugs)). * generated public key `Kexample.org+013+45330.key` * generated private key `Kexample.org+013+45330.private` +* `key aws_secretsmanager` indicates that **KEY** secret(s) should be read from AWS Secrets Manager. Secret + names or ARNs may be used. After generating the keys as described in the `key file` section, you can + store them in AWS Secrets Manager using the following AWS CLI v2 command: + + ```sh + aws secretsmanager create-secret --name "Kexample.org.+013+45330" \ + --description "DNSSEC keys for example.org" \ + --secret-string "$(jq -n --arg key "$(cat Kexample.org.+013+45330.key)" \ + --arg private "$(cat Kexample.org.+013+45330.private)" \ + '{key: $key, private: $private}')" + ``` + + This command reads the contents of the `.key` and `.private` files, constructs a JSON object, and stores it + as a new secret in AWS Secrets Manager with the specified name and description. CoreDNS will then fetch + the key data from AWS Secrets Manager when using the `key aws_secretsmanager` directive. + + [AWS SDK for Go V2](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials) is used + for authentication with AWS Secrets Manager. Make sure the provided AWS credentials have the necessary + permissions (e.g., `secretsmanager:GetSecretValue`) to access the specified secrets in AWS Secrets Manager. + * `cache_capacity` indicates the capacity of the cache. The dnssec plugin uses a cache to store RRSIGs. The default for **CAPACITY** is 10000. @@ -72,6 +95,18 @@ example.org { } ~~~ +Sign responses for `example.org` with the key stored in AWS Secrets Manager under the secret name +"Kexample.org.+013+45330". + +~~~ +example.org { + dnssec { + key aws_secretsmanager Kexample.org.+013+45330 + } + whoami +} +~~~ + Sign responses for a kubernetes zone with the key "Kcluster.local+013+45129.key". ~~~ diff --git a/plugin/dnssec/black_lies.go b/plugin/dnssec/black_lies.go index 68fae22bb5..d01fa7c840 100644 --- a/plugin/dnssec/black_lies.go +++ b/plugin/dnssec/black_lies.go @@ -1,6 +1,8 @@ package dnssec import ( + "strings" + "github.com/coredns/coredns/plugin/pkg/response" "github.com/coredns/coredns/request" @@ -11,15 +13,27 @@ import ( // See https://tools.ietf.org/html/draft-valsorda-dnsop-black-lies-00 // For example, a request for the non-existing name a.example.com would // cause the following NSEC record to be generated: +// // a.example.com. 3600 IN NSEC \000.a.example.com. ( RRSIG NSEC ... ) +// // This inturn makes every NXDOMAIN answer a NODATA one, don't forget to flip // the header rcode to NOERROR. func (d Dnssec) nsec(state request.Request, mt response.Type, ttl, incep, expir uint32, server string) ([]dns.RR, error) { nsec := &dns.NSEC{} nsec.Hdr = dns.RR_Header{Name: state.QName(), Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeNSEC} nsec.NextDomain = "\\000." + state.QName() + if state.QName() == "." { + nsec.NextDomain = "\\000." // If You want to play as root server + } if state.Name() == state.Zone { nsec.TypeBitMap = filter18(state.QType(), apexBitmap, mt) + } else if mt == response.Delegation || state.QType() == dns.TypeDS { + nsec.TypeBitMap = delegationBitmap[:] + if mt == response.Delegation { + labels := dns.SplitDomainName(state.QName()) + labels[0] += "\\000" + nsec.NextDomain = strings.Join(labels, ".") + "." + } } else { nsec.TypeBitMap = filter14(state.QType(), zoneBitmap, mt) } @@ -34,13 +48,14 @@ func (d Dnssec) nsec(state request.Request, mt response.Type, ttl, incep, expir // The NSEC bit maps we return. var ( - zoneBitmap = [...]uint16{dns.TypeA, dns.TypeHINFO, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF} - apexBitmap = [...]uint16{dns.TypeA, dns.TypeNS, dns.TypeSOA, dns.TypeHINFO, dns.TypeMX, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeDNSKEY, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF} + delegationBitmap = [...]uint16{dns.TypeA, dns.TypeNS, dns.TypeHINFO, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF} + zoneBitmap = [...]uint16{dns.TypeA, dns.TypeHINFO, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF} + apexBitmap = [...]uint16{dns.TypeA, dns.TypeNS, dns.TypeSOA, dns.TypeHINFO, dns.TypeMX, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeDNSKEY, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF} ) // filter14 filters out t from bitmap (if it exists). If mt is not an NODATA response, just return the entire bitmap. func filter14(t uint16, bitmap [14]uint16, mt response.Type) []uint16 { - if mt != response.NoData && mt != response.NameError { + if mt != response.NoData && mt != response.NameError || t == dns.TypeNSEC { return zoneBitmap[:] } for i := range bitmap { @@ -52,7 +67,7 @@ func filter14(t uint16, bitmap [14]uint16, mt response.Type) []uint16 { } func filter18(t uint16, bitmap [18]uint16, mt response.Type) []uint16 { - if mt != response.NoData && mt != response.NameError { + if mt != response.NoData && mt != response.NameError || t == dns.TypeNSEC { return apexBitmap[:] } for i := range bitmap { diff --git a/plugin/dnssec/black_lies_bitmap_test.go b/plugin/dnssec/black_lies_bitmap_test.go index a4a487fcd8..4e9a10cf6b 100644 --- a/plugin/dnssec/black_lies_bitmap_test.go +++ b/plugin/dnssec/black_lies_bitmap_test.go @@ -28,7 +28,7 @@ func TestBlackLiesBitmapNoData(t *testing.T) { } } for _, b := range nsec.TypeBitMap { - if uint16(b) == dns.TypeTLSA { + if b == dns.TypeTLSA { t.Errorf("Type TLSA should not be present in the type bitmap: %v", nsec.TypeBitMap) } } @@ -50,7 +50,7 @@ func TestBlackLiesBitmapNameError(t *testing.T) { } } for _, b := range nsec.TypeBitMap { - if uint16(b) == dns.TypeTLSA { + if b == dns.TypeTLSA { t.Errorf("Type TLSA should not be present in the type bitmap: %v", nsec.TypeBitMap) } } @@ -59,6 +59,6 @@ func TestBlackLiesBitmapNameError(t *testing.T) { func testTLSAMsg() *dns.Msg { return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeSuccess}, Question: []dns.Question{{Name: "25._tcp.example.org.", Qclass: dns.ClassINET, Qtype: dns.TypeTLSA}}, - Ns: []dns.RR{test.SOA("example.org. 1800 IN SOA linode.example.org. miek.example.org. 1461471181 14400 3600 604800 14400")}, + Ns: []dns.RR{test.SOA("example.org. 1800 IN SOA linode.example.org. miek.example.org. 1461471181 14400 3600 604800 14400")}, } } diff --git a/plugin/dnssec/black_lies_test.go b/plugin/dnssec/black_lies_test.go index a9a29029e2..de381e59f6 100644 --- a/plugin/dnssec/black_lies_test.go +++ b/plugin/dnssec/black_lies_test.go @@ -71,16 +71,188 @@ func TestBlackLiesNoError(t *testing.T) { } } +func TestBlackLiesApexNsec(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testNsecMsg() + m.SetQuestion("miek.nl.", dns.TypeNSEC) + state := request.Request{Req: m, Zone: "miek.nl."} + m = d.Sign(state, time.Now().UTC(), server) + if len(m.Ns) > 0 { + t.Error("Authority section should be empty") + } + if len(m.Answer) != 2 { + t.Errorf("Answer section should have 2 RRs") + } + sig, nsec := false, false + for _, rr := range m.Answer { + if _, ok := rr.(*dns.RRSIG); ok { + sig = true + } + if rnsec, ok := rr.(*dns.NSEC); ok { + nsec = true + var bitpresent uint + for _, typeBit := range rnsec.TypeBitMap { + switch typeBit { + case dns.TypeSOA: + bitpresent |= 4 + case dns.TypeNSEC: + bitpresent |= 1 + case dns.TypeRRSIG: + bitpresent |= 2 + } + } + if bitpresent != 7 { + t.Error("NSEC must have SOA, RRSIG and NSEC in its bitmap") + } + } + } + if !sig || !nsec { + t.Errorf("Expected RRSIG and NSEC in answer section") + } +} + +func TestBlackLiesNsec(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testNsecMsg() + m.SetQuestion("www.miek.nl.", dns.TypeNSEC) + state := request.Request{Req: m, Zone: "miek.nl."} + m = d.Sign(state, time.Now().UTC(), server) + if len(m.Ns) > 0 { + t.Error("Authority section should be empty") + } + if len(m.Answer) != 2 { + t.Errorf("Answer section should have 2 RRs") + } + sig, nsec := false, false + for _, rr := range m.Answer { + if _, ok := rr.(*dns.RRSIG); ok { + sig = true + } + if rnsec, ok := rr.(*dns.NSEC); ok { + nsec = true + var bitpresent uint + for _, typeBit := range rnsec.TypeBitMap { + switch typeBit { + case dns.TypeNSEC: + bitpresent |= 1 + case dns.TypeRRSIG: + bitpresent |= 2 + } + } + if bitpresent != 3 { + t.Error("NSEC must have RRSIG and NSEC in its bitmap") + } + } + } + if !sig || !nsec { + t.Errorf("Expected RRSIG and NSEC in answer section") + } +} + +func TestBlackLiesApexDS(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testApexDSMsg() + m.SetQuestion("miek.nl.", dns.TypeDS) + state := request.Request{Req: m, Zone: "miek.nl."} + m = d.Sign(state, time.Now().UTC(), server) + if !section(m.Ns, 2) { + t.Errorf("Authority section should have 2 sigs") + } + var nsec *dns.NSEC + for _, r := range m.Ns { + if r.Header().Rrtype == dns.TypeNSEC { + nsec = r.(*dns.NSEC) + } + } + if nsec == nil { + t.Error("Expected NSEC, got none") + } else if correctNsecForDS(nsec) { + t.Error("NSEC DS at the apex zone should cover all apex type.") + } +} + +func TestBlackLiesDS(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testApexDSMsg() + m.SetQuestion("sub.miek.nl.", dns.TypeDS) + state := request.Request{Req: m, Zone: "miek.nl."} + m = d.Sign(state, time.Now().UTC(), server) + if !section(m.Ns, 2) { + t.Errorf("Authority section should have 2 sigs") + } + var nsec *dns.NSEC + for _, r := range m.Ns { + if r.Header().Rrtype == dns.TypeNSEC { + nsec = r.(*dns.NSEC) + } + } + if nsec == nil { + t.Error("Expected NSEC, got none") + } else if !correctNsecForDS(nsec) { + t.Error("NSEC DS should cover delegation type only.") + } +} + +func correctNsecForDS(nsec *dns.NSEC) bool { + var bitmask uint + /* Coherent TypeBitMap for NSEC of DS should contain at least: + * {TypeNS, TypeNSEC, TypeRRSIG} and no SOA. + * Any missing type will confuse resolver because + * it will prove that the dns query cannot be a delegation point, + * which will break trust resolution for unsigned delegated domain. + * No SOA is obvious for none apex query. + */ + for _, typeBitmask := range nsec.TypeBitMap { + switch typeBitmask { + case dns.TypeNS: + bitmask |= 1 + case dns.TypeNSEC: + bitmask |= 2 + case dns.TypeRRSIG: + bitmask |= 4 + case dns.TypeSOA: + return false + } + } + return bitmask == 7 +} + func testNxdomainMsg() *dns.Msg { return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError}, Question: []dns.Question{{Name: "ww.miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeTXT}}, - Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")}, + Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")}, } } func testSuccessMsg() *dns.Msg { return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeSuccess}, Question: []dns.Question{{Name: "www.miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeTXT}}, - Answer: []dns.RR{test.TXT(`www.miek.nl. 1800 IN TXT "response"`)}, + Answer: []dns.RR{test.TXT(`www.miek.nl. 1800 IN TXT "response"`)}, + } +} + +func testNsecMsg() *dns.Msg { + return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError}, + Question: []dns.Question{{Name: "www.miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeNSEC}}, + Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")}, + } +} + +func testApexDSMsg() *dns.Msg { + return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError}, + Question: []dns.Question{{Name: "miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeDS}}, + Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")}, } } diff --git a/plugin/dnssec/cache.go b/plugin/dnssec/cache.go index d93cdabd1f..9c94efd91b 100644 --- a/plugin/dnssec/cache.go +++ b/plugin/dnssec/cache.go @@ -3,8 +3,9 @@ package dnssec import ( "hash/fnv" "io" - "strconv" - "strings" + "time" + + "github.com/coredns/coredns/plugin/pkg/cache" "github.com/miekg/dns" ) @@ -12,16 +13,36 @@ import ( // hash serializes the RRset and returns a signature cache key. func hash(rrs []dns.RR) uint64 { h := fnv.New64() - // Only need this to be unique for ownername + qtype (+class), but we - // only care about IN. Its already an RRSet, so the ownername is the - // same as is the qtype. Take the first one and construct the hash - // string that creates the key - io.WriteString(h, strings.ToLower(rrs[0].Header().Name)) - typ, ok := dns.TypeToString[rrs[0].Header().Rrtype] - if !ok { - typ = "TYPE" + strconv.FormatUint(uint64(rrs[0].Header().Rrtype), 10) + // we need to hash the entire RRset to pick the correct sig, if the rrset + // changes for whatever reason we should resign. + // We could use wirefmt, or the string format, both create garbage when creating + // the hash key. And of course is a uint64 big enough? + for _, rr := range rrs { + io.WriteString(h, rr.String()) + } + return h.Sum64() +} + +func periodicClean(c *cache.Cache, stop <-chan struct{}) { + tick := time.NewTicker(8 * time.Hour) + defer tick.Stop() + for { + select { + case <-tick.C: + // we sign for 8 days, check if a signature in the cache reached 75% of that (i.e. 6), if found delete + // the signature + is75 := time.Now().UTC().Add(twoDays) + c.Walk(func(items map[uint64]any, key uint64) bool { + for _, rr := range items[key].([]dns.RR) { + if !rr.(*dns.RRSIG).ValidityPeriod(is75) { + delete(items, key) + } + } + return true + }) + + case <-stop: + return + } } - io.WriteString(h, typ) - i := h.Sum64() - return i } diff --git a/plugin/dnssec/dnskey.go b/plugin/dnssec/dnskey.go index 11e18fdc66..cfe59845ae 100644 --- a/plugin/dnssec/dnskey.go +++ b/plugin/dnssec/dnskey.go @@ -1,15 +1,21 @@ package dnssec import ( + "context" "crypto" "crypto/ecdsa" "crypto/rsa" + "encoding/json" "errors" "os" + "path/filepath" + "strings" "time" "github.com/coredns/coredns/request" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" "github.com/miekg/dns" "golang.org/x/crypto/ed25519" ) @@ -22,10 +28,16 @@ type DNSKEY struct { tag uint16 } +// SecretKeyData represents the structure of the DNS keys stored in AWS Secrets Manager. +type SecretKeyData struct { + Key string `json:"key"` + Private string `json:"private"` +} + // ParseKeyFile read a DNSSEC keyfile as generated by dnssec-keygen or other // utilities. It adds ".key" for the public key and ".private" for the private key. func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) { - f, e := os.Open(pubFile) + f, e := os.Open(filepath.Clean(pubFile)) if e != nil { return nil, e } @@ -35,7 +47,7 @@ func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) { return nil, e } - f, e = os.Open(privFile) + f, e = os.Open(filepath.Clean(privFile)) if e != nil { return nil, e } @@ -62,6 +74,69 @@ func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) { return &DNSKEY{K: dk, D: dk.ToDS(dns.SHA256), s: nil, tag: 0}, errors.New("no private key found") } +// ParseKeyFromAWSSecretsManager retrieves and parses a DNSSEC key pair from AWS Secrets Manager. +func ParseKeyFromAWSSecretsManager(secretID string) (*DNSKEY, error) { + // Load the AWS SDK configuration + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + return nil, err + } + + // Create a Secrets Manager client + client := secretsmanager.NewFromConfig(cfg) + + // Retrieve the secret value + input := &secretsmanager.GetSecretValueInput{ + SecretId: &secretID, + } + result, err := client.GetSecretValue(context.TODO(), input) + if err != nil { + return nil, err + } + + // Parse the secret string into SecretKeyData + var secretData SecretKeyData + err = json.Unmarshal([]byte(*result.SecretString), &secretData) + if err != nil { + return nil, err + } + + // Parse the public key + rr, err := dns.NewRR(secretData.Key) + if err != nil { + return nil, err + } + dk, ok := rr.(*dns.DNSKEY) + if !ok { + return nil, errors.New("invalid public key format") + } + + // Parse the private key + p, err := dk.ReadPrivateKey(strings.NewReader(secretData.Private), secretID) + if err != nil { + return nil, err + } + + // Create the DNSKEY structure + var s crypto.Signer + var tag uint16 + switch key := p.(type) { + case *rsa.PrivateKey: + s = key + tag = dk.KeyTag() + case *ecdsa.PrivateKey: + s = key + tag = dk.KeyTag() + case ed25519.PrivateKey: + s = key + tag = dk.KeyTag() + default: + return nil, errors.New("unsupported key type") + } + + return &DNSKEY{K: dk, D: dk.ToDS(dns.SHA256), s: s, tag: tag}, nil +} + // getDNSKEY returns the correct DNSKEY to the client. Signatures are added when do is true. func (d Dnssec) getDNSKEY(state request.Request, zone string, do bool, server string) *dns.Msg { keys := make([]dns.RR, len(d.keys)) diff --git a/plugin/dnssec/dnssec.go b/plugin/dnssec/dnssec.go index 6f943ec882..2d52c84b93 100644 --- a/plugin/dnssec/dnssec.go +++ b/plugin/dnssec/dnssec.go @@ -48,6 +48,22 @@ func (d Dnssec) Sign(state request.Request, now time.Time, server string) *dns.M mt, _ := response.Typify(req, time.Now().UTC()) // TODO(miek): need opt record here? if mt == response.Delegation { + // We either sign DS or NSEC of DS. + ttl := req.Ns[0].Header().Ttl + + ds := []dns.RR{} + for i := range req.Ns { + if req.Ns[i].Header().Rrtype == dns.TypeDS { + ds = append(ds, req.Ns[i]) + } + } + if len(ds) == 0 { + if sigs, err := d.nsec(state, mt, ttl, incep, expir, server); err == nil { + req.Ns = append(req.Ns, sigs...) + } + } else if sigs, err := d.sign(ds, state.Zone, ttl, incep, expir, server); err == nil { + req.Ns = append(req.Ns, sigs...) + } return req } @@ -66,6 +82,10 @@ func (d Dnssec) Sign(state request.Request, now time.Time, server string) *dns.M } if len(req.Ns) > 1 { // actually added nsec and sigs, reset the rcode req.Rcode = dns.RcodeSuccess + if state.QType() == dns.TypeNSEC { // If original query was NSEC move Ns to Answer without SOA + req.Answer = req.Ns[len(req.Ns)-2 : len(req.Ns)] + req.Ns = nil + } } return req } @@ -98,7 +118,7 @@ func (d Dnssec) sign(rrs []dns.RR, signerName string, ttl, incep, expir uint32, return sgs, nil } - sigs, err := d.inflight.Do(k, func() (interface{}, error) { + sigs, err := d.inflight.Do(k, func() (any, error) { var sigs []dns.RR for _, k := range d.keys { if d.splitkeys { @@ -131,7 +151,7 @@ func (d Dnssec) set(key uint64, sigs []dns.RR) { d.cache.Add(key, sigs) } func (d Dnssec) get(key uint64, server string) ([]dns.RR, bool) { if s, ok := d.cache.Get(key); ok { // we sign for 8 days, check if a signature in the cache reached 3/4 of that - is75 := time.Now().UTC().Add(sixDays) + is75 := time.Now().UTC().Add(twoDays) for _, rr := range s.([]dns.RR) { if !rr.(*dns.RRSIG).ValidityPeriod(is75) { cacheMisses.WithLabelValues(server).Inc() @@ -154,6 +174,6 @@ func incepExpir(now time.Time) (uint32, uint32) { const ( eightDays = 8 * 24 * time.Hour - sixDays = 6 * 24 * time.Hour + twoDays = 2 * 24 * time.Hour defaultCap = 10000 // default capacity of the cache. ) diff --git a/plugin/dnssec/dnssec_test.go b/plugin/dnssec/dnssec_test.go index fb8a128def..f48d9a9ec5 100644 --- a/plugin/dnssec/dnssec_test.go +++ b/plugin/dnssec/dnssec_test.go @@ -123,6 +123,60 @@ func TestSigningEmpty(t *testing.T) { } } +func TestDelegationSigned(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testMsgDelegationSigned() + m.SetQuestion("sub.miek.nl.", dns.TypeNS) + state := request.Request{Req: m, Zone: "miek.nl."} + m = d.Sign(state, time.Now().UTC(), server) + if !section(m.Ns, 1) { + t.Errorf("Authority section should have 1 RRSIGs") + } + if !section(m.Extra, 0) { + t.Error("Extra section should not have RRSIGs") + } +} + +func TestDelegationUnSigned(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testMsgDelegationUnSigned() + m.SetQuestion("sub.miek.nl.", dns.TypeNS) + state := request.Request{Req: m, Zone: "miek.nl."} + m = d.Sign(state, time.Now().UTC(), server) + if !section(m.Ns, 1) { + t.Errorf("Authority section should have 1 RRSIG") + } + if !section(m.Extra, 0) { + t.Error("Extra section should not have RRSIG") + } + var nsec *dns.NSEC + var rrsig *dns.RRSIG + for _, r := range m.Ns { + if r.Header().Rrtype == dns.TypeNSEC { + nsec = r.(*dns.NSEC) + } + if r.Header().Rrtype == dns.TypeRRSIG { + rrsig = r.(*dns.RRSIG) + } + } + if nsec == nil { + t.Error("Authority section should hold a NSEC record") + } + if rrsig.TypeCovered != dns.TypeNSEC { + t.Errorf("RRSIG should cover type %s, got %s", + dns.TypeToString[dns.TypeNSEC], dns.TypeToString[rrsig.TypeCovered]) + } + if !correctNsecForDS(nsec) { + t.Error("NSEC as invalid TypeBitMap for a DS") + } +} + func section(rss []dns.RR, nrSigs int) bool { i := 0 for _, r := range rss { @@ -137,13 +191,13 @@ func testMsg() *dns.Msg { // don't care about the message header return &dns.Msg{ Answer: []dns.RR{test.MX("miek.nl. 1703 IN MX 1 aspmx.l.google.com.")}, - Ns: []dns.RR{test.NS("miek.nl. 1703 IN NS omval.tednet.nl.")}, + Ns: []dns.RR{test.NS("miek.nl. 1703 IN NS omval.tednet.nl.")}, } } func testMsgEx() *dns.Msg { return &dns.Msg{ Answer: []dns.RR{test.MX("example.org. 1703 IN MX 1 aspmx.l.google.com.")}, - Ns: []dns.RR{test.NS("example.org. 1703 IN NS omval.tednet.nl.")}, + Ns: []dns.RR{test.NS("example.org. 1703 IN NS omval.tednet.nl.")}, } } @@ -163,6 +217,29 @@ func testMsgDname() *dns.Msg { } } +func testMsgDelegationSigned() *dns.Msg { + return &dns.Msg{ + Ns: []dns.RR{ + test.NS("sub.miek.nl. 1800 IN NS ns1.sub.miek.nl."), + test.DS("sub." + dsKey), + }, + Extra: []dns.RR{ + test.A("ns1.sub.miek.nl. 1800 IN A 192.0.2.1"), + }, + } +} + +func testMsgDelegationUnSigned() *dns.Msg { + return &dns.Msg{ + Ns: []dns.RR{ + test.NS("sub.miek.nl. 1800 IN NS ns1.sub.miek.nl."), + }, + Extra: []dns.RR{ + test.A("ns1.sub.miek.nl. 1800 IN A 192.0.2.1"), + }, + } +} + func testEmptyMsg() *dns.Msg { // don't care about the message header return &dns.Msg{ @@ -171,6 +248,7 @@ func testEmptyMsg() *dns.Msg { } func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) { + t.Helper() k, rm1, rm2 := newKey(t) c := cache.New(defaultCap) d := New(zones, []*DNSKEY{k}, false, nil, c) @@ -178,6 +256,7 @@ func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) { } func newKey(t *testing.T) (*DNSKEY, func(), func()) { + t.Helper() fPriv, rmPriv, _ := test.TempFile(".", privKey) fPub, rmPub, _ := test.TempFile(".", pubKey) @@ -197,6 +276,7 @@ Created: 20160423195532 Publish: 20160423195532 Activate: 20160423195532 ` + dsKey = `miek.nl. IN DS 18512 13 2 D4E806322598BC97A003EF1ACDFF352EEFF7B42DBB0D41B8224714C36AEF08D9` pubKey1 = `example.org. IN DNSKEY 257 3 13 tVRWNSGpHZbCi7Pr7OmbADVUO3MxJ0Lb8Lk3o/HBHqCxf5K/J50lFqRa 98lkdAIiFOVRy8LyMvjwmxZKwB5MNw==` privKey1 = `Private-key-format: v1.3 Algorithm: 13 (ECDSAP256SHA256) diff --git a/plugin/dnssec/handler_test.go b/plugin/dnssec/handler_test.go index a1e24b7bb6..e82e546d34 100644 --- a/plugin/dnssec/handler_test.go +++ b/plugin/dnssec/handler_test.go @@ -37,6 +37,73 @@ var dnsTestCases = []test.Case{ test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"), }, }, + { + Qname: "miek.nl.", Qtype: dns.TypeNS, Do: true, + Answer: []dns.RR{ + test.NS("miek.nl. 1800 IN NS linode.atoom.net."), + test.RRSIG("miek.nl. 1800 IN RRSIG NS 13 2 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"), + }, + }, + { + Qname: "deleg.miek.nl.", Qtype: dns.TypeNS, Do: true, + Ns: []dns.RR{ + test.DS("deleg.miek.nl. 1800 IN DS 18512 13 2 D4E806322598BC97A003EF1ACDFF352EEFF7B42DBB0D41B8224714C36AEF08D9"), + test.NS("deleg.miek.nl. 1800 IN NS ns01.deleg.miek.nl."), + test.RRSIG("deleg.miek.nl. 1800 IN RRSIG DS 13 3 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"), + }, + }, + { + Qname: "unsigned.miek.nl.", Qtype: dns.TypeNS, Do: true, + Ns: []dns.RR{ + test.NS("unsigned.miek.nl. 1800 IN NS ns01.deleg.miek.nl."), + test.NSEC("unsigned.miek.nl. 1800 IN NSEC unsigned\\000.miek.nl. NS RRSIG NSEC"), + test.RRSIG("unsigned.miek.nl. 1800 IN RRSIG NSEC 13 3 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"), + }, + }, + { // DS should not come from dnssec plugin + Qname: "deleg.miek.nl.", Qtype: dns.TypeDS, + Answer: []dns.RR{ + test.DS("deleg.miek.nl. 1800 IN DS 18512 13 2 D4E806322598BC97A003EF1ACDFF352EEFF7B42DBB0D41B8224714C36AEF08D9"), + }, + Ns: []dns.RR{ + test.NS("miek.nl. 1800 IN NS linode.atoom.net."), + }, + }, + { + Qname: "unsigned.miek.nl.", Qtype: dns.TypeDS, + Ns: []dns.RR{ + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeDS, Do: true, + Ns: []dns.RR{ + test.NSEC("miek.nl. 1800 IN NSEC \\000.miek.nl. A HINFO NS SOA MX TXT AAAA LOC SRV CERT SSHFP RRSIG NSEC DNSKEY TLSA HIP OPENPGPKEY SPF"), + test.RRSIG("miek.nl. 1800 IN RRSIG NSEC 13 2 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"), + test.RRSIG("miek.nl. 1800 IN RRSIG SOA 13 2 3600 20171220141741 20171212111741 18512 miek.nl. 8bLTReqmuQtw=="), + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, + { + Qname: "deleg.miek.nl.", Qtype: dns.TypeDS, Do: true, + Answer: []dns.RR{ + test.DS("deleg.miek.nl. 1800 IN DS 18512 13 2 D4E806322598BC97A003EF1ACDFF352EEFF7B42DBB0D41B8224714C36AEF08D9"), + test.RRSIG("deleg.miek.nl. 1800 IN RRSIG DS 13 3 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"), + }, + Ns: []dns.RR{ + test.NS("miek.nl. 1800 IN NS linode.atoom.net."), + test.RRSIG("miek.nl. 1800 IN RRSIG NS 13 2 3600 20161217114912 20161209084912 18512 miek.nl. ad9gA8VWgF1H8ze9/0Rk2Q=="), + }, + }, + { + Qname: "unsigned.miek.nl.", Qtype: dns.TypeDS, Do: true, + Ns: []dns.RR{ + test.RRSIG("miek.nl. 1800 IN RRSIG SOA 13 2 3600 20171220141741 20171212111741 18512 miek.nl. 8bLTReqmuQtw=="), + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + test.NSEC("unsigned.miek.nl. 1800 IN NSEC \\000.unsigned.miek.nl. NS RRSIG NSEC"), + test.RRSIG("unsigned.miek.nl. 1800 IN RRSIG NSEC 13 3 1800 20220101121212 20220201121212 18512 miek.nl. RandomNotChecked"), + }, + }, { Qname: "miek.nl.", Qtype: dns.TypeMX, Answer: []dns.RR{ @@ -179,4 +246,8 @@ $ORIGIN miek.nl. a IN A 139.162.196.78 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 -www IN CNAME a` +www IN CNAME a +deleg IN NS ns01.deleg + IN DS 18512 13 2 D4E806322598BC97A003EF1ACDFF352EEFF7B42DBB0D41B8224714C36AEF08D9 +unsigned IN NS ns01.deleg +` diff --git a/plugin/dnssec/setup.go b/plugin/dnssec/setup.go index d3056dc198..e3feae05b3 100644 --- a/plugin/dnssec/setup.go +++ b/plugin/dnssec/setup.go @@ -3,6 +3,7 @@ package dnssec import ( "fmt" "path/filepath" + "slices" "strconv" "strings" @@ -24,6 +25,17 @@ func setup(c *caddy.Controller) error { } ca := cache.New(capacity) + stop := make(chan struct{}) + + c.OnShutdown(func() error { + close(stop) + return nil + }) + c.OnStartup(func() error { + go periodicClean(ca, stop) + return nil + }) + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { return New(zones, keys, splitkeys, next, ca) }) @@ -33,9 +45,7 @@ func setup(c *caddy.Controller) error { func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, bool, error) { zones := []string{} - keys := []*DNSKEY{} - capacity := defaultCap i := 0 @@ -46,15 +56,9 @@ func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, bool, error) { i++ // dnssec [zones...] - zones = make([]string, len(c.ServerBlockKeys)) - copy(zones, c.ServerBlockKeys) - args := c.RemainingArgs() - if len(args) > 0 { - zones = args - } + zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) for c.NextBlock() { - switch x := c.Val(); x { case "key": k, e := keyParse(c) @@ -75,13 +79,8 @@ func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, bool, error) { default: return nil, nil, 0, false, c.Errf("unknown property '%s'", x) } - } } - for i := range zones { - zones[i] = plugin.Host(zones[i]).Normalize() - } - // Check if we have both KSKs and ZSKs. zsk, ksk := 0, 0 for _, k := range keys { @@ -96,13 +95,7 @@ func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, bool, error) { // Check if each keys owner name can actually sign the zones we want them to sign. for _, k := range keys { kname := plugin.Name(k.K.Header().Name) - ok := false - for i := range zones { - if kname.Matches(zones[i]) { - ok = true - break - } - } + ok := slices.ContainsFunc(zones, kname.Matches) if !ok { return zones, keys, capacity, splitkeys, fmt.Errorf("key %s (keyid: %d) can not sign any of the zones", string(kname), k.tag) } @@ -119,7 +112,8 @@ func keyParse(c *caddy.Controller) ([]*DNSKEY, error) { return nil, c.ArgErr() } value := c.Val() - if value == "file" { + switch value { + case "file": ks := c.RemainingArgs() if len(ks) == 0 { return nil, c.ArgErr() @@ -143,6 +137,19 @@ func keyParse(c *caddy.Controller) ([]*DNSKEY, error) { } keys = append(keys, k) } + case "aws_secretsmanager": + ks := c.RemainingArgs() + if len(ks) == 0 { + return nil, c.ArgErr() + } + + for _, k := range ks { + k, err := ParseKeyFromAWSSecretsManager(k) + if err != nil { + return nil, err + } + keys = append(keys, k) + } } return keys, nil } diff --git a/plugin/dnssec/setup_test.go b/plugin/dnssec/setup_test.go index 7132d04ebf..66ff45f1f6 100644 --- a/plugin/dnssec/setup_test.go +++ b/plugin/dnssec/setup_test.go @@ -1,7 +1,6 @@ package dnssec import ( - "io/ioutil" "os" "strings" "testing" @@ -10,19 +9,19 @@ import ( ) func TestSetupDnssec(t *testing.T) { - if err := ioutil.WriteFile("Kcluster.local.key", []byte(keypub), 0644); err != nil { + if err := os.WriteFile("Kcluster.local.key", []byte(keypub), 0644); err != nil { t.Fatalf("Failed to write pub key file: %s", err) } defer func() { os.Remove("Kcluster.local.key") }() - if err := ioutil.WriteFile("Kcluster.local.private", []byte(keypriv), 0644); err != nil { + if err := os.WriteFile("Kcluster.local.private", []byte(keypriv), 0644); err != nil { t.Fatalf("Failed to write private key file: %s", err) } defer func() { os.Remove("Kcluster.local.private") }() - if err := ioutil.WriteFile("ksk_Kcluster.local.key", []byte(kskpub), 0644); err != nil { + if err := os.WriteFile("ksk_Kcluster.local.key", []byte(kskpub), 0644); err != nil { t.Fatalf("Failed to write pub key file: %s", err) } defer func() { os.Remove("ksk_Kcluster.local.key") }() - if err := ioutil.WriteFile("ksk_Kcluster.local.private", []byte(kskpriv), 0644); err != nil { + if err := os.WriteFile("ksk_Kcluster.local.private", []byte(kskpriv), 0644); err != nil { t.Fatalf("Failed to write private key file: %s", err) } defer func() { os.Remove("ksk_Kcluster.local.private") }() diff --git a/plugin/dnstap/README.md b/plugin/dnstap/README.md index 095d33e24b..4d53612cf9 100644 --- a/plugin/dnstap/README.md +++ b/plugin/dnstap/README.md @@ -15,11 +15,23 @@ Every message is sent to the socket as soon as it comes in, the *dnstap* plugin ## Syntax ~~~ txt -dnstap SOCKET [full] +dnstap SOCKET [full] [writebuffer] [queue] { + [identity IDENTITY] + [version VERSION] + [extra EXTRA] + [skipverify] +} ~~~ * **SOCKET** is the socket (path) supplied to the dnstap command line tool. * `full` to include the wire-format DNS message. +* **writebuffer** sets the TCP write buffer multiplier in MiB. Valid range: [1, 1024]. +* **queue** sets the queue multiplier, applied to 10,000 messages. Valid range: [1, 4096]. +* **IDENTITY** to override the identity of the server. Defaults to the hostname. +* **VERSION** to override the version field. Defaults to the CoreDNS version. +* **EXTRA** to define "extra" field in dnstap payload, [metadata](../metadata/) replacement available here. +* `skipverify` to skip tls verification during connection. Default to be secure + ## Examples @@ -29,6 +41,12 @@ Log information about client requests and responses to */tmp/dnstap.sock*. dnstap /tmp/dnstap.sock ~~~ +Log information about client requests and responses with a custom TCP write buffer (1024 MiB) and queue capacity (2048 x 10000). + +~~~ txt +dnstap /tmp/dnstap.sock full 1024 2048 +~~~ + Log information including the wire-format DNS message about client requests and responses to */tmp/dnstap.sock*. ~~~ txt @@ -41,10 +59,52 @@ Log to a remote endpoint. dnstap tcp://127.0.0.1:6000 full ~~~ +Log to a remote endpoint by FQDN. + +~~~ txt +dnstap tcp://example.com:6000 full +~~~ + +Log to a socket, overriding the default identity and version. + +~~~ txt +dnstap /tmp/dnstap.sock { + identity my-dns-server1 + version MyDNSServer-1.2.3 +} +~~~ + +Log to a socket, customize the "extra" field in dnstap payload. You may use metadata provided by other plugins in the extra field. + +~~~ txt +forward . 8.8.8.8 +metadata +dnstap /tmp/dnstap.sock { + extra "upstream: {/forward/upstream}" +} +~~~ + +Log to a remote TLS endpoint. + +~~~ txt +dnstap tls://127.0.0.1:6000 full { + skipverify +} +~~~ + +You can use _dnstap_ more than once to define multiple taps. The following logs information including the +wire-format DNS message about client requests and responses to */tmp/dnstap.sock*, +and also sends client requests and responses without wire-format DNS messages to a remote FQDN. + +~~~ txt +dnstap /tmp/dnstap.sock full +dnstap tcp://example.com:6000 +~~~ + ## Command Line Tool Dnstap has a command line tool that can be used to inspect the logging. The tool can be found -at Github: . It's written in Go. +at GitHub: . It's written in Go. The following command listens on the given socket and decodes messages to stdout. @@ -66,13 +126,15 @@ $ dnstap -l 127.0.0.1:6000 ## Using Dnstap in your plugin -In your setup function, check to see if the *dnstap* plugin is loaded: +In your setup function, collect and store a list of all *dnstap* plugins loaded in the config: ~~~ go +x := &ExamplePlugin{} + c.OnStartup(func() error { if taph := dnsserver.GetConfig(c).Handler("dnstap"); taph != nil { - if tapPlugin, ok := taph.(dnstap.Dnstap); ok { - f.tapPlugin = &tapPlugin + for tapPlugin, ok := taph.(*dnstap.Dnstap); ok; tapPlugin, ok = tapPlugin.Next.(*dnstap.Dnstap) { + x.tapPlugins = append(x.tapPlugins, tapPlugin) } } return nil @@ -82,8 +144,15 @@ c.OnStartup(func() error { And then in your plugin: ~~~ go -func (x RandomPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { - if tapPlugin != nil { +import ( + "github.com/coredns/coredns/plugin/dnstap/msg" + "github.com/coredns/coredns/request" + + tap "github.com/dnstap/golang-dnstap" +) + +func (x ExamplePlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + for _, tapPlugin := range x.tapPlugins { q := new(msg.Msg) msg.SetQueryTime(q, time.Now()) msg.SetQueryAddress(q, w.RemoteAddr()) @@ -92,7 +161,12 @@ func (x RandomPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns q.QueryMessage = buf } msg.SetType(q, tap.Message_CLIENT_QUERY) + + // if no metadata interpretation is needed, just send the message tapPlugin.TapMessage(q) + + // OR: to interpret the metadata in "extra" field, give more context info + tapPlugin.TapMessageWithMetadata(ctx, q, request.Request{W: w, Req: query}) } // ... } diff --git a/plugin/dnstap/encoder.go b/plugin/dnstap/encoder.go index 09b1e2e6f8..93d3e73d91 100644 --- a/plugin/dnstap/encoder.go +++ b/plugin/dnstap/encoder.go @@ -6,7 +6,7 @@ import ( tap "github.com/dnstap/golang-dnstap" fs "github.com/farsightsec/golang-framestream" - "github.com/golang/protobuf/proto" + "google.golang.org/protobuf/proto" ) // encoder wraps a golang-framestream.Encoder. diff --git a/plugin/dnstap/handler.go b/plugin/dnstap/handler.go index 1ae0e3c62d..d322aab908 100644 --- a/plugin/dnstap/handler.go +++ b/plugin/dnstap/handler.go @@ -6,6 +6,8 @@ import ( "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/dnstap/msg" + "github.com/coredns/coredns/plugin/pkg/replacer" + "github.com/coredns/coredns/request" tap "github.com/dnstap/golang-dnstap" "github.com/miekg/dns" @@ -15,18 +17,42 @@ import ( type Dnstap struct { Next plugin.Handler io tapper + repl replacer.Replacer // IncludeRawMessage will include the raw DNS message into the dnstap messages if true. - IncludeRawMessage bool + IncludeRawMessage bool + Identity []byte + Version []byte + ExtraFormat string + MultipleTcpWriteBuf int // *Mb + MultipleQueue int // *10000 } -// TapMessage sends the message m to the dnstap interface. -func (h Dnstap) TapMessage(m *tap.Message) { +// TapMessage sends the message m to the dnstap interface, without populating "Extra" field. +func (h *Dnstap) TapMessage(m *tap.Message) { + if h.ExtraFormat == "" { + h.tapWithExtra(m, nil) + } else { + h.tapWithExtra(m, []byte(h.ExtraFormat)) + } +} + +// TapMessageWithMetadata sends the message m to the dnstap interface, with "Extra" field being populated. +func (h *Dnstap) TapMessageWithMetadata(ctx context.Context, m *tap.Message, state request.Request) { + if h.ExtraFormat == "" { + h.tapWithExtra(m, nil) + return + } + extraStr := h.repl.Replace(ctx, state, nil, h.ExtraFormat) + h.tapWithExtra(m, []byte(extraStr)) +} + +func (h *Dnstap) tapWithExtra(m *tap.Message, extra []byte) { t := tap.Dnstap_MESSAGE - h.io.Dnstap(tap.Dnstap{Type: &t, Message: m}) + h.io.Dnstap(&tap.Dnstap{Type: &t, Message: m, Identity: h.Identity, Version: h.Version, Extra: extra}) } -func (h Dnstap) tapQuery(w dns.ResponseWriter, query *dns.Msg, queryTime time.Time) { +func (h *Dnstap) tapQuery(ctx context.Context, w dns.ResponseWriter, query *dns.Msg, queryTime time.Time) { q := new(tap.Message) msg.SetQueryTime(q, queryTime) msg.SetQueryAddress(q, w.RemoteAddr()) @@ -36,24 +62,26 @@ func (h Dnstap) tapQuery(w dns.ResponseWriter, query *dns.Msg, queryTime time.Ti q.QueryMessage = buf } msg.SetType(q, tap.Message_CLIENT_QUERY) - h.TapMessage(q) + state := request.Request{W: w, Req: query} + h.TapMessageWithMetadata(ctx, q, state) } // ServeDNS logs the client query and response to dnstap and passes the dnstap Context. -func (h Dnstap) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { +func (h *Dnstap) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { rw := &ResponseWriter{ ResponseWriter: w, Dnstap: h, query: r, + ctx: ctx, queryTime: time.Now(), } // The query tap message should be sent before sending the query to the // forwarder. Otherwise, the tap messages will come out out of order. - h.tapQuery(w, r, rw.queryTime) + h.tapQuery(ctx, w, r, rw.queryTime) return plugin.NextOrFailure(h.Name(), h.Next, ctx, rw, r) } // Name implements the plugin.Plugin interface. -func (h Dnstap) Name() string { return "dnstap" } +func (h *Dnstap) Name() string { return "dnstap" } diff --git a/plugin/dnstap/handler_test.go b/plugin/dnstap/handler_test.go index 74f72521d3..6e71a890a0 100644 --- a/plugin/dnstap/handler_test.go +++ b/plugin/dnstap/handler_test.go @@ -6,24 +6,33 @@ import ( "testing" "github.com/coredns/coredns/plugin/dnstap/msg" + "github.com/coredns/coredns/plugin/metadata" test "github.com/coredns/coredns/plugin/test" tap "github.com/dnstap/golang-dnstap" "github.com/miekg/dns" ) -func testCase(t *testing.T, tapq, tapr *tap.Message, q, r *dns.Msg) { +func testCase(t *testing.T, tapq, tapr *tap.Dnstap, q, r *dns.Msg, extraFormat string) { + t.Helper() w := writer{t: t} w.queue = append(w.queue, tapq, tapr) h := Dnstap{ Next: test.HandlerFunc(func(_ context.Context, w dns.ResponseWriter, _ *dns.Msg) (int, error) { - return 0, w.WriteMsg(r) }), - io: &w, + io: &w, + ExtraFormat: extraFormat, + } + ctx := metadata.ContextWithMetadata(context.TODO()) + ok := metadata.SetValueFunc(ctx, "metadata/test", func() string { + return "MetadataValue" + }) + if !ok { + t.Fatal("Failed to set metadata") } - _, err := h.ServeDNS(context.TODO(), &test.ResponseWriter{}, q) + _, err := h.ServeDNS(ctx, &test.ResponseWriter{}, q) if err != nil { t.Fatal(err) } @@ -31,28 +40,45 @@ func testCase(t *testing.T, tapq, tapr *tap.Message, q, r *dns.Msg) { type writer struct { t *testing.T - queue []*tap.Message + queue []*tap.Dnstap } -func (w *writer) Dnstap(e tap.Dnstap) { +func (w *writer) Dnstap(e *tap.Dnstap) { if len(w.queue) == 0 { w.t.Error("Message not expected") } - ex := w.queue[0] - got := e.Message + ex := w.queue[0].GetMessage() + got := e.GetMessage() + + eaddr := string(ex.GetQueryAddress()) + gaddr := string(got.GetQueryAddress()) + if eaddr != gaddr { + w.t.Errorf("Expected source address %s, got %s", eaddr, gaddr) + } - if string(ex.QueryAddress) != string(got.QueryAddress) { - w.t.Errorf("Expected source address %s, got %s", ex.QueryAddress, got.QueryAddress) + eraddr := string(ex.GetResponseAddress()) + graddr := string(got.GetResponseAddress()) + if eraddr != graddr { + w.t.Errorf("Expected response address %s, got %s", eraddr, graddr) } - if string(ex.ResponseAddress) != string(got.ResponseAddress) { - w.t.Errorf("Expected response address %s, got %s", ex.ResponseAddress, got.ResponseAddress) + + ep := ex.GetQueryPort() + gp := got.GetQueryPort() + if ep != gp { + w.t.Errorf("Expected port %d, got %d", ep, gp) } - if *ex.QueryPort != *got.QueryPort { - w.t.Errorf("Expected port %d, got %d", *ex.QueryPort, *got.QueryPort) + + ef := ex.GetSocketFamily() + sf := got.GetSocketFamily() + if ef != sf { + w.t.Errorf("Expected socket family %d, got %d", ef, sf) } - if *ex.SocketFamily != *got.SocketFamily { - w.t.Errorf("Expected socket family %d, got %d", *ex.SocketFamily, *got.SocketFamily) + + eext := string(w.queue[0].GetExtra()) + gext := string(e.GetExtra()) + if eext != gext { + w.t.Errorf("Expected extra %s, got %s", eext, gext) } w.queue = w.queue[1:] } @@ -65,11 +91,29 @@ func TestDnstap(t *testing.T) { test.A("example.org. 3600 IN A 10.0.0.1"), }, }.Msg() - tapq := testMessage() // leave type unset for deepEqual - msg.SetType(tapq, tap.Message_CLIENT_QUERY) - tapr := testMessage() - msg.SetType(tapr, tap.Message_CLIENT_RESPONSE) - testCase(t, tapq, tapr, q, r) + + tapq := &tap.Dnstap{ + Message: testMessage(), + } + msg.SetType(tapq.GetMessage(), tap.Message_CLIENT_QUERY) + tapr := &tap.Dnstap{ + Message: testMessage(), + } + msg.SetType(tapr.GetMessage(), tap.Message_CLIENT_RESPONSE) + testCase(t, tapq, tapr, q, r, "") + + tapq_with_extra := &tap.Dnstap{ + Message: testMessage(), // leave type unset for deepEqual + Extra: []byte("extra_field_MetadataValue_A_example.org._IN_udp_29_10.240.0.1_40212_127.0.0.1"), + } + msg.SetType(tapq_with_extra.GetMessage(), tap.Message_CLIENT_QUERY) + tapr_with_extra := &tap.Dnstap{ + Message: testMessage(), + Extra: []byte("extra_field_MetadataValue_A_example.org._IN_udp_29_10.240.0.1_40212_127.0.0.1"), + } + msg.SetType(tapr_with_extra.GetMessage(), tap.Message_CLIENT_RESPONSE) + extraFormat := "extra_field_{/metadata/test}_{type}_{name}_{class}_{proto}_{size}_{remote}_{port}_{local}" + testCase(t, tapq_with_extra, tapr_with_extra, q, r, extraFormat) } func testMessage() *tap.Message { @@ -83,3 +127,25 @@ func testMessage() *tap.Message { QueryPort: &port, } } + +func TestTapMessage(t *testing.T) { + extraFormat := "extra_field_no_replacement_{/metadata/test}_{type}_{name}_{class}_{proto}_{size}_{remote}_{port}_{local}" + tapq := &tap.Dnstap{ + Message: testMessage(), + // extra field would not be replaced, since TapMessage won't pass context + Extra: []byte(extraFormat), + } + msg.SetType(tapq.GetMessage(), tap.Message_CLIENT_QUERY) + + w := writer{t: t} + w.queue = append(w.queue, tapq) + h := Dnstap{ + Next: test.HandlerFunc(func(_ context.Context, + w dns.ResponseWriter, r *dns.Msg) (int, error) { + return 0, w.WriteMsg(r) + }), + io: &w, + ExtraFormat: extraFormat, + } + h.TapMessage(tapq.GetMessage()) +} diff --git a/plugin/dnstap/io.go b/plugin/dnstap/io.go index 6823fa8a63..4a6af6cb54 100644 --- a/plugin/dnstap/io.go +++ b/plugin/dnstap/io.go @@ -1,6 +1,7 @@ package dnstap import ( + "crypto/tls" "net" "sync/atomic" "time" @@ -14,45 +15,67 @@ const ( tcpTimeout = 4 * time.Second flushTimeout = 1 * time.Second + + skipVerify = false // by default, every tls connection is verified to be secure ) // tapper interface is used in testing to mock the Dnstap method. type tapper interface { - Dnstap(tap.Dnstap) + Dnstap(*tap.Dnstap) } // dio implements the Tapper interface. type dio struct { - endpoint string - proto string - conn net.Conn - enc *encoder - queue chan tap.Dnstap - dropped uint32 - quit chan struct{} - flushTimeout time.Duration - tcpTimeout time.Duration + endpoint string + proto string + enc *encoder + queue chan *tap.Dnstap + dropped uint32 + quit chan struct{} + flushTimeout time.Duration + tcpTimeout time.Duration + skipVerify bool + tcpWriteBufSize int } // newIO returns a new and initialized pointer to a dio. -func newIO(proto, endpoint string) *dio { +func newIO(proto, endpoint string, multipleQueue int, multipleTcpWriteBuf int) *dio { return &dio{ - endpoint: endpoint, - proto: proto, - queue: make(chan tap.Dnstap, queueSize), - quit: make(chan struct{}), - flushTimeout: flushTimeout, - tcpTimeout: tcpTimeout, + endpoint: endpoint, + proto: proto, + queue: make(chan *tap.Dnstap, multipleQueue*queueSize), + quit: make(chan struct{}), + flushTimeout: flushTimeout, + tcpTimeout: tcpTimeout, + skipVerify: skipVerify, + tcpWriteBufSize: multipleTcpWriteBuf * tcpWriteBufSize, } } func (d *dio) dial() error { - conn, err := net.DialTimeout(d.proto, d.endpoint, d.tcpTimeout) - if err != nil { - return err + var conn net.Conn + var err error + + if d.proto == "tls" { + config := &tls.Config{ + InsecureSkipVerify: d.skipVerify, + } + dialer := &net.Dialer{ + Timeout: d.tcpTimeout, + } + conn, err = tls.DialWithDialer(dialer, "tcp", d.endpoint, config) + if err != nil { + return err + } + } else { + conn, err = net.DialTimeout(d.proto, d.endpoint, d.tcpTimeout) + if err != nil { + return err + } } + if tcpConn, ok := conn.(*net.TCPConn); ok { - tcpConn.SetWriteBuffer(tcpWriteBufSize) + tcpConn.SetWriteBuffer(d.tcpWriteBufSize) tcpConn.SetNoDelay(false) } @@ -68,7 +91,7 @@ func (d *dio) connect() error { } // Dnstap enqueues the payload for log. -func (d *dio) Dnstap(payload tap.Dnstap) { +func (d *dio) Dnstap(payload *tap.Dnstap) { select { case d.queue <- payload: default: @@ -92,8 +115,10 @@ func (d *dio) write(payload *tap.Dnstap) error { } func (d *dio) serve() { - timeout := time.After(d.flushTimeout) + timeout := time.NewTimer(d.flushTimeout) + defer timeout.Stop() for { + timeout.Reset(d.flushTimeout) select { case <-d.quit: if d.enc == nil { @@ -103,10 +128,10 @@ func (d *dio) serve() { d.enc.close() return case payload := <-d.queue: - if err := d.write(&payload); err != nil { + if err := d.write(payload); err != nil { d.dial() } - case <-timeout: + case <-timeout.C: if dropped := atomic.SwapUint32(&d.dropped, 0); dropped > 0 { log.Warningf("Dropped dnstap messages: %d", dropped) } @@ -115,7 +140,6 @@ func (d *dio) serve() { } else { d.enc.flush() } - timeout = time.After(d.flushTimeout) } } } diff --git a/plugin/dnstap/io_test.go b/plugin/dnstap/io_test.go index 30f0c75fbb..489b4da918 100644 --- a/plugin/dnstap/io_test.go +++ b/plugin/dnstap/io_test.go @@ -18,6 +18,7 @@ var ( ) func accept(t *testing.T, l net.Listener, count int) { + t.Helper() server, err := l.Accept() if err != nil { t.Fatalf("Server accepted: %s", err) @@ -30,7 +31,7 @@ func accept(t *testing.T, l net.Listener, count int) { t.Fatalf("Server decoder: %s", err) } - for i := 0; i < count; i++ { + for range count { if _, err := dec.Decode(); err != nil { t.Errorf("Server decode: %s", err) } @@ -60,12 +61,12 @@ func TestTransport(t *testing.T) { wg.Done() }() - dio := newIO(param[0], l.Addr().String()) + dio := newIO(param[0], l.Addr().String(), 1, 1) dio.tcpTimeout = 10 * time.Millisecond dio.flushTimeout = 30 * time.Millisecond dio.connect() - dio.Dnstap(tmsg) + dio.Dnstap(&tmsg) wg.Wait() l.Close() @@ -89,17 +90,17 @@ func TestRace(t *testing.T) { wg.Done() }() - dio := newIO("tcp", l.Addr().String()) + dio := newIO("tcp", l.Addr().String(), 1, 1) dio.tcpTimeout = 10 * time.Millisecond dio.flushTimeout = 30 * time.Millisecond dio.connect() defer dio.close() wg.Add(count) - for i := 0; i < count; i++ { + for range count { go func() { tmsg := tap.Dnstap_MESSAGE - dio.Dnstap(tap.Dnstap{Type: &tmsg}) + dio.Dnstap(&tap.Dnstap{Type: &tmsg}) wg.Done() }() } @@ -122,13 +123,13 @@ func TestReconnect(t *testing.T) { }() addr := l.Addr().String() - dio := newIO("tcp", addr) + dio := newIO("tcp", addr, 1, 1) dio.tcpTimeout = 10 * time.Millisecond dio.flushTimeout = 30 * time.Millisecond dio.connect() defer dio.close() - dio.Dnstap(tmsg) + dio.Dnstap(&tmsg) wg.Wait() @@ -147,9 +148,9 @@ func TestReconnect(t *testing.T) { wg.Done() }() - for i := 0; i < count; i++ { + for range count { time.Sleep(100 * time.Millisecond) - dio.Dnstap(tmsg) + dio.Dnstap(&tmsg) } wg.Wait() } diff --git a/plugin/dnstap/setup.go b/plugin/dnstap/setup.go index 4324087dd9..6e758a2eaf 100644 --- a/plugin/dnstap/setup.go +++ b/plugin/dnstap/setup.go @@ -1,75 +1,175 @@ package dnstap import ( + "net/url" + "os" + "strconv" "strings" "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" clog "github.com/coredns/coredns/plugin/pkg/log" - "github.com/coredns/coredns/plugin/pkg/parse" + "github.com/coredns/coredns/plugin/pkg/replacer" ) var log = clog.NewWithPlugin("dnstap") func init() { plugin.Register("dnstap", setup) } -func parseConfig(c *caddy.Controller) (Dnstap, error) { - c.Next() // directive name - d := Dnstap{} - endpoint := "" +const ( + // Upper bounds chosen to keep memory use and kernel socket buffer requests reasonable + // while allowing large configurations. Write buffer multiple is in MiB units; queue + // multiple is applied to 10,000 messages. See plugin README for parameter semantics. + maxMultipleTcpWriteBuf = 1024 // up to 1 GiB write buffer per TCP connection + maxMultipleQueue = 4096 // up to 40,960,000 enqueued messages +) - if !c.Args(&endpoint) { - return d, c.ArgErr() - } +func parseConfig(c *caddy.Controller) ([]*Dnstap, error) { + dnstaps := []*Dnstap{} - if strings.HasPrefix(endpoint, "tcp://") { - // remote IP endpoint - servers, err := parse.HostPortOrFile(endpoint[6:]) - if err != nil { - return d, c.ArgErr() + for c.Next() { // directive name + d := Dnstap{ + MultipleTcpWriteBuf: 1, + MultipleQueue: 1, } - dio := newIO("tcp", servers[0]) - d = Dnstap{io: dio} - } else { - endpoint = strings.TrimPrefix(endpoint, "unix://") - dio := newIO("unix", endpoint) - d = Dnstap{io: dio} - } - d.IncludeRawMessage = c.NextArg() && c.Val() == "full" + d.repl = replacer.New() + + args := c.RemainingArgs() + + if len(args) == 0 { + return nil, c.ArgErr() + } + + endpoint := args[0] + + if len(args) >= 3 { + tcpWriteBuf := args[2] + if v, err := strconv.Atoi(tcpWriteBuf); err == nil { + if v < 1 || v > maxMultipleTcpWriteBuf { + return nil, c.Errf("dnstap: MultipleTcpWriteBuf must be between 1 and %d (MiB units): %d", maxMultipleTcpWriteBuf, v) + } + d.MultipleTcpWriteBuf = v + } else { + return nil, c.Errf("dnstap: invalid MultipleTcpWriteBuf %q: %v", tcpWriteBuf, err) + } + } + if len(args) >= 4 { + qSize := args[3] + if v, err := strconv.Atoi(qSize); err == nil { + if v < 1 || v > maxMultipleQueue { + return nil, c.Errf("dnstap: MultipleQueue must be between 1 and %d (x10k messages): %d", maxMultipleQueue, v) + } + d.MultipleQueue = v + } else { + return nil, c.Errf("dnstap: invalid MultipleQueue %q: %v", qSize, err) + } + } + + var dio *dio + if strings.HasPrefix(endpoint, "tls://") { + // remote network endpoint + endpointURL, err := url.Parse(endpoint) + if err != nil { + return nil, c.ArgErr() + } + dio = newIO("tls", endpointURL.Host, d.MultipleQueue, d.MultipleTcpWriteBuf) + d.io = dio + } else if strings.HasPrefix(endpoint, "tcp://") { + // remote network endpoint + endpointURL, err := url.Parse(endpoint) + if err != nil { + return nil, c.ArgErr() + } + dio = newIO("tcp", endpointURL.Host, d.MultipleQueue, d.MultipleTcpWriteBuf) + d.io = dio + } else { + endpoint = strings.TrimPrefix(endpoint, "unix://") + dio = newIO("unix", endpoint, d.MultipleQueue, d.MultipleTcpWriteBuf) + d.io = dio + } + + d.IncludeRawMessage = len(args) >= 2 && args[1] == "full" + + hostname, _ := os.Hostname() + d.Identity = []byte(hostname) + d.Version = []byte(caddy.AppName + "-" + caddy.AppVersion) - return d, nil + for c.NextBlock() { + switch c.Val() { + case "skipverify": + { + dio.skipVerify = true + } + case "identity": + { + if !c.NextArg() { + return nil, c.ArgErr() + } + d.Identity = []byte(c.Val()) + } + case "version": + { + if !c.NextArg() { + return nil, c.ArgErr() + } + d.Version = []byte(c.Val()) + } + case "extra": + { + if !c.NextArg() { + return nil, c.ArgErr() + } + d.ExtraFormat = c.Val() + } + } + } + dnstaps = append(dnstaps, &d) + } + return dnstaps, nil } func setup(c *caddy.Controller) error { - dnstap, err := parseConfig(c) + dnstaps, err := parseConfig(c) if err != nil { return plugin.Error("dnstap", err) } - c.OnStartup(func() error { - if err := dnstap.io.(*dio).connect(); err != nil { - log.Errorf("No connection to dnstap endpoint: %s", err) - } - return nil - }) - - c.OnRestart(func() error { - dnstap.io.(*dio).close() - return nil - }) - - c.OnFinalShutdown(func() error { - dnstap.io.(*dio).close() - return nil - }) - - dnsserver.GetConfig(c).AddPlugin( - func(next plugin.Handler) plugin.Handler { - dnstap.Next = next - return dnstap + for i := range dnstaps { + dnstap := dnstaps[i] + c.OnStartup(func() error { + if err := dnstap.io.(*dio).connect(); err != nil { + log.Errorf("No connection to dnstap endpoint: %s", err) + } + return nil + }) + + c.OnRestart(func() error { + dnstap.io.(*dio).close() + return nil + }) + + c.OnFinalShutdown(func() error { + dnstap.io.(*dio).close() + return nil }) + if i == len(dnstaps)-1 { + // last dnstap plugin in block: point next to next plugin + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + dnstap.Next = next + return dnstap + }) + } else { + // not last dnstap plugin in block: point next to next dnstap + nextDnstap := dnstaps[i+1] + dnsserver.GetConfig(c).AddPlugin(func(plugin.Handler) plugin.Handler { + dnstap.Next = nextDnstap + return dnstap + }) + } + } + return nil } diff --git a/plugin/dnstap/setup_test.go b/plugin/dnstap/setup_test.go index 6b9ad284b6..f2edae5aa7 100644 --- a/plugin/dnstap/setup_test.go +++ b/plugin/dnstap/setup_test.go @@ -1,27 +1,71 @@ package dnstap import ( + "os" + "reflect" "testing" "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" ) +type results struct { + endpoint string + full bool + proto string + identity []byte + version []byte + extraFormat string + multipleTcpWriteBuf int + multipleQueue int +} + func TestConfig(t *testing.T) { + hostname, _ := os.Hostname() tests := []struct { - in string - endpoint string - full bool - proto string - fail bool + in string + fail bool + expect []results }{ - {"dnstap dnstap.sock full", "dnstap.sock", true, "unix", false}, - {"dnstap unix://dnstap.sock", "dnstap.sock", false, "unix", false}, - {"dnstap tcp://127.0.0.1:6000", "127.0.0.1:6000", false, "tcp", false}, - {"dnstap", "fail", false, "tcp", true}, + {"dnstap dnstap.sock full", false, []results{{"dnstap.sock", true, "unix", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap unix://dnstap.sock", false, []results{{"dnstap.sock", false, "unix", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap tcp://127.0.0.1:6000", false, []results{{"127.0.0.1:6000", false, "tcp", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap tcp://[::1]:6000", false, []results{{"[::1]:6000", false, "tcp", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap tcp://example.com:6000", false, []results{{"example.com:6000", false, "tcp", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap", true, []results{{"fail", false, "tcp", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap dnstap.sock full {\nidentity NAME\nversion VER\n}\n", false, []results{{"dnstap.sock", true, "unix", []byte("NAME"), []byte("VER"), "", 1, 1}}}, + {"dnstap dnstap.sock full {\nidentity NAME\nversion VER\nextra EXTRA\n}\n", false, []results{{"dnstap.sock", true, "unix", []byte("NAME"), []byte("VER"), "EXTRA", 1, 1}}}, + {"dnstap dnstap.sock {\nidentity NAME\nversion VER\nextra EXTRA\n}\n", false, []results{{"dnstap.sock", false, "unix", []byte("NAME"), []byte("VER"), "EXTRA", 1, 1}}}, + {"dnstap {\nidentity NAME\nversion VER\nextra EXTRA\n}\n", true, []results{{"fail", false, "tcp", []byte("NAME"), []byte("VER"), "EXTRA", 1, 1}}}, + {`dnstap dnstap.sock full { + identity NAME + version VER + extra EXTRA + } + dnstap tcp://127.0.0.1:6000 { + identity NAME2 + version VER2 + extra EXTRA2 + }`, false, []results{ + {"dnstap.sock", true, "unix", []byte("NAME"), []byte("VER"), "EXTRA", 1, 1}, + {"127.0.0.1:6000", false, "tcp", []byte("NAME2"), []byte("VER2"), "EXTRA2", 1, 1}, + }}, + {"dnstap tls://127.0.0.1:6000", false, []results{{"127.0.0.1:6000", false, "tls", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap dnstap.sock {\nidentity\n}\n", true, []results{{"dnstap.sock", false, "unix", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap dnstap.sock {\nversion\n}\n", true, []results{{"dnstap.sock", false, "unix", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap dnstap.sock {\nextra\n}\n", true, []results{{"dnstap.sock", false, "unix", []byte(hostname), []byte("-"), "", 1, 1}}}, + // Limits and parsing for writebuffer (MiB) and queue (x10k) + {"dnstap dnstap.sock full 1024 2048", false, []results{{"dnstap.sock", true, "unix", []byte(hostname), []byte("-"), "", 1024, 2048}}}, + {"dnstap dnstap.sock full 1025 1", true, []results{{"dnstap.sock", true, "unix", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap dnstap.sock full 1 4097", true, []results{{"dnstap.sock", true, "unix", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap dnstap.sock full 0 10", true, []results{{"dnstap.sock", true, "unix", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap dnstap.sock full 10 0", true, []results{{"dnstap.sock", true, "unix", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap dnstap.sock full x 10", true, []results{{"dnstap.sock", true, "unix", []byte(hostname), []byte("-"), "", 1, 1}}}, + {"dnstap dnstap.sock full 10 y", true, []results{{"dnstap.sock", true, "unix", []byte(hostname), []byte("-"), "", 1, 1}}}, } for i, tc := range tests { c := caddy.NewTestController("dns", tc.in) - tap, err := parseConfig(c) + taps, err := parseConfig(c) if tc.fail && err == nil { t.Fatalf("Test %d: expected test to fail: %s: %s", i, tc.in, err) } @@ -32,14 +76,78 @@ func TestConfig(t *testing.T) { if err != nil { t.Fatalf("Test %d: expected no error, got %s", i, err) } - if x := tap.io.(*dio).endpoint; x != tc.endpoint { - t.Errorf("Test %d: expected endpoint %s, got %s", i, tc.endpoint, x) - } - if x := tap.io.(*dio).proto; x != tc.proto { - t.Errorf("Test %d: expected proto %s, got %s", i, tc.proto, x) - } - if x := tap.IncludeRawMessage; x != tc.full { - t.Errorf("Test %d: expected IncludeRawMessage %t, got %t", i, tc.full, x) + for i, tap := range taps { + if x := tap.io.(*dio).endpoint; x != tc.expect[i].endpoint { + t.Errorf("Test %d: expected endpoint %s, got %s", i, tc.expect[i].endpoint, x) + } + if x := tap.io.(*dio).proto; x != tc.expect[i].proto { + t.Errorf("Test %d: expected proto %s, got %s", i, tc.expect[i].proto, x) + } + if x := tap.IncludeRawMessage; x != tc.expect[i].full { + t.Errorf("Test %d: expected IncludeRawMessage %t, got %t", i, tc.expect[i].full, x) + } + if x := string(tap.Identity); x != string(tc.expect[i].identity) { + t.Errorf("Test %d: expected identity %s, got %s", i, tc.expect[i].identity, x) + } + if x := string(tap.Version); x != string(tc.expect[i].version) { + t.Errorf("Test %d: expected version %s, got %s", i, tc.expect[i].version, x) + } + if x := tap.MultipleTcpWriteBuf; x != tc.expect[i].multipleTcpWriteBuf { + t.Errorf("Test %d: expected MultipleTcpWriteBuf %d, got %d", i, tc.expect[i].multipleTcpWriteBuf, x) + } + if x := tap.MultipleQueue; x != tc.expect[i].multipleQueue { + t.Errorf("Test %d: expected MultipleQueue %d, got %d", i, tc.expect[i].multipleQueue, x) + } + if x := tap.ExtraFormat; x != tc.expect[i].extraFormat { + t.Errorf("Test %d: expected extra format %s, got %s", i, tc.expect[i].extraFormat, x) + } } } } + +func TestMultiDnstap(t *testing.T) { + input := ` + dnstap dnstap1.sock + dnstap dnstap2.sock + dnstap dnstap3.sock + ` + + c := caddy.NewTestController("dns", input) + setup(c) + dnsserver.NewServer("", []*dnsserver.Config{dnsserver.GetConfig(c)}) + + handlers := dnsserver.GetConfig(c).Handlers() + d1, ok := handlers[0].(*Dnstap) + if !ok { + t.Fatalf("expected first plugin to be Dnstap, got %v", reflect.TypeOf(handlers[0])) + } + + if d1.io.(*dio).endpoint != "dnstap1.sock" { + t.Errorf("expected first dnstap to \"dnstap1.sock\", got %q", d1.io.(*dio).endpoint) + } + if d1.Next == nil { + t.Fatal("expected first dnstap to point to next dnstap instance") + } + + d2, ok := d1.Next.(*Dnstap) + if !ok { + t.Fatalf("expected second plugin to be Dnstap, got %v", reflect.TypeOf(d1.Next)) + } + if d2.io.(*dio).endpoint != "dnstap2.sock" { + t.Errorf("expected second dnstap to \"dnstap2.sock\", got %q", d2.io.(*dio).endpoint) + } + if d2.Next == nil { + t.Fatal("expected second dnstap to point to third dnstap instance") + } + + d3, ok := d2.Next.(*Dnstap) + if !ok { + t.Fatalf("expected third plugin to be Dnstap, got %v", reflect.TypeOf(d2.Next)) + } + if d3.io.(*dio).endpoint != "dnstap3.sock" { + t.Errorf("expected third dnstap to \"dnstap3.sock\", got %q", d3.io.(*dio).endpoint) + } + if d3.Next != nil { + t.Error("expected third plugin to be last, but Next is not nil") + } +} diff --git a/plugin/dnstap/writer.go b/plugin/dnstap/writer.go index 1772634967..9ef6e620c4 100644 --- a/plugin/dnstap/writer.go +++ b/plugin/dnstap/writer.go @@ -1,9 +1,11 @@ package dnstap import ( + "context" "time" "github.com/coredns/coredns/plugin/dnstap/msg" + "github.com/coredns/coredns/request" tap "github.com/dnstap/golang-dnstap" "github.com/miekg/dns" @@ -13,8 +15,9 @@ import ( type ResponseWriter struct { queryTime time.Time query *dns.Msg + ctx context.Context dns.ResponseWriter - Dnstap + *Dnstap } // WriteMsg writes back the response to the client and THEN works on logging the request and response to dnstap. @@ -35,6 +38,7 @@ func (w *ResponseWriter) WriteMsg(resp *dns.Msg) error { } msg.SetType(r, tap.Message_CLIENT_RESPONSE) - w.TapMessage(r) + state := request.Request{W: w.ResponseWriter, Req: w.query} + w.TapMessageWithMetadata(w.ctx, r, state) return nil } diff --git a/plugin/erratic/erratic.go b/plugin/erratic/erratic.go index da7f68a649..a764abdfeb 100644 --- a/plugin/erratic/erratic.go +++ b/plugin/erratic/erratic.go @@ -57,7 +57,7 @@ func (e *Erratic) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg rr.Header().Name = state.QName() m.Answer = append(m.Answer, &rr) if e.large { - for i := 0; i < 29; i++ { + for range 29 { m.Answer = append(m.Answer, &rr) } } diff --git a/plugin/erratic/erratic_test.go b/plugin/erratic/erratic_test.go index ec2ec5c0a4..de8dbe4d61 100644 --- a/plugin/erratic/erratic_test.go +++ b/plugin/erratic/erratic_test.go @@ -37,7 +37,7 @@ func TestErraticDrop(t *testing.T) { if err != tc.expectedErr { t.Errorf("Test %d: Expected error %q, but got %q", i, tc.expectedErr, err) } - if code != int(tc.expectedCode) { + if code != tc.expectedCode { t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code) } @@ -71,7 +71,7 @@ func TestErraticTruncate(t *testing.T) { if err != tc.expectedErr { t.Errorf("Test %d: Expected error %q, but got %q", i, tc.expectedErr, err) } - if code != int(tc.expectedCode) { + if code != tc.expectedCode { t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code) } diff --git a/plugin/errors/README.md b/plugin/errors/README.md index 61ed582cb5..27ba1058a8 100644 --- a/plugin/errors/README.md +++ b/plugin/errors/README.md @@ -22,12 +22,15 @@ Extra knobs are available with an expanded syntax: ~~~ errors { - consolidate DURATION REGEXP + stacktrace + consolidate DURATION REGEXP [LEVEL] } ~~~ -Option `consolidate` allows collecting several error messages matching the regular expression **REGEXP** during **DURATION**. After the **DURATION** since receiving the first such message, the consolidated message will be printed to standard output, e.g. +Option `stacktrace` will log a stacktrace during panic recovery. +Option `consolidate` allows collecting several error messages matching the regular expression **REGEXP** during **DURATION**. After the **DURATION** since receiving the first such message, the consolidated message will be printed to standard output with +log level, which is configurable by optional option **LEVEL**. Supported options for **LEVEL** option are `warning`,`error`,`info` and `debug`. ~~~ 2 errors like '^read udp .* i/o timeout$' occurred in last 30s ~~~ @@ -47,13 +50,15 @@ example.org { } ~~~ -Use the *forward* to resolve queries via 8.8.8.8 and print consolidated error messages for errors with suffix " i/o timeout" or with prefix "Failed to ". +Use the *forward* plugin to resolve queries via 8.8.8.8 and print consolidated messages +for errors with suffix " i/o timeout" as warnings, +and errors with prefix "Failed to " as errors. ~~~ corefile . { forward . 8.8.8.8 errors { - consolidate 5m ".* i/o timeout$" + consolidate 5m ".* i/o timeout$" warning consolidate 30s "^Failed to .+" } } diff --git a/plugin/errors/benchmark_test.go b/plugin/errors/benchmark_test.go index 04e64332c4..65a5c65ec9 100644 --- a/plugin/errors/benchmark_test.go +++ b/plugin/errors/benchmark_test.go @@ -18,7 +18,7 @@ func BenchmarkServeDNS(b *testing.B) { w := &test.ResponseWriter{} ctx := context.TODO() - for i := 0; i < b.N; i++ { + for b.Loop() { _, err := h.ServeDNS(ctx, w, r) if err != nil { b.Errorf("ServeDNS returned error: %s", err) diff --git a/plugin/errors/errors.go b/plugin/errors/errors.go index 9565f11abd..213ec77d98 100644 --- a/plugin/errors/errors.go +++ b/plugin/errors/errors.go @@ -18,10 +18,11 @@ import ( var log = clog.NewWithPlugin("errors") type pattern struct { - ptimer unsafe.Pointer - count uint32 - period time.Duration - pattern *regexp.Regexp + ptimer unsafe.Pointer + count uint32 + period time.Duration + pattern *regexp.Regexp + logCallback func(format string, v ...any) } func (p *pattern) timer() *time.Timer { @@ -46,7 +47,7 @@ func newErrorHandler() *errorHandler { func (h *errorHandler) logPattern(i int) { cnt := atomic.SwapUint32(&h.patterns[i].count, 0) if cnt > 0 { - log.Errorf("%d errors like '%s' occurred in last %s", + h.patterns[i].logCallback("%d errors like '%s' occurred in last %s", cnt, h.patterns[i].pattern.String(), h.patterns[i].period) } } diff --git a/plugin/errors/errors_test.go b/plugin/errors/errors_test.go index 9563b73230..d06c854f3d 100644 --- a/plugin/errors/errors_test.go +++ b/plugin/errors/errors_test.go @@ -14,6 +14,7 @@ import ( "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/pkg/dnstest" + clog "github.com/coredns/coredns/plugin/pkg/log" "github.com/coredns/coredns/plugin/test" "github.com/miekg/dns" @@ -71,21 +72,56 @@ func TestErrors(t *testing.T) { } func TestLogPattern(t *testing.T) { - buf := bytes.Buffer{} - golog.SetOutput(&buf) - - h := &errorHandler{ - patterns: []*pattern{{ - count: 4, - period: 2 * time.Second, - pattern: regexp.MustCompile("^error.*!$"), - }}, + type args struct { + logCallback func(format string, v ...any) + } + tests := []struct { + name string + args args + want string + }{ + { + name: "error log", + args: args{logCallback: log.Errorf}, + want: "[ERROR] plugin/errors: 4 errors like '^error.*!$' occurred in last 2s", + }, + { + name: "warn log", + args: args{logCallback: log.Warningf}, + want: "[WARNING] plugin/errors: 4 errors like '^error.*!$' occurred in last 2s", + }, + { + name: "info log", + args: args{logCallback: log.Infof}, + want: "[INFO] plugin/errors: 4 errors like '^error.*!$' occurred in last 2s", + }, + { + name: "debug log", + args: args{logCallback: log.Debugf}, + want: "[DEBUG] plugin/errors: 4 errors like '^error.*!$' occurred in last 2s", + }, } - h.logPattern(0) - expLog := "4 errors like '^error.*!$' occurred in last 2s" - if log := buf.String(); !strings.Contains(log, expLog) { - t.Errorf("Expected log %q, but got %q", expLog, log) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bytes.Buffer{} + clog.D.Set() + golog.SetOutput(&buf) + + h := &errorHandler{ + patterns: []*pattern{{ + count: 4, + period: 2 * time.Second, + pattern: regexp.MustCompile("^error.*!$"), + logCallback: tt.args.logCallback, + }}, + } + h.logPattern(0) + + if log := buf.String(); !strings.Contains(log, tt.want) { + t.Errorf("Expected log %q, but got %q", tt.want, log) + } + }) } } @@ -152,8 +188,9 @@ func TestStop(t *testing.T) { h := &errorHandler{ patterns: []*pattern{{ - period: 2 * time.Second, - pattern: regexp.MustCompile("^error.*!$"), + period: 2 * time.Second, + pattern: regexp.MustCompile("^error.*!$"), + logCallback: log.Errorf, }}, } diff --git a/plugin/errors/setup.go b/plugin/errors/setup.go index ef279dadfd..78b12f3a6a 100644 --- a/plugin/errors/setup.go +++ b/plugin/errors/setup.go @@ -52,32 +52,58 @@ func errorsParse(c *caddy.Controller) (*errorHandler, error) { } for c.NextBlock() { - if err := parseBlock(c, handler); err != nil { - return nil, err + switch c.Val() { + case "stacktrace": + dnsserver.GetConfig(c).Stacktrace = true + case "consolidate": + pattern, err := parseConsolidate(c) + if err != nil { + return nil, err + } + handler.patterns = append(handler.patterns, pattern) + default: + return handler, c.SyntaxErr("Unknown field " + c.Val()) } } } return handler, nil } -func parseBlock(c *caddy.Controller, h *errorHandler) error { - if c.Val() != "consolidate" { - return c.SyntaxErr("consolidate") - } - +func parseConsolidate(c *caddy.Controller) (*pattern, error) { args := c.RemainingArgs() - if len(args) != 2 { - return c.ArgErr() + if len(args) < 2 || len(args) > 3 { + return nil, c.ArgErr() } p, err := time.ParseDuration(args[0]) if err != nil { - return c.Err(err.Error()) + return nil, c.Err(err.Error()) } re, err := regexp.Compile(args[1]) if err != nil { - return c.Err(err.Error()) + return nil, c.Err(err.Error()) } - h.patterns = append(h.patterns, &pattern{period: p, pattern: re}) + lc, err := parseLogLevel(c, args) + if err != nil { + return nil, err + } + return &pattern{period: p, pattern: re, logCallback: lc}, nil +} - return nil +func parseLogLevel(c *caddy.Controller, args []string) (func(format string, v ...any), error) { + if len(args) != 3 { + return log.Errorf, nil + } + + switch args[2] { + case "warning": + return log.Warningf, nil + case "error": + return log.Errorf, nil + case "info": + return log.Infof, nil + case "debug": + return log.Debugf, nil + default: + return nil, c.Errf("unknown log level argument in consolidate: %s", args[2]) + } } diff --git a/plugin/errors/setup_test.go b/plugin/errors/setup_test.go index c61cf54c44..5dbc9eca60 100644 --- a/plugin/errors/setup_test.go +++ b/plugin/errors/setup_test.go @@ -1,9 +1,14 @@ package errors import ( + "bytes" + golog "log" + "strings" "testing" "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + clog "github.com/coredns/coredns/plugin/pkg/log" ) func TestErrorsParse(t *testing.T) { @@ -11,47 +16,55 @@ func TestErrorsParse(t *testing.T) { inputErrorsRules string shouldErr bool optCount int + stacktrace bool }{ - {`errors`, false, 0}, - {`errors stdout`, false, 0}, - {`errors errors.txt`, true, 0}, - {`errors visible`, true, 0}, - {`errors { log visible }`, true, 0}, + {`errors`, false, 0, false}, + {`errors stdout`, false, 0, false}, + {`errors errors.txt`, true, 0, false}, + {`errors visible`, true, 0, false}, + {`errors { log visible }`, true, 0, false}, {`errors - errors `, true, 0}, - {`errors a b`, true, 0}, + errors `, true, 0, false}, + {`errors a b`, true, 0, false}, {`errors { consolidate - }`, true, 0}, + }`, true, 0, false}, {`errors { consolidate 1m - }`, true, 0}, + }`, true, 0, false}, {`errors { consolidate 1m .* extra - }`, true, 0}, + }`, true, 0, false}, {`errors { consolidate abc .* - }`, true, 0}, + }`, true, 0, false}, {`errors { consolidate 1 .* - }`, true, 0}, + }`, true, 0, false}, {`errors { consolidate 1m ()) - }`, true, 0}, + }`, true, 0, false}, + {`errors { + stacktrace + }`, false, 0, true}, + {`errors { + stacktrace + consolidate 1m ^exact$ + }`, false, 1, true}, {`errors { consolidate 1m ^exact$ - }`, false, 1}, + }`, false, 1, false}, {`errors { consolidate 1m error - }`, false, 1}, + }`, false, 1, false}, {`errors { consolidate 1m "format error" - }`, false, 1}, + }`, false, 1, false}, {`errors { consolidate 1m error1 consolidate 5s error2 - }`, false, 2}, + }`, false, 2, false}, } for i, test := range tests { c := caddy.NewTestController("dns", test.inputErrorsRules) @@ -65,5 +78,71 @@ func TestErrorsParse(t *testing.T) { t.Errorf("Test %d: pattern count mismatch, expected %d, got %d", i, test.optCount, len(h.patterns)) } + if dnsserver.GetConfig(c).Stacktrace != test.stacktrace { + t.Errorf("Test %d: stacktrace, expected %t, got %t", + i, test.stacktrace, dnsserver.GetConfig(c).Stacktrace) + } + } +} + +func TestProperLogCallbackIsSet(t *testing.T) { + tests := []struct { + name string + inputErrorsRules string + wantLogLevel string + }{ + { + name: "warning is parsed properly", + inputErrorsRules: `errors { + consolidate 1m .* warning + }`, + wantLogLevel: "[WARNING]", + }, + { + name: "error is parsed properly", + inputErrorsRules: `errors { + consolidate 1m .* error + }`, + wantLogLevel: "[ERROR]", + }, + { + name: "info is parsed properly", + inputErrorsRules: `errors { + consolidate 1m .* info + }`, + wantLogLevel: "[INFO]", + }, + { + name: "debug is parsed properly", + inputErrorsRules: `errors { + consolidate 1m .* debug + }`, + wantLogLevel: "[DEBUG]", + }, + { + name: "default is error", + inputErrorsRules: `errors { + consolidate 1m .* + }`, + wantLogLevel: "[ERROR]", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bytes.Buffer{} + golog.SetOutput(&buf) + clog.D.Set() + + c := caddy.NewTestController("dns", tt.inputErrorsRules) + h, _ := errorsParse(c) + + l := h.patterns[0].logCallback + l("some error happened") + + if log := buf.String(); !strings.Contains(log, tt.wantLogLevel) { + t.Errorf("Expected log %q, but got %q", tt.wantLogLevel, log) + } + }) } } diff --git a/plugin/etcd/README.md b/plugin/etcd/README.md index 463bc044cd..f01a669312 100644 --- a/plugin/etcd/README.md +++ b/plugin/etcd/README.md @@ -55,6 +55,8 @@ etcd [ZONES...] { * three arguments - path to cert PEM file, path to client private key PEM file, path to CA PEM file - if the server certificate is not signed by a system-installed CA and client certificate is needed. +* `min-lease-ttl` the minimum TTL for DNS records based on etcd lease duration. Accepts flexible time formats like '30', '30s', '5m', '1h', '2h30m'. Default: 30 seconds. +* `max-lease-ttl` the maximum TTL for DNS records based on etcd lease duration. Accepts flexible time formats like '30', '30s', '5m', '1h', '2h30m'. Default: 24 hours. ## Special Behaviour @@ -62,7 +64,7 @@ The *etcd* plugin leverages directory structure to look for related entries. For an entry `/skydns/test/skydns/mx` would have entries like `/skydns/test/skydns/mx/a`, `/skydns/test/skydns/mx/b` and so on. Similarly a directory `/skydns/test/skydns/mx1` will have all `mx1` entries. Note this plugin will search through the entire (sub)tree for records. In case of the -first example, a query for `mx.skydns.text` will return both the contents of the `a` and `b` records. +first example, a query for `mx.skydns.test` will return both the contents of the `a` and `b` records. If the directory extends deeper those records are returned as well. With etcd3, support for [hierarchical keys are @@ -83,6 +85,8 @@ skydns.local { etcd { path /skydns endpoint http://localhost:2379 + min-lease-ttl 60 # minimum 1 minute for lease-based records + max-lease-ttl 1h # maximum 1 hour for lease-based records } prometheus cache @@ -221,12 +225,14 @@ If you query the zone name for `SRV` now, you will get the following response: If you would like to use `TXT` records, you can set the following: ~~~ % etcdctl put /skydns/local/skydns/x6 '{"ttl":60,"text":"this is a random text message."}' +% etcdctl put /skydns/local/skydns/x7 '{"ttl":60,"text":"this is a another random text message."}' ~~~ If you query the zone name for `TXT` now, you will get the following response: ~~~ sh % dig +short skydns.local TXT @localhost "this is a random text message." +"this is a another random text message." ~~~ ## See Also diff --git a/plugin/etcd/cname_test.go b/plugin/etcd/cname_test.go index a7d45d5100..1e64d6dfdd 100644 --- a/plugin/etcd/cname_test.go +++ b/plugin/etcd/cname_test.go @@ -1,4 +1,4 @@ -// +build etcd +//go:build etcd package etcd @@ -22,7 +22,7 @@ func TestCnameLookup(t *testing.T) { set(t, etc, serv.Key, 0, serv) defer delete(t, etc, serv.Key) } - for _, tc := range dnsTestCasesCname { + for i, tc := range dnsTestCasesCname { m := tc.Msg() rec := dnstest.NewRecorder(&test.ResponseWriter{}) @@ -34,17 +34,17 @@ func TestCnameLookup(t *testing.T) { resp := rec.Msg if err := test.Header(tc, resp); err != nil { - t.Error(err) + t.Errorf("Test %d: %v", i, err) continue } if err := test.Section(tc, test.Answer, resp.Answer); err != nil { - t.Error(err) + t.Errorf("Test %d: %v", i, err) } if err := test.Section(tc, test.Ns, resp.Ns); err != nil { - t.Error(err) + t.Errorf("Test %d: %v", i, err) } if err := test.Section(tc, test.Extra, resp.Extra); err != nil { - t.Error(err) + t.Errorf("Test %d: %v", i, err) } } } @@ -57,16 +57,18 @@ var servicesCname = []*msg.Service{ {Host: "cname5.region2.skydns.test", Key: "cname4.region2.skydns.test."}, {Host: "cname6.region2.skydns.test", Key: "cname5.region2.skydns.test."}, {Host: "endpoint.region2.skydns.test", Key: "cname6.region2.skydns.test."}, + {Host: "10.240.0.1", Key: "endpoint.region2.skydns.test."}, + {Host: "mainendpoint.region2.skydns.test", Key: "region2.skydns.test."}, + {Host: "cname2.region3.skydns.test", Key: "cname3.region3.skydns.test."}, {Host: "cname1.region3.skydns.test", Key: "cname2.region3.skydns.test."}, - {Host: "region3.skydns.test", Key: "cname1.region3.skydns.test."}, - {Host: "", Key: "region3.skydns.test.", Text: "SOME-RECORD-TEXT"}, - {Host: "10.240.0.1", Key: "endpoint.region2.skydns.test."}, + {Host: "endpoint.region3.skydns.test", Key: "cname1.region3.skydns.test."}, + {Host: "", Key: "endpoint.region3.skydns.test.", Text: "SOME-RECORD-TEXT"}, } var dnsTestCasesCname = []test.Case{ - { + { // Test 0 Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{ test.SRV("a.server1.dev.region1.skydns.test. 300 IN SRV 10 100 0 cname1.region2.skydns.test."), @@ -81,26 +83,26 @@ var dnsTestCasesCname = []test.Case{ test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.1"), }, }, - { + { // Test 1 Qname: "region2.skydns.test.", Qtype: dns.TypeCNAME, Answer: []dns.RR{ test.CNAME("region2.skydns.test. 300 IN CNAME mainendpoint.region2.skydns.test."), }, }, - { - Qname: "region3.skydns.test.", Qtype: dns.TypeCNAME, + { // Test 2 + Qname: "endpoint.region3.skydns.test.", Qtype: dns.TypeCNAME, Rcode: dns.RcodeSuccess, Ns: []dns.RR{ test.SOA("skydns.test. 303 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1546424605 7200 1800 86400 30"), }, }, - { + { // Test 3 Qname: "cname3.region3.skydns.test.", Qtype: dns.TypeTXT, Answer: []dns.RR{ test.CNAME("cname3.region3.skydns.test. 300 IN CNAME cname2.region3.skydns.test."), test.CNAME("cname2.region3.skydns.test. 300 IN CNAME cname1.region3.skydns.test."), - test.CNAME("cname1.region3.skydns.test. 300 IN CNAME region3.skydns.test."), - test.TXT("region3.skydns.test. 300 IN TXT \"SOME-RECORD-TEXT\""), + test.CNAME("cname1.region3.skydns.test. 300 IN CNAME endpoint.region3.skydns.test."), + test.TXT("endpoint.region3.skydns.test. 300 IN TXT \"SOME-RECORD-TEXT\""), }, }, } diff --git a/plugin/etcd/etcd.go b/plugin/etcd/etcd.go index 3935baf0b3..b311def1b5 100644 --- a/plugin/etcd/etcd.go +++ b/plugin/etcd/etcd.go @@ -16,26 +16,30 @@ import ( "github.com/coredns/coredns/request" "github.com/miekg/dns" - etcdcv3 "go.etcd.io/etcd/clientv3" - "go.etcd.io/etcd/mvcc/mvccpb" + "go.etcd.io/etcd/api/v3/mvccpb" + etcdcv3 "go.etcd.io/etcd/client/v3" ) const ( - priority = 10 // default priority when nothing is set - ttl = 300 // default ttl when nothing is set - etcdTimeout = 5 * time.Second + defaultPriority = 10 // default priority when nothing is set + defaultTTL = 300 // default ttl when nothing is set + defaultLeaseMinTTL = 30 // default minimum TTL for lease-based records + defaultLeaseMaxTTL = 86400 // default maximum TTL for lease-based records + etcdTimeout = 5 * time.Second ) var errKeyNotFound = errors.New("key not found") // Etcd is a plugin talks to an etcd cluster. type Etcd struct { - Next plugin.Handler - Fall fall.F - Zones []string - PathPrefix string - Upstream *upstream.Upstream - Client *etcdcv3.Client + Next plugin.Handler + Fall fall.F + Zones []string + PathPrefix string + Upstream *upstream.Upstream + Client *etcdcv3.Client + MinLeaseTTL uint32 // minimum TTL for lease-based records + MaxLeaseTTL uint32 // maximum TTL for lease-based records endpoints []string // Stored here as well, to aid in testing. } @@ -146,7 +150,7 @@ Nodes: serv.TTL = e.TTL(n, serv) if serv.Priority == 0 { - serv.Priority = priority + serv.Priority = defaultPriority } if shouldInclude(serv, qType) { @@ -159,10 +163,39 @@ Nodes: // TTL returns the smaller of the etcd TTL and the service's // TTL. If neither of these are set (have a zero value), a default is used. func (e *Etcd) TTL(kv *mvccpb.KeyValue, serv *msg.Service) uint32 { - etcdTTL := uint32(kv.Lease) + var etcdTTL uint32 + + // Get actual lease TTL from etcd if lease exists and client is available + if kv.Lease != 0 && e.Client != nil { + if resp, err := e.Client.TimeToLive(context.Background(), etcdcv3.LeaseID(kv.Lease)); err == nil && resp.TTL > 0 { + leaseTTL := resp.TTL + + // Get bounds with defaults + minTTL := e.MinLeaseTTL + if minTTL == 0 { + minTTL = defaultLeaseMinTTL + } + maxTTL := e.MaxLeaseTTL + if maxTTL == 0 { + maxTTL = defaultLeaseMaxTTL + } + + // Clamp lease TTL to configured bounds + minTTL64 := int64(minTTL) + maxTTL64 := int64(maxTTL) + + if leaseTTL < minTTL64 { + leaseTTL = minTTL64 + } else if leaseTTL > maxTTL64 { + leaseTTL = maxTTL64 + } + + etcdTTL = uint32(leaseTTL) + } + } if etcdTTL == 0 && serv.TTL == 0 { - return ttl + return defaultTTL } if etcdTTL == 0 { return serv.TTL @@ -183,3 +216,11 @@ func (e *Etcd) TTL(kv *mvccpb.KeyValue, serv *msg.Service) uint32 { func shouldInclude(serv *msg.Service, qType uint16) bool { return (qType == dns.TypeTXT && serv.Text != "") || serv.Host != "" } + +// OnShutdown shuts down etcd client when caddy instance restart +func (e *Etcd) OnShutdown() error { + if e.Client != nil { + e.Client.Close() + } + return nil +} diff --git a/plugin/etcd/group_test.go b/plugin/etcd/group_test.go index d5c846238c..2620bf20e8 100644 --- a/plugin/etcd/group_test.go +++ b/plugin/etcd/group_test.go @@ -1,4 +1,4 @@ -// +build etcd +//go:build etcd package etcd @@ -46,12 +46,15 @@ var servicesGroup = []*msg.Service{ {Host: "127.0.0.1", Key: "a.dom1.skydns.test.", Group: "g1"}, {Host: "127.0.0.2", Key: "b.sub.dom1.skydns.test.", Group: "g2"}, + + {Text: "foo", Key: "a.dom3.skydns.test.", Group: "g1"}, + {Text: "bar", Key: "b.sub.dom3.skydns.test.", Group: "g1"}, } var dnsTestCasesGroup = []test.Case{ // Groups { - // hits the group 'g1' and only includes those records + // hits the group 'g1' and only includes those A records Qname: "dom.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{ test.A("dom.skydns.test. 300 IN A 127.0.0.1"), @@ -73,4 +76,12 @@ var dnsTestCasesGroup = []test.Case{ test.A("dom1.skydns.test. 300 IN A 127.0.0.1"), }, }, + { + // hits the group 'g1' and only includes those TXT records + Qname: "dom3.skydns.test.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT("dom3.skydns.test. 300 IN TXT bar"), + test.TXT("dom3.skydns.test. 300 IN TXT foo"), + }, + }, } diff --git a/plugin/etcd/handler.go b/plugin/etcd/handler.go index 395199bd02..72222dd2a3 100644 --- a/plugin/etcd/handler.go +++ b/plugin/etcd/handler.go @@ -21,16 +21,17 @@ func (e *Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ( var ( records, extra []dns.RR + truncated bool err error ) switch state.QType() { case dns.TypeA: - records, err = plugin.A(ctx, e, zone, state, nil, opt) + records, truncated, err = plugin.A(ctx, e, zone, state, nil, opt) case dns.TypeAAAA: - records, err = plugin.AAAA(ctx, e, zone, state, nil, opt) + records, truncated, err = plugin.AAAA(ctx, e, zone, state, nil, opt) case dns.TypeTXT: - records, err = plugin.TXT(ctx, e, zone, state, nil, opt) + records, truncated, err = plugin.TXT(ctx, e, zone, state, nil, opt) case dns.TypeCNAME: records, err = plugin.CNAME(ctx, e, zone, state, opt) case dns.TypePTR: @@ -49,7 +50,7 @@ func (e *Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ( fallthrough default: // Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN - _, err = plugin.A(ctx, e, zone, state, nil, opt) + _, _, err = plugin.A(ctx, e, zone, state, nil, opt) } if err != nil && e.IsNameError(err) { if e.Fall.Through(state.Name()) { @@ -68,9 +69,10 @@ func (e *Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ( m := new(dns.Msg) m.SetReply(r) + m.Truncated = truncated m.Authoritative = true - m.Answer = append(m.Answer, records...) - m.Extra = append(m.Extra, extra...) + m.Answer = records + m.Extra = extra w.WriteMsg(m) return dns.RcodeSuccess, nil diff --git a/plugin/etcd/lookup_test.go b/plugin/etcd/lookup_test.go index b1d74856a5..0b689b083c 100644 --- a/plugin/etcd/lookup_test.go +++ b/plugin/etcd/lookup_test.go @@ -1,4 +1,4 @@ -// +build etcd +//go:build etcd package etcd @@ -28,7 +28,8 @@ var services = []*msg.Service{ {Host: "10.0.0.2", Port: 8080, Key: "b.server1.prod.region1.skydns.test."}, {Host: "::1", Port: 8080, Key: "b.server6.prod.region1.skydns.test."}, // TXT record in server1. - {Host: "", Port: 8080, Text: "sometext", Key: "txt.server1.prod.region1.skydns.test."}, + {Text: "sometext", Key: "a.txt.server1.prod.region1.skydns.test."}, + {Text: "moretext", Key: "b.txt.server1.prod.region1.skydns.test."}, // Unresolvable internal name. {Host: "unresolvable.skydns.test", Key: "cname.prod.region1.skydns.test."}, // Priority. @@ -37,12 +38,16 @@ var services = []*msg.Service{ {Host: "sub.server1", Port: 0, Key: "a.sub.region1.skydns.test."}, {Host: "sub.server2", Port: 80, Key: "b.sub.region1.skydns.test."}, {Host: "10.0.0.1", Port: 8080, Key: "c.sub.region1.skydns.test."}, + // TargetStrip. + {Host: "10.0.0.1", Port: 8080, Key: "a.targetstrip.skydns.test.", TargetStrip: 1}, // Cname loop. {Host: "a.cname.skydns.test", Key: "b.cname.skydns.test."}, {Host: "b.cname.skydns.test", Key: "a.cname.skydns.test."}, // Nameservers. {Host: "10.0.0.2", Key: "a.ns.dns.skydns.test."}, {Host: "10.0.0.3", Key: "b.ns.dns.skydns.test."}, + {Host: "10.0.0.4", Key: "ns1.c.ns.dns.skydns.test.", TargetStrip: 1}, + {Host: "10.0.0.5", Key: "ns2.c.ns.dns.skydns.test.", TargetStrip: 1}, // Zone name as A record (basic, return all) {Host: "10.0.0.2", Key: "x.skydns_zonea.test."}, {Host: "10.0.0.3", Key: "y.skydns_zonea.test."}, @@ -124,6 +129,14 @@ var dnsTestCases = []test.Case{ }, Extra: []dns.RR{test.A("c.sub.region1.skydns.test. 300 IN A 10.0.0.1")}, }, + // SRV TargetStrip Test + { + Qname: "targetstrip.skydns.test.", Qtype: dns.TypeSRV, + Answer: []dns.RR{ + test.SRV("targetstrip.skydns.test. 300 IN SRV 10 100 8080 targetstrip.skydns.test."), + }, + Extra: []dns.RR{test.A("targetstrip.skydns.test. 300 IN A 10.0.0.1")}, + }, // CNAME (unresolvable internal name) { Qname: "cname.prod.region1.skydns.test.", Qtype: dns.TypeA, @@ -133,6 +146,7 @@ var dnsTestCases = []test.Case{ { Qname: "txt.server1.prod.region1.skydns.test.", Qtype: dns.TypeTXT, Answer: []dns.RR{ + test.TXT("txt.server1.prod.region1.skydns.test. 303 IN TXT moretext"), test.TXT("txt.server1.prod.region1.skydns.test. 303 IN TXT sometext"), }, }, @@ -217,10 +231,13 @@ var dnsTestCases = []test.Case{ Answer: []dns.RR{ test.NS("skydns.test. 300 NS a.ns.dns.skydns.test."), test.NS("skydns.test. 300 NS b.ns.dns.skydns.test."), + test.NS("skydns.test. 300 NS c.ns.dns.skydns.test."), }, Extra: []dns.RR{ test.A("a.ns.dns.skydns.test. 300 A 10.0.0.2"), test.A("b.ns.dns.skydns.test. 300 A 10.0.0.3"), + test.A("c.ns.dns.skydns.test. 300 A 10.0.0.4"), + test.A("c.ns.dns.skydns.test. 300 A 10.0.0.5"), }, }, // NS Record Test @@ -234,6 +251,8 @@ var dnsTestCases = []test.Case{ Answer: []dns.RR{ test.A("ns.dns.skydns.test. 300 A 10.0.0.2"), test.A("ns.dns.skydns.test. 300 A 10.0.0.3"), + test.A("ns.dns.skydns.test. 300 A 10.0.0.4"), + test.A("ns.dns.skydns.test. 300 A 10.0.0.5"), }, }, { @@ -320,7 +339,7 @@ func TestLookup(t *testing.T) { defer delete(t, etc, serv.Key) } - for _, tc := range dnsTestCases { + for i, tc := range dnsTestCases { m := tc.Msg() rec := dnstest.NewRecorder(&test.ResponseWriter{}) @@ -328,7 +347,7 @@ func TestLookup(t *testing.T) { resp := rec.Msg if err := test.SortAndCheck(resp, tc); err != nil { - t.Error(err) + t.Errorf("Test %d: %v", i, err) } } } diff --git a/plugin/etcd/msg/path.go b/plugin/etcd/msg/path.go index bfa4588632..2c6cbff0f2 100644 --- a/plugin/etcd/msg/path.go +++ b/plugin/etcd/msg/path.go @@ -22,6 +22,9 @@ func Path(s, prefix string) string { // Domain is the opposite of Path. func Domain(s string) string { l := strings.Split(s, "/") + if l[len(l)-1] == "" { + l = l[:len(l)-1] + } // start with 1, to strip /skydns for i, j := 1, len(l)-1; i < j; i, j = i+1, j-1 { l[i], l[j] = l[j], l[i] diff --git a/plugin/etcd/msg/path_test.go b/plugin/etcd/msg/path_test.go index a9ec59713d..a20d783335 100644 --- a/plugin/etcd/msg/path_test.go +++ b/plugin/etcd/msg/path_test.go @@ -10,3 +10,15 @@ func TestPath(t *testing.T) { } } } + +func TestDomain(t *testing.T) { + result1 := Domain("/skydns/local/cluster/staging/service/") + if result1 != "service.staging.cluster.local." { + t.Errorf("Failure to get domain from etcd key (with a trailing '/'), expect: 'service.staging.cluster.local.', actually get: '%s'", result1) + } + + result2 := Domain("/skydns/local/cluster/staging/service") + if result2 != "service.staging.cluster.local." { + t.Errorf("Failure to get domain from etcd key (without trailing '/'), expect: 'service.staging.cluster.local.' actually get: '%s'", result2) + } +} diff --git a/plugin/etcd/msg/service.go b/plugin/etcd/msg/service.go index 4e049e10a1..e653d07d72 100644 --- a/plugin/etcd/msg/service.go +++ b/plugin/etcd/msg/service.go @@ -149,13 +149,11 @@ func split255(s string) []string { sx := []string{} p, i := 0, 255 for { - if i <= len(s) { - sx = append(sx, s[p:i]) - } else { + if i > len(s) { sx = append(sx, s[p:]) break - } + sx = append(sx, s[p:i]) p, i = p+255, i+255 } @@ -165,7 +163,7 @@ func split255(s string) []string { // targetStrip strips "targetstrip" labels from the left side of the fully qualified name. func targetStrip(name string, targetStrip int) string { offset, end := 0, false - for i := 0; i < targetStrip; i++ { + for range targetStrip { offset, end = dns.NextLabel(name, offset) } if end { diff --git a/plugin/etcd/msg/service_test.go b/plugin/etcd/msg/service_test.go index f334aa5cee..4c997c5bcf 100644 --- a/plugin/etcd/msg/service_test.go +++ b/plugin/etcd/msg/service_test.go @@ -8,7 +8,7 @@ func TestSplit255(t *testing.T) { t.Errorf("Failure to split abc") } s := "" - for i := 0; i < 255; i++ { + for range 255 { s += "a" } xs = split255(s) @@ -20,7 +20,7 @@ func TestSplit255(t *testing.T) { if len(xs) != 2 || xs[1] != "b" { t.Errorf("Failure to split 256 char long string: %d", len(xs)) } - for i := 0; i < 255; i++ { + for range 255 { s += "a" } xs = split255(s) diff --git a/plugin/etcd/msg/type.go b/plugin/etcd/msg/type.go index ad09e74fb9..a300eac760 100644 --- a/plugin/etcd/msg/type.go +++ b/plugin/etcd/msg/type.go @@ -15,11 +15,9 @@ import ( // // Note that a service can double/triple as a TXT record or MX record. func (s *Service) HostType() (what uint16, normalized net.IP) { - ip := net.ParseIP(s.Host) switch { - case ip == nil: if len(s.Text) == 0 { return dns.TypeCNAME, nil diff --git a/plugin/etcd/multi_test.go b/plugin/etcd/multi_test.go index 1efce1861d..7993a25db6 100644 --- a/plugin/etcd/multi_test.go +++ b/plugin/etcd/multi_test.go @@ -1,4 +1,4 @@ -// +build etcd +//go:build etcd package etcd diff --git a/plugin/etcd/other_test.go b/plugin/etcd/other_test.go index 1a69bd6175..a71260ff43 100644 --- a/plugin/etcd/other_test.go +++ b/plugin/etcd/other_test.go @@ -1,4 +1,4 @@ -// +build etcd +//go:build etcd // tests mx and txt records diff --git a/plugin/etcd/setup.go b/plugin/etcd/setup.go index 622e97b91b..2ddbf45971 100644 --- a/plugin/etcd/setup.go +++ b/plugin/etcd/setup.go @@ -2,6 +2,11 @@ package etcd import ( "crypto/tls" + "errors" + "path/filepath" + "strconv" + "strings" + "time" "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" @@ -9,7 +14,7 @@ import ( mwtls "github.com/coredns/coredns/plugin/pkg/tls" "github.com/coredns/coredns/plugin/pkg/upstream" - etcdcv3 "go.etcd.io/etcd/clientv3" + etcdcv3 "go.etcd.io/etcd/client/v3" ) func init() { plugin.Register("etcd", setup) } @@ -20,6 +25,8 @@ func setup(c *caddy.Controller) error { return plugin.Error("etcd", err) } + c.OnShutdown(e.OnShutdown) + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { e.Next = next return e @@ -29,7 +36,12 @@ func setup(c *caddy.Controller) error { } func etcdParse(c *caddy.Controller) (*Etcd, error) { - etc := Etcd{PathPrefix: "skydns"} + config := dnsserver.GetConfig(c) + etc := Etcd{ + PathPrefix: "skydns", + MinLeaseTTL: defaultLeaseMinTTL, + MaxLeaseTTL: defaultLeaseMaxTTL, + } var ( tlsConfig *tls.Config err error @@ -40,16 +52,8 @@ func etcdParse(c *caddy.Controller) (*Etcd, error) { etc.Upstream = upstream.New() - for c.Next() { - etc.Zones = c.RemainingArgs() - if len(etc.Zones) == 0 { - etc.Zones = make([]string, len(c.ServerBlockKeys)) - copy(etc.Zones, c.ServerBlockKeys) - } - for i, str := range etc.Zones { - etc.Zones[i] = plugin.Host(str).Normalize() - } - + if c.Next() { + etc.Zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) for c.NextBlock() { switch c.Val() { case "stubzones": @@ -74,6 +78,11 @@ func etcdParse(c *caddy.Controller) (*Etcd, error) { c.RemainingArgs() case "tls": // cert key cacertfile args := c.RemainingArgs() + for i := range args { + if !filepath.IsAbs(args[i]) && config.Root != "" { + args[i] = filepath.Join(config.Root, args[i]) + } + } tlsConfig, err = mwtls.NewTLSConfigFromArgs(args...) if err != nil { return &Etcd{}, err @@ -87,6 +96,24 @@ func etcdParse(c *caddy.Controller) (*Etcd, error) { return &Etcd{}, c.Errf("credentials requires 2 arguments, username and password") } username, password = args[0], args[1] + case "min-lease-ttl": + if !c.NextArg() { + return &Etcd{}, c.ArgErr() + } + minLeaseTTL, err := parseTTL(c.Val()) + if err != nil { + return &Etcd{}, c.Errf("invalid min-lease-ttl value: %v", err) + } + etc.MinLeaseTTL = minLeaseTTL + case "max-lease-ttl": + if !c.NextArg() { + return &Etcd{}, c.ArgErr() + } + maxLeaseTTL, err := parseTTL(c.Val()) + if err != nil { + return &Etcd{}, c.Errf("invalid max-lease-ttl value: %v", err) + } + etc.MaxLeaseTTL = maxLeaseTTL default: if c.Val() != "}" { return &Etcd{}, c.Errf("unknown property '%s'", c.Val()) @@ -107,8 +134,9 @@ func etcdParse(c *caddy.Controller) (*Etcd, error) { func newEtcdClient(endpoints []string, cc *tls.Config, username, password string) (*etcdcv3.Client, error) { etcdCfg := etcdcv3.Config{ - Endpoints: endpoints, - TLS: cc, + Endpoints: endpoints, + TLS: cc, + DialKeepAliveTime: etcdTimeout, } if username != "" && password != "" { etcdCfg.Username = username @@ -122,3 +150,35 @@ func newEtcdClient(endpoints []string, cc *tls.Config, username, password string } const defaultEndpoint = "http://localhost:2379" + +// parseTTL parses a TTL value with flexible time units using Go's standard duration parsing. +// Supports formats like: "30", "30s", "5m", "1h", "90s", "2h30m", etc. +func parseTTL(s string) (uint32, error) { + s = strings.TrimSpace(s) + if s == "" { + return 0, nil + } + + // Handle plain numbers (assume seconds) + if _, err := strconv.ParseUint(s, 10, 64); err == nil { + // If it's just a number, append "s" for seconds + s += "s" + } + + // Use Go's standard time.ParseDuration for robust parsing + duration, err := time.ParseDuration(s) + if err != nil { + return 0, errors.New("invalid TTL format, use format like '30', '30s', '5m', '1h', or '2h30m'") + } + + // Convert to seconds and check bounds + seconds := duration.Seconds() + if seconds < 0 { + return 0, errors.New("TTL must be non-negative") + } + if seconds > 4294967295 { // uint32 max value + return 0, errors.New("TTL too large, maximum is 4294967295 seconds") + } + + return uint32(seconds), nil +} diff --git a/plugin/etcd/setup_test.go b/plugin/etcd/setup_test.go index 2195636640..c88dd1044e 100644 --- a/plugin/etcd/setup_test.go +++ b/plugin/etcd/setup_test.go @@ -1,4 +1,4 @@ -// +build etcd +//go:build etcd package etcd @@ -66,6 +66,31 @@ func TestSetupEtcd(t *testing.T) { } `, true, "skydns", []string{"http://localhost:2379"}, "Wrong argument count", "", "", }, + // with custom min-lease-ttl + { + `etcd { + endpoint http://localhost:2379 + min-lease-ttl 60 + } + `, false, "skydns", []string{"http://localhost:2379"}, "", "", "", + }, + // with custom max-lease-ttl + { + `etcd { + endpoint http://localhost:2379 + max-lease-ttl 1h + } + `, false, "skydns", []string{"http://localhost:2379"}, "", "", "", + }, + // with both custom min-lease-ttl and max-lease-ttl + { + `etcd { + endpoint http://localhost:2379 + min-lease-ttl 120 + max-lease-ttl 7200 + } + `, false, "skydns", []string{"http://localhost:2379"}, "", "", "", + }, } for i, test := range tests { @@ -113,6 +138,84 @@ func TestSetupEtcd(t *testing.T) { t.Errorf("Etcd password not correctly set for input %s. Expected: '%+v', actual: '%+v'", test.input, test.password, etcd.Client.Password) } } + + // Check TTL configuration for specific test cases + if strings.Contains(test.input, "min-lease-ttl 60") { + if etcd.MinLeaseTTL != 60 { + t.Errorf("MinLeaseTTL not set correctly for input %s. Expected: 60, actual: %d", test.input, etcd.MinLeaseTTL) + } + } + if strings.Contains(test.input, "max-lease-ttl 1h") { + if etcd.MaxLeaseTTL != 3600 { + t.Errorf("MaxLeaseTTL not set correctly for input %s. Expected: 3600, actual: %d", test.input, etcd.MaxLeaseTTL) + } + } + if strings.Contains(test.input, "min-lease-ttl 120") && strings.Contains(test.input, "max-lease-ttl 7200") { + if etcd.MinLeaseTTL != 120 { + t.Errorf("MinLeaseTTL not set correctly for input %s. Expected: 120, actual: %d", test.input, etcd.MinLeaseTTL) + } + if etcd.MaxLeaseTTL != 7200 { + t.Errorf("MaxLeaseTTL not set correctly for input %s. Expected: 7200, actual: %d", test.input, etcd.MaxLeaseTTL) + } + } } } } + +func TestParseTTL(t *testing.T) { + tests := []struct { + input string + expected uint32 + hasError bool + desc string + }{ + // Plain numbers (assumed to be seconds) + {"30", 30, false, "plain number should be treated as seconds"}, + {"300", 300, false, "plain number should be treated as seconds"}, + + // Explicit seconds + {"30s", 30, false, "explicit seconds"}, + {"90s", 90, false, "explicit seconds"}, + + // Minutes + {"5m", 300, false, "5 minutes"}, + {"1m", 60, false, "1 minute"}, + + // Hours + {"1h", 3600, false, "1 hour"}, + {"2h", 7200, false, "2 hours"}, + + // Complex durations (Go's ParseDuration supports this) + {"2h30m", 9000, false, "2 hours 30 minutes"}, + {"1h30m45s", 5445, false, "1 hour 30 minutes 45 seconds"}, + + // Edge cases + {"0", 0, false, "zero should be allowed"}, + {"0s", 0, false, "zero seconds should be allowed"}, + {"", 0, false, "empty string should return 0"}, + + // Error cases + {"-30s", 0, true, "negative duration should error"}, + {"abc", 0, true, "invalid format should error"}, + {"1y", 0, true, "unsupported unit should error"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + result, err := parseTTL(tt.input) + + if tt.hasError { + if err == nil { + t.Errorf("parseTTL(%q) expected error but got none", tt.input) + } + } else { + if err != nil { + t.Errorf("parseTTL(%q) unexpected error: %v", tt.input, err) + } + if result != tt.expected { + t.Errorf("parseTTL(%q) = %d, expected %d", tt.input, result, tt.expected) + } + } + }) + } +} diff --git a/plugin/etcd/ttl_test.go b/plugin/etcd/ttl_test.go new file mode 100644 index 0000000000..44e76428f5 --- /dev/null +++ b/plugin/etcd/ttl_test.go @@ -0,0 +1,113 @@ +package etcd + +import ( + "testing" + + "github.com/coredns/coredns/plugin/etcd/msg" + + "go.etcd.io/etcd/api/v3/mvccpb" +) + +func TestTTL(t *testing.T) { + tests := []struct { + name string + leaseID int64 + serviceTTL uint32 + minLeaseTTL uint32 + maxLeaseTTL uint32 + hasClient bool + expectedTTL uint32 + }{ + { + name: "no client, large lease ID falls back to default", + leaseID: 0x12345678FFFFFFFF, // Large lease ID that would cause issues + serviceTTL: 0, + minLeaseTTL: 0, + maxLeaseTTL: 0, + hasClient: false, + expectedTTL: defaultTTL, + }, + { + name: "no client, zero lease ID falls back to default", + leaseID: 0, + serviceTTL: 0, + minLeaseTTL: 0, + maxLeaseTTL: 0, + hasClient: false, + expectedTTL: defaultTTL, + }, + { + name: "no client, service TTL takes precedence", + leaseID: 120, + serviceTTL: 300, + minLeaseTTL: 0, + maxLeaseTTL: 0, + hasClient: false, + expectedTTL: 300, + }, + { + name: "no client, smaller service TTL wins", + leaseID: 600, + serviceTTL: 120, + minLeaseTTL: 0, + maxLeaseTTL: 0, + hasClient: false, + expectedTTL: 120, + }, + { + name: "custom bounds, no client", + leaseID: 0x12345678FFFFFFFF, + serviceTTL: 0, + minLeaseTTL: 60, // 1 minute + maxLeaseTTL: 3600, // 1 hour + hasClient: false, + expectedTTL: defaultTTL, + }, + { + name: "zero service TTL with lease ID", + leaseID: 600, + serviceTTL: 0, + minLeaseTTL: 0, + maxLeaseTTL: 0, + hasClient: false, + expectedTTL: defaultTTL, + }, + { + name: "both zero, falls back to default", + leaseID: 0, + serviceTTL: 0, + minLeaseTTL: 0, + maxLeaseTTL: 0, + hasClient: false, + expectedTTL: defaultTTL, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create Etcd instance with test configuration + e := &Etcd{ + MinLeaseTTL: tt.minLeaseTTL, + MaxLeaseTTL: tt.maxLeaseTTL, + } + + // Create test data + kv := &mvccpb.KeyValue{ + Key: []byte("/test/service"), + Value: []byte(`{"host": "test.example.com"}`), + Lease: tt.leaseID, + } + + serv := &msg.Service{ + Host: "test.example.com", + TTL: tt.serviceTTL, + } + + resultingTTL := e.TTL(kv, serv) + + if resultingTTL != tt.expectedTTL { + t.Errorf("TTL() = %d, expected %d", resultingTTL, tt.expectedTTL) + } + }) + } +} diff --git a/plugin/file/README.md b/plugin/file/README.md index d1bd42531d..ce49827d27 100644 --- a/plugin/file/README.md +++ b/plugin/file/README.md @@ -27,12 +27,17 @@ If you want to round-robin A and AAAA responses look at the *loadbalance* plugin ~~~ file DBFILE [ZONES... ] { reload DURATION + fallthrough [ZONES...] } ~~~ * `reload` interval to perform a reload of the zone if the SOA version changes. Default is one minute. Value of `0` means to not scan for changes and reload. For example, `30s` checks the zonefile every 30 seconds and reloads the zone when serial changes. +* `fallthrough` If zone matches and no record can be generated, pass request to the next plugin. + If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin + is authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then only + queries for those zones will be subject to fallthrough. If you need outgoing zone transfers, take a look at the *transfer* plugin. diff --git a/plugin/file/closest.go b/plugin/file/closest.go index 64652af83a..7a8efd5d91 100644 --- a/plugin/file/closest.go +++ b/plugin/file/closest.go @@ -8,17 +8,16 @@ import ( // ClosestEncloser returns the closest encloser for qname. func (z *Zone) ClosestEncloser(qname string) (*tree.Elem, bool) { - offset, end := dns.NextLabel(qname, 0) for !end { - elem, _ := z.Tree.Search(qname) + elem, _ := z.Search(qname) if elem != nil { return elem, true } qname = qname[offset:] - offset, end = dns.NextLabel(qname, offset) + offset, end = dns.NextLabel(qname, 0) } - return z.Tree.Search(z.origin) + return z.Search(z.origin) } diff --git a/plugin/file/closest_test.go b/plugin/file/closest_test.go index 40c04ff26c..282823fcc7 100644 --- a/plugin/file/closest_test.go +++ b/plugin/file/closest_test.go @@ -21,6 +21,7 @@ func TestClosestEncloser(t *testing.T) { {"blaat.www.miek.nl.", "www.miek.nl."}, {"www.blaat.miek.nl.", "miek.nl."}, {"blaat.a.miek.nl.", "a.miek.nl."}, + {"blaat.z.a.miek.nl.", "a.miek.nl."}, } for _, tc := range tests { diff --git a/plugin/file/delegation_test.go b/plugin/file/delegation_test.go index a6da6215c3..2372f8d73a 100644 --- a/plugin/file/delegation_test.go +++ b/plugin/file/delegation_test.go @@ -170,6 +170,7 @@ func TestLookupSecureDelegation(t *testing.T) { } func testDelegation(t *testing.T, z, origin string, testcases []test.Case) { + t.Helper() zone, err := Parse(strings.NewReader(z), origin, "stdin", 0) if err != nil { t.Fatalf("Expect no error when reading zone, got %q", err) diff --git a/plugin/file/dnssec_test.go b/plugin/file/dnssec_test.go index 7292523fef..446fb8d5cb 100644 --- a/plugin/file/dnssec_test.go +++ b/plugin/file/dnssec_test.go @@ -167,9 +167,7 @@ func BenchmarkFileLookupDNSSEC(b *testing.B) { m := tc.Msg() - b.ResetTimer() - - for i := 0; i < b.N; i++ { + for b.Loop() { fm.ServeDNS(ctx, rec, m) } } diff --git a/plugin/file/file.go b/plugin/file/file.go index 0834ddc4da..67b81e9a53 100644 --- a/plugin/file/file.go +++ b/plugin/file/file.go @@ -7,6 +7,7 @@ import ( "io" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/fall" clog "github.com/coredns/coredns/plugin/pkg/log" "github.com/coredns/coredns/plugin/transfer" "github.com/coredns/coredns/request" @@ -22,6 +23,8 @@ type ( Next plugin.Handler Zones transfer *transfer.Transfer + + Fall fall.F } // Zones maps zone names to a *Zone. @@ -39,10 +42,14 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i // TODO(miek): match the qname better in the map zone := plugin.Zones(f.Zones.Names).Matches(qname) if zone == "" { + // If no next plugin is configured, it's more correct to return REFUSED as file acts as an authoritative server + if f.Next == nil { + return dns.RcodeRefused, nil + } return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r) } - z, ok := f.Zones.Z[zone] + z, ok := f.Z[zone] if !ok || z == nil { return dns.RcodeServerFailure, nil } @@ -86,6 +93,13 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i answer, ns, extra, result := z.Lookup(ctx, state, qname) + // Only on NXDOMAIN we will fallthrough. + // `z.Lookup` can also return NOERROR for NXDOMAIN see comment see comment "Hacky way to get around empty-non-terminals" inside `Zone.Lookup`. + // It's safe to fallthrough with `result` Sucess (NOERROR) since all other return points in Lookup with Success have answer(s). + if len(answer) == 0 && (result == NameError || result == Success) && f.Fall.Through(qname) { + return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r) + } + m := new(dns.Msg) m.SetReply(r) m.Authoritative = true @@ -99,7 +113,14 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i case Delegation: m.Authoritative = false case ServerFailure: - return dns.RcodeServerFailure, nil + // If the result is SERVFAIL and the answer is non-empty, then the SERVFAIL came from an + // external CNAME lookup and the answer contains the CNAME with no target record. We should + // write the CNAME record to the client instead of sending an empty SERVFAIL response. + if len(m.Answer) == 0 { + return dns.RcodeServerFailure, nil + } + // The rcode in the response should be the rcode received from the target lookup. RFC 6604 section 3 + m.Rcode = dns.RcodeServerFailure } w.WriteMsg(m) @@ -129,10 +150,6 @@ func Parse(f io.Reader, origin, fileName string, serial int64) (*Zone, error) { z := NewZone(origin, fileName) seenSOA := false for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { - if err := zp.Err(); err != nil { - return nil, err - } - if !seenSOA { if s, ok := rr.(*dns.SOA); ok { seenSOA = true @@ -152,6 +169,13 @@ func Parse(f io.Reader, origin, fileName string, serial int64) (*Zone, error) { if !seenSOA { return nil, fmt.Errorf("file %q has no SOA record for origin %s", fileName, origin) } + if zp.Err() != nil { + return nil, fmt.Errorf("failed to parse file %q for origin %s with error %v", fileName, origin, zp.Err()) + } + + if err := zp.Err(); err != nil { + return nil, err + } return z, nil } diff --git a/plugin/file/file_test.go b/plugin/file/file_test.go index 0e4050e386..e1afc7f493 100644 --- a/plugin/file/file_test.go +++ b/plugin/file/file_test.go @@ -6,7 +6,7 @@ import ( ) func BenchmarkFileParseInsert(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { Parse(strings.NewReader(dbMiekENTNL), testzone, "stdin", 0) } } @@ -29,3 +29,31 @@ www IN A 192.168.0.14 mail IN A 192.168.0.15 imap IN CNAME mail ` + +func TestParseSyntaxError(t *testing.T) { + _, err := Parse(strings.NewReader(dbSyntaxError), "example.org.", "stdin", 0) + if err == nil { + t.Fatalf("Zone %q should have failed to load", "example.org.") + } + if !strings.Contains(err.Error(), "\"invalid\"") { + t.Fatalf("Zone %q should have failed with syntax error: %s", "example.org.", err) + } +} + +const dbSyntaxError = ` +$TTL 1M +$ORIGIN example.org. + +@ IN SOA ns1.example.com. admin.example.com. ( + 2005011437 ; Serial + 1200 ; Refresh + 144 ; Retry + 1814400 ; Expire + 2h ) ; Minimum +@ IN NS ns1.example.com. + +# invalid comment +www IN A 192.168.0.14 +mail IN A 192.168.0.15 +imap IN CNAME mail +` diff --git a/plugin/file/fuzz.go b/plugin/file/fuzz.go index e693f58bfb..9c59ab8da4 100644 --- a/plugin/file/fuzz.go +++ b/plugin/file/fuzz.go @@ -1,4 +1,4 @@ -// +build gofuzz +//go:build gofuzz package file diff --git a/plugin/file/include_test.go b/plugin/file/include_test.go index fad91df5c1..490f05a30d 100644 --- a/plugin/file/include_test.go +++ b/plugin/file/include_test.go @@ -10,7 +10,6 @@ import ( // Make sure the external miekg/dns dependency is up to date func TestInclude(t *testing.T) { - name, rm, err := test.TempFile(".", "foo\tIN\tA\t127.0.0.1\n") if err != nil { t.Fatalf("Unable to create tmpfile %q: %s", name, err) diff --git a/plugin/file/lookup.go b/plugin/file/lookup.go index 6eeb4c397f..7e1c6e8fb9 100644 --- a/plugin/file/lookup.go +++ b/plugin/file/lookup.go @@ -6,6 +6,7 @@ import ( "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin/file/rrutil" "github.com/coredns/coredns/plugin/file/tree" + "github.com/coredns/coredns/plugin/metadata" "github.com/coredns/coredns/request" "github.com/miekg/dns" @@ -115,8 +116,21 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) // Only one DNAME is allowed per name. We just pick the first one to synthesize from. dname := dnamerrs[0] if cname := synthesizeCNAME(state.Name(), dname.(*dns.DNAME)); cname != nil { - ctx = context.WithValue(ctx, dnsserver.LoopKey{}, loop+1) - answer, ns, extra, rcode := z.externalLookup(ctx, state, elem, []dns.RR{cname}) + var ( + answer, ns, extra []dns.RR + rcode Result + ) + + // We don't need to chase CNAME chain for synthesized CNAME + if qtype == dns.TypeCNAME { + answer = []dns.RR{cname} + ns = ap.ns(do) + extra = nil + rcode = Success + } else { + ctx = context.WithValue(ctx, dnsserver.LoopKey{}, loop+1) + answer, ns, extra, rcode = z.externalLookup(ctx, state, elem, []dns.RR{cname}) + } if do { sigs := elem.Type(dns.TypeRRSIG) @@ -137,7 +151,6 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) // If we see NS records, it means the name as been delegated, and we should return the delegation. if nsrrs := elem.Type(dns.TypeNS); nsrrs != nil { - // If the query is specifically for DS and the qname matches the delegated name, we should // return the DS in the answer section and leave the rest empty, i.e. just continue the loop // and continue searching. @@ -165,7 +178,6 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) // Found entire name. if found && shot { - if rrs := elem.Type(dns.TypeCNAME); len(rrs) > 0 && qtype != dns.TypeCNAME { ctx = context.WithValue(ctx, dnsserver.LoopKey{}, loop+1) return z.externalLookup(ctx, state, elem, rrs) @@ -194,16 +206,18 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) } return rrs, ap.ns(do), additional, Success - } // Haven't found the original name. // Found wildcard. if wildElem != nil { - auth := ap.ns(do) + // set metadata value for the wildcard record that synthesized the result + metadata.SetValueFunc(ctx, "zone/wildcard", func() string { + return wildElem.Name() + }) - if rrs := wildElem.TypeForWildcard(dns.TypeCNAME, qname); len(rrs) > 0 { + if rrs := wildElem.TypeForWildcard(dns.TypeCNAME, qname); len(rrs) > 0 && qtype != dns.TypeCNAME { ctx = context.WithValue(ctx, dnsserver.LoopKey{}, loop+1) return z.externalLookup(ctx, state, wildElem, rrs) } @@ -217,9 +231,10 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) nsec := typeFromElem(wildElem, dns.TypeNSEC, do) ret = append(ret, nsec...) } - return nil, ret, nil, Success + return nil, ret, nil, NoData } + auth := ap.ns(do) if do { // An NSEC is needed to say no longer name exists under this wildcard. if deny, found := tr.Prev(qname); found { @@ -230,7 +245,6 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) sigs := wildElem.TypeForWildcard(dns.TypeRRSIG, qname) sigs = rrutil.SubTypeSignature(sigs, qtype) rrs = append(rrs, sigs...) - } return rrs, auth, nil, Success } @@ -272,7 +286,6 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) } } } - } Out: return nil, ret, nil, rcode @@ -307,7 +320,6 @@ func (a Apex) ns(do bool) []dns.RR { // externalLookup adds signatures and tries to resolve CNAMEs that point to external names. func (z *Zone) externalLookup(ctx context.Context, state request.Request, elem *tree.Elem, rrs []dns.RR) ([]dns.RR, []dns.RR, []dns.RR, Result) { - qtype := state.QType() do := state.Do() @@ -318,11 +330,11 @@ func (z *Zone) externalLookup(ctx context.Context, state request.Request, elem * } targetName := rrs[0].(*dns.CNAME).Target - elem, _ = z.Tree.Search(targetName) + elem, _ = z.Search(targetName) if elem == nil { lookupRRs, result := z.doLookup(ctx, state, targetName, qtype) rrs = append(rrs, lookupRRs...) - return rrs, z.Apex.ns(do), nil, result + return rrs, z.ns(do), nil, result } i := 0 @@ -338,22 +350,22 @@ Redo: rrs = append(rrs, sigs...) } targetName := cname[0].(*dns.CNAME).Target - elem, _ = z.Tree.Search(targetName) + elem, _ = z.Search(targetName) if elem == nil { lookupRRs, result := z.doLookup(ctx, state, targetName, qtype) rrs = append(rrs, lookupRRs...) - return rrs, z.Apex.ns(do), nil, result + return rrs, z.ns(do), nil, result } i++ if i > 8 { - return rrs, z.Apex.ns(do), nil, Success + return rrs, z.ns(do), nil, Success } goto Redo } - targets := rrutil.CNAMEForType(elem.All(), qtype) + targets := elem.Type(qtype) if len(targets) > 0 { rrs = append(rrs, targets...) @@ -364,13 +376,13 @@ Redo: } } - return rrs, z.Apex.ns(do), nil, Success + return rrs, z.ns(do), nil, Success } func (z *Zone) doLookup(ctx context.Context, state request.Request, target string, qtype uint16) ([]dns.RR, Result) { m, e := z.Upstream.Lookup(ctx, state, target, qtype) if e != nil { - return nil, Success + return nil, ServerFailure } if m == nil { return nil, Success @@ -402,7 +414,7 @@ func (z *Zone) additionalProcessing(answer []dns.RR, do bool) (extra []dns.RR) { continue } - elem, _ := z.Tree.Search(name) + elem, _ := z.Search(name) if elem == nil { continue } diff --git a/plugin/file/lookup_test.go b/plugin/file/lookup_test.go index 71004397b0..e9aaf6df2b 100644 --- a/plugin/file/lookup_test.go +++ b/plugin/file/lookup_test.go @@ -6,7 +6,9 @@ import ( "testing" "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" "github.com/miekg/dns" ) @@ -95,6 +97,57 @@ var dnsTestCases = []test.Case{ }, Ns: miekAuth, }, + { + Qname: "asterisk.x.miek.nl.", Qtype: dns.TypeCNAME, + Answer: []dns.RR{ + test.CNAME("asterisk.x.miek.nl. 1800 IN CNAME www.miek.nl."), + }, + Ns: miekAuth, + }, + { + Qname: "a.b.x.miek.nl.", Qtype: dns.TypeCNAME, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, + { + Qname: "asterisk.y.miek.nl.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("asterisk.y.miek.nl. 1800 IN A 139.162.196.78"), + }, + Ns: miekAuth, + }, + { + Qname: "foo.dname.miek.nl.", Qtype: dns.TypeCNAME, + Answer: []dns.RR{ + test.DNAME("dname.miek.nl. 1800 IN DNAME x.miek.nl."), + test.CNAME("foo.dname.miek.nl. 1800 IN CNAME foo.x.miek.nl."), + }, + Ns: miekAuth, + }, + { + Qname: "ext-cname.miek.nl.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("ext-cname.miek.nl. 1800 IN CNAME example.com."), + }, + Rcode: dns.RcodeServerFailure, + Ns: miekAuth, + }, + { + Qname: "txt.miek.nl.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT(`txt.miek.nl. 1800 IN TXT "v=spf1 a mx ~all"`), + }, + Ns: miekAuth, + }, + { + Qname: "caa.miek.nl.", Qtype: dns.TypeCAA, + Answer: []dns.RR{ + test.CAA(`caa.miek.nl. 1800 IN CAA 0 issue letsencrypt.org`), + }, + Ns: miekAuth, + }, } const ( @@ -137,6 +190,112 @@ func TestLookupNil(t *testing.T) { fm.ServeDNS(ctx, rec, m) } +func TestLookUpNoDataResult(t *testing.T) { + zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0) + if err != nil { + t.Fatalf("Expected no error when reading zone, got %q", err) + } + + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}} + ctx := context.TODO() + var noDataTestCases = []test.Case{ + { + Qname: "a.miek.nl.", Qtype: dns.TypeMX, + }, + { + Qname: "wildcard.nodata.miek.nl.", Qtype: dns.TypeMX, + }, + } + + for _, tc := range noDataTestCases { + m := tc.Msg() + state := request.Request{W: &test.ResponseWriter{}, Req: m} + _, _, _, result := fm.Z[testzone].Lookup(ctx, state, tc.Qname) + if result != NoData { + t.Errorf("Expected result == 3 but result == %v ", result) + } + } +} + +func TestLookupFallthrough(t *testing.T) { + zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0) + if err != nil { + t.Fatalf("Expected no error when reading zone, got %q", err) + } + + type FallWithTestCases struct { + Fall fall.F + Cases []test.Case + } + var fallsWithTestCases = []FallWithTestCases{ + { + Fall: fall.Root, + Cases: []test.Case{ + { + Qname: "doesnotexist.miek.nl.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + }, + { + Qname: "x.miek.nl.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + }, + }, + }, + { + Fall: fall.F{Zones: []string{"a.miek.nl."}}, + Cases: []test.Case{ + { + Qname: "a.miek.nl.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + }, + { + Qname: "doesnotexist.miek.nl.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + }, + { + Qname: "passthrough.a.miek.nl.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + Answer: []dns.RR{}, + }, + }, + }, + { + Fall: fall.F{Zones: []string{"x.miek.nl."}}, + Cases: []test.Case{ + { + Qname: "x.miek.nl.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + }, + { + Qname: "wildcard.x.miek.nl.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + }, + }, + }, + } + + for _, fallWithTestCases := range fallsWithTestCases { + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}, Fall: fallWithTestCases.Fall} + ctx := context.TODO() + + for _, tc := range fallWithTestCases.Cases { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := fm.ServeDNS(ctx, rec, m) + if err != nil { + t.Errorf("Expected no error, got %v", err) + return + } + + if rec.Msg.Rcode != tc.Rcode { + t.Errorf("rcode is %q, expected %q", dns.RcodeToString[rec.Msg.Rcode], dns.RcodeToString[tc.Rcode]) + return + } + } + } +} + func BenchmarkFileLookup(b *testing.B) { zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0) if err != nil { @@ -157,9 +316,7 @@ func BenchmarkFileLookup(b *testing.B) { m := tc.Msg() - b.ResetTimer() - - for i := 0; i < b.N; i++ { + for b.Loop() { fm.ServeDNS(ctx, rec, m) } } @@ -191,6 +348,15 @@ a IN A 139.162.196.78 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 www IN CNAME a archive IN CNAME a +*.x IN CNAME www +b.x IN CNAME a +*.y IN A 139.162.196.78 +dname IN DNAME x srv IN SRV 10 10 8080 a.miek.nl. -mx IN MX 10 a.miek.nl.` +mx IN MX 10 a.miek.nl. + +txt IN TXT "v=spf1 a mx ~all" +caa IN CAA 0 issue letsencrypt.org +*.nodata IN A 139.162.196.79 +ext-cname IN CNAME example.com.` diff --git a/plugin/file/reload.go b/plugin/file/reload.go index 426a986b09..4db8e11d24 100644 --- a/plugin/file/reload.go +++ b/plugin/file/reload.go @@ -2,12 +2,13 @@ package file import ( "os" + "path/filepath" "time" "github.com/coredns/coredns/plugin/transfer" ) -// Reload reloads a zone when it is changed on disk. If z.NoReload is true, no reloading will be done. +// Reload reloads a zone when it is changed on disk. If z.ReloadInterval is zero, no reloading will be done. func (z *Zone) Reload(t *transfer.Transfer) error { if z.ReloadInterval == 0 { return nil @@ -19,7 +20,7 @@ func (z *Zone) Reload(t *transfer.Transfer) error { select { case <-tick.C: zFile := z.File() - reader, err := os.Open(zFile) + reader, err := os.Open(filepath.Clean(zFile)) if err != nil { log.Errorf("Failed to open zone %q in %q: %v", z.origin, zFile, err) continue @@ -41,7 +42,7 @@ func (z *Zone) Reload(t *transfer.Transfer) error { z.Tree = zone.Tree z.Unlock() - log.Infof("Successfully reloaded zone %q in %q with %d SOA serial", z.origin, zFile, z.Apex.SOA.Serial) + log.Infof("Successfully reloaded zone %q in %q with %d SOA serial", z.origin, zFile, z.SOA.Serial) if t != nil { if err := t.Notify(z.origin); err != nil { log.Warningf("Failed sending notifies: %s", err) @@ -61,8 +62,8 @@ func (z *Zone) Reload(t *transfer.Transfer) error { func (z *Zone) SOASerialIfDefined() int64 { z.RLock() defer z.RUnlock() - if z.Apex.SOA != nil { - return int64(z.Apex.SOA.Serial) + if z.SOA != nil { + return int64(z.SOA.Serial) } return -1 } diff --git a/plugin/file/reload_test.go b/plugin/file/reload_test.go index 1a8c540674..c404bc4439 100644 --- a/plugin/file/reload_test.go +++ b/plugin/file/reload_test.go @@ -2,7 +2,6 @@ package file import ( "context" - "io/ioutil" "os" "strings" "testing" @@ -56,7 +55,7 @@ func TestZoneReload(t *testing.T) { if len(rrs) != 5 { t.Fatalf("Expected 5 RRs, got %d", len(rrs)) } - if err := ioutil.WriteFile(fileName, []byte(reloadZone2Test), 0644); err != nil { + if err := os.WriteFile(fileName, []byte(reloadZone2Test), 0644); err != nil { t.Fatalf("Failed to write new zone data: %s", err) } // Could still be racy, but we need to wait a bit for the event to be seen diff --git a/plugin/file/rrutil/util.go b/plugin/file/rrutil/util.go index 63e447196a..564b82cd15 100644 --- a/plugin/file/rrutil/util.go +++ b/plugin/file/rrutil/util.go @@ -16,14 +16,3 @@ func SubTypeSignature(rrs []dns.RR, subtype uint16) []dns.RR { } return sigs } - -// CNAMEForType returns the RR that have the qtype from targets. -func CNAMEForType(rrs []dns.RR, qtype uint16) []dns.RR { - ret := []dns.RR{} - for _, target := range rrs { - if target.Header().Rrtype == qtype { - ret = append(ret, target) - } - } - return ret -} diff --git a/plugin/file/secondary.go b/plugin/file/secondary.go index 160d8e0132..9898c697f2 100644 --- a/plugin/file/secondary.go +++ b/plugin/file/secondary.go @@ -89,10 +89,10 @@ Transfer: if serial == -1 { return false, Err } - if z.Apex.SOA == nil { + if z.SOA == nil { return true, Err } - return less(z.Apex.SOA.Serial, uint32(serial)), Err + return less(z.SOA.Serial, uint32(serial)), Err } // less returns true of a is smaller than b when taking RFC 1982 serial arithmetic into account. @@ -109,15 +109,15 @@ func less(a, b uint32) bool { // will be marked expired. func (z *Zone) Update() error { // If we don't have a SOA, we don't have a zone, wait for it to appear. - for z.Apex.SOA == nil { + for z.SOA == nil { time.Sleep(1 * time.Second) } retryActive := false Restart: - refresh := time.Second * time.Duration(z.Apex.SOA.Refresh) - retry := time.Second * time.Duration(z.Apex.SOA.Retry) - expire := time.Second * time.Duration(z.Apex.SOA.Expire) + refresh := time.Second * time.Duration(z.SOA.Refresh) + retry := time.Second * time.Duration(z.SOA.Retry) + expire := time.Second * time.Duration(z.SOA.Expire) refreshTicker := time.NewTicker(refresh) retryTicker := time.NewTicker(retry) @@ -183,7 +183,6 @@ Restart: retryTicker.Stop() expireTicker.Stop() goto Restart - } } } @@ -192,7 +191,6 @@ Restart: func jitter(n int) time.Duration { r := rand.Intn(n) return time.Duration(r) * time.Millisecond - } // MaxSerialIncrement is the maximum difference between two serial numbers. If the difference between diff --git a/plugin/file/secondary_test.go b/plugin/file/secondary_test.go index 67d151e537..3e36c4b4e8 100644 --- a/plugin/file/secondary_test.go +++ b/plugin/file/secondary_test.go @@ -84,7 +84,7 @@ func TestShouldTransfer(t *testing.T) { t.Fatalf("ShouldTransfer should return true for serial: %d", soa.serial) } // Serial smaller - z.Apex.SOA = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, soa.serial-1)) + z.SOA = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, soa.serial-1)) should, err = z.shouldTransfer() if err != nil { t.Fatalf("Unable to run shouldTransfer: %v", err) @@ -93,7 +93,7 @@ func TestShouldTransfer(t *testing.T) { t.Fatalf("ShouldTransfer should return true for serial: %q", soa.serial-1) } // Serial equal - z.Apex.SOA = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, soa.serial)) + z.SOA = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, soa.serial)) should, err = z.shouldTransfer() if err != nil { t.Fatalf("Unable to run shouldTransfer: %v", err) @@ -116,7 +116,7 @@ func TestTransferIn(t *testing.T) { if err := z.TransferIn(); err != nil { t.Fatalf("Unable to run TransferIn: %v", err) } - if z.Apex.SOA.String() != fmt.Sprintf("%s 3600 IN SOA bla. bla. 250 0 0 0 0", testZone) { + if z.SOA.String() != fmt.Sprintf("%s 3600 IN SOA bla. bla. 250 0 0 0 0", testZone) { t.Fatalf("Unknown SOA transferred") } } diff --git a/plugin/file/setup.go b/plugin/file/setup.go index c5ff4c203d..eabfcc8556 100644 --- a/plugin/file/setup.go +++ b/plugin/file/setup.go @@ -1,6 +1,7 @@ package file import ( + "errors" "os" "path/filepath" "time" @@ -8,6 +9,7 @@ import ( "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/plugin/pkg/upstream" "github.com/coredns/coredns/plugin/transfer" ) @@ -15,12 +17,12 @@ import ( func init() { plugin.Register("file", setup) } func setup(c *caddy.Controller) error { - zones, err := fileParse(c) + zones, fall, err := fileParse(c) if err != nil { return plugin.Error("file", err) } - f := File{Zones: zones} + f := File{Zones: zones, Fall: fall} // get the transfer plugin, so we can send notifies and send notifies on startup as well. c.OnStartup(func() error { t := dnsserver.GetConfig(c).Handler("transfer") @@ -66,9 +68,10 @@ func setup(c *caddy.Controller) error { return nil } -func fileParse(c *caddy.Controller) (Zones, error) { +func fileParse(c *caddy.Controller) (Zones, fall.F, error) { z := make(map[string]*Zone) names := []string{} + fall := fall.F{} config := dnsserver.GetConfig(c) @@ -78,47 +81,54 @@ func fileParse(c *caddy.Controller) (Zones, error) { for c.Next() { // file db.file [zones...] if !c.NextArg() { - return Zones{}, c.ArgErr() + return Zones{}, fall, c.ArgErr() } fileName := c.Val() - origins := make([]string, len(c.ServerBlockKeys)) - copy(origins, c.ServerBlockKeys) - args := c.RemainingArgs() - if len(args) > 0 { - origins = args - } - + origins := plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) if !filepath.IsAbs(fileName) && config.Root != "" { fileName = filepath.Join(config.Root, fileName) } - reader, err := os.Open(fileName) + reader, err := os.Open(filepath.Clean(fileName)) if err != nil { openErr = err } - for i := range origins { - origins[i] = plugin.Host(origins[i]).Normalize() - z[origins[i]] = NewZone(origins[i], fileName) - if openErr == nil { - reader.Seek(0, 0) - zone, err := Parse(reader, origins[i], fileName, 0) - if err == nil { + err = func() error { + defer reader.Close() + + for i := range origins { + z[origins[i]] = NewZone(origins[i], fileName) + if openErr == nil { + reader.Seek(0, 0) + zone, err := Parse(reader, origins[i], fileName, 0) + if err != nil { + return err + } z[origins[i]] = zone - } else { - return Zones{}, err } + names = append(names, origins[i]) } - names = append(names, origins[i]) + return nil + }() + + if err != nil { + return Zones{}, fall, err } for c.NextBlock() { switch c.Val() { + case "fallthrough": + fall.SetZonesFromArgs(c.RemainingArgs()) case "reload": - d, err := time.ParseDuration(c.RemainingArgs()[0]) + t := c.RemainingArgs() + if len(t) < 1 { + return Zones{}, fall, errors.New("reload duration value is expected") + } + d, err := time.ParseDuration(t[0]) if err != nil { - return Zones{}, plugin.Error("file", err) + return Zones{}, fall, plugin.Error("file", err) } reload = d case "upstream": @@ -126,23 +136,22 @@ func fileParse(c *caddy.Controller) (Zones, error) { c.RemainingArgs() default: - return Zones{}, c.Errf("unknown property '%s'", c.Val()) + return Zones{}, fall, c.Errf("unknown property '%s'", c.Val()) } } - } - for origin := range z { - z[origin].ReloadInterval = reload - z[origin].Upstream = upstream.New() + for i := range origins { + z[origins[i]].ReloadInterval = reload + z[origins[i]].Upstream = upstream.New() + } } if openErr != nil { if reload == 0 { // reload hasn't been set make this a fatal error - return Zones{}, plugin.Error("file", openErr) + return Zones{}, fall, plugin.Error("file", openErr) } log.Warningf("Failed to open %q: trying again in %s", openErr, reload) - } - return Zones{Z: z, Names: names}, nil + return Zones{Z: z, Names: names}, fall, nil } diff --git a/plugin/file/setup_test.go b/plugin/file/setup_test.go index 1d3b8dc885..d7c1c58871 100644 --- a/plugin/file/setup_test.go +++ b/plugin/file/setup_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/plugin/test" ) @@ -22,24 +23,44 @@ func TestFileParse(t *testing.T) { defer rm() tests := []struct { - inputFileRules string - shouldErr bool - expectedZones Zones + inputFileRules string + shouldErr bool + expectedZones Zones + expectedFallthrough fall.F }{ { `file ` + zoneFileName1 + ` miek.nl.`, false, Zones{Names: []string{"miek.nl."}}, + fall.Zero, }, { `file ` + zoneFileName2 + ` dnssex.nl.`, false, Zones{Names: []string{"dnssex.nl."}}, + fall.Zero, }, { `file ` + zoneFileName2 + ` 10.0.0.0/8`, false, Zones{Names: []string{"10.in-addr.arpa."}}, + fall.Zero, + }, + { + `file ` + zoneFileName2 + ` example.org. { + fallthrough + }`, + false, + Zones{Names: []string{"example.org."}}, + fall.Root, + }, + { + `file ` + zoneFileName2 + ` example.org. { + fallthrough www.example.org + }`, + false, + Zones{Names: []string{"example.org."}}, + fall.F{Zones: []string{"www.example.org."}}, }, // errors. { @@ -48,11 +69,13 @@ func TestFileParse(t *testing.T) { }`, true, Zones{}, + fall.Zero, }, { `file`, true, Zones{}, + fall.Zero, }, { `file ` + zoneFileName1 + ` example.net. { @@ -60,6 +83,7 @@ func TestFileParse(t *testing.T) { }`, true, Zones{}, + fall.Zero, }, { `file ` + zoneFileName1 + ` example.net. { @@ -67,12 +91,13 @@ func TestFileParse(t *testing.T) { }`, true, Zones{}, + fall.Zero, }, } for i, test := range tests { c := caddy.NewTestController("dns", test.inputFileRules) - actualZones, err := fileParse(c) + actualZones, actualFallthrough, err := fileParse(c) if err == nil && test.shouldErr { t.Fatalf("Test %d expected errors, but got no error", i) @@ -87,6 +112,9 @@ func TestFileParse(t *testing.T) { t.Fatalf("Test %d expected %v for %d th zone, got %v", i, name, j, actualZones.Names[j]) } } + if !actualFallthrough.Equal(test.expectedFallthrough) { + t.Errorf("Test %d expected fallthrough of %v, got %v", i, test.expectedFallthrough, actualFallthrough) + } } } } @@ -116,7 +144,7 @@ func TestParseReload(t *testing.T) { for i, test := range tests { c := caddy.NewTestController("dns", test.input) - z, _ := fileParse(c) + z, _, _ := fileParse(c) if x := z.Z["example.org."].ReloadInterval; x != test.reload { t.Errorf("Test %d expected reload to be %s, but got %s", i, test.reload, x) } diff --git a/plugin/file/tree/elem.go b/plugin/file/tree/elem.go index c1909649d6..195e35a3e6 100644 --- a/plugin/file/tree/elem.go +++ b/plugin/file/tree/elem.go @@ -12,6 +12,8 @@ type Elem struct { func newElem(rr dns.RR) *Elem { e := Elem{m: make(map[uint16][]dns.RR)} e.m[rr.Header().Rrtype] = []dns.RR{rr} + // Eagerly set the cached owner name to avoid racy lazy writes later. + e.name = rr.Header().Name return &e } @@ -56,12 +58,12 @@ func (e *Elem) All() []dns.RR { // Name returns the name for this node. func (e *Elem) Name() string { + // Read-only: name is eagerly set in newElem and should not be mutated here. if e.name != "" { return e.name } for _, rrs := range e.m { - e.name = rrs[0].Header().Name - return e.name + return rrs[0].Header().Name } return "" } diff --git a/plugin/file/tree/elem_test.go b/plugin/file/tree/elem_test.go new file mode 100644 index 0000000000..d1956f5d0b --- /dev/null +++ b/plugin/file/tree/elem_test.go @@ -0,0 +1,41 @@ +package tree + +import ( + "testing" + + "github.com/miekg/dns" +) + +// Test that Name() falls back to reading from the stored RRs when the cached name is empty. +func TestElemName_FallbackWhenCachedEmpty(t *testing.T) { + rr, err := dns.NewRR("a.example. 3600 IN A 1.2.3.4") + if err != nil { + t.Fatalf("failed to create RR: %v", err) + } + + // Build via newElem to ensure m is populated + e := newElem(rr) + got := e.Name() + want := "a.example." + if got != want { + t.Fatalf("unexpected name; want %q, got %q", want, got) + } + + // clear the cached name + e.name = "" + + got = e.Name() + want = "a.example." + if got != want { + t.Fatalf("unexpected name; want %q, got %q", want, got) + } + + // clear the map + e.m = make(map[uint16][]dns.RR, 0) + + got = e.Name() + want = "" + if got != want { + t.Fatalf("unexpected name after clearing RR map; want %q, got %q", want, got) + } +} diff --git a/plugin/file/tree/less.go b/plugin/file/tree/less.go index 7421cf08d4..7793009018 100644 --- a/plugin/file/tree/less.go +++ b/plugin/file/tree/less.go @@ -2,6 +2,7 @@ package tree import ( "bytes" + "strings" "github.com/miekg/dns" ) @@ -27,8 +28,8 @@ func less(a, b string) int { // sadly this []byte will allocate... TODO(miek): check if this is needed // for a name, otherwise compare the strings. - ab := []byte(a[ai:aj]) - bb := []byte(b[bi:bj]) + ab := []byte(strings.ToLower(a[ai:aj])) + bb := []byte(strings.ToLower(b[bi:bj])) doDDD(ab) doDDD(bb) diff --git a/plugin/file/tree/less_test.go b/plugin/file/tree/less_test.go index dfd702c555..a1af23a389 100644 --- a/plugin/file/tree/less_test.go +++ b/plugin/file/tree/less_test.go @@ -3,7 +3,10 @@ package tree import ( "sort" "strings" + "sync" "testing" + + "github.com/miekg/dns" ) type set []string @@ -63,7 +66,7 @@ Tests: } sort.Sort(set(test.in)) - for i := 0; i < len(test.in); i++ { + for i := range len(test.in) { if test.in[i] != test.out[i] { t.Errorf("Test %d: expected %s, got %s", j, test.out[i], test.in[i]) n := "" @@ -75,7 +78,44 @@ Tests: } continue Tests } - } } } + +func TestLess_EmptyVsName(t *testing.T) { + if d := less("", "a."); d >= 0 { + t.Fatalf("expected < 0, got %d", d) + } + if d := less("a.", ""); d <= 0 { + t.Fatalf("expected > 0, got %d", d) + } +} + +func TestLess_EmptyVsEmpty(t *testing.T) { + if d := less("", ""); d != 0 { + t.Fatalf("expected 0, got %d", d) + } +} + +// Test that concurrent calls to Less (which calls Elem.Name) do not race or panic. +// See issue #7561 for reference. +func TestLess_ConcurrentNameAccess(t *testing.T) { + rr, err := dns.NewRR("a.example. 3600 IN A 1.2.3.4") + if err != nil { + t.Fatalf("failed to create RR: %v", err) + } + e := newElem(rr) + + const n = 200 + var wg sync.WaitGroup + wg.Add(n) + for range n { + go func() { + defer wg.Done() + // Compare the same name repeatedly; previously this could race due to lazy Name() writes. + _ = Less(e, "a.example.") + _ = e.Name() + }() + } + wg.Wait() +} diff --git a/plugin/file/tree/print.go b/plugin/file/tree/print.go index bd86ef690b..b2df70e1bd 100644 --- a/plugin/file/tree/print.go +++ b/plugin/file/tree/print.go @@ -29,9 +29,9 @@ func (n *Node) print() { } if nodesInCurrentLevel == 0 { fmt.Println() + nodesInCurrentLevel = nodesInNextLevel + nodesInNextLevel = 0 } - nodesInCurrentLevel = nodesInNextLevel - nodesInNextLevel = 0 } fmt.Println() } diff --git a/plugin/file/tree/print_test.go b/plugin/file/tree/print_test.go new file mode 100644 index 0000000000..8c6c2b3318 --- /dev/null +++ b/plugin/file/tree/print_test.go @@ -0,0 +1,100 @@ +package tree + +import ( + "net" + "os" + "strings" + "testing" + + "github.com/miekg/dns" +) + +func TestPrint(t *testing.T) { + rr1 := dns.A{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn("server1.example.com"), + Rrtype: 1, + Class: 1, + Ttl: 3600, + Rdlength: 0, + }, + A: net.IPv4(10, 0, 1, 1), + } + rr2 := dns.A{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn("server2.example.com"), + Rrtype: 1, + Class: 1, + Ttl: 3600, + Rdlength: 0, + }, + A: net.IPv4(10, 0, 1, 2), + } + rr3 := dns.A{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn("server3.example.com"), + Rrtype: 1, + Class: 1, + Ttl: 3600, + Rdlength: 0, + }, + A: net.IPv4(10, 0, 1, 3), + } + rr4 := dns.A{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn("server4.example.com"), + Rrtype: 1, + Class: 1, + Ttl: 3600, + Rdlength: 0, + }, + A: net.IPv4(10, 0, 1, 4), + } + tree := Tree{ + Root: nil, + Count: 0, + } + tree.Insert(&rr1) + tree.Insert(&rr2) + tree.Insert(&rr3) + tree.Insert(&rr4) + + /** + build a LLRB tree, the height of the tree is 3, look like: + + server2.example.com. + / \ + server1.example.com. server4.example.com. + / + server3.example.com. + + */ + + f, err := os.CreateTemp(t.TempDir(), "print_test_tmp") + if err != nil { + t.Error(err) + } + defer os.Remove(f.Name()) + //Redirect the printed results to a tmp file for later comparison + os.Stdout = f + + tree.Print() + /** + server2.example.com. + server1.example.com. server4.example.com. + server3.example.com. + */ + + buf := make([]byte, 256) + f.Seek(0, 0) + _, err = f.Read(buf) + if err != nil { + f.Close() + t.Error(err) + } + height := strings.Count(string(buf), ". \n") + //Compare the height of the print with the actual height of the tree + if height != 3 { + t.Fatal("The number of rows is inconsistent with the actual number of rows in the tree itself.") + } +} diff --git a/plugin/file/tree/tree.go b/plugin/file/tree/tree.go index a6caafe163..e69befabff 100644 --- a/plugin/file/tree/tree.go +++ b/plugin/file/tree/tree.go @@ -213,7 +213,7 @@ func (n *Node) insert(rr dns.RR) (root *Node, d int) { root = n - return + return root, d } // DeleteMin deletes the node with the minimum value in the tree. diff --git a/plugin/file/wildcard.go b/plugin/file/wildcard.go index 9526cb53f7..7e8e806a1f 100644 --- a/plugin/file/wildcard.go +++ b/plugin/file/wildcard.go @@ -2,7 +2,7 @@ package file import "github.com/miekg/dns" -// replaceWithWildcard replaces the left most label with '*'. +// replaceWithAsteriskLabel replaces the left most label with '*'. func replaceWithAsteriskLabel(qname string) (wildcard string) { i, shot := dns.NextLabel(qname, 0) if shot { diff --git a/plugin/file/wildcard_test.go b/plugin/file/wildcard_test.go index 894a088c13..fc6ad120c2 100644 --- a/plugin/file/wildcard_test.go +++ b/plugin/file/wildcard_test.go @@ -185,12 +185,12 @@ var apexWildcardTestCases = []test.Case{ { Qname: "foo.example.org.", Qtype: dns.TypeA, Answer: []dns.RR{test.A(`foo.example.org. 3600 IN A 127.0.0.54`)}, - Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)}, + Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)}, }, { Qname: "bar.example.org.", Qtype: dns.TypeA, Answer: []dns.RR{test.A(`bar.example.org. 3600 IN A 127.0.0.53`)}, - Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)}, + Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)}, }, } @@ -225,17 +225,17 @@ var multiWildcardTestCases = []test.Case{ { Qname: "foo.example.org.", Qtype: dns.TypeA, Answer: []dns.RR{test.A(`foo.example.org. 3600 IN A 127.0.0.54`)}, - Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)}, + Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)}, }, { Qname: "bar.example.org.", Qtype: dns.TypeA, Answer: []dns.RR{test.A(`bar.example.org. 3600 IN A 127.0.0.53`)}, - Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)}, + Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)}, }, { Qname: "bar.intern.example.org.", Qtype: dns.TypeA, Answer: []dns.RR{test.A(`bar.intern.example.org. 3600 IN A 127.0.1.52`)}, - Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)}, + Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)}, }, } diff --git a/plugin/file/xfr.go b/plugin/file/xfr.go index 28c3a3a9db..eab880c33a 100644 --- a/plugin/file/xfr.go +++ b/plugin/file/xfr.go @@ -9,7 +9,7 @@ import ( // Transfer implements the transfer.Transfer interface. func (f File) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) { - z, ok := f.Zones.Z[zone] + z, ok := f.Z[zone] if !ok || z == nil { return nil, transfer.ErrNotAuthoritative } diff --git a/plugin/file/zone.go b/plugin/file/zone.go index aa5f3cac08..aeb2a593e2 100644 --- a/plugin/file/zone.go +++ b/plugin/file/zone.go @@ -73,21 +73,24 @@ func (z *Zone) CopyWithoutApex() *Zone { // Insert inserts r into z. func (z *Zone) Insert(r dns.RR) error { - r.Header().Name = strings.ToLower(r.Header().Name) + // r.Header().Name = strings.ToLower(r.Header().Name) + if r.Header().Rrtype != dns.TypeSRV { + r.Header().Name = strings.ToLower(r.Header().Name) + } switch h := r.Header().Rrtype; h { case dns.TypeNS: r.(*dns.NS).Ns = strings.ToLower(r.(*dns.NS).Ns) if r.Header().Name == z.origin { - z.Apex.NS = append(z.Apex.NS, r) + z.NS = append(z.NS, r) return nil } case dns.TypeSOA: r.(*dns.SOA).Ns = strings.ToLower(r.(*dns.SOA).Ns) r.(*dns.SOA).Mbox = strings.ToLower(r.(*dns.SOA).Mbox) - z.Apex.SOA = r.(*dns.SOA) + z.SOA = r.(*dns.SOA) return nil case dns.TypeNSEC3, dns.TypeNSEC3PARAM: return fmt.Errorf("NSEC3 zone is not supported, dropping RR: %s for zone: %s", r.Header().Name, z.origin) @@ -95,11 +98,11 @@ func (z *Zone) Insert(r dns.RR) error { x := r.(*dns.RRSIG) switch x.TypeCovered { case dns.TypeSOA: - z.Apex.SIGSOA = append(z.Apex.SIGSOA, x) + z.SIGSOA = append(z.SIGSOA, x) return nil case dns.TypeNS: if r.Header().Name == z.origin { - z.Apex.SIGNS = append(z.Apex.SIGNS, x) + z.SIGNS = append(z.SIGNS, x) return nil } } @@ -108,7 +111,7 @@ func (z *Zone) Insert(r dns.RR) error { case dns.TypeMX: r.(*dns.MX).Mx = strings.ToLower(r.(*dns.MX).Mx) case dns.TypeSRV: - r.(*dns.SRV).Target = strings.ToLower(r.(*dns.SRV).Target) + // r.(*dns.SRV).Target = strings.ToLower(r.(*dns.SRV).Target) } z.Tree.Insert(r) @@ -133,20 +136,20 @@ func (z *Zone) SetFile(path string) { func (z *Zone) ApexIfDefined() ([]dns.RR, error) { z.RLock() defer z.RUnlock() - if z.Apex.SOA == nil { + if z.SOA == nil { return nil, fmt.Errorf("no SOA") } - rrs := []dns.RR{z.Apex.SOA} + rrs := []dns.RR{z.SOA} - if len(z.Apex.SIGSOA) > 0 { - rrs = append(rrs, z.Apex.SIGSOA...) + if len(z.SIGSOA) > 0 { + rrs = append(rrs, z.SIGSOA...) } - if len(z.Apex.NS) > 0 { - rrs = append(rrs, z.Apex.NS...) + if len(z.NS) > 0 { + rrs = append(rrs, z.NS...) } - if len(z.Apex.SIGNS) > 0 { - rrs = append(rrs, z.Apex.SIGNS...) + if len(z.SIGNS) > 0 { + rrs = append(rrs, z.SIGNS...) } return rrs, nil diff --git a/plugin/file/zone_test.go b/plugin/file/zone_test.go index aa42fd8705..f81a871ff3 100644 --- a/plugin/file/zone_test.go +++ b/plugin/file/zone_test.go @@ -1,6 +1,12 @@ package file -import "testing" +import ( + "testing" + + "github.com/coredns/coredns/plugin/file/tree" + + "github.com/miekg/dns" +) func TestNameFromRight(t *testing.T) { z := NewZone("example.org.", "stdin") @@ -28,3 +34,41 @@ func TestNameFromRight(t *testing.T) { } } } + +func TestInsertPreservesSRVCase(t *testing.T) { + z := NewZone("home.arpa.", "stdin") + + // SRV with mixed case and space-escaped instance name + srv, err := dns.NewRR(`Home\032Media._smb._tcp.home.arpa. 5 IN SRV 0 0 445 samba.home.arpa.`) + if err != nil { + t.Fatalf("Failed to parse SRV RR: %v", err) + } + + if err := z.Insert(srv); err != nil { + t.Fatalf("Insert failed: %v", err) + } + + found := false + err = z.Walk(func(elem *tree.Elem, rrsets map[uint16][]dns.RR) error { + for _, rrs := range rrsets { + for _, rr := range rrs { + if srvRR, ok := rr.(*dns.SRV); ok { + if srvRR.Hdr.Name == "Home\\032Media._smb._tcp.home.arpa." { + found = true + if srvRR.Target != "samba.home.arpa." { + t.Errorf("Expected SRV target to be 'samba.home.arpa.', got %q", srvRR.Target) + } + } + } + } + } + return nil + }) + if err != nil { + t.Fatalf("Tree walk failed: %v", err) + } + + if !found { + t.Errorf("SRV record with original case not found in tree") + } +} diff --git a/plugin/forward/README.md b/plugin/forward/README.md index 7daecf4284..692ad8f390 100644 --- a/plugin/forward/README.md +++ b/plugin/forward/README.md @@ -19,8 +19,6 @@ is taken as a healthy upstream. The health check uses the same protocol as speci When *all* upstreams are down it assumes health checking as a mechanism has failed and will try to connect to a random upstream (which may or may not work). -This plugin can only be used once per Server Block. - ## Syntax In its most basic form, a simple forwarder uses this syntax: @@ -29,7 +27,8 @@ In its most basic form, a simple forwarder uses this syntax: forward FROM TO... ~~~ -* **FROM** is the base domain to match for the request to be forwarded. +* **FROM** is the base domain to match for the request to be forwarded. Domains using CIDR notation + that expand to multiple reverse zones are not fully supported; only the first expanded zone is used. * **TO...** are the destination endpoints to forward to. The **TO** syntax allows you to specify a protocol, `tls://9.9.9.9` or `dns://` (or no protocol) for plain DNS. The number of upstreams is limited to 15. @@ -49,8 +48,11 @@ forward FROM TO... { tls CERT KEY CA tls_servername NAME policy random|round_robin|sequential - health_check DURATION [no_rec] + health_check DURATION [no_rec] [domain FQDN] max_concurrent MAX + next RCODE_1 [RCODE_2] [RCODE_3...] + failfast_all_unhealthy_upstreams + failover RCODE_1 [RCODE_2] [RCODE_3...] } ~~~ @@ -78,7 +80,9 @@ forward FROM TO... { * `tls_servername` **NAME** allows you to set a server name in the TLS configuration; for instance 9.9.9.9 needs this to be set to `dns.quad9.net`. Multiple upstreams are still allowed in this scenario, but they have to use the same `tls_servername`. E.g. mixing 9.9.9.9 (QuadDNS) with 1.1.1.1 - (Cloudflare) will not work. + (Cloudflare) will not work. Using TLS forwarding but not setting `tls_servername` results in anyone + being able to man-in-the-middle your connection to the DNS server you are forwarding to. Because of this, + it is strongly recommended to set this value when using TLS forwarding. * `policy` specifies the policy to use for selecting upstream servers. The default is `random`. * `random` is a policy that implements random upstream selection. * `round_robin` is a policy that selects hosts based on round robin ordering. @@ -87,18 +91,23 @@ forward FROM TO... { * `` - use a different duration for health checking, the default duration is 0.5s. * `no_rec` - optional argument that sets the RecursionDesired-flag of the dns-query used in health checking to `false`. The flag is default `true`. + * `domain FQDN` - set the domain name used for health checks to **FQDN**. + If not configured, the domain name used for health checks is `.`. * `max_concurrent` **MAX** will limit the number of concurrent queries to **MAX**. Any new query that would raise the number of concurrent queries above the **MAX** will result in a REFUSED response. This response does not count as a health failure. When choosing a value for **MAX**, pick a number at least greater than the expected *upstream query rate* * *latency* of the upstream servers. As an upper bound for **MAX**, consider that each concurrent query will use about 2kb of memory. +* `next` If the `RCODE` (i.e. `NXDOMAIN`) is returned by the remote then execute the next plugin. If no next plugin is defined, or the next plugin is not a `forward` plugin, this setting is ignored +* `failfast_all_unhealthy_upstreams` - determines the handling of requests when all upstream servers are unhealthy and unresponsive to health checks. Enabling this option will immediately return SERVFAIL responses for all requests. By default, requests are sent to a random upstream. +* `failover` - By default when a DNS lookup fails to return a DNS response (e.g. timeout), _forward_ will attempt a lookup on the next upstream server. The `failover` option will make _forward_ do the same for any response with a response code matching an `RCODE` ( e.g. `SERVFAIL`、`REFUSED`). If all upstreams have been tried, the response from the last attempt is returned. Also note the TLS config is "global" for the whole forwarding proxy if you need a different -`tls-name` for different upstreams you're out of luck. +`tls_servername` for different upstreams you're out of luck. On each endpoint, the timeouts for communication are set as follows: -* The dial timeout by default is 30s, and can decrease automatically down to 100ms based on early results. +* The dial timeout by default is 30s, and can decrease automatically down to 1s based on early results. * The read timeout is static at 2s. ## Metadata @@ -112,20 +121,28 @@ plugin is also enabled: If monitoring is enabled (via the *prometheus* plugin) then the following metric are exported: -* `coredns_forward_requests_total{to}` - query count per upstream. -* `coredns_forward_responses_total{to}` - Counter of responses received per upstream. -* `coredns_forward_request_duration_seconds{to, rcode, type}` - duration per upstream, RCODE, type -* `coredns_forward_responses_total{to, rcode}` - count of RCODEs per upstream. -* `coredns_forward_healthcheck_failures_total{to}` - number of failed health checks per upstream. -* `coredns_forward_healthcheck_broken_total{}` - counter of when all upstreams are unhealthy, +* `coredns_forward_healthcheck_broken_total{}` - count of when all upstreams are unhealthy, and we are randomly (this always uses the `random` policy) spraying to an upstream. -* `coredns_forward_max_concurrent_rejects_total{}` - counter of the number of queries rejected because the +* `coredns_forward_max_concurrent_rejects_total{}` - count of queries rejected because the number of concurrent queries were at maximum. -* `coredns_forward_conn_cache_hits_total{to, proto}` - counter of connection cache hits per upstream and protocol. -* `coredns_forward_conn_cache_misses_total{to, proto}` - counter of connection cache misses per upstream and protocol. +* `coredns_proxy_request_duration_seconds{proxy_name="forward", to, rcode}` - histogram per upstream, RCODE +* `coredns_proxy_healthcheck_failures_total{proxy_name="forward", to, rcode}`- count of failed health checks per upstream. +* `coredns_proxy_conn_cache_hits_total{proxy_name="forward", to, proto}`- count of connection cache hits per upstream and protocol. +* `coredns_proxy_conn_cache_misses_total{proxy_name="forward", to, proto}` - count of connection cache misses per upstream and protocol. + Where `to` is one of the upstream servers (**TO** from the config), `rcode` is the returned RCODE from the upstream, `proto` is the transport protocol like `udp`, `tcp`, `tcp-tls`. +The following metrics have recently been deprecated: +* `coredns_forward_healthcheck_failures_total{to, rcode}` + * Can be replaced with `coredns_proxy_healthcheck_failures_total{proxy_name="forward", to, rcode}` +* `coredns_forward_requests_total{to}` + * Can be replaced with `sum(coredns_proxy_request_duration_seconds_count{proxy_name="forward", to})` +* `coredns_forward_responses_total{to, rcode}` + * Can be replaced with `coredns_proxy_request_duration_seconds_count{proxy_name="forward", to, rcode}` +* `coredns_forward_request_duration_seconds{to, rcode}` + * Can be replaced with `coredns_proxy_request_duration_seconds{proxy_name="forward", to, rcode}` + ## Examples Proxy all requests within `example.org.` to a nameserver running on a different port: @@ -136,6 +153,40 @@ example.org { } ~~~ +Send all requests within `lab.example.local.` to `10.20.0.1`, all requests within `example.local.` (and not in +`lab.example.local.`) to `10.0.0.1`, all others requests to the servers defined in `/etc/resolv.conf`, and +caches results. Note that a CoreDNS server configured with multiple _forward_ plugins in a server block will evaluate those +forward plugins in the order they are listed when serving a request. Therefore, subdomains should be +placed before parent domains otherwise subdomain requests will be forwarded to the parent domain's upstream. +Accordingly, in this example `lab.example.local` is before `example.local`, and `example.local` is before `.`. + +~~~ corefile +. { + cache + forward lab.example.local 10.20.0.1 + forward example.local 10.0.0.1 + forward . /etc/resolv.conf +} +~~~ + +The example above is almost equivalent to the following example, except that example below defines three separate plugin +chains (and thus 3 separate instances of _cache_). + +~~~ corefile +lab.example.local { + cache + forward . 10.20.0.1 +} +example.local { + cache + forward . 10.0.0.1 +} +. { + cache + forward . /etc/resolv.conf +} +~~~ + Load balance all requests between three resolvers, one of which has a IPv6 address. ~~~ corefile @@ -179,6 +230,18 @@ service with health checks. } ~~~ +Or configure other domain name for health check requests + +~~~ corefile +. { + forward . tls://9.9.9.9 { + tls_servername dns.quad9.net + health_check 5s domain example.org + } + cache 30 +} +~~~ + Or with multiple upstreams from the same provider ~~~ corefile @@ -199,18 +262,44 @@ Or when you have multiple DoT upstreams with different `tls_servername`s, you ca } .:5301 { - forward . 8.8.8.8 8.8.4.4 { + forward . tls://8.8.8.8 tls://8.8.4.4 { tls_servername dns.google } } .:5302 { - forward . 1.1.1.1 1.0.0.1 { + forward . tls://1.1.1.1 tls://1.0.0.1 { tls_servername cloudflare-dns.com } } ~~~ +The following would try 1.2.3.4 first. If the response is `NXDOMAIN`, try 5.6.7.8. If the response from 5.6.7.8 is `NXDOMAIN`, try 9.0.1.2. + +~~~ corefile +. { + forward . 1.2.3.4 { + next NXDOMAIN + } + forward . 5.6.7.8 { + next NXDOMAIN + } + forward . 9.0.1.2 { + } +} +~~~ + +In the following example, if the response from `1.2.3.4` is `SERVFAIL` or `REFUSED`, it will try `5.6.7.8`. If the response from `5.6.7.8` is `SERVFAIL ` or `REFUSED`, it will try `9.0.1.2`. + +~~~ corefile +. { + forward . 1.2.3.4 5.6.7.8 9.0.1.2 { + policy sequential + failover SERVFAIL REFUSED + } +} +~~~ + ## See Also [RFC 7858](https://tools.ietf.org/html/rfc7858) for DNS over TLS. diff --git a/plugin/forward/connect.go b/plugin/forward/connect.go deleted file mode 100644 index 7e34837e1c..0000000000 --- a/plugin/forward/connect.go +++ /dev/null @@ -1,142 +0,0 @@ -// Package forward implements a forwarding proxy. It caches an upstream net.Conn for some time, so if the same -// client returns the upstream's Conn will be precached. Depending on how you benchmark this looks to be -// 50% faster than just opening a new connection for every client. It works with UDP and TCP and uses -// inband healthchecking. -package forward - -import ( - "context" - "io" - "strconv" - "sync/atomic" - "time" - - "github.com/coredns/coredns/plugin/pkg/dnsutil" - "github.com/coredns/coredns/request" - - "github.com/miekg/dns" -) - -// limitTimeout is a utility function to auto-tune timeout values -// average observed time is moved towards the last observed delay moderated by a weight -// next timeout to use will be the double of the computed average, limited by min and max frame. -func limitTimeout(currentAvg *int64, minValue time.Duration, maxValue time.Duration) time.Duration { - rt := time.Duration(atomic.LoadInt64(currentAvg)) - if rt < minValue { - return minValue - } - if rt < maxValue/2 { - return 2 * rt - } - return maxValue -} - -func averageTimeout(currentAvg *int64, observedDuration time.Duration, weight int64) { - dt := time.Duration(atomic.LoadInt64(currentAvg)) - atomic.AddInt64(currentAvg, int64(observedDuration-dt)/weight) -} - -func (t *Transport) dialTimeout() time.Duration { - return limitTimeout(&t.avgDialTime, minDialTimeout, maxDialTimeout) -} - -func (t *Transport) updateDialTimeout(newDialTime time.Duration) { - averageTimeout(&t.avgDialTime, newDialTime, cumulativeAvgWeight) -} - -// Dial dials the address configured in transport, potentially reusing a connection or creating a new one. -func (t *Transport) Dial(proto string) (*persistConn, bool, error) { - // If tls has been configured; use it. - if t.tlsConfig != nil { - proto = "tcp-tls" - } - - t.dial <- proto - pc := <-t.ret - - if pc != nil { - ConnCacheHitsCount.WithLabelValues(t.addr, proto).Add(1) - return pc, true, nil - } - ConnCacheMissesCount.WithLabelValues(t.addr, proto).Add(1) - - reqTime := time.Now() - timeout := t.dialTimeout() - if proto == "tcp-tls" { - conn, err := dns.DialTimeoutWithTLS("tcp", t.addr, t.tlsConfig, timeout) - t.updateDialTimeout(time.Since(reqTime)) - return &persistConn{c: conn}, false, err - } - conn, err := dns.DialTimeout(proto, t.addr, timeout) - t.updateDialTimeout(time.Since(reqTime)) - return &persistConn{c: conn}, false, err -} - -// Connect selects an upstream, sends the request and waits for a response. -func (p *Proxy) Connect(ctx context.Context, state request.Request, opts options) (*dns.Msg, error) { - start := time.Now() - - proto := "" - switch { - case opts.forceTCP: // TCP flag has precedence over UDP flag - proto = "tcp" - case opts.preferUDP: - proto = "udp" - default: - proto = state.Proto() - } - - pc, cached, err := p.transport.Dial(proto) - if err != nil { - return nil, err - } - - // Set buffer size correctly for this client. - pc.c.UDPSize = uint16(state.Size()) - if pc.c.UDPSize < 512 { - pc.c.UDPSize = 512 - } - - pc.c.SetWriteDeadline(time.Now().Add(maxTimeout)) - if err := pc.c.WriteMsg(state.Req); err != nil { - pc.c.Close() // not giving it back - if err == io.EOF && cached { - return nil, ErrCachedClosed - } - return nil, err - } - - var ret *dns.Msg - pc.c.SetReadDeadline(time.Now().Add(readTimeout)) - for { - ret, err = pc.c.ReadMsg() - if err != nil { - pc.c.Close() // not giving it back - if err == io.EOF && cached { - return nil, ErrCachedClosed - } - return ret, err - } - // drop out-of-order responses - if state.Req.Id == ret.Id { - break - } - } - - p.transport.Yield(pc) - - rc, ok := dns.RcodeToString[ret.Rcode] - if !ok { - rc = strconv.Itoa(ret.Rcode) - } - - qtype := dnsutil.QTypeMonitorLabel(state.QType()) - - RequestCount.WithLabelValues(p.addr).Add(1) - RcodeCount.WithLabelValues(rc, p.addr).Add(1) - RequestDuration.WithLabelValues(p.addr, rc, qtype).Observe(time.Since(start).Seconds()) - - return ret, nil -} - -const cumulativeAvgWeight = 4 diff --git a/plugin/forward/dnstap.go b/plugin/forward/dnstap.go index 4e06ac1ff9..293dc10cf3 100644 --- a/plugin/forward/dnstap.go +++ b/plugin/forward/dnstap.go @@ -1,11 +1,13 @@ package forward import ( + "context" "net" - "strconv" + "net/netip" "time" "github.com/coredns/coredns/plugin/dnstap/msg" + "github.com/coredns/coredns/plugin/pkg/proxy" "github.com/coredns/coredns/request" tap "github.com/dnstap/golang-dnstap" @@ -13,51 +15,55 @@ import ( ) // toDnstap will send the forward and received message to the dnstap plugin. -func toDnstap(f *Forward, host string, state request.Request, opts options, reply *dns.Msg, start time.Time) { - // Query - q := new(tap.Message) - msg.SetQueryTime(q, start) - h, p, _ := net.SplitHostPort(host) // this is preparsed and can't err here - port, _ := strconv.ParseUint(p, 10, 32) // same here - ip := net.ParseIP(h) - - var ta net.Addr = &net.UDPAddr{IP: ip, Port: int(port)} +func toDnstap(ctx context.Context, f *Forward, host string, state request.Request, opts proxy.Options, reply *dns.Msg, start time.Time) { + ap, _ := netip.ParseAddrPort(host) // this is preparsed and can't err here + ip := net.IP(ap.Addr().AsSlice()) + port := int(ap.Port()) + + var ta net.Addr = &net.UDPAddr{ + IP: ip, + Port: port, + } t := state.Proto() switch { - case opts.forceTCP: + case opts.ForceTCP: t = "tcp" - case opts.preferUDP: + case opts.PreferUDP: t = "udp" } if t == "tcp" { - ta = &net.TCPAddr{IP: ip, Port: int(port)} + ta = &net.TCPAddr{IP: ip, Port: port} } - // Forwarder dnstap messages are from the perspective of the downstream server - // (upstream is the forward server) - msg.SetQueryAddress(q, state.W.RemoteAddr()) - msg.SetResponseAddress(q, ta) + for _, t := range f.tapPlugins { + // Query + q := new(tap.Message) + msg.SetQueryTime(q, start) + // Forwarder dnstap messages are from the perspective of the downstream server + // (upstream is the forward server) + msg.SetQueryAddress(q, state.W.RemoteAddr()) + msg.SetResponseAddress(q, ta) + if t.IncludeRawMessage { + buf, _ := state.Req.Pack() + q.QueryMessage = buf + } + msg.SetType(q, tap.Message_FORWARDER_QUERY) + t.TapMessageWithMetadata(ctx, q, state) - if f.tapPlugin.IncludeRawMessage { - buf, _ := state.Req.Pack() - q.QueryMessage = buf - } - msg.SetType(q, tap.Message_FORWARDER_QUERY) - f.tapPlugin.TapMessage(q) - - // Response - if reply != nil { - r := new(tap.Message) - if f.tapPlugin.IncludeRawMessage { - buf, _ := reply.Pack() - r.ResponseMessage = buf + // Response + if reply != nil { + r := new(tap.Message) + if t.IncludeRawMessage { + buf, _ := reply.Pack() + r.ResponseMessage = buf + } + msg.SetQueryTime(r, start) + msg.SetQueryAddress(r, state.W.RemoteAddr()) + msg.SetResponseAddress(r, ta) + msg.SetResponseTime(r, time.Now()) + msg.SetType(r, tap.Message_FORWARDER_RESPONSE) + t.TapMessageWithMetadata(ctx, r, state) } - msg.SetQueryTime(r, start) - msg.SetQueryAddress(r, state.W.RemoteAddr()) - msg.SetResponseAddress(r, ta) - msg.SetResponseTime(r, time.Now()) - msg.SetType(r, tap.Message_FORWARDER_RESPONSE) - f.tapPlugin.TapMessage(r) } } diff --git a/plugin/forward/forward.go b/plugin/forward/forward.go index 624d08967e..cec1adb9cd 100644 --- a/plugin/forward/forward.go +++ b/plugin/forward/forward.go @@ -16,53 +16,77 @@ import ( "github.com/coredns/coredns/plugin/dnstap" "github.com/coredns/coredns/plugin/metadata" clog "github.com/coredns/coredns/plugin/pkg/log" + proxyPkg "github.com/coredns/coredns/plugin/pkg/proxy" "github.com/coredns/coredns/request" "github.com/miekg/dns" ot "github.com/opentracing/opentracing-go" + otext "github.com/opentracing/opentracing-go/ext" ) var log = clog.NewWithPlugin("forward") +const ( + defaultExpire = 10 * time.Second + hcInterval = 500 * time.Millisecond +) + // Forward represents a plugin instance that can proxy requests to another (DNS) server. It has a list // of proxies each representing one upstream proxy. type Forward struct { concurrent int64 // atomic counters need to be first in struct for proper alignment - proxies []*Proxy + proxies []*proxyPkg.Proxy p Policy hcInterval time.Duration from string ignored []string - tlsConfig *tls.Config - tlsServerName string - maxfails uint32 - expire time.Duration - maxConcurrent int64 + nextAlternateRcodes []int + + tlsConfig *tls.Config + tlsServerName string + maxfails uint32 + expire time.Duration + maxConcurrent int64 + failfastUnhealthyUpstreams bool + failoverRcodes []int - opts options // also here for testing + opts proxyPkg.Options // also here for testing // ErrLimitExceeded indicates that a query was rejected because the number of concurrent queries has exceeded // the maximum allowed (maxConcurrent) ErrLimitExceeded error - tapPlugin *dnstap.Dnstap // when the dnstap plugin is loaded, we use to this to send messages out. + tapPlugins []*dnstap.Dnstap // when dnstap plugins are loaded, we use to this to send messages out. Next plugin.Handler } // New returns a new Forward. func New() *Forward { - f := &Forward{maxfails: 2, tlsConfig: new(tls.Config), expire: defaultExpire, p: new(random), from: ".", hcInterval: hcInterval, opts: options{forceTCP: false, preferUDP: false, hcRecursionDesired: true}} + f := &Forward{maxfails: 2, tlsConfig: new(tls.Config), expire: defaultExpire, p: new(random), from: ".", hcInterval: hcInterval, opts: proxyPkg.Options{ForceTCP: false, PreferUDP: false, HCRecursionDesired: true, HCDomain: "."}} return f } // SetProxy appends p to the proxy list and starts healthchecking. -func (f *Forward) SetProxy(p *Proxy) { +func (f *Forward) SetProxy(p *proxyPkg.Proxy) { f.proxies = append(f.proxies, p) - p.start(f.hcInterval) + p.Start(f.hcInterval) +} + +// SetProxyOptions setup proxy options +func (f *Forward) SetProxyOptions(opts proxyPkg.Options) { + f.opts = opts +} + +// SetTapPlugin appends one or more dnstap plugins to the tap plugin list. +func (f *Forward) SetTapPlugin(tapPlugin *dnstap.Dnstap) { + f.tapPlugins = append(f.tapPlugins, tapPlugin) + if nextPlugin, ok := tapPlugin.Next.(*dnstap.Dnstap); ok { + f.SetTapPlugin(nextPlugin) + } } // Len returns the number of configured proxies. @@ -73,7 +97,6 @@ func (f *Forward) Name() string { return "forward" } // ServeDNS implements plugin.Handler. func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { - state := request.Request{W: w, Req: r} if !f.match(state) { return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r) @@ -83,7 +106,7 @@ func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg count := atomic.AddInt64(&(f.concurrent), 1) defer atomic.AddInt64(&(f.concurrent), -1) if count > f.maxConcurrent { - MaxConcurrentRejectCount.Add(1) + maxConcurrentRejectCount.Add(1) return dns.RcodeRefused, f.ErrLimitExceeded } } @@ -96,7 +119,7 @@ func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg list := f.List() deadline := time.Now().Add(defaultTimeout) start := time.Now() - for time.Now().Before(deadline) { + for time.Now().Before(deadline) && ctx.Err() == nil { if i >= len(list) { // reached the end of list, reset to begin i = 0 @@ -110,21 +133,26 @@ func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg if fails < len(f.proxies) { continue } - // All upstream proxies are dead, assume healthcheck is completely broken and randomly + + healthcheckBrokenCount.Add(1) + // All upstreams are dead, return servfail if all upstreams are down + if f.failfastUnhealthyUpstreams { + break + } + // assume healthcheck is completely broken and randomly // select an upstream to connect to. r := new(random) proxy = r.List(f.proxies)[0] - - HealthcheckBrokenCount.Add(1) } if span != nil { child = span.Tracer().StartSpan("connect", ot.ChildOf(span.Context())) + otext.PeerAddress.Set(child, proxy.Addr()) ctx = ot.ContextWithSpan(ctx, child) } metadata.SetValueFunc(ctx, "forward/upstream", func() string { - return proxy.addr + return proxy.Addr() }) var ( @@ -132,14 +160,16 @@ func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg err error ) opts := f.opts + for { ret, err = proxy.Connect(ctx, state, opts) - if err == ErrCachedClosed { // Remote side closed conn, can only happen with TCP. + + if err == proxyPkg.ErrCachedClosed { // Remote side closed conn, can only happen with TCP. continue } // Retry with TCP if truncated and prefer_udp configured. - if ret != nil && ret.Truncated && !opts.forceTCP && opts.preferUDP { - opts.forceTCP = true + if ret != nil && ret.Truncated && !opts.ForceTCP && opts.PreferUDP { + opts.ForceTCP = true continue } break @@ -149,8 +179,8 @@ func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg child.Finish() } - if f.tapPlugin != nil { - toDnstap(f, proxy.addr, state, opts, ret, start) + if len(f.tapPlugins) != 0 { + toDnstap(ctx, f, proxy.Addr(), state, opts, ret, start) } upstreamErr = err @@ -177,6 +207,30 @@ func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg return 0, nil } + // Check if we have a failover Rcode defined, check if we match on the code + tryNext := false + for _, failoverRcode := range f.failoverRcodes { + // if we match, we continue to the next upstream in the list + if failoverRcode == ret.Rcode { + if fails < len(f.proxies) { + tryNext = true + } + } + } + if tryNext { + fails++ + continue + } + + // Check if we have an alternate Rcode defined, check if we match on the code + for _, alternateRcode := range f.nextAlternateRcodes { + if alternateRcode == ret.Rcode && f.Next != nil { // In case we do not have a Next handler, just continue normally + if _, ok := f.Next.(*Forward); ok { // Only continue if the next forwarder is also a Forworder + return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r) + } + } + } + w.WriteMsg(ret) return 0, nil } @@ -210,13 +264,13 @@ func (f *Forward) isAllowedDomain(name string) bool { } // ForceTCP returns if TCP is forced to be used even when the request comes in over UDP. -func (f *Forward) ForceTCP() bool { return f.opts.forceTCP } +func (f *Forward) ForceTCP() bool { return f.opts.ForceTCP } // PreferUDP returns if UDP is preferred to be used even when the request comes in over TCP. -func (f *Forward) PreferUDP() bool { return f.opts.preferUDP } +func (f *Forward) PreferUDP() bool { return f.opts.PreferUDP } // List returns a set of proxies to be used for this client depending on the policy in f. -func (f *Forward) List() []*Proxy { return f.p.List(f.proxies) } +func (f *Forward) List() []*proxyPkg.Proxy { return f.p.List(f.proxies) } var ( // ErrNoHealthy means no healthy proxies left. @@ -227,11 +281,16 @@ var ( ErrCachedClosed = errors.New("cached connection was closed by peer") ) -// options holds various options that can be set. -type options struct { - forceTCP bool - preferUDP bool - hcRecursionDesired bool +// Options holds various Options that can be set. +type Options struct { + // ForceTCP use TCP protocol for upstream DNS request. Has precedence over PreferUDP flag + ForceTCP bool + // PreferUDP use UDP protocol for upstream DNS request. + PreferUDP bool + // HCRecursionDesired sets recursion desired flag for Proxy healthcheck requests + HCRecursionDesired bool + // HCDomain sets domain for Proxy healthcheck requests + HCDomain string } var defaultTimeout = 5 * time.Second diff --git a/plugin/forward/forward_test.go b/plugin/forward/forward_test.go index b0ef47ba92..aca58cbf9a 100644 --- a/plugin/forward/forward_test.go +++ b/plugin/forward/forward_test.go @@ -1,24 +1,76 @@ package forward import ( + "strings" "testing" + + "github.com/coredns/caddy" + "github.com/coredns/caddy/caddyfile" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin/dnstap" + "github.com/coredns/coredns/plugin/pkg/proxy" + "github.com/coredns/coredns/plugin/pkg/transport" ) func TestList(t *testing.T) { f := Forward{ - proxies: []*Proxy{{addr: "1.1.1.1:53"}, {addr: "2.2.2.2:53"}, {addr: "3.3.3.3:53"}}, - p: &roundRobin{}, + proxies: []*proxy.Proxy{ + proxy.NewProxy("TestList", "1.1.1.1:53", transport.DNS), + proxy.NewProxy("TestList", "2.2.2.2:53", transport.DNS), + proxy.NewProxy("TestList", "3.3.3.3:53", transport.DNS), + }, + p: &roundRobin{}, } - expect := []*Proxy{{addr: "2.2.2.2:53"}, {addr: "1.1.1.1:53"}, {addr: "3.3.3.3:53"}} + expect := []*proxy.Proxy{ + proxy.NewProxy("TestList", "2.2.2.2:53", transport.DNS), + proxy.NewProxy("TestList", "1.1.1.1:53", transport.DNS), + proxy.NewProxy("TestList", "3.3.3.3:53", transport.DNS), + } got := f.List() if len(got) != len(expect) { t.Fatalf("Expected: %v results, got: %v", len(expect), len(got)) } for i, p := range got { - if p.addr != expect[i].addr { - t.Fatalf("Expected proxy %v to be '%v', got: '%v'", i, expect[i].addr, p.addr) + if p.Addr() != expect[i].Addr() { + t.Fatalf("Expected proxy %v to be '%v', got: '%v'", i, expect[i].Addr(), p.Addr()) } } } + +func TestSetTapPlugin(t *testing.T) { + input := `forward . 127.0.0.1 + dnstap /tmp/dnstap.sock full + dnstap tcp://example.com:6000 + ` + stanzas := strings.Split(input, "\n") + c := caddy.NewTestController("dns", strings.Join(stanzas[1:], "\n")) + dnstapSetup, err := caddy.DirectiveAction("dns", "dnstap") + if err != nil { + t.Fatal(err) + } + if err = dnstapSetup(c); err != nil { + t.Fatal(err) + } + c.Dispenser = caddyfile.NewDispenser("", strings.NewReader(stanzas[0])) + if err = setup(c); err != nil { + t.Fatal(err) + } + dnsserver.NewServer("", []*dnsserver.Config{dnsserver.GetConfig(c)}) + f, ok := dnsserver.GetConfig(c).Handler("forward").(*Forward) + if !ok { + t.Fatal("Expected a forward plugin") + } + tap, ok := dnsserver.GetConfig(c).Handler("dnstap").(*dnstap.Dnstap) + if !ok { + t.Fatal("Expected a dnstap plugin") + } + f.SetTapPlugin(tap) + if len(f.tapPlugins) != 2 { + t.Fatalf("Expected: 2 results, got: %v", len(f.tapPlugins)) + } + if f.tapPlugins[0] != tap || tap.Next != f.tapPlugins[1] { + t.Error("Unexpected order of dnstap plugins") + } +} diff --git a/plugin/forward/fuzz.go b/plugin/forward/fuzz.go index d07b1345bd..4367f651cb 100644 --- a/plugin/forward/fuzz.go +++ b/plugin/forward/fuzz.go @@ -1,10 +1,11 @@ -// +build gofuzz +//go:build gofuzz package forward import ( "github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/pkg/fuzz" + "github.com/coredns/coredns/plugin/pkg/proxy" "github.com/miekg/dns" ) @@ -16,8 +17,8 @@ var f *Forward func init() { f = New() s := dnstest.NewServer(r{}.reflectHandler) - f.SetProxy(NewProxy(s.Addr, "tcp")) - f.SetProxy(NewProxy(s.Addr, "udp")) + f.SetProxy(proxy.NewProxy("FuzzForwardPlugin1", s.Addr, "tcp")) + f.SetProxy(proxy.NewProxy("FuzzForwardPlugin2", s.Addr, "udp")) } // Fuzz fuzzes forward. diff --git a/plugin/forward/health_test.go b/plugin/forward/health_test.go index 88a96e8033..3f511385ed 100644 --- a/plugin/forward/health_test.go +++ b/plugin/forward/health_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/proxy" "github.com/coredns/coredns/plugin/pkg/transport" "github.com/coredns/coredns/plugin/test" @@ -14,9 +15,6 @@ import ( ) func TestHealth(t *testing.T) { - hcReadTimeout = 10 * time.Millisecond - hcWriteTimeout = 10 * time.Millisecond - readTimeout = 10 * time.Millisecond defaultTimeout = 10 * time.Millisecond i := uint32(0) @@ -35,7 +33,9 @@ func TestHealth(t *testing.T) { }) defer s.Close() - p := NewProxy(s.Addr, transport.DNS) + p := proxy.NewProxy("TestHealth", s.Addr, transport.DNS) + p.GetHealthchecker().SetReadTimeout(10 * time.Millisecond) + p.GetHealthchecker().SetWriteTimeout(10 * time.Millisecond) f := New() f.SetProxy(p) defer f.OnShutdown() @@ -52,11 +52,47 @@ func TestHealth(t *testing.T) { } } +func TestHealthTCP(t *testing.T) { + defaultTimeout = 10 * time.Millisecond + + i := uint32(0) + q := uint32(0) + s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { + if atomic.LoadUint32(&q) == 0 { //drop the first query to trigger health-checking + atomic.AddUint32(&q, 1) + return + } + if r.Question[0].Name == "." && r.RecursionDesired == true { + atomic.AddUint32(&i, 1) + } + ret := new(dns.Msg) + ret.SetReply(r) + w.WriteMsg(ret) + }) + defer s.Close() + + p := proxy.NewProxy("TestHealthTCP", s.Addr, transport.DNS) + p.GetHealthchecker().SetReadTimeout(10 * time.Millisecond) + p.GetHealthchecker().SetWriteTimeout(10 * time.Millisecond) + p.GetHealthchecker().SetTCPTransport() + f := New() + f.SetProxy(p) + defer f.OnShutdown() + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + + f.ServeDNS(context.TODO(), &test.ResponseWriter{TCP: true}, req) + + time.Sleep(20 * time.Millisecond) + i1 := atomic.LoadUint32(&i) + if i1 != 1 { + t.Errorf("Expected number of health checks with RecursionDesired==true to be %d, got %d", 1, i1) + } +} + func TestHealthNoRecursion(t *testing.T) { - hcReadTimeout = 10 * time.Millisecond - readTimeout = 10 * time.Millisecond defaultTimeout = 10 * time.Millisecond - hcWriteTimeout = 10 * time.Millisecond i := uint32(0) q := uint32(0) @@ -74,8 +110,10 @@ func TestHealthNoRecursion(t *testing.T) { }) defer s.Close() - p := NewProxy(s.Addr, transport.DNS) - p.health.SetRecursionDesired(false) + p := proxy.NewProxy("TestHealthNoRecursion", s.Addr, transport.DNS) + p.GetHealthchecker().SetReadTimeout(10 * time.Millisecond) + p.GetHealthchecker().SetWriteTimeout(10 * time.Millisecond) + p.GetHealthchecker().SetRecursionDesired(false) f := New() f.SetProxy(p) defer f.OnShutdown() @@ -93,9 +131,6 @@ func TestHealthNoRecursion(t *testing.T) { } func TestHealthTimeout(t *testing.T) { - hcReadTimeout = 10 * time.Millisecond - hcWriteTimeout = 10 * time.Millisecond - readTimeout = 10 * time.Millisecond defaultTimeout = 10 * time.Millisecond i := uint32(0) @@ -119,7 +154,9 @@ func TestHealthTimeout(t *testing.T) { }) defer s.Close() - p := NewProxy(s.Addr, transport.DNS) + p := proxy.NewProxy("TestHealthTimeout", s.Addr, transport.DNS) + p.GetHealthchecker().SetReadTimeout(10 * time.Millisecond) + p.GetHealthchecker().SetWriteTimeout(10 * time.Millisecond) f := New() f.SetProxy(p) defer f.OnShutdown() @@ -137,19 +174,20 @@ func TestHealthTimeout(t *testing.T) { } func TestHealthMaxFails(t *testing.T) { - hcReadTimeout = 10 * time.Millisecond - hcWriteTimeout = 10 * time.Millisecond - readTimeout = 10 * time.Millisecond defaultTimeout = 10 * time.Millisecond - hcInterval = 10 * time.Millisecond + //,hcInterval = 10 * time.Millisecond s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { // timeout }) defer s.Close() - p := NewProxy(s.Addr, transport.DNS) + p := proxy.NewProxy("TestHealthMaxFails", s.Addr, transport.DNS) + p.SetReadTimeout(10 * time.Millisecond) + p.GetHealthchecker().SetReadTimeout(10 * time.Millisecond) + p.GetHealthchecker().SetWriteTimeout(10 * time.Millisecond) f := New() + f.hcInterval = 10 * time.Millisecond f.maxfails = 2 f.SetProxy(p) defer f.OnShutdown() @@ -160,18 +198,14 @@ func TestHealthMaxFails(t *testing.T) { f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req) time.Sleep(100 * time.Millisecond) - fails := atomic.LoadUint32(&p.fails) + fails := p.Fails() if !p.Down(f.maxfails) { t.Errorf("Expected Proxy fails to be greater than %d, got %d", f.maxfails, fails) } } func TestHealthNoMaxFails(t *testing.T) { - hcReadTimeout = 10 * time.Millisecond - hcWriteTimeout = 10 * time.Millisecond - readTimeout = 10 * time.Millisecond defaultTimeout = 10 * time.Millisecond - hcInterval = 10 * time.Millisecond i := uint32(0) s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { @@ -185,7 +219,9 @@ func TestHealthNoMaxFails(t *testing.T) { }) defer s.Close() - p := NewProxy(s.Addr, transport.DNS) + p := proxy.NewProxy("TestHealthNoMaxFails", s.Addr, transport.DNS) + p.GetHealthchecker().SetReadTimeout(10 * time.Millisecond) + p.GetHealthchecker().SetWriteTimeout(10 * time.Millisecond) f := New() f.maxfails = 0 f.SetProxy(p) @@ -202,3 +238,118 @@ func TestHealthNoMaxFails(t *testing.T) { t.Errorf("Expected number of health checks to be %d, got %d", 0, i1) } } + +func TestHealthDomain(t *testing.T) { + defaultTimeout = 10 * time.Millisecond + + hcDomain := "example.org." + i := uint32(0) + q := uint32(0) + s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { + if atomic.LoadUint32(&q) == 0 { //drop the first query to trigger health-checking + atomic.AddUint32(&q, 1) + return + } + if r.Question[0].Name == hcDomain && r.RecursionDesired == true { + atomic.AddUint32(&i, 1) + } + ret := new(dns.Msg) + ret.SetReply(r) + w.WriteMsg(ret) + }) + defer s.Close() + p := proxy.NewProxy("TestHealthDomain", s.Addr, transport.DNS) + p.GetHealthchecker().SetReadTimeout(10 * time.Millisecond) + p.GetHealthchecker().SetWriteTimeout(10 * time.Millisecond) + p.GetHealthchecker().SetDomain(hcDomain) + f := New() + f.SetProxy(p) + defer f.OnShutdown() + + req := new(dns.Msg) + req.SetQuestion(".", dns.TypeNS) + + f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req) + + time.Sleep(20 * time.Millisecond) + i1 := atomic.LoadUint32(&i) + if i1 != 1 { + t.Errorf("Expected number of health checks with Domain==%s to be %d, got %d", hcDomain, 1, i1) + } +} + +func TestAllUpstreamsDown(t *testing.T) { + qs := uint32(0) + s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { + // count non-healthcheck queries + if r.Question[0].Name != "." { + atomic.AddUint32(&qs, 1) + } + // timeout + }) + defer s.Close() + + s1 := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { + // count non-healthcheck queries + if r.Question[0].Name != "." { + atomic.AddUint32(&qs, 1) + } + // timeout + }) + defer s1.Close() + + p := proxy.NewProxy("TestHealthAllUpstreamsDown", s.Addr, transport.DNS) + p1 := proxy.NewProxy("TestHealthAllUpstreamsDown2", s1.Addr, transport.DNS) + p.GetHealthchecker().SetReadTimeout(10 * time.Millisecond) + p1.GetHealthchecker().SetReadTimeout(10 * time.Millisecond) + + f := New() + f.SetProxy(p) + f.SetProxy(p1) + f.failfastUnhealthyUpstreams = true + f.maxfails = 1 + // Make proxys fail by checking health twice + // i.e, fails > maxfails + for range f.maxfails + 1 { + p.GetHealthchecker().Check(p) + p1.GetHealthchecker().Check(p1) + } + + defer f.OnShutdown() + + // Check if all proxies are down + if !p.Down(f.maxfails) || !p1.Down(f.maxfails) { + t.Fatalf("Expected all proxies to be down") + } + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + resp, err := f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req) + + if resp != dns.RcodeServerFailure { + t.Errorf("Expected Response code: %d, Got: %d", dns.RcodeServerFailure, resp) + } + + if err != ErrNoHealthy { + t.Errorf("Expected error message: no healthy proxies, Got: %s", err.Error()) + } + + q1 := atomic.LoadUint32(&qs) + if q1 != 0 { + t.Errorf("Expected queries to the upstream: 0, Got: %d", q1) + } + + // set failfast to false to check if queries get answered + f.failfastUnhealthyUpstreams = false + + req = new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + _, err = f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req) + if err == ErrNoHealthy { + t.Error("Unexpected error message: no healthy proxies") + } + + q1 = atomic.LoadUint32(&qs) + if q1 != 1 { + t.Errorf("Expected queries to the upstream: 1, Got: %d", q1) + } +} diff --git a/plugin/forward/metrics.go b/plugin/forward/metrics.go index 51685a5997..246dc65006 100644 --- a/plugin/forward/metrics.go +++ b/plugin/forward/metrics.go @@ -9,59 +9,17 @@ import ( // Variables declared for monitoring. var ( - RequestCount = promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: plugin.Namespace, - Subsystem: "forward", - Name: "requests_total", - Help: "Counter of requests made per upstream.", - }, []string{"to"}) - RcodeCount = promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: plugin.Namespace, - Subsystem: "forward", - Name: "responses_total", - Help: "Counter of responses received per upstream.", - }, []string{"rcode", "to"}) - RequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: plugin.Namespace, - Subsystem: "forward", - Name: "request_duration_seconds", - Buckets: plugin.TimeBuckets, - Help: "Histogram of the time each request took.", - }, []string{"to", "rcode", "type"}) - HealthcheckFailureCount = promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: plugin.Namespace, - Subsystem: "forward", - Name: "healthcheck_failures_total", - Help: "Counter of the number of failed healthchecks.", - }, []string{"to"}) - HealthcheckBrokenCount = promauto.NewCounter(prometheus.CounterOpts{ + healthcheckBrokenCount = promauto.NewCounter(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "forward", Name: "healthcheck_broken_total", Help: "Counter of the number of complete failures of the healthchecks.", }) - SocketGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: plugin.Namespace, - Subsystem: "forward", - Name: "sockets_open", - Help: "Gauge of open sockets per upstream.", - }, []string{"to"}) - MaxConcurrentRejectCount = promauto.NewCounter(prometheus.CounterOpts{ + + maxConcurrentRejectCount = promauto.NewCounter(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "forward", Name: "max_concurrent_rejects_total", Help: "Counter of the number of queries rejected because the concurrent queries were at maximum.", }) - ConnCacheHitsCount = promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: plugin.Namespace, - Subsystem: "forward", - Name: "conn_cache_hits_total", - Help: "Counter of connection cache hits per upstream and protocol.", - }, []string{"to", "proto"}) - ConnCacheMissesCount = promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: plugin.Namespace, - Subsystem: "forward", - Name: "conn_cache_misses_total", - Help: "Counter of connection cache misses per upstream and protocol.", - }, []string{"to", "proto"}) ) diff --git a/plugin/forward/policy.go b/plugin/forward/policy.go index 2066e1316e..7bd1f316a3 100644 --- a/plugin/forward/policy.go +++ b/plugin/forward/policy.go @@ -1,13 +1,16 @@ package forward import ( - "math/rand" "sync/atomic" + "time" + + "github.com/coredns/coredns/plugin/pkg/proxy" + "github.com/coredns/coredns/plugin/pkg/rand" ) // Policy defines a policy we use for selecting upstreams. type Policy interface { - List([]*Proxy) []*Proxy + List([]*proxy.Proxy) []*proxy.Proxy String() string } @@ -16,19 +19,19 @@ type random struct{} func (r *random) String() string { return "random" } -func (r *random) List(p []*Proxy) []*Proxy { +func (r *random) List(p []*proxy.Proxy) []*proxy.Proxy { switch len(p) { case 1: return p case 2: - if rand.Int()%2 == 0 { - return []*Proxy{p[1], p[0]} // swap + if rn.Int()%2 == 0 { + return []*proxy.Proxy{p[1], p[0]} // swap } return p } - perms := rand.Perm(len(p)) - rnd := make([]*Proxy, len(p)) + perms := rn.Perm(len(p)) + rnd := make([]*proxy.Proxy, len(p)) for i, p1 := range perms { rnd[i] = p[p1] @@ -43,11 +46,11 @@ type roundRobin struct { func (r *roundRobin) String() string { return "round_robin" } -func (r *roundRobin) List(p []*Proxy) []*Proxy { +func (r *roundRobin) List(p []*proxy.Proxy) []*proxy.Proxy { poolLen := uint32(len(p)) i := atomic.AddUint32(&r.robin, 1) % poolLen - robin := []*Proxy{p[i]} + robin := []*proxy.Proxy{p[i]} robin = append(robin, p[:i]...) robin = append(robin, p[i+1:]...) @@ -59,6 +62,8 @@ type sequential struct{} func (r *sequential) String() string { return "sequential" } -func (r *sequential) List(p []*Proxy) []*Proxy { +func (r *sequential) List(p []*proxy.Proxy) []*proxy.Proxy { return p } + +var rn = rand.New(time.Now().UnixNano()) diff --git a/plugin/forward/proxy_test.go b/plugin/forward/proxy_test.go index dc1f5fb184..daf5f964cc 100644 --- a/plugin/forward/proxy_test.go +++ b/plugin/forward/proxy_test.go @@ -6,9 +6,7 @@ import ( "github.com/coredns/caddy" "github.com/coredns/coredns/plugin/pkg/dnstest" - "github.com/coredns/coredns/plugin/pkg/transport" "github.com/coredns/coredns/plugin/test" - "github.com/coredns/coredns/request" "github.com/miekg/dns" ) @@ -23,7 +21,8 @@ func TestProxy(t *testing.T) { defer s.Close() c := caddy.NewTestController("dns", "forward . "+s.Addr) - f, err := parseForward(c) + fs, err := parseForward(c) + f := fs[0] if err != nil { t.Errorf("Failed to create forwarder: %s", err) } @@ -53,7 +52,8 @@ func TestProxyTLSFail(t *testing.T) { defer s.Close() c := caddy.NewTestController("dns", "forward . tls://"+s.Addr) - f, err := parseForward(c) + fs, err := parseForward(c) + f := fs[0] if err != nil { t.Errorf("Failed to create forwarder: %s", err) } @@ -68,30 +68,3 @@ func TestProxyTLSFail(t *testing.T) { t.Fatal("Expected *not* to receive reply, but got one") } } - -func TestProtocolSelection(t *testing.T) { - p := NewProxy("bad_address", transport.DNS) - - stateUDP := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} - stateTCP := request.Request{W: &test.ResponseWriter{TCP: true}, Req: new(dns.Msg)} - ctx := context.TODO() - - go func() { - p.Connect(ctx, stateUDP, options{}) - p.Connect(ctx, stateUDP, options{forceTCP: true}) - p.Connect(ctx, stateUDP, options{preferUDP: true}) - p.Connect(ctx, stateUDP, options{preferUDP: true, forceTCP: true}) - p.Connect(ctx, stateTCP, options{}) - p.Connect(ctx, stateTCP, options{forceTCP: true}) - p.Connect(ctx, stateTCP, options{preferUDP: true}) - p.Connect(ctx, stateTCP, options{preferUDP: true, forceTCP: true}) - }() - - for i, exp := range []string{"udp", "tcp", "udp", "tcp", "tcp", "tcp", "udp", "tcp"} { - proto := <-p.transport.dial - p.transport.ret <- nil - if proto != exp { - t.Errorf("Unexpected protocol in case %d, expected %q, actual %q", i, exp, proto) - } - } -} diff --git a/plugin/forward/setup.go b/plugin/forward/setup.go index 7504e94099..7ffcef0d19 100644 --- a/plugin/forward/setup.go +++ b/plugin/forward/setup.go @@ -4,7 +4,9 @@ import ( "crypto/tls" "errors" "fmt" + "path/filepath" "strconv" + "strings" "time" "github.com/coredns/caddy" @@ -12,41 +14,57 @@ import ( "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/dnstap" "github.com/coredns/coredns/plugin/pkg/parse" + "github.com/coredns/coredns/plugin/pkg/proxy" pkgtls "github.com/coredns/coredns/plugin/pkg/tls" "github.com/coredns/coredns/plugin/pkg/transport" + + "github.com/miekg/dns" ) -func init() { plugin.Register("forward", setup) } +func init() { + plugin.Register("forward", setup) +} func setup(c *caddy.Controller) error { - f, err := parseForward(c) + fs, err := parseForward(c) if err != nil { return plugin.Error("forward", err) } - if f.Len() > max { - return plugin.Error("forward", fmt.Errorf("more than %d TOs configured: %d", max, f.Len())) - } + for i := range fs { + f := fs[i] + if f.Len() > max { + return plugin.Error("forward", fmt.Errorf("more than %d TOs configured: %d", max, f.Len())) + } - dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { - f.Next = next - return f - }) - - c.OnStartup(func() error { - return f.OnStartup() - }) - c.OnStartup(func() error { - if taph := dnsserver.GetConfig(c).Handler("dnstap"); taph != nil { - if tapPlugin, ok := taph.(dnstap.Dnstap); ok { - f.tapPlugin = &tapPlugin - } + if i == len(fs)-1 { + // last forward: point next to next plugin + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + f.Next = next + return f + }) + } else { + // middle forward: point next to next forward + nextForward := fs[i+1] + dnsserver.GetConfig(c).AddPlugin(func(plugin.Handler) plugin.Handler { + f.Next = nextForward + return f + }) } - return nil - }) - c.OnShutdown(func() error { - return f.OnShutdown() - }) + c.OnStartup(func() error { + return f.OnStartup() + }) + c.OnStartup(func() error { + if taph := dnsserver.GetConfig(c).Handler("dnstap"); taph != nil { + f.SetTapPlugin(taph.(*dnstap.Dnstap)) + } + return nil + }) + + c.OnShutdown(func() error { + return f.OnShutdown() + }) + } return nil } @@ -54,7 +72,7 @@ func setup(c *caddy.Controller) error { // OnStartup starts a goroutines for all proxies. func (f *Forward) OnStartup() (err error) { for _, p := range f.proxies { - p.start(f.hcInterval) + p.Start(f.hcInterval) } return nil } @@ -62,28 +80,21 @@ func (f *Forward) OnStartup() (err error) { // OnShutdown stops all configured proxies. func (f *Forward) OnShutdown() error { for _, p := range f.proxies { - p.stop() + p.Stop() } return nil } -func parseForward(c *caddy.Controller) (*Forward, error) { - var ( - f *Forward - err error - i int - ) +func parseForward(c *caddy.Controller) ([]*Forward, error) { + var fs = []*Forward{} for c.Next() { - if i > 0 { - return nil, plugin.ErrOnce - } - i++ - f, err = parseStanza(c) + f, err := parseStanza(c) if err != nil { return nil, err } + fs = append(fs, f) } - return f, nil + return fs, nil } func parseStanza(c *caddy.Controller) (*Forward, error) { @@ -92,7 +103,16 @@ func parseStanza(c *caddy.Controller) (*Forward, error) { if !c.Args(&f.from) { return f, c.ArgErr() } - f.from = plugin.Host(f.from).Normalize() + origFrom := f.from + zones := plugin.Host(f.from).NormalizeExact() + if len(zones) == 0 { + return f, fmt.Errorf("unable to normalize '%s'", f.from) + } + f.from = zones[0] // there can only be one here, won't work with non-octet reverse + + if len(zones) > 1 { + log.Warningf("Unsupported CIDR notation: '%s' expands to multiple zones. Using only '%s'.", origFrom, f.from) + } to := c.RemainingArgs() if len(to) == 0 { @@ -112,7 +132,7 @@ func parseStanza(c *caddy.Controller) (*Forward, error) { if !allowedTrans[trans] { return f, fmt.Errorf("'%s' is not supported as a destination protocol in forward: %s", trans, host) } - p := NewProxy(h, trans) + p := proxy.NewProxy("forward", h, trans) f.proxies = append(f.proxies, p) transports[i] = trans } @@ -137,34 +157,36 @@ func parseStanza(c *caddy.Controller) (*Forward, error) { f.proxies[i].SetTLSConfig(f.tlsConfig) } f.proxies[i].SetExpire(f.expire) - f.proxies[i].health.SetRecursionDesired(f.opts.hcRecursionDesired) + f.proxies[i].GetHealthchecker().SetRecursionDesired(f.opts.HCRecursionDesired) + // when TLS is used, checks are set to tcp-tls + if f.opts.ForceTCP && transports[i] != transport.TLS { + f.proxies[i].GetHealthchecker().SetTCPTransport() + } + f.proxies[i].GetHealthchecker().SetDomain(f.opts.HCDomain) } return f, nil } func parseBlock(c *caddy.Controller, f *Forward) error { + config := dnsserver.GetConfig(c) switch c.Val() { case "except": ignore := c.RemainingArgs() if len(ignore) == 0 { return c.ArgErr() } - for i := 0; i < len(ignore); i++ { - ignore[i] = plugin.Host(ignore[i]).Normalize() + for i := range ignore { + f.ignored = append(f.ignored, plugin.Host(ignore[i]).NormalizeExact()...) } - f.ignored = ignore case "max_fails": if !c.NextArg() { return c.ArgErr() } - n, err := strconv.Atoi(c.Val()) + n, err := strconv.ParseUint(c.Val(), 10, 32) if err != nil { return err } - if n < 0 { - return fmt.Errorf("max_fails can't be negative: %d", n) - } f.maxfails = uint32(n) case "health_check": if !c.NextArg() { @@ -178,11 +200,21 @@ func parseBlock(c *caddy.Controller, f *Forward) error { return fmt.Errorf("health_check can't be negative: %d", dur) } f.hcInterval = dur + f.opts.HCDomain = "." for c.NextArg() { switch hcOpts := c.Val(); hcOpts { case "no_rec": - f.opts.hcRecursionDesired = false + f.opts.HCRecursionDesired = false + case "domain": + if !c.NextArg() { + return c.ArgErr() + } + hcDomain := c.Val() + if _, ok := dns.IsDomainName(hcDomain); !ok { + return fmt.Errorf("health_check: invalid domain name %s", hcDomain) + } + f.opts.HCDomain = plugin.Name(hcDomain).Normalize() default: return fmt.Errorf("health_check: unknown option %s", hcOpts) } @@ -192,18 +224,23 @@ func parseBlock(c *caddy.Controller, f *Forward) error { if c.NextArg() { return c.ArgErr() } - f.opts.forceTCP = true + f.opts.ForceTCP = true case "prefer_udp": if c.NextArg() { return c.ArgErr() } - f.opts.preferUDP = true + f.opts.PreferUDP = true case "tls": args := c.RemainingArgs() if len(args) > 3 { return c.ArgErr() } + for i := range args { + if !filepath.IsAbs(args[i]) && config.Root != "" { + args[i] = filepath.Join(config.Root, args[i]) + } + } tlsConfig, err := pkgtls.NewTLSConfigFromArgs(args...) if err != nil { return err @@ -253,7 +290,49 @@ func parseBlock(c *caddy.Controller, f *Forward) error { } f.ErrLimitExceeded = errors.New("concurrent queries exceeded maximum " + c.Val()) f.maxConcurrent = int64(n) + case "next": + args := c.RemainingArgs() + if len(args) == 0 { + return c.ArgErr() + } + + for _, rcode := range args { + var rc int + var ok bool + + if rc, ok = dns.StringToRcode[strings.ToUpper(rcode)]; !ok { + return fmt.Errorf("%s is not a valid rcode", rcode) + } + f.nextAlternateRcodes = append(f.nextAlternateRcodes, rc) + } + case "failfast_all_unhealthy_upstreams": + args := c.RemainingArgs() + if len(args) != 0 { + return c.ArgErr() + } + f.failfastUnhealthyUpstreams = true + case "failover": + args := c.RemainingArgs() + if len(args) == 0 { + return c.ArgErr() + } + toRcode := dns.StringToRcode + + for _, rcode := range args { + var rc int + var ok bool + + if rc, ok = toRcode[strings.ToUpper(rcode)]; !ok { + if rc == dns.RcodeSuccess { + return fmt.Errorf("NoError cannot be used in failover") + } + + return fmt.Errorf("%s is not a valid rcode", rcode) + } + + f.failoverRcodes = append(f.failoverRcodes, rc) + } default: return c.Errf("unknown property '%s'", c.Val()) } diff --git a/plugin/forward/setup_policy_test.go b/plugin/forward/setup_policy_test.go index 2786f9a7a2..13466d7a34 100644 --- a/plugin/forward/setup_policy_test.go +++ b/plugin/forward/setup_policy_test.go @@ -24,7 +24,7 @@ func TestSetupPolicy(t *testing.T) { for i, test := range tests { c := caddy.NewTestController("dns", test.input) - f, err := parseForward(c) + fs, err := parseForward(c) if test.shouldErr && err == nil { t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input) @@ -40,8 +40,8 @@ func TestSetupPolicy(t *testing.T) { } } - if !test.shouldErr && f.p.String() != test.expectedPolicy { - t.Errorf("Test %d: expected: %s, got: %s", i, test.expectedPolicy, f.p.String()) + if !test.shouldErr && (len(fs) == 0 || fs[0].p.String() != test.expectedPolicy) { + t.Errorf("Test %d: expected: %s, got: %s", i, test.expectedPolicy, fs[0].p.String()) } } } diff --git a/plugin/forward/setup_test.go b/plugin/forward/setup_test.go index ac62f2fa82..5d76b954bf 100644 --- a/plugin/forward/setup_test.go +++ b/plugin/forward/setup_test.go @@ -1,13 +1,21 @@ package forward import ( - "io/ioutil" + "context" + "fmt" "os" "reflect" "strings" "testing" + "time" "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/proxy" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" ) func TestSetup(t *testing.T) { @@ -17,32 +25,36 @@ func TestSetup(t *testing.T) { expectedFrom string expectedIgnored []string expectedFails uint32 - expectedOpts options + expectedOpts proxy.Options expectedErr string }{ // positive - {"forward . 127.0.0.1", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - {"forward . 127.0.0.1 {\nexcept miek.nl\n}\n", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - {"forward . 127.0.0.1 {\nmax_fails 3\n}\n", false, ".", nil, 3, options{hcRecursionDesired: true}, ""}, - {"forward . 127.0.0.1 {\nforce_tcp\n}\n", false, ".", nil, 2, options{forceTCP: true, hcRecursionDesired: true}, ""}, - {"forward . 127.0.0.1 {\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true, hcRecursionDesired: true}, ""}, - {"forward . 127.0.0.1 {\nforce_tcp\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true, forceTCP: true, hcRecursionDesired: true}, ""}, - {"forward . 127.0.0.1:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - {"forward . 127.0.0.1:8080", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - {"forward . [::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - {"forward . [2003::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - {"forward . 127.0.0.1 \n", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - // negative - {"forward . a27.0.0.1", true, "", nil, 0, options{hcRecursionDesired: true}, "not an IP"}, - {"forward . 127.0.0.1 {\nblaatl\n}\n", true, "", nil, 0, options{hcRecursionDesired: true}, "unknown property"}, + {"forward . 127.0.0.1", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain example.org\n}\n", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "example.org."}, ""}, + {"forward . 127.0.0.1 {\nexcept miek.nl\n}\n", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nmax_fails 3\n}\n", false, ".", nil, 3, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nforce_tcp\n}\n", false, ".", nil, 2, proxy.Options{ForceTCP: true, HCRecursionDesired: true, HCDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nprefer_udp\n}\n", false, ".", nil, 2, proxy.Options{PreferUDP: true, HCRecursionDesired: true, HCDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nforce_tcp\nprefer_udp\n}\n", false, ".", nil, 2, proxy.Options{PreferUDP: true, ForceTCP: true, HCRecursionDesired: true, HCDomain: "."}, ""}, + {"forward . 127.0.0.1:53", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""}, + {"forward . 127.0.0.1:8080", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""}, + {"forward . [::1]:53", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""}, + {"forward . [2003::1]:53", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""}, + {"forward . 127.0.0.1 \n", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""}, + {"forward 10.9.3.0/18 127.0.0.1", false, "0.9.10.in-addr.arpa.", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""}, {`forward . ::1 - forward com ::2`, true, "", nil, 0, options{hcRecursionDesired: true}, "plugin"}, - {"forward . https://127.0.0.1 \n", true, ".", nil, 2, options{hcRecursionDesired: true}, "'https' is not supported as a destination protocol in forward: https://127.0.0.1"}, + forward com ::2`, false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "plugin"}, + // negative + {"forward . a27.0.0.1", true, "", nil, 0, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "not an IP"}, + {"forward . 127.0.0.1 {\nblaatl\n}\n", true, "", nil, 0, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "unknown property"}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain\n}\n", true, "", nil, 0, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "Wrong argument count or unexpected line ending after 'domain'"}, + {"forward . https://127.0.0.1 \n", true, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "'https' is not supported as a destination protocol in forward: https://127.0.0.1"}, + {"forward xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 127.0.0.1 \n", true, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "unable to normalize 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'"}, } for i, test := range tests { c := caddy.NewTestController("dns", test.input) - f, err := parseForward(c) + fs, err := parseForward(c) if test.shouldErr && err == nil { t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input) @@ -50,7 +62,7 @@ func TestSetup(t *testing.T) { if err != nil { if !test.shouldErr { - t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err) + t.Fatalf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err) } if !strings.Contains(err.Error(), test.expectedErr) { @@ -58,19 +70,22 @@ func TestSetup(t *testing.T) { } } - if !test.shouldErr && f.from != test.expectedFrom { - t.Errorf("Test %d: expected: %s, got: %s", i, test.expectedFrom, f.from) - } - if !test.shouldErr && test.expectedIgnored != nil { - if !reflect.DeepEqual(f.ignored, test.expectedIgnored) { - t.Errorf("Test %d: expected: %q, actual: %q", i, test.expectedIgnored, f.ignored) + if !test.shouldErr { + f := fs[0] + if f.from != test.expectedFrom { + t.Errorf("Test %d: expected: %s, got: %s", i, test.expectedFrom, f.from) + } + if test.expectedIgnored != nil { + if !reflect.DeepEqual(f.ignored, test.expectedIgnored) { + t.Errorf("Test %d: expected: %q, actual: %q", i, test.expectedIgnored, f.ignored) + } + } + if f.maxfails != test.expectedFails { + t.Errorf("Test %d: expected: %d, got: %d", i, test.expectedFails, f.maxfails) + } + if f.opts != test.expectedOpts { + t.Errorf("Test %d: expected: %v, got: %v", i, test.expectedOpts, f.opts) } - } - if !test.shouldErr && f.maxfails != test.expectedFails { - t.Errorf("Test %d: expected: %d, got: %d", i, test.expectedFails, f.maxfails) - } - if !test.shouldErr && f.opts != test.expectedOpts { - t.Errorf("Test %d: expected: %v, got: %v", i, test.expectedOpts, f.opts) } } } @@ -97,7 +112,7 @@ func TestSetupTLS(t *testing.T) { for i, test := range tests { c := caddy.NewTestController("dns", test.input) - f, err := parseForward(c) + fs, err := parseForward(c) if test.shouldErr && err == nil { t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input) @@ -113,25 +128,34 @@ func TestSetupTLS(t *testing.T) { } } + f := fs[0] + if !test.shouldErr && test.expectedServerName != "" && test.expectedServerName != f.tlsConfig.ServerName { t.Errorf("Test %d: expected: %q, actual: %q", i, test.expectedServerName, f.tlsConfig.ServerName) } - if !test.shouldErr && test.expectedServerName != "" && test.expectedServerName != f.proxies[0].health.(*dnsHc).c.TLSConfig.ServerName { - t.Errorf("Test %d: expected: %q, actual: %q", i, test.expectedServerName, f.proxies[0].health.(*dnsHc).c.TLSConfig.ServerName) + if !test.shouldErr && test.expectedServerName != "" && test.expectedServerName != f.proxies[0].GetHealthchecker().GetTLSConfig().ServerName { + t.Errorf("Test %d: expected: %q, actual: %q", i, test.expectedServerName, f.proxies[0].GetHealthchecker().GetTLSConfig().ServerName) } } } func TestSetupResolvconf(t *testing.T) { const resolv = "resolv.conf" - if err := ioutil.WriteFile(resolv, + if err := os.WriteFile(resolv, []byte(`nameserver 10.10.255.252 nameserver 10.10.255.253`), 0666); err != nil { t.Fatalf("Failed to write resolv.conf file: %s", err) } defer os.Remove(resolv) + const resolvIPV6 = "resolv-ipv6.conf" + if err := os.WriteFile(resolvIPV6, + []byte(`nameserver 0388:d254:7aec:6892:9f7f:e93b:5806:1b0f%en0`), 0666); err != nil { + t.Fatalf("Failed to write %v file: %s", resolvIPV6, err) + } + defer os.Remove(resolvIPV6) + tests := []struct { input string shouldErr bool @@ -142,11 +166,13 @@ nameserver 10.10.255.253`), 0666); err != nil { {`forward . ` + resolv, false, "", []string{"10.10.255.252:53", "10.10.255.253:53"}}, // fail {`forward . /dev/null`, true, "no nameservers", nil}, + // IPV6 with local zone + {`forward . ` + resolvIPV6, false, "", []string{"[0388:d254:7aec:6892:9f7f:e93b:5806:1b0f]:53"}}, } for i, test := range tests { c := caddy.NewTestController("dns", test.input) - f, err := parseForward(c) + fs, err := parseForward(c) if test.shouldErr && err == nil { t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input) @@ -163,19 +189,20 @@ nameserver 10.10.255.253`), 0666); err != nil { } } - if !test.shouldErr { - for j, n := range test.expectedNames { - addr := f.proxies[j].addr - if n != addr { - t.Errorf("Test %d, expected %q, got %q", j, n, addr) - } - } - } if test.shouldErr { continue } + + f := fs[0] + for j, n := range test.expectedNames { + addr := f.proxies[j].Addr() + if n != addr { + t.Errorf("Test %d, expected %q, got %q", j, n, addr) + } + } + for _, p := range f.proxies { - p.health.Check(p) // this should almost always err, we don't care it shouldn't crash + p.Healthcheck() // this should almost always err, we don't care it shouldn't crash } } } @@ -196,7 +223,7 @@ func TestSetupMaxConcurrent(t *testing.T) { for i, test := range tests { c := caddy.NewTestController("dns", test.input) - f, err := parseForward(c) + fs, err := parseForward(c) if test.shouldErr && err == nil { t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input) @@ -212,7 +239,11 @@ func TestSetupMaxConcurrent(t *testing.T) { } } - if !test.shouldErr && f.maxConcurrent != test.expectedVal { + if test.shouldErr { + continue + } + f := fs[0] + if f.maxConcurrent != test.expectedVal { t.Errorf("Test %d: expected: %d, got: %d", i, test.expectedVal, f.maxConcurrent) } } @@ -220,23 +251,31 @@ func TestSetupMaxConcurrent(t *testing.T) { func TestSetupHealthCheck(t *testing.T) { tests := []struct { - input string - shouldErr bool - expectedVal bool - expectedErr string + input string + shouldErr bool + expectedRecVal bool + expectedDomain string + expectedErr string }{ // positive - {"forward . 127.0.0.1\n", false, true, ""}, - {"forward . 127.0.0.1 {\nhealth_check 0.5s\n}\n", false, true, ""}, - {"forward . 127.0.0.1 {\nhealth_check 0.5s no_rec\n}\n", false, false, ""}, + {"forward . 127.0.0.1\n", false, true, ".", ""}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s\n}\n", false, true, ".", ""}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s no_rec\n}\n", false, false, ".", ""}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s no_rec domain example.org\n}\n", false, false, "example.org.", ""}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain example.org\n}\n", false, true, "example.org.", ""}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain .\n}\n", false, true, ".", ""}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain example.org.\n}\n", false, true, "example.org.", ""}, // negative - {"forward . 127.0.0.1 {\nhealth_check no_rec\n}\n", true, true, "time: invalid duration"}, - {"forward . 127.0.0.1 {\nhealth_check 0.5s rec\n}\n", true, true, "health_check: unknown option rec"}, + {"forward . 127.0.0.1 {\nhealth_check no_rec\n}\n", true, true, ".", "time: invalid duration"}, + {"forward . 127.0.0.1 {\nhealth_check domain example.org\n}\n", true, true, "example.org", "time: invalid duration"}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s rec\n}\n", true, true, ".", "health_check: unknown option rec"}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain\n}\n", true, true, ".", "Wrong argument count or unexpected line ending after 'domain'"}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain example..org\n}\n", true, true, ".", "health_check: invalid domain name"}, } for i, test := range tests { c := caddy.NewTestController("dns", test.input) - f, err := parseForward(c) + fs, err := parseForward(c) if test.shouldErr && err == nil { t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input) @@ -246,13 +285,221 @@ func TestSetupHealthCheck(t *testing.T) { if !test.shouldErr { t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err) } + if !strings.Contains(err.Error(), test.expectedErr) { + t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input) + } + } + + if test.shouldErr { + continue + } + f := fs[0] + if f.opts.HCRecursionDesired != test.expectedRecVal || f.proxies[0].GetHealthchecker().GetRecursionDesired() != test.expectedRecVal || + f.opts.HCDomain != test.expectedDomain || f.proxies[0].GetHealthchecker().GetDomain() != test.expectedDomain || !dns.IsFqdn(f.proxies[0].GetHealthchecker().GetDomain()) { + t.Errorf("Test %d: expectedRec: %v, got: %v. expectedDomain: %s, got: %s. ", i, test.expectedRecVal, f.opts.HCRecursionDesired, test.expectedDomain, f.opts.HCDomain) + } + } +} + +func TestMultiForward(t *testing.T) { + input := ` + forward 1st.example.org 10.0.0.1 + forward 2nd.example.org 10.0.0.2 + forward 3rd.example.org 10.0.0.3 + ` + + c := caddy.NewTestController("dns", input) + setup(c) + dnsserver.NewServer("", []*dnsserver.Config{dnsserver.GetConfig(c)}) + + handlers := dnsserver.GetConfig(c).Handlers() + f1, ok := handlers[0].(*Forward) + if !ok { + t.Fatalf("expected first plugin to be Forward, got %v", reflect.TypeOf(handlers[0])) + } + + if f1.from != "1st.example.org." { + t.Errorf("expected first forward from \"1st.example.org.\", got %q", f1.from) + } + if f1.Next == nil { + t.Fatal("expected first forward to point to next forward instance, not nil") + } + + f2, ok := f1.Next.(*Forward) + if !ok { + t.Fatalf("expected second plugin to be Forward, got %v", reflect.TypeOf(f1.Next)) + } + if f2.from != "2nd.example.org." { + t.Errorf("expected second forward from \"2nd.example.org.\", got %q", f2.from) + } + if f2.Next == nil { + t.Fatal("expected second forward to point to third forward instance, got nil") + } + + f3, ok := f2.Next.(*Forward) + if !ok { + t.Fatalf("expected third plugin to be Forward, got %v", reflect.TypeOf(f2.Next)) + } + if f3.from != "3rd.example.org." { + t.Errorf("expected third forward from \"3rd.example.org.\", got %q", f3.from) + } + if f3.Next != nil { + t.Error("expected third plugin to be last, but Next is not nil") + } +} +func TestNextAlternate(t *testing.T) { + testsValid := []struct { + input string + expected []int + }{ + {"forward . 127.0.0.1 {\nnext NXDOMAIN\n}\n", []int{dns.RcodeNameError}}, + {"forward . 127.0.0.1 {\nnext SERVFAIL\n}\n", []int{dns.RcodeServerFailure}}, + {"forward . 127.0.0.1 {\nnext NXDOMAIN SERVFAIL\n}\n", []int{dns.RcodeNameError, dns.RcodeServerFailure}}, + {"forward . 127.0.0.1 {\nnext NXDOMAIN SERVFAIL REFUSED\n}\n", []int{dns.RcodeNameError, dns.RcodeServerFailure, dns.RcodeRefused}}, + } + for i, test := range testsValid { + c := caddy.NewTestController("dns", test.input) + f, err := parseForward(c) + forward := f[0] + if err != nil { + t.Errorf("Test %d: %v", i, err) + } + if len(forward.nextAlternateRcodes) != len(test.expected) { + t.Errorf("Test %d: expected %d next rcodes, got %d", i, len(test.expected), len(forward.nextAlternateRcodes)) + } + for j, rcode := range forward.nextAlternateRcodes { + if rcode != test.expected[j] { + t.Errorf("Test %d: expected next rcode %d, got %d", i, test.expected[j], rcode) + } + } + } + + testsInvalid := []string{ + "forward . 127.0.0.1 {\nnext\n}\n", + "forward . 127.0.0.1 {\nnext INVALID\n}\n", + "forward . 127.0.0.1 {\nnext NXDOMAIN INVALID\n}\n", + } + for i, test := range testsInvalid { + c := caddy.NewTestController("dns", test) + _, err := parseForward(c) + if err == nil { + t.Errorf("Test %d: expected error, got nil", i) + } + } +} + +func TestFailfastAllUnhealthyUpstreams(t *testing.T) { + tests := []struct { + input string + expectedRecVal bool + expectedErr string + }{ + // positive + {"forward . 127.0.0.1\n", false, ""}, + {"forward . 127.0.0.1 {\nfailfast_all_unhealthy_upstreams\n}\n", true, ""}, + // negative + {"forward . 127.0.0.1 {\nfailfast_all_unhealthy_upstreams false\n}\n", false, "Wrong argument count"}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + fs, err := parseForward(c) + + if err != nil { + if test.expectedErr == "" { + t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err) + } if !strings.Contains(err.Error(), test.expectedErr) { t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input) } + } else { + if test.expectedErr != "" { + t.Errorf("Test %d: expected error but found no error for input %s", i, test.input) + } + } + + if test.expectedErr != "" { + continue } - if !test.shouldErr && (f.opts.hcRecursionDesired != test.expectedVal || f.proxies[0].health.GetRecursionDesired() != test.expectedVal) { - t.Errorf("Test %d: expected: %t, got: %d", i, test.expectedVal, f.maxConcurrent) + + f := fs[0] + if f.failfastUnhealthyUpstreams != test.expectedRecVal { + t.Errorf("Test %d: Expected Rec:%v, got:%v", i, test.expectedRecVal, f.failfastUnhealthyUpstreams) + } + } +} + +func TestFailover(t *testing.T) { + server_fail_s := dnstest.NewMultipleServer(func(w dns.ResponseWriter, r *dns.Msg) { + ret := new(dns.Msg) + ret.SetRcode(r, dns.RcodeServerFailure) + w.WriteMsg(ret) + }) + defer server_fail_s.Close() + + server_refused_s := dnstest.NewMultipleServer(func(w dns.ResponseWriter, r *dns.Msg) { + ret := new(dns.Msg) + ret.SetRcode(r, dns.RcodeRefused) + w.WriteMsg(ret) + }) + defer server_refused_s.Close() + + s := dnstest.NewMultipleServer(func(w dns.ResponseWriter, r *dns.Msg) { + ret := new(dns.Msg) + ret.SetReply(r) + ret.Answer = append(ret.Answer, test.A("example.org. IN A 127.0.0.1")) + w.WriteMsg(ret) + }) + defer s.Close() + + tests := []struct { + input string + hasRecord bool + failMsg string + }{ + {fmt.Sprintf( + `forward . %s %s %s { + policy sequential + failover ServFail Refused + }`, server_fail_s.Addr, server_refused_s.Addr, s.Addr), true, "If failover is set, records should be returned as long as one of the upstreams is work"}, + {fmt.Sprintf( + `forward . %s %s %s { + policy sequential + }`, server_fail_s.Addr, server_refused_s.Addr, s.Addr), false, "If failover is not set and the first upstream is not work, no records should be returned"}, + {fmt.Sprintf( + `forward . %s %s %s { + policy sequential + }`, s.Addr, server_fail_s.Addr, server_refused_s.Addr), true, "Although failover is not set, as long as the first upstream is work, there should be has a record return"}, + } + + for _, testCase := range tests { + c := caddy.NewTestController("dns", testCase.input) + fs, err := parseForward(c) + + f := fs[0] + if err != nil { + t.Errorf("Failed to create forwarder: %s", err) + } + f.OnStartup() + defer f.OnShutdown() + + // Reduce per-upstream read timeout to make the test fit within the + // per-query deadline defaultTimeout of 5 seconds. + for _, p := range f.proxies { + p.SetReadTimeout(500 * time.Millisecond) + } + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + if _, err := f.ServeDNS(context.TODO(), rec, m); err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + + if (len(rec.Msg.Answer) > 0) != testCase.hasRecord { + t.Errorf(" %s: \n %s", testCase.failMsg, testCase.input) } } } diff --git a/plugin/geoip/README.md b/plugin/geoip/README.md new file mode 100644 index 0000000000..febad8ad75 --- /dev/null +++ b/plugin/geoip/README.md @@ -0,0 +1,117 @@ +# geoip + +## Name + +*geoip* - Lookup maxmind geoip2 databases using the client IP, then add associated geoip data to the context request. + +## Description + +The *geoip* plugin add geo location data associated with the client IP, it allows you to configure a [geoIP2 maxmind database](https://dev.maxmind.com/geoip/docs/databases) to add the geo location data associated with the IP address. + +The data is added leveraging the *metadata* plugin, values can then be retrieved using it as well, for example: + +```go +import ( + "strconv" + "github.com/coredns/coredns/plugin/metadata" +) +// ... +if getLongitude := metadata.ValueFunc(ctx, "geoip/longitude"); getLongitude != nil { + if longitude, err := strconv.ParseFloat(getLongitude(), 64); err == nil { + // Do something useful with longitude. + } +} else { + // The metadata label geoip/longitude for some reason, was not set. +} +// ... +``` + +## Databases + +The supported databases use city schema such as `City` and `Enterprise`. Other databases types with different schemas are not supported yet. + +You can download a [free and public City database](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data). + +## Syntax + +```text +geoip [DBFILE] +``` + +or + +```text +geoip [DBFILE] { + [edns-subnet] +} +``` + +* **DBFILE** the mmdb database file path. We recommend updating your mmdb database periodically for more accurate results. +* `edns-subnet`: Optional. Use [EDNS0 subnet](https://en.wikipedia.org/wiki/EDNS_Client_Subnet) (if present) for Geo IP instead of the source IP of the DNS request. This helps identifying the closest source IP address through intermediary DNS resolvers, and it also makes GeoIP testing easy: `dig +subnet=1.2.3.4 @dns-server.example.com www.geo-aware.com`. + + **NOTE:** due to security reasons, recursive DNS resolvers may mask a few bits off of the clients' IP address, which can cause inaccuracies in GeoIP resolution. + + There is no defined mask size in the standards, but there are examples: [RFC 7871's example](https://datatracker.ietf.org/doc/html/rfc7871#section-13) conceals the last 72 bits of an IPv6 source address, and NS1 Help Center [mentions](https://help.ns1.com/hc/en-us/articles/360020256573-About-the-EDNS-Client-Subnet-ECS-DNS-extension) that ECS-enabled DNS resolvers send only the first three octets (eg. /24) of the source IPv4 address. + +## Examples + +The following configuration configures the `City` database, and looks up geolocation based on EDNS0 subnet if present. + +```txt +. { + geoip /opt/geoip2/db/GeoLite2-City.mmdb { + edns-subnet + } + metadata # Note that metadata plugin must be enabled as well. +} +``` + +The *view* plugin can use *geoip* metadata as selection criteria to provide GSLB functionality. +In this example, clients from the city "Exampleshire" will receive answers for `example.com` from the zone defined in +`example.com.exampleshire-db`. All other clients will receive answers from the zone defined in `example.com.db`. +Note that the order of the two `example.com` server blocks below is important; the default viewless server block +must be last. + +```txt +example.com { + view exampleshire { + expr metadata('geoip/city/name') == 'Exampleshire' + } + geoip /opt/geoip2/db/GeoLite2-City.mmdb + metadata + file example.com.exampleshire-db +} + +example.com { + file example.com.db +} +``` + +## Metadata Labels + +A limited set of fields will be exported as labels, all values are stored using strings **regardless of their underlying value type**, and therefore you may have to convert it back to its original type, note that numeric values are always represented in base 10. + +| Label | Type | Example | Description +| :----------------------------------- | :-------- | :-------------- | :------------------ +| `geoip/city/name` | `string` | `Cambridge` | Then city name in English language. +| `geoip/country/code` | `string` | `GB` | Country [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) code. +| `geoip/country/name` | `string` | `United Kingdom` | The country name in English language. +| `geoip/country/is_in_european_union` | `bool` | `false` | Either `true` or `false`. +| `geoip/continent/code` | `string` | `EU` | See [Continent codes](#ContinentCodes). +| `geoip/continent/name` | `string` | `Europe` | The continent name in English language. +| `geoip/latitude` | `float64` | `52.2242` | Base 10, max available precision. +| `geoip/longitude` | `float64` | `0.1315` | Base 10, max available precision. +| `geoip/timezone` | `string` | `Europe/London` | The timezone. +| `geoip/postalcode` | `string` | `CB4` | The postal code. + +## Continent Codes + +| Value | Continent (EN) | +| :---- | :------------- | +| AF | Africa | +| AN | Antarctica | +| AS | Asia | +| EU | Europe | +| NA | North America | +| OC | Oceania | +| SA | South America | diff --git a/plugin/geoip/city.go b/plugin/geoip/city.go new file mode 100644 index 0000000000..2e5d9f7eaa --- /dev/null +++ b/plugin/geoip/city.go @@ -0,0 +1,58 @@ +package geoip + +import ( + "context" + "strconv" + + "github.com/coredns/coredns/plugin/metadata" + + "github.com/oschwald/geoip2-golang" +) + +const defaultLang = "en" + +func (g GeoIP) setCityMetadata(ctx context.Context, data *geoip2.City) { + // Set labels for city, country and continent names. + cityName := data.City.Names[defaultLang] + metadata.SetValueFunc(ctx, pluginName+"/city/name", func() string { + return cityName + }) + countryName := data.Country.Names[defaultLang] + metadata.SetValueFunc(ctx, pluginName+"/country/name", func() string { + return countryName + }) + continentName := data.Continent.Names[defaultLang] + metadata.SetValueFunc(ctx, pluginName+"/continent/name", func() string { + return continentName + }) + + countryCode := data.Country.IsoCode + metadata.SetValueFunc(ctx, pluginName+"/country/code", func() string { + return countryCode + }) + isInEurope := strconv.FormatBool(data.Country.IsInEuropeanUnion) + metadata.SetValueFunc(ctx, pluginName+"/country/is_in_european_union", func() string { + return isInEurope + }) + continentCode := data.Continent.Code + metadata.SetValueFunc(ctx, pluginName+"/continent/code", func() string { + return continentCode + }) + + latitude := strconv.FormatFloat(data.Location.Latitude, 'f', -1, 64) + metadata.SetValueFunc(ctx, pluginName+"/latitude", func() string { + return latitude + }) + longitude := strconv.FormatFloat(data.Location.Longitude, 'f', -1, 64) + metadata.SetValueFunc(ctx, pluginName+"/longitude", func() string { + return longitude + }) + timeZone := data.Location.TimeZone + metadata.SetValueFunc(ctx, pluginName+"/timezone", func() string { + return timeZone + }) + postalCode := data.Postal.Code + metadata.SetValueFunc(ctx, pluginName+"/postalcode", func() string { + return postalCode + }) +} diff --git a/plugin/geoip/geoip.go b/plugin/geoip/geoip.go new file mode 100644 index 0000000000..765ac05c00 --- /dev/null +++ b/plugin/geoip/geoip.go @@ -0,0 +1,107 @@ +// Package geoip implements a max mind database plugin. +package geoip + +import ( + "context" + "fmt" + "net" + "path/filepath" + + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + "github.com/oschwald/geoip2-golang" +) + +var log = clog.NewWithPlugin(pluginName) + +// GeoIP is a plugin that add geo location data to the request context by looking up a maxmind +// geoIP2 database, and which data can be later consumed by other middlewares. +type GeoIP struct { + Next plugin.Handler + db db + edns0 bool +} + +type db struct { + *geoip2.Reader + // provides defines the schemas that can be obtained by querying this database, by using + // bitwise operations. + provides int +} + +const ( + city = 1 << iota +) + +var probingIP = net.ParseIP("127.0.0.1") + +func newGeoIP(dbPath string, edns0 bool) (*GeoIP, error) { + reader, err := geoip2.Open(dbPath) + if err != nil { + return nil, fmt.Errorf("failed to open database file: %v", err) + } + db := db{Reader: reader} + schemas := []struct { + provides int + name string + validate func() error + }{ + {name: "city", provides: city, validate: func() error { _, err := reader.City(probingIP); return err }}, + } + // Query the database to figure out the database type. + for _, schema := range schemas { + if err := schema.validate(); err != nil { + // If we get an InvalidMethodError then we know this database does not provide that schema. + if _, ok := err.(geoip2.InvalidMethodError); !ok { + return nil, fmt.Errorf("unexpected failure looking up database %q schema %q: %v", filepath.Base(dbPath), schema.name, err) + } + } else { + db.provides |= schema.provides + } + } + + if db.provides&city == 0 { + return nil, fmt.Errorf("database does not provide city schema") + } + + return &GeoIP{db: db, edns0: edns0}, nil +} + +// ServeDNS implements the plugin.Handler interface. +func (g GeoIP) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + return plugin.NextOrFailure(pluginName, g.Next, ctx, w, r) +} + +// Metadata implements the metadata.Provider Interface in the metadata plugin, and is used to store +// the data associated with the source IP of every request. +func (g GeoIP) Metadata(ctx context.Context, state request.Request) context.Context { + srcIP := net.ParseIP(state.IP()) + + if g.edns0 { + if o := state.Req.IsEdns0(); o != nil { + for _, s := range o.Option { + if e, ok := s.(*dns.EDNS0_SUBNET); ok { + srcIP = e.Address + break + } + } + } + } + + switch g.db.provides & city { + case city: + data, err := g.db.City(srcIP) + if err != nil { + log.Debugf("Setting up metadata failed due to database lookup error: %v", err) + return ctx + } + g.setCityMetadata(ctx, data) + } + return ctx +} + +// Name implements the Handler interface. +func (g GeoIP) Name() string { return pluginName } diff --git a/plugin/geoip/geoip_test.go b/plugin/geoip/geoip_test.go new file mode 100644 index 0000000000..7f12be9d12 --- /dev/null +++ b/plugin/geoip/geoip_test.go @@ -0,0 +1,91 @@ +package geoip + +import ( + "context" + "fmt" + "net" + "testing" + + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +func TestMetadata(t *testing.T) { + tests := []struct { + label string + expectedValue string + }{ + {"geoip/city/name", "Cambridge"}, + + {"geoip/country/code", "GB"}, + {"geoip/country/name", "United Kingdom"}, + // is_in_european_union is set to true only to work around bool zero value, and test is really being set. + {"geoip/country/is_in_european_union", "true"}, + + {"geoip/continent/code", "EU"}, + {"geoip/continent/name", "Europe"}, + + {"geoip/latitude", "52.2242"}, + {"geoip/longitude", "0.1315"}, + {"geoip/timezone", "Europe/London"}, + {"geoip/postalcode", "CB4"}, + } + + knownIPAddr := "81.2.69.142" // This IP should be part of the CDIR address range used to create the database fixtures. + for _, tc := range tests { + t.Run(fmt.Sprintf("%s/%s", tc.label, "direct"), func(t *testing.T) { + geoIP, err := newGeoIP(cityDBPath, false) + if err != nil { + t.Fatalf("unable to create geoIP plugin: %v", err) + } + state := request.Request{ + Req: new(dns.Msg), + W: &test.ResponseWriter{RemoteIP: knownIPAddr}, + } + testMetadata(t, state, geoIP, tc.label, tc.expectedValue) + }) + + t.Run(fmt.Sprintf("%s/%s", tc.label, "subnet"), func(t *testing.T) { + geoIP, err := newGeoIP(cityDBPath, true) + if err != nil { + t.Fatalf("unable to create geoIP plugin: %v", err) + } + state := request.Request{ + Req: new(dns.Msg), + W: &test.ResponseWriter{RemoteIP: "127.0.0.1"}, + } + state.Req.SetEdns0(4096, false) + if o := state.Req.IsEdns0(); o != nil { + addr := net.ParseIP(knownIPAddr) + o.Option = append(o.Option, (&dns.EDNS0_SUBNET{ + SourceNetmask: 32, + Address: addr, + })) + } + testMetadata(t, state, geoIP, tc.label, tc.expectedValue) + }) + } +} + +func testMetadata(t *testing.T, state request.Request, geoIP *GeoIP, label, expectedValue string) { + t.Helper() + ctx := metadata.ContextWithMetadata(context.Background()) + rCtx := geoIP.Metadata(ctx, state) + if fmt.Sprintf("%p", ctx) != fmt.Sprintf("%p", rCtx) { + t.Errorf("returned context is expected to be the same one passed in the Metadata function") + } + + fn := metadata.ValueFunc(ctx, label) + if fn == nil { + t.Errorf("label %q not set in metadata plugin context", label) + return + } + value := fn() + if value != expectedValue { + t.Errorf("expected value for label %q should be %q, got %q instead", + label, expectedValue, value) + } +} diff --git a/plugin/geoip/setup.go b/plugin/geoip/setup.go new file mode 100644 index 0000000000..7f6e16f3ee --- /dev/null +++ b/plugin/geoip/setup.go @@ -0,0 +1,57 @@ +package geoip + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +const pluginName = "geoip" + +func init() { plugin.Register(pluginName, setup) } + +func setup(c *caddy.Controller) error { + geoip, err := geoipParse(c) + if err != nil { + return plugin.Error(pluginName, err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + geoip.Next = next + return geoip + }) + + return nil +} + +func geoipParse(c *caddy.Controller) (*GeoIP, error) { + var dbPath string + var edns0 bool + + for c.Next() { + if !c.NextArg() { + return nil, c.ArgErr() + } + if dbPath != "" { + return nil, c.Errf("configuring multiple databases is not supported") + } + dbPath = c.Val() + // There shouldn't be any more arguments. + if len(c.RemainingArgs()) != 0 { + return nil, c.ArgErr() + } + + for c.NextBlock() { + if c.Val() != "edns-subnet" { + return nil, c.Errf("unknown property %q", c.Val()) + } + edns0 = true + } + } + + geoIP, err := newGeoIP(dbPath, edns0) + if err != nil { + return geoIP, c.Err(err.Error()) + } + return geoIP, nil +} diff --git a/plugin/geoip/setup_test.go b/plugin/geoip/setup_test.go new file mode 100644 index 0000000000..b9b0030ee3 --- /dev/null +++ b/plugin/geoip/setup_test.go @@ -0,0 +1,110 @@ +package geoip + +import ( + "fmt" + "net" + "path/filepath" + "strings" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" +) + +var ( + fixturesDir = "./testdata" + cityDBPath = filepath.Join(fixturesDir, "GeoLite2-City.mmdb") + unknownDBPath = filepath.Join(fixturesDir, "GeoLite2-UnknownDbType.mmdb") +) + +func TestProbingIP(t *testing.T) { + if probingIP == nil { + t.Fatalf("Invalid probing IP: %q", probingIP) + } +} + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", fmt.Sprintf("%s %s", pluginName, cityDBPath)) + plugins := dnsserver.GetConfig(c).Plugin + if len(plugins) != 0 { + t.Fatalf("Expected zero plugins after setup, %d found", len(plugins)) + } + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + plugins = dnsserver.GetConfig(c).Plugin + if len(plugins) != 1 { + t.Fatalf("Expected one plugin after setup, %d found", len(plugins)) + } +} + +func TestGeoIPParse(t *testing.T) { + c := caddy.NewTestController("dns", fmt.Sprintf("%s %s", pluginName, cityDBPath)) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + tests := []struct { + shouldErr bool + config string + expectedErr string + expectedDBType int + }{ + // Valid + {false, fmt.Sprintf("%s %s\n", pluginName, cityDBPath), "", city}, + {false, fmt.Sprintf("%s %s { edns-subnet }", pluginName, cityDBPath), "", city}, + + // Invalid + {true, pluginName, "Wrong argument count", 0}, + {true, fmt.Sprintf("%s %s {\n\tlanguages en fr es zh-CN\n}\n", pluginName, cityDBPath), "unknown property \"languages\"", 0}, + {true, fmt.Sprintf("%s %s\n%s %s\n", pluginName, cityDBPath, pluginName, cityDBPath), "configuring multiple databases is not supported", 0}, + {true, fmt.Sprintf("%s 1 2 3", pluginName), "Wrong argument count", 0}, + {true, fmt.Sprintf("%s { }", pluginName), "Error during parsing", 0}, + {true, fmt.Sprintf("%s /dbpath { city }", pluginName), "unknown property \"city\"", 0}, + {true, fmt.Sprintf("%s /invalidPath\n", pluginName), "failed to open database file: open /invalidPath: no such file or directory", 0}, + {true, fmt.Sprintf("%s %s\n", pluginName, unknownDBPath), "reader does not support the \"UnknownDbType\" database type", 0}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.config) + geoIP, err := geoipParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: expected error but found none for input %s", i, test.config) + } + + if err != nil { + if !test.shouldErr { + t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.config, err) + } + + if !strings.Contains(err.Error(), test.expectedErr) { + t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.config) + } + continue + } + + if geoIP.db.Reader == nil { + t.Errorf("Test %d: after parsing database reader should be initialized", i) + } + + if geoIP.db.provides&test.expectedDBType == 0 { + t.Errorf("Test %d: expected db type %d not found, database file provides %d", i, test.expectedDBType, geoIP.db.provides) + } + } + + // Set nil probingIP to test unexpected validate error() + defer func(ip net.IP) { probingIP = ip }(probingIP) + probingIP = nil + + c = caddy.NewTestController("dns", fmt.Sprintf("%s %s\n", pluginName, cityDBPath)) + _, err := geoipParse(c) + if err != nil { + expectedErr := "unexpected failure looking up database" + if !strings.Contains(err.Error(), expectedErr) { + t.Errorf("expected error to contain: %s", expectedErr) + } + } else { + t.Errorf("with a nil probingIP test is expected to fail") + } +} diff --git a/plugin/geoip/testdata/GeoLite2-City.mmdb b/plugin/geoip/testdata/GeoLite2-City.mmdb new file mode 100644 index 0000000000..cd79ed914f Binary files /dev/null and b/plugin/geoip/testdata/GeoLite2-City.mmdb differ diff --git a/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb b/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb new file mode 100644 index 0000000000..23efbf3962 Binary files /dev/null and b/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb differ diff --git a/plugin/geoip/testdata/README.md b/plugin/geoip/testdata/README.md new file mode 100644 index 0000000000..2f6f884c9a --- /dev/null +++ b/plugin/geoip/testdata/README.md @@ -0,0 +1,112 @@ +# testdata +This directory contains mmdb database files used during the testing of this plugin. + +# Create mmdb database files +If you need to change them to add a new value, or field the best is to recreate them, the code snipped used to create them initially is provided next. + +```golang +package main + +import ( + "log" + "net" + "os" + + "github.com/maxmind/mmdbwriter" + "github.com/maxmind/mmdbwriter/inserter" + "github.com/maxmind/mmdbwriter/mmdbtype" +) + +const cdir = "81.2.69.142/32" + +// Create new mmdb database fixtures in this directory. +func main() { + createCityDB("GeoLite2-City.mmdb", "DBIP-City-Lite") + // Create unkwnon database type. + createCityDB("GeoLite2-UnknownDbType.mmdb", "UnknownDbType") +} + +func createCityDB(dbName, dbType string) { + // Load a database writer. + writer, err := mmdbwriter.New(mmdbwriter.Options{DatabaseType: dbType}) + if err != nil { + log.Fatal(err) + } + + // Define and insert the new data. + _, ip, err := net.ParseCIDR(cdir) + if err != nil { + log.Fatal(err) + } + + // TODO(snebel29): Find an alternative location in Europe Union. + record := mmdbtype.Map{ + "city": mmdbtype.Map{ + "geoname_id": mmdbtype.Uint64(2653941), + "names": mmdbtype.Map{ + "en": mmdbtype.String("Cambridge"), + "es": mmdbtype.String("Cambridge"), + }, + }, + "continent": mmdbtype.Map{ + "code": mmdbtype.String("EU"), + "geoname_id": mmdbtype.Uint64(6255148), + "names": mmdbtype.Map{ + "en": mmdbtype.String("Europe"), + "es": mmdbtype.String("Europa"), + }, + }, + "country": mmdbtype.Map{ + "iso_code": mmdbtype.String("GB"), + "geoname_id": mmdbtype.Uint64(2635167), + "names": mmdbtype.Map{ + "en": mmdbtype.String("United Kingdom"), + "es": mmdbtype.String("Reino Unido"), + }, + "is_in_european_union": mmdbtype.Bool(true), + }, + "location": mmdbtype.Map{ + "accuracy_radius": mmdbtype.Uint16(200), + "latitude": mmdbtype.Float64(52.2242), + "longitude": mmdbtype.Float64(0.1315), + "metro_code": mmdbtype.Uint64(0), + "time_zone": mmdbtype.String("Europe/London"), + }, + "postal": mmdbtype.Map{ + "code": mmdbtype.String("CB4"), + }, + "registered_country": mmdbtype.Map{ + "iso_code": mmdbtype.String("GB"), + "geoname_id": mmdbtype.Uint64(2635167), + "names": mmdbtype.Map{"en": mmdbtype.String("United Kingdom")}, + "is_in_european_union": mmdbtype.Bool(false), + }, + "subdivisions": mmdbtype.Slice{ + mmdbtype.Map{ + "iso_code": mmdbtype.String("ENG"), + "geoname_id": mmdbtype.Uint64(6269131), + "names": mmdbtype.Map{"en": mmdbtype.String("England")}, + }, + mmdbtype.Map{ + "iso_code": mmdbtype.String("CAM"), + "geoname_id": mmdbtype.Uint64(2653940), + "names": mmdbtype.Map{"en": mmdbtype.String("Cambridgeshire")}, + }, + }, + } + + if err := writer.InsertFunc(ip, inserter.TopLevelMergeWith(record)); err != nil { + log.Fatal(err) + } + + // Write the DB to the filesystem. + fh, err := os.Create(dbName) + if err != nil { + log.Fatal(err) + } + _, err = writer.WriteTo(fh) + if err != nil { + log.Fatal(err) + } +} +``` diff --git a/plugin/grpc/README.md b/plugin/grpc/README.md index 5e6148da9c..d4ee7e0066 100644 --- a/plugin/grpc/README.md +++ b/plugin/grpc/README.md @@ -33,6 +33,7 @@ grpc FROM TO... { tls CERT KEY CA tls_servername NAME policy random|round_robin|sequential + fallthrough [ZONES...] } ~~~ @@ -54,6 +55,12 @@ grpc FROM TO... { but they have to use the same `tls_servername`. E.g. mixing 9.9.9.9 (QuadDNS) with 1.1.1.1 (Cloudflare) will not work. * `policy` specifies the policy to use for selecting upstream servers. The default is `random`. +* `fallthrough` **[ZONES...]** If a query results in NXDOMAIN from the gRPC backend, pass the request + to the next plugin instead of returning the NXDOMAIN response. This is useful when the gRPC backend + is authoritative for a zone but should not return authoritative NXDOMAIN responses for queries that + don't actually belong to that zone (e.g., search path queries). If **[ZONES...]** is omitted, then + fallthrough happens for all zones. If specific zones are listed, then only queries for those zones + will be subject to fallthrough. Also note the TLS config is "global" for the whole grpc proxy if you need a different `tls-name` for different upstreams you're out of luck. @@ -129,7 +136,26 @@ Or with multiple upstreams from the same provider } ~~~ +Forward requests to a local upstream listening on a Unix domain socket. + +~~~ corefile +. { + grpc . unix:///path/to/grpc.sock +} +~~~ + +Proxy requests for `example.org.` to a gRPC backend, but fallthrough to the next plugin for NXDOMAIN responses to handle search path queries correctly. + +~~~ corefile +example.org { + grpc . 127.0.0.1:9005 { + fallthrough + } + forward . 8.8.8.8 +} +~~~ + ## Bugs The TLS config is global for the whole grpc proxy if you need a different `tls_servername` for -different upstreams you're out of luck. +different upstreams you're out of luck. \ No newline at end of file diff --git a/plugin/grpc/fuzz.go b/plugin/grpc/fuzz.go new file mode 100644 index 0000000000..df3a917ff0 --- /dev/null +++ b/plugin/grpc/fuzz.go @@ -0,0 +1,216 @@ +//go:build gofuzz + +package grpc + +import ( + "context" + + "github.com/coredns/coredns/pb" + "github.com/coredns/coredns/plugin/pkg/fuzz" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" + grpcgo "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// fakeClient implements pb.DnsServiceClient without doing any network I/O. +// Its behavior is controlled by the mode field. +type fakeClient struct { + mode byte + idx int +} + +func (f *fakeClient) Query(_ context.Context, in *pb.DnsPacket, _ ...grpcgo.CallOption) (*pb.DnsPacket, error) { + // Derive mode deterministically from request bytes to vary behavior per call. + m := f.mode + if len(in.GetMsg()) > 0 { + b := in.GetMsg()[f.idx%len(in.GetMsg())] + f.idx++ + m = b + } + + switch m % 12 { + case 0: + // Success echo: return the same bytes. + return &pb.DnsPacket{Msg: in.GetMsg()}, nil + case 1: + // Return NotFound to exercise NXDOMAIN conversion and optional fallthrough. + return nil, status.Error(codes.NotFound, "not found") + case 2: + // Return a transient error to trigger retry/rotation. + return nil, status.Error(codes.Unavailable, "unavailable") + case 3: + // Corrupt response that fails dns.Msg Unpack. + return &pb.DnsPacket{Msg: []byte{0x00, 0x01, 0x02}}, nil + case 4: + // Valid DNS message with mismatched ID/qname to trigger formerr path in ServeDNS. + var req dns.Msg + if err := req.Unpack(in.GetMsg()); err != nil { + // If input isn't a DNS message, just echo to avoid blocking fuzzing. + return &pb.DnsPacket{Msg: in.GetMsg()}, nil + } + resp := new(dns.Msg) + resp.SetReply(&req) + resp.Id = req.Id + 1 + // Alter question name if present. + if len(req.Question) > 0 { + resp.Question[0].Name = "example.net." + } + packed, err := resp.Pack() + if err != nil { + return &pb.DnsPacket{Msg: in.GetMsg()}, nil + } + return &pb.DnsPacket{Msg: packed}, nil + case 5: + // Success with EDNS and larger answer to stress flags and sizes. + var req dns.Msg + if err := req.Unpack(in.GetMsg()); err != nil { + return &pb.DnsPacket{Msg: in.GetMsg()}, nil + } + resp := new(dns.Msg) + resp.SetReply(&req) + // Set EDNS0 with varying UDP size and DO bit based on m. + size := uint16(512) + if (m>>1)&1 == 1 { + size = 1232 + } + if (m>>2)&1 == 1 { + size = 4096 + } + do := ((m>>3)&1 == 1) + resp.SetEdns0(size, do) + // Optionally set TC bit to exercise truncation handling. + if (m>>4)&1 == 1 { + resp.Truncated = true + } + // Add a few TXT records to grow the payload. + name := "." + if len(req.Question) > 0 { + name = req.Question[0].Name + } + n := int(1 + (m % 16)) + for range n { + resp.Answer = append(resp.Answer, &dns.TXT{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0}, Txt: []string{"aaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbb"}}) + } + packed, err := resp.Pack() + if err != nil { + return &pb.DnsPacket{Msg: in.GetMsg()}, nil + } + return &pb.DnsPacket{Msg: packed}, nil + case 6: + return nil, status.Error(codes.DeadlineExceeded, "timeout") + case 7: + return nil, status.Error(codes.Internal, "internal") + case 8: + return nil, status.Error(codes.ResourceExhausted, "quota") + case 9: + return nil, status.Error(codes.PermissionDenied, "denied") + case 10: + // NODATA: NOERROR with empty Answer and SOA in Authority. + var req dns.Msg + if err := req.Unpack(in.GetMsg()); err != nil { + return &pb.DnsPacket{Msg: in.GetMsg()}, nil + } + resp := new(dns.Msg) + resp.SetRcode(&req, dns.RcodeSuccess) + name := "." + if len(req.Question) > 0 { + name = req.Question[0].Name + } + resp.Ns = append(resp.Ns, &dns.SOA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 60}, Ns: "ns.example.", Mbox: "hostmaster.example.", Serial: 1, Refresh: 3600, Retry: 600, Expire: 86400, Minttl: 60}) + packed, err := resp.Pack() + if err != nil { + return &pb.DnsPacket{Msg: in.GetMsg()}, nil + } + return &pb.DnsPacket{Msg: packed}, nil + case 11: + // TC-only: truncated response without answers. + var req dns.Msg + if err := req.Unpack(in.GetMsg()); err != nil { + return &pb.DnsPacket{Msg: in.GetMsg()}, nil + } + resp := new(dns.Msg) + resp.SetReply(&req) + resp.Truncated = true + packed, err := resp.Pack() + if err != nil { + return &pb.DnsPacket{Msg: in.GetMsg()}, nil + } + return &pb.DnsPacket{Msg: packed}, nil + default: + // Empty/zero-length response to exercise unpack error path. + return &pb.DnsPacket{Msg: nil}, nil + } +} + +// Fuzz exercises the grpc plugin using a fake client and the shared fuzz harness. +func Fuzz(data []byte) int { + if len(data) == 0 { + return 0 + } + + cfg := data[0] + rest := data[1:] + + g := &GRPC{ + from: ".", + Next: test.ErrorHandler(), + } + + // Select policy based on cfg bits to vary list() ordering. + switch cfg % 3 { + case 0: + g.p = &random{} + case 1: + g.p = &roundRobin{} + default: + g.p = &sequential{} + } + + // Optionally enable fallthrough; choose scope based on input bit. + if cfg&0x80 != 0 { + if cfg&0x01 != 0 { + g.Fall.SetZonesFromArgs([]string{"."}) + } else { + g.Fall.SetZonesFromArgs([]string{g.from}) + } + } + + // Create 0–3 fake proxies with varied behaviors. + numProxies := int((cfg >> 4) & 0x03) + if numProxies == 0 { + if _, is := g.p.(*roundRobin); is { + // Avoid divide-by-zero in roundRobin policy when pool is empty. + g.p = &sequential{} + } + } + for i := range numProxies { + mode := byte(i) + if len(rest) > 0 { + mode = rest[i%len(rest)] + } + p := &Proxy{addr: "fake"} + p.client = &fakeClient{mode: mode} + g.proxies = append(g.proxies, p) + } + + // Deterministically set a narrow from to miss match and hit Next/SERVFAIL paths. + if cfg&0x20 != 0 { + g.from = "_not_matching_." + } + + // Optionally construct a tiny deterministic query to vary RD/CD flags. + if cfg&0x08 != 0 { + var rq dns.Msg + rq.SetQuestion("example.org.", dns.TypeA) + rq.RecursionDesired = (cfg&0x04 != 0) + rq.CheckingDisabled = (cfg&0x02 != 0) + if packed, err := rq.Pack(); err == nil { + rest = packed + } + } + + return fuzz.Do(g, rest) +} diff --git a/plugin/grpc/grpc.go b/plugin/grpc/grpc.go index 2ecf29597e..31c3f7de89 100644 --- a/plugin/grpc/grpc.go +++ b/plugin/grpc/grpc.go @@ -8,6 +8,7 @@ import ( "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/debug" + "github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/request" "github.com/miekg/dns" @@ -26,6 +27,7 @@ type GRPC struct { tlsConfig *tls.Config tlsServerName string + Fall fall.F Next plugin.Handler } @@ -33,14 +35,18 @@ type GRPC struct { func (g *GRPC) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { state := request.Request{W: w, Req: r} if !g.match(state) { - return plugin.NextOrFailure(g.Name(), g.Next, ctx, w, r) + if g.Next != nil { + return plugin.NextOrFailure(g.Name(), g.Next, ctx, w, r) + } + // No next plugin, return SERVFAIL + return dns.RcodeServerFailure, nil } var ( - span, child ot.Span - ret *dns.Msg - upstreamErr, err error - i int + span ot.Span + ret *dns.Msg + err error + i int ) span = ot.SpanFromContext(ctx) list := g.list() @@ -59,22 +65,25 @@ func (g *GRPC) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ( proxy := list[i] i++ + callCtx := ctx + var child ot.Span if span != nil { - child = span.Tracer().StartSpan("query", ot.ChildOf(span.Context())) - ctx = ot.ContextWithSpan(ctx, child) + child, callCtx = ot.StartSpanFromContext(callCtx, "query") } - ret, err = proxy.query(ctx, r) - if err != nil { - // Continue with the next proxy - continue - } + var cancel context.CancelFunc + callCtx, cancel = context.WithDeadline(callCtx, deadline) + + ret, err = proxy.query(callCtx, r) + cancel() if child != nil { child.Finish() } - - upstreamErr = err + if err != nil { + // Continue with the next proxy + continue + } // Check if the reply is correct; if not return FormErr. if !state.Match(ret) { @@ -86,12 +95,31 @@ func (g *GRPC) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ( return 0, nil } + // Check if we should fallthrough on NXDOMAIN responses + if ret.Rcode == dns.RcodeNameError && g.Fall.Through(state.Name()) { + if g.Next != nil { + return plugin.NextOrFailure(g.Name(), g.Next, ctx, w, r) + } + // No next plugin to fallthrough to, return the NXDOMAIN response + } + w.WriteMsg(ret) return 0, nil } - if upstreamErr != nil { - return dns.RcodeServerFailure, upstreamErr + // SERVFAIL if all healthy proxys returned errors. + if err != nil { + // If fallthrough is enabled, try the next plugin instead of returning SERVFAIL + if g.Fall.Through(state.Name()) && g.Next != nil { + return plugin.NextOrFailure(g.Name(), g.Next, ctx, w, r) + } + // just return the last error received + return dns.RcodeServerFailure, err + } + + // If fallthrough is enabled, try the next plugin instead of returning SERVFAIL + if g.Fall.Through(state.Name()) && g.Next != nil { + return plugin.NextOrFailure(g.Name(), g.Next, ctx, w, r) } return dns.RcodeServerFailure, ErrNoHealthy diff --git a/plugin/grpc/grpc_test.go b/plugin/grpc/grpc_test.go index 06375ec5e5..f0ea12efbb 100644 --- a/plugin/grpc/grpc_test.go +++ b/plugin/grpc/grpc_test.go @@ -3,13 +3,19 @@ package grpc import ( "context" "errors" + "strings" "testing" + "time" "github.com/coredns/coredns/pb" "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/plugin/test" "github.com/miekg/dns" + ot "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/mocktracer" + grpcgo "google.golang.org/grpc" ) func TestGRPC(t *testing.T) { @@ -73,3 +79,103 @@ func TestGRPC(t *testing.T) { }) } } + +// Test that fallthrough works correctly when there's no next plugin +func TestGRPCFallthroughNoNext(t *testing.T) { + g := newGRPC() // Use the constructor to properly initialize + g.Fall = fall.Root // Enable fallthrough for all zones + g.Next = nil // No next plugin + g.from = "." + + // Create a test request + r := new(dns.Msg) + r.SetQuestion("test.example.org.", dns.TypeA) + + w := &test.ResponseWriter{} + + // Should return SERVFAIL since no backends are configured and no next plugin + rcode, err := g.ServeDNS(context.Background(), w, r) + + // Should not return the "no next plugin found" error + if err != nil && strings.Contains(err.Error(), "no next plugin found") { + t.Errorf("Expected no 'no next plugin found' error, got: %v", err) + } + + // Should return SERVFAIL + if rcode != dns.RcodeServerFailure { + t.Errorf("Expected SERVFAIL when no backends and no next plugin, got: %d", rcode) + } +} + +// deadlineCheckingClient records whether a deadline was attached to ctx. +type deadlineCheckingClient struct { + sawDeadline bool + lastDeadline time.Time + dnsPacket *pb.DnsPacket + err error +} + +func (c *deadlineCheckingClient) Query(ctx context.Context, in *pb.DnsPacket, opts ...grpcgo.CallOption) (*pb.DnsPacket, error) { + if dl, ok := ctx.Deadline(); ok { + c.sawDeadline = true + c.lastDeadline = dl + } + return c.dnsPacket, c.err +} + +// Test that on error paths we still finish child spans, and that we set a per-call deadline. +func TestGRPC_SpansOnErrorPath(t *testing.T) { + m := &dns.Msg{} + msgBytes, err := m.Pack() + if err != nil { + t.Fatalf("Error packing response: %s", err) + } + dnsPacket := &pb.DnsPacket{Msg: msgBytes} + + // Proxy 1: returns error, we should still finish its child span and have a deadline + p1 := &deadlineCheckingClient{dnsPacket: nil, err: errors.New("kaboom")} + // Proxy 2: returns success + p2 := &deadlineCheckingClient{dnsPacket: dnsPacket, err: nil} + + g := newGRPC() + g.from = "." + g.proxies = []*Proxy{{client: p1}, {client: p2}} + + // Ensure deterministic order of the retries: try p1 then p2 + g.p = new(sequential) + + // Set a parent span in context so ServeDNS creates child spans per attempt + tracer := mocktracer.New() + prev := ot.GlobalTracer() + ot.SetGlobalTracer(tracer) + defer ot.SetGlobalTracer(prev) + + parent := tracer.StartSpan("parent") + ctx := ot.ContextWithSpan(t.Context(), parent) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + if _, err := g.ServeDNS(ctx, rec, m); err != nil { + t.Fatalf("ServeDNS returned error: %v", err) + } + + // Assert both attempts finished child spans with retries + // (2 query spans: error + success) + finished := tracer.FinishedSpans() + var finishedQueries int + for _, s := range finished { + if s.OperationName == "query" { + finishedQueries++ + } + } + if finishedQueries != 2 { + t.Fatalf("expected 2 finished 'query' spans, got %d (finished: %v)", finishedQueries, finished) + } + + // Assert we set a deadline on the call contexts + if !p1.sawDeadline { + t.Fatalf("expected deadline to be set on first proxy call context") + } + if !p2.sawDeadline { + t.Fatalf("expected deadline to be set on second proxy call context") + } +} diff --git a/plugin/grpc/metrics.go b/plugin/grpc/metrics.go index 2857042cd6..0e8760acf8 100644 --- a/plugin/grpc/metrics.go +++ b/plugin/grpc/metrics.go @@ -22,10 +22,11 @@ var ( Help: "Counter of requests made per upstream.", }, []string{"rcode", "to"}) RequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: plugin.Namespace, - Subsystem: "grpc", - Name: "request_duration_seconds", - Buckets: plugin.TimeBuckets, - Help: "Histogram of the time each request took.", + Namespace: plugin.Namespace, + Subsystem: "grpc", + Name: "request_duration_seconds", + Buckets: plugin.TimeBuckets, + NativeHistogramBucketFactor: plugin.NativeHistogramBucketFactor, + Help: "Histogram of the time each request took.", }, []string{"to"}) ) diff --git a/plugin/grpc/policy.go b/plugin/grpc/policy.go index 66351d8224..0caf629456 100644 --- a/plugin/grpc/policy.go +++ b/plugin/grpc/policy.go @@ -1,8 +1,10 @@ package grpc import ( - "math/rand" "sync/atomic" + "time" + + "github.com/coredns/coredns/plugin/pkg/rand" ) // Policy defines a policy we use for selecting upstreams. @@ -18,16 +20,18 @@ func (r *random) String() string { return "random" } func (r *random) List(p []*Proxy) []*Proxy { switch len(p) { + case 0: + return nil case 1: return p case 2: - if rand.Int()%2 == 0 { + if rn.Int()%2 == 0 { return []*Proxy{p[1], p[0]} // swap } return p } - perms := rand.Perm(len(p)) + perms := rn.Perm(len(p)) rnd := make([]*Proxy, len(p)) for i, p1 := range perms { @@ -44,6 +48,9 @@ type roundRobin struct { func (r *roundRobin) String() string { return "round_robin" } func (r *roundRobin) List(p []*Proxy) []*Proxy { + if len(p) == 0 { + return nil + } poolLen := uint32(len(p)) i := atomic.AddUint32(&r.robin, 1) % poolLen @@ -62,3 +69,5 @@ func (r *sequential) String() string { return "sequential" } func (r *sequential) List(p []*Proxy) []*Proxy { return p } + +var rn = rand.New(time.Now().UnixNano()) diff --git a/plugin/grpc/policy_test.go b/plugin/grpc/policy_test.go new file mode 100644 index 0000000000..5cd2d6ff31 --- /dev/null +++ b/plugin/grpc/policy_test.go @@ -0,0 +1,128 @@ +package grpc + +import ( + "testing" +) + +func TestRoundRobinEmpty(t *testing.T) { + t.Parallel() + + r := &roundRobin{} + got := r.List(nil) + if len(got) != 0 { + t.Fatalf("expected length 0, got %d", len(got)) + } +} + +func TestRandomEmpty(t *testing.T) { + t.Parallel() + + r := &random{} + got := r.List(nil) + if len(got) != 0 { + t.Fatalf("expected length 0, got %d", len(got)) + } +} + +func TestSequentialEmpty(t *testing.T) { + t.Parallel() + + r := &sequential{} + got := r.List(nil) + if len(got) != 0 { + t.Fatalf("expected length 0, got %d", len(got)) + } +} + +func TestPoliciesOrdering(t *testing.T) { + t.Parallel() + + p0 := &Proxy{addr: "p0"} + p1 := &Proxy{addr: "p1"} + p2 := &Proxy{addr: "p2"} + in := []*Proxy{p0, p1, p2} + + t.Run("sequential keeps order", func(t *testing.T) { + t.Parallel() + + r := &sequential{} + got := r.List(in) + if len(got) != len(in) { + t.Fatalf("expected length %d, got %d", len(in), len(got)) + } + for i := range in { + if got[i] != in[i] { + t.Fatalf("sequential order changed at %d: want %p, got %p", i, in[i], got[i]) + } + } + }) + + t.Run("round robin advances and permutation", func(t *testing.T) { + t.Parallel() + + r := &roundRobin{} + + got1 := r.List(in) + if !isPermutation(in, got1) { + t.Fatalf("first call: expected permutation of input") + } + if got1[0] != p1 { + t.Fatalf("first element should advance to p1, got %p", got1[0]) + } + + got2 := r.List(in) + if !isPermutation(in, got2) { + t.Fatalf("second call: expected permutation of input") + } + if got2[0] != p2 { + t.Fatalf("first element should advance to p2 on second call, got %p", got2[0]) + } + + got3 := r.List(in) + if !isPermutation(in, got3) { + t.Fatalf("third call: expected permutation of input") + } + if got3[0] != p0 { + t.Fatalf("first element should wrap to p0 on third call, got %p", got3[0]) + } + }) + + t.Run("random is a permutation", func(t *testing.T) { + t.Parallel() + + r := &random{} + got := r.List(in) + if !isPermutation(in, got) { + t.Fatalf("random did not return a permutation of input") + } + }) + + t.Run("random with two proxies", func(t *testing.T) { + t.Parallel() + + r := &random{} + in2 := []*Proxy{p0, p1} + got := r.List(in2) + if !isPermutation(in2, got) { + t.Fatalf("random did not return a permutation of input") + } + }) +} + +// Helper: returns true if b is a permutation of a (same multiset of pointers). +func isPermutation(a, b []*Proxy) bool { + if len(a) != len(b) { + return false + } + count := make(map[*Proxy]int, len(a)) + for _, p := range a { + count[p]++ + } + for _, p := range b { + count[p]-- + if count[p] < 0 { + return false + } + } + return true +} diff --git a/plugin/grpc/proxy.go b/plugin/grpc/proxy.go index f2bee95c0e..fc06a5a463 100644 --- a/plugin/grpc/proxy.go +++ b/plugin/grpc/proxy.go @@ -3,6 +3,8 @@ package grpc import ( "context" "crypto/tls" + "errors" + "fmt" "strconv" "time" @@ -12,9 +14,24 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" ) +const ( + // maxDNSMessageBytes is the maximum size of a DNS message on the wire. + maxDNSMessageBytes = dns.MaxMsgSize + + // maxProtobufPayloadBytes accounts for protobuf overhead. + // Field tag=1 (1 byte) + length varint for 65535 (3 bytes) = 4 bytes total + maxProtobufPayloadBytes = maxDNSMessageBytes + 4 +) + +var ( + // ErrDNSMessageTooLarge is returned when a DNS message exceeds the maximum allowed size. + ErrDNSMessageTooLarge = errors.New("dns message exceeds size limit") +) + // Proxy defines an upstream host. type Proxy struct { addr string @@ -33,10 +50,19 @@ func newProxy(addr string, tlsConfig *tls.Config) (*Proxy, error) { if tlsConfig != nil { p.dialOpts = append(p.dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) } else { - p.dialOpts = append(p.dialOpts, grpc.WithInsecure()) + p.dialOpts = append(p.dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) } - conn, err := grpc.Dial(p.addr, p.dialOpts...) + // Cap send/recv sizes to avoid oversized messages. + // Note: gRPC size limits apply to the serialized protobuf message size. + p.dialOpts = append(p.dialOpts, + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(maxProtobufPayloadBytes), + grpc.MaxCallSendMsgSize(maxProtobufPayloadBytes), + ), + ) + + conn, err := grpc.NewClient(p.addr, p.dialOpts...) if err != nil { return nil, err } @@ -54,6 +80,10 @@ func (p *Proxy) query(ctx context.Context, req *dns.Msg) (*dns.Msg, error) { return nil, err } + if err := validateDNSSize(msg); err != nil { + return nil, err + } + reply, err := p.client.Query(ctx, &pb.DnsPacket{Msg: msg}) if err != nil { // if not found message, return empty message with NXDomain code @@ -63,8 +93,14 @@ func (p *Proxy) query(ctx context.Context, req *dns.Msg) (*dns.Msg, error) { } return nil, err } + wire := reply.GetMsg() + + if err := validateDNSSize(wire); err != nil { + return nil, err + } + ret := new(dns.Msg) - if err := ret.Unpack(reply.Msg); err != nil { + if err := ret.Unpack(wire); err != nil { return nil, err } @@ -79,3 +115,11 @@ func (p *Proxy) query(ctx context.Context, req *dns.Msg) (*dns.Msg, error) { return ret, nil } + +func validateDNSSize(data []byte) error { + l := len(data) + if l > maxDNSMessageBytes { + return fmt.Errorf("%w: %d bytes (limit %d)", ErrDNSMessageTooLarge, l, maxDNSMessageBytes) + } + return nil +} diff --git a/plugin/grpc/proxy_test.go b/plugin/grpc/proxy_test.go index cc4ebec829..b5c92f8e8b 100644 --- a/plugin/grpc/proxy_test.go +++ b/plugin/grpc/proxy_test.go @@ -3,9 +3,15 @@ package grpc import ( "context" "errors" + "net" + "path" + "slices" "testing" + "github.com/coredns/caddy" "github.com/coredns/coredns/pb" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" "github.com/miekg/dns" "google.golang.org/grpc" @@ -56,6 +62,33 @@ func TestProxy(t *testing.T) { } } +func TestProxy_RejectsOversizedReply(t *testing.T) { + p := &Proxy{} + oversized := make([]byte, maxDNSMessageBytes+1) + p.client = testServiceClient{dnsPacket: &pb.DnsPacket{Msg: oversized}, err: nil} + _, err := p.query(context.TODO(), new(dns.Msg)) + if !errors.Is(err, ErrDNSMessageTooLarge) { + t.Fatalf("expected %v, got %v", ErrDNSMessageTooLarge, err) + } +} + +func TestProxy_RejectsOversizedRequest(t *testing.T) { + p := &Proxy{} + p.client = testServiceClient{dnsPacket: &pb.DnsPacket{Msg: []byte("ok")}, err: nil} + + oversizedMsg := &dns.Msg{} + oversizedMsg.SetQuestion("example.org.", dns.TypeA) + oversizedMsg.Extra = slices.Repeat([]dns.RR{&dns.TXT{ + Hdr: dns.RR_Header{Name: "example.org.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 300}, + Txt: []string{"very long text record to make the message oversized when packed"}, + }}, 2000) + + _, err := p.query(context.TODO(), oversizedMsg) + if !errors.Is(err, ErrDNSMessageTooLarge) { + t.Fatalf("expected %v, got %v", ErrDNSMessageTooLarge, err) + } +} + type testServiceClient struct { dnsPacket *pb.DnsPacket err error @@ -64,3 +97,52 @@ type testServiceClient struct { func (m testServiceClient) Query(ctx context.Context, in *pb.DnsPacket, opts ...grpc.CallOption) (*pb.DnsPacket, error) { return m.dnsPacket, m.err } + +func TestProxyUnix(t *testing.T) { + tdir := t.TempDir() + + fd := path.Join(tdir, "test.grpc") + listener, err := net.Listen("unix", fd) + if err != nil { + t.Fatal("Failed to listen: ", err) + } + defer listener.Close() + + server := grpc.NewServer() + pb.RegisterDnsServiceServer(server, &grpcDnsServiceServer{}) + + go server.Serve(listener) + defer server.Stop() + + c := caddy.NewTestController("dns", "grpc . unix://"+fd) + g, err := parseGRPC(c) + + if err != nil { + t.Errorf("Failed to create forwarder: %s", err) + } + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + if _, err := g.ServeDNS(context.TODO(), rec, m); err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + if x := rec.Msg.Answer[0].Header().Name; x != "example.org." { + t.Errorf("Expected %s, got %s", "example.org.", x) + } +} + +type grpcDnsServiceServer struct { + pb.UnimplementedDnsServiceServer +} + +func (*grpcDnsServiceServer) Query(ctx context.Context, in *pb.DnsPacket) (*pb.DnsPacket, error) { + msg := &dns.Msg{} + msg.Unpack(in.GetMsg()) + answer := new(dns.Msg) + answer.Answer = append(answer.Answer, test.A("example.org. IN A 127.0.0.1")) + answer.SetRcode(msg, dns.RcodeSuccess) + buf, _ := answer.Pack() + return &pb.DnsPacket{Msg: buf}, nil +} diff --git a/plugin/grpc/setup.go b/plugin/grpc/setup.go index 6c29292d37..ab72eb8ba8 100644 --- a/plugin/grpc/setup.go +++ b/plugin/grpc/setup.go @@ -3,6 +3,7 @@ package grpc import ( "crypto/tls" "fmt" + "path/filepath" "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" @@ -56,7 +57,11 @@ func parseStanza(c *caddy.Controller) (*GRPC, error) { if !c.Args(&g.from) { return g, c.ArgErr() } - g.from = plugin.Host(g.from).Normalize() + normalized := plugin.Host(g.from).NormalizeExact() + if len(normalized) == 0 { + return g, fmt.Errorf("unable to normalize '%s'", g.from) + } + g.from = normalized[0] // only the first is used. to := c.RemainingArgs() if len(to) == 0 { @@ -92,23 +97,26 @@ func parseStanza(c *caddy.Controller) (*GRPC, error) { } func parseBlock(c *caddy.Controller, g *GRPC) error { - switch c.Val() { case "except": ignore := c.RemainingArgs() if len(ignore) == 0 { return c.ArgErr() } - for i := 0; i < len(ignore); i++ { - ignore[i] = plugin.Host(ignore[i]).Normalize() + for i := range ignore { + g.ignored = append(g.ignored, plugin.Host(ignore[i]).NormalizeExact()...) } - g.ignored = ignore case "tls": args := c.RemainingArgs() if len(args) > 3 { return c.ArgErr() } + for i := range args { + if !filepath.IsAbs(args[i]) && dnsserver.GetConfig(c).Root != "" { + args[i] = filepath.Join(dnsserver.GetConfig(c).Root, args[i]) + } + } tlsConfig, err := pkgtls.NewTLSConfigFromArgs(args...) if err != nil { return err @@ -133,6 +141,8 @@ func parseBlock(c *caddy.Controller, g *GRPC) error { default: return c.Errf("unknown policy '%s'", x) } + case "fallthrough": + g.Fall.SetZonesFromArgs(c.RemainingArgs()) default: if c.Val() != "}" { return c.Errf("unknown property '%s'", c.Val()) diff --git a/plugin/grpc/setup_test.go b/plugin/grpc/setup_test.go index 48ed94af62..bafd638cfc 100644 --- a/plugin/grpc/setup_test.go +++ b/plugin/grpc/setup_test.go @@ -1,13 +1,13 @@ package grpc import ( - "io/ioutil" "os" "reflect" "strings" "testing" "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/pkg/fall" ) func TestSetup(t *testing.T) { @@ -26,11 +26,13 @@ func TestSetup(t *testing.T) { {"grpc . 127.0.0.1:8080", false, ".", nil, ""}, {"grpc . [::1]:53", false, ".", nil, ""}, {"grpc . [2003::1]:53", false, ".", nil, ""}, + {"grpc . unix:///var/run/g.sock", false, ".", nil, ""}, // negative {"grpc . a27.0.0.1", true, "", nil, "not an IP"}, {"grpc . 127.0.0.1 {\nblaatl\n}\n", true, "", nil, "unknown property"}, {`grpc . ::1 grpc com ::2`, true, "", nil, "plugin"}, + {"grpc xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 127.0.0.1", true, "", nil, "unable to normalize 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'"}, } for i, test := range tests { @@ -105,7 +107,7 @@ tls func TestSetupResolvconf(t *testing.T) { const resolv = "resolv.conf" - if err := ioutil.WriteFile(resolv, + if err := os.WriteFile(resolv, []byte(`nameserver 10.10.255.252 nameserver 10.10.255.253`), 0666); err != nil { t.Fatalf("Failed to write resolv.conf file: %s", err) @@ -151,3 +153,47 @@ nameserver 10.10.255.253`), 0666); err != nil { } } } + +func TestSetupFallthrough(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedFallthrough fall.F + expectedErr string + }{ + // positive cases + {`grpc . 127.0.0.1 { + fallthrough +}`, false, fall.Root, ""}, + {`grpc . 127.0.0.1 { + fallthrough example.org +}`, false, fall.F{Zones: []string{"example.org."}}, ""}, + {`grpc . 127.0.0.1 { + fallthrough example.org example.com +}`, false, fall.F{Zones: []string{"example.org.", "example.com."}}, ""}, + {`grpc . 127.0.0.1`, false, fall.Zero, ""}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + g, err := parseGRPC(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: expected error but found none for input %s", i, test.input) + } + + if err != nil { + if !test.shouldErr { + t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err) + } + + if !strings.Contains(err.Error(), test.expectedErr) { + t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input) + } + } + + if !test.shouldErr && !g.Fall.Equal(test.expectedFallthrough) { + t.Errorf("Test %d: expected fallthrough %+v, got %+v", i, test.expectedFallthrough, g.Fall) + } + } +} diff --git a/plugin/header/README.md b/plugin/header/README.md new file mode 100644 index 0000000000..a27a1b9cb4 --- /dev/null +++ b/plugin/header/README.md @@ -0,0 +1,63 @@ +# header + +## Name + +*header* - modifies the header for queries and responses. + +## Description + +*header* ensures that the flags are in the desired state for queries and responses. +The modifications are made transparently for the client and subsequent plugins. + +## Syntax + +~~~ +header { + SELECTOR ACTION FLAGS... + SELECTOR ACTION FLAGS... +} +~~~ + +* **SELECTOR** defines if the action should be applied on `query` or `response`. + +* **ACTION** defines the state for DNS message header flags. Actions are evaluated in the order they are defined so last one has the + most precedence. Allowed values are: + * `set` + * `clear` +* **FLAGS** are the DNS header flags that will be modified. Current supported flags include: + * `aa` - Authoritative(Answer) + * `ra` - RecursionAvailable + * `rd` - RecursionDesired + +## Examples + +Make sure recursive available `ra` flag is set in all the responses: + +~~~ corefile +. { + header { + response set ra + } +} +~~~ + +Make sure "recursion available" `ra` and "authoritative answer" `aa` flags are set and "recursion desired" is cleared in all responses: + +~~~ corefile +. { + header { + response set ra aa + response clear rd + } +} +~~~ + +Make sure "recursion desired" `rd` is set for all subsequent plugins:: + +~~~ corefile +. { + header { + query set rd + } +} +~~~ diff --git a/plugin/header/handler.go b/plugin/header/handler.go new file mode 100644 index 0000000000..e11eb03c46 --- /dev/null +++ b/plugin/header/handler.go @@ -0,0 +1,27 @@ +package header + +import ( + "context" + + "github.com/coredns/coredns/plugin" + + "github.com/miekg/dns" +) + +// Header modifies flags of dns.MsgHdr in queries and / or responses +type Header struct { + QueryRules []Rule + ResponseRules []Rule + Next plugin.Handler +} + +// ServeDNS implements the plugin.Handler interface. +func (h Header) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + applyRules(r, h.QueryRules) + + wr := ResponseHeaderWriter{ResponseWriter: w, Rules: h.ResponseRules} + return plugin.NextOrFailure(h.Name(), h.Next, ctx, &wr, r) +} + +// Name implements the plugin.Handler interface. +func (h Header) Name() string { return "header" } diff --git a/plugin/header/header.go b/plugin/header/header.go new file mode 100644 index 0000000000..785d7ff087 --- /dev/null +++ b/plugin/header/header.go @@ -0,0 +1,95 @@ +package header + +import ( + "fmt" + "strings" + + clog "github.com/coredns/coredns/plugin/pkg/log" + + "github.com/miekg/dns" +) + +// Supported flags +const ( + authoritative = "aa" + recursionAvailable = "ra" + recursionDesired = "rd" +) + +var log = clog.NewWithPlugin("header") + +// ResponseHeaderWriter is a response writer that allows modifying dns.MsgHdr +type ResponseHeaderWriter struct { + dns.ResponseWriter + Rules []Rule +} + +// WriteMsg implements the dns.ResponseWriter interface. +func (r *ResponseHeaderWriter) WriteMsg(res *dns.Msg) error { + applyRules(res, r.Rules) + return r.ResponseWriter.WriteMsg(res) +} + +// Write implements the dns.ResponseWriter interface. +func (r *ResponseHeaderWriter) Write(buf []byte) (int, error) { + log.Warning("ResponseHeaderWriter called with Write: not ensuring headers") + n, err := r.ResponseWriter.Write(buf) + return n, err +} + +// Rule is used to set/clear Flag in dns.MsgHdr +type Rule struct { + Flag string + State bool +} + +func newRules(key string, args []string) ([]Rule, error) { + if key == "" { + return nil, fmt.Errorf("no flag action provided") + } + + if len(args) < 1 { + return nil, fmt.Errorf("invalid length for flags, at least one should be provided") + } + + var state bool + action := strings.ToLower(key) + switch action { + case "set": + state = true + case "clear": + state = false + default: + return nil, fmt.Errorf("unknown flag action=%s, should be set or clear", action) + } + + rules := make([]Rule, 0, len(args)) + for _, arg := range args { + flag := strings.ToLower(arg) + switch flag { + case authoritative: + case recursionAvailable: + case recursionDesired: + default: + return nil, fmt.Errorf("unknown/unsupported flag=%s", flag) + } + rule := Rule{Flag: flag, State: state} + rules = append(rules, rule) + } + + return rules, nil +} + +func applyRules(res *dns.Msg, rules []Rule) { + // handle all supported flags + for _, rule := range rules { + switch rule.Flag { + case authoritative: + res.Authoritative = rule.State + case recursionAvailable: + res.RecursionAvailable = rule.State + case recursionDesired: + res.RecursionDesired = rule.State + } + } +} diff --git a/plugin/header/header_test.go b/plugin/header/header_test.go new file mode 100644 index 0000000000..118265419c --- /dev/null +++ b/plugin/header/header_test.go @@ -0,0 +1,152 @@ +package header + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestHeaderResponseRules(t *testing.T) { + wr := dnstest.NewRecorder(&test.ResponseWriter{}) + next := plugin.HandlerFunc(func(ctx context.Context, writer dns.ResponseWriter, msg *dns.Msg) (int, error) { + writer.WriteMsg(msg) + return dns.RcodeSuccess, nil + }) + + tests := []struct { + handler plugin.Handler + got func(msg *dns.Msg) bool + expected bool + }{ + { + handler: Header{ + ResponseRules: []Rule{{Flag: recursionAvailable, State: true}}, + Next: next, + }, + got: func(msg *dns.Msg) bool { + return msg.RecursionAvailable + }, + expected: true, + }, + { + handler: Header{ + ResponseRules: []Rule{{Flag: recursionAvailable, State: false}}, + Next: next, + }, + got: func(msg *dns.Msg) bool { + return msg.RecursionAvailable + }, + expected: false, + }, + { + handler: Header{ + ResponseRules: []Rule{{Flag: recursionDesired, State: true}}, + Next: next, + }, + got: func(msg *dns.Msg) bool { + return msg.RecursionDesired + }, + expected: true, + }, + { + handler: Header{ + ResponseRules: []Rule{{Flag: authoritative, State: true}}, + Next: next, + }, + got: func(msg *dns.Msg) bool { + return msg.Authoritative + }, + expected: true, + }, + } + + for i, test := range tests { + m := new(dns.Msg) + + _, err := test.handler.ServeDNS(context.TODO(), wr, m) + if err != nil { + t.Errorf("Test %d: Expected no error, but got %s", i, err) + continue + } + + if test.got(m) != test.expected { + t.Errorf("Test %d: Expected flag state=%t, but got %t", i, test.expected, test.got(m)) + continue + } + } +} + +func TestHeaderQueryRules(t *testing.T) { + wr := dnstest.NewRecorder(&test.ResponseWriter{}) + next := plugin.HandlerFunc(func(ctx context.Context, writer dns.ResponseWriter, msg *dns.Msg) (int, error) { + writer.WriteMsg(msg) + return dns.RcodeSuccess, nil + }) + + tests := []struct { + handler plugin.Handler + got func(msg *dns.Msg) bool + expected bool + }{ + { + handler: Header{ + QueryRules: []Rule{{Flag: recursionAvailable, State: true}}, + Next: next, + }, + got: func(msg *dns.Msg) bool { + return msg.RecursionAvailable + }, + expected: true, + }, + { + handler: Header{ + QueryRules: []Rule{{Flag: recursionDesired, State: true}}, + Next: next, + }, + got: func(msg *dns.Msg) bool { + return msg.RecursionDesired + }, + expected: true, + }, + { + handler: Header{ + QueryRules: []Rule{{Flag: recursionDesired, State: false}}, + Next: next, + }, + got: func(msg *dns.Msg) bool { + return msg.RecursionDesired + }, + expected: false, + }, + { + handler: Header{ + QueryRules: []Rule{{Flag: authoritative, State: true}}, + Next: next, + }, + got: func(msg *dns.Msg) bool { + return msg.Authoritative + }, + expected: true, + }, + } + + for i, tc := range tests { + m := new(dns.Msg) + + _, err := tc.handler.ServeDNS(context.TODO(), wr, m) + if err != nil { + t.Errorf("Test %d: Expected no error, but got %s", i, err) + continue + } + + if tc.got(m) != tc.expected { + t.Errorf("Test %d: Expected flag state=%t, but got %t", i, tc.expected, tc.got(m)) + continue + } + } +} diff --git a/plugin/header/setup.go b/plugin/header/setup.go new file mode 100644 index 0000000000..2a7761bb32 --- /dev/null +++ b/plugin/header/setup.go @@ -0,0 +1,67 @@ +package header + +import ( + "fmt" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("header", setup) } + +func setup(c *caddy.Controller) error { + queryRules, responseRules, err := parse(c) + if err != nil { + return plugin.Error("header", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return Header{ + QueryRules: queryRules, + ResponseRules: responseRules, + Next: next, + } + }) + + return nil +} + +func parse(c *caddy.Controller) ([]Rule, []Rule, error) { + for c.Next() { + var queryRules []Rule + var responseRules []Rule + + for c.NextBlock() { + selector := strings.ToLower(c.Val()) + + var action string + switch selector { + case "query", "response": + if c.NextArg() { + action = c.Val() + } + default: + return nil, nil, fmt.Errorf("setting up rule: invalid selector=%s should be query or response", selector) + } + + args := c.RemainingArgs() + rules, err := newRules(action, args) + if err != nil { + return nil, nil, fmt.Errorf("setting up rule: %w", err) + } + + if selector == "response" { + responseRules = append(responseRules, rules...) + } else { + queryRules = append(queryRules, rules...) + } + } + + if len(queryRules) > 0 || len(responseRules) > 0 { + return queryRules, responseRules, nil + } + } + return nil, nil, c.ArgErr() +} diff --git a/plugin/header/setup_test.go b/plugin/header/setup_test.go new file mode 100644 index 0000000000..48d23335d7 --- /dev/null +++ b/plugin/header/setup_test.go @@ -0,0 +1,59 @@ +package header + +import ( + "strings" + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupHeader(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedErrContent string + }{ + {`header {}`, true, "Wrong argument count or unexpected line ending after"}, + {`header { + foo +}`, true, "invalid selector=foo should be query or response"}, + {`header { + response set +}`, true, "invalid length for flags, at least one should be provided"}, + {`header { + query foo +}`, true, "invalid length for flags, at least one should be provided"}, + {`header { + query foo rd +}`, true, "unknown flag action=foo, should be set or clear"}, + {`header { + query set rd + }`, false, ""}, + {`header { + response set aa + }`, false, ""}, + {`header { + response set ra aa + query clear rd +}`, false, ""}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + err := setup(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found none for input %s", i, test.input) + } + + if err != nil { + if !test.shouldErr { + t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) + } + + if !strings.Contains(err.Error(), test.expectedErrContent) { + t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input) + } + } + } +} diff --git a/plugin/health/README.md b/plugin/health/README.md index 1247cb0882..b18d2ec3cb 100644 --- a/plugin/health/README.md +++ b/plugin/health/README.md @@ -27,7 +27,7 @@ health [ADDRESS] { ~~~ * Where `lameduck` will delay shutdown for **DURATION**. /health will still answer 200 OK. - Note: The *ready* plugin will not answer OK while CoreDNS is in lameduck mode prior to shutdown. + Note: The *ready* plugin will not answer OK while CoreDNS is in lame duck mode prior to shutdown. If you have multiple Server Blocks, *health* can only be enabled in one of them (as it is process wide). If you really need multiple endpoints, you must run health endpoints on different ports: @@ -48,13 +48,15 @@ Doing this is supported but both endpoints ":8080" and ":8081" will export the e ## Metrics -If monitoring is enabled (via the *prometheus* plugin) then the following metric is exported: +If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported: - * `coredns_health_request_duration_seconds{}` - duration to process a HTTP query to the local - `/health` endpoint. As this a local operation it should be fast. A (large) increase in this + * `coredns_health_request_duration_seconds{}` - The *health* plugin performs a self health check + once per second on the `/health` endpoint. This metric is the duration to process that request. + As this is a local operation it should be fast. A (large) increase in this duration indicates the CoreDNS process is having trouble keeping up with its query load. + * `coredns_health_request_failures_total{}` - The number of times the self health check failed. -Note that this metric *does not* have a `server` label, because being overloaded is a symptom of +Note that these metrics *do not* have a `server` label, because being overloaded is a symptom of the running process, *not* a specific server. ## Examples @@ -67,7 +69,7 @@ Run another health endpoint on http://localhost:8091. } ~~~ -Set a lameduck duration of 1 second: +Set a lame duck duration of 1 second: ~~~ corefile . { diff --git a/plugin/health/health.go b/plugin/health/health.go index b5b4b95a26..980cf2bc88 100644 --- a/plugin/health/health.go +++ b/plugin/health/health.go @@ -2,9 +2,11 @@ package health import ( + "context" "io" "net" "net/http" + "net/url" "time" clog "github.com/coredns/coredns/plugin/pkg/log" @@ -15,21 +17,34 @@ var log = clog.NewWithPlugin("health") // Health implements healthchecks by exporting a HTTP endpoint. type health struct { - Addr string - lameduck time.Duration + Addr string + lameduck time.Duration + healthURI *url.URL ln net.Listener nlSetup bool mux *http.ServeMux - stop chan bool + stop context.CancelFunc } func (h *health) OnStartup() error { if h.Addr == "" { h.Addr = ":8080" } - h.stop = make(chan bool) + + var err error + h.healthURI, err = url.Parse("http://" + h.Addr) + if err != nil { + return err + } + + h.healthURI.Path = "/health" + if h.healthURI.Host == "" { + // while we can listen on multiple network interfaces, we need to pick one to poll + h.healthURI.Host = "localhost" + } + ln, err := reuseport.Listen("tcp", h.Addr) if err != nil { return err @@ -39,14 +54,17 @@ func (h *health) OnStartup() error { h.mux = http.NewServeMux() h.nlSetup = true - h.mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + h.mux.HandleFunc(h.healthURI.Path, func(w http.ResponseWriter, r *http.Request) { // We're always healthy. w.WriteHeader(http.StatusOK) io.WriteString(w, http.StatusText(http.StatusOK)) }) + ctx := context.Background() + ctx, h.stop = context.WithCancel(ctx) + go func() { http.Serve(h.ln, h.mux) }() - go func() { h.overloaded() }() + go func() { h.overloaded(ctx) }() return nil } @@ -61,9 +79,21 @@ func (h *health) OnFinalShutdown() error { time.Sleep(h.lameduck) } + h.stop() + h.ln.Close() + h.nlSetup = false + return nil +} +func (h *health) OnReload() error { + if !h.nlSetup { + return nil + } + + h.stop() + + h.ln.Close() h.nlSetup = false - close(h.stop) return nil } diff --git a/plugin/health/health_test.go b/plugin/health/health_test.go index 35ab285fee..c49a5d73de 100644 --- a/plugin/health/health_test.go +++ b/plugin/health/health_test.go @@ -2,14 +2,14 @@ package health import ( "fmt" - "io/ioutil" + "io" "net/http" "testing" "time" ) func TestHealth(t *testing.T) { - h := &health{Addr: ":0", stop: make(chan bool)} + h := &health{Addr: ":0"} if err := h.OnStartup(); err != nil { t.Fatalf("Unable to startup the health server: %v", err) @@ -22,10 +22,10 @@ func TestHealth(t *testing.T) { if err != nil { t.Fatalf("Unable to query %s: %v", address, err) } - if response.StatusCode != 200 { + if response.StatusCode != http.StatusOK { t.Errorf("Invalid status code: expecting '200', got '%d'", response.StatusCode) } - content, err := ioutil.ReadAll(response.Body) + content, err := io.ReadAll(response.Body) if err != nil { t.Fatalf("Unable to get response body from %s: %v", address, err) } @@ -37,7 +37,7 @@ func TestHealth(t *testing.T) { } func TestHealthLameduck(t *testing.T) { - h := &health{Addr: ":0", stop: make(chan bool), lameduck: 250 * time.Millisecond} + h := &health{Addr: ":0", lameduck: 250 * time.Millisecond} if err := h.OnStartup(); err != nil { t.Fatalf("Unable to startup the health server: %v", err) diff --git a/plugin/health/overloaded.go b/plugin/health/overloaded.go index 05f4ec39d7..f8b3256bf6 100644 --- a/plugin/health/overloaded.go +++ b/plugin/health/overloaded.go @@ -1,6 +1,8 @@ package health import ( + "context" + "net" "net/http" "time" @@ -11,12 +13,26 @@ import ( ) // overloaded queries the health end point and updates a metrics showing how long it took. -func (h *health) overloaded() { - timeout := time.Duration(5 * time.Second) +func (h *health) overloaded(ctx context.Context) { + bypassProxy := &http.Transport{ + Proxy: nil, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + timeout := 3 * time.Second client := http.Client{ - Timeout: timeout, + Timeout: timeout, + Transport: bypassProxy, } - url := "http://" + h.Addr + "/health" + + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, h.healthURI.String(), nil) tick := time.NewTicker(1 * time.Second) defer tick.Stop() @@ -24,15 +40,25 @@ func (h *health) overloaded() { select { case <-tick.C: start := time.Now() - resp, err := client.Get(url) + resp, err := client.Do(req) + if err != nil && ctx.Err() == context.Canceled { + // request was cancelled by parent goroutine + return + } if err != nil { - HealthDuration.Observe(timeout.Seconds()) + HealthDuration.Observe(time.Since(start).Seconds()) + HealthFailures.Inc() + log.Warningf("Local health request to %q failed: %s", req.URL.String(), err) continue } resp.Body.Close() - HealthDuration.Observe(time.Since(start).Seconds()) + elapsed := time.Since(start) + HealthDuration.Observe(elapsed.Seconds()) + if elapsed > time.Second { // 1s is pretty random, but a *local* scrape taking that long isn't good + log.Warningf("Local health request to %q took more than 1s: %s", req.URL.String(), elapsed) + } - case <-h.stop: + case <-ctx.Done(): return } } @@ -41,10 +67,18 @@ func (h *health) overloaded() { var ( // HealthDuration is the metric used for exporting how fast we can retrieve the /health endpoint. HealthDuration = promauto.NewHistogram(prometheus.HistogramOpts{ + Namespace: plugin.Namespace, + Subsystem: "health", + Name: "request_duration_seconds", + Buckets: plugin.SlimTimeBuckets, + NativeHistogramBucketFactor: plugin.NativeHistogramBucketFactor, + Help: "Histogram of the time (in seconds) each request took.", + }) + // HealthFailures is the metric used to count how many times the health request failed + HealthFailures = promauto.NewCounter(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "health", - Name: "request_duration_seconds", - Buckets: plugin.TimeBuckets, - Help: "Histogram of the time (in seconds) each request took.", + Name: "request_failures_total", + Help: "The number of times the health check failed.", }) ) diff --git a/plugin/health/overloaded_test.go b/plugin/health/overloaded_test.go new file mode 100644 index 0000000000..da40a4e7b3 --- /dev/null +++ b/plugin/health/overloaded_test.go @@ -0,0 +1,49 @@ +package health + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" +) + +func Test_health_overloaded_cancellation(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(1 * time.Second) + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + h := &health{ + Addr: ts.URL, + stop: cancel, + } + + var err error + h.healthURI, err = url.Parse(ts.URL) + if err != nil { + t.Fatal(err) + } + h.healthURI.Path = "/health" + + stopped := make(chan struct{}) + go func() { + h.overloaded(ctx) + stopped <- struct{}{} + }() + + // wait for overloaded function to start atleast once + time.Sleep(1 * time.Second) + + cancel() + + select { + case <-stopped: + case <-time.After(5 * time.Second): + t.Fatal("overloaded function should have been cancelled") + } +} diff --git a/plugin/health/setup.go b/plugin/health/setup.go index 9cabeb7e6d..e9163ad441 100644 --- a/plugin/health/setup.go +++ b/plugin/health/setup.go @@ -17,10 +17,10 @@ func setup(c *caddy.Controller) error { return plugin.Error("health", err) } - h := &health{Addr: addr, stop: make(chan bool), lameduck: lame} + h := &health{Addr: addr, lameduck: lame} c.OnStartup(h.OnStartup) - c.OnRestart(h.OnFinalShutdown) + c.OnRestart(h.OnReload) c.OnFinalShutdown(h.OnFinalShutdown) c.OnRestartFailed(h.OnStartup) diff --git a/plugin/hosts/hostsfile.go b/plugin/hosts/hostsfile.go index 924c827da8..84e451029e 100644 --- a/plugin/hosts/hostsfile.go +++ b/plugin/hosts/hostsfile.go @@ -45,7 +45,7 @@ func newOptions() *options { return &options{ autoReverse: true, ttl: 3600, - reload: time.Duration(5 * time.Second), + reload: 5 * time.Second, } } @@ -117,11 +117,14 @@ func (h *Hostsfile) readHosts() { defer file.Close() stat, err := file.Stat() + if err != nil { + return + } h.RLock() size := h.size h.RUnlock() - if err == nil && h.mtime.Equal(stat.ModTime()) && size == stat.Size() { + if h.mtime.Equal(stat.ModTime()) && size == stat.Size() { return } @@ -135,7 +138,7 @@ func (h *Hostsfile) readHosts() { h.mtime = stat.ModTime() h.size = stat.Size() - hostsEntries.WithLabelValues().Set(float64(h.inline.Len() + h.hmap.Len())) + hostsEntries.WithLabelValues(h.path).Set(float64(h.inline.Len() + h.hmap.Len())) hostsReloadTime.Set(float64(stat.ModTime().UnixNano()) / 1e9) h.Unlock() } @@ -168,7 +171,7 @@ func (h *Hostsfile) parse(r io.Reader) *Map { continue } - family := 0 + var family int if addr.To4() != nil { family = 1 } else { diff --git a/plugin/hosts/hostsfile_test.go b/plugin/hosts/hostsfile_test.go index 626b8918d4..fb558ebd29 100644 --- a/plugin/hosts/hostsfile_test.go +++ b/plugin/hosts/hostsfile_test.go @@ -45,16 +45,13 @@ var ( singlelinehosts = `127.0.0.2 odin` ipv4hosts = `# See https://tools.ietf.org/html/rfc1123. # - # The literal IPv4 address parser in the net package is a relaxed - # one. It may accept a literal IPv4 address in dotted-decimal notation - # with leading zeros such as "001.2.003.4". # internet address and host name 127.0.0.1 localhost # inline comment separated by tab - 127.000.000.002 localhost # inline comment separated by space + 127.0.0.2 localhost # inline comment separated by space # internet address, host name and aliases - 127.000.000.003 localhost localhost.localdomain` + 127.0.0.3 localhost localhost.localdomain` ipv6hosts = `# See https://tools.ietf.org/html/rfc5952, https://tools.ietf.org/html/rfc4007. # internet address and host name @@ -123,6 +120,7 @@ func TestLookupStaticHost(t *testing.T) { } func testStaticHost(t *testing.T, ent staticHostEntry, h *Hostsfile) { + t.Helper() ins := []string{ent.in, plugin.Name(ent.in).Normalize(), strings.ToLower(ent.in), strings.ToUpper(ent.in)} for k, in := range ins { addrsV4 := h.LookupStaticHostV4(in) @@ -209,6 +207,7 @@ func TestLookupStaticAddr(t *testing.T) { } func testStaticAddr(t *testing.T, ent staticIPEntry, h *Hostsfile) { + t.Helper() hosts := h.LookupStaticAddr(ent.in) for i := range ent.out { ent.out[i] = plugin.Name(ent.out[i]).Normalize() diff --git a/plugin/hosts/metrics.go b/plugin/hosts/metrics.go index f97497bf21..d3999e1e9f 100644 --- a/plugin/hosts/metrics.go +++ b/plugin/hosts/metrics.go @@ -14,7 +14,7 @@ var ( Subsystem: "hosts", Name: "entries", Help: "The combined number of entries in hosts and Corefile.", - }, []string{}) + }, []string{"hostsfile"}) // hostsReloadTime is the timestamp of the last reload of hosts file. hostsReloadTime = promauto.NewGauge(prometheus.GaugeOpts{ Namespace: plugin.Namespace, diff --git a/plugin/hosts/setup.go b/plugin/hosts/setup.go index f79b2446cd..0201e58247 100644 --- a/plugin/hosts/setup.go +++ b/plugin/hosts/setup.go @@ -26,6 +26,7 @@ func periodicHostsUpdate(h *Hosts) chan bool { go func() { ticker := time.NewTicker(h.options.reload) + defer ticker.Stop() for { select { case <-parseChan: @@ -95,27 +96,17 @@ func hostsParse(c *caddy.Controller) (Hosts, error) { } s, err := os.Stat(h.path) if err != nil { - if os.IsNotExist(err) { - log.Warningf("File does not exist: %s", h.path) - } else { + if !os.IsNotExist(err) { return h, c.Errf("unable to access hosts file '%s': %v", h.path, err) } + log.Warningf("File does not exist: %s", h.path) } if s != nil && s.IsDir() { log.Warningf("Hosts file %q is a directory", h.path) } } - origins := make([]string, len(c.ServerBlockKeys)) - copy(origins, c.ServerBlockKeys) - if len(args) > 0 { - origins = args - } - - for i := range origins { - origins[i] = plugin.Host(origins[i]).Normalize() - } - h.Origins = origins + h.Origins = plugin.OriginsFromArgsOrServerBlock(args, c.ServerBlockKeys) for c.NextBlock() { switch c.Val() { diff --git a/plugin/import/README.md b/plugin/import/README.md index aaaaa1b2ae..a7ab690a29 100644 --- a/plugin/import/README.md +++ b/plugin/import/README.md @@ -21,6 +21,8 @@ import PATTERN * **PATTERN** is the file, glob pattern (`*`) or snippet to include. Its contents will replace this line, as if that file's contents appeared here to begin with. +Corefile may contain at most 10000 import statements. A glob pattern counts as a single import. The limit protects the configuration from recursive imports. + ## Files You can use *import* to include a file or files. This file's location is relative to the diff --git a/plugin/k8s_external/README.md b/plugin/k8s_external/README.md index 0faaca3cf5..893a131584 100644 --- a/plugin/k8s_external/README.md +++ b/plugin/k8s_external/README.md @@ -2,16 +2,15 @@ ## Name -*k8s_external* - resolves load balancer and external IPs from outside Kubernetes clusters. +*k8s_external* - resolves load balancer, external IPs from outside Kubernetes clusters and if enabled headless services. ## Description This plugin allows an additional zone to resolve the external IP address(es) of a Kubernetes -service. This plugin is only useful if the *kubernetes* plugin is also loaded. +service and headless services. This plugin is only useful if the *kubernetes* plugin is also loaded. The plugin uses an external zone to resolve in-cluster IP addresses. It only handles queries for A, -AAAA and SRV records; all others result in NODATA responses. To make it a proper DNS zone, it handles -SOA and NS queries for the apex of the zone. +AAAA, SRV, and PTR records; To make it a proper DNS zone, it handles SOA and NS queries for the apex of the zone. By default the apex of the zone will look like the following (assuming the zone used is `example.org`): @@ -57,6 +56,24 @@ k8s_external [ZONE...] { * **APEX** is the name (DNS label) to use for the apex records; it defaults to `dns`. * `ttl` allows you to set a custom **TTL** for responses. The default is 5 (seconds). +If you want to enable headless service resolution, you can do so by adding `headless` option. + +~~~ +k8s_external [ZONE...] { + headless +} +~~~ + +* if there is a headless service with external IPs set, external IPs will be resolved + +If the queried domain does not exist, you can fall through to next plugin by adding the `fallthrough` option. + +~~~ +k8s_external [ZONE...] { + fallthrough [ZONE...] +} +~~~ + ## Examples Enable names under `example.org` to be resolved to in-cluster DNS addresses. @@ -83,12 +100,33 @@ spec: type: ClusterIP ~~~ +The *k8s_external* plugin can be used in conjunction with the *transfer* plugin to enable +zone transfers. Notifies are not supported. + + ~~~ + . { + transfer example.org { + to * + } + kubernetes cluster.local + k8s_external example.org + } + ~~~ + +With the `fallthrough` option, if the queried domain does not exist, it will be passed to the next plugin that matches the zone. + +~~~ +. { + kubernetes cluster.local + k8s_external example.org { + fallthrough + } + forward . 8.8.8.8 +} +~~~ # See Also For some background see [resolve external IP address](https://github.com/kubernetes/dns/issues/242). And [A records for services with Load Balancer IP](https://github.com/coredns/coredns/issues/1851). -# Bugs - -PTR queries for the reverse zone is not supported. diff --git a/plugin/k8s_external/apex.go b/plugin/k8s_external/apex.go index 85edbea6cd..e575e5ea32 100644 --- a/plugin/k8s_external/apex.go +++ b/plugin/k8s_external/apex.go @@ -11,13 +11,14 @@ import ( func (e *External) serveApex(state request.Request) (int, error) { m := new(dns.Msg) m.SetReply(state.Req) + m.Authoritative = true switch state.QType() { case dns.TypeSOA: m.Answer = []dns.RR{e.soa(state)} case dns.TypeNS: m.Answer = []dns.RR{e.ns(state)} - addr := e.externalAddrFunc(state) + addr := e.externalAddrFunc(state, e.headless) for _, rr := range addr { rr.Header().Ttl = e.ttl rr.Header().Name = dnsutil.Join("ns1", e.apex, state.QName()) @@ -37,6 +38,7 @@ func (e *External) serveSubApex(state request.Request) (int, error) { m := new(dns.Msg) m.SetReply(state.Req) + m.Authoritative = true // base is either dns. of ns1.dns (or another name), if it's longer return nxdomain switch labels := dns.CountLabel(base); labels { @@ -56,7 +58,7 @@ func (e *External) serveSubApex(state request.Request) (int, error) { return 0, nil } - addr := e.externalAddrFunc(state) + addr := e.externalAddrFunc(state, e.headless) for _, rr := range addr { rr.Header().Ttl = e.ttl rr.Header().Name = state.QName() @@ -93,7 +95,7 @@ func (e *External) soa(state request.Request) *dns.SOA { soa := &dns.SOA{Hdr: header, Mbox: dnsutil.Join(e.hostmaster, e.apex, state.Zone), Ns: dnsutil.Join("ns1", e.apex, state.Zone), - Serial: 12345, // Also dynamic? + Serial: e.externalSerialFunc(state.Zone), Refresh: 7200, Retry: 1800, Expire: 86400, diff --git a/plugin/k8s_external/apex_test.go b/plugin/k8s_external/apex_test.go index 2f6923f56c..ab0818708c 100644 --- a/plugin/k8s_external/apex_test.go +++ b/plugin/k8s_external/apex_test.go @@ -17,10 +17,12 @@ func TestApex(t *testing.T) { k.APIConn = &external{} e := New() + e.headless = true e.Zones = []string{"example.com."} e.Next = test.NextHandler(dns.RcodeSuccess, nil) e.externalFunc = k.External - e.externalAddrFunc = externalAddress // internal test function + e.externalAddrFunc = externalAddress // internal test function + e.externalSerialFunc = externalSerial // internal test function ctx := context.TODO() for i, tc := range testsApex { @@ -40,9 +42,22 @@ func TestApex(t *testing.T) { if resp == nil { t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name) } + if !resp.Authoritative { + t.Error("Expected authoritative answer") + } if err := test.SortAndCheck(resp, tc); err != nil { t.Error(err) } + for i, rr := range tc.Ns { + expectsoa := rr.(*dns.SOA) + gotsoa, ok := resp.Ns[i].(*dns.SOA) + if !ok { + t.Fatalf("Unexpected record type in Authority section") + } + if expectsoa.Serial != gotsoa.Serial { + t.Fatalf("Expected soa serial %d, got %d", expectsoa.Serial, gotsoa.Serial) + } + } } } @@ -50,7 +65,7 @@ var testsApex = []test.Case{ { Qname: "example.com.", Qtype: dns.TypeSOA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"), }, }, { @@ -65,37 +80,37 @@ var testsApex = []test.Case{ { Qname: "example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, Ns: []dns.RR{ - test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"), }, }, { Qname: "dns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, Ns: []dns.RR{ - test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"), }, }, { Qname: "dns.example.com.", Qtype: dns.TypeNS, Rcode: dns.RcodeSuccess, Ns: []dns.RR{ - test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"), }, }, { Qname: "ns1.dns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, Ns: []dns.RR{ - test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"), }, }, { Qname: "ns1.dns.example.com.", Qtype: dns.TypeNS, Rcode: dns.RcodeSuccess, Ns: []dns.RR{ - test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"), }, }, { Qname: "ns1.dns.example.com.", Qtype: dns.TypeAAAA, Rcode: dns.RcodeSuccess, Ns: []dns.RR{ - test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"), }, }, { diff --git a/plugin/k8s_external/external.go b/plugin/k8s_external/external.go index 1dffe1daa0..442119bd09 100644 --- a/plugin/k8s_external/external.go +++ b/plugin/k8s_external/external.go @@ -7,7 +7,6 @@ NXDOMAIN depending on the state of the cluster. A plugin willing to provide these services must implement the Externaler interface, although it likely only makes sense for the *kubernetes* plugin. - */ package external @@ -16,6 +15,7 @@ import ( "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/plugin/pkg/upstream" "github.com/coredns/coredns/request" @@ -26,24 +26,32 @@ import ( type Externaler interface { // External returns a slice of msg.Services that are looked up in the backend and match // the request. - External(request.Request) ([]msg.Service, int) + External(request.Request, bool) ([]msg.Service, int) // ExternalAddress should return a string slice of addresses for the nameserving endpoint. - ExternalAddress(state request.Request) []dns.RR + ExternalAddress(state request.Request, headless bool) []dns.RR + // ExternalServices returns all services in the given zone as a slice of msg.Service and if enabled, headless services as a map of services. + ExternalServices(zone string, headless bool) ([]msg.Service, map[string][]msg.Service) + // ExternalSerial gets the current serial. + ExternalSerial(string) uint32 } -// External resolves Ingress and Loadbalance IPs from kubernetes clusters. +// External serves records for External IPs and Loadbalance IPs of Services in Kubernetes clusters. type External struct { Next plugin.Handler Zones []string + Fall fall.F hostmaster string apex string ttl uint32 + headless bool upstream *upstream.Upstream - externalFunc func(request.Request) ([]msg.Service, int) - externalAddrFunc func(request.Request) []dns.RR + externalFunc func(request.Request, bool) ([]msg.Service, int) + externalAddrFunc func(request.Request, bool) []dns.RR + externalSerialFunc func(string) uint32 + externalServicesFunc func(string, bool) ([]msg.Service, map[string][]msg.Service) } // New returns a new and initialized *External. @@ -61,10 +69,6 @@ func (e *External) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r) } - if e.externalFunc == nil { - return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r) - } - state.Zone = zone for _, z := range e.Zones { // TODO(miek): save this in the External struct. @@ -79,12 +83,17 @@ func (e *External) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms } } - svc, rcode := e.externalFunc(state) + svc, rcode := e.externalFunc(state, e.headless) m := new(dns.Msg) m.SetReply(state.Req) + m.Authoritative = true if len(svc) == 0 { + if e.Fall.Through(state.Name()) && rcode == dns.RcodeNameError { + return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r) + } + m.Rcode = rcode m.Ns = []dns.RR{e.soa(state)} w.WriteMsg(m) @@ -93,11 +102,13 @@ func (e *External) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms switch state.QType() { case dns.TypeA: - m.Answer = e.a(ctx, svc, state) + m.Answer, m.Truncated = e.a(ctx, svc, state) case dns.TypeAAAA: - m.Answer = e.aaaa(ctx, svc, state) + m.Answer, m.Truncated = e.aaaa(ctx, svc, state) case dns.TypeSRV: - m.Answer, m.Extra = e.srv(svc, state) + m.Answer, m.Extra = e.srv(ctx, svc, state) + case dns.TypePTR: + m.Answer = e.ptr(svc, state) default: m.Ns = []dns.RR{e.soa(state)} } diff --git a/plugin/k8s_external/external_test.go b/plugin/k8s_external/external_test.go index e51fc68951..ae265e9556 100644 --- a/plugin/k8s_external/external_test.go +++ b/plugin/k8s_external/external_test.go @@ -12,7 +12,6 @@ import ( "github.com/miekg/dns" api "k8s.io/api/core/v1" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestExternal(t *testing.T) { @@ -21,10 +20,12 @@ func TestExternal(t *testing.T) { k.APIConn = &external{} e := New() - e.Zones = []string{"example.com."} + e.Zones = []string{"example.com.", "in-addr.arpa."} + e.headless = true e.Next = test.NextHandler(dns.RcodeSuccess, nil) e.externalFunc = k.External - e.externalAddrFunc = externalAddress // internal test function + e.externalAddrFunc = externalAddress // internal test function + e.externalSerialFunc = externalSerial // internal test function ctx := context.TODO() for i, tc := range tests { @@ -45,13 +46,37 @@ func TestExternal(t *testing.T) { if resp == nil { t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name) } + if !resp.Authoritative { + t.Error("Expected authoritative answer") + } if err = test.SortAndCheck(resp, tc); err != nil { - t.Error(err) + t.Errorf("Test %d: %v", i, err) } } } var tests = []test.Case{ + // PTR reverse lookup + { + Qname: "4.3.2.1.in-addr.arpa.", Qtype: dns.TypePTR, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.PTR("4.3.2.1.in-addr.arpa. 5 IN PTR svc1.testns.example.com."), + }, + }, + // Bad PTR reverse lookup using existing service name + { + Qname: "svc1.testns.example.com.", Qtype: dns.TypePTR, Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + }, + }, + // Bad PTR reverse lookup using non-existing service name + { + Qname: "not-existing.testns.example.com.", Qtype: dns.TypePTR, Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + }, + }, // A Service { Qname: "svc1.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, @@ -62,7 +87,7 @@ var tests = []test.Case{ { Qname: "svc1.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, Answer: []dns.RR{test.SRV("svc1.testns.example.com. 5 IN SRV 0 100 80 svc1.testns.example.com.")}, - Extra: []dns.RR{test.A("svc1.testns.example.com. 5 IN A 1.2.3.4")}, + Extra: []dns.RR{test.A("svc1.testns.example.com. 5 IN A 1.2.3.4")}, }, // SRV Service Not udp/tcp { @@ -148,42 +173,196 @@ var tests = []test.Case{ test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), }, }, + // svc11 { Qname: "svc11.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.A("svc11.testns.example.com. 5 IN A 1.2.3.4"), + test.A("svc11.testns.example.com. 5 IN A 2.3.4.5"), + }, + }, + { + Qname: "_http._tcp.svc11.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("_http._tcp.svc11.testns.example.com. 5 IN SRV 0 100 80 svc11.testns.example.com."), + }, + Extra: []dns.RR{ + test.A("svc11.testns.example.com. 5 IN A 2.3.4.5"), + }, + }, + { + Qname: "svc11.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("svc11.testns.example.com. 5 IN SRV 0 100 80 svc11.testns.example.com."), + }, + Extra: []dns.RR{ + test.A("svc11.testns.example.com. 5 IN A 2.3.4.5"), }, }, + // svc12 { Qname: "svc12.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ test.CNAME("svc12.testns.example.com. 5 IN CNAME dummy.hostname"), }, }, + { + Qname: "_http._tcp.svc12.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("_http._tcp.svc12.testns.example.com. 5 IN SRV 0 100 80 dummy.hostname."), + }, + }, + { + Qname: "svc12.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("svc12.testns.example.com. 5 IN SRV 0 100 80 dummy.hostname."), + }, + }, + // headless service + { + Qname: "svc-headless.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("svc-headless.testns.example.com. 5 IN A 1.2.3.4"), + test.A("svc-headless.testns.example.com. 5 IN A 1.2.3.5"), + }, + }, + { + Qname: "svc-headless.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("svc-headless.testns.example.com. 5 IN SRV 0 50 80 endpoint-svc-0.svc-headless.testns.example.com."), + test.SRV("svc-headless.testns.example.com. 5 IN SRV 0 50 80 endpoint-svc-1.svc-headless.testns.example.com."), + }, + Extra: []dns.RR{ + test.A("endpoint-svc-0.svc-headless.testns.example.com. 5 IN A 1.2.3.4"), + test.A("endpoint-svc-1.svc-headless.testns.example.com. 5 IN A 1.2.3.5"), + }, + }, + { + Qname: "_http._tcp.svc-headless.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("_http._tcp.svc-headless.testns.example.com. 5 IN SRV 0 50 80 endpoint-svc-0.svc-headless.testns.example.com."), + test.SRV("_http._tcp.svc-headless.testns.example.com. 5 IN SRV 0 50 80 endpoint-svc-1.svc-headless.testns.example.com."), + }, + Extra: []dns.RR{ + test.A("endpoint-svc-0.svc-headless.testns.example.com. 5 IN A 1.2.3.4"), + test.A("endpoint-svc-1.svc-headless.testns.example.com. 5 IN A 1.2.3.5"), + }, + }, + { + Qname: "endpoint-svc-0.svc-headless.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("endpoint-svc-0.svc-headless.testns.example.com. 5 IN SRV 0 100 80 endpoint-svc-0.svc-headless.testns.example.com."), + }, + Extra: []dns.RR{ + test.A("endpoint-svc-0.svc-headless.testns.example.com. 5 IN A 1.2.3.4"), + }, + }, + { + Qname: "endpoint-svc-1.svc-headless.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("endpoint-svc-1.svc-headless.testns.example.com. 5 IN SRV 0 100 80 endpoint-svc-1.svc-headless.testns.example.com."), + }, + Extra: []dns.RR{ + test.A("endpoint-svc-1.svc-headless.testns.example.com. 5 IN A 1.2.3.5"), + }, + }, + { + Qname: "endpoint-svc-0.svc-headless.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("endpoint-svc-0.svc-headless.testns.example.com. 5 IN A 1.2.3.4"), + }, + }, + { + Qname: "endpoint-svc-1.svc-headless.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("endpoint-svc-1.svc-headless.testns.example.com. 5 IN A 1.2.3.5"), + }, + }, } type external struct{} -func (external) HasSynced() bool { return true } -func (external) Run() {} -func (external) Stop() error { return nil } -func (external) EpIndexReverse(string) []*object.Endpoints { return nil } -func (external) SvcIndexReverse(string) []*object.Service { return nil } -func (external) Modified() int64 { return 0 } -func (external) EpIndex(s string) []*object.Endpoints { return nil } -func (external) EndpointsList() []*object.Endpoints { return nil } +func (external) HasSynced() bool { return true } +func (external) Run() {} +func (external) Stop() error { return nil } +func (external) EpIndexReverse(string) []*object.Endpoints { return nil } +func (external) SvcIndexReverse(string) []*object.Service { return nil } +func (external) Modified(kubernetes.ModifiedMode) int64 { return 0 } + +func (external) SvcImportIndex(s string) []*object.ServiceImport { return nil } +func (external) ServiceImportList() []*object.ServiceImport { return nil } +func (external) McEpIndex(s string) []*object.MultiClusterEndpoints { return nil } +func (external) MultiClusterEndpointsList(s string) []*object.MultiClusterEndpoints { return nil } + +func (external) EpIndex(s string) []*object.Endpoints { + return epIndexExternal[s] +} + +func (external) EndpointsList() []*object.Endpoints { + var eps []*object.Endpoints + for _, ep := range epIndexExternal { + eps = append(eps, ep...) + } + return eps +} func (external) GetNodeByName(ctx context.Context, name string) (*api.Node, error) { return nil, nil } func (external) SvcIndex(s string) []*object.Service { return svcIndexExternal[s] } func (external) PodIndex(string) []*object.Pod { return nil } -func (external) GetNamespaceByName(name string) (*api.Namespace, error) { - return &api.Namespace{ - ObjectMeta: meta.ObjectMeta{ - Name: name, - }, +func (external) SvcExtIndexReverse(ip string) (result []*object.Service) { + for _, svcs := range svcIndexExternal { + for _, svc := range svcs { + for _, exIp := range svc.ExternalIPs { + if exIp != ip { + continue + } + result = append(result, svc) + } + } + } + return result +} + +func (external) GetNamespaceByName(name string) (*object.Namespace, error) { + return &object.Namespace{ + Name: name, }, nil } +var epIndexExternal = map[string][]*object.Endpoints{ + "svc-headless.testns": { + { + Name: "svc-headless", + Namespace: "testns", + Index: "svc-headless.testns", + Subsets: []object.EndpointSubset{ + { + Ports: []object.EndpointPort{ + { + Port: 80, + Name: "http", + Protocol: "TCP", + }, + }, + Addresses: []object.EndpointAddress{ + { + IP: "1.2.3.4", + Hostname: "endpoint-svc-0", + NodeName: "test-node", + TargetRefName: "endpoint-svc-0", + }, + { + IP: "1.2.3.5", + Hostname: "endpoint-svc-1", + NodeName: "test-node", + TargetRefName: "endpoint-svc-1", + }, + }, + }, + }, + }, + }, +} + var svcIndexExternal = map[string][]*object.Service{ "svc1.testns": { { @@ -210,7 +389,8 @@ var svcIndexExternal = map[string][]*object.Service{ Name: "svc11", Namespace: "testns", Type: api.ServiceTypeLoadBalancer, - ExternalIPs: []string{"1.2.3.4"}, + ExternalIPs: []string{"2.3.4.5"}, + ClusterIPs: []string{"10.0.0.3"}, Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, }, }, @@ -219,10 +399,20 @@ var svcIndexExternal = map[string][]*object.Service{ Name: "svc12", Namespace: "testns", Type: api.ServiceTypeLoadBalancer, + ClusterIPs: []string{"10.0.0.3"}, ExternalIPs: []string{"dummy.hostname"}, Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, }, }, + "svc-headless.testns": { + { + Name: "svc-headless", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{"None"}, + Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, + }, + }, } func (external) ServiceList() []*object.Service { @@ -233,7 +423,11 @@ func (external) ServiceList() []*object.Service { return svcs } -func externalAddress(state request.Request) []dns.RR { +func externalAddress(state request.Request, headless bool) []dns.RR { a := test.A("example.org. IN A 127.0.0.1") return []dns.RR{a} } + +func externalSerial(string) uint32 { + return 1499347823 +} diff --git a/plugin/k8s_external/msg_to_dns.go b/plugin/k8s_external/msg_to_dns.go index e61adf6579..6975718b9e 100644 --- a/plugin/k8s_external/msg_to_dns.go +++ b/plugin/k8s_external/msg_to_dns.go @@ -5,16 +5,16 @@ import ( "math" "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/pkg/dnsutil" "github.com/coredns/coredns/request" "github.com/miekg/dns" ) -func (e *External) a(ctx context.Context, services []msg.Service, state request.Request) (records []dns.RR) { +func (e *External) a(ctx context.Context, services []msg.Service, state request.Request) (records []dns.RR, truncated bool) { dup := make(map[string]struct{}) for _, s := range services { - what, ip := s.HostType() switch what { @@ -22,8 +22,9 @@ func (e *External) a(ctx context.Context, services []msg.Service, state request. rr := s.NewCNAME(state.QName(), s.Host) records = append(records, rr) if resp, err := e.upstream.Lookup(ctx, state, dns.Fqdn(s.Host), dns.TypeA); err == nil { - for _, rr := range resp.Answer { - records = append(records, rr) + records = append(records, resp.Answer...) + if resp.Truncated { + truncated = true } } @@ -39,14 +40,13 @@ func (e *External) a(ctx context.Context, services []msg.Service, state request. // nada } } - return records + return records, truncated } -func (e *External) aaaa(ctx context.Context, services []msg.Service, state request.Request) (records []dns.RR) { +func (e *External) aaaa(ctx context.Context, services []msg.Service, state request.Request) (records []dns.RR, truncated bool) { dup := make(map[string]struct{}) for _, s := range services { - what, ip := s.HostType() switch what { @@ -54,8 +54,9 @@ func (e *External) aaaa(ctx context.Context, services []msg.Service, state reque rr := s.NewCNAME(state.QName(), s.Host) records = append(records, rr) if resp, err := e.upstream.Lookup(ctx, state, dns.Fqdn(s.Host), dns.TypeAAAA); err == nil { - for _, rr := range resp.Answer { - records = append(records, rr) + records = append(records, resp.Answer...) + if resp.Truncated { + truncated = true } } @@ -71,10 +72,23 @@ func (e *External) aaaa(ctx context.Context, services []msg.Service, state reque } } } + return records, truncated +} + +func (e *External) ptr(services []msg.Service, state request.Request) (records []dns.RR) { + dup := make(map[string]struct{}) + for _, s := range services { + if _, ok := dup[s.Host]; !ok { + dup[s.Host] = struct{}{} + rr := s.NewPTR(state.QName(), dnsutil.Join(s.Host, e.Zones[0])) + rr.Hdr.Ttl = e.ttl + records = append(records, rr) + } + } return records } -func (e *External) srv(services []msg.Service, state request.Request) (records, extra []dns.RR) { +func (e *External) srv(ctx context.Context, services []msg.Service, state request.Request) (records, extra []dns.RR) { dup := make(map[item]struct{}) // Looping twice to get the right weight vs priority. This might break because we may drop duplicate SRV records latter on. @@ -112,8 +126,19 @@ func (e *External) srv(services []msg.Service, state request.Request) (records, switch what { case dns.TypeCNAME: - // can't happen - + addr := dns.Fqdn(s.Host) + srv := s.NewSRV(state.QName(), weight) + if ok := isDuplicate(dup, srv.Target, "", srv.Port); !ok { + records = append(records, srv) + } + if ok := isDuplicate(dup, srv.Target, addr, 0); !ok { + if resp, err := e.upstream.Lookup(ctx, state, addr, dns.TypeA); err == nil { + extra = append(extra, resp.Answer...) + } + if resp, err := e.upstream.Lookup(ctx, state, addr, dns.TypeAAAA); err == nil { + extra = append(extra, resp.Answer...) + } + } case dns.TypeA, dns.TypeAAAA: addr := s.Host s.Host = msg.Domain(s.Key) diff --git a/plugin/k8s_external/setup.go b/plugin/k8s_external/setup.go index 8783507bd5..f42f7de23d 100644 --- a/plugin/k8s_external/setup.go +++ b/plugin/k8s_external/setup.go @@ -1,6 +1,7 @@ package external import ( + "errors" "strconv" "github.com/coredns/caddy" @@ -9,7 +10,9 @@ import ( "github.com/coredns/coredns/plugin/pkg/upstream" ) -func init() { plugin.Register("k8s_external", setup) } +const pluginName = "k8s_external" + +func init() { plugin.Register(pluginName, setup) } func setup(c *caddy.Controller) error { e, err := parse(c) @@ -21,12 +24,18 @@ func setup(c *caddy.Controller) error { c.OnStartup(func() error { m := dnsserver.GetConfig(c).Handler("kubernetes") if m == nil { - return nil + return plugin.Error(pluginName, errors.New("kubernetes plugin not loaded")) } - if x, ok := m.(Externaler); ok { - e.externalFunc = x.External - e.externalAddrFunc = x.ExternalAddress + + x, ok := m.(Externaler) + if !ok { + return plugin.Error(pluginName, errors.New("kubernetes plugin does not implement the Externaler interface")) } + + e.externalFunc = x.External + e.externalAddrFunc = x.ExternalAddress + e.externalServicesFunc = x.ExternalServices + e.externalSerialFunc = x.ExternalSerial return nil }) @@ -44,15 +53,7 @@ func parse(c *caddy.Controller) (*External, error) { e := New() for c.Next() { // external - zones := c.RemainingArgs() - e.Zones = zones - if len(zones) == 0 { - e.Zones = make([]string, len(c.ServerBlockKeys)) - copy(e.Zones, c.ServerBlockKeys) - } - for i, str := range e.Zones { - e.Zones[i] = plugin.Host(str).Normalize() - } + e.Zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) for c.NextBlock() { switch c.Val() { case "ttl": @@ -74,6 +75,10 @@ func parse(c *caddy.Controller) (*External, error) { return nil, c.ArgErr() } e.apex = args[0] + case "headless": + e.headless = true + case "fallthrough": + e.Fall.SetZonesFromArgs(c.RemainingArgs()) default: return nil, c.Errf("unknown property '%s'", c.Val()) } diff --git a/plugin/k8s_external/setup_test.go b/plugin/k8s_external/setup_test.go index fde5ca6376..8814554ef3 100644 --- a/plugin/k8s_external/setup_test.go +++ b/plugin/k8s_external/setup_test.go @@ -4,20 +4,33 @@ import ( "testing" "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/pkg/fall" ) func TestSetup(t *testing.T) { tests := []struct { - input string - shouldErr bool - expectedZone string - expectedApex string + input string + shouldErr bool + expectedZone string + expectedApex string + expectedHeadless bool + expectedFallthrough fall.F }{ - {`k8s_external`, false, "", "dns"}, - {`k8s_external example.org`, false, "example.org.", "dns"}, + {`k8s_external`, false, "", "dns", false, fall.Zero}, + {`k8s_external example.org`, false, "example.org.", "dns", false, fall.Zero}, {`k8s_external example.org { apex testdns -}`, false, "example.org.", "testdns"}, +}`, false, "example.org.", "testdns", false, fall.Zero}, + {`k8s_external example.org { + headless +}`, false, "example.org.", "dns", true, fall.Zero}, + {`k8s_external example.org { + fallthrough +}`, false, "example.org.", "dns", false, fall.Root}, + {`k8s_external example.org { + fallthrough ip6.arpa inaddr.arpa foo.com +}`, false, "example.org.", "dns", false, + fall.F{Zones: []string{"ip6.arpa.", "inaddr.arpa.", "foo.com."}}}, } for i, test := range tests { @@ -44,5 +57,15 @@ func TestSetup(t *testing.T) { t.Errorf("Test %d, expected apex %q for input %s, got: %q", i, test.expectedApex, test.input, e.apex) } } + if !test.shouldErr { + if test.expectedHeadless != e.headless { + t.Errorf("Test %d, expected headless %q for input %s, got: %v", i, test.expectedApex, test.input, e.headless) + } + } + if !test.shouldErr { + if !e.Fall.Equal(test.expectedFallthrough) { + t.Errorf("Test %d, expected to be initialized with fallthrough %q for input %s, got: %v", i, test.expectedFallthrough, test.input, e.Fall) + } + } } } diff --git a/plugin/k8s_external/transfer.go b/plugin/k8s_external/transfer.go new file mode 100644 index 0000000000..781f19f21f --- /dev/null +++ b/plugin/k8s_external/transfer.go @@ -0,0 +1,150 @@ +package external + +import ( + "context" + "strings" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/kubernetes" + "github.com/coredns/coredns/plugin/transfer" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Transfer implements transfer.Transferer +func (e *External) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) { + z := plugin.Zones(e.Zones).Matches(zone) + if z != zone { + return nil, transfer.ErrNotAuthoritative + } + + ctx := context.Background() + ch := make(chan []dns.RR, 2) + if zone == "." { + zone = "" + } + state := request.Request{Zone: zone} + + // SOA + soa := e.soa(state) + ch <- []dns.RR{soa} + if serial != 0 && serial >= soa.Serial { + close(ch) + return ch, nil + } + + go func() { + // Add NS + nsName := "ns1." + e.apex + "." + zone + nsHdr := dns.RR_Header{Name: zone, Rrtype: dns.TypeNS, Ttl: e.ttl, Class: dns.ClassINET} + ch <- []dns.RR{&dns.NS{Hdr: nsHdr, Ns: nsName}} + + // Add Nameserver A/AAAA records + nsRecords := e.externalAddrFunc(state, e.headless) + for i := range nsRecords { + // externalAddrFunc returns incomplete header names, correct here + nsRecords[i].Header().Name = nsName + nsRecords[i].Header().Ttl = e.ttl + ch <- []dns.RR{nsRecords[i]} + } + + svcs, headlessSvcs := e.externalServicesFunc(zone, e.headless) + srvSeen := make(map[string]struct{}) + + for i := range svcs { + name := msg.Domain(svcs[i].Key) + + if svcs[i].TargetStrip == 0 { + // Add Service A/AAAA records + s := request.Request{Req: &dns.Msg{Question: []dns.Question{{Name: name}}}} + as, _ := e.a(ctx, []msg.Service{svcs[i]}, s) + if len(as) > 0 { + ch <- as + } + aaaas, _ := e.aaaa(ctx, []msg.Service{svcs[i]}, s) + if len(aaaas) > 0 { + ch <- aaaas + } + // Add bare SRV record, ensuring uniqueness + recs, _ := e.srv(ctx, []msg.Service{svcs[i]}, s) + for _, srv := range recs { + if !nameSeen(srvSeen, srv) { + ch <- []dns.RR{srv} + } + } + continue + } + // Add full SRV record, ensuring uniqueness + s := request.Request{Req: &dns.Msg{Question: []dns.Question{{Name: name}}}} + recs, _ := e.srv(ctx, []msg.Service{svcs[i]}, s) + for _, srv := range recs { + if !nameSeen(srvSeen, srv) { + ch <- []dns.RR{srv} + } + } + } + for key, svcs := range headlessSvcs { + // we have to strip the leading key because it's either port.protocol or endpoint + name := msg.Domain(key[:strings.LastIndex(key, "/")]) + switchKey := key[strings.LastIndex(key, "/")+1:] + switch switchKey { + case kubernetes.Endpoint: + // headless.namespace.example.com records + s := request.Request{Req: &dns.Msg{Question: []dns.Question{{Name: name}}}} + as, _ := e.a(ctx, svcs, s) + if len(as) > 0 { + ch <- as + } + aaaas, _ := e.aaaa(ctx, svcs, s) + if len(aaaas) > 0 { + ch <- aaaas + } + // Add bare SRV record, ensuring uniqueness + recs, _ := e.srv(ctx, svcs, s) + ch <- recs + for _, srv := range recs { + ch <- []dns.RR{srv} + } + + for i := range svcs { + // endpoint.headless.namespace.example.com record + s := request.Request{Req: &dns.Msg{Question: []dns.Question{{Name: msg.Domain(svcs[i].Key)}}}} + + as, _ := e.a(ctx, []msg.Service{svcs[i]}, s) + if len(as) > 0 { + ch <- as + } + aaaas, _ := e.aaaa(ctx, []msg.Service{svcs[i]}, s) + if len(aaaas) > 0 { + ch <- aaaas + } + // Add bare SRV record, ensuring uniqueness + recs, _ := e.srv(ctx, []msg.Service{svcs[i]}, s) + ch <- recs + for _, srv := range recs { + ch <- []dns.RR{srv} + } + } + + case kubernetes.PortProtocol: + s := request.Request{Req: &dns.Msg{Question: []dns.Question{{Name: name}}}} + recs, _ := e.srv(ctx, svcs, s) + ch <- recs + } + } + ch <- []dns.RR{soa} + close(ch) + }() + + return ch, nil +} + +func nameSeen(namesSeen map[string]struct{}, rr dns.RR) bool { + if _, duplicate := namesSeen[rr.Header().Name]; duplicate { + return true + } + namesSeen[rr.Header().Name] = struct{}{} + return false +} diff --git a/plugin/k8s_external/transfer_test.go b/plugin/k8s_external/transfer_test.go new file mode 100644 index 0000000000..26642bb541 --- /dev/null +++ b/plugin/k8s_external/transfer_test.go @@ -0,0 +1,147 @@ +package external + +import ( + "strings" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/kubernetes" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/plugin/transfer" + + "github.com/miekg/dns" +) + +func TestImplementsTransferer(t *testing.T) { + var e plugin.Handler = &External{} + _, ok := e.(transfer.Transferer) + if !ok { + t.Error("Transferer not implemented") + } +} + +func TestTransferAXFR(t *testing.T) { + k := kubernetes.New([]string{"cluster.local."}) + k.Namespaces = map[string]struct{}{"testns": {}} + k.APIConn = &external{} + + e := New() + e.headless = true + e.Zones = []string{"example.com."} + e.externalFunc = k.External + e.externalAddrFunc = externalAddress // internal test function + e.externalSerialFunc = externalSerial // internal test function + e.externalServicesFunc = k.ExternalServices + + ch, err := e.Transfer("example.com.", 0) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + var records []dns.RR + for rrs := range ch { + records = append(records, rrs...) + } + + expect := []dns.RR{} + for _, tc := range append(tests, testsApex...) { + if tc.Rcode != dns.RcodeSuccess { + continue + } + + for _, ans := range tc.Answer { + // Exclude wildcard test cases + if strings.Contains(ans.Header().Name, "*") { + continue + } + + // Exclude TXT records + if ans.Header().Rrtype == dns.TypeTXT { + continue + } + + // Exclude PTR records + if ans.Header().Rrtype == dns.TypePTR { + continue + } + + expect = append(expect, ans) + } + } + + diff := difference(expect, records) + if len(diff) != 0 { + t.Errorf("Got back %d records that do not exist in test cases, should be 0:", len(diff)) + for _, rec := range diff { + t.Errorf("%+v", rec) + } + } + + diff = difference(records, expect) + if len(diff) != 0 { + t.Errorf("Result is missing %d records, should be 0:", len(diff)) + for _, rec := range diff { + t.Errorf("%+v", rec) + } + } +} + +func TestTransferIXFR(t *testing.T) { + k := kubernetes.New([]string{"cluster.local."}) + k.Namespaces = map[string]struct{}{"testns": {}} + k.APIConn = &external{} + + e := New() + e.Zones = []string{"example.com."} + e.headless = true + e.externalFunc = k.External + e.externalAddrFunc = externalAddress // internal test function + e.externalSerialFunc = externalSerial // internal test function + e.externalServicesFunc = k.ExternalServices + + ch, err := e.Transfer("example.com.", externalSerial("example.com.")) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + var records []dns.RR + for rrs := range ch { + records = append(records, rrs...) + } + + expect := []dns.RR{ + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"), + } + + diff := difference(expect, records) + if len(diff) != 0 { + t.Errorf("Got back %d records that do not exist in test cases, should be 0:", len(diff)) + for _, rec := range diff { + t.Errorf("%+v", rec) + } + } + + diff = difference(records, expect) + if len(diff) != 0 { + t.Errorf("Result is missing %d records, should be 0:", len(diff)) + for _, rec := range diff { + t.Errorf("%+v", rec) + } + } +} + +// difference shows what we're missing when comparing two RR slices +func difference(testRRs []dns.RR, gotRRs []dns.RR) []dns.RR { + expectedRRs := map[string]struct{}{} + for _, rr := range testRRs { + expectedRRs[rr.String()] = struct{}{} + } + + foundRRs := []dns.RR{} + for _, rr := range gotRRs { + if _, ok := expectedRRs[rr.String()]; !ok { + foundRRs = append(foundRRs, rr) + } + } + return foundRRs +} diff --git a/plugin/kubernetes/README.md b/plugin/kubernetes/README.md index 4557300c29..4273a9ea02 100644 --- a/plugin/kubernetes/README.md +++ b/plugin/kubernetes/README.md @@ -42,6 +42,8 @@ kubernetes [ZONES...] { noendpoints fallthrough [ZONES...] ignore empty_service + multicluster [ZONES...] + startup_timeout DURATION } ``` @@ -87,7 +89,7 @@ kubernetes [ZONES...] { If this directive is included, then name selection for endpoints changes as follows: Use the hostname of the endpoint, or if hostname is not set, use the pod name of the pod targeted by the endpoint. If there is no pod targeted by - the endpoint, use the dashed IP address form. + the endpoint or pod name is longer than 63, use the dashed IP address form. * `ttl` allows you to set a custom TTL for responses. The default is 5 seconds. The minimum TTL allowed is 0 seconds, and the maximum is capped at 3600 seconds. Setting TTL to 0 will prevent records from being cached. * `noendpoints` will turn off the serving of endpoint records by disabling the watch on endpoints. @@ -101,14 +103,37 @@ kubernetes [ZONES...] { * `ignore empty_service` returns NXDOMAIN for services without any ready endpoint addresses (e.g., ready pods). This allows the querying pod to continue searching for the service in the search path. The search path could, for example, include another Kubernetes cluster. +* `multicluster` defines the multicluster zones as defined by Multi-Cluster + Services API (MCS-API). Specifying this option is generally paired with the + installation of an MCS-API implementation and the ServiceImport and ServiceExport + CRDs. The plugin MUST be authoritative for the zones listed here. +* `startup_timeout` specifies the **DURATION** value that limits the time to wait for informer cache synced + when the kubernetes plugin starts. If not specified, the default timeout will be 5s. Enabling zone transfer is done by using the *transfer* plugin. +## Startup + +When CoreDNS starts with the *kubernetes* plugin enabled, it will delay serving DNS for up to 5 seconds +until it can connect to the Kubernetes API and synchronize all object watches. If this cannot happen within +5 seconds, then CoreDNS will start serving DNS while the *kubernetes* plugin continues to try to connect +and synchronize all object watches. CoreDNS will answer SERVFAIL to any request made for a Kubernetes record +that has not yet been synchronized. You can also determine how long to wait by specifying `startup_timeout`. + +## Monitoring Kubernetes Endpoints + +The *kubernetes* plugin watches Endpoints via the `discovery.EndpointSlices` API. + ## Ready This plugin reports readiness to the ready plugin. This will happen after it has synced to the Kubernetes API. +## PTR Records + +This plugin creates PTR records for every Pod selected by a Service. If a given Pod is selected by more than +one Service a separate PTR record will exist for each Service selecting it. + ## Examples Handle all queries in the `cluster.local` zone. Connect to Kubernetes in-cluster. Also handle all @@ -140,6 +165,14 @@ kubernetes cluster.local { } ~~~ +Configure multicluster + +~~~ txt +kubernetes cluster.local clusterset.local { + multicluster clusterset.local +} +~~~ + ## stubDomains and upstreamNameservers Here we use the *forward* plugin to implement a stubDomain that forwards `example.local` to the nameserver `10.100.0.10:53`. @@ -182,25 +215,6 @@ packet received by CoreDNS must be the IP address of the Pod that sent the reque } } -## Wildcards - -Some query labels accept a wildcard value to match any value. If a label is a valid wildcard (\*, -or the word "any"), then that label will match all values. The labels that accept wildcards are: - - * _endpoint_ in an `A` record request: _endpoint_.service.namespace.svc.zone, e.g., `*.nginx.ns.svc.cluster.local` - * _service_ in an `A` record request: _service_.namespace.svc.zone, e.g., `*.ns.svc.cluster.local` - * _namespace_ in an `A` record request: service._namespace_.svc.zone, e.g., `nginx.*.svc.cluster.local` - * _port and/or protocol_ in an `SRV` request: __port_.__protocol_.service.namespace.svc.zone., - e.g., `_http.*.service.ns.svc.cluster.local` - * multiple wildcards are allowed in a single query, e.g., `A` Request `*.*.svc.zone.` or `SRV` request `*.*.*.*.svc.zone.` - - For example, wildcards can be used to resolve all Endpoints for a Service as `A` records. e.g.: `*.service.ns.svc.myzone.local` will return the Endpoint IPs in the Service `service` in namespace `default`: - -``` -*.service.default.svc.cluster.local. 5 IN A 192.168.10.10 -*.service.default.svc.cluster.local. 5 IN A 192.168.25.15 -``` - ## Metadata The kubernetes plugin will publish the following metadata, if the *metadata* @@ -214,9 +228,11 @@ plugin is also enabled: * `kubernetes/service`: the service name in the query * `kubernetes/client-namespace`: the client pod's namespace (see requirements below) * `kubernetes/client-pod-name`: the client pod's name (see requirements below) + * `kubernetes/client-label/