From 022999021890b0a576910f104ebaf4c31fc052d6 Mon Sep 17 00:00:00 2001 From: dbw_gongwei <3037964810@qq.com> Date: Tue, 9 Jul 2024 18:16:32 +0800 Subject: first_commit --- ag_201_coredns/-text | 25 + ag_201_coredns/.circleci/config.yml | 66 + ag_201_coredns/.codecov.yml | 8 + ag_201_coredns/.dockerignore | 2 + ag_201_coredns/.dreck.yaml | 13 + ag_201_coredns/.github/CODE_OF_CONDUCT.md | 3 + ag_201_coredns/.github/CONTRIBUTING.md | 92 ++ .../.github/ISSUE_TEMPLATE/bug-report.md | 27 + .../.github/ISSUE_TEMPLATE/enhancement.md | 11 + ag_201_coredns/.github/ISSUE_TEMPLATE/question.md | 10 + ag_201_coredns/.github/PULL_REQUEST_TEMPLATE.md | 12 + ag_201_coredns/.github/SECURITY.md | 189 +++ ag_201_coredns/.github/dependabot.yml | 12 + ag_201_coredns/.github/fixup_file_mtime.sh | 15 + ag_201_coredns/.github/workflows/cifuzz.yml | 27 + .../.github/workflows/codeql-analysis.yml | 41 + ag_201_coredns/.github/workflows/depsreview.yml | 14 + ag_201_coredns/.github/workflows/docker.yml | 29 + ag_201_coredns/.github/workflows/go.coverage.yml | 30 + ag_201_coredns/.github/workflows/go.fmt.yml | 36 + ag_201_coredns/.github/workflows/go.test.yml | 83 + ag_201_coredns/.github/workflows/go.tidy.yml | 43 + ag_201_coredns/.github/workflows/golangci-lint.yml | 17 + ag_201_coredns/.github/workflows/make.doc.yml | 42 + ag_201_coredns/.github/workflows/reviewdog.yml | 25 + ag_201_coredns/.github/workflows/scorecards.yml | 55 + ag_201_coredns/.github/workflows/stale.yml | 26 + ag_201_coredns/.github/workflows/whitespace.yml | 36 + ag_201_coredns/.github/workflows/yamllint.yml | 19 + ag_201_coredns/.gitignore | 6 + ag_201_coredns/.golangci.yml | 13 + ag_201_coredns/.stickler.yml | 10 + ag_201_coredns/.yamllint | 17 + ag_201_coredns/ADOPTERS.md | 33 + ag_201_coredns/CODEOWNERS | 62 + ag_201_coredns/CODE_OF_CONDUCT.md | 0 ag_201_coredns/CONTRIBUTING.md | 0 ag_201_coredns/Corefile | 24 + ag_201_coredns/Corefile.1023 | 8 + ag_201_coredns/Corefile.223.5.5.5 | 7 + ag_201_coredns/Dockerfile | 19 + ag_201_coredns/GOVERNANCE.md | 152 ++ ag_201_coredns/LICENSE | 201 +++ ag_201_coredns/Makefile | 37 + ag_201_coredns/Makefile.doc | 64 + ag_201_coredns/Makefile.docker | 115 ++ ag_201_coredns/Makefile.release | 141 ++ ag_201_coredns/README.md | 297 ++++ ag_201_coredns/SECURITY.md | 0 ag_201_coredns/core/coredns.go | 7 + ag_201_coredns/core/dnsserver/address.go | 86 ++ ag_201_coredns/core/dnsserver/address_test.go | 113 ++ ag_201_coredns/core/dnsserver/config.go | 99 ++ ag_201_coredns/core/dnsserver/https.go | 30 + ag_201_coredns/core/dnsserver/https_test.go | 90 ++ ag_201_coredns/core/dnsserver/log_test.go | 5 + ag_201_coredns/core/dnsserver/onstartup.go | 57 + ag_201_coredns/core/dnsserver/onstartup_test.go | 39 + ag_201_coredns/core/dnsserver/register.go | 324 ++++ ag_201_coredns/core/dnsserver/register_test.go | 120 ++ ag_201_coredns/core/dnsserver/server.go | 428 +++++ ag_201_coredns/core/dnsserver/server_grpc.go | 184 +++ ag_201_coredns/core/dnsserver/server_https.go | 209 +++ ag_201_coredns/core/dnsserver/server_https_test.go | 66 + ag_201_coredns/core/dnsserver/server_test.go | 117 ++ ag_201_coredns/core/dnsserver/server_tls.go | 89 ++ ag_201_coredns/core/dnsserver/view.go | 20 + ag_201_coredns/core/dnsserver/zdirectives.go | 65 + ag_201_coredns/core/plugin/zplugin.go | 59 + ag_201_coredns/coredns.1.md | 52 + ag_201_coredns/coredns.go | 13 + ag_201_coredns/corefile.5.md | 140 ++ ag_201_coredns/coremain/run.go | 184 +++ ag_201_coredns/coremain/version.go | 8 + ag_201_coredns/directives_generate.go | 114 ++ ag_201_coredns/go.mod | 123 ++ ag_201_coredns/go.sum | 1630 ++++++++++++++++++++ ag_201_coredns/man/coredns-acl.7 | 135 ++ ag_201_coredns/man/coredns-any.7 | 53 + ag_201_coredns/man/coredns-auto.7 | 112 ++ ag_201_coredns/man/coredns-autopath.7 | 95 ++ ag_201_coredns/man/coredns-azure.7 | 74 + ag_201_coredns/man/coredns-bind.7 | 119 ++ ag_201_coredns/man/coredns-bufsize.7 | 67 + ag_201_coredns/man/coredns-cache.7 | 165 ++ ag_201_coredns/man/coredns-cancel.7 | 67 + ag_201_coredns/man/coredns-chaos.7 | 78 + ag_201_coredns/man/coredns-clouddns.7 | 96 ++ ag_201_coredns/man/coredns-debug.7 | 73 + ag_201_coredns/man/coredns-dns64.7 | 144 ++ ag_201_coredns/man/coredns-dnssec.7 | 119 ++ ag_201_coredns/man/coredns-dnstap.7 | 163 ++ ag_201_coredns/man/coredns-erratic.7 | 132 ++ ag_201_coredns/man/coredns-errors.7 | 93 ++ ag_201_coredns/man/coredns-etcd.7 | 372 +++++ ag_201_coredns/man/coredns-file.7 | 167 ++ ag_201_coredns/man/coredns-forward.7 | 326 ++++ ag_201_coredns/man/coredns-geoip.7 | 118 ++ ag_201_coredns/man/coredns-grpc.7 | 205 +++ ag_201_coredns/man/coredns-header.7 | 84 + ag_201_coredns/man/coredns-health.7 | 115 ++ ag_201_coredns/man/coredns-hosts.7 | 175 +++ ag_201_coredns/man/coredns-import.7 | 110 ++ ag_201_coredns/man/coredns-k8s_external.7 | 130 ++ ag_201_coredns/man/coredns-kubernetes.7 | 352 +++++ ag_201_coredns/man/coredns-loadbalance.7 | 48 + ag_201_coredns/man/coredns-local.7 | 67 + ag_201_coredns/man/coredns-log.7 | 249 +++ ag_201_coredns/man/coredns-loop.7 | 123 ++ ag_201_coredns/man/coredns-metadata.7 | 64 + ag_201_coredns/man/coredns-metrics.7 | 116 ++ ag_201_coredns/man/coredns-minimal.7 | 49 + ag_201_coredns/man/coredns-nsid.7 | 78 + ag_201_coredns/man/coredns-pprof.7 | 115 ++ ag_201_coredns/man/coredns-ready.7 | 77 + ag_201_coredns/man/coredns-reload.7 | 152 ++ ag_201_coredns/man/coredns-rewrite.7 | 470 ++++++ ag_201_coredns/man/coredns-root.7 | 43 + ag_201_coredns/man/coredns-route53.7 | 142 ++ ag_201_coredns/man/coredns-secondary.7 | 97 ++ ag_201_coredns/man/coredns-sign.7 | 228 +++ ag_201_coredns/man/coredns-template.7 | 359 +++++ ag_201_coredns/man/coredns-tls.7 | 95 ++ ag_201_coredns/man/coredns-trace.7 | 156 ++ ag_201_coredns/man/coredns-transfer.7 | 50 + ag_201_coredns/man/coredns-tsig.7 | 150 ++ ag_201_coredns/man/coredns-view.7 | 184 +++ ag_201_coredns/man/coredns-whoami.7 | 82 + ag_201_coredns/man/coredns.1 | 62 + ag_201_coredns/man/corefile.5 | 207 +++ ag_201_coredns/notes/coredns-0.9.10.md | 50 + ag_201_coredns/notes/coredns-0.9.9.md | 70 + ag_201_coredns/notes/coredns-001.md | 57 + ag_201_coredns/notes/coredns-002.md | 108 ++ ag_201_coredns/notes/coredns-003.md | 50 + ag_201_coredns/notes/coredns-004.md | 45 + ag_201_coredns/notes/coredns-005.md | 65 + ag_201_coredns/notes/coredns-006.md | 55 + ag_201_coredns/notes/coredns-007.md | 69 + ag_201_coredns/notes/coredns-008.md | 66 + ag_201_coredns/notes/coredns-009.md | 47 + ag_201_coredns/notes/coredns-010.md | 51 + ag_201_coredns/notes/coredns-011.md | 75 + ag_201_coredns/notes/coredns-1.0.0.md | 75 + ag_201_coredns/notes/coredns-1.0.1.md | 34 + ag_201_coredns/notes/coredns-1.0.2.md | 44 + ag_201_coredns/notes/coredns-1.0.3.md | 43 + ag_201_coredns/notes/coredns-1.0.4.md | 17 + ag_201_coredns/notes/coredns-1.0.5.md | 48 + ag_201_coredns/notes/coredns-1.0.6.md | 51 + ag_201_coredns/notes/coredns-1.1.0.md | 79 + ag_201_coredns/notes/coredns-1.1.1.md | 40 + ag_201_coredns/notes/coredns-1.1.2.md | 40 + ag_201_coredns/notes/coredns-1.1.3.md | 57 + ag_201_coredns/notes/coredns-1.1.4.md | 54 + ag_201_coredns/notes/coredns-1.10.0.md | 24 + ag_201_coredns/notes/coredns-1.2.0.md | 50 + ag_201_coredns/notes/coredns-1.2.1.md | 41 + ag_201_coredns/notes/coredns-1.2.2.md | 24 + ag_201_coredns/notes/coredns-1.2.3.md | 61 + ag_201_coredns/notes/coredns-1.2.4.md | 31 + ag_201_coredns/notes/coredns-1.2.5.md | 40 + ag_201_coredns/notes/coredns-1.2.6.md | 49 + ag_201_coredns/notes/coredns-1.3.0.md | 51 + ag_201_coredns/notes/coredns-1.3.1.md | 45 + ag_201_coredns/notes/coredns-1.4.0.md | 88 ++ ag_201_coredns/notes/coredns-1.5.0.md | 62 + ag_201_coredns/notes/coredns-1.5.1.md | 75 + ag_201_coredns/notes/coredns-1.5.2.md | 40 + ag_201_coredns/notes/coredns-1.6.0.md | 62 + ag_201_coredns/notes/coredns-1.6.1.md | 37 + ag_201_coredns/notes/coredns-1.6.2.md | 41 + ag_201_coredns/notes/coredns-1.6.3.md | 66 + ag_201_coredns/notes/coredns-1.6.4.md | 41 + ag_201_coredns/notes/coredns-1.6.5.md | 52 + ag_201_coredns/notes/coredns-1.6.6.md | 50 + ag_201_coredns/notes/coredns-1.6.7.md | 41 + ag_201_coredns/notes/coredns-1.6.8.md | 44 + ag_201_coredns/notes/coredns-1.6.9.md | 49 + ag_201_coredns/notes/coredns-1.7.0.md | 93 ++ ag_201_coredns/notes/coredns-1.7.1.md | 53 + ag_201_coredns/notes/coredns-1.8.0.md | 68 + ag_201_coredns/notes/coredns-1.8.1.md | 49 + ag_201_coredns/notes/coredns-1.8.2.md | 45 + ag_201_coredns/notes/coredns-1.8.3.md | 49 + ag_201_coredns/notes/coredns-1.8.4.md | 57 + ag_201_coredns/notes/coredns-1.8.5.md | 62 + ag_201_coredns/notes/coredns-1.8.6.md | 20 + ag_201_coredns/notes/coredns-1.8.7.md | 58 + ag_201_coredns/notes/coredns-1.9.0.md | 28 + ag_201_coredns/notes/coredns-1.9.1.md | 38 + ag_201_coredns/notes/coredns-1.9.2.md | 46 + ag_201_coredns/notes/coredns-1.9.3.md | 31 + ag_201_coredns/notes/coredns-1.9.4.md | 59 + ag_201_coredns/owners_generate.go | 89 ++ ag_201_coredns/pb/Makefile | 20 + ag_201_coredns/pb/dns.pb.go | 147 ++ ag_201_coredns/pb/dns.proto | 12 + ag_201_coredns/pb/dns_grpc.pb.go | 105 ++ ag_201_coredns/pkcs8.pem | 28 + ag_201_coredns/plugin.cfg | 73 + ag_201_coredns/plugin.md | 164 ++ ag_201_coredns/plugin/acl/README.md | 98 ++ ag_201_coredns/plugin/acl/acl.go | 144 ++ ag_201_coredns/plugin/acl/acl_test.go | 449 ++++++ ag_201_coredns/plugin/acl/metrics.go | 32 + ag_201_coredns/plugin/acl/setup.go | 152 ++ ag_201_coredns/plugin/acl/setup_test.go | 259 ++++ ag_201_coredns/plugin/any/README.md | 36 + ag_201_coredns/plugin/any/any.go | 32 + ag_201_coredns/plugin/any/any_test.go | 28 + ag_201_coredns/plugin/any/setup.go | 20 + ag_201_coredns/plugin/auto/README.md | 82 + ag_201_coredns/plugin/auto/auto.go | 100 ++ ag_201_coredns/plugin/auto/log_test.go | 5 + ag_201_coredns/plugin/auto/regexp.go | 20 + ag_201_coredns/plugin/auto/regexp_test.go | 20 + ag_201_coredns/plugin/auto/setup.go | 165 ++ ag_201_coredns/plugin/auto/setup_test.go | 177 +++ ag_201_coredns/plugin/auto/walk.go | 106 ++ ag_201_coredns/plugin/auto/walk_test.go | 88 ++ ag_201_coredns/plugin/auto/watcher_test.go | 100 ++ ag_201_coredns/plugin/auto/xfr.go | 19 + ag_201_coredns/plugin/auto/zone.go | 77 + ag_201_coredns/plugin/autopath/README.md | 68 + ag_201_coredns/plugin/autopath/autopath.go | 157 ++ ag_201_coredns/plugin/autopath/autopath_test.go | 166 ++ ag_201_coredns/plugin/autopath/cname.go | 25 + ag_201_coredns/plugin/autopath/metrics.go | 18 + ag_201_coredns/plugin/autopath/setup.go | 70 + ag_201_coredns/plugin/autopath/setup_test.go | 77 + ag_201_coredns/plugin/azure/README.md | 60 + ag_201_coredns/plugin/azure/azure.go | 352 +++++ ag_201_coredns/plugin/azure/azure_test.go | 180 +++ ag_201_coredns/plugin/azure/setup.go | 144 ++ ag_201_coredns/plugin/azure/setup_test.go | 71 + ag_201_coredns/plugin/backend.go | 40 + ag_201_coredns/plugin/backend_lookup.go | 560 +++++++ ag_201_coredns/plugin/bind/README.md | 113 ++ ag_201_coredns/plugin/bind/bind.go | 17 + ag_201_coredns/plugin/bind/log_test.go | 5 + ag_201_coredns/plugin/bind/setup.go | 111 ++ ag_201_coredns/plugin/bind/setup_test.go | 47 + ag_201_coredns/plugin/bufsize/README.md | 39 + ag_201_coredns/plugin/bufsize/bufsize.go | 27 + ag_201_coredns/plugin/bufsize/bufsize_test.go | 102 ++ ag_201_coredns/plugin/bufsize/setup.go | 51 + ag_201_coredns/plugin/bufsize/setup_test.go | 46 + ag_201_coredns/plugin/cache/README.md | 139 ++ ag_201_coredns/plugin/cache/cache.go | 299 ++++ ag_201_coredns/plugin/cache/cache_test.go | 696 +++++++++ ag_201_coredns/plugin/cache/dnssec.go | 46 + ag_201_coredns/plugin/cache/dnssec_test.go | 117 ++ ag_201_coredns/plugin/cache/error_test.go | 38 + ag_201_coredns/plugin/cache/freq/freq.go | 55 + ag_201_coredns/plugin/cache/freq/freq_test.go | 36 + ag_201_coredns/plugin/cache/fuzz.go | 12 + ag_201_coredns/plugin/cache/handler.go | 175 +++ ag_201_coredns/plugin/cache/item.go | 107 ++ ag_201_coredns/plugin/cache/log_test.go | 5 + ag_201_coredns/plugin/cache/metrics.go | 67 + ag_201_coredns/plugin/cache/prefech_test.go | 163 ++ ag_201_coredns/plugin/cache/setup.go | 255 +++ ag_201_coredns/plugin/cache/setup_test.go | 233 +++ ag_201_coredns/plugin/cache/spoof_test.go | 82 + ag_201_coredns/plugin/cancel/README.md | 47 + ag_201_coredns/plugin/cancel/cancel.go | 66 + ag_201_coredns/plugin/cancel/cancel_test.go | 51 + ag_201_coredns/plugin/cancel/setup_test.go | 29 + ag_201_coredns/plugin/chaos/README.md | 51 + ag_201_coredns/plugin/chaos/chaos.go | 58 + ag_201_coredns/plugin/chaos/chaos_test.go | 80 + ag_201_coredns/plugin/chaos/fuzz.go | 13 + ag_201_coredns/plugin/chaos/log_test.go | 5 + ag_201_coredns/plugin/chaos/setup.go | 66 + ag_201_coredns/plugin/chaos/setup_test.go | 54 + ag_201_coredns/plugin/chaos/zowners.go | 4 + ag_201_coredns/plugin/clouddns/README.md | 73 + ag_201_coredns/plugin/clouddns/clouddns.go | 225 +++ ag_201_coredns/plugin/clouddns/clouddns_test.go | 309 ++++ ag_201_coredns/plugin/clouddns/gcp.go | 40 + ag_201_coredns/plugin/clouddns/log_test.go | 5 + ag_201_coredns/plugin/clouddns/setup.go | 108 ++ ag_201_coredns/plugin/clouddns/setup_test.go | 49 + ag_201_coredns/plugin/debug/README.md | 51 + ag_201_coredns/plugin/debug/debug.go | 22 + ag_201_coredns/plugin/debug/debug_test.go | 44 + ag_201_coredns/plugin/debug/log_test.go | 5 + ag_201_coredns/plugin/debug/pcap.go | 72 + ag_201_coredns/plugin/debug/pcap_test.go | 73 + ag_201_coredns/plugin/deprecated/setup.go | 34 + ag_201_coredns/plugin/dns64/README.md | 106 ++ ag_201_coredns/plugin/dns64/dns64.go | 208 +++ ag_201_coredns/plugin/dns64/dns64_test.go | 556 +++++++ ag_201_coredns/plugin/dns64/metrics.go | 18 + ag_201_coredns/plugin/dns64/setup.go | 92 ++ ag_201_coredns/plugin/dns64/setup_test.go | 153 ++ ag_201_coredns/plugin/dnsovertor.zip | Bin 0 -> 4272 bytes ag_201_coredns/plugin/dnsovertor/dnsovertor.go | 135 ++ ag_201_coredns/plugin/dnsovertor/metrics.go | 18 + ag_201_coredns/plugin/dnsovertor/ready.go | 5 + ag_201_coredns/plugin/dnsovertor/setup.go | 55 + ag_201_coredns/plugin/dnsovertor/setup_test.go | 52 + ag_201_coredns/plugin/dnssec/README.md | 87 ++ ag_201_coredns/plugin/dnssec/black_lies.go | 64 + .../plugin/dnssec/black_lies_bitmap_test.go | 64 + ag_201_coredns/plugin/dnssec/black_lies_test.go | 86 ++ ag_201_coredns/plugin/dnssec/cache.go | 48 + ag_201_coredns/plugin/dnssec/cache_test.go | 82 + ag_201_coredns/plugin/dnssec/dnskey.go | 95 ++ ag_201_coredns/plugin/dnssec/dnssec.go | 159 ++ ag_201_coredns/plugin/dnssec/dnssec_test.go | 208 +++ ag_201_coredns/plugin/dnssec/handler.go | 50 + ag_201_coredns/plugin/dnssec/handler_test.go | 182 +++ ag_201_coredns/plugin/dnssec/log_test.go | 5 + ag_201_coredns/plugin/dnssec/metrics.go | 32 + ag_201_coredns/plugin/dnssec/responsewriter.go | 43 + ag_201_coredns/plugin/dnssec/rrsig.go | 53 + ag_201_coredns/plugin/dnssec/setup.go | 146 ++ ag_201_coredns/plugin/dnssec/setup_test.go | 160 ++ ag_201_coredns/plugin/dnstap/README.md | 124 ++ ag_201_coredns/plugin/dnstap/encoder.go | 40 + ag_201_coredns/plugin/dnstap/handler.go | 61 + ag_201_coredns/plugin/dnstap/handler_test.go | 84 + ag_201_coredns/plugin/dnstap/io.go | 121 ++ ag_201_coredns/plugin/dnstap/io_test.go | 155 ++ ag_201_coredns/plugin/dnstap/log_test.go | 5 + ag_201_coredns/plugin/dnstap/msg/msg.go | 97 ++ ag_201_coredns/plugin/dnstap/setup.go | 103 ++ ag_201_coredns/plugin/dnstap/setup_test.go | 60 + ag_201_coredns/plugin/dnstap/writer.go | 40 + ag_201_coredns/plugin/done.go | 13 + ag_201_coredns/plugin/erratic/README.md | 89 ++ ag_201_coredns/plugin/erratic/autopath.go | 8 + ag_201_coredns/plugin/erratic/erratic.go | 109 ++ ag_201_coredns/plugin/erratic/erratic_test.go | 116 ++ ag_201_coredns/plugin/erratic/log_test.go | 5 + ag_201_coredns/plugin/erratic/ready.go | 13 + ag_201_coredns/plugin/erratic/setup.go | 113 ++ ag_201_coredns/plugin/erratic/setup_test.go | 103 ++ ag_201_coredns/plugin/erratic/xfr.go | 57 + ag_201_coredns/plugin/errors/README.md | 65 + ag_201_coredns/plugin/errors/benchmark_test.go | 27 + ag_201_coredns/plugin/errors/errors.go | 104 ++ ag_201_coredns/plugin/errors/errors_test.go | 237 +++ ag_201_coredns/plugin/errors/log_test.go | 5 + ag_201_coredns/plugin/errors/setup.go | 109 ++ ag_201_coredns/plugin/errors/setup_test.go | 148 ++ ag_201_coredns/plugin/etcd/README.md | 236 +++ ag_201_coredns/plugin/etcd/cname_test.go | 108 ++ ag_201_coredns/plugin/etcd/etcd.go | 185 +++ ag_201_coredns/plugin/etcd/group_test.go | 87 ++ ag_201_coredns/plugin/etcd/handler.go | 82 + ag_201_coredns/plugin/etcd/log_test.go | 5 + ag_201_coredns/plugin/etcd/lookup_test.go | 355 +++++ ag_201_coredns/plugin/etcd/msg/path.go | 51 + ag_201_coredns/plugin/etcd/msg/path_test.go | 24 + ag_201_coredns/plugin/etcd/msg/service.go | 176 +++ ag_201_coredns/plugin/etcd/msg/service_test.go | 125 ++ ag_201_coredns/plugin/etcd/msg/type.go | 35 + ag_201_coredns/plugin/etcd/msg/type_test.go | 30 + ag_201_coredns/plugin/etcd/multi_test.go | 60 + ag_201_coredns/plugin/etcd/other_test.go | 138 ++ ag_201_coredns/plugin/etcd/setup.go | 116 ++ ag_201_coredns/plugin/etcd/setup_test.go | 118 ++ ag_201_coredns/plugin/etcd/xfr.go | 17 + ag_201_coredns/plugin/file/README.md | 112 ++ ag_201_coredns/plugin/file/apex_test.go | 45 + ag_201_coredns/plugin/file/closest.go | 23 + ag_201_coredns/plugin/file/closest_test.go | 38 + ag_201_coredns/plugin/file/delegation_test.go | 228 +++ ag_201_coredns/plugin/file/delete_test.go | 65 + ag_201_coredns/plugin/file/dname.go | 44 + ag_201_coredns/plugin/file/dname_test.go | 300 ++++ ag_201_coredns/plugin/file/dnssec_test.go | 350 +++++ ag_201_coredns/plugin/file/dnssex_test.go | 145 ++ ag_201_coredns/plugin/file/ds_test.go | 77 + ag_201_coredns/plugin/file/ent_test.go | 159 ++ ag_201_coredns/plugin/file/example_org.go | 113 ++ ag_201_coredns/plugin/file/file.go | 164 ++ ag_201_coredns/plugin/file/file_test.go | 31 + ag_201_coredns/plugin/file/fuzz.go | 50 + ag_201_coredns/plugin/file/glue_test.go | 254 +++ ag_201_coredns/plugin/file/include_test.go | 31 + ag_201_coredns/plugin/file/log_test.go | 5 + ag_201_coredns/plugin/file/lookup.go | 435 ++++++ ag_201_coredns/plugin/file/lookup_test.go | 284 ++++ ag_201_coredns/plugin/file/notify.go | 33 + ag_201_coredns/plugin/file/nsec3_test.go | 28 + ag_201_coredns/plugin/file/reload.go | 69 + ag_201_coredns/plugin/file/reload_test.go | 90 ++ ag_201_coredns/plugin/file/rrutil/util.go | 18 + ag_201_coredns/plugin/file/secondary.go | 198 +++ ag_201_coredns/plugin/file/secondary_test.go | 146 ++ ag_201_coredns/plugin/file/setup.go | 144 ++ ag_201_coredns/plugin/file/setup_test.go | 124 ++ ag_201_coredns/plugin/file/shutdown.go | 9 + ag_201_coredns/plugin/file/tree/all.go | 21 + ag_201_coredns/plugin/file/tree/auth_walk.go | 58 + ag_201_coredns/plugin/file/tree/elem.go | 101 ++ ag_201_coredns/plugin/file/tree/glue.go | 44 + ag_201_coredns/plugin/file/tree/less.go | 59 + ag_201_coredns/plugin/file/tree/less_test.go | 80 + ag_201_coredns/plugin/file/tree/print.go | 62 + ag_201_coredns/plugin/file/tree/print_test.go | 102 ++ ag_201_coredns/plugin/file/tree/tree.go | 453 ++++++ ag_201_coredns/plugin/file/tree/walk.go | 33 + ag_201_coredns/plugin/file/wildcard.go | 13 + ag_201_coredns/plugin/file/wildcard_test.go | 298 ++++ ag_201_coredns/plugin/file/xfr.go | 45 + ag_201_coredns/plugin/file/xfr_test.go | 72 + ag_201_coredns/plugin/file/zone.go | 178 +++ ag_201_coredns/plugin/file/zone_test.go | 30 + ag_201_coredns/plugin/forward/README.md | 265 ++++ ag_201_coredns/plugin/forward/connect.go | 152 ++ ag_201_coredns/plugin/forward/dnstap.go | 63 + ag_201_coredns/plugin/forward/forward.go | 239 +++ ag_201_coredns/plugin/forward/forward_test.go | 24 + ag_201_coredns/plugin/forward/fuzz.go | 34 + ag_201_coredns/plugin/forward/health.go | 106 ++ ag_201_coredns/plugin/forward/health_test.go | 283 ++++ ag_201_coredns/plugin/forward/log_test.go | 5 + ag_201_coredns/plugin/forward/metrics.go | 61 + ag_201_coredns/plugin/forward/persistent.go | 161 ++ ag_201_coredns/plugin/forward/persistent_test.go | 109 ++ ag_201_coredns/plugin/forward/policy.go | 68 + ag_201_coredns/plugin/forward/proxy.go | 82 + ag_201_coredns/plugin/forward/proxy_test.go | 99 ++ ag_201_coredns/plugin/forward/setup.go | 292 ++++ ag_201_coredns/plugin/forward/setup_policy_test.go | 47 + ag_201_coredns/plugin/forward/setup_test.go | 334 ++++ ag_201_coredns/plugin/forward/type.go | 37 + ag_201_coredns/plugin/geoip/README.md | 96 ++ ag_201_coredns/plugin/geoip/city.go | 58 + ag_201_coredns/plugin/geoip/geoip.go | 107 ++ ag_201_coredns/plugin/geoip/geoip_test.go | 90 ++ ag_201_coredns/plugin/geoip/setup.go | 57 + ag_201_coredns/plugin/geoip/setup_test.go | 110 ++ .../plugin/geoip/testdata/GeoLite2-City.mmdb | Bin 0 -> 3281 bytes .../geoip/testdata/GeoLite2-UnknownDbType.mmdb | Bin 0 -> 3280 bytes ag_201_coredns/plugin/geoip/testdata/README.md | 112 ++ ag_201_coredns/plugin/grpc/README.md | 135 ++ ag_201_coredns/plugin/grpc/grpc.go | 143 ++ ag_201_coredns/plugin/grpc/grpc_test.go | 75 + ag_201_coredns/plugin/grpc/metrics.go | 31 + ag_201_coredns/plugin/grpc/policy.go | 68 + ag_201_coredns/plugin/grpc/proxy.go | 82 + ag_201_coredns/plugin/grpc/proxy_test.go | 66 + ag_201_coredns/plugin/grpc/setup.go | 147 ++ ag_201_coredns/plugin/grpc/setup_policy_test.go | 47 + ag_201_coredns/plugin/grpc/setup_test.go | 153 ++ ag_201_coredns/plugin/header/README.md | 63 + ag_201_coredns/plugin/header/handler.go | 27 + ag_201_coredns/plugin/header/header.go | 95 ++ ag_201_coredns/plugin/header/header_test.go | 152 ++ ag_201_coredns/plugin/header/setup.go | 74 + ag_201_coredns/plugin/header/setup_test.go | 65 + ag_201_coredns/plugin/health/README.md | 80 + ag_201_coredns/plugin/health/health.go | 84 + ag_201_coredns/plugin/health/health_test.go | 47 + ag_201_coredns/plugin/health/log_test.go | 5 + ag_201_coredns/plugin/health/overloaded.go | 84 + ag_201_coredns/plugin/health/overloaded_test.go | 41 + ag_201_coredns/plugin/health/setup.go | 66 + ag_201_coredns/plugin/health/setup_test.go | 45 + ag_201_coredns/plugin/hosts/README.md | 125 ++ ag_201_coredns/plugin/hosts/hosts.go | 122 ++ ag_201_coredns/plugin/hosts/hosts_test.go | 120 ++ ag_201_coredns/plugin/hosts/hostsfile.go | 259 ++++ ag_201_coredns/plugin/hosts/hostsfile_test.go | 241 +++ ag_201_coredns/plugin/hosts/log_test.go | 5 + ag_201_coredns/plugin/hosts/metrics.go | 25 + ag_201_coredns/plugin/hosts/setup.go | 157 ++ ag_201_coredns/plugin/hosts/setup_test.go | 169 ++ ag_201_coredns/plugin/import/README.md | 73 + ag_201_coredns/plugin/k8s_external/README.md | 113 ++ ag_201_coredns/plugin/k8s_external/apex.go | 112 ++ ag_201_coredns/plugin/k8s_external/apex_test.go | 122 ++ ag_201_coredns/plugin/k8s_external/external.go | 125 ++ .../plugin/k8s_external/external_test.go | 426 +++++ ag_201_coredns/plugin/k8s_external/msg_to_dns.go | 190 +++ ag_201_coredns/plugin/k8s_external/setup.go | 79 + ag_201_coredns/plugin/k8s_external/setup_test.go | 57 + ag_201_coredns/plugin/k8s_external/transfer.go | 150 ++ .../plugin/k8s_external/transfer_test.go | 148 ++ ag_201_coredns/plugin/kubernetes/README.md | 239 +++ ag_201_coredns/plugin/kubernetes/autopath.go | 62 + ag_201_coredns/plugin/kubernetes/controller.go | 757 +++++++++ .../plugin/kubernetes/controller_test.go | 238 +++ ag_201_coredns/plugin/kubernetes/external.go | 236 +++ ag_201_coredns/plugin/kubernetes/external_test.go | 199 +++ ag_201_coredns/plugin/kubernetes/handler.go | 94 ++ .../plugin/kubernetes/handler_case_test.go | 80 + .../kubernetes/handler_ignore_emptyservice_test.go | 67 + .../plugin/kubernetes/handler_pod_disabled_test.go | 60 + .../plugin/kubernetes/handler_pod_insecure_test.go | 95 ++ .../plugin/kubernetes/handler_pod_verified_test.go | 81 + ag_201_coredns/plugin/kubernetes/handler_test.go | 816 ++++++++++ ag_201_coredns/plugin/kubernetes/informer_test.go | 120 ++ ag_201_coredns/plugin/kubernetes/kubernetes.go | 601 ++++++++ .../plugin/kubernetes/kubernetes_apex_test.go | 92 ++ .../plugin/kubernetes/kubernetes_test.go | 367 +++++ ag_201_coredns/plugin/kubernetes/local.go | 37 + ag_201_coredns/plugin/kubernetes/log_test.go | 5 + ag_201_coredns/plugin/kubernetes/logger.go | 38 + ag_201_coredns/plugin/kubernetes/metadata.go | 62 + ag_201_coredns/plugin/kubernetes/metadata_test.go | 155 ++ .../plugin/kubernetes/metrics_test.backup | 203 +++ ag_201_coredns/plugin/kubernetes/namespace.go | 24 + ag_201_coredns/plugin/kubernetes/namespace_test.go | 72 + ag_201_coredns/plugin/kubernetes/ns.go | 103 ++ ag_201_coredns/plugin/kubernetes/ns_test.go | 219 +++ .../plugin/kubernetes/object/endpoint.go | 276 ++++ .../plugin/kubernetes/object/informer.go | 88 ++ ag_201_coredns/plugin/kubernetes/object/metrics.go | 82 + .../plugin/kubernetes/object/namespace.go | 61 + ag_201_coredns/plugin/kubernetes/object/object.go | 113 ++ ag_201_coredns/plugin/kubernetes/object/pod.go | 78 + ag_201_coredns/plugin/kubernetes/object/service.go | 120 ++ ag_201_coredns/plugin/kubernetes/parse.go | 103 ++ ag_201_coredns/plugin/kubernetes/parse_test.go | 62 + ag_201_coredns/plugin/kubernetes/ready.go | 4 + ag_201_coredns/plugin/kubernetes/reverse.go | 55 + ag_201_coredns/plugin/kubernetes/reverse_test.go | 256 +++ ag_201_coredns/plugin/kubernetes/setup.go | 253 +++ ag_201_coredns/plugin/kubernetes/setup_test.go | 612 ++++++++ ag_201_coredns/plugin/kubernetes/setup_ttl_test.go | 45 + ag_201_coredns/plugin/kubernetes/xfr.go | 195 +++ ag_201_coredns/plugin/kubernetes/xfr_test.go | 156 ++ ag_201_coredns/plugin/loadbalance/README.md | 33 + ag_201_coredns/plugin/loadbalance/handler.go | 24 + ag_201_coredns/plugin/loadbalance/loadbalance.go | 80 + .../plugin/loadbalance/loadbalance_test.go | 203 +++ ag_201_coredns/plugin/loadbalance/log_test.go | 5 + ag_201_coredns/plugin/loadbalance/setup.go | 43 + ag_201_coredns/plugin/loadbalance/setup_test.go | 43 + ag_201_coredns/plugin/local/README.md | 52 + ag_201_coredns/plugin/local/local.go | 127 ++ ag_201_coredns/plugin/local/local_test.go | 77 + ag_201_coredns/plugin/local/metrics.go | 18 + ag_201_coredns/plugin/local/setup.go | 20 + ag_201_coredns/plugin/log/README.md | 155 ++ ag_201_coredns/plugin/log/log.go | 74 + ag_201_coredns/plugin/log/log_test.go | 280 ++++ ag_201_coredns/plugin/log/setup.go | 102 ++ ag_201_coredns/plugin/log/setup_test.go | 184 +++ ag_201_coredns/plugin/log_test.go | 5 + ag_201_coredns/plugin/loop/README.md | 93 ++ ag_201_coredns/plugin/loop/log_test.go | 5 + ag_201_coredns/plugin/loop/loop.go | 109 ++ ag_201_coredns/plugin/loop/loop_test.go | 11 + ag_201_coredns/plugin/loop/setup.go | 87 ++ ag_201_coredns/plugin/loop/setup_test.go | 19 + ag_201_coredns/plugin/metadata/README.md | 49 + ag_201_coredns/plugin/metadata/log_test.go | 5 + ag_201_coredns/plugin/metadata/metadata.go | 44 + ag_201_coredns/plugin/metadata/metadata_test.go | 93 ++ ag_201_coredns/plugin/metadata/provider.go | 127 ++ ag_201_coredns/plugin/metadata/setup.go | 44 + ag_201_coredns/plugin/metadata/setup_test.go | 70 + ag_201_coredns/plugin/metrics/README.md | 90 ++ ag_201_coredns/plugin/metrics/context.go | 37 + ag_201_coredns/plugin/metrics/handler.go | 57 + ag_201_coredns/plugin/metrics/log_test.go | 5 + ag_201_coredns/plugin/metrics/metrics.go | 172 +++ ag_201_coredns/plugin/metrics/metrics_test.go | 82 + ag_201_coredns/plugin/metrics/recorder.go | 28 + ag_201_coredns/plugin/metrics/recorder_test.go | 68 + ag_201_coredns/plugin/metrics/registry.go | 28 + ag_201_coredns/plugin/metrics/setup.go | 105 ++ ag_201_coredns/plugin/metrics/setup_test.go | 42 + ag_201_coredns/plugin/metrics/vars/monitor.go | 36 + ag_201_coredns/plugin/metrics/vars/report.go | 33 + ag_201_coredns/plugin/metrics/vars/vars.go | 82 + ag_201_coredns/plugin/minimal/README.md | 36 + ag_201_coredns/plugin/minimal/minimal.go | 55 + ag_201_coredns/plugin/minimal/minimal_test.go | 153 ++ ag_201_coredns/plugin/minimal/setup.go | 24 + ag_201_coredns/plugin/minimal/setup_test.go | 19 + ag_201_coredns/plugin/normalize.go | 196 +++ ag_201_coredns/plugin/normalize_test.go | 140 ++ ag_201_coredns/plugin/nsid/README.md | 57 + ag_201_coredns/plugin/nsid/log_test.go | 5 + ag_201_coredns/plugin/nsid/nsid.go | 69 + ag_201_coredns/plugin/nsid/nsid_test.go | 136 ++ ag_201_coredns/plugin/nsid/setup.go | 45 + ag_201_coredns/plugin/nsid/setup_test.go | 68 + ag_201_coredns/plugin/pkg/cache/cache.go | 157 ++ ag_201_coredns/plugin/pkg/cache/cache_test.go | 85 + ag_201_coredns/plugin/pkg/cache/shard_test.go | 139 ++ ag_201_coredns/plugin/pkg/cidr/cidr.go | 83 + ag_201_coredns/plugin/pkg/cidr/cidr_test.go | 47 + ag_201_coredns/plugin/pkg/dnstest/multirecorder.go | 41 + .../plugin/pkg/dnstest/multirecorder_test.go | 38 + ag_201_coredns/plugin/pkg/dnstest/recorder.go | 54 + ag_201_coredns/plugin/pkg/dnstest/recorder_test.go | 50 + ag_201_coredns/plugin/pkg/dnstest/server.go | 65 + ag_201_coredns/plugin/pkg/dnstest/server_test.go | 37 + ag_201_coredns/plugin/pkg/dnsutil/cname.go | 15 + ag_201_coredns/plugin/pkg/dnsutil/cname_test.go | 55 + ag_201_coredns/plugin/pkg/dnsutil/doc.go | 2 + ag_201_coredns/plugin/pkg/dnsutil/join.go | 17 + ag_201_coredns/plugin/pkg/dnsutil/join_test.go | 21 + ag_201_coredns/plugin/pkg/dnsutil/reverse.go | 81 + ag_201_coredns/plugin/pkg/dnsutil/reverse_test.go | 70 + ag_201_coredns/plugin/pkg/dnsutil/ttl.go | 52 + ag_201_coredns/plugin/pkg/dnsutil/ttl_test.go | 72 + ag_201_coredns/plugin/pkg/dnsutil/zone.go | 20 + ag_201_coredns/plugin/pkg/dnsutil/zone_test.go | 39 + ag_201_coredns/plugin/pkg/doh/doh.go | 116 ++ ag_201_coredns/plugin/pkg/doh/doh_test.go | 52 + ag_201_coredns/plugin/pkg/edns/edns.go | 74 + ag_201_coredns/plugin/pkg/edns/edns_test.go | 37 + ag_201_coredns/plugin/pkg/expression/expression.go | 47 + .../plugin/pkg/expression/expression_test.go | 73 + ag_201_coredns/plugin/pkg/fall/fall.go | 71 + ag_201_coredns/plugin/pkg/fall/fall_test.go | 65 + ag_201_coredns/plugin/pkg/fuzz/do.go | 31 + ag_201_coredns/plugin/pkg/log/listener.go | 141 ++ ag_201_coredns/plugin/pkg/log/listener_test.go | 120 ++ ag_201_coredns/plugin/pkg/log/log.go | 113 ++ ag_201_coredns/plugin/pkg/log/log_test.go | 72 + ag_201_coredns/plugin/pkg/log/plugin.go | 91 ++ ag_201_coredns/plugin/pkg/log/plugin_test.go | 21 + ag_201_coredns/plugin/pkg/nonwriter/nonwriter.go | 21 + .../plugin/pkg/nonwriter/nonwriter_test.go | 19 + ag_201_coredns/plugin/pkg/parse/host.go | 115 ++ ag_201_coredns/plugin/pkg/parse/host_test.go | 111 ++ ag_201_coredns/plugin/pkg/parse/parse.go | 38 + ag_201_coredns/plugin/pkg/parse/parse_test.go | 59 + ag_201_coredns/plugin/pkg/parse/transport.go | 33 + ag_201_coredns/plugin/pkg/parse/transport_test.go | 25 + ag_201_coredns/plugin/pkg/rand/rand.go | 35 + ag_201_coredns/plugin/pkg/rcode/rcode.go | 15 + ag_201_coredns/plugin/pkg/rcode/rcode_test.go | 29 + ag_201_coredns/plugin/pkg/replacer/replacer.go | 277 ++++ .../plugin/pkg/replacer/replacer_test.go | 442 ++++++ ag_201_coredns/plugin/pkg/response/classify.go | 61 + ag_201_coredns/plugin/pkg/response/typify.go | 151 ++ ag_201_coredns/plugin/pkg/response/typify_test.go | 101 ++ .../plugin/pkg/reuseport/listen_no_reuseport.go | 13 + .../plugin/pkg/reuseport/listen_reuseport.go | 36 + .../plugin/pkg/singleflight/singleflight.go | 64 + .../plugin/pkg/singleflight/singleflight_test.go | 85 + ag_201_coredns/plugin/pkg/tls/tls.go | 146 ++ ag_201_coredns/plugin/pkg/tls/tls_test.go | 101 ++ ag_201_coredns/plugin/pkg/trace/trace.go | 13 + ag_201_coredns/plugin/pkg/transport/transport.go | 21 + ag_201_coredns/plugin/pkg/uniq/uniq.go | 46 + ag_201_coredns/plugin/pkg/uniq/uniq_test.go | 17 + ag_201_coredns/plugin/pkg/up/up.go | 83 + ag_201_coredns/plugin/pkg/up/up_test.go | 40 + ag_201_coredns/plugin/pkg/upstream/upstream.go | 35 + ag_201_coredns/plugin/plugin.go | 112 ++ ag_201_coredns/plugin/pprof/README.md | 74 + ag_201_coredns/plugin/pprof/log_test.go | 5 + ag_201_coredns/plugin/pprof/pprof.go | 60 + ag_201_coredns/plugin/pprof/setup.go | 65 + ag_201_coredns/plugin/pprof/setup_test.go | 44 + ag_201_coredns/plugin/ready/README.md | 58 + ag_201_coredns/plugin/ready/list.go | 56 + ag_201_coredns/plugin/ready/readiness.go | 7 + ag_201_coredns/plugin/ready/ready.go | 81 + ag_201_coredns/plugin/ready/ready_test.go | 69 + ag_201_coredns/plugin/ready/setup.go | 73 + ag_201_coredns/plugin/ready/setup_test.go | 34 + ag_201_coredns/plugin/register.go | 11 + ag_201_coredns/plugin/reload/README.md | 108 ++ ag_201_coredns/plugin/reload/log_test.go | 5 + ag_201_coredns/plugin/reload/metrics.go | 26 + ag_201_coredns/plugin/reload/reload.go | 127 ++ ag_201_coredns/plugin/reload/setup.go | 87 ++ ag_201_coredns/plugin/reload/setup_test.go | 51 + ag_201_coredns/plugin/rewrite/README.md | 406 +++++ ag_201_coredns/plugin/rewrite/class.go | 44 + ag_201_coredns/plugin/rewrite/edns0.go | 371 +++++ ag_201_coredns/plugin/rewrite/fuzz.go | 20 + ag_201_coredns/plugin/rewrite/log_test.go | 5 + ag_201_coredns/plugin/rewrite/name.go | 449 ++++++ ag_201_coredns/plugin/rewrite/name_test.go | 376 +++++ ag_201_coredns/plugin/rewrite/reverter.go | 146 ++ ag_201_coredns/plugin/rewrite/reverter_test.go | 177 +++ ag_201_coredns/plugin/rewrite/rewrite.go | 145 ++ ag_201_coredns/plugin/rewrite/rewrite_test.go | 747 +++++++++ ag_201_coredns/plugin/rewrite/setup.go | 42 + ag_201_coredns/plugin/rewrite/setup_test.go | 51 + ag_201_coredns/plugin/rewrite/ttl.go | 205 +++ ag_201_coredns/plugin/rewrite/ttl_test.go | 157 ++ ag_201_coredns/plugin/rewrite/type.go | 45 + ag_201_coredns/plugin/rewrite/wire.go | 35 + ag_201_coredns/plugin/root/README.md | 30 + ag_201_coredns/plugin/root/log_test.go | 5 + ag_201_coredns/plugin/root/root.go | 39 + ag_201_coredns/plugin/root/root_test.go | 102 ++ ag_201_coredns/plugin/route53/README.md | 131 ++ ag_201_coredns/plugin/route53/log_test.go | 5 + ag_201_coredns/plugin/route53/route53.go | 293 ++++ ag_201_coredns/plugin/route53/route53_test.go | 298 ++++ ag_201_coredns/plugin/route53/setup.go | 144 ++ ag_201_coredns/plugin/route53/setup_test.go | 87 ++ ag_201_coredns/plugin/secondary/README.md | 73 + ag_201_coredns/plugin/secondary/log_test.go | 5 + ag_201_coredns/plugin/secondary/secondary.go | 10 + ag_201_coredns/plugin/secondary/setup.go | 99 ++ ag_201_coredns/plugin/secondary/setup_test.go | 63 + ag_201_coredns/plugin/sign/README.md | 168 ++ ag_201_coredns/plugin/sign/dnssec.go | 20 + ag_201_coredns/plugin/sign/file.go | 92 ++ ag_201_coredns/plugin/sign/file_test.go | 43 + ag_201_coredns/plugin/sign/keys.go | 119 ++ ag_201_coredns/plugin/sign/log_test.go | 5 + ag_201_coredns/plugin/sign/nsec.go | 36 + ag_201_coredns/plugin/sign/nsec_test.go | 27 + ag_201_coredns/plugin/sign/resign_test.go | 40 + ag_201_coredns/plugin/sign/setup.go | 100 ++ ag_201_coredns/plugin/sign/setup_test.go | 75 + ag_201_coredns/plugin/sign/sign.go | 38 + ag_201_coredns/plugin/sign/signer.go | 210 +++ ag_201_coredns/plugin/sign/signer_test.go | 177 +++ .../plugin/sign/testdata/Kmiek.nl.+013+59725.key | 5 + .../sign/testdata/Kmiek.nl.+013+59725.private | 6 + ag_201_coredns/plugin/sign/testdata/db.miek.nl | 17 + ag_201_coredns/plugin/sign/testdata/db.miek.nl_ns | 10 + ag_201_coredns/plugin/template/README.md | 294 ++++ ag_201_coredns/plugin/template/cname_test.go | 96 ++ ag_201_coredns/plugin/template/log_test.go | 5 + ag_201_coredns/plugin/template/metrics.go | 32 + ag_201_coredns/plugin/template/setup.go | 145 ++ ag_201_coredns/plugin/template/setup_test.go | 176 +++ ag_201_coredns/plugin/template/template.go | 215 +++ ag_201_coredns/plugin/template/template_test.go | 642 ++++++++ ag_201_coredns/plugin/test/doc.go | 2 + ag_201_coredns/plugin/test/file.go | 106 ++ ag_201_coredns/plugin/test/file_test.go | 11 + ag_201_coredns/plugin/test/helpers.go | 329 ++++ ag_201_coredns/plugin/test/responsewriter.go | 80 + ag_201_coredns/plugin/test/scrape.go | 263 ++++ ag_201_coredns/plugin/tls/README.md | 73 + ag_201_coredns/plugin/tls/log_test.go | 5 + ag_201_coredns/plugin/tls/test_ca.pem | 20 + ag_201_coredns/plugin/tls/test_cert.pem | 20 + ag_201_coredns/plugin/tls/test_key.pem | 28 + ag_201_coredns/plugin/tls/tls.go | 71 + ag_201_coredns/plugin/tls/tls_test.go | 87 ++ ag_201_coredns/plugin/trace/README.md | 113 ++ ag_201_coredns/plugin/trace/log_test.go | 5 + ag_201_coredns/plugin/trace/logger.go | 20 + ag_201_coredns/plugin/trace/setup.go | 163 ++ ag_201_coredns/plugin/trace/setup_test.go | 88 ++ ag_201_coredns/plugin/trace/trace.go | 204 +++ ag_201_coredns/plugin/trace/trace_test.go | 173 +++ ag_201_coredns/plugin/transfer/README.md | 59 + .../plugin/transfer/failed_write_test.go | 30 + ag_201_coredns/plugin/transfer/notify.go | 58 + ag_201_coredns/plugin/transfer/select_test.go | 58 + ag_201_coredns/plugin/transfer/setup.go | 81 + ag_201_coredns/plugin/transfer/setup_test.go | 131 ++ ag_201_coredns/plugin/transfer/transfer.go | 221 +++ ag_201_coredns/plugin/transfer/transfer_test.go | 278 ++++ ag_201_coredns/plugin/tsig/README.md | 118 ++ ag_201_coredns/plugin/tsig/setup.go | 168 ++ ag_201_coredns/plugin/tsig/setup_test.go | 245 +++ ag_201_coredns/plugin/tsig/tsig.go | 140 ++ ag_201_coredns/plugin/tsig/tsig_test.go | 255 +++ ag_201_coredns/plugin/view/README.md | 135 ++ ag_201_coredns/plugin/view/metadata.go | 16 + ag_201_coredns/plugin/view/setup.go | 65 + ag_201_coredns/plugin/view/setup_test.go | 38 + ag_201_coredns/plugin/view/view.go | 48 + ag_201_coredns/plugin/whoami/README.md | 58 + ag_201_coredns/plugin/whoami/fuzz.go | 13 + ag_201_coredns/plugin/whoami/log_test.go | 5 + ag_201_coredns/plugin/whoami/setup.go | 22 + ag_201_coredns/plugin/whoami/setup_test.go | 19 + ag_201_coredns/plugin/whoami/whoami.go | 60 + ag_201_coredns/plugin/whoami/whoami_test.go | 81 + ag_201_coredns/request/edns0.go | 31 + ag_201_coredns/request/request.go | 363 +++++ ag_201_coredns/request/request_test.go | 283 ++++ ag_201_coredns/request/writer.go | 21 + ag_201_coredns/test/auto_test.go | 169 ++ ag_201_coredns/test/cache_test.go | 157 ++ ag_201_coredns/test/chaos_test.go | 37 + ag_201_coredns/test/compression_scrub_test.go | 60 + ag_201_coredns/test/corefile_test.go | 17 + ag_201_coredns/test/doc.go | 2 + ag_201_coredns/test/ds_file_test.go | 59 + ag_201_coredns/test/edns0_test.go | 32 + ag_201_coredns/test/erratic_autopath_test.go | 119 ++ ag_201_coredns/test/etcd_cache_test.go | 70 + ag_201_coredns/test/etcd_credentials_test.go | 84 + ag_201_coredns/test/etcd_test.go | 107 ++ ag_201_coredns/test/example_test.go | 16 + ag_201_coredns/test/file_cname_proxy_test.go | 77 + ag_201_coredns/test/file_loop_test.go | 48 + ag_201_coredns/test/file_reload_test.go | 67 + ag_201_coredns/test/file_serve_test.go | 100 ++ ag_201_coredns/test/file_srv_additional_test.go | 42 + ag_201_coredns/test/file_test.go | 16 + ag_201_coredns/test/file_upstream_test.go | 229 +++ ag_201_coredns/test/file_xfr_test.go | 102 ++ ag_201_coredns/test/fuzz_corefile.go | 12 + ag_201_coredns/test/grpc_test.go | 58 + ag_201_coredns/test/hosts_file_test.go | 39 + ag_201_coredns/test/log_test.go | 5 + ag_201_coredns/test/metric_naming_test.go | 163 ++ ag_201_coredns/test/metrics_test.go | 257 +++ ag_201_coredns/test/miek_test.go | 31 + ag_201_coredns/test/no_plugins_test.go | 28 + ag_201_coredns/test/plugin_dnssec_test.go | 75 + ag_201_coredns/test/presubmit_test.go | 362 +++++ ag_201_coredns/test/proxy_health_test.go | 80 + ag_201_coredns/test/proxy_test.go | 79 + ag_201_coredns/test/readme_test.go | 182 +++ ag_201_coredns/test/reload_test.go | 392 +++++ ag_201_coredns/test/reverse_test.go | 42 + ag_201_coredns/test/rewrite_test.go | 114 ++ ag_201_coredns/test/secondary_test.go | 217 +++ ag_201_coredns/test/server.go | 74 + ag_201_coredns/test/server_reverse_test.go | 141 ++ ag_201_coredns/test/server_test.go | 160 ++ ag_201_coredns/test/template_upstream_test.go | 71 + ag_201_coredns/test/tls_test.go | 46 + ag_201_coredns/test/tsig_test.go | 166 ++ ag_201_coredns/test/view_test.go | 163 ++ ag_201_coredns/test/wildcard_test.go | 89 ++ ag_201_coredns/testprivkey.pem | 30 + dohclient/dohclient.go | 27 + dohclient/dohclient.py | 18 + ...70\200\344\272\233\345\221\275\344\273\244.txt" | 4 + readme_1.md | 28 + 831 files changed, 82216 insertions(+) create mode 100644 ag_201_coredns/-text create mode 100644 ag_201_coredns/.circleci/config.yml create mode 100644 ag_201_coredns/.codecov.yml create mode 100644 ag_201_coredns/.dockerignore create mode 100644 ag_201_coredns/.dreck.yaml create mode 100644 ag_201_coredns/.github/CODE_OF_CONDUCT.md create mode 100644 ag_201_coredns/.github/CONTRIBUTING.md create mode 100644 ag_201_coredns/.github/ISSUE_TEMPLATE/bug-report.md create mode 100644 ag_201_coredns/.github/ISSUE_TEMPLATE/enhancement.md create mode 100644 ag_201_coredns/.github/ISSUE_TEMPLATE/question.md create mode 100644 ag_201_coredns/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 ag_201_coredns/.github/SECURITY.md create mode 100644 ag_201_coredns/.github/dependabot.yml create mode 100644 ag_201_coredns/.github/fixup_file_mtime.sh create mode 100644 ag_201_coredns/.github/workflows/cifuzz.yml create mode 100644 ag_201_coredns/.github/workflows/codeql-analysis.yml create mode 100644 ag_201_coredns/.github/workflows/depsreview.yml create mode 100644 ag_201_coredns/.github/workflows/docker.yml create mode 100644 ag_201_coredns/.github/workflows/go.coverage.yml create mode 100644 ag_201_coredns/.github/workflows/go.fmt.yml create mode 100644 ag_201_coredns/.github/workflows/go.test.yml create mode 100644 ag_201_coredns/.github/workflows/go.tidy.yml create mode 100644 ag_201_coredns/.github/workflows/golangci-lint.yml create mode 100644 ag_201_coredns/.github/workflows/make.doc.yml create mode 100644 ag_201_coredns/.github/workflows/reviewdog.yml create mode 100644 ag_201_coredns/.github/workflows/scorecards.yml create mode 100644 ag_201_coredns/.github/workflows/stale.yml create mode 100644 ag_201_coredns/.github/workflows/whitespace.yml create mode 100644 ag_201_coredns/.github/workflows/yamllint.yml create mode 100644 ag_201_coredns/.gitignore create mode 100644 ag_201_coredns/.golangci.yml create mode 100644 ag_201_coredns/.stickler.yml create mode 100644 ag_201_coredns/.yamllint create mode 100644 ag_201_coredns/ADOPTERS.md create mode 100644 ag_201_coredns/CODEOWNERS create mode 100644 ag_201_coredns/CODE_OF_CONDUCT.md create mode 100644 ag_201_coredns/CONTRIBUTING.md create mode 100644 ag_201_coredns/Corefile create mode 100644 ag_201_coredns/Corefile.1023 create mode 100644 ag_201_coredns/Corefile.223.5.5.5 create mode 100644 ag_201_coredns/Dockerfile create mode 100644 ag_201_coredns/GOVERNANCE.md create mode 100644 ag_201_coredns/LICENSE create mode 100644 ag_201_coredns/Makefile create mode 100644 ag_201_coredns/Makefile.doc create mode 100644 ag_201_coredns/Makefile.docker create mode 100644 ag_201_coredns/Makefile.release create mode 100644 ag_201_coredns/README.md create mode 100644 ag_201_coredns/SECURITY.md create mode 100644 ag_201_coredns/core/coredns.go create mode 100644 ag_201_coredns/core/dnsserver/address.go create mode 100644 ag_201_coredns/core/dnsserver/address_test.go create mode 100644 ag_201_coredns/core/dnsserver/config.go create mode 100644 ag_201_coredns/core/dnsserver/https.go create mode 100644 ag_201_coredns/core/dnsserver/https_test.go create mode 100644 ag_201_coredns/core/dnsserver/log_test.go create mode 100644 ag_201_coredns/core/dnsserver/onstartup.go create mode 100644 ag_201_coredns/core/dnsserver/onstartup_test.go create mode 100644 ag_201_coredns/core/dnsserver/register.go create mode 100644 ag_201_coredns/core/dnsserver/register_test.go create mode 100644 ag_201_coredns/core/dnsserver/server.go create mode 100644 ag_201_coredns/core/dnsserver/server_grpc.go create mode 100644 ag_201_coredns/core/dnsserver/server_https.go create mode 100644 ag_201_coredns/core/dnsserver/server_https_test.go create mode 100644 ag_201_coredns/core/dnsserver/server_test.go create mode 100644 ag_201_coredns/core/dnsserver/server_tls.go create mode 100644 ag_201_coredns/core/dnsserver/view.go create mode 100644 ag_201_coredns/core/dnsserver/zdirectives.go create mode 100644 ag_201_coredns/core/plugin/zplugin.go create mode 100644 ag_201_coredns/coredns.1.md create mode 100644 ag_201_coredns/coredns.go create mode 100644 ag_201_coredns/corefile.5.md create mode 100644 ag_201_coredns/coremain/run.go create mode 100644 ag_201_coredns/coremain/version.go create mode 100644 ag_201_coredns/directives_generate.go create mode 100644 ag_201_coredns/go.mod create mode 100644 ag_201_coredns/go.sum create mode 100644 ag_201_coredns/man/coredns-acl.7 create mode 100644 ag_201_coredns/man/coredns-any.7 create mode 100644 ag_201_coredns/man/coredns-auto.7 create mode 100644 ag_201_coredns/man/coredns-autopath.7 create mode 100644 ag_201_coredns/man/coredns-azure.7 create mode 100644 ag_201_coredns/man/coredns-bind.7 create mode 100644 ag_201_coredns/man/coredns-bufsize.7 create mode 100644 ag_201_coredns/man/coredns-cache.7 create mode 100644 ag_201_coredns/man/coredns-cancel.7 create mode 100644 ag_201_coredns/man/coredns-chaos.7 create mode 100644 ag_201_coredns/man/coredns-clouddns.7 create mode 100644 ag_201_coredns/man/coredns-debug.7 create mode 100644 ag_201_coredns/man/coredns-dns64.7 create mode 100644 ag_201_coredns/man/coredns-dnssec.7 create mode 100644 ag_201_coredns/man/coredns-dnstap.7 create mode 100644 ag_201_coredns/man/coredns-erratic.7 create mode 100644 ag_201_coredns/man/coredns-errors.7 create mode 100644 ag_201_coredns/man/coredns-etcd.7 create mode 100644 ag_201_coredns/man/coredns-file.7 create mode 100644 ag_201_coredns/man/coredns-forward.7 create mode 100644 ag_201_coredns/man/coredns-geoip.7 create mode 100644 ag_201_coredns/man/coredns-grpc.7 create mode 100644 ag_201_coredns/man/coredns-header.7 create mode 100644 ag_201_coredns/man/coredns-health.7 create mode 100644 ag_201_coredns/man/coredns-hosts.7 create mode 100644 ag_201_coredns/man/coredns-import.7 create mode 100644 ag_201_coredns/man/coredns-k8s_external.7 create mode 100644 ag_201_coredns/man/coredns-kubernetes.7 create mode 100644 ag_201_coredns/man/coredns-loadbalance.7 create mode 100644 ag_201_coredns/man/coredns-local.7 create mode 100644 ag_201_coredns/man/coredns-log.7 create mode 100644 ag_201_coredns/man/coredns-loop.7 create mode 100644 ag_201_coredns/man/coredns-metadata.7 create mode 100644 ag_201_coredns/man/coredns-metrics.7 create mode 100644 ag_201_coredns/man/coredns-minimal.7 create mode 100644 ag_201_coredns/man/coredns-nsid.7 create mode 100644 ag_201_coredns/man/coredns-pprof.7 create mode 100644 ag_201_coredns/man/coredns-ready.7 create mode 100644 ag_201_coredns/man/coredns-reload.7 create mode 100644 ag_201_coredns/man/coredns-rewrite.7 create mode 100644 ag_201_coredns/man/coredns-root.7 create mode 100644 ag_201_coredns/man/coredns-route53.7 create mode 100644 ag_201_coredns/man/coredns-secondary.7 create mode 100644 ag_201_coredns/man/coredns-sign.7 create mode 100644 ag_201_coredns/man/coredns-template.7 create mode 100644 ag_201_coredns/man/coredns-tls.7 create mode 100644 ag_201_coredns/man/coredns-trace.7 create mode 100644 ag_201_coredns/man/coredns-transfer.7 create mode 100644 ag_201_coredns/man/coredns-tsig.7 create mode 100644 ag_201_coredns/man/coredns-view.7 create mode 100644 ag_201_coredns/man/coredns-whoami.7 create mode 100644 ag_201_coredns/man/coredns.1 create mode 100644 ag_201_coredns/man/corefile.5 create mode 100644 ag_201_coredns/notes/coredns-0.9.10.md create mode 100644 ag_201_coredns/notes/coredns-0.9.9.md create mode 100644 ag_201_coredns/notes/coredns-001.md create mode 100644 ag_201_coredns/notes/coredns-002.md create mode 100644 ag_201_coredns/notes/coredns-003.md create mode 100644 ag_201_coredns/notes/coredns-004.md create mode 100644 ag_201_coredns/notes/coredns-005.md create mode 100644 ag_201_coredns/notes/coredns-006.md create mode 100644 ag_201_coredns/notes/coredns-007.md create mode 100644 ag_201_coredns/notes/coredns-008.md create mode 100644 ag_201_coredns/notes/coredns-009.md create mode 100644 ag_201_coredns/notes/coredns-010.md create mode 100644 ag_201_coredns/notes/coredns-011.md create mode 100644 ag_201_coredns/notes/coredns-1.0.0.md create mode 100644 ag_201_coredns/notes/coredns-1.0.1.md create mode 100644 ag_201_coredns/notes/coredns-1.0.2.md create mode 100644 ag_201_coredns/notes/coredns-1.0.3.md create mode 100644 ag_201_coredns/notes/coredns-1.0.4.md create mode 100644 ag_201_coredns/notes/coredns-1.0.5.md create mode 100644 ag_201_coredns/notes/coredns-1.0.6.md create mode 100644 ag_201_coredns/notes/coredns-1.1.0.md create mode 100644 ag_201_coredns/notes/coredns-1.1.1.md create mode 100644 ag_201_coredns/notes/coredns-1.1.2.md create mode 100644 ag_201_coredns/notes/coredns-1.1.3.md create mode 100644 ag_201_coredns/notes/coredns-1.1.4.md create mode 100644 ag_201_coredns/notes/coredns-1.10.0.md create mode 100644 ag_201_coredns/notes/coredns-1.2.0.md create mode 100644 ag_201_coredns/notes/coredns-1.2.1.md create mode 100644 ag_201_coredns/notes/coredns-1.2.2.md create mode 100644 ag_201_coredns/notes/coredns-1.2.3.md create mode 100644 ag_201_coredns/notes/coredns-1.2.4.md create mode 100644 ag_201_coredns/notes/coredns-1.2.5.md create mode 100644 ag_201_coredns/notes/coredns-1.2.6.md create mode 100644 ag_201_coredns/notes/coredns-1.3.0.md create mode 100644 ag_201_coredns/notes/coredns-1.3.1.md create mode 100644 ag_201_coredns/notes/coredns-1.4.0.md create mode 100644 ag_201_coredns/notes/coredns-1.5.0.md create mode 100644 ag_201_coredns/notes/coredns-1.5.1.md create mode 100644 ag_201_coredns/notes/coredns-1.5.2.md create mode 100644 ag_201_coredns/notes/coredns-1.6.0.md create mode 100644 ag_201_coredns/notes/coredns-1.6.1.md create mode 100644 ag_201_coredns/notes/coredns-1.6.2.md create mode 100644 ag_201_coredns/notes/coredns-1.6.3.md create mode 100644 ag_201_coredns/notes/coredns-1.6.4.md create mode 100644 ag_201_coredns/notes/coredns-1.6.5.md create mode 100644 ag_201_coredns/notes/coredns-1.6.6.md create mode 100644 ag_201_coredns/notes/coredns-1.6.7.md create mode 100644 ag_201_coredns/notes/coredns-1.6.8.md create mode 100644 ag_201_coredns/notes/coredns-1.6.9.md create mode 100644 ag_201_coredns/notes/coredns-1.7.0.md create mode 100644 ag_201_coredns/notes/coredns-1.7.1.md create mode 100644 ag_201_coredns/notes/coredns-1.8.0.md create mode 100644 ag_201_coredns/notes/coredns-1.8.1.md create mode 100644 ag_201_coredns/notes/coredns-1.8.2.md create mode 100644 ag_201_coredns/notes/coredns-1.8.3.md create mode 100644 ag_201_coredns/notes/coredns-1.8.4.md create mode 100644 ag_201_coredns/notes/coredns-1.8.5.md create mode 100644 ag_201_coredns/notes/coredns-1.8.6.md create mode 100644 ag_201_coredns/notes/coredns-1.8.7.md create mode 100644 ag_201_coredns/notes/coredns-1.9.0.md create mode 100644 ag_201_coredns/notes/coredns-1.9.1.md create mode 100644 ag_201_coredns/notes/coredns-1.9.2.md create mode 100644 ag_201_coredns/notes/coredns-1.9.3.md create mode 100644 ag_201_coredns/notes/coredns-1.9.4.md create mode 100644 ag_201_coredns/owners_generate.go create mode 100644 ag_201_coredns/pb/Makefile create mode 100644 ag_201_coredns/pb/dns.pb.go create mode 100644 ag_201_coredns/pb/dns.proto create mode 100644 ag_201_coredns/pb/dns_grpc.pb.go create mode 100644 ag_201_coredns/pkcs8.pem create mode 100644 ag_201_coredns/plugin.cfg create mode 100644 ag_201_coredns/plugin.md create mode 100644 ag_201_coredns/plugin/acl/README.md create mode 100644 ag_201_coredns/plugin/acl/acl.go create mode 100644 ag_201_coredns/plugin/acl/acl_test.go create mode 100644 ag_201_coredns/plugin/acl/metrics.go create mode 100644 ag_201_coredns/plugin/acl/setup.go create mode 100644 ag_201_coredns/plugin/acl/setup_test.go create mode 100644 ag_201_coredns/plugin/any/README.md create mode 100644 ag_201_coredns/plugin/any/any.go create mode 100644 ag_201_coredns/plugin/any/any_test.go create mode 100644 ag_201_coredns/plugin/any/setup.go create mode 100644 ag_201_coredns/plugin/auto/README.md create mode 100644 ag_201_coredns/plugin/auto/auto.go create mode 100644 ag_201_coredns/plugin/auto/log_test.go create mode 100644 ag_201_coredns/plugin/auto/regexp.go create mode 100644 ag_201_coredns/plugin/auto/regexp_test.go create mode 100644 ag_201_coredns/plugin/auto/setup.go create mode 100644 ag_201_coredns/plugin/auto/setup_test.go create mode 100644 ag_201_coredns/plugin/auto/walk.go create mode 100644 ag_201_coredns/plugin/auto/walk_test.go create mode 100644 ag_201_coredns/plugin/auto/watcher_test.go create mode 100644 ag_201_coredns/plugin/auto/xfr.go create mode 100644 ag_201_coredns/plugin/auto/zone.go create mode 100644 ag_201_coredns/plugin/autopath/README.md create mode 100644 ag_201_coredns/plugin/autopath/autopath.go create mode 100644 ag_201_coredns/plugin/autopath/autopath_test.go create mode 100644 ag_201_coredns/plugin/autopath/cname.go create mode 100644 ag_201_coredns/plugin/autopath/metrics.go create mode 100644 ag_201_coredns/plugin/autopath/setup.go create mode 100644 ag_201_coredns/plugin/autopath/setup_test.go create mode 100644 ag_201_coredns/plugin/azure/README.md create mode 100644 ag_201_coredns/plugin/azure/azure.go create mode 100644 ag_201_coredns/plugin/azure/azure_test.go create mode 100644 ag_201_coredns/plugin/azure/setup.go create mode 100644 ag_201_coredns/plugin/azure/setup_test.go create mode 100644 ag_201_coredns/plugin/backend.go create mode 100644 ag_201_coredns/plugin/backend_lookup.go create mode 100644 ag_201_coredns/plugin/bind/README.md create mode 100644 ag_201_coredns/plugin/bind/bind.go create mode 100644 ag_201_coredns/plugin/bind/log_test.go create mode 100644 ag_201_coredns/plugin/bind/setup.go create mode 100644 ag_201_coredns/plugin/bind/setup_test.go create mode 100644 ag_201_coredns/plugin/bufsize/README.md create mode 100644 ag_201_coredns/plugin/bufsize/bufsize.go create mode 100644 ag_201_coredns/plugin/bufsize/bufsize_test.go create mode 100644 ag_201_coredns/plugin/bufsize/setup.go create mode 100644 ag_201_coredns/plugin/bufsize/setup_test.go create mode 100644 ag_201_coredns/plugin/cache/README.md create mode 100644 ag_201_coredns/plugin/cache/cache.go create mode 100644 ag_201_coredns/plugin/cache/cache_test.go create mode 100644 ag_201_coredns/plugin/cache/dnssec.go create mode 100644 ag_201_coredns/plugin/cache/dnssec_test.go create mode 100644 ag_201_coredns/plugin/cache/error_test.go create mode 100644 ag_201_coredns/plugin/cache/freq/freq.go create mode 100644 ag_201_coredns/plugin/cache/freq/freq_test.go create mode 100644 ag_201_coredns/plugin/cache/fuzz.go create mode 100644 ag_201_coredns/plugin/cache/handler.go create mode 100644 ag_201_coredns/plugin/cache/item.go create mode 100644 ag_201_coredns/plugin/cache/log_test.go create mode 100644 ag_201_coredns/plugin/cache/metrics.go create mode 100644 ag_201_coredns/plugin/cache/prefech_test.go create mode 100644 ag_201_coredns/plugin/cache/setup.go create mode 100644 ag_201_coredns/plugin/cache/setup_test.go create mode 100644 ag_201_coredns/plugin/cache/spoof_test.go create mode 100644 ag_201_coredns/plugin/cancel/README.md create mode 100644 ag_201_coredns/plugin/cancel/cancel.go create mode 100644 ag_201_coredns/plugin/cancel/cancel_test.go create mode 100644 ag_201_coredns/plugin/cancel/setup_test.go create mode 100644 ag_201_coredns/plugin/chaos/README.md create mode 100644 ag_201_coredns/plugin/chaos/chaos.go create mode 100644 ag_201_coredns/plugin/chaos/chaos_test.go create mode 100644 ag_201_coredns/plugin/chaos/fuzz.go create mode 100644 ag_201_coredns/plugin/chaos/log_test.go create mode 100644 ag_201_coredns/plugin/chaos/setup.go create mode 100644 ag_201_coredns/plugin/chaos/setup_test.go create mode 100644 ag_201_coredns/plugin/chaos/zowners.go create mode 100644 ag_201_coredns/plugin/clouddns/README.md create mode 100644 ag_201_coredns/plugin/clouddns/clouddns.go create mode 100644 ag_201_coredns/plugin/clouddns/clouddns_test.go create mode 100644 ag_201_coredns/plugin/clouddns/gcp.go create mode 100644 ag_201_coredns/plugin/clouddns/log_test.go create mode 100644 ag_201_coredns/plugin/clouddns/setup.go create mode 100644 ag_201_coredns/plugin/clouddns/setup_test.go create mode 100644 ag_201_coredns/plugin/debug/README.md create mode 100644 ag_201_coredns/plugin/debug/debug.go create mode 100644 ag_201_coredns/plugin/debug/debug_test.go create mode 100644 ag_201_coredns/plugin/debug/log_test.go create mode 100644 ag_201_coredns/plugin/debug/pcap.go create mode 100644 ag_201_coredns/plugin/debug/pcap_test.go create mode 100644 ag_201_coredns/plugin/deprecated/setup.go create mode 100644 ag_201_coredns/plugin/dns64/README.md create mode 100644 ag_201_coredns/plugin/dns64/dns64.go create mode 100644 ag_201_coredns/plugin/dns64/dns64_test.go create mode 100644 ag_201_coredns/plugin/dns64/metrics.go create mode 100644 ag_201_coredns/plugin/dns64/setup.go create mode 100644 ag_201_coredns/plugin/dns64/setup_test.go create mode 100644 ag_201_coredns/plugin/dnsovertor.zip create mode 100644 ag_201_coredns/plugin/dnsovertor/dnsovertor.go create mode 100644 ag_201_coredns/plugin/dnsovertor/metrics.go create mode 100644 ag_201_coredns/plugin/dnsovertor/ready.go create mode 100644 ag_201_coredns/plugin/dnsovertor/setup.go create mode 100644 ag_201_coredns/plugin/dnsovertor/setup_test.go create mode 100644 ag_201_coredns/plugin/dnssec/README.md create mode 100644 ag_201_coredns/plugin/dnssec/black_lies.go create mode 100644 ag_201_coredns/plugin/dnssec/black_lies_bitmap_test.go create mode 100644 ag_201_coredns/plugin/dnssec/black_lies_test.go create mode 100644 ag_201_coredns/plugin/dnssec/cache.go create mode 100644 ag_201_coredns/plugin/dnssec/cache_test.go create mode 100644 ag_201_coredns/plugin/dnssec/dnskey.go create mode 100644 ag_201_coredns/plugin/dnssec/dnssec.go create mode 100644 ag_201_coredns/plugin/dnssec/dnssec_test.go create mode 100644 ag_201_coredns/plugin/dnssec/handler.go create mode 100644 ag_201_coredns/plugin/dnssec/handler_test.go create mode 100644 ag_201_coredns/plugin/dnssec/log_test.go create mode 100644 ag_201_coredns/plugin/dnssec/metrics.go create mode 100644 ag_201_coredns/plugin/dnssec/responsewriter.go create mode 100644 ag_201_coredns/plugin/dnssec/rrsig.go create mode 100644 ag_201_coredns/plugin/dnssec/setup.go create mode 100644 ag_201_coredns/plugin/dnssec/setup_test.go create mode 100644 ag_201_coredns/plugin/dnstap/README.md create mode 100644 ag_201_coredns/plugin/dnstap/encoder.go create mode 100644 ag_201_coredns/plugin/dnstap/handler.go create mode 100644 ag_201_coredns/plugin/dnstap/handler_test.go create mode 100644 ag_201_coredns/plugin/dnstap/io.go create mode 100644 ag_201_coredns/plugin/dnstap/io_test.go create mode 100644 ag_201_coredns/plugin/dnstap/log_test.go create mode 100644 ag_201_coredns/plugin/dnstap/msg/msg.go create mode 100644 ag_201_coredns/plugin/dnstap/setup.go create mode 100644 ag_201_coredns/plugin/dnstap/setup_test.go create mode 100644 ag_201_coredns/plugin/dnstap/writer.go create mode 100644 ag_201_coredns/plugin/done.go create mode 100644 ag_201_coredns/plugin/erratic/README.md create mode 100644 ag_201_coredns/plugin/erratic/autopath.go create mode 100644 ag_201_coredns/plugin/erratic/erratic.go create mode 100644 ag_201_coredns/plugin/erratic/erratic_test.go create mode 100644 ag_201_coredns/plugin/erratic/log_test.go create mode 100644 ag_201_coredns/plugin/erratic/ready.go create mode 100644 ag_201_coredns/plugin/erratic/setup.go create mode 100644 ag_201_coredns/plugin/erratic/setup_test.go create mode 100644 ag_201_coredns/plugin/erratic/xfr.go create mode 100644 ag_201_coredns/plugin/errors/README.md create mode 100644 ag_201_coredns/plugin/errors/benchmark_test.go create mode 100644 ag_201_coredns/plugin/errors/errors.go create mode 100644 ag_201_coredns/plugin/errors/errors_test.go create mode 100644 ag_201_coredns/plugin/errors/log_test.go create mode 100644 ag_201_coredns/plugin/errors/setup.go create mode 100644 ag_201_coredns/plugin/errors/setup_test.go create mode 100644 ag_201_coredns/plugin/etcd/README.md create mode 100644 ag_201_coredns/plugin/etcd/cname_test.go create mode 100644 ag_201_coredns/plugin/etcd/etcd.go create mode 100644 ag_201_coredns/plugin/etcd/group_test.go create mode 100644 ag_201_coredns/plugin/etcd/handler.go create mode 100644 ag_201_coredns/plugin/etcd/log_test.go create mode 100644 ag_201_coredns/plugin/etcd/lookup_test.go create mode 100644 ag_201_coredns/plugin/etcd/msg/path.go create mode 100644 ag_201_coredns/plugin/etcd/msg/path_test.go create mode 100644 ag_201_coredns/plugin/etcd/msg/service.go create mode 100644 ag_201_coredns/plugin/etcd/msg/service_test.go create mode 100644 ag_201_coredns/plugin/etcd/msg/type.go create mode 100644 ag_201_coredns/plugin/etcd/msg/type_test.go create mode 100644 ag_201_coredns/plugin/etcd/multi_test.go create mode 100644 ag_201_coredns/plugin/etcd/other_test.go create mode 100644 ag_201_coredns/plugin/etcd/setup.go create mode 100644 ag_201_coredns/plugin/etcd/setup_test.go create mode 100644 ag_201_coredns/plugin/etcd/xfr.go create mode 100644 ag_201_coredns/plugin/file/README.md create mode 100644 ag_201_coredns/plugin/file/apex_test.go create mode 100644 ag_201_coredns/plugin/file/closest.go create mode 100644 ag_201_coredns/plugin/file/closest_test.go create mode 100644 ag_201_coredns/plugin/file/delegation_test.go create mode 100644 ag_201_coredns/plugin/file/delete_test.go create mode 100644 ag_201_coredns/plugin/file/dname.go create mode 100644 ag_201_coredns/plugin/file/dname_test.go create mode 100644 ag_201_coredns/plugin/file/dnssec_test.go create mode 100644 ag_201_coredns/plugin/file/dnssex_test.go create mode 100644 ag_201_coredns/plugin/file/ds_test.go create mode 100644 ag_201_coredns/plugin/file/ent_test.go create mode 100644 ag_201_coredns/plugin/file/example_org.go create mode 100644 ag_201_coredns/plugin/file/file.go create mode 100644 ag_201_coredns/plugin/file/file_test.go create mode 100644 ag_201_coredns/plugin/file/fuzz.go create mode 100644 ag_201_coredns/plugin/file/glue_test.go create mode 100644 ag_201_coredns/plugin/file/include_test.go create mode 100644 ag_201_coredns/plugin/file/log_test.go create mode 100644 ag_201_coredns/plugin/file/lookup.go create mode 100644 ag_201_coredns/plugin/file/lookup_test.go create mode 100644 ag_201_coredns/plugin/file/notify.go create mode 100644 ag_201_coredns/plugin/file/nsec3_test.go create mode 100644 ag_201_coredns/plugin/file/reload.go create mode 100644 ag_201_coredns/plugin/file/reload_test.go create mode 100644 ag_201_coredns/plugin/file/rrutil/util.go create mode 100644 ag_201_coredns/plugin/file/secondary.go create mode 100644 ag_201_coredns/plugin/file/secondary_test.go create mode 100644 ag_201_coredns/plugin/file/setup.go create mode 100644 ag_201_coredns/plugin/file/setup_test.go create mode 100644 ag_201_coredns/plugin/file/shutdown.go create mode 100644 ag_201_coredns/plugin/file/tree/all.go create mode 100644 ag_201_coredns/plugin/file/tree/auth_walk.go create mode 100644 ag_201_coredns/plugin/file/tree/elem.go create mode 100644 ag_201_coredns/plugin/file/tree/glue.go create mode 100644 ag_201_coredns/plugin/file/tree/less.go create mode 100644 ag_201_coredns/plugin/file/tree/less_test.go create mode 100644 ag_201_coredns/plugin/file/tree/print.go create mode 100644 ag_201_coredns/plugin/file/tree/print_test.go create mode 100644 ag_201_coredns/plugin/file/tree/tree.go create mode 100644 ag_201_coredns/plugin/file/tree/walk.go create mode 100644 ag_201_coredns/plugin/file/wildcard.go create mode 100644 ag_201_coredns/plugin/file/wildcard_test.go create mode 100644 ag_201_coredns/plugin/file/xfr.go create mode 100644 ag_201_coredns/plugin/file/xfr_test.go create mode 100644 ag_201_coredns/plugin/file/zone.go create mode 100644 ag_201_coredns/plugin/file/zone_test.go create mode 100644 ag_201_coredns/plugin/forward/README.md create mode 100644 ag_201_coredns/plugin/forward/connect.go create mode 100644 ag_201_coredns/plugin/forward/dnstap.go create mode 100644 ag_201_coredns/plugin/forward/forward.go create mode 100644 ag_201_coredns/plugin/forward/forward_test.go create mode 100644 ag_201_coredns/plugin/forward/fuzz.go create mode 100644 ag_201_coredns/plugin/forward/health.go create mode 100644 ag_201_coredns/plugin/forward/health_test.go create mode 100644 ag_201_coredns/plugin/forward/log_test.go create mode 100644 ag_201_coredns/plugin/forward/metrics.go create mode 100644 ag_201_coredns/plugin/forward/persistent.go create mode 100644 ag_201_coredns/plugin/forward/persistent_test.go create mode 100644 ag_201_coredns/plugin/forward/policy.go create mode 100644 ag_201_coredns/plugin/forward/proxy.go create mode 100644 ag_201_coredns/plugin/forward/proxy_test.go create mode 100644 ag_201_coredns/plugin/forward/setup.go create mode 100644 ag_201_coredns/plugin/forward/setup_policy_test.go create mode 100644 ag_201_coredns/plugin/forward/setup_test.go create mode 100644 ag_201_coredns/plugin/forward/type.go create mode 100644 ag_201_coredns/plugin/geoip/README.md create mode 100644 ag_201_coredns/plugin/geoip/city.go create mode 100644 ag_201_coredns/plugin/geoip/geoip.go create mode 100644 ag_201_coredns/plugin/geoip/geoip_test.go create mode 100644 ag_201_coredns/plugin/geoip/setup.go create mode 100644 ag_201_coredns/plugin/geoip/setup_test.go create mode 100644 ag_201_coredns/plugin/geoip/testdata/GeoLite2-City.mmdb create mode 100644 ag_201_coredns/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb create mode 100644 ag_201_coredns/plugin/geoip/testdata/README.md create mode 100644 ag_201_coredns/plugin/grpc/README.md create mode 100644 ag_201_coredns/plugin/grpc/grpc.go create mode 100644 ag_201_coredns/plugin/grpc/grpc_test.go create mode 100644 ag_201_coredns/plugin/grpc/metrics.go create mode 100644 ag_201_coredns/plugin/grpc/policy.go create mode 100644 ag_201_coredns/plugin/grpc/proxy.go create mode 100644 ag_201_coredns/plugin/grpc/proxy_test.go create mode 100644 ag_201_coredns/plugin/grpc/setup.go create mode 100644 ag_201_coredns/plugin/grpc/setup_policy_test.go create mode 100644 ag_201_coredns/plugin/grpc/setup_test.go create mode 100644 ag_201_coredns/plugin/header/README.md create mode 100644 ag_201_coredns/plugin/header/handler.go create mode 100644 ag_201_coredns/plugin/header/header.go create mode 100644 ag_201_coredns/plugin/header/header_test.go create mode 100644 ag_201_coredns/plugin/header/setup.go create mode 100644 ag_201_coredns/plugin/header/setup_test.go create mode 100644 ag_201_coredns/plugin/health/README.md create mode 100644 ag_201_coredns/plugin/health/health.go create mode 100644 ag_201_coredns/plugin/health/health_test.go create mode 100644 ag_201_coredns/plugin/health/log_test.go create mode 100644 ag_201_coredns/plugin/health/overloaded.go create mode 100644 ag_201_coredns/plugin/health/overloaded_test.go create mode 100644 ag_201_coredns/plugin/health/setup.go create mode 100644 ag_201_coredns/plugin/health/setup_test.go create mode 100644 ag_201_coredns/plugin/hosts/README.md create mode 100644 ag_201_coredns/plugin/hosts/hosts.go create mode 100644 ag_201_coredns/plugin/hosts/hosts_test.go create mode 100644 ag_201_coredns/plugin/hosts/hostsfile.go create mode 100644 ag_201_coredns/plugin/hosts/hostsfile_test.go create mode 100644 ag_201_coredns/plugin/hosts/log_test.go create mode 100644 ag_201_coredns/plugin/hosts/metrics.go create mode 100644 ag_201_coredns/plugin/hosts/setup.go create mode 100644 ag_201_coredns/plugin/hosts/setup_test.go create mode 100644 ag_201_coredns/plugin/import/README.md create mode 100644 ag_201_coredns/plugin/k8s_external/README.md create mode 100644 ag_201_coredns/plugin/k8s_external/apex.go create mode 100644 ag_201_coredns/plugin/k8s_external/apex_test.go create mode 100644 ag_201_coredns/plugin/k8s_external/external.go create mode 100644 ag_201_coredns/plugin/k8s_external/external_test.go create mode 100644 ag_201_coredns/plugin/k8s_external/msg_to_dns.go create mode 100644 ag_201_coredns/plugin/k8s_external/setup.go create mode 100644 ag_201_coredns/plugin/k8s_external/setup_test.go create mode 100644 ag_201_coredns/plugin/k8s_external/transfer.go create mode 100644 ag_201_coredns/plugin/k8s_external/transfer_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/README.md create mode 100644 ag_201_coredns/plugin/kubernetes/autopath.go create mode 100644 ag_201_coredns/plugin/kubernetes/controller.go create mode 100644 ag_201_coredns/plugin/kubernetes/controller_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/external.go create mode 100644 ag_201_coredns/plugin/kubernetes/external_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/handler.go create mode 100644 ag_201_coredns/plugin/kubernetes/handler_case_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/handler_ignore_emptyservice_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/handler_pod_disabled_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/handler_pod_insecure_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/handler_pod_verified_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/handler_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/informer_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/kubernetes.go create mode 100644 ag_201_coredns/plugin/kubernetes/kubernetes_apex_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/kubernetes_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/local.go create mode 100644 ag_201_coredns/plugin/kubernetes/log_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/logger.go create mode 100644 ag_201_coredns/plugin/kubernetes/metadata.go create mode 100644 ag_201_coredns/plugin/kubernetes/metadata_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/metrics_test.backup create mode 100644 ag_201_coredns/plugin/kubernetes/namespace.go create mode 100644 ag_201_coredns/plugin/kubernetes/namespace_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/ns.go create mode 100644 ag_201_coredns/plugin/kubernetes/ns_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/object/endpoint.go create mode 100644 ag_201_coredns/plugin/kubernetes/object/informer.go create mode 100644 ag_201_coredns/plugin/kubernetes/object/metrics.go create mode 100644 ag_201_coredns/plugin/kubernetes/object/namespace.go create mode 100644 ag_201_coredns/plugin/kubernetes/object/object.go create mode 100644 ag_201_coredns/plugin/kubernetes/object/pod.go create mode 100644 ag_201_coredns/plugin/kubernetes/object/service.go create mode 100644 ag_201_coredns/plugin/kubernetes/parse.go create mode 100644 ag_201_coredns/plugin/kubernetes/parse_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/ready.go create mode 100644 ag_201_coredns/plugin/kubernetes/reverse.go create mode 100644 ag_201_coredns/plugin/kubernetes/reverse_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/setup.go create mode 100644 ag_201_coredns/plugin/kubernetes/setup_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/setup_ttl_test.go create mode 100644 ag_201_coredns/plugin/kubernetes/xfr.go create mode 100644 ag_201_coredns/plugin/kubernetes/xfr_test.go create mode 100644 ag_201_coredns/plugin/loadbalance/README.md create mode 100644 ag_201_coredns/plugin/loadbalance/handler.go create mode 100644 ag_201_coredns/plugin/loadbalance/loadbalance.go create mode 100644 ag_201_coredns/plugin/loadbalance/loadbalance_test.go create mode 100644 ag_201_coredns/plugin/loadbalance/log_test.go create mode 100644 ag_201_coredns/plugin/loadbalance/setup.go create mode 100644 ag_201_coredns/plugin/loadbalance/setup_test.go create mode 100644 ag_201_coredns/plugin/local/README.md create mode 100644 ag_201_coredns/plugin/local/local.go create mode 100644 ag_201_coredns/plugin/local/local_test.go create mode 100644 ag_201_coredns/plugin/local/metrics.go create mode 100644 ag_201_coredns/plugin/local/setup.go create mode 100644 ag_201_coredns/plugin/log/README.md create mode 100644 ag_201_coredns/plugin/log/log.go create mode 100644 ag_201_coredns/plugin/log/log_test.go create mode 100644 ag_201_coredns/plugin/log/setup.go create mode 100644 ag_201_coredns/plugin/log/setup_test.go create mode 100644 ag_201_coredns/plugin/log_test.go create mode 100644 ag_201_coredns/plugin/loop/README.md create mode 100644 ag_201_coredns/plugin/loop/log_test.go create mode 100644 ag_201_coredns/plugin/loop/loop.go create mode 100644 ag_201_coredns/plugin/loop/loop_test.go create mode 100644 ag_201_coredns/plugin/loop/setup.go create mode 100644 ag_201_coredns/plugin/loop/setup_test.go create mode 100644 ag_201_coredns/plugin/metadata/README.md create mode 100644 ag_201_coredns/plugin/metadata/log_test.go create mode 100644 ag_201_coredns/plugin/metadata/metadata.go create mode 100644 ag_201_coredns/plugin/metadata/metadata_test.go create mode 100644 ag_201_coredns/plugin/metadata/provider.go create mode 100644 ag_201_coredns/plugin/metadata/setup.go create mode 100644 ag_201_coredns/plugin/metadata/setup_test.go create mode 100644 ag_201_coredns/plugin/metrics/README.md create mode 100644 ag_201_coredns/plugin/metrics/context.go create mode 100644 ag_201_coredns/plugin/metrics/handler.go create mode 100644 ag_201_coredns/plugin/metrics/log_test.go create mode 100644 ag_201_coredns/plugin/metrics/metrics.go create mode 100644 ag_201_coredns/plugin/metrics/metrics_test.go create mode 100644 ag_201_coredns/plugin/metrics/recorder.go create mode 100644 ag_201_coredns/plugin/metrics/recorder_test.go create mode 100644 ag_201_coredns/plugin/metrics/registry.go create mode 100644 ag_201_coredns/plugin/metrics/setup.go create mode 100644 ag_201_coredns/plugin/metrics/setup_test.go create mode 100644 ag_201_coredns/plugin/metrics/vars/monitor.go create mode 100644 ag_201_coredns/plugin/metrics/vars/report.go create mode 100644 ag_201_coredns/plugin/metrics/vars/vars.go create mode 100644 ag_201_coredns/plugin/minimal/README.md create mode 100644 ag_201_coredns/plugin/minimal/minimal.go create mode 100644 ag_201_coredns/plugin/minimal/minimal_test.go create mode 100644 ag_201_coredns/plugin/minimal/setup.go create mode 100644 ag_201_coredns/plugin/minimal/setup_test.go create mode 100644 ag_201_coredns/plugin/normalize.go create mode 100644 ag_201_coredns/plugin/normalize_test.go create mode 100644 ag_201_coredns/plugin/nsid/README.md create mode 100644 ag_201_coredns/plugin/nsid/log_test.go create mode 100644 ag_201_coredns/plugin/nsid/nsid.go create mode 100644 ag_201_coredns/plugin/nsid/nsid_test.go create mode 100644 ag_201_coredns/plugin/nsid/setup.go create mode 100644 ag_201_coredns/plugin/nsid/setup_test.go create mode 100644 ag_201_coredns/plugin/pkg/cache/cache.go create mode 100644 ag_201_coredns/plugin/pkg/cache/cache_test.go create mode 100644 ag_201_coredns/plugin/pkg/cache/shard_test.go create mode 100644 ag_201_coredns/plugin/pkg/cidr/cidr.go create mode 100644 ag_201_coredns/plugin/pkg/cidr/cidr_test.go create mode 100644 ag_201_coredns/plugin/pkg/dnstest/multirecorder.go create mode 100644 ag_201_coredns/plugin/pkg/dnstest/multirecorder_test.go create mode 100644 ag_201_coredns/plugin/pkg/dnstest/recorder.go create mode 100644 ag_201_coredns/plugin/pkg/dnstest/recorder_test.go create mode 100644 ag_201_coredns/plugin/pkg/dnstest/server.go create mode 100644 ag_201_coredns/plugin/pkg/dnstest/server_test.go create mode 100644 ag_201_coredns/plugin/pkg/dnsutil/cname.go create mode 100644 ag_201_coredns/plugin/pkg/dnsutil/cname_test.go create mode 100644 ag_201_coredns/plugin/pkg/dnsutil/doc.go create mode 100644 ag_201_coredns/plugin/pkg/dnsutil/join.go create mode 100644 ag_201_coredns/plugin/pkg/dnsutil/join_test.go create mode 100644 ag_201_coredns/plugin/pkg/dnsutil/reverse.go create mode 100644 ag_201_coredns/plugin/pkg/dnsutil/reverse_test.go create mode 100644 ag_201_coredns/plugin/pkg/dnsutil/ttl.go create mode 100644 ag_201_coredns/plugin/pkg/dnsutil/ttl_test.go create mode 100644 ag_201_coredns/plugin/pkg/dnsutil/zone.go create mode 100644 ag_201_coredns/plugin/pkg/dnsutil/zone_test.go create mode 100644 ag_201_coredns/plugin/pkg/doh/doh.go create mode 100644 ag_201_coredns/plugin/pkg/doh/doh_test.go create mode 100644 ag_201_coredns/plugin/pkg/edns/edns.go create mode 100644 ag_201_coredns/plugin/pkg/edns/edns_test.go create mode 100644 ag_201_coredns/plugin/pkg/expression/expression.go create mode 100644 ag_201_coredns/plugin/pkg/expression/expression_test.go create mode 100644 ag_201_coredns/plugin/pkg/fall/fall.go create mode 100644 ag_201_coredns/plugin/pkg/fall/fall_test.go create mode 100644 ag_201_coredns/plugin/pkg/fuzz/do.go create mode 100644 ag_201_coredns/plugin/pkg/log/listener.go create mode 100644 ag_201_coredns/plugin/pkg/log/listener_test.go create mode 100644 ag_201_coredns/plugin/pkg/log/log.go create mode 100644 ag_201_coredns/plugin/pkg/log/log_test.go create mode 100644 ag_201_coredns/plugin/pkg/log/plugin.go create mode 100644 ag_201_coredns/plugin/pkg/log/plugin_test.go create mode 100644 ag_201_coredns/plugin/pkg/nonwriter/nonwriter.go create mode 100644 ag_201_coredns/plugin/pkg/nonwriter/nonwriter_test.go create mode 100644 ag_201_coredns/plugin/pkg/parse/host.go create mode 100644 ag_201_coredns/plugin/pkg/parse/host_test.go create mode 100644 ag_201_coredns/plugin/pkg/parse/parse.go create mode 100644 ag_201_coredns/plugin/pkg/parse/parse_test.go create mode 100644 ag_201_coredns/plugin/pkg/parse/transport.go create mode 100644 ag_201_coredns/plugin/pkg/parse/transport_test.go create mode 100644 ag_201_coredns/plugin/pkg/rand/rand.go create mode 100644 ag_201_coredns/plugin/pkg/rcode/rcode.go create mode 100644 ag_201_coredns/plugin/pkg/rcode/rcode_test.go create mode 100644 ag_201_coredns/plugin/pkg/replacer/replacer.go create mode 100644 ag_201_coredns/plugin/pkg/replacer/replacer_test.go create mode 100644 ag_201_coredns/plugin/pkg/response/classify.go create mode 100644 ag_201_coredns/plugin/pkg/response/typify.go create mode 100644 ag_201_coredns/plugin/pkg/response/typify_test.go create mode 100644 ag_201_coredns/plugin/pkg/reuseport/listen_no_reuseport.go create mode 100644 ag_201_coredns/plugin/pkg/reuseport/listen_reuseport.go create mode 100644 ag_201_coredns/plugin/pkg/singleflight/singleflight.go create mode 100644 ag_201_coredns/plugin/pkg/singleflight/singleflight_test.go create mode 100644 ag_201_coredns/plugin/pkg/tls/tls.go create mode 100644 ag_201_coredns/plugin/pkg/tls/tls_test.go create mode 100644 ag_201_coredns/plugin/pkg/trace/trace.go create mode 100644 ag_201_coredns/plugin/pkg/transport/transport.go create mode 100644 ag_201_coredns/plugin/pkg/uniq/uniq.go create mode 100644 ag_201_coredns/plugin/pkg/uniq/uniq_test.go create mode 100644 ag_201_coredns/plugin/pkg/up/up.go create mode 100644 ag_201_coredns/plugin/pkg/up/up_test.go create mode 100644 ag_201_coredns/plugin/pkg/upstream/upstream.go create mode 100644 ag_201_coredns/plugin/plugin.go create mode 100644 ag_201_coredns/plugin/pprof/README.md create mode 100644 ag_201_coredns/plugin/pprof/log_test.go create mode 100644 ag_201_coredns/plugin/pprof/pprof.go create mode 100644 ag_201_coredns/plugin/pprof/setup.go create mode 100644 ag_201_coredns/plugin/pprof/setup_test.go create mode 100644 ag_201_coredns/plugin/ready/README.md create mode 100644 ag_201_coredns/plugin/ready/list.go create mode 100644 ag_201_coredns/plugin/ready/readiness.go create mode 100644 ag_201_coredns/plugin/ready/ready.go create mode 100644 ag_201_coredns/plugin/ready/ready_test.go create mode 100644 ag_201_coredns/plugin/ready/setup.go create mode 100644 ag_201_coredns/plugin/ready/setup_test.go create mode 100644 ag_201_coredns/plugin/register.go create mode 100644 ag_201_coredns/plugin/reload/README.md create mode 100644 ag_201_coredns/plugin/reload/log_test.go create mode 100644 ag_201_coredns/plugin/reload/metrics.go create mode 100644 ag_201_coredns/plugin/reload/reload.go create mode 100644 ag_201_coredns/plugin/reload/setup.go create mode 100644 ag_201_coredns/plugin/reload/setup_test.go create mode 100644 ag_201_coredns/plugin/rewrite/README.md create mode 100644 ag_201_coredns/plugin/rewrite/class.go create mode 100644 ag_201_coredns/plugin/rewrite/edns0.go create mode 100644 ag_201_coredns/plugin/rewrite/fuzz.go create mode 100644 ag_201_coredns/plugin/rewrite/log_test.go create mode 100644 ag_201_coredns/plugin/rewrite/name.go create mode 100644 ag_201_coredns/plugin/rewrite/name_test.go create mode 100644 ag_201_coredns/plugin/rewrite/reverter.go create mode 100644 ag_201_coredns/plugin/rewrite/reverter_test.go create mode 100644 ag_201_coredns/plugin/rewrite/rewrite.go create mode 100644 ag_201_coredns/plugin/rewrite/rewrite_test.go create mode 100644 ag_201_coredns/plugin/rewrite/setup.go create mode 100644 ag_201_coredns/plugin/rewrite/setup_test.go create mode 100644 ag_201_coredns/plugin/rewrite/ttl.go create mode 100644 ag_201_coredns/plugin/rewrite/ttl_test.go create mode 100644 ag_201_coredns/plugin/rewrite/type.go create mode 100644 ag_201_coredns/plugin/rewrite/wire.go create mode 100644 ag_201_coredns/plugin/root/README.md create mode 100644 ag_201_coredns/plugin/root/log_test.go create mode 100644 ag_201_coredns/plugin/root/root.go create mode 100644 ag_201_coredns/plugin/root/root_test.go create mode 100644 ag_201_coredns/plugin/route53/README.md create mode 100644 ag_201_coredns/plugin/route53/log_test.go create mode 100644 ag_201_coredns/plugin/route53/route53.go create mode 100644 ag_201_coredns/plugin/route53/route53_test.go create mode 100644 ag_201_coredns/plugin/route53/setup.go create mode 100644 ag_201_coredns/plugin/route53/setup_test.go create mode 100644 ag_201_coredns/plugin/secondary/README.md create mode 100644 ag_201_coredns/plugin/secondary/log_test.go create mode 100644 ag_201_coredns/plugin/secondary/secondary.go create mode 100644 ag_201_coredns/plugin/secondary/setup.go create mode 100644 ag_201_coredns/plugin/secondary/setup_test.go create mode 100644 ag_201_coredns/plugin/sign/README.md create mode 100644 ag_201_coredns/plugin/sign/dnssec.go create mode 100644 ag_201_coredns/plugin/sign/file.go create mode 100644 ag_201_coredns/plugin/sign/file_test.go create mode 100644 ag_201_coredns/plugin/sign/keys.go create mode 100644 ag_201_coredns/plugin/sign/log_test.go create mode 100644 ag_201_coredns/plugin/sign/nsec.go create mode 100644 ag_201_coredns/plugin/sign/nsec_test.go create mode 100644 ag_201_coredns/plugin/sign/resign_test.go create mode 100644 ag_201_coredns/plugin/sign/setup.go create mode 100644 ag_201_coredns/plugin/sign/setup_test.go create mode 100644 ag_201_coredns/plugin/sign/sign.go create mode 100644 ag_201_coredns/plugin/sign/signer.go create mode 100644 ag_201_coredns/plugin/sign/signer_test.go create mode 100644 ag_201_coredns/plugin/sign/testdata/Kmiek.nl.+013+59725.key create mode 100644 ag_201_coredns/plugin/sign/testdata/Kmiek.nl.+013+59725.private create mode 100644 ag_201_coredns/plugin/sign/testdata/db.miek.nl create mode 100644 ag_201_coredns/plugin/sign/testdata/db.miek.nl_ns create mode 100644 ag_201_coredns/plugin/template/README.md create mode 100644 ag_201_coredns/plugin/template/cname_test.go create mode 100644 ag_201_coredns/plugin/template/log_test.go create mode 100644 ag_201_coredns/plugin/template/metrics.go create mode 100644 ag_201_coredns/plugin/template/setup.go create mode 100644 ag_201_coredns/plugin/template/setup_test.go create mode 100644 ag_201_coredns/plugin/template/template.go create mode 100644 ag_201_coredns/plugin/template/template_test.go create mode 100644 ag_201_coredns/plugin/test/doc.go create mode 100644 ag_201_coredns/plugin/test/file.go create mode 100644 ag_201_coredns/plugin/test/file_test.go create mode 100644 ag_201_coredns/plugin/test/helpers.go create mode 100644 ag_201_coredns/plugin/test/responsewriter.go create mode 100644 ag_201_coredns/plugin/test/scrape.go create mode 100644 ag_201_coredns/plugin/tls/README.md create mode 100644 ag_201_coredns/plugin/tls/log_test.go create mode 100644 ag_201_coredns/plugin/tls/test_ca.pem create mode 100644 ag_201_coredns/plugin/tls/test_cert.pem create mode 100644 ag_201_coredns/plugin/tls/test_key.pem create mode 100644 ag_201_coredns/plugin/tls/tls.go create mode 100644 ag_201_coredns/plugin/tls/tls_test.go create mode 100644 ag_201_coredns/plugin/trace/README.md create mode 100644 ag_201_coredns/plugin/trace/log_test.go create mode 100644 ag_201_coredns/plugin/trace/logger.go create mode 100644 ag_201_coredns/plugin/trace/setup.go create mode 100644 ag_201_coredns/plugin/trace/setup_test.go create mode 100644 ag_201_coredns/plugin/trace/trace.go create mode 100644 ag_201_coredns/plugin/trace/trace_test.go create mode 100644 ag_201_coredns/plugin/transfer/README.md create mode 100644 ag_201_coredns/plugin/transfer/failed_write_test.go create mode 100644 ag_201_coredns/plugin/transfer/notify.go create mode 100644 ag_201_coredns/plugin/transfer/select_test.go create mode 100644 ag_201_coredns/plugin/transfer/setup.go create mode 100644 ag_201_coredns/plugin/transfer/setup_test.go create mode 100644 ag_201_coredns/plugin/transfer/transfer.go create mode 100644 ag_201_coredns/plugin/transfer/transfer_test.go create mode 100644 ag_201_coredns/plugin/tsig/README.md create mode 100644 ag_201_coredns/plugin/tsig/setup.go create mode 100644 ag_201_coredns/plugin/tsig/setup_test.go create mode 100644 ag_201_coredns/plugin/tsig/tsig.go create mode 100644 ag_201_coredns/plugin/tsig/tsig_test.go create mode 100644 ag_201_coredns/plugin/view/README.md create mode 100644 ag_201_coredns/plugin/view/metadata.go create mode 100644 ag_201_coredns/plugin/view/setup.go create mode 100644 ag_201_coredns/plugin/view/setup_test.go create mode 100644 ag_201_coredns/plugin/view/view.go create mode 100644 ag_201_coredns/plugin/whoami/README.md create mode 100644 ag_201_coredns/plugin/whoami/fuzz.go create mode 100644 ag_201_coredns/plugin/whoami/log_test.go create mode 100644 ag_201_coredns/plugin/whoami/setup.go create mode 100644 ag_201_coredns/plugin/whoami/setup_test.go create mode 100644 ag_201_coredns/plugin/whoami/whoami.go create mode 100644 ag_201_coredns/plugin/whoami/whoami_test.go create mode 100644 ag_201_coredns/request/edns0.go create mode 100644 ag_201_coredns/request/request.go create mode 100644 ag_201_coredns/request/request_test.go create mode 100644 ag_201_coredns/request/writer.go create mode 100644 ag_201_coredns/test/auto_test.go create mode 100644 ag_201_coredns/test/cache_test.go create mode 100644 ag_201_coredns/test/chaos_test.go create mode 100644 ag_201_coredns/test/compression_scrub_test.go create mode 100644 ag_201_coredns/test/corefile_test.go create mode 100644 ag_201_coredns/test/doc.go create mode 100644 ag_201_coredns/test/ds_file_test.go create mode 100644 ag_201_coredns/test/edns0_test.go create mode 100644 ag_201_coredns/test/erratic_autopath_test.go create mode 100644 ag_201_coredns/test/etcd_cache_test.go create mode 100644 ag_201_coredns/test/etcd_credentials_test.go create mode 100644 ag_201_coredns/test/etcd_test.go create mode 100644 ag_201_coredns/test/example_test.go create mode 100644 ag_201_coredns/test/file_cname_proxy_test.go create mode 100644 ag_201_coredns/test/file_loop_test.go create mode 100644 ag_201_coredns/test/file_reload_test.go create mode 100644 ag_201_coredns/test/file_serve_test.go create mode 100644 ag_201_coredns/test/file_srv_additional_test.go create mode 100644 ag_201_coredns/test/file_test.go create mode 100644 ag_201_coredns/test/file_upstream_test.go create mode 100644 ag_201_coredns/test/file_xfr_test.go create mode 100644 ag_201_coredns/test/fuzz_corefile.go create mode 100644 ag_201_coredns/test/grpc_test.go create mode 100644 ag_201_coredns/test/hosts_file_test.go create mode 100644 ag_201_coredns/test/log_test.go create mode 100644 ag_201_coredns/test/metric_naming_test.go create mode 100644 ag_201_coredns/test/metrics_test.go create mode 100644 ag_201_coredns/test/miek_test.go create mode 100644 ag_201_coredns/test/no_plugins_test.go create mode 100644 ag_201_coredns/test/plugin_dnssec_test.go create mode 100644 ag_201_coredns/test/presubmit_test.go create mode 100644 ag_201_coredns/test/proxy_health_test.go create mode 100644 ag_201_coredns/test/proxy_test.go create mode 100644 ag_201_coredns/test/readme_test.go create mode 100644 ag_201_coredns/test/reload_test.go create mode 100644 ag_201_coredns/test/reverse_test.go create mode 100644 ag_201_coredns/test/rewrite_test.go create mode 100644 ag_201_coredns/test/secondary_test.go create mode 100644 ag_201_coredns/test/server.go create mode 100644 ag_201_coredns/test/server_reverse_test.go create mode 100644 ag_201_coredns/test/server_test.go create mode 100644 ag_201_coredns/test/template_upstream_test.go create mode 100644 ag_201_coredns/test/tls_test.go create mode 100644 ag_201_coredns/test/tsig_test.go create mode 100644 ag_201_coredns/test/view_test.go create mode 100644 ag_201_coredns/test/wildcard_test.go create mode 100644 ag_201_coredns/testprivkey.pem create mode 100644 dohclient/dohclient.go create mode 100644 dohclient/dohclient.py create mode 100644 "dohclient/\344\275\277\347\224\250go\344\273\243\347\240\201\347\232\204\344\270\200\344\272\233\345\221\275\344\273\244.txt" create mode 100644 readme_1.md diff --git a/ag_201_coredns/-text b/ag_201_coredns/-text new file mode 100644 index 0000000..0079a07 --- /dev/null +++ b/ag_201_coredns/-text @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELTCCAhWgAwIBAgIUShPmgccTG/nyVbmLDn8BVgoDxR4wDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQ04xETAPBgNVBAgMCFNoYW5Eb25nMRUwEwYDVQQDDAxk +ZW1vLnRvb2xib3gxDDAKBgNVBAoMA2R6YjAeFw0yMzEwMjMwOTM2MjJaFw0zMzEw +MjAwOTM2MjJaMEcxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhTaGFuZ0hhaTEXMBUG +A1UEAwwOc2VydmVyLnRvb2xib3gxDDAKBgNVBAoMA2RhaTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBANtCSYU5dMbEdysNUW8Y3xRo8jgxT1HDufKWvHT5 +xv2E8RetoqH+lutAZWccDGz5w1ndmrvezKsV4JVPBoLlQcTJSPnxZm0pgmcBE3Yv +aB3DxlVSWhqBkEXYyKWaj8nhRg3j7SX3J1RJeRxm3Ki2pFg8sX4RHAcGKkAHi+OK +p2dgrOqDiQgd/73Bi/pGIxSMUZpaD2cBP9rVwGZHmAn3NtAGzdKyA0fZBqBBu6ok +HdNAR60LeCCvBiW3RTBKcwVKKg1E8lUT80m9KJLokuHygMxLuBiukGxTJ4titaQR +zLn3jitRRp0RuLVoxRZO55HM8AkQI70CkFG2VoH0jwTpJMcCAwEAAaMTMBEwDwYD +VR0RBAgwBocEwKhryTANBgkqhkiG9w0BAQsFAAOCAgEAlf9XDRW/EC8/kT0Q07L9 +CHpqGpp7Y9ZThQzPonO6K0H2HSvSz99CbH22+0/uKOHCDEv2alDQH6v4PRzAAb9f +HM8D4nKeCjECWrQi/7lS/Og2xH04Aj+/xauryOoWIW8GkVa4pRXlOMg0EiD68v/f +Rsovyy6s5MC2gfSimHR6pa2wKyQfUl78AAs5PELabFXLY1bBOO6EiFXy9oezcEMy +8dNoEk44v7AlV4Eqdw8m6270gD7VjcoE1VfFqzq4Mu5jf8ffOiC0bQZ5lxDisuB7 +RrSHhrWTH086tkSmxCTW3r5WcrNDsFY9poMs+WRou1pyIY4SlBF+DiiLmZ0ad0Yr +IJrnkTpvAmYzc7JXmUCtY9OPSKvOu54lJ/6vaLFHySmx0/NXSZvJmo90ACym5Xx6 +V/FtnhqPYwrLGaDG3yt0u6UAqRfd2v8tgcIJ2VuOHxhJeUp8r+5zl+Ih4OhpOas2 +Y1C7ByEpOk1PG8ts9e3WSuyb2PAftebhsPhyZErnJZ4uyPvy/TWxH+dj6JIxezoT +lvQxcH7bZtbTyre1wkYOkqC9/H/QMecB3FL+EsOCaD7zIsLCFo+gMbNX1Hw+JIs9 +uNU5ygrX6cz5xJ6mDMABIJ+nzuH5JRnFSZQAbZnsUPh1z6xGpY+9LCosbQVR47J9 +TpAabCIz+2vrRKj2PgjWXXw= +-----END CERTIFICATE----- diff --git a/ag_201_coredns/.circleci/config.yml b/ag_201_coredns/.circleci/config.yml new file mode 100644 index 0000000..e1e0c0a --- /dev/null +++ b/ag_201_coredns/.circleci/config.yml @@ -0,0 +1,66 @@ +version: 2 + +initWorkingDir: &initWorkingDir + type: shell + name: Initialize Working Directory + pwd: / + command: | + mkdir -p ~/go/src/${CIRCLE_PROJECT_USERNAME}/coredns + sudo chown -R circleci ~/go + mkdir -p ~/go/out/tests + mkdir -p ~/go/out/logs + mkdir -p /home/circleci/logs + GOROOT=$(go env GOROOT) + sudo rm -r $(go env GOROOT) + sudo mkdir $GOROOT + LATEST=$(curl -s https://go.dev/VERSION?m=text) + curl https://dl.google.com/go/${LATEST}.linux-amd64.tar.gz | sudo tar xz -C $GOROOT --strip-components=1 + +integrationDefaults: &integrationDefaults + machine: + image: ubuntu-2004:2022.04.2 + working_directory: ~/go/src/${CIRCLE_PROJECT_USERNAME}/coredns + environment: + - K8S_VERSION: v1.22.0 + - KIND_VERSION: v0.11.1 + - KUBECONFIG: /home/circleci/.kube/kind-config-kind + +setupKubernetes: &setupKubernetes + - run: + name: Setup Kubernetes + command: ~/go/src/${CIRCLE_PROJECT_USERNAME}/ci/build/kubernetes/k8s_setup.sh + +buildCoreDNSImage: &buildCoreDNSImage + - run: + name: Build latest CoreDNS Docker image + command: | + cd ~/go/src/${CIRCLE_PROJECT_USERNAME}/coredns + make coredns SYSTEM="GOOS=linux" && \ + docker build -t coredns . && \ + kind load docker-image coredns + +jobs: + kubernetes-tests: + <<: *integrationDefaults + steps: + - <<: *initWorkingDir + - checkout + - run: + name: Get CI repo + 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 + - <<: *buildCoreDNSImage + - run: + name: Run Kubernetes tests + command: | + cd ~/go/src/${CIRCLE_PROJECT_USERNAME}/ci/test/kubernetes + go mod tidy + go test -v ./... + +workflows: + version: 2 + integration-tests: + jobs: + - kubernetes-tests diff --git a/ag_201_coredns/.codecov.yml b/ag_201_coredns/.codecov.yml new file mode 100644 index 0000000..167f563 --- /dev/null +++ b/ag_201_coredns/.codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + target: 50% + threshold: null + patch: false + changes: false diff --git a/ag_201_coredns/.dockerignore b/ag_201_coredns/.dockerignore new file mode 100644 index 0000000..1ff16c5 --- /dev/null +++ b/ag_201_coredns/.dockerignore @@ -0,0 +1,2 @@ +* +!coredns diff --git a/ag_201_coredns/.dreck.yaml b/ag_201_coredns/.dreck.yaml new file mode 100644 index 0000000..3528962 --- /dev/null +++ b/ag_201_coredns/.dreck.yaml @@ -0,0 +1,13 @@ +features: + - aliases + - exec + +aliases: + - | + /plugin (.*) -> /label plugin/$1 + - | + /wai -> /label works as intended + - | + /release (.*) -> /exec /opt/bin/release-coredns $1 + - | + /docker (.*) -> /exec /opt/bin/docker-coredns $1 diff --git a/ag_201_coredns/.github/CODE_OF_CONDUCT.md b/ag_201_coredns/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7d0c3a6 --- /dev/null +++ b/ag_201_coredns/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +## Coredns Community Code of Conduct + +Coredns follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). diff --git a/ag_201_coredns/.github/CONTRIBUTING.md b/ag_201_coredns/.github/CONTRIBUTING.md new file mode 100644 index 0000000..09ad9ed --- /dev/null +++ b/ag_201_coredns/.github/CONTRIBUTING.md @@ -0,0 +1,92 @@ +# Contributing to CoreDNS + +Welcome! Our community focuses on helping others and making CoreDNS the best it can be. We gladly +accept contributions and encourage you to get involved! + +## Bug Reports + +First, please [search this +repository](https://github.com/coredns/coredns/search?q=&type=Issues&utf8=%E2%9C%93) with a variety +of keywords to ensure your bug is not already reported. + +If not, [open an issue](https://github.com/coredns/coredns/issues) and answer the questions so we +can understand and reproduce the problematic behavior. + +The burden is on you to convince us that it is actually a bug in CoreDNS. This is easiest to do when +you write clear, concise instructions so we can reproduce the behavior (even if it seems obvious). +The more detailed and specific you are, the faster we will be able to help you. Check out [How to +Report Bugs Effectively](https://www.chiark.greenend.org.uk/~sgtatham/bugs.html). + +Please be kind. :smile: Remember that CoreDNS comes at no cost to you, and you're getting free help. + +## Minor Improvements and New Tests + +Submit [pull requests](https://github.com/coredns/coredns/pulls) at any time. Make sure to write +tests to assert your change is working properly and is thoroughly covered. + +## New Features + +First, please [search](https://github.com/coredns/coredns/search?q=&type=Issues&utf8=%E2%9C%93) with +a variety of keywords to ensure your suggestion/proposal is new. + +Please also check for existing pull requests to see if someone is already working on this. We want +to avoid duplication of effort. + +If the proposal is new and no one has opened pull request yet, you may open either an issue or a +pull request for discussion and feedback. + +If you are going to spend significant time implementing code for a pull request, best to open an +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.** + +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 +faster. + +## New Plugins + +A new plugin is (usually) about 1000 lines of Go. This includes tests and some plugin boiler plate. +This is a considerable amount of code and will take time to review. To prevent too much back and +forth it is advisable to start with the plugin's `README.md`; This will be its main documentation +and will help nail down the correct name of the plugin and its various config options. + +From there it can work its way through the rest (`setup.go`, the `ServeDNS` handler function, etc.). +Doing this will help the reviewers, as each chunk of code is relatively small. + +Also read [plugin.md](https://raw.githubusercontent.com/coredns/coredns/master/plugin.md) for +advice on how to write a plugin. + +## Updating Dependencies + +We use [Go Modules](https://github.com/golang/go/wiki/Modules) as the tool to manage vendor dependencies. + +Use the following to update the version of all dependencies +```sh +$ go get -u +``` + +After the dependencies have been updated or added, you might run the following to +cleanup the go module files: +```sh +$ go mod tidy +``` + +Please refer to [Go Modules](https://github.com/golang/go/wiki/Modules) for more details. + +## Developer Certificate of Origin + +As required by the CNCF's [charter](https://github.com/cncf/foundation/blob/master/charter.md#11-ip-policy), +all new code contributions must be accompanied by a [Developer Certificate of Origin (DCO)](https://developercertificate.org/). CoreDNS uses [Probot](https://github.com/probot/dco#how-it-works) to enforce the DCO on pull requests. + +You may use git option `-s` to append automatically to the `Sign-off-by` line to your commit messages: + +``` +$ git commit -s -m 'This is my commit message' +``` + +# Thank You + +Thanks for your help! CoreDNS would not be what it is today without your contributions. diff --git a/ag_201_coredns/.github/ISSUE_TEMPLATE/bug-report.md b/ag_201_coredns/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..9472683 --- /dev/null +++ b/ag_201_coredns/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,27 @@ +--- +name: Bug Report +about: Report a bug encountered while using CoreDNS +labels: bug + +--- + + + +**What happened**: + +**What you expected to happen**: + +**How to reproduce it (as minimally and precisely as possible)**: + +**Anything else we need to know?**: + +**Environment**: + +- the version of CoreDNS: +- Corefile: +- logs, if applicable: +- OS (e.g: `cat /etc/os-release`): +- Others: diff --git a/ag_201_coredns/.github/ISSUE_TEMPLATE/enhancement.md b/ag_201_coredns/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 0000000..d39c37c --- /dev/null +++ b/ag_201_coredns/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,11 @@ +--- +name: Enhancement Request +about: Suggest an enhancement to the CoreDNS project +labels: enhancement + +--- + + +**What would you like to be added**: + +**Why is this needed**: diff --git a/ag_201_coredns/.github/ISSUE_TEMPLATE/question.md b/ag_201_coredns/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..2b7c45f --- /dev/null +++ b/ag_201_coredns/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,10 @@ +--- +name: Question +about: A question related to CoreDNS +labels: question + +--- + diff --git a/ag_201_coredns/.github/PULL_REQUEST_TEMPLATE.md b/ag_201_coredns/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c110174 --- /dev/null +++ b/ag_201_coredns/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ + + +### 1. Why is this pull request needed and what does it do? + +### 2. Which issues (if any) are related? + +### 3. Which documentation changes (if any) need to be made? + +### 4. Does this introduce a backward incompatible change or deprecation? diff --git a/ag_201_coredns/.github/SECURITY.md b/ag_201_coredns/.github/SECURITY.md new file mode 100644 index 0000000..fd8cc5b --- /dev/null +++ b/ag_201_coredns/.github/SECURITY.md @@ -0,0 +1,189 @@ +# Security Release Process + +The CoreDNS project has adopted this security disclosures and response policy +to ensure responsible handling of critical issues. + + +## Product Security Team (PST) + +Security vulnerabilities should be handled quickly and sometimes privately. +The primary goal of this process is to reduce the total time users are vulnerable to publicly known exploits. + +The Product Security Team (PST) is responsible for organizing the entire response including internal communication and external disclosure. + +The initial Product Security Team will consist of the set of maintainers that volunteered. + +### mailing lists + +* security@coredns.io : for any security concerns. Received by Product Security Team members, and used by this Team to discuss security issues and fixes. +* coredns-distributors-announce@lists.cncf.io: for early private information on Security patch releases. see below how CoreDNS distributors can apply for this list. + + +## Disclosures + +### 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. +Instead, send your report privately to security@coredns.io. +Security reports are greatly appreciated and we will publicly thank you for it. + +Please provide as much information as possible, so we can react quickly. +For instance, that could include: +- Description of the location and potential impact of the vulnerability; +- A detailed description of the steps required to reproduce the vulnerability (POC scripts, screenshots, and compressed packet captures are all helpful to us) +- Whatever else you think we might need to identify the source of this vulnerability + +### Public Disclosure Processes + +If you know of a publicly disclosed security vulnerability please IMMEDIATELY email security@coredns.io +to inform the Product Security Team (PST) about the vulnerability so we start the patch, release, and communication process. + +If possible the PST will ask the person making the public report if the issue can be handled via a private disclosure process +(for example if the full exploit details have not yet been published). +If the reporter denies the request for private disclosure, the PST will move swiftly with the fix and release process. +In extreme cases you can ask GitHub to delete the issue but this generally isn't necessary and is unlikely to make a public disclosure less damaging. + +## Patch, Release, and Public Communication + +For each vulnerability a member of the PST will volunteer to lead coordination with the "Fix Team" +and is responsible for sending disclosure emails to the rest of the community. +This lead will be referred to as the "Fix Lead." + +The role of Fix Lead should rotate round-robin across the PST. + +Note that given the current size of the CoreDNS community it is likely that the PST is the same as the "Fix team." +The PST may decide to bring in additional contributors for added expertise depending on the area of the code that contains the vulnerability. + +All of the timelines below are suggestions and assume a Private Disclosure. +If the Team is dealing with a Public Disclosure all timelines become ASAP. +If the fix relies on another upstream project's disclosure timeline, that will adjust the process as well. +We will work with the upstream project to fit their timeline and best protect our users. + +### Fix Team Organization + +These steps should be completed within the first 24 hours of disclosure. + +- The Fix Lead will work quickly to identify relevant engineers from the affected projects and + packages and CC those engineers into the disclosure thread. These selected developers are the Fix + Team. +- The Fix Lead will get the Fix Team access to private security repos to develop the fix. + + +### Fix Development Process + +These steps should be completed within the 1-7 days of Disclosure. + +- The Fix Lead and the Fix Team will create a + [CVSS](https://www.first.org/cvss/specification-document) using the [CVSS + Calculator](https://www.first.org/cvss/calculator/3.0). The Fix Lead makes the final call on the + calculated CVSS; it is better to move quickly than making the CVSS perfect. +- The Fix Team will notify the Fix Lead that work on the fix branch is complete once there are LGTMs + on all commits in the private repo from one or more maintainers. + +If the CVSS score is under 4.0 ([a low severity +score](https://www.first.org/cvss/specification-document#i5)) the Fix Team can decide to slow the +release process down in the face of holidays, developer bandwidth, etc. These decisions must be +discussed on the security@coredns.io mailing list. + +### Fix Disclosure Process + +With the Fix Development underway the CoreDNS Security Team needs to come up with an overall communication plan for the wider community. +This Disclosure process should begin after the Team has developed a fix or mitigation +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 +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. + +The communication to users should be actionable. +They should know when to block time to apply patches, understand exact mitigation steps, etc. + +**Optional Fix Disclosure to Private Distributors List** (Completed within 1-14 days of Disclosure): + +- The Fix Lead will make a determination with the help of the Fix Team if an issue is critical enough to require early disclosure to distributors. +Generally this Private Distributor Disclosure process should be reserved for remotely exploitable or privilege escalation issues. +Otherwise, this process can be skipped. +- The Fix Lead will email the patches to coredns-distributors-announce@lists.cncf.io so distributors can prepare their own release to be available to users on the day of the issue's announcement. +Distributors should read about the [Private Distributor List](#private-distributor-list) to find out the requirements for being added to this list. +- **What if a distributor breaks embargo?** The PST will assess the damage and may make the call to release earlier or continue with the plan. +When in doubt push forward and go public ASAP. + +**Fix Release Day** (Completed within 1-21 days of Disclosure) + +- the Fix Team will selectively choose all needed commits from the Master branch in order to create a new release on top of the current last version released. +- Release process will be as usual. +- The Fix Lead will request a CVE from [DWF](https://github.com/distributedweaknessfiling/cvelist) + and include the CVSS and release details. +- The Fix Lead will inform all users, devs and integrators, now that everything is public, + announcing the new releases, the CVE number, and the relevant merged PRs to get wide distribution + and user action. As much as possible this email should be actionable and include links on how to apply + the fix to user's environments; this can include links to external distributor documentation. + + +## Private Distributor List + +This list is intended to be used primarily to provide actionable information to +multiple distributor projects at once. This list is not intended for +individuals to find out about security issues. + +### Embargo Policy + +The information members receive on coredns-distributors-announce@lists.cncf.io must not be +made public, shared, nor even hinted at anywhere beyond the need-to-know within +your specific team except with the list's explicit approval. +This holds true until the public disclosure date/time that was agreed upon by the list. +Members of the list and others may not use the information for anything other +than getting the issue fixed for your respective distribution's users. + +Before any information from the list is shared with respective members of your +team required to fix said issue, they must agree to the same terms and only +find out information on a need-to-know basis. + +In the unfortunate event you share the information beyond what is allowed by +this policy, you _must_ urgently inform the security@coredns.io +mailing list of exactly what information leaked and to whom. + +If you continue to leak information and break the policy outlined here, you +will be removed from the list. + +### Contributing Back + +This is a team effort. As a member of the list you must carry some water. This +could be in the form of the following: + +**Technical** + +- Review and/or test the proposed patches and point out potential issues with + them (such as incomplete fixes for the originally reported issues, additional + issues you might notice, and newly introduced bugs), and inform the list of the + work done even if no issues were encountered. + +**Administrative** + +- Help draft emails to the public disclosure mailing list. +- Help with release notes. + +### Membership Criteria + +To be eligible for the coredns-distributors-announce@lists.cncf.io mailing list, your +distribution should: + +1. Be an active distributor of CoreDNS component. +2. Have a user base not limited to your own organization. +3. Have a publicly verifiable track record up to present day of fixing security + issues. +4. Not be a downstream or rebuild of another distributor. +5. Be a participant and active contributor in the community. +6. Accept the [Embargo Policy](#embargo-policy) that is outlined above. +7. Have someone already on the list vouch for the person requesting membership + on behalf of your distribution. + +### Requesting to Join + +New membership requests are sent to security@coredns.io. + +In the body of your request please specify how you qualify and fulfill each +criterion listed in [Membership Criteria](#membership-criteria). diff --git a/ag_201_coredns/.github/dependabot.yml b/ag_201_coredns/.github/dependabot.yml new file mode 100644 index 0000000..69c884a --- /dev/null +++ b/ag_201_coredns/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 + +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/ag_201_coredns/.github/fixup_file_mtime.sh b/ag_201_coredns/.github/fixup_file_mtime.sh new file mode 100644 index 0000000..af401a5 --- /dev/null +++ b/ag_201_coredns/.github/fixup_file_mtime.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# +# Description: Fix up the file mtimes based on the git log. + +set -u -o pipefail + +if [[ ! -f 'coredns.1.md' ]]; then + echo 'ERROR: Must be run from the top of the git repo.' + exit 1 +fi + +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/ag_201_coredns/.github/workflows/cifuzz.yml b/ag_201_coredns/.github/workflows/cifuzz.yml new file mode 100644 index 0000000..10e1ec9 --- /dev/null +++ b/ag_201_coredns/.github/workflows/cifuzz.yml @@ -0,0 +1,27 @@ +name: CIFuzz +on: + pull_request: + branches: + - master +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@3cea5372237819ed00197afe530f5a7ea3e805c8 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts diff --git a/ag_201_coredns/.github/workflows/codeql-analysis.yml b/ag_201_coredns/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..0a5f84e --- /dev/null +++ b/ag_201_coredns/.github/workflows/codeql-analysis.yml @@ -0,0 +1,41 @@ +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + 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 + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + + steps: + - name: Checkout repository + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - name: Initialize CodeQL + uses: github/codeql-action/init@b398f525a5587552e573b247ac661067fafa920b + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@b398f525a5587552e573b247ac661067fafa920b + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@b398f525a5587552e573b247ac661067fafa920b diff --git a/ag_201_coredns/.github/workflows/depsreview.yml b/ag_201_coredns/.github/workflows/depsreview.yml new file mode 100644 index 0000000..f312d6e --- /dev/null +++ b/ag_201_coredns/.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@2541b1294d2704b0964813337f33b291d3f8596b + - name: 'Dependency Review' + uses: actions/dependency-review-action@23d1ffffb6fa5401173051ec21eba8c35242733f diff --git a/ag_201_coredns/.github/workflows/docker.yml b/ag_201_coredns/.github/workflows/docker.yml new file mode 100644 index 0000000..9774257 --- /dev/null +++ b/ag_201_coredns/.github/workflows/docker.yml @@ -0,0 +1,29 @@ +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@2541b1294d2704b0964813337f33b291d3f8596b + - name: Build Docker Images + run: make VERSION=${RELEASE:1} DOCKER=coredns -f Makefile.docker release + - name: Show Docker Images + run: docker images + - name: Publish Docker Images + run: make VERSION=${RELEASE:1} DOCKER=coredns -f Makefile.docker docker-push diff --git a/ag_201_coredns/.github/workflows/go.coverage.yml b/ag_201_coredns/.github/workflows/go.coverage.yml new file mode 100644 index 0000000..f52c28d --- /dev/null +++ b/ag_201_coredns/.github/workflows/go.coverage.yml @@ -0,0 +1,30 @@ +name: Go Coverage +on: [pull_request] +permissions: + contents: read + +jobs: + test: + name: Coverage + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f + with: + go-version: '1.19.0' + id: go + + - name: Check out code + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - 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: Upload coverage to Codecov + uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 diff --git a/ag_201_coredns/.github/workflows/go.fmt.yml b/ag_201_coredns/.github/workflows/go.fmt.yml new file mode 100644 index 0000000..b93e48c --- /dev/null +++ b/ag_201_coredns/.github/workflows/go.fmt.yml @@ -0,0 +1,36 @@ +name: Go Fmt + +on: + schedule: + - cron: '22 10 * * 1' + +permissions: read-all + +jobs: + fix: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - 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[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 + run: | + git add . + if output=$(git status --porcelain) && [ ! -z "$output" ]; then + git commit -s -m 'auto go fmt' + git push + fi diff --git a/ag_201_coredns/.github/workflows/go.test.yml b/ag_201_coredns/.github/workflows/go.test.yml new file mode 100644 index 0000000..36a70cd --- /dev/null +++ b/ag_201_coredns/.github/workflows/go.test.yml @@ -0,0 +1,83 @@ +name: Go Tests +on: [push, pull_request] +permissions: + contents: read + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f + with: + go-version: '1.19.0' + id: go + + - name: Check out code + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - name: Build + run: go build -v ./... + + - 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: Install Go + uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f + with: + go-version: '1.19.0' + id: go + + - name: Check out code + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - name: Build + run: go build -v ./... + + - name: Test + run: ( cd plugin; go test -race ./... ) + + test-e2e: + name: Test e2e + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f + with: + go-version: '1.19.0' + id: go + + - name: Check out code + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - name: Build + run: go build -v ./... + + - name: Test + run: | + go install github.com/fatih/faillint || true + ( cd test; go test -race ./... ) + + test-makefile-release: + name: Test Makefile.release + runs-on: ubuntu-latest + steps: + - name: Install dependencies + run: sudo apt-get install make curl + + - name: Check out code + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - name: Test Makefile.release + run: make GITHUB_ACCESS_TOKEN=x -n release github-push -f Makefile.release + + - name: Test Makefile.docker + run: make VERSION=x DOCKER=x -n release docker-push -f Makefile.docker diff --git a/ag_201_coredns/.github/workflows/go.tidy.yml b/ag_201_coredns/.github/workflows/go.tidy.yml new file mode 100644 index 0000000..5d5d532 --- /dev/null +++ b/ag_201_coredns/.github/workflows/go.tidy.yml @@ -0,0 +1,43 @@ +name: Go Tidy + +on: + schedule: + - cron: '22 10 * * *' + +permissions: read-all + +jobs: + fix: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Install Go + uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f + with: + go-version: '1.19.0' + id: go + + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - name: Tidy + run: | + rm -f go.sum + go mod tidy -compat=1.17 + + - name: Set up Git + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + 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 + run: | + git add . + if output=$(git status --porcelain) && [ ! -z "$output" ]; then + git commit -s -m 'auto go mod tidy' + git push + fi diff --git a/ag_201_coredns/.github/workflows/golangci-lint.yml b/ag_201_coredns/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..3819936 --- /dev/null +++ b/ag_201_coredns/.github/workflows/golangci-lint.yml @@ -0,0 +1,17 @@ +name: golangci-lint +on: + pull_request: +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f + with: + go-version: '1.19.0' + - uses: actions/checkout@v3 + # See https://github.com/golangci/golangci-lint-action/issues/442#issuecomment-1203786890 + - name: Install golangci-lint + run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.3 + - name: Run golangci-lint + run: golangci-lint run --version --verbose --out-format=github-actions diff --git a/ag_201_coredns/.github/workflows/make.doc.yml b/ag_201_coredns/.github/workflows/make.doc.yml new file mode 100644 index 0000000..33fcf0f --- /dev/null +++ b/ag_201_coredns/.github/workflows/make.doc.yml @@ -0,0 +1,42 @@ +name: Make Doc + +on: + schedule: + - cron: '22 10 * * 0' + +permissions: read-all + +jobs: + fix: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - name: Setup Go + uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f + with: + go-version: '1.19.0' + + - name: Update Docs + run: | + bash -x -e ./.github/fixup_file_mtime.sh + make -f Makefile.doc + + - name: Set up Git + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + 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 + run: | + git add . + if output=$(git status --porcelain) && [ ! -z "$output" ]; then + git commit -s -m 'auto make -f Makefile.doc' + git push + fi diff --git a/ag_201_coredns/.github/workflows/reviewdog.yml b/ag_201_coredns/.github/workflows/reviewdog.yml new file mode 100644 index 0000000..a591dff --- /dev/null +++ b/ag_201_coredns/.github/workflows/reviewdog.yml @@ -0,0 +1,25 @@ +name: Reviewdog + +on: + pull_request: + branches: + - master + +permissions: read-all + +jobs: + gofmt: + name: Go Fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + - run: find . -not -path '*/\.git/*' -type f -name '*.go' -exec gofmt -s -w {} \+ + - uses: reviewdog/action-suggester@8f83d27e749053b2029600995c115026a010408e + + whitespace: + name: Whitespace + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + - run: find . -not -path '*/\.git/*' -type f -not -name '*.go' -exec sed -i 's/[[:space:]]\{1,\}$//' {} \+ + - uses: reviewdog/action-suggester@8f83d27e749053b2029600995c115026a010408e diff --git a/ag_201_coredns/.github/workflows/scorecards.yml b/ag_201_coredns/.github/workflows/scorecards.yml new file mode 100644 index 0000000..91f38d5 --- /dev/null +++ b/ag_201_coredns/.github/workflows/scorecards.yml @@ -0,0 +1,55 @@ +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 + + steps: + - name: "Checkout code" + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@ce330fde6b1a5c9c75b417e7efc510b822a35564 + 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@3cea5372237819ed00197afe530f5a7ea3e805c8 + 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@b398f525a5587552e573b247ac661067fafa920b + with: + sarif_file: results.sarif diff --git a/ag_201_coredns/.github/workflows/stale.yml b/ag_201_coredns/.github/workflows/stale.yml new file mode 100644 index 0000000..6f1084d --- /dev/null +++ b/ag_201_coredns/.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@9c1b1c6e115ca2af09755448e0dbba24e5061cc8 + 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/ag_201_coredns/.github/workflows/whitespace.yml b/ag_201_coredns/.github/workflows/whitespace.yml new file mode 100644 index 0000000..4f9a97f --- /dev/null +++ b/ag_201_coredns/.github/workflows/whitespace.yml @@ -0,0 +1,36 @@ +name: Remove Trailing Whitespaces + +on: + schedule: + - cron: '22 10 * * 2' + +permissions: read-all + +jobs: + fix: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - 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[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 + run: | + git add . + if output=$(git status --porcelain) && [ ! -z "$output" ]; then + git commit -s -m 'auto remove trailing whitespaces' + git push + fi diff --git a/ag_201_coredns/.github/workflows/yamllint.yml b/ag_201_coredns/.github/workflows/yamllint.yml new file mode 100644 index 0000000..25bd099 --- /dev/null +++ b/ag_201_coredns/.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@2541b1294d2704b0964813337f33b291d3f8596b + - name: 'Yamllint' + uses: karancode/yamllint-github-action@dd59165b84d90d37fc919c3c7dd84c7e37cd6bfb + with: + yamllint_file_or_dir: '.' + yamllint_strict: false + yamllint_comment: true + env: + GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/ag_201_coredns/.gitignore b/ag_201_coredns/.gitignore new file mode 100644 index 0000000..67780a4 --- /dev/null +++ b/ag_201_coredns/.gitignore @@ -0,0 +1,6 @@ +# only add build artifacts concerning coredns - no editor related files +coredns +coredns.exe +build/ +release/ +vendor/ diff --git a/ag_201_coredns/.golangci.yml b/ag_201_coredns/.golangci.yml new file mode 100644 index 0000000..475ae82 --- /dev/null +++ b/ag_201_coredns/.golangci.yml @@ -0,0 +1,13 @@ +run: + timeout: 5m +linters: + disable-all: true + enable: + - deadcode + - govet + - ineffassign + - staticcheck + - structcheck + - typecheck + - varcheck + - whitespace diff --git a/ag_201_coredns/.stickler.yml b/ag_201_coredns/.stickler.yml new file mode 100644 index 0000000..cf662ed --- /dev/null +++ b/ag_201_coredns/.stickler.yml @@ -0,0 +1,10 @@ +--- +linters: + golint: + min_confidence: 0.85 + fixer: true + +files: + ignore: + - 'vendor/*' + - 'pb/*' diff --git a/ag_201_coredns/.yamllint b/ag_201_coredns/.yamllint new file mode 100644 index 0000000..1fd8ea1 --- /dev/null +++ b/ag_201_coredns/.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/ag_201_coredns/ADOPTERS.md b/ag_201_coredns/ADOPTERS.md new file mode 100644 index 0000000..5f8b5b6 --- /dev/null +++ b/ag_201_coredns/ADOPTERS.md @@ -0,0 +1,33 @@ +* [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. +* [Admiral](https://getadmiral.com) uses CoreDNS to handle geographic DNS requests for our public-facing microservices. +* [Qunar](https://qunar.com) uses CoreDNS for service discovery of its GPU machine learning cloud with TensorFlow and Kubernetes. +* [seansean2](https://web.mit.edu) uses CoreDNS in production at MIT for DNS. +* [Tradeshift](https://tradeshift.com/) uses CoreDNS to look up company identifiers across multiple shards/regions/zones +* [SoundCloud](https://soundcloud.com/) uses CoreDNS as internal cache+proxy in Kubernetes clusters to handle hundreds of thousands DNS service discovery requests per second. +* [Z Lab](https://zlab.co.jp) uses CoreDNS in production combination with Consul and Kubernetes Clusters. +* [Serpro/estaleiro](estaleiro.serpro.gov.br) uses CoreDNS as Kubernetes' DNS Server, in production with tuned Kubernetes plugin options +* [Lumo](https://thinklumo.com) uses CoreDNS as Kubernetes' DNS Server, in production and lab with default configuration +* [Booming Games](https://booming-games.com) uses CoreDNS in multiple Kubernetes clusters, with Federation plugin. expect to go into production soon. +* [Sodimac](https://www.sodimac.cl) uses CoreDNS with Kubernetes in production with default configuration. +* [Bose](https://www.bose.com/) uses CoreDNS with Kubernetes in production on very large cluster (over 250 nodes) +* [farmotive](https://farmotive.io) uses CoreDNS in Kubernetes using default configuration, in its Lab. Expect to be in production soon. +* [Zalando SE](https://www.zalando.de) uses CoreDNS as Kubernetes' DNS Server, in production. +* [Trainline](https://trainline.com) uses CoreDNS along with Kubernetes in production, with a tuned configuration. +* [AnchorFree](https://www.anchorfree.com) uses CoreDNS within Kubernetes in production, with standard configuration. +* [Datacom](https://datacom.co.nz) uses CoreDNS with a tuned configuration for Kubernetes, as production. +* [Takealot.com](https://www.takealot.com) uses CoreDNS as Kubernetes' DNS Server, in production. +* [scalable minds](https://scalableminds.com) uses CoreDNS with default configuration for Kubernetes in its production environment. +* [ObjectRocket](https://www.objectrocket.com) uses CoreDNS on its numerous Kubernetes' clusters, using refined configurations. Address both Lab and Production environment +* [Devino Telecom](https://devinotele.com) uses CoreDNS with default configuration for Kubernetes for its Lab and its Production. +* [Yandex Money](https://money.yandex.ru) uses CoreDNS in Lab and Production, using default configuration for Kubernetes. +* [AdGuard](https://adguard.com/) uses CoreDNS in [AdGuard Home](https://github.com/AdguardTeam/AdGuardHome) and, therefore, in production public AdGuard DNS servers. +* [Skyscanner](https://www.skyscanner.net) uses CoreDNS within Kubernetes in production with the configuration tuned to use the Autopath plugin. +* [Zinza Technology](https://zinza.com.vn) uses CoreDNS within Kubernetes in production, with standard configuration. +* [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. \ No newline at end of file diff --git a/ag_201_coredns/CODEOWNERS b/ag_201_coredns/CODEOWNERS new file mode 100644 index 0000000..26e4271 --- /dev/null +++ b/ag_201_coredns/CODEOWNERS @@ -0,0 +1,62 @@ +# 5 steering committee members +# steering committee member: , , term ends +# steering committee member: , , term ends +# steering committee member: , , term ends +# steering committee member: , , term ends +# steering committee member: , , term ends + +* @bradbeam @chrisohaver @dilyevsky @jameshartig @greenpau @isolus @johnbelamaric @miekg @pmoroney @rajansandeep @stp-ip @superq @yongtang @Tantalor93 + +/.circleci/ @miekg @chrisohaver @rajansandeep +/plugin/pkg/ @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip +/coremain/ @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip +/core/ @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip +/request/ @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip +/plugin/* @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip +go.sum @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip +go.mod @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip + +/plugin/acl/ @miekg @ihac +/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 @chrisohaver +/plugin/cancel/ @miekg +/plugin/chaos/ @miekg @zouyee +/plugin/clouddns/ @miekg @yongtang +/plugin/dns64 @superq +/plugin/dnssec/ @isolus @miekg +/plugin/dnstap/ @varyoo @yongtang +/plugin/erratic/ @miekg +/plugin/errors/ @miekg @Tantalor93 +/plugin/etcd/ @miekg @nitisht +/plugin/file/ @miekg @yongtang @stp-ip +/plugin/forward/ @johnbelamaric @miekg @rdrozhdzh @Tantalor93 @chrisohaver +/plugin/geoip/ @miekg @snebel29 +/plugin/grpc/ @inigohu @miekg @zouyee +/plugin/health/ @jameshartig @miekg @zouyee +/plugin/header/ @miekg @mqasimsarfraz +/plugin/hosts/ @johnbelamaric @pmoroney +/plugin/k8s_external/ @miekg @chrisohaver +/plugin/kubernetes/ @bradbeam @chrisohaver @johnbelamaric @miekg @rajansandeep @yongtang @zouyee +/plugin/loadbalance/ @miekg +/plugin/log/ @miekg @nchrisdk @Tantalor93 +/plugin/loop/ @miekg @chrisohaver +/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 @yongtang +/plugin/route53/ @yongtang @dilyevsky +/plugin/secondary/ @bradbeam @miekg +/plugin/template/ @rtreffer +/plugin/tls/ @johnbelamaric +/plugin/trace/ @johnbelamaric @zouyee @Tantalor93 +/plugin/transfer/ @miekg @chrisohaver +/plugin/tsig/ @chrisohaver +/plugin/whoami/ @miekg @chrisohaver @yongtang diff --git a/ag_201_coredns/CODE_OF_CONDUCT.md b/ag_201_coredns/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e69de29 diff --git a/ag_201_coredns/CONTRIBUTING.md b/ag_201_coredns/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/ag_201_coredns/Corefile b/ag_201_coredns/Corefile new file mode 100644 index 0000000..115aba2 --- /dev/null +++ b/ag_201_coredns/Corefile @@ -0,0 +1,24 @@ +https://.:443 { + tls /root/genCert/server/server.crt.pem /dbw_proxy/coredns/pkcs8.pem + cache + hosts /root/coredns/hostfile { + fallthrough + } + errors + dnsovertor { + hsaddr hlpvjy4xmxgquzebw24fdfqct55arjyaxvpc5zx4qqjqzxafg6onekad.onion + } + log +} +.:53 { + cache + hosts /root/coredns/hostfile { + fallthrough + } + dnsovertor { + hsaddr hlpvjy4xmxgquzebw24fdfqct55arjyaxvpc5zx4qqjqzxafg6onekad.onion + } + log + errors +} + diff --git a/ag_201_coredns/Corefile.1023 b/ag_201_coredns/Corefile.1023 new file mode 100644 index 0000000..7767cdc --- /dev/null +++ b/ag_201_coredns/Corefile.1023 @@ -0,0 +1,8 @@ +https://.:443 { + tls /root/genCert/server/server.crt.pem /dbw_proxy/coredns/pkcs8.pem + cache + errors + log + forward . 223.5.5.5 +} + diff --git a/ag_201_coredns/Corefile.223.5.5.5 b/ag_201_coredns/Corefile.223.5.5.5 new file mode 100644 index 0000000..dad9c80 --- /dev/null +++ b/ag_201_coredns/Corefile.223.5.5.5 @@ -0,0 +1,7 @@ +.:53 { + cache + forward . 223.5.5.5 + log + errors +} + diff --git a/ag_201_coredns/Dockerfile b/ag_201_coredns/Dockerfile new file mode 100644 index 0000000..0eace25 --- /dev/null +++ b/ag_201_coredns/Dockerfile @@ -0,0 +1,19 @@ +FROM debian:stable-slim +SHELL [ "/bin/sh", "-ec" ] + +RUN export DEBCONF_NONINTERACTIVE_SEEN=true \ + DEBIAN_FRONTEND=noninteractive \ + DEBIAN_PRIORITY=critical \ + TERM=linux ; \ + apt-get -qq update ; \ + apt-get -yyqq upgrade ; \ + apt-get -yyqq install ca-certificates ; \ + apt-get clean + +FROM scratch + +COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +ADD coredns /coredns + +EXPOSE 53 53/udp +ENTRYPOINT ["/coredns"] diff --git a/ag_201_coredns/GOVERNANCE.md b/ag_201_coredns/GOVERNANCE.md new file mode 100644 index 0000000..ef358a7 --- /dev/null +++ b/ag_201_coredns/GOVERNANCE.md @@ -0,0 +1,152 @@ +# CoreDNS Governance + +## Principles + +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). +- 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 Steering Committee + +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 committe membership, and changes to project governance. See `Changes in Project Steeting Committee Membership` +and `Changes in Project Governance`. + +Any decision made must not conflict with CNCF policy. + +The maximum term length of each steering committee member is one year, with no term limit restriction. + +Steering committee member are elected by CoreDNS maintainers. + +The steering committee members are identified in the [CODEOWNERS](CODEOWNERS) file. + +## Expectations from Maintainers + +Every one carries water... + +Making a community work requires input/effort from everyone. Maintainers should actively +participate in Pull Request reviews. Maintainers are expected to respond to assigned Pull Requests +in a *reasonable* time frame, either providing insights, or assign the Pull Requests to other +maintainers. + +Every Maintainer is listed in the +[CODEOWNERS](https://github.com/coredns/coredns/blob/master/CODEOWNERS) +file, with their Github handle. + +A Maintainer should be a member of `maintainers@coredns.io`, although this is not a hard requirement. + +## Becoming a Maintainer + +On successful merge of a significant pull request any current maintainer can reach +to the author behind the pull request and ask them if they are willing to become a CoreDNS +maintainer. The email of the new maintainer invitation should be cc'ed to `maintainers@coredns.io` +as part of the process. + +## Changes in Maintainership + +If a Maintainer feels she/he can not fulfill the "Expectations from Maintainers", they are free to +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). + +## Changes in Project Steering Committee Membership + +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 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 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 +replaced committe 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. + +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. + +A project steering committee member may volunteer to step down, ending their term early. + +## Changes in Project Governance + +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 comittee 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 Steeting Committee Membership`. + +## 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, +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 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 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. + +The decision-making process should be transparent to adhere to the CoreDNS Code of Conduct. + +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 + +The __coredns__ GitHub project maintainers team reflects the list of Maintainers. + +## Other Projects + +The CoreDNS organization is open to receive new sub-projects under its umbrella. To accept a project +into the __CoreDNS__ organization, it has to meet the following criteria: + +- Must be licensed under the terms of the Apache License v2.0 +- Must be related to one or more scopes of the CoreDNS ecosystem: + - CoreDNS project artifacts (website, deployments, CI, etc) + - External plugins + - Other DNS related processing +- Must be supported by a Maintainer not associated or affiliated with the author(s) of the sub-projects + +The submission process starts as a Pull Request or Issue on the +[coredns/coredns](https://github.com/coredns/coredns) repository with the required information +mentioned above. Once a project is accepted, it's considered a __CNCF sub-project under the umbrella +of CoreDNS__. + +## New Plugins + +The CoreDNS is open to receive new plugins as part of the CoreDNS repo. The submission process +is the same as a Pull Request submission. Unlike small Pull Requests though, a new plugin submission +should only be approved by a maintainer not associated or affiliated with the author(s) of the +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 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. + +## Credits + +Sections of this documents have been borrowed from [Fluentd](https://github.com/fluent/fluentd/blob/master/GOVERNANCE.md) and [Envoy](https://github.com/envoyproxy/envoy/blob/master/GOVERNANCE.md) projects. diff --git a/ag_201_coredns/LICENSE b/ag_201_coredns/LICENSE new file mode 100644 index 0000000..1249731 --- /dev/null +++ b/ag_201_coredns/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016-2020 The CoreDNS authors and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ag_201_coredns/Makefile b/ag_201_coredns/Makefile new file mode 100644 index 0000000..43d03e2 --- /dev/null +++ b/ag_201_coredns/Makefile @@ -0,0 +1,37 @@ +# Makefile for building CoreDNS +GITCOMMIT:=$(shell git describe --dirty --always) +BINARY:=coredns +SYSTEM:= +CHECKS:=check +BUILDOPTS:=-v +GOPATH?=$(HOME)/go +MAKEPWD:=$(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +CGO_ENABLED?=0 + +.PHONY: all +all: coredns + +.PHONY: coredns +coredns: $(CHECKS) + CGO_ENABLED=$(CGO_ENABLED) $(SYSTEM) go build $(BUILDOPTS) -ldflags="-s -w -X github.com/coredns/coredns/coremain.GitCommit=$(GITCOMMIT)" -o $(BINARY) + +.PHONY: check +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: + $(MAKE) -C pb + +.PHONY: clean +clean: + go clean + rm -f coredns diff --git a/ag_201_coredns/Makefile.doc b/ag_201_coredns/Makefile.doc new file mode 100644 index 0000000..2adc143 --- /dev/null +++ b/ag_201_coredns/Makefile.doc @@ -0,0 +1,64 @@ +# This Makefile generates the manual pages from the markdown README.mds. It depends +# on https://github.com/mmarkdown/mmark to be installed. Generally we want this to be +# updated before doing a release. The Debian package, for instance, looks at these pages +# and will install them on your system. + +MMARK_VERSION:=2.2.4 +PLUGINS:=$(wildcard plugin/*/README.md) +READMES:=$(subst plugin/,,$(PLUGINS)) +READMES:=$(subst /README.md,,$(READMES)) +PLUGINS:=$(subst plugin/,coredns-,$(PLUGINS)) +PLUGINS:=$(subst /README.md,(7),$(PLUGINS)) + +all: mmark man/coredns.1 man/corefile.5 plugins + +GO ?= go +GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) +GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) +GO_BUILD_PLATFORM ?= $(GOHOSTOS)_$(GOHOSTARCH) + +FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) +MMARK_BIN := $(FIRST_GOPATH)/bin/mmark +MMARK := $(FIRST_GOPATH)/bin/mmark -man + +MMARK_URL := https://github.com/mmarkdown/mmark/releases/download/v$(MMARK_VERSION)/mmark_$(MMARK_VERSION)_$(GO_BUILD_PLATFORM).tgz + +.PHONY: mmark +mmark: $(MMARK_BIN) + +$(MMARK_BIN): + $(eval MMARK_TMP := $(shell mktemp -d)) + curl -s -L $(MMARK_URL) | tar -xvzf - -C $(MMARK_TMP) + mkdir -p $(FIRST_GOPATH)/bin + cp $(MMARK_TMP)/mmark $(FIRST_GOPATH)/bin/mmark + rm -r $(MMARK_TMP) + +man/coredns.1: coredns.1.md + @/bin/echo -e '%%%\n title = "coredns 1"\n' \ + 'area = "CoreDNS"\n workgroup = "CoreDNS"\n%%%\n\n' > $@.header + @cat $@.header $< > $@.md && rm $@.header + @sed -i -e "s/@@PLUGINS@@/$(PLUGINS)/" $@.md + $(MMARK) $@.md > $@ && rm $@.md + +man/corefile.5: corefile.5.md + @/bin/echo -e '%%%\n title = "corefile 5"\n' \ + 'area = "CoreDNS"\n workgroup = "CoreDNS"\n%%%\n\n' > $@.header + @cat $@.header $< > $@.md && rm $@.header + $(MMARK) $@.md > $@ && rm $@.md + +.PHONY: plugins +plugins: + for README in $(READMES); do \ + $(MAKE) -f Makefile.doc man/coredns-$$README.7; \ + done + +man/coredns-%.7: plugin/%/README.md + @/bin/echo -e "%%%\n title = \"`basename $@ | sed s\/\.7\/\/` 7\"\n" \ + 'area = "CoreDNS"\n workgroup = "CoreDNS Plugins"\n%%%\n\n' > $@.header + @cat $@.header $< > $@.md && rm $@.header + @sed -i '/^# .*/d' $@.md + $(MMARK) $@.md > $@ && rm $@.md + +PHONY: clean +clean: + rm -f man/* diff --git a/ag_201_coredns/Makefile.docker b/ag_201_coredns/Makefile.docker new file mode 100644 index 0000000..65f5fe4 --- /dev/null +++ b/ag_201_coredns/Makefile.docker @@ -0,0 +1,115 @@ +# 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 +DOCKER_IMAGE_NAME:=$(DOCKER)/$(NAME) +DOCKER_IMAGE_LIST_VERSIONED:=$(shell echo $(LINUX_ARCH) | 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 ); \ + cp Dockerfile build/docker/$${arch} ; \ + 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 \ + docker build -t $(DOCKER_IMAGE_NAME):$${arch}-$(VERSION) build/docker/$${arch} ;\ + done +endif + +.PHONY: docker-push +docker-push: +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 $(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) ;\ + 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) + 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}-$(VERSION) ;\ + done + 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/ag_201_coredns/Makefile.release b/ag_201_coredns/Makefile.release new file mode 100644 index 0000000..38b38b7 --- /dev/null +++ b/ag_201_coredns/Makefile.release @@ -0,0 +1,141 @@ +# Makefile for releasing CoreDNS +# +# The release is controlled from coremain/version.go. The version found there is +# used to tag the git repo and to build the assets that are uploaded to GitHub. +# +# The release should be accompanied by release notes in the notes/ subdirectory. +# 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. +# +# 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: +# +# * important changes at the top +# * people who committed/review code (the latter is harder to get) +# * Slightly abbreviated list of pull requests merged for this release. +# +# Steps to release, first: +# +# 1. Up the version in coremain/version.go +# 2. Do a make -f Makefile.doc # This has been automated in GitHub, so you can probably skip this step +# 3. go generate +# 4. Send PR to get this merged. +# +# Then: +# +# 1. Open an issue for this release +# 2. In an issue give the command: /release master VERSION +# Where VERSION is the version of the release - the release script double checks this with the +# actual CoreDNS version in coremain/version.go +# 3. (to test as release /release -t master VERSION can be used. +# +# See https://github.com/coredns/release for documentation README on what needs to be setup for this to be +# automated (can still be done by hand if needed). Especially what environment variables need to be +# set! This further depends on Caddy being setup and [dreck](https://github.com/miekg/dreck) running as a plugin in Caddy. +# +# To release we run, these target from the this Makefile.release ordered like: +# * make release +# * make github-push +# +# 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 + +NAME:=coredns +VERSION:=$(shell grep 'CoreVersion' coremain/version.go | awk '{ print $$3 }' | tr -d '"') +GITHUB:=coredns +LINUX_ARCH:=amd64 arm arm64 mips64le ppc64le s390x mips + +all: + @echo Use the 'release' target to build a release + +release: build tar + +.PHONY: build +build: + @go version + @echo Cleaning old builds + @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: 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/$(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="" ;\ + done + +.PHONY: tar +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)_windows_amd64.tgz -C build/windows/amd64 $(NAME).exe + for arch in $(LINUX_ARCH); do \ + tar -zcf release/$(NAME)_$(VERSION)_linux_$$arch.tgz -C build/linux/$$arch $(NAME) ;\ + done + +.PHONY: github-push +github-push: +ifeq ($(GITHUB_ACCESS_TOKEN),) + $(error "Please set the GITHUB_ACCESS_TOKEN environment variable") +else + @echo Releasing: $(VERSION) + @$(eval RELEASE:=$(shell curl -s -d '{"tag_name": "v$(VERSION)", "name": "v$(VERSION)"}' -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" "https://api.github.com/repos/$(GITHUB)/$(NAME)/releases" | grep -m 1 '"id"' | tr -cd '[[:digit:]]')) + @echo ReleaseID: $(RELEASE) + @( cd release; for asset in `ls -A *tgz`; do \ + echo $$asset; \ + curl -o /dev/null -X POST \ + -H "Content-Type: application/gzip" \ + -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" \ + --data-binary "@$$asset" \ + "https://uploads.github.com/repos/$(GITHUB)/$(NAME)/releases/$(RELEASE)/assets?name=$${asset}" ; \ + done ) + @( cd release; for asset in `ls -A *tgz`; do \ + sha256sum $$asset > $$asset.sha256; \ + done ) + @( cd release; for asset in `ls -A *sha256`; do \ + echo $$asset; \ + curl -o /dev/null -X POST \ + -H "Content-Type: text/plain" \ + -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" \ + --data-binary "@$$asset" \ + "https://uploads.github.com/repos/$(GITHUB)/$(NAME)/releases/$(RELEASE)/assets?name=$${asset}" ; \ + done ) +endif + +.PHONY: version +version: + @echo $(VERSION) + +.PHONY: clean +clean: + rm -rf release + rm -rf build + +.PHONY: notes +notes: + @$(MAKE) -s -f Makefile.release authors + @echo + @$(MAKE) -s -f Makefile.release prs + +.PHONY: prs +prs: + @echo "## Noteworthy Changes" + @echo + @git log $$(git describe --tags --abbrev=0)..HEAD --oneline | awk ' { $$1="";print } ' | sed 's/^ //' | sed -e 's|#\([0-9]\)|https://github.com/coredns/coredns/pull/\1|' | \ + grep -v '^build(deps)' | \ + grep -v '^auto go mod tidy' | grep -v '^auto remove' | grep -v '^auto make' | sed 's/^/* /' + +.PHONY: authors +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 '^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/ag_201_coredns/README.md b/ag_201_coredns/README.md new file mode 100644 index 0000000..c8b37c3 --- /dev/null +++ b/ag_201_coredns/README.md @@ -0,0 +1,297 @@ +[![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) +![CodeQL](https://github.com/coredns/coredns/actions/workflows/codeql-analysis.yml/badge.svg) +![Go Fmt](https://github.com/coredns/coredns/actions/workflows/go.fmt.yml/badge.svg) +![Go Tests](https://github.com/coredns/coredns/actions/workflows/go.test.yml/badge.svg) +![Go Tidy](https://github.com/coredns/coredns/actions/workflows/go.tidy.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) + +CoreDNS is a DNS server/forwarder, written in Go, that chains [plugins](https://coredns.io/plugins). +Each plugin performs a (DNS) function. + +CoreDNS is a [Cloud Native Computing Foundation](https://cncf.io) graduated project. + +CoreDNS is a fast and flexible DNS server. The key word here is *flexible*: with CoreDNS you +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). + +Currently CoreDNS is able to: + +* Serve zone data from a file; both DNSSEC (NSEC only) and DNS are supported (*file* and *auto*). +* Retrieve zone data from primaries, i.e., act as a secondary server (AXFR only) (*secondary*). +* Sign zone data on-the-fly (*dnssec*). +* Load balancing of responses (*loadbalance*). +* Allow for zone transfers, i.e., act as a primary server (*file* + *transfer*). +* Automatically load zone files from disk (*auto*). +* Caching of DNS responses (*cache*). +* Use etcd as a backend (replacing [SkyDNS](https://github.com/skynetservices/skydns)) (*etcd*). +* Use k8s (kubernetes) as a backend (*kubernetes*). +* Serve as a proxy to forward queries to some other (recursive) nameserver (*forward*). +* Provide metrics (by using Prometheus) (*prometheus*). +* Provide query (*log*) and error (*errors*) logging. +* Integrate with cloud providers (*route53*). +* Support the CH class: `version.bind` and friends (*chaos*). +* Support the RFC 5001 DNS name server identifier (NSID) option (*nsid*). +* Profiling support (*pprof*). +* Rewrite queries (qtype, qclass and qname) (*rewrite* and *template*). +* Block ANY queries (*any*). +* Provide DNS64 IPv6 Translation (*dns64*). + +And more. Each of the plugins is documented. See [coredns.io/plugins](https://coredns.io/plugins) +for all in-tree plugins, and [coredns.io/explugins](https://coredns.io/explugins) for all +out-of-tree plugins. + +## Compilation from Source + +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.17 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: + +~~~ +$ git clone https://github.com/coredns/coredns +$ cd coredns +$ make +~~~ + +This should yield a `coredns` binary. + +## Compilation with Docker + +CoreDNS requires Go to compile. However, if you already have docker installed and prefer not to +setup a Go environment, you could build CoreDNS easily: + +``` +$ docker run --rm -i -t -v $PWD:/v -w /v golang:1.18 make +``` + +The above command alone will have `coredns` binary generated. + +## Examples + +When starting CoreDNS without any configuration, it loads the +[*whoami*](https://coredns.io/plugins/whoami) and [*log*](https://coredns.io/plugins/log) plugins +and starts listening on port 53 (override with `-dns.port`), it should show the following: + +~~~ txt +.:53 +CoreDNS-1.6.6 +linux/amd64, go1.16.10, aa8c32 +~~~ + +The following could be used to query the CoreDNS server that is running now: + +~~~ txt +dig @127.0.0.1 -p 53 www.example.com +~~~ + +Any query sent to port 53 should return some information; your sending address, port and protocol +used. The query should also be logged to standard output. + +The configuration of CoreDNS is done through a file named `Corefile`. When CoreDNS starts, it will +look for the `Corefile` from the current working directory. A `Corefile` for CoreDNS server that listens +on port `53` and enables `whoami` plugin is: + +~~~ corefile +.:53 { + whoami +} +~~~ + +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. + +~~~ corefile +.:1053 { + whoami +} +~~~ + +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 +.:53 { + forward . 8.8.8.8:53 + log +} +~~~ + +Start CoreDNS and then query on that port (53). The query should be forwarded to 8.8.8.8 and the +response will be returned. Each query should also show up in the log which is printed on standard +output. + +To serve the (NSEC) DNSSEC-signed `example.org` on port 1053, with errors and logging sent to standard +output. Allow zone transfers to everybody, but specifically mention 1 IP address so that CoreDNS can +send notifies to it. + +~~~ txt +example.org:1053 { + file /var/lib/coredns/example.org.signed + transfer { + to * 2001:500:8f::53 + } + errors + log +} +~~~ + +Serve `example.org` on port 1053, but forward everything that does *not* match `example.org` to a +recursive nameserver *and* rewrite ANY queries to HINFO. + +~~~ txt +example.org:1053 { + file /var/lib/coredns/example.org.signed + transfer { + to * 2001:500:8f::53 + } + errors + log +} + +. { + any + forward . 8.8.8.8:53 + errors + log +} +~~~ + +IP addresses are also allowed. They are automatically converted to reverse zones: + +~~~ corefile +10.0.0.0/24 { + whoami +} +~~~ +Means you are authoritative for `0.0.10.in-addr.arpa.`. + +This also works for IPv6 addresses. If for some reason you want to serve a zone named `10.0.0.0/24` +add the closing dot: `10.0.0.0/24.` as this also stops the conversion. + +This even works for CIDR (See RFC 1518 and 1519) addressing, i.e. `10.0.0.0/25`, CoreDNS will then +check if the `in-addr` request falls in the correct range. + +Listening on TLS (DoT) and for gRPC? Use: + +~~~ corefile +tls://example.org grpc://example.org { + whoami +} +~~~ + +And for DNS over HTTP/2 (DoH) use: + +~~~ corefile +https://example.org { + whoami + tls mycert mykey +} +~~~ +in this setup, the CoreDNS will be responsible for TLS termination + +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: + +~~~ txt +grpc://example.org:1443 https://example.org:1444 { + # ... +} +~~~ + +When no transport protocol is specified the default `dns://` is assumed. + +## Community + +We're most active on Github (and Slack): + +- Github: +- Slack: #coredns on + +More resources can be found: + +- Website: +- 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). + +## Deployment + +Examples for deployment via systemd and other use cases can be found in the [deployment +repository](https://github.com/coredns/deployment). + +## Deprecation Policy + +When there is a backwards incompatible change in CoreDNS the following process is followed: + +* Release x.y.z: Announce that in the next release we will make backward incompatible changes. +* Release x.y+1.0: Increase the minor version and set the patch version to 0. Make the changes, + but allow the old configuration to be parsed. I.e. CoreDNS will start from an unchanged + Corefile. +* Release x.y+1.1: Increase the patch version to 1. Remove the lenient parsing, so CoreDNS will + not start if those features are still used. + +E.g. 1.3.1 announce a change. 1.4.0 a new release with the change but backward compatible config. +And finally 1.4.1 that removes the config workarounds. + +## Security + +### 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 + +If you find a security vulnerability or any security related issues, please DO NOT file a public +issue, instead send your report privately to `security@coredns.io`. Security reports are greatly +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) diff --git a/ag_201_coredns/SECURITY.md b/ag_201_coredns/SECURITY.md new file mode 100644 index 0000000..e69de29 diff --git a/ag_201_coredns/core/coredns.go b/ag_201_coredns/core/coredns.go new file mode 100644 index 0000000..0ff1dc9 --- /dev/null +++ b/ag_201_coredns/core/coredns.go @@ -0,0 +1,7 @@ +// Package core registers the server and all plugins we support. +package core + +import ( + // plug in the server + _ "github.com/coredns/coredns/core/dnsserver" +) diff --git a/ag_201_coredns/core/dnsserver/address.go b/ag_201_coredns/core/dnsserver/address.go new file mode 100644 index 0000000..872e44c --- /dev/null +++ b/ag_201_coredns/core/dnsserver/address.go @@ -0,0 +1,86 @@ +package dnsserver + +import ( + "fmt" + "net" + "strings" +) + +type zoneAddr struct { + Zone string + Port string + Transport string // dns, tls or grpc + Address string // used for bound zoneAddr - validation of overlapping +} + +// String returns the string representation of z. +func (z zoneAddr) String() string { + s := z.Transport + "://" + z.Zone + ":" + z.Port + if z.Address != "" { + s += " on " + z.Address + } + return s +} + +// 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, "://") + switch len(parts) { + case 1: + ip, port, err := net.SplitHostPort(parts[0]) + return "", ip, port, err + case 2: + ip, port, err := net.SplitHostPort(parts[1]) + return parts[0], ip, port, err + default: + return "", "", "", fmt.Errorf("provided value is not in an address format : %s", address) + } +} + +type zoneOverlap struct { + registeredAddr map[zoneAddr]zoneAddr // each zoneAddr is registered once by its key + unboundOverlap map[zoneAddr]zoneAddr // the "no bind" equiv ZoneAddr is registered by its original key +} + +func newOverlapZone() *zoneOverlap { + return &zoneOverlap{registeredAddr: make(map[zoneAddr]zoneAddr), unboundOverlap: make(map[zoneAddr]zoneAddr)} +} + +// 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 := 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 + return nil, &already + } + if _, ok := zo.registeredAddr[uz]; ok { + // current zone is bound to an address, but there is already an overlapping zone+port with no bind address + return nil, &uz + } + } + // 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/ag_201_coredns/core/dnsserver/address_test.go b/ag_201_coredns/core/dnsserver/address_test.go new file mode 100644 index 0000000..05b6013 --- /dev/null +++ b/ag_201_coredns/core/dnsserver/address_test.go @@ -0,0 +1,113 @@ +package dnsserver + +import "testing" + +func TestSplitProtocolHostPort(t *testing.T) { + for i, test := range []struct { + input string + proto string + ip string + port string + shouldErr bool + }{ + {"dns://:53", "dns", "", "53", false}, + {"dns://127.0.0.1:4005", "dns", "127.0.0.1", "4005", false}, + {"[ffe0:34ab:1]:4005", "", "ffe0:34ab:1", "4005", false}, + + // port part is mandatory + {"dns://", "dns", "", "", true}, + {"dns://127.0.0.1", "dns", "127.0.0.1", "", true}, + // cannot be empty + {"", "", "", "", true}, + // invalid format with twice :// + {"dns://127.0.0.1://53", "", "", "", true}, + } { + proto, ip, port, err := SplitProtocolHostPort(test.input) + if test.shouldErr && err == nil { + t.Errorf("Test %d: (address = %s) expected error, but there wasn't any", i, test.input) + continue + } + if !test.shouldErr && err != nil { + t.Errorf("Test %d: (address = %s) expected no error, but there was one: %v", i, test.input, err) + continue + } + if err == nil || test.shouldErr { + continue + } + if proto != test.proto { + t.Errorf("Test %d: (address = %s) expected protocol with value %s but got %s", i, test.input, test.proto, proto) + } + if ip != test.ip { + t.Errorf("Test %d: (address = %s) expected ip with value %s but got %s", i, test.input, test.ip, ip) + } + if port != test.port { + t.Errorf("Test %d: (address = %s) expected port with value %s but got %s", i, test.input, test.port, port) + } + } +} + +type checkCall struct { + zone zoneAddr + same bool + overlap bool + overlapKey string +} + +type checkTest struct { + sequence []checkCall +} + +func TestOverlapAddressChecker(t *testing.T) { + for i, test := range []checkTest{ + {sequence: []checkCall{ + {zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "53"}, false, false, ""}, + {zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "53"}, true, false, ""}, + }, + }, + {sequence: []checkCall{ + {zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "53"}, false, false, ""}, + {zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "54"}, false, false, ""}, + {zoneAddr{Transport: "dns", Zone: ".", Address: "127.0.0.1", Port: "53"}, false, true, "dns://.:53"}, + }, + }, + {sequence: []checkCall{ + {zoneAddr{Transport: "dns", Zone: ".", Address: "127.0.0.1", Port: "53"}, false, false, ""}, + {zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "54"}, false, false, ""}, + {zoneAddr{Transport: "dns", Zone: ".", Address: "127.0.0.1", Port: "53"}, true, false, ""}, + }, + }, + {sequence: []checkCall{ + {zoneAddr{Transport: "dns", Zone: ".", Address: "127.0.0.1", Port: "53"}, false, false, ""}, + {zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "54"}, false, false, ""}, + {zoneAddr{Transport: "dns", Zone: ".", Address: "128.0.0.1", Port: "53"}, false, false, ""}, + {zoneAddr{Transport: "dns", Zone: ".", Address: "129.0.0.1", Port: "53"}, false, false, ""}, + {zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "53"}, false, true, "dns://.:53 on 129.0.0.1"}, + }, + }, + {sequence: []checkCall{ + {zoneAddr{Transport: "dns", Zone: ".", Address: "127.0.0.1", Port: "53"}, false, false, ""}, + {zoneAddr{Transport: "dns", Zone: "com.", Address: "127.0.0.1", Port: "53"}, false, false, ""}, + {zoneAddr{Transport: "dns", Zone: "com.", Address: "", Port: "53"}, false, true, "dns://com.:53 on 127.0.0.1"}, + }, + }, + } { + checker := newOverlapZone() + for _, call := range test.sequence { + same, overlap := checker.registerAndCheck(call.zone) + sZone := call.zone.String() + if (same != nil) != call.same { + t.Errorf("Test %d: error, for zone %s, 'same' (%v) has not the expected value (%v)", i, sZone, same != nil, call.same) + } + if same == nil { + if (overlap != nil) != call.overlap { + t.Errorf("Test %d: error, for zone %s, 'overlap' (%v) has not the expected value (%v)", i, sZone, overlap != nil, call.overlap) + } + if overlap != nil { + 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/ag_201_coredns/core/dnsserver/config.go b/ag_201_coredns/core/dnsserver/config.go new file mode 100644 index 0000000..3da8627 --- /dev/null +++ b/ag_201_coredns/core/dnsserver/config.go @@ -0,0 +1,99 @@ +package dnsserver + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" +) + +// Config configuration for a single server. +type Config struct { + // The zone of the site. + Zone string + + // one or several hostnames to bind the server to. + // defaults to a single empty string that denote the wildcard address + ListenHosts []string + + // The port to listen on. + Port string + + // 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 + + // 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 + + // If this function is not nil it will be used to inspect and validate + // HTTP requests. Although this isn't referenced in-tree, external plugins + // may depend on it. + HTTPRequestValidateFunc func(*http.Request) bool + + // 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 + 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 + + // TSIG secrets, [name]key. + TsigSecret map[string]string + + // Plugin stack. + Plugin []plugin.Plugin + + // Compiled plugin stack. + pluginChain plugin.Handler + + // Plugin interested in announcing that they exist, so other plugin can call methods + // 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) +} + +// GetConfig gets the Config that corresponds to c. +// If none exist nil is returned. +func GetConfig(c *caddy.Controller) *Config { + ctx := c.Context().(*dnsContext) + key := keyForConfig(c.ServerBlockIndex, c.ServerBlockKeyIndex) + if cfg, ok := ctx.keysToConfigs[key]; ok { + return cfg + } + // we should only get here during tests because directive + // actions typically skip the server blocks where we make + // the configs. + ctx.saveConfig(key, &Config{ListenHosts: []string{""}}) + return GetConfig(c) +} diff --git a/ag_201_coredns/core/dnsserver/https.go b/ag_201_coredns/core/dnsserver/https.go new file mode 100644 index 0000000..382e06e --- /dev/null +++ b/ag_201_coredns/core/dnsserver/https.go @@ -0,0 +1,30 @@ +package dnsserver + +import ( + "net" + "net/http" + + "github.com/coredns/coredns/plugin/pkg/nonwriter" +) + +// DoHWriter is a nonwriter.Writer 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. + laddr net.Addr + + // request is the HTTP request we're currently handling. + request *http.Request +} + +// RemoteAddr returns the remote address. +func (d *DoHWriter) RemoteAddr() net.Addr { return d.raddr } + +// LocalAddr returns the local address. +func (d *DoHWriter) LocalAddr() net.Addr { return d.laddr } + +// Request returns the HTTP request +func (d *DoHWriter) Request() *http.Request { return d.request } diff --git a/ag_201_coredns/core/dnsserver/https_test.go b/ag_201_coredns/core/dnsserver/https_test.go new file mode 100644 index 0000000..00ed366 --- /dev/null +++ b/ag_201_coredns/core/dnsserver/https_test.go @@ -0,0 +1,90 @@ +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) + } + }) + } +} diff --git a/ag_201_coredns/core/dnsserver/log_test.go b/ag_201_coredns/core/dnsserver/log_test.go new file mode 100644 index 0000000..cee92cc --- /dev/null +++ b/ag_201_coredns/core/dnsserver/log_test.go @@ -0,0 +1,5 @@ +package dnsserver + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/core/dnsserver/onstartup.go b/ag_201_coredns/core/dnsserver/onstartup.go new file mode 100644 index 0000000..572b358 --- /dev/null +++ b/ag_201_coredns/core/dnsserver/onstartup.go @@ -0,0 +1,57 @@ +package dnsserver + +import ( + "fmt" + "regexp" + "sort" + + "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 := "" + + keys := make([]string, len(zones)) + i := 0 + + for k := range zones { + keys[i] = k + i++ + } + sort.Strings(keys) + + for _, zone := range keys { + if !checkZoneSyntax(zone) { + s += 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) + continue + } + if ip == "" { + s += 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) + } + return s +} diff --git a/ag_201_coredns/core/dnsserver/onstartup_test.go b/ag_201_coredns/core/dnsserver/onstartup_test.go new file mode 100644 index 0000000..41d4d82 --- /dev/null +++ b/ag_201_coredns/core/dnsserver/onstartup_test.go @@ -0,0 +1,39 @@ +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) + } + } +} diff --git a/ag_201_coredns/core/dnsserver/register.go b/ag_201_coredns/core/dnsserver/register.go new file mode 100644 index 0000000..e94accc --- /dev/null +++ b/ag_201_coredns/core/dnsserver/register.go @@ -0,0 +1,324 @@ +package dnsserver + +import ( + "flag" + "fmt" + "net" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/caddy/caddyfile" + "github.com/coredns/coredns/plugin" + "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") + flag.StringVar(&Port, "p", DefaultPort, "Default port") + + caddy.RegisterServerType(serverType, caddy.ServerType{ + Directives: func() []string { return Directives }, + DefaultInput: func() caddy.Input { + return caddy.CaddyfileInput{ + Filepath: "Corefile", + Contents: []byte(".:" + Port + " {\nwhoami\nlog\n}\n"), + ServerTypeName: serverType, + } + }, + NewContext: newContext, + }) +} + +func newContext(i *caddy.Instance) caddy.Context { + return &dnsContext{keysToConfigs: make(map[string]*Config)} +} + +type dnsContext struct { + keysToConfigs map[string]*Config + + // configs is the master list of all site configs. + configs []*Config +} + +func (h *dnsContext) saveConfig(key string, cfg *Config) { + h.configs = append(h.configs, cfg) + h.keysToConfigs[key] = cfg +} + +// Compile-time check to ensure dnsContext implements the caddy.Context interface +var _ caddy.Context = &dnsContext{} + +// InspectServerBlocks make sure that everything checks out before +// executing directives and otherwise prepares the directives to +// be parsed and executed. +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 { + 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.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{ + Zone: za.Zone, + ListenHosts: []string{""}, + Port: za.Port, + Transport: za.Transport, + } + + // 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) + } + } + return serverBlocks, nil +} + +// MakeServers uses the newly-created siteConfigs to create and return a list of server instances. +func (h *dnsContext) MakeServers() ([]caddy.Server, error) { + // Copy the Plugin, ListenHosts and Debug 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. + for _, c := range h.configs { + c.Plugin = c.firstConfigInBlock.Plugin + c.ListenHosts = c.firstConfigInBlock.ListenHosts + c.Debug = c.firstConfigInBlock.Debug + c.Stacktrace = c.firstConfigInBlock.Stacktrace + c.TLSConfig = c.firstConfigInBlock.TLSConfig + c.TsigSecret = c.firstConfigInBlock.TsigSecret + } + + // 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) + + case transport.HTTPS: + s, err := NewServerHTTPS(addr, group) + if err != nil { + return nil, err + } + servers = append(servers, s) + } + } + + // 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) + } + } + } + + // 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 +} + +// AddPlugin adds a plugin to a site's plugin stack. +func (c *Config) AddPlugin(m plugin.Plugin) { + c.Plugin = append(c.Plugin, m) +} + +// registerHandler adds a handler to a site's handler registration. Handlers +// 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) + } + + // Just overwrite... + c.registry[h.Name()] = h +} + +// Handler returns the plugin handler that has been added to the config under its name. +// This is useful to inspect if a certain plugin is active in this server. +// Note that this is order dependent and the order is defined in directives.go, i.e. if your plugin +// comes before the plugin you are checking; it will not be there (yet). +func (c *Config) Handler(name string) plugin.Handler { + if c.registry == nil { + return nil + } + if h, ok := c.registry[name]; ok { + return h + } + return nil +} + +// Handlers returns a slice of plugins that have been registered. This can be used to +// inspect and interact with registered plugins but cannot be used to remove or add plugins. +// Note that this is order dependent and the order is defined in directives.go, i.e. if your plugin +// comes before the plugin you are checking; it will not be there (yet). +func (c *Config) Handlers() []plugin.Handler { + if c.registry == nil { + return nil + } + hs := make([]plugin.Handler, 0, len(c.registry)) + for k := range c.registry { + hs = append(hs, c.registry[k]) + } + return hs +} + +func (h *dnsContext) validateZonesAndListeningAddresses() error { + //Validate Zone and addresses + checker := newOverlapZone() + for _, conf := range h.configs { + for _, h := range conf.ListenHosts { + // Validate the overlapping of ZoneAddr + akey := zoneAddr{Transport: conf.Transport, Zone: conf.Zone, Address: h, Port: conf.Port} + 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 +} + +// groupSiteConfigsByListenAddr 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 { + addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(h, conf.Port)) + if err != nil { + return nil, err + } + addrstr := conf.Transport + "://" + addr.String() + groups[addrstr] = append(groups[addrstr], conf) + } + } + + return groups, nil +} + +// DefaultPort is the default port. +const DefaultPort = transport.Port + +// These "soft defaults" are configurable by +// command line flags, etc. +var ( + // Port is the port we listen on by default. + Port = DefaultPort + + // GracefulTimeout is the maximum duration of a graceful shutdown. + GracefulTimeout time.Duration +) + +var _ caddy.GracefulServer = new(Server) diff --git a/ag_201_coredns/core/dnsserver/register_test.go b/ag_201_coredns/core/dnsserver/register_test.go new file mode 100644 index 0000000..4d3ce11 --- /dev/null +++ b/ag_201_coredns/core/dnsserver/register_test.go @@ -0,0 +1,120 @@ +package dnsserver + +import ( + "testing" +) + +func TestHandler(t *testing.T) { + tp := testPlugin{} + c := testConfig("dns", tp) + 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 { + t.Errorf("Expected testPlugin from Handler, got %T", h) + } + if h := c.Handler("nothing"); h != nil { + t.Errorf("Expected nil from Handler, got %T", h) + } +} + +func TestHandlers(t *testing.T) { + tp := testPlugin{} + c := testConfig("dns", tp) + if _, err := NewServer("127.0.0.1:53", []*Config{c}); err != nil { + t.Errorf("Expected no error for NewServer, got %s", err) + } + hs := c.Handlers() + if len(hs) != 1 || hs[0] != tp { + t.Errorf("Expected [testPlugin] from Handlers, got %v", hs) + } +} + +func TestGroupingServers(t *testing.T) { + for i, test := range []struct { + configs []*Config + expectedGroups []string + failing bool + }{ + // single config -> one group + {configs: []*Config{ + {Transport: "dns", Zone: ".", Port: "53", ListenHosts: []string{""}}, + }, + expectedGroups: []string{"dns://:53"}, + failing: false}, + + // 2 configs on different port -> 2 groups + {configs: []*Config{ + {Transport: "dns", Zone: ".", Port: "53", ListenHosts: []string{""}}, + {Transport: "dns", Zone: ".", Port: "54", ListenHosts: []string{""}}, + }, + expectedGroups: []string{"dns://:53", "dns://:54"}, + failing: false}, + + // 2 configs on same port, both not using bind, diff zones -> 1 group + {configs: []*Config{ + {Transport: "dns", Zone: ".", Port: "53", ListenHosts: []string{""}}, + {Transport: "dns", Zone: "com.", Port: "53", ListenHosts: []string{""}}, + }, + expectedGroups: []string{"dns://:53"}, + failing: false}, + + // 2 configs on same port, one addressed - one not using bind, diff zones -> 1 group + {configs: []*Config{ + {Transport: "dns", Zone: ".", Port: "53", ListenHosts: []string{"127.0.0.1"}}, + {Transport: "dns", Zone: ".", Port: "54", ListenHosts: []string{""}}, + }, + expectedGroups: []string{"dns://127.0.0.1:53", "dns://:54"}, + failing: false}, + + // 2 configs on diff ports, 3 different address, diff zones -> 3 group + {configs: []*Config{ + {Transport: "dns", Zone: ".", Port: "53", ListenHosts: []string{"127.0.0.1", "::1"}}, + {Transport: "dns", Zone: ".", Port: "54", ListenHosts: []string{""}}}, + expectedGroups: []string{"dns://127.0.0.1:53", "dns://[::1]:53", "dns://:54"}, + failing: false}, + + // 2 configs on same port, same address, diff zones -> 1 group + {configs: []*Config{ + {Transport: "dns", Zone: ".", Port: "53", ListenHosts: []string{"127.0.0.1", "::1"}}, + {Transport: "dns", Zone: "com.", Port: "53", ListenHosts: []string{"127.0.0.1", "::1"}}, + }, + expectedGroups: []string{"dns://127.0.0.1:53", "dns://[::1]:53"}, + failing: false}, + + // 2 configs on same port, total 2 diff addresses, diff zones -> 2 groups + {configs: []*Config{ + {Transport: "dns", Zone: ".", Port: "53", ListenHosts: []string{"127.0.0.1"}}, + {Transport: "dns", Zone: "com.", Port: "53", ListenHosts: []string{"::1"}}, + }, + expectedGroups: []string{"dns://127.0.0.1:53", "dns://[::1]:53"}, + failing: false}, + + // 2 configs on same port, total 3 diff addresses, diff zones -> 3 groups + {configs: []*Config{ + {Transport: "dns", Zone: ".", Port: "53", ListenHosts: []string{"127.0.0.1", "::1"}}, + {Transport: "dns", Zone: "com.", Port: "53", ListenHosts: []string{""}}}, + expectedGroups: []string{"dns://127.0.0.1:53", "dns://[::1]:53", "dns://:53"}, + failing: false}, + } { + groups, err := groupConfigsByListenAddr(test.configs) + if err != nil { + if !test.failing { + t.Fatalf("Test %d, expected no errors, but got: %v", i, err) + } + continue + } + if test.failing { + t.Fatalf("Test %d, expected to failed but did not, returned values", i) + } + if len(groups) != len(test.expectedGroups) { + t.Errorf("Test %d : expected the group's size to be %d, was %d", i, len(test.expectedGroups), len(groups)) + continue + } + 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) + } + } + } +} diff --git a/ag_201_coredns/core/dnsserver/server.go b/ag_201_coredns/core/dnsserver/server.go new file mode 100644 index 0000000..478287b --- /dev/null +++ b/ag_201_coredns/core/dnsserver/server.go @@ -0,0 +1,428 @@ +// Package dnsserver implements all the interfaces from Caddy, so that CoreDNS can be a servertype plugin. +package dnsserver + +import ( + "context" + "fmt" + "net" + "runtime" + "runtime/debug" + "strings" + "sync" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metrics/vars" + "github.com/coredns/coredns/plugin/pkg/edns" + "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/rcode" + "github.com/coredns/coredns/plugin/pkg/reuseport" + "github.com/coredns/coredns/plugin/pkg/trace" + "github.com/coredns/coredns/plugin/pkg/transport" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + ot "github.com/opentracing/opentracing-go" +) + +// Server represents an instance of a server, which serves +// DNS requests at a particular address (host and port). A +// server is capable of serving numerous zones on +// the same address and the listener may be stopped for +// graceful termination (POSIX only). +type Server struct { + Addr string // Address we listen on + + 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() + stacktrace bool // enable stacktrace in recover error log + classChaos bool // allow non-INET class queries + + tsigSecret map[string]string +} + +// 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), + graceTimeout: 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() + } + s.stacktrace = site.Stacktrace + + // append the config to the zone's configs + s.zones[site.Zone] = append(s.zones[site.Zone], site) + + // copy tsig secrets + for key, secret := range site.TsigSecret { + s.tsigSecret[key] = secret + } + + // compile custom plugin for everything + var stack plugin.Handler + for i := len(site.Plugin) - 1; i >= 0; i-- { + stack = site.Plugin[i](stack) + + // 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 + if t, ok := stack.(trace.Trace); ok { + s.trace = t + } + } + // Unblock CH class queries when any of these plugins are loaded. + if _, ok := EnableChaos[stack.Name()]; ok { + s.classChaos = true + } + } + site.pluginChain = stack + } + + if !s.debug { + // When reloading we need to explicitly disable debug logging if it is now disabled. + log.D.Clear() + } + + return s, nil +} + +// Compile-time check to ensure Server implements the caddy.GracefulServer interface +var _ caddy.GracefulServer = &Server{} + +// Serve starts the server with an existing listener. It blocks until the server stops. +// 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) + }), TsigSecret: s.tsigSecret} + s.m.Unlock() + + return s.server[tcp].ActivateAndServe() +} + +// ServePacket starts the server with an existing packetconn. It blocks until the server stops. +// This implements caddy.UDPServer interface. +func (s *Server) ServePacket(p net.PacketConn) error { + s.m.Lock() + s.server[udp] = &dns.Server{PacketConn: p, Net: "udp", 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) + }), TsigSecret: s.tsigSecret} + s.m.Unlock() + + return s.server[udp].ActivateAndServe() +} + +// Listen implements caddy.TCPServer interface. +func (s *Server) Listen() (net.Listener, error) { + l, err := reuseport.Listen("tcp", s.Addr[len(transport.DNS+"://"):]) + if err != nil { + return nil, err + } + return l, nil +} + +// WrapListener Listen implements caddy.GracefulServer interface. +func (s *Server) WrapListener(ln net.Listener) net.Listener { + return ln +} + +// ListenPacket implements caddy.UDPServer interface. +func (s *Server) ListenPacket() (net.PacketConn, error) { + p, err := reuseport.ListenPacket("udp", s.Addr[len(transport.DNS+"://"):]) + if err != nil { + return nil, err + } + + 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. +// 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) + }() + + // Wait for remaining connections to finish or + // force them all to close after timeout + select { + case <-time.After(s.graceTimeout): + case <-done: + } + } + + // 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 +} + +// Address together with Stop() implement caddy.GracefulServer. +func (s *Server) Address() string { return s.Addr } + +// ServeDNS is the entry point for every request to the address that +// is bound to. It acts as a multiplexer for the requests zonename as +// defined in the request so that the correct zone +// (configuration and plugin stack) will handle the request. +func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) { + // The default dns.Mux checks the question section size, but we have our + // own mux here. Check if we have a question section. If not drop them here. + if r == nil || len(r.Question) == 0 { + errorAndMetricsFunc(s.Addr, w, r, dns.RcodeServerFailure) + return + } + + if !s.debug { + defer func() { + // 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 { + 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) + } + }() + } + + if !s.classChaos && r.Question[0].Qclass != dns.ClassINET { + errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused) + return + } + + if m, err := edns.Version(r); err != nil { // Wrong EDNS version, return at once. + w.WriteMsg(m) + return + } + + // Wrap the response writer in a ScrubWriter so we automatically make the reply fit in the client's buffer. + w = request.NewScrubWriter(r, w) + + q := strings.ToLower(r.Question[0].Name) + var ( + off int + end bool + dshandler *Config + ) + + for { + 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 + } + + 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) + } + 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 + } + } + } + off, end = dns.NextLabel(q, off) + if end { + break + } + } + + if r.Question[0].Qtype == dns.TypeDS && dshandler != nil && dshandler.pluginChain != nil { + // DS request, and we found a zone, use the handler for the query. + rcode, _ := dshandler.pluginChain.ServeDNS(ctx, w, r) + if !plugin.ClientWrite(rcode) { + errorFunc(s.Addr, w, r, rcode) + } + return + } + + // Wildcard match, if we have found nothing try the root zone as a last resort. + 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 + } + } + } + + // 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() { + if Quiet { + return + } + + out := startUpZones("", s.Addr, s.zones) + if out != "" { + fmt.Print(out) + } +} + +// Tracer returns the tracer in the server if defined. +func (s *Server) Tracer() ot.Tracer { + if s.trace == nil { + return nil + } + + return s.trace.Tracer() +} + +// errorFunc responds to an DNS request with an error. +func errorFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int) { + state := request.Request{W: w, Req: r} + + answer := new(dns.Msg) + answer.SetRcode(r, rc) + state.SizeAndDo(answer) + + w.WriteMsg(answer) +} + +func errorAndMetricsFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int) { + state := request.Request{W: w, Req: r} + + answer := new(dns.Msg) + answer.SetRcode(r, rc) + state.SizeAndDo(answer) + + vars.Report(server, state, vars.Dropped, "", rcode.ToString(rc), "" /* plugin */, answer.Len(), time.Now()) + + w.WriteMsg(answer) +} + +const ( + tcp = 0 + udp = 1 +) + +type ( + // Key is the context key for the current server added to the context. + Key struct{} + + // 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. +var EnableChaos = map[string]struct{}{ + "chaos": {}, + "forward": {}, + "proxy": {}, +} + +// Quiet mode will not show any informative output on initialization. +var Quiet bool diff --git a/ag_201_coredns/core/dnsserver/server_grpc.go b/ag_201_coredns/core/dnsserver/server_grpc.go new file mode 100644 index 0000000..9d7a95a --- /dev/null +++ b/ag_201_coredns/core/dnsserver/server_grpc.go @@ -0,0 +1,184 @@ +package dnsserver + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/pb" + "github.com/coredns/coredns/plugin/pkg/reuseport" + "github.com/coredns/coredns/plugin/pkg/transport" + + "github.com/grpc-ecosystem/grpc-opentracing/go/otgrpc" + "github.com/miekg/dns" + "github.com/opentracing/opentracing-go" + "google.golang.org/grpc" + "google.golang.org/grpc/peer" +) + +// 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 +} + +// NewServergRPC returns a new CoreDNS GRPC server and compiles all plugin in to it. +func NewServergRPC(addr string, group []*Config) (*ServergRPC, 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 + } + } + // 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{} + +// Serve implements caddy.TCPServer interface. +func (s *ServergRPC) Serve(l net.Listener) error { + s.m.Lock() + s.listenAddr = l.Addr() + s.m.Unlock() + + if s.Tracer() != nil { + onlyIfParent := func(parentSpanCtx opentracing.SpanContext, method string, req, resp interface{}) bool { + return parentSpanCtx != nil + } + intercept := otgrpc.OpenTracingServerInterceptor(s.Tracer(), otgrpc.IncludingSpans(onlyIfParent)) + s.grpcServer = grpc.NewServer(grpc.UnaryInterceptor(intercept)) + } else { + s.grpcServer = grpc.NewServer() + } + + pb.RegisterDnsServiceServer(s.grpcServer, s) + + if s.tlsConfig != nil { + l = tls.NewListener(l, s.tlsConfig) + } + return s.grpcServer.Serve(l) +} + +// ServePacket implements caddy.UDPServer interface. +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 + } + return l, nil +} + +// ListenPacket implements caddy.UDPServer interface. +func (s *ServergRPC) ListenPacket() (net.PacketConn, error) { return nil, nil } + +// OnStartupComplete lists the sites served by this server +// and any relevant information, assuming Quiet is false. +func (s *ServergRPC) OnStartupComplete() { + if Quiet { + return + } + + out := startUpZones(transport.GRPC+"://", s.Addr, s.zones) + if out != "" { + fmt.Print(out) + } +} + +// Stop stops the server. It blocks until the server is +// totally stopped. +func (s *ServergRPC) Stop() (err error) { + s.m.Lock() + defer s.m.Unlock() + if s.grpcServer != nil { + s.grpcServer.GracefulStop() + } + return +} + +// Query is the main entry-point into the gRPC server. From here we call ServeDNS like +// any normal server. We use a custom responseWriter to pick up the bytes we need to write +// 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) + if err != nil { + return nil, err + } + + p, ok := peer.FromContext(ctx) + if !ok { + return nil, errors.New("no peer in gRPC context") + } + + a, ok := p.Addr.(*net.TCPAddr) + if !ok { + return nil, fmt.Errorf("no TCP peer in gRPC context: %v", p.Addr) + } + + w := &gRPCresponse{localAddr: s.listenAddr, remoteAddr: a, Msg: msg} + + dnsCtx := context.WithValue(ctx, Key{}, s.Server) + dnsCtx = context.WithValue(dnsCtx, LoopKey{}, 0) + s.ServeDNS(dnsCtx, w, msg) + + packed, err := w.Msg.Pack() + if err != nil { + return nil, err + } + + return &pb.DnsPacket{Msg: packed}, nil +} + +// Shutdown stops the server (non gracefully). +func (s *ServergRPC) Shutdown() error { + if s.grpcServer != nil { + s.grpcServer.Stop() + } + return nil +} + +type gRPCresponse struct { + localAddr net.Addr + remoteAddr net.Addr + Msg *dns.Msg +} + +// Write is the hack that makes this work. It does not actually write the message +// but returns the bytes we need to write in r. We can then pick this up in Query +// and write a proper protobuf back to the client. +func (r *gRPCresponse) Write(b []byte) (int, error) { + r.Msg = new(dns.Msg) + return len(b), r.Msg.Unpack(b) +} + +// These methods implement the dns.ResponseWriter interface from Go DNS. +func (r *gRPCresponse) Close() error { return nil } +func (r *gRPCresponse) TsigStatus() error { return nil } +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) WriteMsg(m *dns.Msg) error { r.Msg = m; return nil } diff --git a/ag_201_coredns/core/dnsserver/server_https.go b/ag_201_coredns/core/dnsserver/server_https.go new file mode 100644 index 0000000..eda39c1 --- /dev/null +++ b/ag_201_coredns/core/dnsserver/server_https.go @@ -0,0 +1,209 @@ +package dnsserver + +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" +) + +// ServerHTTPS represents an instance of a DNS-over-HTTPS server. +type ServerHTTPS struct { + *Server + httpsServer *http.Server + listenAddr net.Addr + tlsConfig *tls.Config + 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 current processed HTTP request (if current processed request was done over DOH) +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) + 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 + } + } + + // http/2 is recommended when using DoH. We need to specify it in next protos + // or the upgrade won't happen. + 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 _, 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, + ErrorLog: stdlog.New(&loggerAdapter{}, "", 0), + } + sh := &ServerHTTPS{ + Server: s, tlsConfig: tlsConfig, httpsServer: srv, validRequest: validator, + } + sh.httpsServer.Handler = sh + + return sh, nil +} + +// Compile-time check to ensure Server implements the caddy.GracefulServer interface +var _ caddy.GracefulServer = &Server{} + +// Serve implements caddy.TCPServer interface. +func (s *ServerHTTPS) Serve(l net.Listener) error { + s.m.Lock() + s.listenAddr = l.Addr() + s.m.Unlock() + + if s.tlsConfig != nil { + l = tls.NewListener(l, s.tlsConfig) + } + return s.httpsServer.Serve(l) +} + +// ServePacket implements caddy.UDPServer interface. +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 + } + return l, nil +} + +// ListenPacket implements caddy.UDPServer interface. +func (s *ServerHTTPS) ListenPacket() (net.PacketConn, error) { return nil, nil } + +// OnStartupComplete lists the sites served by this server +// and any relevant information, assuming Quiet is false. +func (s *ServerHTTPS) OnStartupComplete() { + if Quiet { + return + } + + out := startUpZones(transport.HTTPS+"://", s.Addr, s.zones) + if out != "" { + fmt.Print(out) + } +} + +// Stop stops the server. It blocks until the server is totally stopped. +func (s *ServerHTTPS) Stop() error { + s.m.Lock() + defer s.m.Unlock() + if s.httpsServer != nil { + s.httpsServer.Shutdown(context.Background()) + } + return nil +} + +// 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 + } + + // Create a DoHWriter with the correct addresses in it. + h, p, _ := net.SplitHostPort(r.RemoteAddr) + port, _ := strconv.Atoi(p) + dw := &DoHWriter{ + laddr: s.listenAddr, + raddr: &net.TCPAddr{IP: net.ParseIP(h), Port: port}, + request: r, + } + + // 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) + 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. + // We are using code 500 to indicate an unexpected situation when the chain + // handler has not provided any response message. + if dw.Msg == nil { + http.Error(w, "No response", http.StatusInternalServerError) + s.countResponse(http.StatusInternalServerError) + return + } + + buf, _ := dw.Msg.Pack() + + mt, _ := response.Typify(dw.Msg, time.Now().UTC()) + 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("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 { + s.httpsServer.Shutdown(context.Background()) + } + return nil +} diff --git a/ag_201_coredns/core/dnsserver/server_https_test.go b/ag_201_coredns/core/dnsserver/server_https_test.go new file mode 100644 index 0000000..6663c10 --- /dev/null +++ b/ag_201_coredns/core/dnsserver/server_https_test.go @@ -0,0 +1,66 @@ +package dnsserver + +import ( + "bytes" + "crypto/tls" + "net/http" + "net/http/httptest" + "regexp" + "testing" + + "github.com/miekg/dns" +) + +var ( + validPath = regexp.MustCompile("^/(dns-query|(?P[0-9a-f]+))$") + validator = func(r *http.Request) bool { return validPath.MatchString(r.URL.Path) } +) + +func testServerHTTPS(t *testing.T, path string, validator func(*http.Request) bool) *http.Response { + c := Config{ + Zone: "example.com.", + Transport: "https", + TLSConfig: &tls.Config{}, + ListenHosts: []string{"127.0.0.1"}, + Port: "443", + HTTPRequestValidateFunc: validator, + } + s, err := NewServerHTTPS("127.0.0.1:443", []*Config{&c}) + if err != nil { + t.Log(err) + t.Fatal("could not create HTTPS server") + } + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeDNSKEY) + buf, err := m.Pack() + if err != nil { + t.Fatal(err) + } + + r := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(buf)) + w := httptest.NewRecorder() + s.ServeHTTP(w, r) + + return w.Result() +} + +func TestCustomHTTPRequestValidator(t *testing.T) { + testCases := map[string]struct { + path string + expected int + validator func(*http.Request) bool + }{ + "default": {"/dns-query", http.StatusOK, nil}, + "custom validator": {"/b10cada", http.StatusOK, validator}, + "no validator set": {"/adb10c", http.StatusNotFound, nil}, + "invalid path with validator": {"/helloworld", http.StatusNotFound, validator}, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + res := testServerHTTPS(t, tc.path, tc.validator) + if res.StatusCode != tc.expected { + t.Error("unexpected HTTP code", res.StatusCode) + } + }) + } +} diff --git a/ag_201_coredns/core/dnsserver/server_test.go b/ag_201_coredns/core/dnsserver/server_test.go new file mode 100644 index 0000000..c52b6a2 --- /dev/null +++ b/ag_201_coredns/core/dnsserver/server_test.go @@ -0,0 +1,117 @@ +package dnsserver + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +type testPlugin struct{} + +func (tp testPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + return 0, nil +} + +func (tp testPlugin) Name() string { return "testplugin" } + +func testConfig(transport string, p plugin.Handler) *Config { + c := &Config{ + Zone: "example.com.", + Transport: transport, + ListenHosts: []string{"127.0.0.1"}, + Port: "53", + Debug: false, + Stacktrace: false, + } + + c.AddPlugin(func(next plugin.Handler) plugin.Handler { return p }) + return c +} + +func TestNewServer(t *testing.T) { + _, err := NewServer("127.0.0.1:53", []*Config{testConfig("dns", testPlugin{})}) + if err != nil { + t.Errorf("Expected no error for NewServer, got %s", err) + } + + _, err = NewServergRPC("127.0.0.1:53", []*Config{testConfig("grpc", testPlugin{})}) + if err != nil { + t.Errorf("Expected no error for NewServergRPC, got %s", err) + } + + _, err = NewServerTLS("127.0.0.1:53", []*Config{testConfig("tls", testPlugin{})}) + if err != nil { + t.Errorf("Expected no error for NewServerTLS, got %s", err) + } +} + +func TestDebug(t *testing.T) { + configNoDebug, configDebug := testConfig("dns", testPlugin{}), testConfig("dns", testPlugin{}) + configDebug.Debug = true + + s1, err := NewServer("127.0.0.1:53", []*Config{configDebug, configNoDebug}) + if err != nil { + t.Errorf("Expected no error for NewServer, got %s", err) + } + if !s1.debug { + t.Errorf("Expected debug mode enabled for server s1") + } + if !log.D.Value() { + t.Errorf("Expected debug logging enabled") + } + + s2, err := NewServer("127.0.0.1:53", []*Config{configNoDebug}) + if err != nil { + t.Errorf("Expected no error for NewServer, got %s", err) + } + if s2.debug { + t.Errorf("Expected debug mode disabled for server s2") + } + if log.D.Value() { + t.Errorf("Expected debug logging disabled") + } +} + +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 BenchmarkCoreServeDNS(b *testing.B) { + s, err := NewServer("127.0.0.1:53", []*Config{testConfig("dns", testPlugin{})}) + if err != nil { + b.Errorf("Expected no error for NewServer, got %s", err) + } + + ctx := context.TODO() + w := &test.ResponseWriter{} + m := new(dns.Msg) + m.SetQuestion("aaa.example.com.", dns.TypeTXT) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + s.ServeDNS(ctx, w, m) + } +} diff --git a/ag_201_coredns/core/dnsserver/server_tls.go b/ag_201_coredns/core/dnsserver/server_tls.go new file mode 100644 index 0000000..6fff61d --- /dev/null +++ b/ag_201_coredns/core/dnsserver/server_tls.go @@ -0,0 +1,89 @@ +package dnsserver + +import ( + "context" + "crypto/tls" + "fmt" + "net" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/pkg/reuseport" + "github.com/coredns/coredns/plugin/pkg/transport" + + "github.com/miekg/dns" +) + +// ServerTLS represents an instance of a TLS-over-DNS-server. +type ServerTLS struct { + *Server + tlsConfig *tls.Config +} + +// NewServerTLS returns a new CoreDNS TLS server and compiles all plugin in to it. +func NewServerTLS(addr string, group []*Config) (*ServerTLS, 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 + } + } + + return &ServerTLS{Server: s, tlsConfig: tlsConfig}, nil +} + +// Compile-time check to ensure Server implements the caddy.GracefulServer interface +var _ caddy.GracefulServer = &Server{} + +// Serve implements caddy.TCPServer interface. +func (s *ServerTLS) Serve(l net.Listener) error { + s.m.Lock() + + if s.tlsConfig != nil { + l = tls.NewListener(l, s.tlsConfig) + } + + // 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.m.Unlock() + + return s.server[tcp].ActivateAndServe() +} + +// ServePacket implements caddy.UDPServer interface. +func (s *ServerTLS) ServePacket(p net.PacketConn) error { return nil } + +// Listen implements caddy.TCPServer interface. +func (s *ServerTLS) Listen() (net.Listener, error) { + l, err := reuseport.Listen("tcp", s.Addr[len(transport.TLS+"://"):]) + if err != nil { + return nil, err + } + return l, nil +} + +// ListenPacket implements caddy.UDPServer interface. +func (s *ServerTLS) ListenPacket() (net.PacketConn, error) { return nil, nil } + +// OnStartupComplete lists the sites served by this server +// and any relevant information, assuming Quiet is false. +func (s *ServerTLS) OnStartupComplete() { + if Quiet { + return + } + + out := startUpZones(transport.TLS+"://", s.Addr, s.zones) + if out != "" { + fmt.Print(out) + } +} diff --git a/ag_201_coredns/core/dnsserver/view.go b/ag_201_coredns/core/dnsserver/view.go new file mode 100644 index 0000000..ac79783 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/core/dnsserver/zdirectives.go b/ag_201_coredns/core/dnsserver/zdirectives.go new file mode 100644 index 0000000..a1cd304 --- /dev/null +++ b/ag_201_coredns/core/dnsserver/zdirectives.go @@ -0,0 +1,65 @@ +// generated by directives_generate.go; DO NOT EDIT + +package dnsserver + +// Directives are registered in the order they should be +// executed. +// +// Ordering is VERY important. Every plugin will +// feel the effects of all other plugin below +// (after) them during a request, but they must not +// care what plugin above them are doing. +var Directives = []string{ + "metadata", + "geoip", + "cancel", + "tls", + "reload", + "nsid", + "bufsize", + "root", + "bind", + "debug", + "trace", + "ready", + "health", + "pprof", + "prometheus", + "errors", + "log", + "dnstap", + "local", + "dns64", + "acl", + "any", + "chaos", + "loadbalance", + "tsig", + "cache", + "rewrite", + "header", + "dnssec", + "autopath", + "minimal", + "template", + "transfer", + "hosts", + "route53", + "azure", + "clouddns", + "k8s_external", + "kubernetes", + "file", + "auto", + "secondary", + "etcd", + "loop", + "forward", + "grpc", + "erratic", + "whoami", + "on", + "sign", + "view", + "dnsovertor", +} diff --git a/ag_201_coredns/core/plugin/zplugin.go b/ag_201_coredns/core/plugin/zplugin.go new file mode 100644 index 0000000..3b2202d --- /dev/null +++ b/ag_201_coredns/core/plugin/zplugin.go @@ -0,0 +1,59 @@ +// generated by directives_generate.go; DO NOT EDIT + +package plugin + +import ( + // Include all plugins. + _ "github.com/coredns/caddy/onevent" + _ "github.com/coredns/coredns/plugin/acl" + _ "github.com/coredns/coredns/plugin/any" + _ "github.com/coredns/coredns/plugin/auto" + _ "github.com/coredns/coredns/plugin/autopath" + _ "github.com/coredns/coredns/plugin/azure" + _ "github.com/coredns/coredns/plugin/bind" + _ "github.com/coredns/coredns/plugin/bufsize" + _ "github.com/coredns/coredns/plugin/cache" + _ "github.com/coredns/coredns/plugin/cancel" + _ "github.com/coredns/coredns/plugin/chaos" + _ "github.com/coredns/coredns/plugin/clouddns" + _ "github.com/coredns/coredns/plugin/debug" + _ "github.com/coredns/coredns/plugin/dns64" + _ "github.com/coredns/coredns/plugin/dnsovertor" + _ "github.com/coredns/coredns/plugin/dnssec" + _ "github.com/coredns/coredns/plugin/dnstap" + _ "github.com/coredns/coredns/plugin/erratic" + _ "github.com/coredns/coredns/plugin/errors" + _ "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" + _ "github.com/coredns/coredns/plugin/kubernetes" + _ "github.com/coredns/coredns/plugin/loadbalance" + _ "github.com/coredns/coredns/plugin/local" + _ "github.com/coredns/coredns/plugin/log" + _ "github.com/coredns/coredns/plugin/loop" + _ "github.com/coredns/coredns/plugin/metadata" + _ "github.com/coredns/coredns/plugin/metrics" + _ "github.com/coredns/coredns/plugin/minimal" + _ "github.com/coredns/coredns/plugin/nsid" + _ "github.com/coredns/coredns/plugin/pprof" + _ "github.com/coredns/coredns/plugin/ready" + _ "github.com/coredns/coredns/plugin/reload" + _ "github.com/coredns/coredns/plugin/rewrite" + _ "github.com/coredns/coredns/plugin/root" + _ "github.com/coredns/coredns/plugin/route53" + _ "github.com/coredns/coredns/plugin/secondary" + _ "github.com/coredns/coredns/plugin/sign" + _ "github.com/coredns/coredns/plugin/template" + _ "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/ag_201_coredns/coredns.1.md b/ag_201_coredns/coredns.1.md new file mode 100644 index 0000000..64daaca --- /dev/null +++ b/ag_201_coredns/coredns.1.md @@ -0,0 +1,52 @@ +## CoreDNS + +*coredns* - pluggable DNS nameserver optimized for service discovery and flexibility. + +## Synopsis + +*coredns* **[-conf FILE]** **[-dns.port PORT}** **[OPTION]**... + +## Description + +CoreDNS is a DNS server that chains plugins. Each plugin handles a DNS feature, like rewriting +queries, kubernetes service discovery or just exporting metrics. There are many other plugins, +each described on and their respective manual pages. Plugins not +bundled by default in CoreDNS are listed on . + +When started without options CoreDNS will look for a file named `Corefile` in the current +directory, if found, it will parse its contents and start up accordingly. If no `Corefile` is found +it will start with the *whoami* plugin (coredns-whoami(7)) and start listening on port 53 (unless +overridden with `-dns.port`). + +Available options: + +**-conf** **FILE** +: specify Corefile to load, if not given CoreDNS will look for a `Corefile` in the current + directory. + +**-dns.port** **PORT** or **-p** **PORT** +: override default port (53) to listen on. + +**-pidfile** **FILE** +: write PID to **FILE**. + +**-plugins** +: list all plugins and quit. + +**-quiet** +: don't print any version and port information on startup. + +**-version** +: show version and quit. + +## Authors + +CoreDNS Authors. + +## Copyright + +Apache License 2.0 + +## See Also + +Corefile(5) @@PLUGINS@@. diff --git a/ag_201_coredns/coredns.go b/ag_201_coredns/coredns.go new file mode 100644 index 0000000..3d0edaa --- /dev/null +++ b/ag_201_coredns/coredns.go @@ -0,0 +1,13 @@ +package main + +//go:generate go run directives_generate.go +//go:generate go run owners_generate.go + +import ( + _ "github.com/coredns/coredns/core/plugin" // Plug in CoreDNS. + "github.com/coredns/coredns/coremain" +) + +func main() { + coremain.Run() +} diff --git a/ag_201_coredns/corefile.5.md b/ag_201_coredns/corefile.5.md new file mode 100644 index 0000000..9cd8e92 --- /dev/null +++ b/ag_201_coredns/corefile.5.md @@ -0,0 +1,140 @@ +## Name + +*corefile* - configuration file for CoreDNS. + +## Description + +A *corefile* specifies the internal servers CoreDNS should run and what plugins each of these +should chain. The syntax is as follows: + +~~~ txt +[SCHEME://]ZONE [[SCHEME://]ZONE]...[:PORT] { + [PLUGIN]... +} +~~~ + +The **ZONE** defines for which name this server should be called, multiple zones are allowed and +should be *white space* separated. You can use a "reverse" syntax to specify a reverse zone (i.e. +ip6.arpa and in-addr.arpa), by using an IP address in the CIDR notation. + +The optional **SCHEME** defaults to `dns://`, but can also be `tls://` (DNS over TLS), `grpc://` +(DNS over gRPC) or `https://` (DNS over HTTP/2). + +The optional **PORT** controls on which port the server will bind, this default to 53. If you use +a port number here, you *can't* override it with `-dns.port` (coredns(1)), also see coredns-bind(7). + +Specifying a **ZONE** *and* **PORT** combination multiple times for *different* servers will lead to +an error on startup. + +When a query comes in, it is matched again all zones for all servers, the server with the longest +match on the query name will receive the query. + +**PLUGIN** defines the plugin(s) we want to load into this server. This is optional as well, but as +server with no plugins will just return SERVFAIL for all queries. Each plugin can have a number of +properties than can have arguments, see the documentation for each plugin. + +Comments are allowed and begin with an unquoted hash `#` and continue to the end of the line. +Comments may be started anywhere on a line. + +Environment variables are supported and either the Unix or Windows form may be used: `{$ENV_VAR_1}` +or `{%ENV_VAR_2%}`. + +You can use the `import` "plugin" (See coredns-import(7)) to include parts of other files. + +If CoreDNS can’t find a Corefile to load it loads the following builtin one: + +~~~ corefile +. { + whoami + log +} +~~~ + +## Import + +You can use the `import` "plugin" to include parts of other files, see +, and coredns-import(7). + +## Snippets + +If you want to reuse a snippet you can define one with and then use it with *import*. + +~~~ corefile +(mysnippet) { + log + whoami +} + +. { + import mysnippet +} +~~~ + +## Examples + +The **ZONE** is root zone `.`, the **PLUGIN** is *chaos*. The *chaos* plugin takes an (optional) argument: +`CoreDNS-001`. This text is returned on a CH class query: `dig CH TXT version.bind @localhost`. + +~~~ corefile +. { + chaos CoreDNS-001 +} +~~~ + +When defining a new zone, you either create a new server, or add it to an existing one. Here we +define one server that handles two zones; that potentially chain different plugins: + +~~~ corefile +example.org { + whoami +} +org { + whoami +} +~~~ + +Is identical to: + +~~~ corefile +example.org org { + whoami +} +~~~ + +Reverse zones can be specified as domain names: + +~~~ corefile +0.0.10.in-addr.arpa { + whoami +} +~~~ + +or by just using the CIDR notation: + +~~~ corefile +10.0.0.0/24 { + whoami +} +~~~ + +This also works on a non octet boundary: + +~~~ corefile +10.0.0.0/27 { + whoami +} +~~~ + +## Authors + +CoreDNS Authors. + +## Copyright + +Apache License 2.0 + +## See Also + +The manual page for CoreDNS: coredns(1) and more documentation on . +Also see the [*import*](https://coredns.io/plugins/import)'s documentation and all the manual pages +for the plugins. diff --git a/ag_201_coredns/coremain/run.go b/ag_201_coredns/coremain/run.go new file mode 100644 index 0000000..fa76578 --- /dev/null +++ b/ag_201_coredns/coremain/run.go @@ -0,0 +1,184 @@ +// Package coremain contains the functions for starting CoreDNS. +package coremain + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" +) + +func init() { + caddy.DefaultConfigFile = "Corefile" + caddy.Quiet = true // don't show init stuff from caddy + setVersion() + + flag.StringVar(&conf, "conf", "", "Corefile to load (default \""+caddy.DefaultConfigFile+"\")") + flag.BoolVar(&plugins, "plugins", false, "List installed plugins") + flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file") + flag.BoolVar(&version, "version", false, "Show version") + flag.BoolVar(&dnsserver.Quiet, "quiet", false, "Quiet mode (no initialization output)") + + caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader)) + caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader)) + + caddy.AppName = coreName + caddy.AppVersion = CoreVersion +} + +// Run is CoreDNS's main() function. +func Run() { + caddy.TrapSignals() + flag.Parse() + + if len(flag.Args()) > 0 { + mustLogFatal(fmt.Errorf("extra command line arguments: %s", flag.Args())) + } + + log.SetOutput(os.Stdout) + log.SetFlags(0) // Set to 0 because we're doing our own time, with timezone + + if version { + showVersion() + os.Exit(0) + } + if plugins { + fmt.Println(caddy.DescribePlugins()) + os.Exit(0) + } + + // Get Corefile input + corefile, err := caddy.LoadCaddyfile(serverType) + if err != nil { + mustLogFatal(err) + } + + // Start your engines + instance, err := caddy.Start(corefile) + if err != nil { + mustLogFatal(err) + } + + if !dnsserver.Quiet { + showVersion() + } + + // Twiddle your thumbs + instance.Wait() +} + +// mustLogFatal wraps log.Fatal() in a way that ensures the +// output is always printed to stderr so the user can see it +// if the user is still there, even if the process log was not +// 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{}) { + if !caddy.IsUpgrade() { + log.SetOutput(os.Stderr) + } + log.Fatal(args...) +} + +// confLoader loads the Caddyfile using the -conf flag. +func confLoader(serverType string) (caddy.Input, error) { + if conf == "" { + return nil, nil + } + + if conf == "stdin" { + return caddy.CaddyfileFromPipe(os.Stdin, serverType) + } + + contents, err := os.ReadFile(filepath.Clean(conf)) + if err != nil { + return nil, err + } + return caddy.CaddyfileInput{ + Contents: contents, + Filepath: conf, + ServerTypeName: serverType, + }, nil +} + +// defaultLoader loads the Corefile from the current working directory. +func defaultLoader(serverType string) (caddy.Input, error) { + contents, err := os.ReadFile(caddy.DefaultConfigFile) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + return caddy.CaddyfileInput{ + Contents: contents, + Filepath: caddy.DefaultConfigFile, + ServerTypeName: serverType, + }, nil +} + +// showVersion prints the version that is starting. +func showVersion() { + fmt.Print(versionString()) + fmt.Print(releaseString()) + if devBuild && gitShortStat != "" { + fmt.Printf("%s\n%s\n", gitShortStat, gitFilesModified) + } +} + +// versionString returns the CoreDNS version as a string. +func versionString() string { + return fmt.Sprintf("%s-%s\n", caddy.AppName, caddy.AppVersion) +} + +// releaseString returns the release information related to CoreDNS version: +// /, , +// e.g., +// linux/amd64, go1.8.3, a6d2d7b5 +func releaseString() string { + return fmt.Sprintf("%s/%s, %s, %s\n", runtime.GOOS, runtime.GOARCH, runtime.Version(), GitCommit) +} + +// setVersion figures out the version information +// based on variables set by -ldflags. +func setVersion() { + // A development build is one that's not at a tag or has uncommitted changes + devBuild = gitTag == "" || gitShortStat != "" + + // Only set the appVersion if -ldflags was used + if gitNearestTag != "" || gitTag != "" { + if devBuild && gitNearestTag != "" { + appVersion = fmt.Sprintf("%s (+%s %s)", strings.TrimPrefix(gitNearestTag, "v"), GitCommit, buildDate) + } else if gitTag != "" { + appVersion = strings.TrimPrefix(gitTag, "v") + } + } +} + +// Flags that control program flow or startup +var ( + conf string + version bool + plugins bool +) + +// Build information obtained with the help of -ldflags +var ( + appVersion = "(untracked dev build)" // inferred at startup + devBuild = true // inferred at startup + + buildDate string // date -u + gitTag string // git describe --exact-match HEAD 2> /dev/null + gitNearestTag string // git describe --abbrev=0 --tags HEAD + gitShortStat string // git diff-index --shortstat + gitFilesModified string // git diff-index --name-only HEAD + + // Gitcommit contains the commit where we built CoreDNS from. + GitCommit string +) diff --git a/ag_201_coredns/coremain/version.go b/ag_201_coredns/coremain/version.go new file mode 100644 index 0000000..7578079 --- /dev/null +++ b/ag_201_coredns/coremain/version.go @@ -0,0 +1,8 @@ +package coremain + +// Various CoreDNS constants. +const ( + CoreVersion = "1.10.0" + coreName = "CoreDNS" + serverType = "dns" +) diff --git a/ag_201_coredns/directives_generate.go b/ag_201_coredns/directives_generate.go new file mode 100644 index 0000000..ccfd43e --- /dev/null +++ b/ag_201_coredns/directives_generate.go @@ -0,0 +1,114 @@ +//go:build ignore + +package main + +import ( + "bufio" + "go/format" + "log" + "os" + "strings" +) + +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, ":") + if len(items) != 2 { + // ignore empty lines + continue + } + name, repo := items[0], items[1] + + if _, ok := mi[name]; ok { + log.Fatalf("Duplicate entry %q", name) + } + + md = append(md, name) + mi[name] = pluginPath + repo // Default, unless overridden by 3rd arg + + if _, err := os.Stat(pluginFSPath + repo); err != nil { // External package has been given + mi[name] = repo + } + } + + genImports("core/plugin/zplugin.go", "plugin", mi) + genDirectives("core/dnsserver/zdirectives.go", "dnsserver", md) +} + +func genImports(file, pack string, mi map[string]string) { + outs := header + "package " + pack + "\n\n" + "import (" + + if len(mi) > 0 { + outs += "\n" + } + + outs += "// Include all plugins.\n" + for _, v := range mi { + outs += `_ "` + v + `"` + "\n" + } + outs += ")\n" + + if err := formatAndWrite(file, outs); err != nil { + log.Fatalf("Failed to format and write: %q", err) + } +} + +func genDirectives(file, pack string, md []string) { + + outs := header + "package " + pack + "\n\n" + outs += ` +// Directives are registered in the order they should be +// executed. +// +// Ordering is VERY important. Every plugin will +// feel the effects of all other plugin below +// (after) them during a request, but they must not +// care what plugin above them are doing. +var Directives = []string{ +` + + for i := range md { + outs += `"` + md[i] + `",` + "\n" + } + + outs += "}\n" + + if err := formatAndWrite(file, outs); err != nil { + log.Fatalf("Failed to format and write: %q", err) + } +} + +func formatAndWrite(file string, data string) error { + res, err := format.Source([]byte(data)) + if err != nil { + return err + } + + if err = os.WriteFile(file, res, 0644); err != nil { + return err + } + return nil +} + +const ( + pluginPath = "github.com/coredns/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/ag_201_coredns/go.mod b/ag_201_coredns/go.mod new file mode 100644 index 0000000..c2c3689 --- /dev/null +++ b/ag_201_coredns/go.mod @@ -0,0 +1,123 @@ +module github.com/coredns/coredns + +go 1.17 + +require ( + github.com/Azure/azure-sdk-for-go v66.0.0+incompatible + github.com/Azure/go-autorest/autorest v0.11.28 + github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 + github.com/antonmedv/expr v1.9.0 + github.com/apparentlymart/go-cidr v1.1.0 + github.com/aws/aws-sdk-go v1.44.95 + github.com/coredns/caddy v1.1.1 + github.com/dnstap/golang-dnstap v0.4.0 + github.com/farsightsec/golang-framestream v0.3.0 + github.com/go-logr/logr v1.2.3 + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 + github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9 + github.com/matttproud/golang_protobuf_extensions v1.0.1 + github.com/miekg/dns v1.1.50 + github.com/opentracing/opentracing-go v1.2.0 + github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 + github.com/openzipkin/zipkin-go v0.4.0 + github.com/oschwald/geoip2-golang v1.8.0 + github.com/prometheus/client_golang v1.13.0 + github.com/prometheus/client_model v0.2.0 + github.com/prometheus/common v0.37.0 + github.com/stretchr/testify v1.8.0 + go.etcd.io/etcd/api/v3 v3.5.4 + go.etcd.io/etcd/client/v3 v3.5.4 + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa + golang.org/x/net v0.0.0-20220722155237-a158d28d115b + golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 + google.golang.org/api v0.95.0 + google.golang.org/grpc v1.49.0 + google.golang.org/protobuf v1.28.1 + gopkg.in/DataDog/dd-trace-go.v1 v1.41.0 + k8s.io/api v0.25.0 + k8s.io/apimachinery v0.25.0 + k8s.io/client-go v0.24.4 + k8s.io/klog/v2 v2.80.1 +) + +require ( + cloud.google.com/go/compute v1.7.0 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // 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/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 // indirect + github.com/DataDog/datadog-go v4.8.2+incompatible // indirect + github.com/DataDog/datadog-go/v5 v5.0.2 // indirect + github.com/DataDog/sketches-go v1.2.1 // indirect + github.com/Microsoft/go-winio v0.5.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/swag v0.19.14 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.2.0 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect + github.com/googleapis/gax-go/v2 v2.4.0 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 // indirect + github.com/oschwald/maxminddb-golang v1.10.0 // indirect + github.com/philhofer/fwd v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/tinylib/msgp v1.1.2 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect + go.opencensus.io v0.23.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + golang.org/x/tools v0.1.12 // indirect + golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect + k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect +) diff --git a/ag_201_coredns/go.sum b/ag_201_coredns/go.sum new file mode 100644 index 0000000..65206fc --- /dev/null +++ b/ag_201_coredns/go.sum @@ -0,0 +1,1630 @@ +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.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= +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/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0 h1:DAq3r8y4mDgyB/ZPJ9v/5VJNqjgJAxTn6ZYLlUywOu8= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +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/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +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/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +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/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.4.0/go.mod h1:LFrqilwgdw4X2cJS9ALgzYmMu+ULyrUN6IHV3CPK4TM= +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= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/99designs/gqlgen v0.14.0/go.mod h1:S7z4boV+Nx4VvzMUpVrY/YuHjFX4n7rDyuTqvAkuoRE= +github.com/Azure/azure-sdk-for-go v66.0.0+incompatible h1:bmmC38SlE8/E81nNADlgmVGurPWMHDX2YNXVQMrBpEE= +github.com/Azure/azure-sdk-for-go v66.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.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM= +github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +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.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +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.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +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.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +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/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 h1:3nVO1nQyh64IUY6BPZUpMYMZ738Pu+LsMt3E0eqqIYw= +github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583/go.mod h1:EP9f4GqaDJyP1F5jTNMtzdIpw3JpNs3rMSJOnYywCiw= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/datadog-go v4.8.2+incompatible h1:qbcKSx29aBLD+5QLvlQZlGmRMF/FfGqFLFev/1TDzRo= +github.com/DataDog/datadog-go v4.8.2+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/datadog-go/v5 v5.0.2 h1:UFtEe7662/Qojxkw1d6SboAeA0CPI3naKhVASwFn+04= +github.com/DataDog/datadog-go/v5 v5.0.2/go.mod h1:ZI9JFB4ewXbw1sBnF4sxsR2k1H3xjV+PUAOUsHvKpcU= +github.com/DataDog/gostackparse v0.5.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= +github.com/DataDog/sketches-go v1.2.1 h1:qTBzWLnZ3kM2kw39ymh6rMcnN+5VULwFs++lEYUUsro= +github.com/DataDog/sketches-go v1.2.1/go.mod h1:1xYmPLY1So10AwxV6MJV0J53XVH+WL9Ad1KetxVivVI= +github.com/DataDog/zstd v1.3.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= +github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +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/sarama v1.22.0/go.mod h1:lm3THZ8reqBDBQKQyb5HB3sY1lKp3grEbQ81aWSgPp4= +github.com/Shopify/sarama v1.30.0/go.mod h1:zujlQQx1kzHsh4jfV1USnptCQrHAEZ2Hk8fTKCulPVs= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +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/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU= +github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +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/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +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-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= +github.com/aws/aws-sdk-go v1.44.95 h1:QwmA+PeR6v4pF0f/dPHVPWGAshAhb9TnGZBTM5uKuI8= +github.com/aws/aws-sdk-go v1.44.95/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go-v2 v1.0.0/go.mod h1:smfAbmpW+tcRVuNUjo3MOArSZmW72t62rkCzc2i0TWM= +github.com/aws/aws-sdk-go-v2/config v1.0.0/go.mod h1:WysE/OpUgE37tjtmtJd8GXgT8s1euilE5XtUkRNUQ1w= +github.com/aws/aws-sdk-go-v2/credentials v1.0.0/go.mod h1:/SvsiqBf509hG4Bddigr3NB12MIpfHhZapyBurJe8aY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.0/go.mod h1:wpMHDCXvOXZxGCRSidyepa8uJHY4vaBGfY2/+oKU/Bc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.0/go.mod h1:3jExOmpbjgPnz2FJaMOfbSk1heTkZ66aD3yNtVhnjvI= +github.com/aws/aws-sdk-go-v2/service/sqs v1.0.0/go.mod h1:w5BclCU8ptTbagzXS/fHBr+vAyXUjggg/72qDIURKMk= +github.com/aws/aws-sdk-go-v2/service/sts v1.0.0/go.mod h1:5f+cELGATgill5Pu3/vK3Ebuigstc+qYEHW5MvGWZO4= +github.com/aws/smithy-go v1.0.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= +github.com/aws/smithy-go v1.11.0/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= +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/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/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/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/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +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/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/confluentinc/confluent-kafka-go v1.4.0/go.mod h1:u2zNLny2xq+5rWeTQjFHbDzzNuba4P1vo31r9r4uAdg= +github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0= +github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= +github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +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 v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-resiliency v1.2.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/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= +github.com/elastic/go-elasticsearch/v7 v7.17.1/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +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/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +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/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +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/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +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/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/garyburd/redigo v1.6.3/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +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/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-chi/chi v1.5.0/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +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/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8= +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-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-pg/pg/v10 v10.0.0/go.mod h1:XHU1AkQW534GFuUdSiQ46+Xw6Ah+9+b8DlT4YwhiXL8= +github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v7 v7.1.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/go-redis/redis/v8 v8.0.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gocql/gocql v0.0.0-20220224095938-0eacd3183625/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofiber/fiber/v2 v2.11.0/go.mod h1:oZTLWqYnqpMMuF922SjGbsYZsdpE1MCfh416HNdweIM= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +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.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +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 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +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/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/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/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.7.0/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +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.4.1/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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +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/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210423192551-a2663126120b/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +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/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/consul/api v1.0.0/go.mod h1:mbFwfRxOTDHZpT3iUsMAFcLNoVm6Xbe1xZ6KiSm8FY0= +github.com/hashicorp/consul/internal v0.1.0/go.mod h1:zi9bMZYbiPHyAjgBWo7kCUcy5l2NrTdrkVupCc7Oo6c= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= +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-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +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-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.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/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.1.6/go.mod h1:5VDNHjqFMgEcclnwmkCnC99IPwxBmIsxwY8qn+Nl0H4= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/serf v0.8.6/go.mod h1:P/AVgr4UHsUYqVHG1y9eFhz8S35pqhGhLZaDpfGKIMo= +github.com/hashicorp/vault/api v1.1.0/go.mod h1:R3Umvhlxi2TN7Ex2hzOowyeNb+SfbVWI973N+ctaFMk= +github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +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/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= +github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.6.4/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= +github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= +github.com/jackc/pgtype v1.4.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= +github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= +github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= +github.com/jackc/pgx/v4 v4.8.1/go.mod h1:4HOLxrl8wToZJReD04/yB20GDwf4KBYETvlHciCnwW0= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/gorm v1.9.10/go.mod h1:Kh6hTsSGffh4ui079FHrR5Gg+5D0hgihqDcsDN2BBJY= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +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/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +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/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/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.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +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/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +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/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= +github.com/labstack/echo/v4 v4.2.0/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +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/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +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/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +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 v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +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 v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +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-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +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/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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +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.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +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/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +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.4.0 h1:CtfRrOVZtbDj8rt1WXjklw0kqqJQwICrCKmlfUuBUUw= +github.com/openzipkin/zipkin-go v0.4.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ= +github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= +github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= +github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= +github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= +github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +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/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +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.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= +github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= +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.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +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.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/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.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/rabbitmq/amqp091-go v1.1.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +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/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/segmentio/kafka-go v0.4.29/go.mod h1:m1lXeqJtIFYZayv0shM/tjrAFljvWLTprxBHd+3PnaU= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/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/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +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/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.7.3/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tidwall/btree v0.3.0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= +github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4= +github.com/tidwall/buntdb v1.2.0/go.mod h1:XLza/dhlwzO6dc5o/KWor4kfZSt3BP8QV+77ZMKfI58= +github.com/tidwall/gjson v1.6.7/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= +github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/grect v0.1.0/go.mod h1:sa5O42oP6jWfTShL9ka6Sgmg3TgIK649veZe05B7+J8= +github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= +github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= +github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= +github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= +github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/twitchtv/twirp v8.1.1+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.26.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA= +github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= +github.com/vektah/gqlparser/v2 v2.2.0/go.mod h1:i3mQIGIrbK2PD1RrCeMTlVbkF2FJ6WkU1KJlJlC+3F4= +github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= +github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI= +github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.opencensus.io v0.20.1/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/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/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/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +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/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +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.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +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-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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/exp v0.0.0-20200901203048-c4f52b2c50aa/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= +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/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/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/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/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/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.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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-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/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-20190628185345-da137c7871d7/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-20191004110552-13f9640d40b9/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-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/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-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 h1:2o1E+E8TpNLklK9nHiPiK1uzIYrIHt+cQx3ynCwq9V8= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +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/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-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/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190403152447-81d4e9dc473e/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-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/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-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/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-20200116001909-b77594299b42/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-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/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-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/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-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 h1:9vYwv7OjYaky/tlAeD7C4oC9EsPTlaFl1H2jS++V+ME= +golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/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-20190125232054-d66bd3c5d5a6/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-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-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-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/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-20200227222343-706bc42d1f0d/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-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200527183253-8e7acdbce89d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +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.19.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.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.25.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.95.0 h1:d1c24AAS01DYqXreBeuVV7ewY/U8Mnhh47pwtsgVtYg= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +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/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/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-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-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-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200528110217-3d3490e7e671/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200726014623-da3ae01ef02d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f h1:hJ/Y5SqPXbarffmAsApliUlcvMU+wScNGfyop4bZm8o= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +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.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +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.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/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/DataDog/dd-trace-go.v1 v1.41.0 h1:tD0e/cQGXSoUBbkOod2LWYCwG3gqShrWItViLcRt9Yw= +gopkg.in/DataDog/dd-trace-go.v1 v1.41.0/go.mod h1:CfhMxr9rU1IDdSNRjeLKhbNcZM6b8kRxOAKSvrG/GiI= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/jinzhu/gorm.v1 v1.9.1/go.mod h1:56JJPUzbikvTVnoyP1nppSkbJ2L8sunqTBDY2fDrmFg= +gopkg.in/olivere/elastic.v3 v3.0.75/go.mod h1:yDEuSnrM51Pc8dM5ov7U8aI/ToR3PG0llA8aRv2qmw0= +gopkg.in/olivere/elastic.v5 v5.0.84/go.mod h1:LXF6q9XNBxpMqrcgax95C6xyARXWbbCXUrtTxrNrxJI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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.3/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw= +gorm.io/driver/postgres v1.0.0/go.mod h1:wtMFcOzmuA5QigNsgEIb7O5lhvH1tHAF1RbWmLWV4to= +gorm.io/driver/sqlserver v1.0.4/go.mod h1:ciEo5btfITTBCj9BkoUVDvgQbUdLWQNqdFY5OGuGnRg= +gorm.io/gorm v1.9.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.6/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +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/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU= +inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= +k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= +k8s.io/api v0.24.4/go.mod h1:42pVfA0NRxrtJhZQOvRSyZcJihzAdU59WBtTjYcB0/M= +k8s.io/api v0.25.0 h1:H+Q4ma2U/ww0iGB78ijZx6DRByPz6/733jIuFpX70e0= +k8s.io/api v0.25.0/go.mod h1:ttceV1GyV1i1rnmvzT3BST08N6nGt+dudGrquzVQWPk= +k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.24.4/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= +k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU= +k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0= +k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= +k8s.io/client-go v0.24.4 h1:hIAIJZIPyaw46AkxwyR0FRfM/pRxpUNTd3ysYu9vyRg= +k8s.io/client-go v0.24.4/go.mod h1:+AxlPWw/H6f+EJhRSjIeALaJT4tbeB/8g9BNvXGPd0Y= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +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.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ= +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/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +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= +sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= diff --git a/ag_201_coredns/man/coredns-acl.7 b/ag_201_coredns/man/coredns-acl.7 new file mode 100644 index 0000000..f03731c --- /dev/null +++ b/ag_201_coredns/man/coredns-acl.7 @@ -0,0 +1,135 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-ACL" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIacl\fP - enforces access control policies on source ip and prevents unauthorized access to DNS servers. + +.SH "DESCRIPTION" +.PP +With \fB\fCacl\fR 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. + +.PP +This plugin can be used multiple times per Server Block. + +.SH "SYNTAX" +.PP +.RS + +.nf +acl [ZONES...] { + ACTION [type QTYPE...] [net SOURCE...] +} + +.fi +.RE + +.IP \(bu 4 +\fBZONES\fP zones it should be authoritative for. If empty, the zones from the configuration block are used. +.IP \(bu 4 +\fBACTION\fP (\fIallow\fP, \fIblock\fP, or \fIfilter\fP) defines the way to deal with DNS queries matched by this rule. The default action is \fIallow\fP, which means a DNS query not matched by any rules will be allowed to recurse. The difference between \fIblock\fP and \fIfilter\fP is that block returns status code of \fIREFUSED\fP while filter returns an empty set \fINOERROR\fP +.IP \(bu 4 +\fBQTYPE\fP is the query type to match for the requests to be allowed or blocked. Common resource record types are supported. \fB\fC*\fR stands for all record types. The default behavior for an omitted \fB\fCtype QTYPE...\fR is to match all kinds of DNS queries (same as \fB\fCtype *\fR). +.IP \(bu 4 +\fBSOURCE\fP is the source IP address to match for the requests to be allowed or blocked. Typical CIDR notation and single IP address are supported. \fB\fC*\fR stands for all possible source IP addresses. + + +.SH "EXAMPLES" +.PP +To demonstrate the usage of plugin acl, here we provide some typical examples. + +.PP +Block all DNS queries with record type A from 192.168.0.0/16: + +.PP +.RS + +.nf +\&. { + acl { + block type A net 192.168.0.0/16 + } +} + +.fi +.RE + +.PP +Filter all DNS queries with record type A from 192.168.0.0/16: + +.PP +.RS + +.nf +\&. { + acl { + filter type A net 192.168.0.0/16 + } +} + +.fi +.RE + +.PP +Block all DNS queries from 192.168.0.0/16 except for 192.168.1.0/24: + +.PP +.RS + +.nf +\&. { + acl { + allow net 192.168.1.0/24 + block net 192.168.0.0/16 + } +} + +.fi +.RE + +.PP +Allow only DNS queries from 192.168.0.0/24 and 192.168.1.0/24: + +.PP +.RS + +.nf +\&. { + acl { + allow net 192.168.0.0/24 192.168.1.0/24 + block + } +} + +.fi +.RE + +.PP +Block all DNS queries from 192.168.1.0/24 towards a.example.org: + +.PP +.RS + +.nf +example.org { + acl a.example.org { + block net 192.168.1.0/24 + } +} + +.fi +.RE + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metrics are exported: + +.IP \(bu 4 +\fB\fCcoredns_acl_blocked_requests_total{server, zone}\fR - counter of DNS requests being blocked. +.IP \(bu 4 +\fB\fCcoredns_acl_allowed_requests_total{server}\fR - counter of DNS requests being allowed. + + +.PP +The \fB\fCserver\fR and \fB\fCzone\fR labels are explained in the \fImetrics\fP plugin documentation. + diff --git a/ag_201_coredns/man/coredns-any.7 b/ag_201_coredns/man/coredns-any.7 new file mode 100644 index 0000000..c17b34c --- /dev/null +++ b/ag_201_coredns/man/coredns-any.7 @@ -0,0 +1,53 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-ANY" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIany\fP - gives a minimal response to ANY queries. + +.SH "DESCRIPTION" +.PP +\fIany\fP basically blocks ANY queries by responding to them with a short HINFO reply. See RFC +8482 +\[la]https://tools.ietf.org/html/rfc8482\[ra] for details. + +.SH "SYNTAX" +.PP +.RS + +.nf +any + +.fi +.RE + +.SH "EXAMPLES" +.PP +.RS + +.nf +example.org { + whoami + any +} + +.fi +.RE + +.PP +A \fB\fCdig +nocmd ANY example.org +noall +answer\fR now returns: + +.PP +.RS + +.nf +example.org. 8482 IN HINFO "ANY obsoleted" "See RFC 8482" + +.fi +.RE + +.SH "SEE ALSO" +.PP +RFC 8482 +\[la]https://tools.ietf.org/html/rfc8482\[ra]. + diff --git a/ag_201_coredns/man/coredns-auto.7 b/ag_201_coredns/man/coredns-auto.7 new file mode 100644 index 0000000..2362129 --- /dev/null +++ b/ag_201_coredns/man/coredns-auto.7 @@ -0,0 +1,112 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-AUTO" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIauto\fP - enables serving zone data from an RFC 1035-style master file, which is automatically picked up from disk. + +.SH "DESCRIPTION" +.PP +The \fIauto\fP plugin is used for an "old-style" DNS server. It serves from a preloaded file that exists +on disk. If the zone file contains signatures (i.e. is signed, i.e. using DNSSEC) correct DNSSEC answers +are returned. Only NSEC is supported! If you use this setup \fIyou\fP are responsible for re-signing the +zonefile. New or changed zones are automatically picked up from disk only when SOA's serial changes. If the zones are not updated via a zone transfer, the serial must be manually changed. + +.SH "SYNTAX" +.PP +.RS + +.nf +auto [ZONES...] { + directory DIR [REGEXP ORIGIN\_TEMPLATE] + reload DURATION +} + +.fi +.RE + +.PP +\fBZONES\fP zones it should be authoritative for. If empty, the zones from the configuration block +are used. + +.IP \(bu 4 +\fB\fCdirectory\fR loads zones from the specified \fBDIR\fP. If a file name matches \fBREGEXP\fP it will be +used to extract the origin. \fBORIGIN_TEMPLATE\fP will be used as a template for the origin. Strings +like \fB\fC{}\fR are replaced with the respective matches in the file name, e.g. \fB\fC{1}\fR is the +first match, \fB\fC{2}\fR is the second. The default is: \fB\fCdb\.(.*) {1}\fR i.e. from a file with the +name \fB\fCdb.example.com\fR, the extracted origin will be \fB\fCexample.com\fR. +.IP \(bu 4 +\fB\fCreload\fR interval to perform reloads of zones if SOA version changes and zonefiles. It specifies how often CoreDNS should scan the directory to watch for file removal and addition. Default is one minute. +Value of \fB\fC0\fR means to not scan for changes and reload. eg. \fB\fC30s\fR checks zonefile every 30 seconds +and reloads zone when serial changes. + + +.PP +For enabling zone transfers look at the \fItransfer\fP plugin. + +.PP +All directives from the \fIfile\fP plugin are supported. Note that \fIauto\fP will load all zones found, +even though the directive might only receive queries for a specific zone. I.e: + +.PP +.RS + +.nf +\&. { + auto example.org { + directory /etc/coredns/zones + } +} + +.fi +.RE + +.PP +Will happily pick up a zone for \fB\fCexample.COM\fR, except it will never be queried, because the \fIauto\fP +directive only is authoritative for \fB\fCexample.ORG\fR. + +.SH "EXAMPLES" +.PP +Load \fB\fCorg\fR domains from \fB\fC/etc/coredns/zones/org\fR and allow transfers to the internet, but send +notifies to 10.240.1.1 + +.PP +.RS + +.nf +org { + auto { + directory /etc/coredns/zones/org + } + transfer { + to * + to 10.240.1.1 + } +} + +.fi +.RE + +.PP +Load \fB\fCorg\fR domains from \fB\fC/etc/coredns/zones/org\fR and looks for file names as \fB\fCwww.db.example.org\fR, +where \fB\fCexample.org\fR is the origin. Scan every 45 seconds. + +.PP +.RS + +.nf +org { + auto { + directory /etc/coredns/zones/org www\\.db\\.(.*) {1} + reload 45s + } +} + +.fi +.RE + +.SH "ALSO" +.PP +Use the \fIroot\fP plugin to help you specify the location of the zone files. See the \fItransfer\fP plugin +to enable outgoing zone transfers. + diff --git a/ag_201_coredns/man/coredns-autopath.7 b/ag_201_coredns/man/coredns-autopath.7 new file mode 100644 index 0000000..4840b8a --- /dev/null +++ b/ag_201_coredns/man/coredns-autopath.7 @@ -0,0 +1,95 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-AUTOPATH" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIautopath\fP - allows for server-side search path completion. + +.SH "DESCRIPTION" +.PP +If the \fIautopath\fP plugin sees a query that matches the first element of the configured search path, it will +follow the chain of search path elements and return the first reply that is not NXDOMAIN. On any +failures, the original reply is returned. Because \fIautopath\fP returns a reply for a name that wasn't +the original question, it will add a CNAME that points from the original name (with the search path +element in it) to the name of this answer. + +.PP +\fBNote\fP: There are several known issues, see the "Bugs" section below. + +.SH "SYNTAX" +.PP +.RS + +.nf +autopath [ZONE...] RESOLV\-CONF + +.fi +.RE + +.IP \(bu 4 +\fBZONES\fP zones \fIautopath\fP should be authoritative for. +.IP \(bu 4 +\fBRESOLV-CONF\fP points to a \fB\fCresolv.conf\fR like file or uses a special syntax to point to another +plugin. For instance \fB\fC@kubernetes\fR, will call out to the kubernetes plugin (for each +query) to retrieve the search list it should use. + + +.PP +If a plugin implements the \fB\fCAutoPather\fR interface then it can be used by \fIautopath\fP. + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metric is exported: + +.IP \(bu 4 +\fB\fCcoredns_autopath_success_total{server}\fR - counter of successfully autopath-ed queries. + + +.PP +The \fB\fCserver\fR label is explained in the \fImetrics\fP plugin documentation. + +.SH "EXAMPLES" +.PP +.RS + +.nf +autopath my\-resolv.conf + +.fi +.RE + +.PP +Use \fB\fCmy-resolv.conf\fR as the file to get the search path from. This file only needs to have one line: +\fB\fCsearch domain1 domain2 ...\fR + +.PP +.RS + +.nf +autopath @kubernetes + +.fi +.RE + +.PP +Use the search path dynamically retrieved from the \fIkubernetes\fP plugin. + +.SH "BUGS" +.PP +In Kubernetes, \fIautopath\fP can derive the wrong namespace of a client Pod (and therefore wrong search +path) in the following case. To properly build the search path of a client \fIautopath\fP needs to know +the namespace of the a Pod making a DNS request. To do this, it relies on the \fIkubernetes\fP 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 +immediately provisioned to a Pod in another namespace, and that new Pod make a DNS lookup \fIbefore\fP +the API watch can notify CoreDNS of the change, \fIautopath\fP will resolve the IP to the previous Pod's +namespace. + +.PP +In Kubernetes, \fIautopath\fP is not compatible with Pods running from Windows nodes. + +.PP +If the server side search ultimately results in a negative answer (e.g. \fB\fCNXDOMAIN\fR), then the client +will fruitlessly search all paths manually, thus negating the \fIautopath\fP optimization. + diff --git a/ag_201_coredns/man/coredns-azure.7 b/ag_201_coredns/man/coredns-azure.7 new file mode 100644 index 0000000..dc92154 --- /dev/null +++ b/ag_201_coredns/man/coredns-azure.7 @@ -0,0 +1,74 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-AZURE" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIazure\fP - enables serving zone data from Microsoft Azure DNS service. + +.SH "DESCRIPTION" +.PP +The azure plugin is useful for serving zones from Microsoft Azure DNS. The \fIazure\fP plugin supports +all the DNS records supported by Azure, viz. A, AAAA, CNAME, MX, NS, PTR, SOA, SRV, and TXT +record types. NS record type is not supported by azure private DNS. + +.SH "SYNTAX" +.PP +.RS + +.nf +azure RESOURCE\_GROUP:ZONE... { + tenant TENANT\_ID + client CLIENT\_ID + secret CLIENT\_SECRET + subscription SUBSCRIPTION\_ID + environment ENVIRONMENT + fallthrough [ZONES...] + access private +} + +.fi +.RE + +.IP \(bu 4 +\fBRESOURCE_GROUP:ZONE\fP is the resource group to which the hosted zones belongs on Azure, +and \fBZONE\fP the zone that contains data. +.IP \(bu 4 +\fBCLIENT_ID\fP and \fBCLIENT_SECRET\fP are the credentials for Azure, and \fB\fCtenant\fR specifies the +\fBTENANT_ID\fP to be used. \fBSUBSCRIPTION_ID\fP is the subscription ID. All of these are needed +to access the data in Azure. +.IP \(bu 4 +\fB\fCenvironment\fR specifies the Azure \fBENVIRONMENT\fP. +.IP \(bu 4 +\fB\fCfallthrough\fR If zone matches and no record can be generated, pass request to the next plugin. +If \fBZONES\fP is omitted, then fallthrough happens for all zones for which the plugin is +authoritative. +.IP \(bu 4 +\fB\fCaccess\fR specifies if the zone is \fB\fCpublic\fR or \fB\fCprivate\fR. Default is \fB\fCpublic\fR. + + +.SH "EXAMPLES" +.PP +Enable the \fIazure\fP plugin with Azure credentials for private zones \fB\fCexample.org\fR, \fB\fCexample.private\fR: + +.PP +.RS + +.nf +example.org { + azure resource\_group\_foo:example.org resource\_group\_foo:example.private { + tenant 123abc\-123abc\-123abc\-123abc + client 123abc\-123abc\-123abc\-234xyz + subscription 123abc\-123abc\-123abc\-563abc + secret mysecret + access private + } +} + +.fi +.RE + +.SH "SEE ALSO" +.PP +The Azure DNS Overview +\[la]https://docs.microsoft.com/en-us/azure/dns/dns-overview\[ra]. + diff --git a/ag_201_coredns/man/coredns-bind.7 b/ag_201_coredns/man/coredns-bind.7 new file mode 100644 index 0000000..2306bca --- /dev/null +++ b/ag_201_coredns/man/coredns-bind.7 @@ -0,0 +1,119 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-BIND" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIbind\fP - overrides the host to which the server should bind. + +.SH "DESCRIPTION" +.PP +Normally, the listener binds to the wildcard host. However, you may want the listener to bind to +another IP instead. + +.PP +If several addresses are provided, a listener will be open on each of the IP provided. + +.PP +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 +.RS + +.nf +bind ADDRESS ... + +.fi +.RE + +.PP +\fBADDRESS\fP is an IP address to bind to. +When several addresses are provided a listener will be opened on each of the addresses. + +.SH "EXAMPLES" +.PP +To make your socket accessible only to that machine, bind to IP 127.0.0.1 (localhost): + +.PP +.RS + +.nf +\&. { + bind 127.0.0.1 +} + +.fi +.RE + +.PP +To allow processing DNS requests only local host on both IPv4 and IPv6 stacks, use the syntax: + +.PP +.RS + +.nf +\&. { + bind 127.0.0.1 ::1 +} + +.fi +.RE + +.PP +If the configuration comes up with several \fIbind\fP plugins, all addresses are consolidated together: +The following sample is equivalent to the preceding: + +.PP +.RS + +.nf +\&. { + bind 127.0.0.1 + bind ::1 +} + +.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 +address and port. Doing so will result in unpredictable behavior (requests may be randomly +served by either server). Keep in mind that \fIwithout\fP the \fIbind\fP plugin, a server will bind to all +interfaces, and this will collide with another server if it's using \fIbind\fP to listen to an interface +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 \fB\fCa.bad.example.com\fR: + +.PP +.RS + +.nf +a.bad.example.com { + bind 127.0.0.1 + forward . 1.2.3.4 +} + +bad.example.com { + forward . 5.6.7.8 +} + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-bufsize.7 b/ag_201_coredns/man/coredns-bufsize.7 new file mode 100644 index 0000000..e5303e3 --- /dev/null +++ b/ag_201_coredns/man/coredns-bufsize.7 @@ -0,0 +1,67 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-BUFSIZE" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIbufsize\fP - sizes EDNS0 buffer size to prevent IP fragmentation. + +.SH "DESCRIPTION" +.PP +\fIbufsize\fP limits a requester's UDP payload size. +It prevents IP fragmentation, mitigating certain DNS vulnerabilities. + +.SH "SYNTAX" +.PP +.RS + +.nf +bufsize [SIZE] + +.fi +.RE + +.PP +\fB[SIZE]\fP is an int value for setting the buffer size. +The default value is 512, and the value must be within 512 - 4096. +Only one argument is acceptable, and it covers both IPv4 and IPv6. + +.SH "EXAMPLES" +.PP +Enable limiting the buffer size of outgoing query to the resolver (172.31.0.10): + +.PP +.RS + +.nf +\&. { + bufsize 512 + forward . 172.31.0.10 + log +} + +.fi +.RE + +.PP +Enable limiting the buffer size as an authoritative nameserver: + +.PP +.RS + +.nf +\&. { + bufsize 512 + file db.example.org + log +} + +.fi +.RE + +.SH "CONSIDERATIONS" +.IP \(bu 4 +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. +.IP \(bu 4 +For now, if a client does not use EDNS, this plugin adds OPT RR. + + diff --git a/ag_201_coredns/man/coredns-cache.7 b/ag_201_coredns/man/coredns-cache.7 new file mode 100644 index 0000000..b05eeae --- /dev/null +++ b/ag_201_coredns/man/coredns-cache.7 @@ -0,0 +1,165 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-CACHE" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIcache\fP - enables a frontend cache. + +.SH "DESCRIPTION" +.PP +With \fIcache\fP enabled, all records except zone transfers and metadata records will be cached for up to +3600s. Caching is mostly useful in a scenario when fetching data from the backend (upstream, +database, etc.) is expensive. + +.PP +\fICache\fP 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. + +.PP +This plugin can only be used once per Server Block. + +.SH "SYNTAX" +.PP +.RS + +.nf +cache [TTL] [ZONES...] + +.fi +.RE + +.IP \(bu 4 +\fBTTL\fP max TTL in seconds. If not specified, the maximum TTL will be used, which is 3600 for +NOERROR responses and 1800 for denial of existence ones. +Setting a TTL of 300: \fB\fCcache 300\fR would cache records up to 300 seconds. +.IP \(bu 4 +\fBZONES\fP zones it should cache for. If empty, the zones from the configuration block are used. + + +.PP +Each element in the cache is cached according to its TTL (with \fBTTL\fP as the max). +A cache is divided into 256 shards, each holding up to 39 items by default - for a total size +of 256 * 39 = 9984 items. + +.PP +If you want more control: + +.PP +.RS + +.nf +cache [TTL] [ZONES...] { + success CAPACITY [TTL] [MINTTL] + denial CAPACITY [TTL] [MINTTL] + prefetch AMOUNT [[DURATION] [PERCENTAGE%]] + serve\_stale [DURATION] +} + +.fi +.RE + +.IP \(bu 4 +\fBTTL\fP and \fBZONES\fP as above. +.IP \(bu 4 +\fB\fCsuccess\fR, override the settings for caching successful responses. \fBCAPACITY\fP indicates the maximum +number of packets we cache before we start evicting (\fIrandomly\fP). \fBTTL\fP overrides the cache maximum TTL. +\fBMINTTL\fP overrides the cache minimum TTL (default 5), which can be useful to limit queries to the backend. +.IP \(bu 4 +\fB\fCdenial\fR, override the settings for caching denial of existence responses. \fBCAPACITY\fP indicates the maximum +number of packets we cache before we start evicting (LRU). \fBTTL\fP overrides the cache maximum TTL. +\fBMINTTL\fP overrides the cache minimum TTL (default 5), which can be useful to limit queries to the backend. +There is a third category (\fB\fCerror\fR) but those responses are never cached. +.IP \(bu 4 +\fB\fCprefetch\fR will prefetch popular items when they are about to be expunged from the cache. +Popular means \fBAMOUNT\fP queries have been seen with no gaps of \fBDURATION\fP or more between them. +\fBDURATION\fP defaults to 1m. Prefetching will happen when the TTL drops below \fBPERCENTAGE\fP, +which defaults to \fB\fC10%\fR, or latest 1 second before TTL expiration. Values should be in the range \fB\fC[10%, 90%]\fR. +Note the percent sign is mandatory. \fBPERCENTAGE\fP is treated as an \fB\fCint\fR. +.IP \(bu 4 +\fB\fCserve_stale\fR, 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. \fBDURATION\fP is how far back to consider +stale responses as fresh. The default duration is 1h. + + +.SH "CAPACITY AND EVICTION" +.PP +If \fBCAPACITY\fP \fIis not\fP specified, the default cache size is 9984 per cache. The minimum allowed cache size is 1024. +If \fBCAPACITY\fP \fIis\fP specified, the actual cache size used will be rounded down to the nearest number divisible by 256 (so all shards are equal in size). + +.PP +Eviction is done per shard. In effect, when a shard reaches capacity, items are evicted from that shard. +Since shards don't fill up perfectly evenly, evictions will occur before the entire cache reaches full capacity. +Each shard capacity is equal to the total cache size / number of shards (256). Eviction is random, not TTL based. +Entries with 0 TTL will remain in the cache until randomly evicted when the shard reaches capacity. + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metrics are exported: + +.IP \(bu 4 +\fB\fCcoredns_cache_entries{server, type}\fR - Total elements in the cache by cache type. +.IP \(bu 4 +\fB\fCcoredns_cache_hits_total{server, type}\fR - Counter of cache hits by cache type. +.IP \(bu 4 +\fB\fCcoredns_cache_misses_total{server}\fR - Counter of cache misses. +.IP \(bu 4 +\fB\fCcoredns_cache_prefetch_total{server}\fR - Counter of times the cache has prefetched a cached item. +.IP \(bu 4 +\fB\fCcoredns_cache_drops_total{server}\fR - Counter of responses excluded from the cache due to request/response question name mismatch. +.IP \(bu 4 +\fB\fCcoredns_cache_served_stale_total{server}\fR - Counter of requests served from stale cache entries. + + +.PP +Cache types are either "denial" or "success". \fB\fCServer\fR is the server handling the request, see the +prometheus plugin for documentation. + +.SH "EXAMPLES" +.PP +Enable caching for all zones, but cap everything to a TTL of 10 seconds: + +.PP +.RS + +.nf +\&. { + cache 10 + whoami +} + +.fi +.RE + +.PP +Proxy to Google Public DNS and only cache responses for example.org (or below). + +.PP +.RS + +.nf +\&. { + forward . 8.8.8.8:53 + cache example.org +} + +.fi +.RE + +.PP +Enable caching for \fB\fCexample.org\fR, keep a positive cache size of 5000 and a negative cache size of 2500: + +.PP +.RS + +.nf +example.org { + cache { + success 5000 + denial 2500 + } +} + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-cancel.7 b/ag_201_coredns/man/coredns-cancel.7 new file mode 100644 index 0000000..a0c8386 --- /dev/null +++ b/ag_201_coredns/man/coredns-cancel.7 @@ -0,0 +1,67 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-CANCEL" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIcancel\fP - cancels a request's context after 5001 milliseconds. + +.SH "DESCRIPTION" +.PP +The \fIcancel\fP plugin creates a canceling context for each request. It adds a timeout that gets +triggered after 5001 milliseconds. + +.PP +The 5001 number was chosen because the default timeout for DNS clients is 5 seconds, after that they +give up. + +.PP +A plugin interested in the cancellation status should call \fB\fCplugin.Done()\fR on the context. If the +context was canceled due to a timeout the plugin should not write anything back to the client and +return a value indicating CoreDNS should not either; a zero return value should suffice for that. + +.SH "SYNTAX" +.PP +.RS + +.nf +cancel [TIMEOUT] + +.fi +.RE + +.IP \(bu 4 +\fBTIMEOUT\fP allows setting a custom timeout. The default timeout is 5001 milliseconds (\fB\fC5001 ms\fR) + + +.SH "EXAMPLES" +.PP +.RS + +.nf +example.org { + cancel + whoami +} + +.fi +.RE + +.PP +Or with a custom timeout: + +.PP +.RS + +.nf +example.org { + cancel 1s + whoami +} + +.fi +.RE + +.SH "SEE ALSO" +.PP +The Go documentation for the context package. + diff --git a/ag_201_coredns/man/coredns-chaos.7 b/ag_201_coredns/man/coredns-chaos.7 new file mode 100644 index 0000000..a873d69 --- /dev/null +++ b/ag_201_coredns/man/coredns-chaos.7 @@ -0,0 +1,78 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-CHAOS" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIchaos\fP - allows for responding to TXT queries in the CH class. + +.SH "DESCRIPTION" +.PP +This is useful for retrieving version or author information from the server by querying a TXT record +for a special domain name in the CH class. + +.SH "SYNTAX" +.PP +.RS + +.nf +chaos [VERSION] [AUTHORS...] + +.fi +.RE + +.IP \(bu 4 +\fBVERSION\fP is the version to return. Defaults to \fB\fCCoreDNS-\fR, if not set. +.IP \(bu 4 +\fBAUTHORS\fP is what authors to return. This defaults to all GitHub handles in the OWNERS files. + + +.PP +Note that you have to make sure that this plugin will get actual queries for the +following zones: \fB\fCversion.bind\fR, \fB\fCversion.server\fR, \fB\fCauthors.bind\fR, \fB\fChostname.bind\fR and +\fB\fCid.server\fR. + +.SH "EXAMPLES" +.PP +Specify all the zones in full. + +.PP +.RS + +.nf +version.bind version.server authors.bind hostname.bind id.server { + chaos CoreDNS\-001 info@coredns.io +} + +.fi +.RE + +.PP +Or just default to \fB\fC.\fR: + +.PP +.RS + +.nf +\&. { + chaos CoreDNS\-001 info@coredns.io +} + +.fi +.RE + +.PP +And test with \fB\fCdig\fR: + +.PP +.RS + +.nf +% dig @localhost CH TXT version.bind +\&... +;; ANSWER SECTION: +version.bind. 0 CH TXT "CoreDNS\-001" +\&... + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-clouddns.7 b/ag_201_coredns/man/coredns-clouddns.7 new file mode 100644 index 0000000..3b83a3a --- /dev/null +++ b/ag_201_coredns/man/coredns-clouddns.7 @@ -0,0 +1,96 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-CLOUDDNS" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIclouddns\fP - enables serving zone data from GCP Cloud DNS. + +.SH "DESCRIPTION" +.PP +The \fIclouddns\fP plugin is useful for serving zones from resource record +sets in GCP Cloud DNS. This plugin supports all Google Cloud DNS +records +\[la]https://cloud.google.com/dns/docs/overview#supported_dns_record_types\[ra]. This plugin can +be used when CoreDNS is deployed on GCP or elsewhere. Note that this plugin accesses the resource +records through the Google Cloud API. For records in a privately hosted zone, it is not necessary to +place CoreDNS and this plugin in the associated VPC network. In fact the private hosted zone could +be created without any associated VPC and this plugin could still access the resource records under +the hosted zone. + +.SH "SYNTAX" +.PP +.RS + +.nf +clouddns [ZONE:PROJECT\_ID:HOSTED\_ZONE\_NAME...] { + credentials [FILENAME] + fallthrough [ZONES...] +} + +.fi +.RE + +.IP \(bu 4 +\fBZONE\fP the name of the domain to be accessed. When there are multiple zones with overlapping +domains (private vs. public hosted zone), CoreDNS does the lookup in the given order here. +Therefore, for a non-existing resource record, SOA response will be from the rightmost zone. +.IP \(bu 4 +\fBPROJECT_ID\fP the project ID of the Google Cloud project. +.IP \(bu 4 +\fBHOSTED_ZONE_NAME\fP the name of the hosted zone that contains the resource record sets to be +accessed. +.IP \(bu 4 +\fB\fCcredentials\fR is used for reading the credential file from \fBFILENAME\fP (normally a .json file). +.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. + + +.SH "EXAMPLES" +.PP +Enable clouddns with implicit GCP credentials and resolve CNAMEs via 10.0.0.1: + +.PP +.RS + +.nf +example.org { + clouddns example.org.:gcp\-example\-project:example\-zone + forward . 10.0.0.1 +} + +.fi +.RE + +.PP +Enable clouddns with fallthrough: + +.PP +.RS + +.nf +example.org { + clouddns example.org.:gcp\-example\-project:example\-zone example.com.:gcp\-example\-project:example\-zone\-2 { + fallthrough example.gov. + } +} + +.fi +.RE + +.PP +Enable clouddns with multiple hosted zones with the same domain: + +.PP +.RS + +.nf +\&. { + clouddns example.org.:gcp\-example\-project:example\-zone example.com.:gcp\-example\-project:other\-example\-zone +} + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-debug.7 b/ag_201_coredns/man/coredns-debug.7 new file mode 100644 index 0000000..90ee349 --- /dev/null +++ b/ag_201_coredns/man/coredns-debug.7 @@ -0,0 +1,73 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-DEBUG" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIdebug\fP - disables the automatic recovery upon a crash so that you'll get a nice stack trace. + +.SH "DESCRIPTION" +.PP +Normally CoreDNS will recover from panics; using \fIdebug\fP inhibits this. The main use of \fIdebug\fP is +to help in testing. A side effect of using \fIdebug\fP is that \fB\fClog.Debug\fR and \fB\fClog.Debugf\fR messages +will be printed to standard output. + +.PP +Note that the \fIerrors\fP plugin (if loaded) will also set a \fB\fCrecover\fR, negating this setting. + +.PP +Enabling this plugin is process-wide: enabling \fIdebug\fP in at least one server block enables +debug mode globally. + +.SH "SYNTAX" +.PP +.RS + +.nf +debug + +.fi +.RE + +.PP +Some plugins will send debug log DNS messages. This is done in the following format: + +.PP +.RS + +.nf +debug: 000000 00 0a 01 00 00 01 00 00 00 00 00 01 07 65 78 61 +debug: 000010 6d 70 6c 65 05 6c 6f 63 61 6c 00 00 01 00 01 00 +debug: 000020 00 29 10 00 00 00 80 00 00 00 +debug: 00002a + +.fi +.RE + +.PP +Using \fB\fCtext2pcap\fR (part of Wireshark), this can be converted back to binary, with the following +command line: \fB\fCtext2pcap -i 17 -u 53,53\fR, where 17 is the protocol (UDP) and 53 are the ports. These +ports allow Wireshark to detect these packets as DNS messages. + +.PP +Each plugin can decide whether to dump messages to aid in debugging. + +.SH "EXAMPLES" +.PP +Disable the ability to recover from crashes and show debug logging: + +.PP +.RS + +.nf +\&. { + debug +} + +.fi +.RE + +.SH "SEE ALSO" +.PP +https://www.wireshark.org/docs/man-pages/text2pcap.html +\[la]https://www.wireshark.org/docs/man-pages/text2pcap.html\[ra]. + diff --git a/ag_201_coredns/man/coredns-dns64.7 b/ag_201_coredns/man/coredns-dns64.7 new file mode 100644 index 0000000..7a5274c --- /dev/null +++ b/ag_201_coredns/man/coredns-dns64.7 @@ -0,0 +1,144 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-DNS64" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIdns64\fP - enables DNS64 IPv6 transition mechanism. + +.SH "DESCRIPTION" +.PP +The \fIdns64\fP plugin will when asked for a domain's AAAA records, but only finds A records, +synthesizes the AAAA records from the A records. + +.PP +The synthesis is \fIonly\fP performed \fBif the query came in via IPv6\fP. + +.PP +This translation is for IPv6-only networks that have NAT64 +\[la]https://en.wikipedia.org/wiki/NAT64\[ra]. + +.SH "SYNTAX" +.PP +.RS + +.nf +dns64 [PREFIX] + +.fi +.RE + +.IP \(bu 4 +\fBPREFIX\fP defines a custom prefix instead of the default \fB\fC64:ff9b::/96\fR. + + +.PP +Or use this slightly longer form with more options: + +.PP +.RS + +.nf +dns64 [PREFIX] { + [translate\_all] + prefix PREFIX +} + +.fi +.RE + +.IP \(bu 4 +\fB\fCprefix\fR specifies any local IPv6 prefix to use, instead of the well known prefix (64:ff9b::/96) +.IP \(bu 4 +\fB\fCtranslate_all\fR translates all queries, including responses that have AAAA results. + + +.SH "EXAMPLES" +.PP +Translate with the default well known prefix. Applies to all queries (if they came in over IPv6). + +.PP +.RS + +.nf +\&. { + dns64 +} + +.fi +.RE + +.PP +Use a custom prefix. + +.PP +.RS + +.nf +\&. { + dns64 64:1337::/96 +} + +.fi +.RE + +.PP +Or + +.PP +.RS + +.nf +\&. { + dns64 { + prefix 64:1337::/96 + } +} + +.fi +.RE + +.PP +Enable translation even if an existing AAAA record is present. + +.PP +.RS + +.nf +\&. { + dns64 { + translate\_all + } +} + +.fi +.RE + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metrics are exported: + +.IP \(bu 4 +\fB\fCcoredns_dns64_requests_translated_total{server}\fR - counter of DNS requests translated + + +.PP +The \fB\fCserver\fR label is explained in the \fIprometheus\fP plugin documentation. + +.SH "BUGS" +.PP +Not all features required by DNS64 are implemented, only basic AAAA synthesis. + +.IP \(bu 4 +Support "mapping of separate IPv4 ranges to separate IPv6 prefixes" +.IP \(bu 4 +Resolve PTR records +.IP \(bu 4 +Make resolver DNSSEC aware. See: RFC 6147 Section 3 +\[la]https://tools.ietf.org/html/rfc6147#section-3\[ra] + + +.SH "SEE ALSO" +.PP +See RFC 6147 +\[la]https://tools.ietf.org/html/rfc6147\[ra] for more information on the DNS64 mechanism. + diff --git a/ag_201_coredns/man/coredns-dnssec.7 b/ag_201_coredns/man/coredns-dnssec.7 new file mode 100644 index 0000000..7376e6b --- /dev/null +++ b/ag_201_coredns/man/coredns-dnssec.7 @@ -0,0 +1,119 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-DNSSEC" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIdnssec\fP - enables on-the-fly DNSSEC signing of served data. + +.SH "DESCRIPTION" +.PP +With \fIdnssec\fP, any reply that doesn't (or can't) do DNSSEC will get signed on the fly. 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 \fInot\fP supported. + +.PP +This plugin can only be used once per Server Block. + +.SH "SYNTAX" +.PP +.RS + +.nf +dnssec [ZONES... ] { + key file KEY... + cache\_capacity CAPACITY +} + +.fi +.RE + +.PP +The signing behavior depends on the keys specified. If multiple keys are specified of which there is +at least one key with the SEP bit set and at least one key with the SEP bit unset, signing will happen +in split ZSK/KSK mode. DNSKEY records will be signed with all keys that have the SEP bit set. All other +records will be signed with all keys that do not have the SEP bit set. + +.PP +In any other case, each specified key will be treated as a CSK (common signing key), forgoing the +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 \fInot\fP supported. + +.PP +If multiple \fIdnssec\fP plugins are specified in the same zone, the last one specified will be +used (See bugs +\[la]#bugs\[ra]). + +.IP \(bu 4 +\fBZONES\fP zones that should be signed. If empty, the zones from the configuration block +are used. +.IP \(bu 4 +\fB\fCkey file\fR indicates that \fBKEY\fP file(s) should be read from disk. When multiple keys are specified, RRsets +will be signed with all keys. Generating a key can be done with \fB\fCdnssec-keygen\fR: \fB\fCdnssec-keygen -a +ECDSAP256SHA256 \fR. A key created for zone \fIA\fP can be safely used for zone \fIB\fP. The name of the +key file can be specified in one of the following formats + +.RS +.IP \(en 4 +basename of the generated key \fB\fCKexample.org+013+45330\fR +.IP \(en 4 +generated public key \fB\fCKexample.org+013+45330.key\fR +.IP \(en 4 +generated private key \fB\fCKexample.org+013+45330.private\fR + +.RE +.IP \(bu 4 +\fB\fCcache_capacity\fR indicates the capacity of the cache. The dnssec plugin uses a cache to store +RRSIGs. The default for \fBCAPACITY\fP is 10000. + + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metrics are exported: + +.IP \(bu 4 +\fB\fCcoredns_dnssec_cache_entries{server, type}\fR - total elements in the cache, type is "signature". +.IP \(bu 4 +\fB\fCcoredns_dnssec_cache_hits_total{server}\fR - Counter of cache hits. +.IP \(bu 4 +\fB\fCcoredns_dnssec_cache_misses_total{server}\fR - Counter of cache misses. + + +.PP +The label \fB\fCserver\fR indicated the server handling the request, see the \fImetrics\fP plugin for details. + +.SH "EXAMPLES" +.PP +Sign responses for \fB\fCexample.org\fR with the key "Kexample.org.+013+45330.key". + +.PP +.RS + +.nf +example.org { + dnssec { + key file Kexample.org.+013+45330 + } + whoami +} + +.fi +.RE + +.PP +Sign responses for a kubernetes zone with the key "Kcluster.local+013+45129.key". + +.PP +.RS + +.nf +cluster.local { + kubernetes + dnssec { + key file Kcluster.local+013+45129 + } +} + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-dnstap.7 b/ag_201_coredns/man/coredns-dnstap.7 new file mode 100644 index 0000000..3dbb0f9 --- /dev/null +++ b/ag_201_coredns/man/coredns-dnstap.7 @@ -0,0 +1,163 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-DNSTAP" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIdnstap\fP - enables logging to dnstap. + +.SH "DESCRIPTION" +.PP +dnstap is a flexible, structured binary log format for DNS software; see https://dnstap.info +\[la]https://dnstap.info\[ra]. With this +plugin you make CoreDNS output dnstap logging. + +.PP +Every message is sent to the socket as soon as it comes in, the \fIdnstap\fP plugin has a buffer of +10000 messages, above that number dnstap messages will be dropped (this is logged). + +.SH "SYNTAX" +.PP +.RS + +.nf +dnstap SOCKET [full] + +.fi +.RE + +.IP \(bu 4 +\fBSOCKET\fP is the socket (path) supplied to the dnstap command line tool. +.IP \(bu 4 +\fB\fCfull\fR to include the wire-format DNS message. + + +.SH "EXAMPLES" +.PP +Log information about client requests and responses to \fI/tmp/dnstap.sock\fP. + +.PP +.RS + +.nf +dnstap /tmp/dnstap.sock + +.fi +.RE + +.PP +Log information including the wire-format DNS message about client requests and responses to \fI/tmp/dnstap.sock\fP. + +.PP +.RS + +.nf +dnstap unix:///tmp/dnstap.sock full + +.fi +.RE + +.PP +Log to a remote endpoint. + +.PP +.RS + +.nf +dnstap tcp://127.0.0.1:6000 full + +.fi +.RE + +.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 +\[la]https://github.com/dnstap/golang-dnstap\[ra]. It's written in Go. + +.PP +The following command listens on the given socket and decodes messages to stdout. + +.PP +.RS + +.nf +$ dnstap \-u /tmp/dnstap.sock + +.fi +.RE + +.PP +The following command listens on the given socket and saves message payloads to a binary dnstap-format log file. + +.PP +.RS + +.nf +$ dnstap \-u /tmp/dnstap.sock \-w /tmp/test.dnstap + +.fi +.RE + +.PP +Listen for dnstap messages on port 6000. + +.PP +.RS + +.nf +$ dnstap \-l 127.0.0.1:6000 + +.fi +.RE + +.SH "USING DNSTAP IN YOUR PLUGIN" +.PP +In your setup function, check to see if the \fIdnstap\fP plugin is loaded: + +.PP +.RS + +.nf +c.OnStartup(func() error { + if taph := dnsserver.GetConfig(c).Handler("dnstap"); taph != nil { + if tapPlugin, ok := taph.(dnstap.Dnstap); ok { + f.tapPlugin = \&tapPlugin + } + } + return nil +}) + +.fi +.RE + +.PP +And then in your plugin: + +.PP +.RS + +.nf +func (x RandomPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if tapPlugin != nil { + q := new(msg.Msg) + msg.SetQueryTime(q, time.Now()) + msg.SetQueryAddress(q, w.RemoteAddr()) + if tapPlugin.IncludeRawMessage { + buf, \_ := r.Pack() // r has been seen packed/unpacked before, this should not fail + q.QueryMessage = buf + } + msg.SetType(q, tap.Message\_CLIENT\_QUERY) + tapPlugin.TapMessage(q) + } + // ... +} + +.fi +.RE + +.SH "SEE ALSO" +.PP +The website dnstap.info +\[la]https://dnstap.info\[ra] has info on the dnstap protocol. The \fIforward\fP +plugin's \fB\fCdnstap.go\fR uses dnstap to tap messages sent to an upstream. + diff --git a/ag_201_coredns/man/coredns-erratic.7 b/ag_201_coredns/man/coredns-erratic.7 new file mode 100644 index 0000000..c851536 --- /dev/null +++ b/ag_201_coredns/man/coredns-erratic.7 @@ -0,0 +1,132 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-ERRATIC" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIerratic\fP - a plugin useful for testing client behavior. + +.SH "DESCRIPTION" +.PP +\fIerratic\fP returns a static response to all queries, but the responses can be delayed, +dropped or truncated. The \fIerratic\fP plugin will respond to every A or AAAA query. For +any other type it will return a SERVFAIL response (except AXFR). The reply for A will return +192.0.2.53 (RFC 5737 +\[la]https://tools.ietf.org/html/rfc5737\[ra]), for AAAA it returns 2001:DB8::53 (RFC +3849 +\[la]https://tools.ietf.org/html/rfc3849\[ra]). For an AXFR request it will respond with a small +zone transfer. + +.SH "SYNTAX" +.PP +.RS + +.nf +erratic { + drop [AMOUNT] + truncate [AMOUNT] + delay [AMOUNT [DURATION]] +} + +.fi +.RE + +.IP \(bu 4 +\fB\fCdrop\fR: drop 1 per \fBAMOUNT\fP of queries, the default is 2. +.IP \(bu 4 +\fB\fCtruncate\fR: truncate 1 per \fBAMOUNT\fP of queries, the default is 2. +.IP \(bu 4 +\fB\fCdelay\fR: delay 1 per \fBAMOUNT\fP of queries for \fBDURATION\fP, the default for \fBAMOUNT\fP is 2 and +the default for \fBDURATION\fP is 100ms. + + +.PP +In case of a zone transfer and truncate the final SOA record \fIisn't\fP added to the response. + +.SH "READY" +.PP +This plugin reports readiness to the ready plugin. + +.SH "EXAMPLES" +.PP +.RS + +.nf +example.org { + erratic { + drop 3 + } +} + +.fi +.RE + +.PP +Or even shorter if the defaults suit you. Note this only drops queries, it does not delay them. + +.PP +.RS + +.nf +example.org { + erratic +} + +.fi +.RE + +.PP +Delay 1 in 3 queries for 50ms + +.PP +.RS + +.nf +example.org { + erratic { + delay 3 50ms + } +} + +.fi +.RE + +.PP +Delay 1 in 3 and truncate 1 in 5. + +.PP +.RS + +.nf +example.org { + erratic { + delay 3 5ms + truncate 5 + } +} + +.fi +.RE + +.PP +Drop every second query. + +.PP +.RS + +.nf +example.org { + erratic { + drop 2 + truncate 2 + } +} + +.fi +.RE + +.SH "SEE ALSO" +.PP +RFC 3849 +\[la]https://tools.ietf.org/html/rfc3849\[ra] and RFC 5737 +\[la]https://tools.ietf.org/html/rfc5737\[ra]. + diff --git a/ag_201_coredns/man/coredns-errors.7 b/ag_201_coredns/man/coredns-errors.7 new file mode 100644 index 0000000..b296fa3 --- /dev/null +++ b/ag_201_coredns/man/coredns-errors.7 @@ -0,0 +1,93 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-ERRORS" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIerrors\fP - enables error logging. + +.SH "DESCRIPTION" +.PP +Any errors encountered during the query processing will be printed to standard output. The errors of particular type can be consolidated and printed once per some period of time. + +.PP +This plugin can only be used once per Server Block. + +.SH "SYNTAX" +.PP +The basic syntax is: + +.PP +.RS + +.nf +errors + +.fi +.RE + +.PP +Extra knobs are available with an expanded syntax: + +.PP +.RS + +.nf +errors { + consolidate DURATION REGEXP +} + +.fi +.RE + +.PP +Option \fB\fCconsolidate\fR allows collecting several error messages matching the regular expression \fBREGEXP\fP during \fBDURATION\fP. After the \fBDURATION\fP since receiving the first such message, the consolidated message will be printed to standard output, e.g. + +.PP +.RS + +.nf +2 errors like '^read udp .* i/o timeout$' occurred in last 30s + +.fi +.RE + +.PP +Multiple \fB\fCconsolidate\fR options with different \fBDURATION\fP and \fBREGEXP\fP are allowed. In case if some error message corresponds to several defined regular expressions the message will be associated with the first appropriate \fBREGEXP\fP. + +.PP +For better performance, it's recommended to use the \fB\fC^\fR or \fB\fC$\fR metacharacters in regular expression when filtering error messages by prefix or suffix, e.g. \fB\fC^failed to .*\fR, or \fB\fC.* timeout$\fR. + +.SH "EXAMPLES" +.PP +Use the \fIwhoami\fP to respond to queries in the example.org domain and Log errors to standard output. + +.PP +.RS + +.nf +example.org { + whoami + errors +} + +.fi +.RE + +.PP +Use the \fIforward\fP 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 ". + +.PP +.RS + +.nf +\&. { + forward . 8.8.8.8 + errors { + consolidate 5m ".* i/o timeout$" + consolidate 30s "^Failed to .+" + } +} + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-etcd.7 b/ag_201_coredns/man/coredns-etcd.7 new file mode 100644 index 0000000..371f81f --- /dev/null +++ b/ag_201_coredns/man/coredns-etcd.7 @@ -0,0 +1,372 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-ETCD" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIetcd\fP - enables SkyDNS service discovery from etcd. + +.SH "DESCRIPTION" +.PP +The \fIetcd\fP plugin implements the (older) SkyDNS service discovery service. It is \fInot\fP suitable as +a generic DNS zone data plugin. Only a subset of DNS record types are implemented, and subdomains +and delegations are not handled at all. The plugin will also recursively descend the tree and return +all records found, see "Special Behavior" below for details. + +.PP +The data in the etcd instance has to be encoded as +a message +\[la]https://github.com/skynetservices/skydns/blob/2fcff74cdc9f9a7dd64189a447ef27ac354b725f/msg/service.go#L26\[ra] +like SkyDNS +\[la]https://github.com/skynetservices/skydns\[ra]. It works just like SkyDNS. + +.PP +The \fIetcd\fP plugin makes extensive use of the \fIforward\fP plugin to forward and query other servers in the +network - if that plugin has been enabled as well. + +.SH "SYNTAX" +.PP +.RS + +.nf +etcd [ZONES...] + +.fi +.RE + +.IP \(bu 4 +\fBZONES\fP zones \fIetcd\fP should be authoritative for. + + +.PP +The path will default to \fB\fC/skydns\fR the local etcd3 proxy (http://localhost:2379 +\[la]http://localhost:2379\[ra]). If no zones are +specified the block's zone will be used as the zone. + +.PP +.RS + +.nf +etcd [ZONES...] { + fallthrough [ZONES...] + path PATH + endpoint ENDPOINT... + credentials USERNAME PASSWORD + tls CERT KEY CACERT +} + +.fi +.RE + +.IP \(bu 4 +\fB\fCfallthrough\fR If zone matches but 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. +.IP \(bu 4 +\fBPATH\fP the path inside etcd. Defaults to "/skydns". +.IP \(bu 4 +\fBENDPOINT\fP the etcd endpoints. Defaults to "http://localhost:2379" +\[la]http://localhost:2379"\[ra]. +.IP \(bu 4 +\fB\fCcredentials\fR is used to set the \fBUSERNAME\fP and \fBPASSWORD\fP for accessing the etcd cluster. +.IP \(bu 4 +\fB\fCtls\fR followed by: + +.RS +.IP \(en 4 +no arguments, if the server certificate is signed by a system-installed CA and no client cert is needed +.IP \(en 4 +a single argument that is the CA PEM file, if the server cert is not signed by a system CA and no client cert is needed +.IP \(en 4 +two arguments - path to cert PEM file, the path to private key PEM file - if the server certificate is signed by a system-installed CA and a client certificate is needed +.IP \(en 4 +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. + +.RE + + +.SH "SPECIAL BEHAVIOUR" +.PP +The \fIetcd\fP plugin leverages directory structure to look for related entries. For example +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. +If the directory extends deeper those records are returned as well. + +.PP +With etcd3, support for hierarchical keys are +dropped +\[la]https://coreos.com/etcd/docs/latest/learning/api.html\[ra]. This means there are no directories +but only flat keys with prefixes in etcd3. To accommodate lookups, the \fIetcd\fP plugin now does a lookup +on prefix \fB\fC/skydns/test/skydns/mx/\fR to search for entries like \fB\fC/skydns/test/skydns/mx/a\fR etc, and +if there is nothing found on \fB\fC/skydns/test/skydns/mx/\fR, it looks for \fB\fC/skydns/test/skydns/mx\fR to +find entries like \fB\fC/skydns/test/skydns/mx1\fR. + +.PP +This causes two lookups from CoreDNS to etcd in certain cases. + +.SH "EXAMPLES" +.PP +This is the default SkyDNS setup, with everything specified in full: + +.PP +.RS + +.nf +skydns.local { + etcd { + path /skydns + endpoint http://localhost:2379 + } + prometheus + cache + loadbalance +} + +\&. { + forward . 8.8.8.8:53 8.8.4.4:53 + cache +} + +.fi +.RE + +.PP +Or a setup where we use \fB\fC/etc/resolv.conf\fR as the basis for the proxy and the upstream +when resolving external pointing CNAMEs. + +.PP +.RS + +.nf +skydns.local { + etcd { + path /skydns + } + cache +} + +\&. { + forward . /etc/resolv.conf + cache +} + +.fi +.RE + +.PP +Multiple endpoints are supported as well. + +.PP +.RS + +.nf +etcd skydns.local { + endpoint http://localhost:2379 http://localhost:4001 +\&... + +.fi +.RE + +.PP +Before getting started with these examples, please setup \fB\fCetcdctl\fR (with \fB\fCetcdv3\fR API) as explained +here +\[la]https://coreos.com/etcd/docs/latest/dev-guide/interacting_v3.html\[ra]. This will help you to put +sample keys in your etcd server. + +.PP +If you prefer, you can use \fB\fCcurl\fR to populate the \fB\fCetcd\fR server, but with \fB\fCcurl\fR the +endpoint URL depends on the version of \fB\fCetcd\fR. For instance, \fB\fCetcd v3.2\fR or before uses only +[CLIENT-URL]/v3alpha/* while \fB\fCetcd v3.5\fR or later uses [CLIENT-URL]/v3/* . Also, Key and Value must +be base64 encoded in the JSON payload. With \fB\fCetcdctl\fR these details are automatically taken care +of. You can check this document +\[la]https://github.com/coreos/etcd/blob/master/Documentation/dev-guide/api_grpc_gateway.md#notes\[ra] +for details. + +.SS "REVERSE ZONES" +.PP +Reverse zones are supported. You need to make CoreDNS aware of the fact that you are also +authoritative for the reverse. For instance if you want to add the reverse for 10.0.0.0/24, you'll +need to add the zone \fB\fC0.0.10.in-addr.arpa\fR to the list of zones. Showing a snippet of a Corefile: + +.PP +.RS + +.nf +etcd skydns.local 10.0.0.0/24 { +\&... + +.fi +.RE + +.PP +Next you'll need to populate the zone with reverse records, here we add a reverse for +10.0.0.127 pointing to reverse.skydns.local. + +.PP +.RS + +.nf +% etcdctl put /skydns/arpa/in\-addr/10/0/0/127 '{"host":"reverse.skydns.local."}' + +.fi +.RE + +.PP +Querying with dig: + +.PP +.RS + +.nf +% dig @localhost \-x 10.0.0.127 +short +reverse.skydns.local. + +.fi +.RE + +.SS "ZONE NAME AS A RECORD" +.PP +The zone name itself can be used as an \fB\fCA\fR record. This behavior can be achieved by writing special +entries to the ETCD path of your zone. If your zone is named \fB\fCskydns.local\fR for example, you can +create an \fB\fCA\fR record for this zone as follows: + +.PP +.RS + +.nf +% etcdctl put /skydns/local/skydns/ '{"host":"1.1.1.1","ttl":60}' + +.fi +.RE + +.PP +If you query the zone name itself, you will receive the created \fB\fCA\fR record: + +.PP +.RS + +.nf +% dig +short skydns.local @localhost +1.1.1.1 + +.fi +.RE + +.PP +If you would like to use DNS RR for the zone name, you can set the following: + +.PP +.RS + +.nf +% etcdctl put /skydns/local/skydns/x1 '{"host":"1.1.1.1","ttl":60}' +% etcdctl put /skydns/local/skydns/x2 '{"host":"1.1.1.2","ttl":60}' + +.fi +.RE + +.PP +If you query the zone name now, you will get the following response: + +.PP +.RS + +.nf +% dig +short skydns.local @localhost +1.1.1.1 +1.1.1.2 + +.fi +.RE + +.SS "ZONE NAME AS AAAA RECORD" +.PP +If you would like to use \fB\fCAAAA\fR records for the zone name too, you can set the following: + +.PP +.RS + +.nf +% etcdctl put /skydns/local/skydns/x3 '{"host":"2003::8:1","ttl":60}' +% etcdctl put /skydns/local/skydns/x4 '{"host":"2003::8:2","ttl":60}' + +.fi +.RE + +.PP +If you query the zone name for \fB\fCAAAA\fR now, you will get the following response: + +.PP +.RS + +.nf +% dig +short skydns.local AAAA @localhost +2003::8:1 +2003::8:2 + +.fi +.RE + +.SS "SRV RECORD" +.PP +If you would like to use \fB\fCSRV\fR records, you can set the following: + +.PP +.RS + +.nf +% etcdctl put /skydns/local/skydns/x5 '{"host":"skydns\-local.server","ttl":60,"priority":10,"port":8080}' + +.fi +.RE + +.PP +Please notice that the key \fB\fChost\fR is the \fB\fCtarget\fR in \fB\fCSRV\fR, so it should be a domain name. + +.PP +If you query the zone name for \fB\fCSRV\fR now, you will get the following response: + +.PP +.RS + +.nf +% dig +short skydns.local SRV @localhost +10 100 8080 skydns\-local.server. + +.fi +.RE + +.SS "TXT RECORD" +.PP +If you would like to use \fB\fCTXT\fR records, you can set the following: + +.PP +.RS + +.nf +% etcdctl put /skydns/local/skydns/x6 '{"ttl":60,"text":"this is a random text message."}' + +.fi +.RE + +.PP +If you query the zone name for \fB\fCTXT\fR now, you will get the following response: + +.PP +.RS + +.nf +% dig +short skydns.local TXT @localhost +"this is a random text message." + +.fi +.RE + +.SH "SEE ALSO" +.PP +If you want to \fB\fCround robin\fR A and AAAA responses look at the \fIloadbalance\fP plugin. + diff --git a/ag_201_coredns/man/coredns-file.7 b/ag_201_coredns/man/coredns-file.7 new file mode 100644 index 0000000..9ba8a7e --- /dev/null +++ b/ag_201_coredns/man/coredns-file.7 @@ -0,0 +1,167 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-FILE" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIfile\fP - enables serving zone data from an RFC 1035-style master file. + +.SH "DESCRIPTION" +.PP +The \fIfile\fP plugin is used for an "old-style" DNS server. It serves from a preloaded file that exists +on disk contained RFC 1035 styled data. If the zone file contains signatures (i.e., is signed using +DNSSEC), correct DNSSEC answers are returned. Only NSEC is supported! If you use this setup \fIyou\fP +are responsible for re-signing the zonefile. + +.SH "SYNTAX" +.PP +.RS + +.nf +file DBFILE [ZONES...] + +.fi +.RE + +.IP \(bu 4 +\fBDBFILE\fP the database file to read and parse. If the path is relative, the path from the \fIroot\fP +plugin will be prepended to it. +.IP \(bu 4 +\fBZONES\fP zones it should be authoritative for. If empty, the zones from the configuration block +are used. + + +.PP +If you want to round-robin A and AAAA responses look at the \fIloadbalance\fP plugin. + +.PP +.RS + +.nf +file DBFILE [ZONES... ] { + reload DURATION +} + +.fi +.RE + +.IP \(bu 4 +\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. + + +.PP +If you need outgoing zone transfers, take a look at the \fItransfer\fP plugin. + +.SH "EXAMPLES" +.PP +Load the \fB\fCexample.org\fR zone from \fB\fCdb.example.org\fR and allow transfers to the internet, but send +notifies to 10.240.1.1 + +.PP +.RS + +.nf +example.org { + file db.example.org + transfer { + to * 10.240.1.1 + } +} + +.fi +.RE + +.PP +Where \fB\fCdb.example.org\fR would contain RRSets (https://tools.ietf.org/html/rfc7719#section-4 +\[la]https://tools.ietf.org/html/rfc7719#section-4\[ra]) in the +(text) presentation format from RFC 1035: + +.PP +.RS + +.nf +$ORIGIN example.org. +@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2017042745 7200 3600 1209600 3600 + 3600 IN NS a.iana\-servers.net. + 3600 IN NS b.iana\-servers.net. + +www IN A 127.0.0.1 + IN AAAA ::1 + +.fi +.RE + +.PP +Or use a single zone file for multiple zones: + +.PP +.RS + +.nf +\&. { + file example.org.signed example.org example.net + transfer example.org example.net { + to * 10.240.1.1 + } +} + +.fi +.RE + +.PP +Note that if you have a configuration like the following you may run into a problem of the origin +not being correctly recognized: + +.PP +.RS + +.nf +\&. { + file db.example.org +} + +.fi +.RE + +.PP +We omit the origin for the file \fB\fCdb.example.org\fR, so this references the zone in the server block, +which, in this case, is the root zone. Any contents of \fB\fCdb.example.org\fR will then read with that +origin set; this may or may not do what you want. +It's better to be explicit here and specify the correct origin. This can be done in two ways: + +.PP +.RS + +.nf +\&. { + file db.example.org example.org +} + +.fi +.RE + +.PP +Or + +.PP +.RS + +.nf +example.org { + file db.example.org +} + +.fi +.RE + +.SH "SEE ALSO" +.PP +See the \fIloadbalance\fP plugin if you need simple record shuffling. And the \fItransfer\fP plugin for zone +transfers. Lastly the \fIroot\fP plugin can help you specify the location of the zone files. + +.PP +See RFC 1035 +\[la]https://www.rfc-editor.org/rfc/rfc1035.txt\[ra] for more info on how to structure zone +files. + diff --git a/ag_201_coredns/man/coredns-forward.7 b/ag_201_coredns/man/coredns-forward.7 new file mode 100644 index 0000000..c6f608d --- /dev/null +++ b/ag_201_coredns/man/coredns-forward.7 @@ -0,0 +1,326 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-FORWARD" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIforward\fP - facilitates proxying DNS messages to upstream resolvers. + +.SH "DESCRIPTION" +.PP +The \fIforward\fP plugin re-uses already opened sockets to the upstreams. It supports UDP, TCP and +DNS-over-TLS and uses in band health checking. + +.PP +When it detects an error a health check is performed. This checks runs in a loop, performing each +check at a \fI0.5s\fP interval for as long as the upstream reports unhealthy. Once healthy we stop +health checking (until the next error). The health checks use a recursive DNS query (\fB\fC. IN NS\fR) +to get upstream health. Any response that is not a network error (REFUSED, NOTIMPL, SERVFAIL, etc) +is taken as a healthy upstream. The health check uses the same protocol as specified in \fBTO\fP. If +\fB\fCmax_fails\fR is set to 0, no checking is performed and upstreams will always be considered healthy. + +.PP +When \fIall\fP 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). + +.PP +This plugin can only be used once per Server Block. + +.SH "SYNTAX" +.PP +In its most basic form, a simple forwarder uses this syntax: + +.PP +.RS + +.nf +forward FROM TO... + +.fi +.RE + +.IP \(bu 4 +\fBFROM\fP is the base domain to match for the request to be forwarded. +.IP \(bu 4 +\fBTO...\fP are the destination endpoints to forward to. The \fBTO\fP syntax allows you to specify +a protocol, \fB\fCtls://9.9.9.9\fR or \fB\fCdns://\fR (or no protocol) for plain DNS. The number of upstreams is +limited to 15. + + +.PP +Multiple upstreams are randomized (see \fB\fCpolicy\fR) on first use. When a healthy proxy returns an error +during the exchange the next upstream in the list is tried. + +.PP +Extra knobs are available with an expanded syntax: + +.PP +.RS + +.nf +forward FROM TO... { + except IGNORED\_NAMES... + force\_tcp + prefer\_udp + expire DURATION + max\_fails INTEGER + tls CERT KEY CA + tls\_servername NAME + policy random|round\_robin|sequential + health\_check DURATION [no\_rec] + max\_concurrent MAX +} + +.fi +.RE + +.IP \(bu 4 +\fBFROM\fP and \fBTO...\fP as above. +.IP \(bu 4 +\fBIGNORED_NAMES\fP in \fB\fCexcept\fR is a space-separated list of domains to exclude from forwarding. +Requests that match none of these names will be passed through. +.IP \(bu 4 +\fB\fCforce_tcp\fR, use TCP even when the request comes in over UDP. +.IP \(bu 4 +\fB\fCprefer_udp\fR, try first using UDP even when the request comes in over TCP. If response is truncated +(TC flag set in response) then do another attempt over TCP. In case if both \fB\fCforce_tcp\fR and +\fB\fCprefer_udp\fR options specified the \fB\fCforce_tcp\fR takes precedence. +.IP \(bu 4 +\fB\fCmax_fails\fR is the number of subsequent failed health checks that are needed before considering +an upstream to be down. If 0, the upstream will never be marked as down (nor health checked). +Default is 2. +.IP \(bu 4 +\fB\fCexpire\fR \fBDURATION\fP, expire (cached) connections after this time, the default is 10s. +.IP \(bu 4 +\fB\fCtls\fR \fBCERT\fP \fBKEY\fP \fBCA\fP define the TLS properties for TLS connection. From 0 to 3 arguments can be +provided with the meaning as described below + +.RS +.IP \(en 4 +\fB\fCtls\fR - no client authentication is used, and the system CAs are used to verify the server certificate +.IP \(en 4 +\fB\fCtls\fR \fBCA\fP - no client authentication is used, and the file CA is used to verify the server certificate +.IP \(en 4 +\fB\fCtls\fR \fBCERT\fP \fBKEY\fP - client authentication is used with the specified cert/key pair. +The server certificate is verified with the system CAs +.IP \(en 4 +\fB\fCtls\fR \fBCERT\fP \fBKEY\fP \fBCA\fP - client authentication is used with the specified cert/key pair. +The server certificate is verified using the specified CA file + +.RE +.IP \(bu 4 +\fB\fCtls_servername\fR \fBNAME\fP allows you to set a server name in the TLS configuration; for instance 9.9.9.9 +needs this to be set to \fB\fCdns.quad9.net\fR. Multiple upstreams are still allowed in this scenario, +but they have to use the same \fB\fCtls_servername\fR. E.g. mixing 9.9.9.9 (QuadDNS) with 1.1.1.1 +(Cloudflare) will not work. +.IP \(bu 4 +\fB\fCpolicy\fR specifies the policy to use for selecting upstream servers. The default is \fB\fCrandom\fR. + +.RS +.IP \(en 4 +\fB\fCrandom\fR is a policy that implements random upstream selection. +.IP \(en 4 +\fB\fCround_robin\fR is a policy that selects hosts based on round robin ordering. +.IP \(en 4 +\fB\fCsequential\fR is a policy that selects hosts based on sequential ordering. + +.RE +.IP \(bu 4 +\fB\fChealth_check\fR configure the behaviour of health checking of the upstream servers + +.RS +.IP \(en 4 +\fB\fC\fR - use a different duration for health checking, the default duration is 0.5s. +.IP \(en 4 +\fB\fCno_rec\fR - optional argument that sets the RecursionDesired-flag of the dns-query used in health checking to \fB\fCfalse\fR. +The flag is default \fB\fCtrue\fR. + +.RE +.IP \(bu 4 +\fB\fCmax_concurrent\fR \fBMAX\fP will limit the number of concurrent queries to \fBMAX\fP. Any new query that would +raise the number of concurrent queries above the \fBMAX\fP will result in a REFUSED response. This +response does not count as a health failure. When choosing a value for \fBMAX\fP, pick a number +at least greater than the expected \fIupstream query rate\fP * \fIlatency\fP of the upstream servers. +As an upper bound for \fBMAX\fP, consider that each concurrent query will use about 2kb of memory. + + +.PP +Also note the TLS config is "global" for the whole forwarding proxy if you need a different +\fB\fCtls-name\fR for different upstreams you're out of luck. + +.PP +On each endpoint, the timeouts for communication are set as follows: + +.IP \(bu 4 +The dial timeout by default is 30s, and can decrease automatically down to 100ms based on early results. +.IP \(bu 4 +The read timeout is static at 2s. + + +.SH "METADATA" +.PP +The forward plugin will publish the following metadata, if the \fImetadata\fP +plugin is also enabled: + +.IP \(bu 4 +\fB\fCforward/upstream\fR: the upstream used to forward the request + + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metric are exported: + +.IP \(bu 4 +\fB\fCcoredns_forward_requests_total{to}\fR - query count per upstream. +.IP \(bu 4 +\fB\fCcoredns_forward_responses_total{to}\fR - Counter of responses received per upstream. +.IP \(bu 4 +\fB\fCcoredns_forward_request_duration_seconds{to, rcode, type}\fR - duration per upstream, RCODE, type +.IP \(bu 4 +\fB\fCcoredns_forward_responses_total{to, rcode}\fR - count of RCODEs per upstream. +.IP \(bu 4 +\fB\fCcoredns_forward_healthcheck_failures_total{to}\fR - number of failed health checks per upstream. +.IP \(bu 4 +\fB\fCcoredns_forward_healthcheck_broken_total{}\fR - counter of when all upstreams are unhealthy, +and we are randomly (this always uses the \fB\fCrandom\fR policy) spraying to an upstream. +.IP \(bu 4 +\fB\fCcoredns_forward_max_concurrent_rejects_total{}\fR - counter of the number of queries rejected because the +number of concurrent queries were at maximum. +.IP \(bu 4 +\fB\fCcoredns_forward_conn_cache_hits_total{to, proto}\fR - counter of connection cache hits per upstream and protocol. +.IP \(bu 4 +\fB\fCcoredns_forward_conn_cache_misses_total{to, proto}\fR - counter of connection cache misses per upstream and protocol. +Where \fB\fCto\fR is one of the upstream servers (\fBTO\fP from the config), \fB\fCrcode\fR is the returned RCODE +from the upstream, \fB\fCproto\fR is the transport protocol like \fB\fCudp\fR, \fB\fCtcp\fR, \fB\fCtcp-tls\fR. + + +.SH "EXAMPLES" +.PP +Proxy all requests within \fB\fCexample.org.\fR to a nameserver running on a different port: + +.PP +.RS + +.nf +example.org { + forward . 127.0.0.1:9005 +} + +.fi +.RE + +.PP +Load balance all requests between three resolvers, one of which has a IPv6 address. + +.PP +.RS + +.nf +\&. { + forward . 10.0.0.10:53 10.0.0.11:1053 [2003::1]:53 +} + +.fi +.RE + +.PP +Forward everything except requests to \fB\fCexample.org\fR + +.PP +.RS + +.nf +\&. { + forward . 10.0.0.10:1234 { + except example.org + } +} + +.fi +.RE + +.PP +Proxy everything except \fB\fCexample.org\fR using the host's \fB\fCresolv.conf\fR's nameservers: + +.PP +.RS + +.nf +\&. { + forward . /etc/resolv.conf { + except example.org + } +} + +.fi +.RE + +.PP +Proxy all requests to 9.9.9.9 using the DNS-over-TLS (DoT) protocol, and cache every answer for up to 30 +seconds. Note the \fB\fCtls_servername\fR is mandatory if you want a working setup, as 9.9.9.9 can't be +used in the TLS negotiation. Also set the health check duration to 5s to not completely swamp the +service with health checks. + +.PP +.RS + +.nf +\&. { + forward . tls://9.9.9.9 { + tls\_servername dns.quad9.net + health\_check 5s + } + cache 30 +} + +.fi +.RE + +.PP +Or with multiple upstreams from the same provider + +.PP +.RS + +.nf +\&. { + forward . tls://1.1.1.1 tls://1.0.0.1 { + tls\_servername cloudflare\-dns.com + health\_check 5s + } + cache 30 +} + +.fi +.RE + +.PP +Or when you have multiple DoT upstreams with different \fB\fCtls_servername\fRs, you can do the following: + +.PP +.RS + +.nf +\&. { + forward . 127.0.0.1:5301 127.0.0.1:5302 +} + +\&.:5301 { + forward . 8.8.8.8 8.8.4.4 { + tls\_servername dns.google + } +} + +\&.:5302 { + forward . 1.1.1.1 1.0.0.1 { + tls\_servername cloudflare\-dns.com + } +} + +.fi +.RE + +.SH "SEE ALSO" +.PP +RFC 7858 +\[la]https://tools.ietf.org/html/rfc7858\[ra] for DNS over TLS. + diff --git a/ag_201_coredns/man/coredns-geoip.7 b/ag_201_coredns/man/coredns-geoip.7 new file mode 100644 index 0000000..2810425 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/man/coredns-grpc.7 b/ag_201_coredns/man/coredns-grpc.7 new file mode 100644 index 0000000..d286cda --- /dev/null +++ b/ag_201_coredns/man/coredns-grpc.7 @@ -0,0 +1,205 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-GRPC" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIgrpc\fP - facilitates proxying DNS messages to upstream resolvers via gRPC protocol. + +.SH "DESCRIPTION" +.PP +The \fIgrpc\fP plugin supports gRPC and TLS. + +.PP +This plugin can only be used once per Server Block. + +.SH "SYNTAX" +.PP +In its most basic form: + +.PP +.RS + +.nf +grpc FROM TO... + +.fi +.RE + +.IP \(bu 4 +\fBFROM\fP is the base domain to match for the request to be proxied. +.IP \(bu 4 +\fBTO...\fP are the destination endpoints to proxy to. The number of upstreams is +limited to 15. + + +.PP +Multiple upstreams are randomized (see \fB\fCpolicy\fR) on first use. When a proxy returns an error +the next upstream in the list is tried. + +.PP +Extra knobs are available with an expanded syntax: + +.PP +.RS + +.nf +grpc FROM TO... { + except IGNORED\_NAMES... + tls CERT KEY CA + tls\_servername NAME + policy random|round\_robin|sequential +} + +.fi +.RE + +.IP \(bu 4 +\fBFROM\fP and \fBTO...\fP as above. +.IP \(bu 4 +\fBIGNORED_NAMES\fP in \fB\fCexcept\fR is a space-separated list of domains to exclude from proxying. +Requests that match none of these names will be passed through. +.IP \(bu 4 +\fB\fCtls\fR \fBCERT\fP \fBKEY\fP \fBCA\fP define the TLS properties for TLS connection. From 0 to 3 arguments can be +provided with the meaning as described below + +.RS +.IP \(en 4 +\fB\fCtls\fR - no client authentication is used, and the system CAs are used to verify the server certificate +.IP \(en 4 +\fB\fCtls\fR \fBCA\fP - no client authentication is used, and the file CA is used to verify the server certificate +.IP \(en 4 +\fB\fCtls\fR \fBCERT\fP \fBKEY\fP - client authentication is used with the specified cert/key pair. +The server certificate is verified with the system CAs +.IP \(en 4 +\fB\fCtls\fR \fBCERT\fP \fBKEY\fP \fBCA\fP - client authentication is used with the specified cert/key pair. +The server certificate is verified using the specified CA file + +.RE +.IP \(bu 4 +\fB\fCtls_servername\fR \fBNAME\fP allows you to set a server name in the TLS configuration; for instance 9.9.9.9 +needs this to be set to \fB\fCdns.quad9.net\fR. Multiple upstreams are still allowed in this scenario, +but they have to use the same \fB\fCtls_servername\fR. E.g. mixing 9.9.9.9 (QuadDNS) with 1.1.1.1 +(Cloudflare) will not work. +.IP \(bu 4 +\fB\fCpolicy\fR specifies the policy to use for selecting upstream servers. The default is \fB\fCrandom\fR. + + +.PP +Also note the TLS config is "global" for the whole grpc proxy if you need a different +\fB\fCtls-name\fR for different upstreams you're out of luck. + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metric are exported: + +.IP \(bu 4 +\fB\fCcoredns_grpc_request_duration_seconds{to}\fR - duration per upstream interaction. +.IP \(bu 4 +\fB\fCcoredns_grpc_requests_total{to}\fR - query count per upstream. +.IP \(bu 4 +\fB\fCcoredns_grpc_responses_total{to, rcode}\fR - count of RCODEs per upstream. +and we are randomly (this always uses the \fB\fCrandom\fR policy) spraying to an upstream. + + +.SH "EXAMPLES" +.PP +Proxy all requests within \fB\fCexample.org.\fR to a nameserver running on a different port: + +.PP +.RS + +.nf +example.org { + grpc . 127.0.0.1:9005 +} + +.fi +.RE + +.PP +Load balance all requests between three resolvers, one of which has a IPv6 address. + +.PP +.RS + +.nf +\&. { + grpc . 10.0.0.10:53 10.0.0.11:1053 [2003::1]:53 +} + +.fi +.RE + +.PP +Forward everything except requests to \fB\fCexample.org\fR + +.PP +.RS + +.nf +\&. { + grpc . 10.0.0.10:1234 { + except example.org + } +} + +.fi +.RE + +.PP +Proxy everything except \fB\fCexample.org\fR using the host's \fB\fCresolv.conf\fR's nameservers: + +.PP +.RS + +.nf +\&. { + grpc . /etc/resolv.conf { + except example.org + } +} + +.fi +.RE + +.PP +Proxy all requests to 9.9.9.9 using the TLS protocol, and cache every answer for up to 30 +seconds. Note the \fB\fCtls_servername\fR is mandatory if you want a working setup, as 9.9.9.9 can't be +used in the TLS negotiation. + +.PP +.RS + +.nf +\&. { + grpc . 9.9.9.9 { + tls\_servername dns.quad9.net + } + cache 30 +} + +.fi +.RE + +.PP +Or with multiple upstreams from the same provider + +.PP +.RS + +.nf +\&. { + grpc . 1.1.1.1 1.0.0.1 { + tls\_servername cloudflare\-dns.com + } + cache 30 +} + +.fi +.RE + +.SH "BUGS" +.PP +The TLS config is global for the whole grpc proxy if you need a different \fB\fCtls_servername\fR for +different upstreams you're out of luck. + diff --git a/ag_201_coredns/man/coredns-header.7 b/ag_201_coredns/man/coredns-header.7 new file mode 100644 index 0000000..c0e76e4 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/man/coredns-health.7 b/ag_201_coredns/man/coredns-health.7 new file mode 100644 index 0000000..388d4e9 --- /dev/null +++ b/ag_201_coredns/man/coredns-health.7 @@ -0,0 +1,115 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-HEALTH" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIhealth\fP - enables a health check endpoint. + +.SH "DESCRIPTION" +.PP +Enabled process wide health endpoint. When CoreDNS is up and running this returns a 200 OK HTTP +status code. The health is exported, by default, on port 8080/health. + +.SH "SYNTAX" +.PP +.RS + +.nf +health [ADDRESS] + +.fi +.RE + +.PP +Optionally takes an address; the default is \fB\fC:8080\fR. The health path is fixed to \fB\fC/health\fR. The +health endpoint returns a 200 response code and the word "OK" when this server is healthy. + +.PP +An extra option can be set with this extended syntax: + +.PP +.RS + +.nf +health [ADDRESS] { + lameduck DURATION +} + +.fi +.RE + +.IP \(bu 4 +Where \fB\fClameduck\fR will delay shutdown for \fBDURATION\fP. /health will still answer 200 OK. +Note: The \fIready\fP plugin will not answer OK while CoreDNS is in lameduck mode prior to shutdown. + + +.PP +If you have multiple Server Blocks, \fIhealth\fP 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: + +.PP +.RS + +.nf +com { + whoami + health :8080 +} + +net { + erratic + health :8081 +} + +.fi +.RE + +.PP +Doing this is supported but both endpoints ":8080" and ":8081" will export the exact same health. + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metric is exported: + +.IP \(bu 4 +\fB\fCcoredns_health_request_duration_seconds{}\fR - duration to process a HTTP query to the local +\fB\fC/health\fR endpoint. As this 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. + + +.PP +Note that this metric \fIdoes not\fP have a \fB\fCserver\fR label, because being overloaded is a symptom of +the running process, \fInot\fP a specific server. + +.SH "EXAMPLES" +.PP +Run another health endpoint on http://localhost:8091 +\[la]http://localhost:8091\[ra]. + +.PP +.RS + +.nf +\&. { + health localhost:8091 +} + +.fi +.RE + +.PP +Set a lameduck duration of 1 second: + +.PP +.RS + +.nf +\&. { + health localhost:8092 { + lameduck 1s + } +} + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-hosts.7 b/ag_201_coredns/man/coredns-hosts.7 new file mode 100644 index 0000000..72bf9a9 --- /dev/null +++ b/ag_201_coredns/man/coredns-hosts.7 @@ -0,0 +1,175 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-HOSTS" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIhosts\fP - enables serving zone data from a \fB\fC/etc/hosts\fR style file. + +.SH "DESCRIPTION" +.PP +The \fIhosts\fP plugin is useful for serving zones from a \fB\fC/etc/hosts\fR file. It serves from a preloaded +file that exists on disk. It checks the file for changes and updates the zones accordingly. This +plugin only supports A, AAAA, and PTR records. The hosts plugin can be used with readily +available hosts files that block access to advertising servers. + +.PP +The plugin reloads the content of the hosts file every 5 seconds. Upon reload, CoreDNS will use the +new definitions. Should the file be deleted, any inlined content will continue to be served. When +the file is restored, it will then again be used. + +.PP +If you want to pass the request to the rest of the plugin chain if there is no match in the \fIhosts\fP +plugin, you must specify the \fB\fCfallthrough\fR option. + +.PP +This plugin can only be used once per Server Block. + +.SH "THE HOSTS FILE" +.PP +Commonly the entries are of the form \fB\fCIP_address canonical_hostname [aliases...]\fR as explained by +the hosts(5) man page. + +.PP +Examples: + +.PP +.RS + +.nf +127.0.0.1 localhost +192.168.1.10 example.com example + +::1 localhost ip6\-localhost ip6\-loopback +fdfc:a744:27b5:3b0e::1 example.com example + +.fi +.RE + +.SS "PTR RECORDS" +.PP +PTR records for reverse lookups are generated automatically by CoreDNS (based on the hosts file +entries) and cannot be created manually. + +.SH "SYNTAX" +.PP +.RS + +.nf +hosts [FILE [ZONES...]] { + [INLINE] + ttl SECONDS + no\_reverse + reload DURATION + fallthrough [ZONES...] +} + +.fi +.RE + +.IP \(bu 4 +\fBFILE\fP the hosts file to read and parse. If the path is relative the path from the \fIroot\fP +plugin will be prepended to it. Defaults to /etc/hosts if omitted. We scan the file for changes +every 5 seconds. +.IP \(bu 4 +\fBZONES\fP zones it should be authoritative for. If empty, the zones from the configuration block +are used. +.IP \(bu 4 +\fBINLINE\fP the hosts file contents inlined in Corefile. If there are any lines before fallthrough +then all of them will be treated as the additional content for hosts file. The specified hosts +file path will still be read but entries will be overridden. +.IP \(bu 4 +\fB\fCttl\fR change the DNS TTL of the records generated (forward and reverse). The default is 3600 seconds (1 hour). +.IP \(bu 4 +\fB\fCreload\fR change the period between each hostsfile reload. A time of zero seconds disables the +feature. Examples of valid durations: "300ms", "1.5h" or "2h45m". See Go's +time +\[la]https://godoc.org/time\[ra]. package. +.IP \(bu 4 +\fB\fCno_reverse\fR disable the automatic generation of the \fB\fCin-addr.arpa\fR or \fB\fCip6.arpa\fR entries for the hosts +.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. + + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metrics are exported: + +.IP \(bu 4 +\fB\fCcoredns_hosts_entries{}\fR - The combined number of entries in hosts and Corefile. +.IP \(bu 4 +\fB\fCcoredns_hosts_reload_timestamp_seconds{}\fR - The timestamp of the last reload of hosts file. + + +.SH "EXAMPLES" +.PP +Load \fB\fC/etc/hosts\fR file. + +.PP +.RS + +.nf +\&. { + hosts +} + +.fi +.RE + +.PP +Load \fB\fCexample.hosts\fR file in the current directory. + +.PP +.RS + +.nf +\&. { + hosts example.hosts +} + +.fi +.RE + +.PP +Load example.hosts file and only serve example.org and example.net from it and fall through to the +next plugin if query doesn't match. + +.PP +.RS + +.nf +\&. { + hosts example.hosts example.org example.net { + fallthrough + } +} + +.fi +.RE + +.PP +Load hosts file inlined in Corefile. + +.PP +.RS + +.nf +example.hosts example.org { + hosts { + 10.0.0.1 example.org + fallthrough + } + whoami +} + +.fi +.RE + +.SH "SEE ALSO" +.PP +The form of the entries in the \fB\fC/etc/hosts\fR file are based on IETF RFC 952 +\[la]https://tools.ietf.org/html/rfc952\[ra] which was updated by IETF RFC 1123 +\[la]https://tools.ietf.org/html/rfc1123\[ra]. + diff --git a/ag_201_coredns/man/coredns-import.7 b/ag_201_coredns/man/coredns-import.7 new file mode 100644 index 0000000..dda17a0 --- /dev/null +++ b/ag_201_coredns/man/coredns-import.7 @@ -0,0 +1,110 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-IMPORT" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIimport\fP - includes files or references snippets from a Corefile. + +.SH "DESCRIPTION" +.PP +The \fIimport\fP plugin can be used to include files into the main configuration. Another use is to +reference predefined snippets. Both can help to avoid some duplication. + +.PP +This is a unique plugin in that \fIimport\fP can appear outside of a server block. In other words, it +can appear at the top of a Corefile where an address would normally be. + +.SH "SYNTAX" +.PP +.RS + +.nf +import PATTERN + +.fi +.RE + +.IP \(bu 4 +\fBPATTERN\fP is the file, glob pattern (\fB\fC*\fR) or snippet to include. Its contents will replace +this line, as if that file's contents appeared here to begin with. + + +.SH "FILES" +.PP +You can use \fIimport\fP to include a file or files. This file's location is relative to the +Corefile's location. It is an error if a specific file cannot be found, but an empty glob pattern is +not an error. + +.SH "SNIPPETS" +.PP +You can define snippets to be reused later in your Corefile by defining a block with a single-token +label surrounded by parentheses: + +.PP +.RS + +.nf +(mysnippet) { + ... +} + +.fi +.RE + +.PP +Then you can invoke the snippet with \fIimport\fP: + +.PP +.RS + +.nf +import mysnippet + +.fi +.RE + +.SH "EXAMPLES" +.PP +Import a shared configuration: + +.PP +.RS + +.nf +\&. { + import config/common.conf +} + +.fi +.RE + +.PP +Where \fB\fCconfig/common.conf\fR contains: + +.PP +.RS + +.nf +prometheus +errors +log + +.fi +.RE + +.PP +This imports files found in the zones directory: + +.PP +.RS + +.nf +import ../zones/* + +.fi +.RE + +.SH "SEE ALSO" +.PP +See corefile(5). + diff --git a/ag_201_coredns/man/coredns-k8s_external.7 b/ag_201_coredns/man/coredns-k8s_external.7 new file mode 100644 index 0000000..f0b902e --- /dev/null +++ b/ag_201_coredns/man/coredns-k8s_external.7 @@ -0,0 +1,130 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-K8S_EXTERNAL" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIk8s_external\fP - resolves load balancer and external IPs from outside Kubernetes clusters. + +.SH "DESCRIPTION" +.PP +This plugin allows an additional zone to resolve the external IP address(es) of a Kubernetes +service. This plugin is only useful if the \fIkubernetes\fP plugin is also loaded. + +.PP +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. + +.PP +By default the apex of the zone will look like the following (assuming the zone used is \fB\fCexample.org\fR): + +.PP +.RS + +.nf +example.org. 5 IN SOA ns1.dns.example.org. hostmaster.example.org. ( + 12345 ; serial + 14400 ; refresh (4 hours) + 3600 ; retry (1 hour) + 604800 ; expire (1 week) + 5 ; minimum (4 hours) + ) +example.org 5 IN NS ns1.dns.example.org. + +ns1.dns.example.org. 5 IN A .... +ns1.dns.example.org. 5 IN AAAA .... + +.fi +.RE + +.PP +Note that we use the \fB\fCdns\fR subdomain for the records DNS needs (see the \fB\fCapex\fR directive). Also +note the SOA's serial number is static. The IP addresses of the nameserver records are those of the +CoreDNS service. + +.PP +The \fIk8s_external\fP plugin handles the subdomain \fB\fCdns\fR and the apex of the zone itself; all other +queries are resolved to addresses in the cluster. + +.SH "SYNTAX" +.PP +.RS + +.nf +k8s\_external [ZONE...] + +.fi +.RE + +.IP \(bu 4 +\fBZONES\fP zones \fIk8s_external\fP should be authoritative for. + + +.PP +If you want to change the apex domain or use a different TTL for the returned records you can use +this extended syntax. + +.PP +.RS + +.nf +k8s\_external [ZONE...] { + apex APEX + ttl TTL +} + +.fi +.RE + +.IP \(bu 4 +\fBAPEX\fP is the name (DNS label) to use for the apex records; it defaults to \fB\fCdns\fR. +.IP \(bu 4 +\fB\fCttl\fR allows you to set a custom \fBTTL\fP for responses. The default is 5 (seconds). + + +.SH "EXAMPLES" +.PP +Enable names under \fB\fCexample.org\fR to be resolved to in-cluster DNS addresses. + +.PP +.RS + +.nf +\&. { + kubernetes cluster.local + k8s\_external example.org +} + +.fi +.RE + +.PP +With the Corefile above, the following Service will get an \fB\fCA\fR record for \fB\fCtest.default.example.org\fR with the IP address \fB\fC192.168.200.123\fR. + +.PP +.RS + +.nf +apiVersion: v1 +kind: Service +metadata: + name: test + namespace: default +spec: + clusterIP: None + externalIPs: + \- 192.168.200.123 + type: ClusterIP + +.fi +.RE + +.PP +For some background see resolve external IP address +\[la]https://github.com/kubernetes/dns/issues/242\[ra]. +And A records for services with Load Balancer IP +\[la]https://github.com/coredns/coredns/issues/1851\[ra]. + +.PP +PTR queries for the reverse zone is not supported. + diff --git a/ag_201_coredns/man/coredns-kubernetes.7 b/ag_201_coredns/man/coredns-kubernetes.7 new file mode 100644 index 0000000..078c73d --- /dev/null +++ b/ag_201_coredns/man/coredns-kubernetes.7 @@ -0,0 +1,352 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-KUBERNETES" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIkubernetes\fP - enables reading zone data from a Kubernetes cluster. + +.SH "DESCRIPTION" +.PP +This plugin implements the Kubernetes DNS-Based Service Discovery +Specification +\[la]https://github.com/kubernetes/dns/blob/master/docs/specification.md\[ra]. + +.PP +CoreDNS running the kubernetes plugin can be used as a replacement for kube-dns in a kubernetes +cluster. See the deployment +\[la]https://github.com/coredns/deployment\[ra] repository for details on how +to deploy CoreDNS in Kubernetes +\[la]https://github.com/coredns/deployment/tree/master/kubernetes\[ra]. + +.PP +stubDomains and upstreamNameservers +\[la]https://kubernetes.io/blog/2017/04/configuring-private-dns-zones-upstream-nameservers-kubernetes/\[ra] +are implemented via the \fIforward\fP plugin. See the examples below. + +.PP +This plugin can only be used once per Server Block. + +.SH "SYNTAX" +.PP +.RS + +.nf +kubernetes [ZONES...] + +.fi +.RE + +.PP +With only the plugin specified, the \fIkubernetes\fP plugin will default to the zone specified in +the server's block. It will handle all queries in that zone and connect to Kubernetes in-cluster. It +will not provide PTR records for services or A records for pods. If \fBZONES\fP is used it specifies +all the zones the plugin should be authoritative for. + +.PP +.RS + +.nf +kubernetes [ZONES...] { + endpoint URL + tls CERT KEY CACERT + kubeconfig KUBECONFIG [CONTEXT] + namespaces NAMESPACE... + labels EXPRESSION + pods POD\-MODE + endpoint\_pod\_names + ttl TTL + noendpoints + fallthrough [ZONES...] + ignore empty\_service +} + +.fi +.RE + +.IP \(bu 4 +\fB\fCendpoint\fR specifies the \fBURL\fP for a remote k8s API endpoint. +If omitted, it will connect to k8s in-cluster using the cluster service account. +.IP \(bu 4 +\fB\fCtls\fR \fBCERT\fP \fBKEY\fP \fBCACERT\fP are the TLS cert, key and the CA cert file names for remote k8s connection. +This option is ignored if connecting in-cluster (i.e. endpoint is not specified). +.IP \(bu 4 +\fB\fCkubeconfig\fR \fBKUBECONFIG [CONTEXT]\fP authenticates the connection to a remote k8s cluster using a kubeconfig file. +\fB[CONTEXT]\fP is optional, if not set, then the current context specified in kubeconfig will be used. +It supports TLS, username and password, or token-based authentication. +This option is ignored if connecting in-cluster (i.e., the endpoint is not specified). +.IP \(bu 4 +\fB\fCnamespaces\fR \fBNAMESPACE [NAMESPACE...]\fP only exposes the k8s namespaces listed. +If this option is omitted all namespaces are exposed +.IP \(bu 4 +\fB\fCnamespace_labels\fR \fBEXPRESSION\fP only expose the records for Kubernetes namespaces that match this label selector. +The label selector syntax is described in the +Kubernetes User Guide - Labels +\[la]https://kubernetes.io/docs/user-guide/labels/\[ra]. An example that +only exposes namespaces labeled as "istio-injection=enabled", would use: +\fB\fClabels istio-injection=enabled\fR. +.IP \(bu 4 +\fB\fClabels\fR \fBEXPRESSION\fP only exposes the records for Kubernetes objects that match this label selector. +The label selector syntax is described in the +Kubernetes User Guide - Labels +\[la]https://kubernetes.io/docs/user-guide/labels/\[ra]. An example that +only exposes objects labeled as "application=nginx" in the "staging" or "qa" environments, would +use: \fB\fClabels environment in (staging, qa),application=nginx\fR. +.IP \(bu 4 +\fB\fCpods\fR \fBPOD-MODE\fP sets the mode for handling IP-based pod A records, e.g. +\fB\fC1-2-3-4.ns.pod.cluster.local. in A 1.2.3.4\fR. +This option is provided to facilitate use of SSL certs when connecting directly to pods. Valid +values for \fBPOD-MODE\fP: + +.RS +.IP \(en 4 +\fB\fCdisabled\fR: Default. Do not process pod requests, always returning \fB\fCNXDOMAIN\fR +.IP \(en 4 +\fB\fCinsecure\fR: Always return an A record with IP from request (without checking k8s). This option +is vulnerable to abuse if used maliciously in conjunction with wildcard SSL certs. This +option is provided for backward compatibility with kube-dns. +.IP \(en 4 +\fB\fCverified\fR: Return an A record if there exists a pod in same namespace with matching IP. This +option requires substantially more memory than in insecure mode, since it will maintain a watch +on all pods. + +.RE +.IP \(bu 4 +\fB\fCendpoint_pod_names\fR uses the pod name of the pod targeted by the endpoint as +the endpoint name in A records, e.g., +\fB\fCendpoint-name.my-service.namespace.svc.cluster.local. in A 1.2.3.4\fR +By default, the endpoint-name name selection is as follows: Use the hostname +of the endpoint, or if hostname is not set, use the dashed form of the endpoint +IP address (e.g., \fB\fC1-2-3-4.my-service.namespace.svc.cluster.local.\fR) +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. +.IP \(bu 4 +\fB\fCttl\fR 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. +.IP \(bu 4 +\fB\fCnoendpoints\fR will turn off the serving of endpoint records by disabling the watch on endpoints. +All endpoint queries and headless service queries will result in an NXDOMAIN. +.IP \(bu 4 +\fB\fCfallthrough\fR \fB[ZONES...]\fP If a query for a record in the zones for which the plugin is authoritative +results in NXDOMAIN, normally that is what the response will be. However, if you specify this option, +the query will instead be passed on down the plugin chain, which can include another plugin to handle +the query. 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. +.IP \(bu 4 +\fB\fCignore empty_service\fR 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. + + +.PP +Enabling zone transfer is done by using the \fItransfer\fP plugin. + +.SH "READY" +.PP +This plugin reports readiness to the ready plugin. This will happen after it has synced to the +Kubernetes API. + +.SH "EXAMPLES" +.PP +Handle all queries in the \fB\fCcluster.local\fR zone. Connect to Kubernetes in-cluster. Also handle all +\fB\fCin-addr.arpa\fR \fB\fCPTR\fR requests for \fB\fC10.0.0.0/17\fR . Verify the existence of pods when answering pod +requests. + +.PP +.RS + +.nf +10.0.0.0/17 cluster.local { + kubernetes { + pods verified + } +} + +.fi +.RE + +.PP +Or you can selectively expose some namespaces: + +.PP +.RS + +.nf +kubernetes cluster.local { + namespaces test staging +} + +.fi +.RE + +.PP +Connect to Kubernetes with CoreDNS running outside the cluster: + +.PP +.RS + +.nf +kubernetes cluster.local { + endpoint https://k8s\-endpoint:8443 + tls cert key cacert +} + +.fi +.RE + +.SH "STUBDOMAINS AND UPSTREAMNAMESERVERS" +.PP +Here we use the \fIforward\fP plugin to implement a stubDomain that forwards \fB\fCexample.local\fR to the nameserver \fB\fC10.100.0.10:53\fR. +Also configured is an upstreamNameserver \fB\fC8.8.8.8:53\fR that will be used for resolving names that do not fall in \fB\fCcluster.local\fR +or \fB\fCexample.local\fR. + +.PP +.RS + +.nf +cluster.local:53 { + kubernetes cluster.local +} +example.local { + forward . 10.100.0.10:53 +} + +\&. { + forward . 8.8.8.8:53 +} + +.fi +.RE + +.PP +The configuration above represents the following Kube-DNS stubDomains and upstreamNameservers configuration. + +.PP +.RS + +.nf +stubDomains: | + {“example.local”: [“10.100.0.10:53”]} +upstreamNameservers: | + [“8.8.8.8:53”] + +.fi +.RE + +.SH "AUTOPATH" +.PP +The \fIkubernetes\fP plugin can be used in conjunction with the \fIautopath\fP plugin. Using this +feature enables server-side domain search path completion in Kubernetes clusters. Note: \fB\fCpods\fR must +be set to \fB\fCverified\fR for this to function properly. Furthermore, the remote IP address in the DNS +packet received by CoreDNS must be the IP address of the Pod that sent the request. + +.PP +.RS + +.nf +cluster.local { + autopath @kubernetes + kubernetes { + pods verified + } +} + +.fi +.RE + +.SH "WILDCARDS" +.PP +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: + +.IP \(bu 4 +\fIendpoint\fP in an \fB\fCA\fR record request: \fIendpoint\fP.service.namespace.svc.zone, e.g., \fB\fC*.nginx.ns.svc.cluster.local\fR +.IP \(bu 4 +\fIservice\fP in an \fB\fCA\fR record request: \fIservice\fP.namespace.svc.zone, e.g., \fB\fC*.ns.svc.cluster.local\fR +.IP \(bu 4 +\fInamespace\fP in an \fB\fCA\fR record request: service.\fInamespace\fP.svc.zone, e.g., \fB\fCnginx.*.svc.cluster.local\fR +.IP \(bu 4 +\fIport and/or protocol\fP in an \fB\fCSRV\fR request: \fBport_.\fPprotocol_.service.namespace.svc.zone., +e.g., \fB\fC_http.*.service.ns.svc.cluster.local\fR +.IP \(bu 4 +multiple wildcards are allowed in a single query, e.g., \fB\fCA\fR Request \fB\fC*.*.svc.zone.\fR or \fB\fCSRV\fR request \fB\fC*.*.*.*.svc.zone.\fR + + +.PP +For example, wildcards can be used to resolve all Endpoints for a Service as \fB\fCA\fR records. e.g.: \fB\fC*.service.ns.svc.myzone.local\fR will return the Endpoint IPs in the Service \fB\fCservice\fR in namespace \fB\fCdefault\fR: + +.PP +.RS + +.nf +*.service.default.svc.cluster.local. 5 IN A 192.168.10.10 +*.service.default.svc.cluster.local. 5 IN A 192.168.25.15 + +.fi +.RE + +.SH "METADATA" +.PP +The kubernetes plugin will publish the following metadata, if the \fImetadata\fP +plugin is also enabled: + +.IP \(bu 4 +\fB\fCkubernetes/endpoint\fR: the endpoint name in the query +.IP \(bu 4 +\fB\fCkubernetes/kind\fR: the resource kind (pod or svc) in the query +.IP \(bu 4 +\fB\fCkubernetes/namespace\fR: the namespace in the query +.IP \(bu 4 +\fB\fCkubernetes/port-name\fR: the port name in an SRV query +.IP \(bu 4 +\fB\fCkubernetes/protocol\fR: the protocol in an SRV query +.IP \(bu 4 +\fB\fCkubernetes/service\fR: the service name in the query +.IP \(bu 4 +\fB\fCkubernetes/client-namespace\fR: the client pod's namespace (see requirements below) +.IP \(bu 4 +\fB\fCkubernetes/client-pod-name\fR: the client pod's name (see requirements below) + + +.PP +The \fB\fCkubernetes/client-namespace\fR and \fB\fCkubernetes/client-pod-name\fR metadata work by reconciling the +client IP address in the DNS request packet to a known pod IP address. Therefore the following is required: + * \fB\fCpods verified\fR mode must be enabled + * the remote IP address in the DNS packet received by CoreDNS must be the IP address + of the Pod that sent the request. + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metrics are exported: + +.IP \(bu 4 +\fB\fCcoredns_kubernetes_dns_programming_duration_seconds{service_kind}\fR - Exports the +DNS programming latency SLI +\[la]https://github.com/kubernetes/community/blob/master/sig-scalability/slos/dns_programming_latency.md\[ra]. +The metrics has the \fB\fCservice_kind\fR label that identifies the kind of the +kubernetes service +\[la]https://kubernetes.io/docs/concepts/services-networking/service\[ra]. +It may take one of the three values: + +.RS +.IP \(en 4 +\fB\fCcluster_ip\fR +.IP \(en 4 +\fB\fCheadless_with_selector\fR +.IP \(en 4 +\fB\fCheadless_without_selector\fR + +.RE + + +.SH "BUGS" +.PP +The duration metric only supports the "headless_with_selector" service currently. + +.SH "SEE ALSO" +.PP +See the \fIautopath\fP plugin to enable search path optimizations. And use the \fItransfer\fP plugin to +enable outgoing zone transfers. + diff --git a/ag_201_coredns/man/coredns-loadbalance.7 b/ag_201_coredns/man/coredns-loadbalance.7 new file mode 100644 index 0000000..d3095cb --- /dev/null +++ b/ag_201_coredns/man/coredns-loadbalance.7 @@ -0,0 +1,48 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-LOADBALANCE" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIloadbalance\fP - randomizes the order of A, AAAA and MX records. + +.SH "DESCRIPTION" +.PP +The \fIloadbalance\fP will act as a round-robin DNS load balancer by randomizing the order of A, AAAA, +and MX records in the answer. + +.PP +See Wikipedia +\[la]https://en.wikipedia.org/wiki/Round-robin_DNS\[ra] about the pros and cons of this +setup. It will take care to sort any CNAMEs before any address records, because some stub resolver +implementations (like glibc) are particular about that. + +.SH "SYNTAX" +.PP +.RS + +.nf +loadbalance [POLICY] + +.fi +.RE + +.IP \(bu 4 +\fBPOLICY\fP is how to balance. The default, and only option, is "round_robin". + + +.SH "EXAMPLES" +.PP +Load balance replies coming back from Google Public DNS: + +.PP +.RS + +.nf +\&. { + loadbalance round\_robin + forward . 8.8.8.8 8.8.4.4 +} + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-local.7 b/ag_201_coredns/man/coredns-local.7 new file mode 100644 index 0000000..6869a5b --- /dev/null +++ b/ag_201_coredns/man/coredns-local.7 @@ -0,0 +1,67 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-LOCAL" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIlocal\fP - respond to local names. + +.SH "DESCRIPTION" +.PP +\fIlocal\fP will respond with a basic reply to a "local request". Local request are defined to be +names in the following zones: localhost, 0.in-addr.arpa, 127.in-addr.arpa and 255.in-addr.arpa \fIand\fP +any query asking for \fB\fClocalhost.\fR. When seeing the latter a metric counter is increased and +if \fIdebug\fP is enabled a debug log is emitted. + +.PP +With \fIlocal\fP enabled any query falling under these zones will get a reply. The prevents the query +from "escaping" to the internet and putting strain on external infrastructure. + +.PP +The zones are mostly empty, only \fB\fClocalhost.\fR address records (A and AAAA) are defined and a +\fB\fC1.0.0.127.in-addr.arpa.\fR reverse (PTR) record. + +.SH "SYNTAX" +.PP +.RS + +.nf +local + +.fi +.RE + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metric is exported: + +.IP \(bu 4 +\fB\fCcoredns_local_localhost_requests_total{}\fR - a counter of the number of \fB\fClocalhost.\fR +requests CoreDNS has seen. Note this does \fInot\fP count \fB\fClocalhost.\fR queries. + + +.PP +Note that this metric \fIdoes not\fP have a \fB\fCserver\fR label, because it's more interesting to find the +client(s) performing these queries than to see which server handled it. You'll need to inspect the +debug log to get the client IP address. + +.SH "EXAMPLES" +.PP +.RS + +.nf +\&. { + local +} + +.fi +.RE + +.SH "BUGS" +.PP +Only the \fB\fCin-addr.arpa.\fR reverse zone is implemented, \fB\fCip6.arpa.\fR queries are not intercepted. + +.SH "SEE ALSO" +.PP +BIND9's configuration in Debian comes with these zones preconfigured. See the \fIdebug\fP plugin for +enabling debug logging. + diff --git a/ag_201_coredns/man/coredns-log.7 b/ag_201_coredns/man/coredns-log.7 new file mode 100644 index 0000000..1d2ee95 --- /dev/null +++ b/ag_201_coredns/man/coredns-log.7 @@ -0,0 +1,249 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-LOG" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIlog\fP - enables query logging to standard output. + +.SH "DESCRIPTION" +.PP +By just using \fIlog\fP you dump all queries (and parts for the reply) on standard output. Options exist +to tweak the output a little. Note that for busy servers logging will incur a performance hit. + +.PP +Enabling or disabling the \fIlog\fP plugin only affects the query logging, any other logging from +CoreDNS will show up regardless. + +.SH "SYNTAX" +.PP +.RS + +.nf +log + +.fi +.RE + +.PP +With no arguments, a query log entry is written to \fIstdout\fP in the common log format for all requests. +Or if you want/need slightly more control: + +.PP +.RS + +.nf +log [NAMES...] [FORMAT] + +.fi +.RE + +.IP \(bu 4 +\fB\fCNAMES\fR is the name list to match in order to be logged +.IP \(bu 4 +\fB\fCFORMAT\fR is the log format to use (default is Common Log Format), \fB\fC{common}\fR is used as a shortcut +for the Common Log Format. You can also use \fB\fC{combined}\fR for a format that adds the query opcode +\fB\fC{>opcode}\fR to the Common Log Format. + + +.PP +You can further specify the classes of responses that get logged: + +.PP +.RS + +.nf +log [NAMES...] [FORMAT] { + class CLASSES... +} + +.fi +.RE + +.IP \(bu 4 +\fB\fCCLASSES\fR is a space-separated list of classes of responses that should be logged + + +.PP +The classes of responses have the following meaning: + +.IP \(bu 4 +\fB\fCsuccess\fR: successful response +.IP \(bu 4 +\fB\fCdenial\fR: either NXDOMAIN or nodata responses (Name exists, type does not). A nodata response +sets the return code to NOERROR. +.IP \(bu 4 +\fB\fCerror\fR: SERVFAIL, NOTIMP, REFUSED, etc. Anything that indicates the remote server is not willing to +resolve the request. +.IP \(bu 4 +\fB\fCall\fR: the default - nothing is specified. Using of this class means that all messages will be +logged whatever we mix together with "all". + + +.PP +If no class is specified, it defaults to \fB\fCall\fR. + +.SH "LOG FORMAT" +.PP +You can specify a custom log format with any placeholder values. Log supports both request and +response placeholders. + +.PP +The following place holders are supported: + +.IP \(bu 4 +\fB\fC{type}\fR: qtype of the request +.IP \(bu 4 +\fB\fC{name}\fR: qname of the request +.IP \(bu 4 +\fB\fC{class}\fR: qclass of the request +.IP \(bu 4 +\fB\fC{proto}\fR: protocol used (tcp or udp) +.IP \(bu 4 +\fB\fC{remote}\fR: client's IP address, for IPv6 addresses these are enclosed in brackets: \fB\fC[::1]\fR +.IP \(bu 4 +\fB\fC{local}\fR: server's IP address, for IPv6 addresses these are enclosed in brackets: \fB\fC[::1]\fR +.IP \(bu 4 +\fB\fC{size}\fR: request size in bytes +.IP \(bu 4 +\fB\fC{port}\fR: client's port +.IP \(bu 4 +\fB\fC{duration}\fR: response duration +.IP \(bu 4 +\fB\fC{rcode}\fR: response RCODE +.IP \(bu 4 +\fB\fC{rsize}\fR: raw (uncompressed), response size (a client may receive a smaller response) +.IP \(bu 4 +\fB\fC{>rflags}\fR: response flags, each set flag will be displayed, e.g. "aa, tc". This includes the qr +bit as well +.IP \(bu 4 +\fB\fC{>bufsize}\fR: the EDNS0 buffer size advertised in the query +.IP \(bu 4 +\fB\fC{>do}\fR: is the EDNS0 DO (DNSSEC OK) bit set in the query +.IP \(bu 4 +\fB\fC{>id}\fR: query ID +.IP \(bu 4 +\fB\fC{>opcode}\fR: query OPCODE +.IP \(bu 4 +\fB\fC{common}\fR: the default Common Log Format. +.IP \(bu 4 +\fB\fC{combined}\fR: the Common Log Format with the query opcode. +.IP \(bu 4 +\fB\fC{/LABEL}\fR: any metadata label is accepted as a place holder if it is enclosed between \fB\fC{/\fR and +\fB\fC}\fR, the place holder will be replaced by the corresponding metadata value or the default value +\fB\fC-\fR if label is not defined. See the \fImetadata\fP plugin for more information. + + +.PP +The default Common Log Format is: + +.PP +.RS + +.nf +`{remote}:{port} \- {>id} "{type} {class} {name} {proto} {size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {duration}` + +.fi +.RE + +.PP +Each of these logs will be outputted with \fB\fClog.Infof\fR, so a typical example looks like this: + +.PP +.RS + +.nf +[INFO] [::1]:50759 \- 29008 "A IN example.org. udp 41 false 4096" NOERROR qr,rd,ra,ad 68 0.037990251s +~~~~ + +## Examples + +Log all requests to stdout + +~~~ corefile +\&. { + log + whoami +} + +.fi +.RE + +.PP +Custom log format, for all zones (\fB\fC.\fR) + +.PP +.RS + +.nf +\&. { + log . "{proto} Request: {name} {type} {>id}" +} + +.fi +.RE + +.PP +Only log denials (NXDOMAIN and nodata) for example.org (and below) + +.PP +.RS + +.nf +\&. { + log example.org { + class denial + } +} + +.fi +.RE + +.PP +Log all queries which were not resolved successfully in the Combined Log Format. + +.PP +.RS + +.nf +\&. { + log . {combined} { + class denial error + } +} + +.fi +.RE + +.PP +Log all queries on which we did not get errors + +.PP +.RS + +.nf +\&. { + log . { + class denial success + } +} + +.fi +.RE + +.PP +Also the multiple statements can be OR-ed, for example, we can rewrite the above case as following: + +.PP +.RS + +.nf +\&. { + log . { + class denial + class success + } +} + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-loop.7 b/ag_201_coredns/man/coredns-loop.7 new file mode 100644 index 0000000..2450dda --- /dev/null +++ b/ag_201_coredns/man/coredns-loop.7 @@ -0,0 +1,123 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-LOOP" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIloop\fP - detects simple forwarding loops and halts the server. + +.SH "DESCRIPTION" +.PP +The \fIloop\fP plugin will send a random probe query to ourselves and will then keep track of how many times +we see it. If we see it more than twice, we assume CoreDNS has seen a forwarding loop and we halt the process. + +.PP +The plugin will try to send the query for up to 30 seconds. This is done to give CoreDNS enough time +to start up. Once a query has been successfully sent, \fIloop\fP disables itself to prevent a query of +death. + +.PP +The query sent is \fB\fC..zone\fR with type set to HINFO. + +.SH "SYNTAX" +.PP +.RS + +.nf +loop + +.fi +.RE + +.SH "EXAMPLES" +.PP +Start a server on the default port and load the \fIloop\fP and \fIforward\fP plugins. The \fIforward\fP plugin +forwards to it self. + +.PP +.RS + +.nf +\&. { + loop + forward . 127.0.0.1 +} + +.fi +.RE + +.PP +After CoreDNS has started it stops the process while logging: + +.PP +.RS + +.nf +plugin/loop: Loop (127.0.0.1:55953 \-> :1053) detected for zone ".", see https://coredns.io/plugins/loop#troubleshooting. Query: "HINFO 4547991504243258144.3688648895315093531." + +.fi +.RE + +.SH "LIMITATIONS" +.PP +This plugin only attempts to find simple static forwarding loops at start up time. To detect a loop, +the following must be true: + +.IP \(bu 4 +the loop must be present at start up time. +.IP \(bu 4 +the loop must occur for the \fB\fCHINFO\fR query type. + + +.SH "TROUBLESHOOTING" +.PP +When CoreDNS logs contain the message \fB\fCLoop ... detected ...\fR, this means that the \fB\fCloop\fR detection +plugin has detected an infinite forwarding loop in one of the upstream DNS servers. This is a fatal +error because operating with an infinite loop will consume memory and CPU until eventual out of +memory death by the host. + +.PP +A forwarding loop is usually caused by: + +.IP \(bu 4 +Most commonly, CoreDNS forwarding requests directly to itself. e.g. via a loopback address such as \fB\fC127.0.0.1\fR, \fB\fC::1\fR or \fB\fC127.0.0.53\fR +.IP \(bu 4 +Less commonly, CoreDNS forwarding to an upstream server that in turn, forwards requests back to CoreDNS. + + +.PP +To troubleshoot this problem, look in your Corefile for any \fB\fCforward\fRs to the zone +in which the loop was detected. Make sure that they are not forwarding to a local address or +to another DNS server that is forwarding requests back to CoreDNS. If \fB\fCforward\fR is +using a file (e.g. \fB\fC/etc/resolv.conf\fR), make sure that file does not contain local addresses. + +.SS "TROUBLESHOOTING LOOPS IN KUBERNETES CLUSTERS" +.PP +When a CoreDNS Pod deployed in Kubernetes detects a loop, the CoreDNS Pod will start to "CrashLoopBackOff". +This is because Kubernetes will try to restart the Pod every time CoreDNS detects the loop and exits. + +.PP +A common cause of forwarding loops in Kubernetes clusters is an interaction with a local DNS cache +on the host node (e.g. \fB\fCsystemd-resolved\fR). For example, in certain configurations \fB\fCsystemd-resolved\fR will +put the loopback address \fB\fC127.0.0.53\fR as a nameserver into \fB\fC/etc/resolv.conf\fR. Kubernetes (via \fB\fCkubelet\fR) by default +will pass this \fB\fC/etc/resolv.conf\fR file to all Pods using the \fB\fCdefault\fR dnsPolicy rendering them +unable to make DNS lookups (this includes CoreDNS Pods). CoreDNS uses this \fB\fC/etc/resolv.conf\fR +as a list of upstreams to forward requests to. Since it contains a loopback address, CoreDNS ends up forwarding +requests to itself. + +.PP +There are many ways to work around this issue, some are listed here: + +.IP \(bu 4 +Add the following to your \fB\fCkubelet\fR config yaml: \fB\fCresolvConf: \fR (or via command line flag \fB\fC--resolv-conf\fR deprecated in 1.10). Your "real" +\fB\fCresolv.conf\fR is the one that contains the actual IPs of your upstream servers, and no local/loopback address. +This flag tells \fB\fCkubelet\fR to pass an alternate \fB\fCresolv.conf\fR to Pods. For systems using \fB\fCsystemd-resolved\fR, +\fB\fC/run/systemd/resolve/resolv.conf\fR is typically the location of the "real" \fB\fCresolv.conf\fR, +although this can be different depending on your distribution. +.IP \(bu 4 +Disable the local DNS cache on host nodes, and restore \fB\fC/etc/resolv.conf\fR to the original. +.IP \(bu 4 +A quick and dirty fix is to edit your Corefile, replacing \fB\fCforward . /etc/resolv.conf\fR with +the IP address of your upstream DNS, for example \fB\fCforward . 8.8.8.8\fR. But this only fixes the issue for CoreDNS, +kubelet will continue to forward the invalid \fB\fCresolv.conf\fR to all \fB\fCdefault\fR dnsPolicy Pods, leaving them unable to resolve DNS. + + diff --git a/ag_201_coredns/man/coredns-metadata.7 b/ag_201_coredns/man/coredns-metadata.7 new file mode 100644 index 0000000..9a295ac --- /dev/null +++ b/ag_201_coredns/man/coredns-metadata.7 @@ -0,0 +1,64 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-METADATA" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fImetadata\fP - enables a metadata collector. + +.SH "DESCRIPTION" +.PP +By enabling \fImetadata\fP any plugin that implements metadata.Provider +interface +\[la]https://godoc.org/github.com/coredns/coredns/plugin/metadata#Provider\[ra] will be called for +each DNS query, at the beginning of the process for that query, in order to add its own metadata to +context. + +.PP +The metadata collected will be available for all plugins, via the Context parameter provided in the +ServeDNS function. The package (code) documentation has examples on how to inspect and retrieve +metadata a plugin might be interested in. + +.PP +The metadata is added by setting a label with a value in the context. These labels should be named +\fB\fCplugin/NAME\fR, where \fBNAME\fP is something descriptive. The only hard requirement the \fImetadata\fP +plugin enforces is that the labels contain a slash. See the documentation for +\fB\fCmetadata.SetValueFunc\fR. + +.PP +The value stored is a string. The empty string signals "no metadata". See the documentation for +\fB\fCmetadata.ValueFunc\fR on how to retrieve this. + +.SH "SYNTAX" +.PP +.RS + +.nf +metadata [ZONES... ] + +.fi +.RE + +.IP \(bu 4 +\fBZONES\fP zones metadata should be invoked for. + + +.SH "PLUGINS" +.PP +\fB\fCmetadata.Provider\fR interface needs to be implemented by each plugin willing to provide metadata +information for other plugins. It will be called by metadata and gather the information from all +plugins in context. + +.PP +Note: this method should work quickly, because it is called for every request. + +.SH "EXAMPLES" +.PP +The \fIrewrite\fP plugin uses meta data to rewrite requests. + +.SH "SEE ALSO" +.PP +The Provider interface +\[la]https://godoc.org/github.com/coredns/coredns/plugin/metadata#Provider\[ra] and +the package level +\[la]https://godoc.org/github.com/coredns/coredns/plugin/metadata\[ra] documentation. + diff --git a/ag_201_coredns/man/coredns-metrics.7 b/ag_201_coredns/man/coredns-metrics.7 new file mode 100644 index 0000000..565af34 --- /dev/null +++ b/ag_201_coredns/man/coredns-metrics.7 @@ -0,0 +1,116 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-METRICS" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIprometheus\fP - enables Prometheus +\[la]https://prometheus.io/\[ra] metrics. + +.SH "DESCRIPTION" +.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: + +.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. +.IP \(bu 4 +\fB\fCcoredns_dns_request_duration_seconds{server, zone, type}\fR - duration to process each query. +.IP \(bu 4 +\fB\fCcoredns_dns_request_size_bytes{server, zone, proto}\fR - size of the request in bytes. +.IP \(bu 4 +\fB\fCcoredns_dns_do_requests_total{server, zone}\fR - queries that have the DO bit set +.IP \(bu 4 +\fB\fCcoredns_dns_response_size_bytes{server, zone, proto}\fR - response size in bytes. +.IP \(bu 4 +\fB\fCcoredns_dns_responses_total{server, zone, rcode}\fR - response per zone and rcode. +.IP \(bu 4 +\fB\fCcoredns_plugin_enabled{server, zone, name}\fR - indicates whether a plugin is enabled on per server and zone basis. + + +.PP +Each counter has a label \fB\fCzone\fR which is the zonename used for the request/response. + +.PP +Extra labels used are: + +.IP \(bu 4 +\fB\fCserver\fR is identifying the server responsible for the request. This is a string formatted +as the server's listening address: \fB\fC://[]:\fR. I.e. for a "normal" DNS server +this is \fB\fCdns://:53\fR. If you are using the \fIbind\fP plugin an IP address is included, e.g.: \fB\fCdns://127.0.0.53:53\fR. +.IP \(bu 4 +\fB\fCproto\fR which holds the transport of the response ("udp" or "tcp") +.IP \(bu 4 +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 +other types. + + +.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 +This plugin can only be used once per Server Block. + +.SH "SYNTAX" +.PP +.RS + +.nf +prometheus [ADDRESS] + +.fi +.RE + +.PP +For each zone that you want to see metrics for. + +.PP +It optionally takes a bind address to which the metrics are exported; the default +listens on \fB\fClocalhost:9153\fR. The metrics path is fixed to \fB\fC/metrics\fR. + +.SH "EXAMPLES" +.PP +Use an alternative listening address: + +.PP +.RS + +.nf +\&. { + prometheus localhost:9253 +} + +.fi +.RE + +.PP +Or via an environment variable (this is supported throughout the Corefile): \fB\fCexport PORT=9253\fR, and +then: + +.PP +.RS + +.nf +\&. { + prometheus localhost:{$PORT} +} + +.fi +.RE + +.SH "BUGS" +.PP +When reloading, the Prometheus handler is stopped before the new server instance is started. +If that new server fails to start, then the initial server instance is still available and DNS queries still served, +but Prometheus handler stays down. +Prometheus will not reply HTTP request until a successful reload or a complete restart of CoreDNS. +Only the plugins that register as Handler are visible in \fB\fCcoredns_plugin_enabled{server, zone, name}\fR. As of today the plugins reload and bind will not be reported. + diff --git a/ag_201_coredns/man/coredns-minimal.7 b/ag_201_coredns/man/coredns-minimal.7 new file mode 100644 index 0000000..52e2e1c --- /dev/null +++ b/ag_201_coredns/man/coredns-minimal.7 @@ -0,0 +1,49 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-MINIMAL" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIminimal\fP - minimizes size of the DNS response message whenever possible. + +.SH "DESCRIPTION" +.PP +The \fIminimal\fP plugin tries to minimize the size of the response. Depending on the response type it +removes resource records from the AUTHORITY and ADDITIONAL sections. + +.PP +Specifically this plugin looks at successful responses (this excludes negative responses, i.e. +nodata or name error). If the successful response isn't a delegation only the RRs in the answer +section are written to the client. + +.SH "SYNTAX" +.PP +.RS + +.nf +minimal + +.fi +.RE + +.SH "EXAMPLES" +.PP +Enable minimal responses: + +.PP +.RS + +.nf +example.org { + whoami + forward . 8.8.8.8 + minimal +} + +.fi +.RE + +.SH "SEE ALSO" +.PP +BIND 9 Configuration Reference +\[la]https://bind9.readthedocs.io/en/latest/reference.html#boolean-options\[ra] + diff --git a/ag_201_coredns/man/coredns-nsid.7 b/ag_201_coredns/man/coredns-nsid.7 new file mode 100644 index 0000000..a089230 --- /dev/null +++ b/ag_201_coredns/man/coredns-nsid.7 @@ -0,0 +1,78 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-NSID" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fInsid\fP - adds an identifier of this server to each reply. + +.SH "DESCRIPTION" +.PP +This plugin implements RFC 5001 +\[la]https://tools.ietf.org/html/rfc5001\[ra] and adds an EDNS0 OPT +resource record to replies that uniquely identify the server. This is useful in anycast setups to +see which server was responsible for generating the reply and for debugging. + +.PP +This plugin can only be used once per Server Block. + +.SH "SYNTAX" +.PP +.RS + +.nf +nsid [DATA] + +.fi +.RE + +.PP +\fBDATA\fP is the string to use in the nsid record. + +.PP +If \fBDATA\fP is not given, the host's name is used. + +.SH "EXAMPLES" +.PP +Enable nsid: + +.PP +.RS + +.nf +example.org { + whoami + nsid Use The Force +} + +.fi +.RE + +.PP +And now a client with NSID support will see an OPT record with the NSID option: + +.PP +.RS + +.nf +% dig +nsid @localhost a whoami.example.org + +;; Got answer: +;; \->>HEADER<<\- opcode: QUERY, status: NOERROR, id: 46880 +;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 3 + +\&.... + +; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 4096 +; NSID: 55 73 65 20 54 68 65 20 46 6f 72 63 65 ("Use The Force") +;; QUESTION SECTION: +;whoami.example.org. IN A + +.fi +.RE + +.SH "SEE ALSO" +.PP +RFC 5001 +\[la]https://tools.ietf.org/html/rfc5001\[ra] + diff --git a/ag_201_coredns/man/coredns-pprof.7 b/ag_201_coredns/man/coredns-pprof.7 new file mode 100644 index 0000000..7948bee --- /dev/null +++ b/ag_201_coredns/man/coredns-pprof.7 @@ -0,0 +1,115 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-PPROF" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIpprof\fP - publishes runtime profiling data at endpoints under \fB\fC/debug/pprof\fR. + +.SH "DESCRIPTION" +.PP +You can visit \fB\fC/debug/pprof\fR on your site for an index of the available endpoints. By default it +will listen on localhost:6053. + +.PP +This is a debugging tool. Certain requests (such as collecting execution traces) can be slow. If +you use pprof on a live server, consider restricting access or enabling it only temporarily. + +.PP +This plugin can only be used once per Server Block. + +.SH "SYNTAX" +.PP +.RS + +.nf +pprof [ADDRESS] + +.fi +.RE + +.PP +Optionally pprof takes an address; the default is \fB\fClocalhost:6053\fR. + +.PP +An extra option can be set with this extended syntax: + +.PP +.RS + +.nf +pprof [ADDRESS] { + block [RATE] +} + +.fi +.RE + +.IP \(bu 4 +\fB\fCblock\fR option enables block profiling, \fBRATE\fP defaults to 1. \fBRATE\fP must be a positive value. +See Diagnostics, chapter profiling +\[la]https://golang.org/doc/diagnostics.html\[ra] and +runtime.SetBlockProfileRate +\[la]https://golang.org/pkg/runtime/#SetBlockProfileRate\[ra] for what block +profiling entails. + + +.SH "EXAMPLES" +.PP +Enable a pprof endpoint: + +.PP +.RS + +.nf +\&. { + pprof +} + +.fi +.RE + +.PP +And use the pprof tool to get statistics: \fB\fCgo tool pprof http://localhost:6053\fR. + +.PP +Listen on an alternate address: + +.PP +.RS + +.nf +\&. { + pprof 10.9.8.7:6060 +} + +.fi +.RE + +.PP +Listen on an all addresses on port 6060, and enable block profiling + +.PP +.RS + +.nf +\&. { + pprof :6060 { + block + } +} + +.fi +.RE + +.SH "SEE ALSO" +.PP +See Go's pprof documentation +\[la]https://golang.org/pkg/net/http/pprof/\[ra] and Profiling Go +Programs +\[la]https://blog.golang.org/profiling-go-programs\[ra]. + +.PP +See runtime.SetBlockProfileRate +\[la]https://golang.org/pkg/runtime/#SetBlockProfileRate\[ra] for +background on block profiling. + diff --git a/ag_201_coredns/man/coredns-ready.7 b/ag_201_coredns/man/coredns-ready.7 new file mode 100644 index 0000000..0821422 --- /dev/null +++ b/ag_201_coredns/man/coredns-ready.7 @@ -0,0 +1,77 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-READY" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIready\fP - enables a readiness check HTTP endpoint. + +.SH "DESCRIPTION" +.PP +By enabling \fIready\fP an HTTP endpoint on port 8181 will return 200 OK, when all plugins that are able +to signal readiness have done so. If some are not ready yet the endpoint will return a 503 with the +body containing the list of plugins that are not ready. Once a plugin has signaled it is ready it +will not be queried again. + +.PP +Each Server Block that enables the \fIready\fP plugin will have the plugins \fIin that server block\fP +report readiness into the /ready endpoint that runs on the same port. This also means that the +\fIsame\fP plugin with different configurations (in potentially \fIdifferent\fP Server Blocks) will have +their readiness reported as the union of their respective readinesses. + +.SH "SYNTAX" +.PP +.RS + +.nf +ready [ADDRESS] + +.fi +.RE + +.PP +\fIready\fP optionally takes an address; the default is \fB\fC:8181\fR. The path is fixed to \fB\fC/ready\fR. The +readiness endpoint returns a 200 response code and the word "OK" when this server is ready. It +returns a 503 otherwise \fIand\fP the list of plugins that are not ready. + +.SH "PLUGINS" +.PP +Any plugin wanting to signal readiness will need to implement the \fB\fCready.Readiness\fR interface by +implementing a method \fB\fCReady() bool\fR that returns true when the plugin is ready and false otherwise. + +.SH "EXAMPLES" +.PP +Let \fIready\fP report readiness for both the \fB\fC.\fR and \fB\fCexample.org\fR servers (assuming the \fIwhois\fP +plugin also exports readiness): + +.PP +.RS + +.nf +\&. { + ready + erratic +} + +example.org { + ready + whoami +} + + +.fi +.RE + +.PP +Run \fIready\fP on a different port. + +.PP +.RS + +.nf +\&. { + ready localhost:8091 +} + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-reload.7 b/ag_201_coredns/man/coredns-reload.7 new file mode 100644 index 0000000..6ff0e49 --- /dev/null +++ b/ag_201_coredns/man/coredns-reload.7 @@ -0,0 +1,152 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-RELOAD" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIreload\fP - allows automatic reload of a changed Corefile. + +.SH "DESCRIPTION" +.PP +This plugin allows automatic reload of a changed \fICorefile\fP. +To enable automatic reloading of \fIzone file\fP changes, use the \fB\fCauto\fR plugin. + +.PP +This plugin periodically checks if the Corefile has changed by reading +it and calculating its MD5 checksum. If the file has changed, it reloads +CoreDNS with the new Corefile. This eliminates the need to send a SIGHUP +or SIGUSR1 after changing the Corefile. + +.PP +The reloads are graceful - you should not see any loss of service when the +reload happens. Even if the new Corefile has an error, CoreDNS will continue +to run the old config and an error message will be printed to the log. But see +the Bugs section for failure modes. + +.PP +In some environments (for example, Kubernetes), there may be many CoreDNS +instances that started very near the same time and all share a common +Corefile. To prevent these all from reloading at the same time, some +jitter is added to the reload check interval. This is jitter from the +perspective of multiple CoreDNS instances; each instance still checks on a +regular interval, but all of these instances will have their reloads spread +out across the jitter duration. This isn't strictly necessary given that the +reloads are graceful, and can be disabled by setting the jitter to \fB\fC0s\fR. + +.PP +Jitter is re-calculated whenever the Corefile is reloaded. + +.PP +This plugin can only be used once per Server Block. + +.SH "SYNTAX" +.PP +.RS + +.nf +reload [INTERVAL] [JITTER] + +.fi +.RE + +.PP +The plugin will check for changes every \fBINTERVAL\fP, subject to +/- the \fBJITTER\fP duration. + +.IP \(bu 4 +\fBINTERVAL\fP and \fBJITTER\fP are Golang durations +\[la]https://golang.org/pkg/time/#ParseDuration\[ra]. +The default \fBINTERVAL\fP is 30s, default \fBJITTER\fP is 15s, the minimal value for \fBINTERVAL\fP +is 2s, and for \fBJITTER\fP it is 1s. If \fBJITTER\fP is more than half of \fBINTERVAL\fP, it will be +set to half of \fBINTERVAL\fP + + +.SH "EXAMPLES" +.PP +Check with the default intervals: + +.PP +.RS + +.nf +\&. { + reload + erratic +} + +.fi +.RE + +.PP +Check every 10 seconds (jitter is automatically set to 10 / 2 = 5 in this case): + +.PP +.RS + +.nf +\&. { + reload 10s + erratic +} + +.fi +.RE + +.SH "BUGS" +.PP +The reload happens without data loss (i.e. DNS queries keep flowing), but there is a corner case +where the reload fails, and you loose functionality. Consider the following Corefile: + +.PP +.RS + +.nf +\&. { + health :8080 + whoami +} + +.fi +.RE + +.PP +CoreDNS starts and serves health from :8080. Now you change \fB\fC:8080\fR to \fB\fC:443\fR not knowing a process +is already listening on that port. The process reloads and performs the following steps: + +.IP 1\. 4 +close the listener on 8080 +.IP 2\. 4 +reload and parse the config again +.IP 3\. 4 +fail to start a new listener on 443 +.IP 4\. 4 +fail loading the new Corefile, abort and keep using the old process + + +.PP +After the aborted attempt to reload we are left with the old processes running, but the listener is +closed in step 1; so the health endpoint is broken. The same can happen in the prometheus plugin. + +.PP +In general be careful with assigning new port and expecting reload to work fully. + +.PP +In CoreDNS v1.6.0 and earlier any \fB\fCimport\fR statements are not discovered by this plugin. +This means if any of these imported files changes the \fIreload\fP plugin is ignorant of that fact. +CoreDNS v1.7.0 and later does parse the Corefile and supports detecting changes in imported files. + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metric is exported: + +.IP \(bu 4 +\fB\fCcoredns_reload_failed_total{}\fR - counts the number of failed reload attempts. +.IP \(bu 4 +\fB\fCcoredns_reload_version_info{hash, value}\fR - record the hash value during reload. + + +.PP +Currently the type of \fB\fChash\fR is "md5", the \fB\fCvalue\fR is the returned hash value. + +.SH "SEE ALSO" +.PP +See coredns-import(7) and corefile(5). + diff --git a/ag_201_coredns/man/coredns-rewrite.7 b/ag_201_coredns/man/coredns-rewrite.7 new file mode 100644 index 0000000..417aca7 --- /dev/null +++ b/ag_201_coredns/man/coredns-rewrite.7 @@ -0,0 +1,470 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-REWRITE" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIrewrite\fP - performs internal message rewriting. + +.SH "DESCRIPTION" +.PP +Rewrites are invisible to the client. There are simple rewrites (fast) and complex rewrites +(slower), but they're powerful enough to accommodate most dynamic back-end applications. + +.SH "SYNTAX" +.PP +A simplified/easy-to-digest syntax for \fIrewrite\fP is... + +.PP +.RS + +.nf +rewrite [continue|stop] FIELD [FROM TO|FROM TTL] + +.fi +.RE + +.IP \(bu 4 +\fBFIELD\fP indicates what part of the request/response is being re-written. + +.RS +.IP \(en 4 +\fB\fCtype\fR - the type field of the request will be rewritten. FROM/TO must be a DNS record type (\fB\fCA\fR, \fB\fCMX\fR, etc.); +e.g., to rewrite ANY queries to HINFO, use \fB\fCrewrite type ANY HINFO\fR. +.IP \(en 4 +\fB\fCclass\fR - the class of the message will be rewritten. FROM/TO must be a DNS class type (\fB\fCIN\fR, \fB\fCCH\fR, or \fB\fCHS\fR); e.g., to rewrite CH queries to IN use \fB\fCrewrite class CH IN\fR. +.IP \(en 4 +\fB\fCname\fR - the query name in the \fIrequest\fP is rewritten; by default this is a full match of the +name, e.g., \fB\fCrewrite name example.net example.org\fR. Other match types are supported, see the \fBName Field Rewrites\fP section below. +.IP \(en 4 +\fB\fCanswer name\fR - the query name in the \fIresponse\fP is rewritten. This option has special restrictions and requirements, in particular it must always combined with a \fB\fCname\fR rewrite. See below in the \fBResponse Rewrites\fP section. +.IP \(en 4 +\fB\fCedns0\fR - an EDNS0 option can be appended to the request as described below in the \fBEDNS0 Options\fP section. +.IP \(en 4 +\fB\fCttl\fR - the TTL value in the \fIresponse\fP is rewritten. + +.RE +.IP \(bu 4 +\fBFROM\fP is the name (exact, suffix, prefix, substring, or regex) or type to match +.IP \(bu 4 +\fBTO\fP is the destination name or type to rewrite to +.IP \(bu 4 +\fBTTL\fP is the number of seconds to set the TTL value to + + +.PP +If you specify multiple rules and an incoming query matches multiple rules, the rewrite +will behave as follows: + +.IP \(bu 4 +\fB\fCcontinue\fR will continue applying the next rule in the rule list. +.IP \(bu 4 +\fB\fCstop\fR will consider the current rule the last rule and will not continue. The default behaviour is \fB\fCstop\fR + + +.SH "EXAMPLES" +.SS "NAME FIELD REWRITES" +.PP +The \fB\fCrewrite\fR plugin offers the ability to match the name in the question section of +a DNS request. The match could be exact, a substring match, or based on a prefix, suffix, or regular +expression. If the newly used name is not a legal domain name, the plugin returns an error to the +client. + +.PP +The syntax for name rewriting is as follows: + +.PP +.RS + +.nf +rewrite [continue|stop] name [exact|prefix|suffix|substring|regex] STRING STRING + +.fi +.RE + +.PP +The match type, e.g., \fB\fCexact\fR, \fB\fCsubstring\fR, etc., triggers rewrite: + +.IP \(bu 4 +\fBexact\fP (default): on an exact match of the name in the question section of a request +.IP \(bu 4 +\fBsubstring\fP: on a partial match of the name in the question section of a request +.IP \(bu 4 +\fBprefix\fP: when the name begins with the matching string +.IP \(bu 4 +\fBsuffix\fP: when the name ends with the matching string +.IP \(bu 4 +\fBregex\fP: when the name in the question section of a request matches a regular expression + + +.PP +If the match type is omitted, the \fB\fCexact\fR match type is assumed. + +.PP +The following instruction allows rewriting names in the query that +contain the substring \fB\fCservice.us-west-1.example.org\fR: + +.PP +.RS + +.nf +rewrite name substring service.us\-west\-1.example.org service.us\-west\-1.consul + +.fi +.RE + +.PP +Thus: + +.IP \(bu 4 +Incoming Request Name: \fB\fCftp.service.us-west-1.example.org\fR +.IP \(bu 4 +Rewritten Request Name: \fB\fCftp.service.us-west-1.consul\fR + + +.PP +The following instruction uses regular expressions. Names in requests +matching the regular expression \fB\fC(.*)-(us-west-1)\.example\.org\fR are replaced with +\fB\fC{1}.service.{2}.consul\fR, where \fB\fC{1}\fR and \fB\fC{2}\fR are regular expression match groups. + +.PP +.RS + +.nf +rewrite name regex (.*)\-(us\-west\-1)\\.example\\.org {1}.service.{2}.consul + +.fi +.RE + +.PP +Thus: + +.IP \(bu 4 +Incoming Request Name: \fB\fCftp-us-west-1.example.org\fR +.IP \(bu 4 +Rewritten Request Name: \fB\fCftp.service.us-west-1.consul\fR + + +.PP +The following example rewrites the \fB\fCschmoogle.com\fR suffix to \fB\fCgoogle.com\fR. + +.PP +.RS + +.nf +rewrite name suffix .schmoogle.com. .google.com. + +.fi +.RE + +.SS "RESPONSE REWRITES" +.PP +When rewriting incoming DNS requests' names, CoreDNS re-writes the \fB\fCQUESTION SECTION\fR +section of the requests. It may be necessary to rewrite the \fB\fCANSWER SECTION\fR of the +requests, because some DNS resolvers treat mismatches between the \fB\fCQUESTION SECTION\fR +and \fB\fCANSWER SECTION\fR as a man-in-the-middle attack (MITM). + +.PP +For example, a user tries to resolve \fB\fCftp-us-west-1.coredns.rocks\fR. The +CoreDNS configuration file has the following rule: + +.PP +.RS + +.nf +rewrite name regex (.*)\-(us\-west\-1)\\.coredns\\.rocks {1}.service.{2}.consul + +.fi +.RE + +.PP +CoreDNS rewrote the request from \fB\fCftp-us-west-1.coredns.rocks\fR to +\fB\fCftp.service.us-west-1.consul\fR and ultimately resolved it to 3 records. +The resolved records, in the \fB\fCANSWER SECTION\fR below, were not from \fB\fCcoredns.rocks\fR, but +rather from \fB\fCservice.us-west-1.consul\fR. + +.PP +.RS + +.nf +$ dig @10.1.1.1 ftp\-us\-west\-1.coredns.rocks + +;; QUESTION SECTION: +;ftp\-us\-west\-1.coredns.rocks. IN A + +;; ANSWER SECTION: +ftp.service.us\-west\-1.consul. 0 IN A 10.10.10.10 +ftp.service.us\-west\-1.consul. 0 IN A 10.20.20.20 +ftp.service.us\-west\-1.consul. 0 IN A 10.30.30.30 + +.fi +.RE + +.PP +The above is a mismatch between the question asked and the answer provided. + +.PP +The following configuration snippet allows for rewriting of the +\fB\fCANSWER SECTION\fR, provided that the \fB\fCQUESTION SECTION\fR was rewritten: + +.PP +.RS + +.nf + rewrite stop { + name regex (.*)\-(us\-west\-1)\\.coredns\\.rocks {1}.service.{2}.consul + answer name (.*)\\.service\\.(us\-west\-1)\\.consul {1}\-{2}.coredns.rocks + } + +.fi +.RE + +.PP +Now, the \fB\fCANSWER SECTION\fR matches the \fB\fCQUESTION SECTION\fR: + +.PP +.RS + +.nf +$ dig @10.1.1.1 ftp\-us\-west\-1.coredns.rocks + +;; QUESTION SECTION: +;ftp\-us\-west\-1.coredns.rocks. IN A + +;; ANSWER SECTION: +ftp\-us\-west\-1.coredns.rocks. 0 IN A 10.10.10.10 +ftp\-us\-west\-1.coredns.rocks. 0 IN A 10.20.20.20 +ftp\-us\-west\-1.coredns.rocks. 0 IN A 10.30.30.30 + +.fi +.RE + +.PP +It is also possible to rewrite other values returned in the DNS response records +(e.g. the server names returned in \fB\fCSRV\fR and \fB\fCMX\fR records). This can be enabled by adding +the \fB\fCanswer value\fR to a name regex rule as specified below. \fB\fCanswer value\fR takes a +regular expression and a rewrite name as parameters and works in the same way as the +\fB\fCanswer name\fR rule. + +.PP +Note that names in the \fB\fCAUTHORITY SECTION\fR and \fB\fCADDITIONAL SECTION\fR will also be +rewritten following the specified rules. The names returned by the following +record types: \fB\fCCNAME\fR, \fB\fCDNAME\fR, \fB\fCSOA\fR, \fB\fCSRV\fR, \fB\fCMX\fR, \fB\fCNAPTR\fR, \fB\fCNS\fR will be rewritten +if the \fB\fCanswer value\fR rule is specified. + +.PP +The syntax for the rewrite of DNS request and response is as follows: + +.PP +.RS + +.nf +rewrite [continue|stop] { + name regex STRING STRING + answer name STRING STRING + [answer value STRING STRING] +} + +.fi +.RE + +.PP +Note that the above syntax is strict. For response rewrites, only \fB\fCname\fR +rules are allowed to match the question section, and only by match type +\fB\fCregex\fR. The answer rewrite must be after the name, as in the +syntax example. + +.PP +An alternate syntax for rewriting a DNS request and response is as +follows: + +.PP +.RS + +.nf +rewrite [continue|stop] name regex STRING STRING answer name STRING STRING [answer value STRING STRING] + +.fi +.RE + +.PP +When using \fB\fCexact\fR name rewrite rules, the answer gets rewritten automatically, +and there is no need to define \fB\fCanswer name\fR. The rule below +rewrites the name in a request from \fB\fCRED\fR to \fB\fCBLUE\fR, and subsequently +rewrites the name in a corresponding response from \fB\fCBLUE\fR to \fB\fCRED\fR. The +client in the request would see only \fB\fCRED\fR and no \fB\fCBLUE\fR. + +.PP +.RS + +.nf +rewrite [continue|stop] name exact RED BLUE + +.fi +.RE + +.SS "TTL FIELD REWRITES" +.PP +At times, the need to rewrite a TTL value could arise. For example, a DNS server +may not cache records with a TTL of zero (\fB\fC0\fR). An administrator +may want to increase the TTL to ensure it is cached, e.g., by increasing it to 15 seconds. + +.PP +In the below example, the TTL in the answers for \fB\fCcoredns.rocks\fR domain are +being set to \fB\fC15\fR: + +.PP +.RS + +.nf + rewrite continue { + ttl regex (.*)\\.coredns\\.rocks 15 + } + +.fi +.RE + +.PP +By the same token, an administrator may use this feature to prevent or limit caching by +setting the TTL value really low. + +.PP +The syntax for the TTL rewrite rule is as follows. The meaning of +\fB\fCexact|prefix|suffix|substring|regex\fR is the same as with the name rewrite rules. + +.PP +.RS + +.nf +rewrite [continue|stop] ttl [exact|prefix|suffix|substring|regex] STRING SECONDS + +.fi +.RE + +.SH "EDNS0 OPTIONS" +.PP +Using the FIELD edns0, you can set, append, or replace specific EDNS0 options in the request. + +.IP \(bu 4 +\fB\fCreplace\fR will modify any "matching" option with the specified option. The criteria for "matching" varies based on EDNS0 type. +.IP \(bu 4 +\fB\fCappend\fR will add the option only if no matching option exists +.IP \(bu 4 +\fB\fCset\fR will modify a matching option or add one if none is found + + +.PP +Currently supported are \fB\fCEDNS0_LOCAL\fR, \fB\fCEDNS0_NSID\fR and \fB\fCEDNS0_SUBNET\fR. + +.SS "EDNS0_LOCAL" +.PP +This has two fields, code and data. A match is defined as having the same code. Data may be a string or a variable. + +.IP \(bu 4 +A string data is treated as hex if it starts with \fB\fC0x\fR. Example: + + +.PP +.RS + +.nf +\&. { + rewrite edns0 local set 0xffee 0x61626364 + whoami +} + +.fi +.RE + +.PP +rewrites the first local option with code 0xffee, setting the data to "abcd". This is equivalent to: + +.PP +.RS + +.nf +\&. { + rewrite edns0 local set 0xffee abcd +} + +.fi +.RE + +.IP \(bu 4 +A variable data is specified with a pair of curly brackets \fB\fC{}\fR. Following are the supported variables: +{qname}, {qtype}, {client\fIip}, {client\fPport}, {protocol}, {server\fIip}, {server\fPport}. +.IP \(bu 4 +If the metadata plugin is enabled, then labels are supported as variables if they are presented within curly brackets. +The variable data will be replaced with the value associated with that label. If that label is not provided, +the variable will be silently substituted with an empty string. + + +.PP +Examples: + +.PP +.RS + +.nf +rewrite edns0 local set 0xffee {client\_ip} + +.fi +.RE + +.PP +The following example uses metadata and an imaginary "some-plugin" that would provide "some-label" as metadata information. + +.PP +.RS + +.nf +metadata +some\-plugin +rewrite edns0 local set 0xffee {some\-plugin/some\-label} + +.fi +.RE + +.SS "EDNS0_NSID" +.PP +This has no fields; it will add an NSID option with an empty string for the NSID. If the option already exists +and the action is \fB\fCreplace\fR or \fB\fCset\fR, then the NSID in the option will be set to the empty string. + +.SS "EDNS0_SUBNET" +.PP +This has two fields, IPv4 bitmask length and IPv6 bitmask length. The bitmask +length is used to extract the client subnet from the source IP address in the query. + +.PP +Example: + +.PP +.RS + +.nf +rewrite edns0 subnet set 24 56 + +.fi +.RE + +.IP \(bu 4 +If the query's source IP address is an IPv4 address, the first 24 bits in the IP will be the network subnet. +.IP \(bu 4 +If the query's source IP address is an IPv6 address, the first 56 bits in the IP will be the network subnet. + + +.SH "FULL SYNTAX" +.PP +The full plugin usage syntax is harder to digest... + +.PP +.RS + +.nf +rewrite [continue|stop] {type|class|edns0|name [exact|prefix|suffix|substring|regex [FROM TO answer name]]} FROM TO + +.fi +.RE + +.PP +The syntax above doesn't cover the multi-line block option for specifying a name request+response rewrite rule described in the \fBResponse Rewrite\fP section. + diff --git a/ag_201_coredns/man/coredns-root.7 b/ag_201_coredns/man/coredns-root.7 new file mode 100644 index 0000000..1a0c1ae --- /dev/null +++ b/ag_201_coredns/man/coredns-root.7 @@ -0,0 +1,43 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-ROOT" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIroot\fP - simply specifies the root of where to find (zone) files. + +.SH "DESCRIPTION" +.PP +The default root is the current working directory of CoreDNS. The \fIroot\fP plugin allows you to change +this. A relative root path is relative to the current working directory. + +.PP +This plugin can only be used once per Server Block. + +.SH "SYNTAX" +.PP +.RS + +.nf +root PATH + +.fi +.RE + +.PP +\fBPATH\fP is the directory to set as CoreDNS' root. + +.SH "EXAMPLES" +.PP +Serve zone data (when the \fIfile\fP plugin is used) from \fB\fC/etc/coredns/zones\fR: + +.PP +.RS + +.nf +\&. { + root /etc/coredns/zones +} + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-route53.7 b/ag_201_coredns/man/coredns-route53.7 new file mode 100644 index 0000000..6e571b4 --- /dev/null +++ b/ag_201_coredns/man/coredns-route53.7 @@ -0,0 +1,142 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-ROUTE53" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIroute53\fP - enables serving zone data from AWS route53. + +.SH "DESCRIPTION" +.PP +The route53 plugin is useful for serving zones from resource record +sets in AWS route53. This plugin supports all Amazon Route 53 records +(https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html +\[la]https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html\[ra]). +The route53 plugin can be used when coredns is deployed on AWS or elsewhere. + +.SH "SYNTAX" +.PP +.RS + +.nf +route53 [ZONE:HOSTED\_ZONE\_ID...] { + aws\_access\_key [AWS\_ACCESS\_KEY\_ID AWS\_SECRET\_ACCESS\_KEY] + credentials PROFILE [FILENAME] + fallthrough [ZONES...] + refresh DURATION +} + +.fi +.RE + +.IP \(bu 4 +\fBZONE\fP the name of the domain to be accessed. When there are multiple zones with overlapping +domains (private vs. public hosted zone), CoreDNS does the lookup in the given order here. +Therefore, for a non-existing resource record, SOA response will be from the rightmost zone. +.IP \(bu 4 +\fBHOSTED_ZONE_ID\fP the ID of the hosted zone that contains the resource record sets to be +accessed. +.IP \(bu 4 +\fBAWS_ACCESS_KEY_ID\fP and \fBAWS_SECRET_ACCESS_KEY\fP the AWS access key ID and secret access key +to be used when query AWS (optional). If they are not provided, then coredns tries to access +AWS credentials the same way as AWS CLI, e.g., environmental variables, AWS credentials file, +instance profile credentials, etc. +.IP \(bu 4 +\fB\fCcredentials\fR is used for reading the credential \fBFILENAME\fP and setting the \fBPROFILE\fP name for a given +zone. \fBPROFILE\fP is the AWS account profile name. Defaults to \fB\fCdefault\fR. \fBFILENAME\fP is the +AWS credentials filename, defaults to \fB\fC~/.aws/credentials\fR. +.IP \(bu 4 +\fB\fCfallthrough\fR If zone matches and no record can be generated, pass request to the next plugin. +If \fBZONES\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. +.IP \(bu 4 +\fB\fCrefresh\fR can be used to control how long between record retrievals from Route 53. It requires +a duration string as a parameter to specify the duration between update cycles. Each update +cycle may result in many AWS API calls depending on how many domains use this plugin and how +many records are in each. Adjusting the update frequency may help reduce the potential of API +rate-limiting imposed by AWS. +.IP \(bu 4 +\fBDURATION\fP A duration string. Defaults to \fB\fC1m\fR. If units are unspecified, seconds are assumed. + + +.SH "EXAMPLES" +.PP +Enable route53 with implicit AWS credentials and resolve CNAMEs via 10.0.0.1: + +.PP +.RS + +.nf +example.org { + route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 +} + +\&. { + forward . 10.0.0.1 +} + +.fi +.RE + +.PP +Enable route53 with explicit AWS credentials: + +.PP +.RS + +.nf +example.org { + route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 { + aws\_access\_key AWS\_ACCESS\_KEY\_ID AWS\_SECRET\_ACCESS\_KEY + } +} + +.fi +.RE + +.PP +Enable route53 with fallthrough: + +.PP +.RS + +.nf +\&. { + route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 example.gov.:Z654321543245 { + fallthrough example.gov. + } +} + +.fi +.RE + +.PP +Enable route53 with multiple hosted zones with the same domain: + +.PP +.RS + +.nf +example.org { + route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 example.org.:Z93A52145678156 +} + +.fi +.RE + +.PP +Enable route53 and refresh records every 3 minutes + +.PP +.RS + +.nf +example.org { + route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 { + refresh 3m + } +} + +.fi +.RE + diff --git a/ag_201_coredns/man/coredns-secondary.7 b/ag_201_coredns/man/coredns-secondary.7 new file mode 100644 index 0000000..018046e --- /dev/null +++ b/ag_201_coredns/man/coredns-secondary.7 @@ -0,0 +1,97 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-SECONDARY" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIsecondary\fP - enables serving a zone retrieved from a primary server. + +.SH "DESCRIPTION" +.PP +With \fIsecondary\fP you can transfer (via AXFR) a zone from another server. The retrieved zone is +\fInot committed\fP to disk (a violation of the RFC). This means restarting CoreDNS will cause it to +retrieve all secondary zones. + +.SH "SYNTAX" +.PP +.RS + +.nf +secondary [ZONES...] + +.fi +.RE + +.IP \(bu 4 +\fBZONES\fP zones it should be authoritative for. If empty, the zones from the configuration block +are used. Note that without a remote address to \fIget\fP the zone from, the above is not that useful. + + +.PP +A working syntax would be: + +.PP +.RS + +.nf +secondary [zones...] { + transfer from ADDRESS [ADDRESS...] +} + +.fi +.RE + +.IP \(bu 4 +\fB\fCtransfer from\fR specifies from which \fBADDRESS\fP to fetch the zone. It can be specified multiple +times; if one does not work, another will be tried. Transfering this zone outwards again can be +done by enabling the \fItransfer\fP plugin. + + +.PP +When a zone is due to be refreshed (refresh timer fires) a random jitter of 5 seconds is applied, +before fetching. In the case of retry this will be 2 seconds. If there are any errors during the +transfer in, the transfer fails; this will be logged. + +.SH "EXAMPLES" +.PP +Transfer \fB\fCexample.org\fR from 10.0.1.1, and if that fails try 10.1.2.1. + +.PP +.RS + +.nf +example.org { + secondary { + transfer from 10.0.1.1 10.1.2.1 + } +} + +.fi +.RE + +.PP +Or re-export the retrieved zone to other secondaries. + +.PP +.RS + +.nf +example.net { + secondary { + transfer from 10.1.2.1 + } + transfer { + to * + } +} + +.fi +.RE + +.SH "BUGS" +.PP +Only AXFR is supported and the retrieved zone is not committed to disk. + +.SH "SEE ALSO" +.PP +See the \fItransfer\fP plugin to enable zone transfers \fIto\fP other servers. + diff --git a/ag_201_coredns/man/coredns-sign.7 b/ag_201_coredns/man/coredns-sign.7 new file mode 100644 index 0000000..9ca4e5b --- /dev/null +++ b/ag_201_coredns/man/coredns-sign.7 @@ -0,0 +1,228 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-SIGN" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIsign\fP - adds DNSSEC records to zone files. + +.SH "DESCRIPTION" +.PP +The \fIsign\fP plugin is used to sign (see RFC 6781) zones. In this process DNSSEC resource records are +added. The signatures that sign the resource records sets have an expiration date, this means the +signing process must be repeated before this expiration data is reached. Otherwise the zone's data +will go BAD (RFC 4035, Section 5.5). The \fIsign\fP plugin takes care of this. + +.PP +Only NSEC is supported, \fIsign\fP does \fInot\fP support NSEC3. + +.PP +\fISign\fP works in conjunction with the \fIfile\fP and \fIauto\fP plugins; this plugin \fBsigns\fP the zones +files, \fIauto\fP and \fIfile\fP \fBserve\fP the zones \fIdata\fP. + +.PP +For this plugin to work at least one Common Signing Key, (see coredns-keygen(1)) is needed. This key +(or keys) will be used to sign the entire zone. \fISign\fP does \fInot\fP support the ZSK/KSK split, nor will +it do key or algorithm rollovers - it just signs. + +.PP +\fISign\fP will: + +.IP \(bu 4 +(Re)-sign the zone with the CSK(s) when: + +.RS +.IP \(en 4 +the last time it was signed is more than a 6 days ago. Each zone will have some jitter +applied to the inception date. +.IP \(en 4 +the signature only has 14 days left before expiring. + +.RE + + +Both these dates are only checked on the SOA's signature(s). +.IP \(bu 4 +Create RRSIGs that have an inception of -3 hours (minus a jitter between 0 and 18 hours) +and a expiration of +32 (plus a jitter between 0 and 5 days) days for every given DNSKEY. +.IP \(bu 4 +Add NSEC records for all names in the zone. The TTL for these is the negative cache TTL from the +SOA record. +.IP \(bu 4 +Add or replace \fIall\fP apex CDS/CDNSKEY records with the ones derived from the given keys. For +each key two CDS are created one with SHA1 and another with SHA256. +.IP \(bu 4 +Update the SOA's serial number to the \fIUnix epoch\fP of when the signing happens. This will +overwrite \fIany\fP previous serial number. + + +.PP +There are two ways that dictate when a zone is signed. Normally every 6 days (plus jitter) it will +be resigned. If for some reason we fail this check, the 14 days before expiring kicks in. + +.PP +Keys are named (following BIND9): \fB\fCK++.key\fR and \fB\fCK++.private\fR. +The keys \fBmust not\fP be included in your zone; they will be added by \fIsign\fP. These keys can be +generated with \fB\fCcoredns-keygen\fR or BIND9's \fB\fCdnssec-keygen\fR. You don't have to adhere to this naming +scheme, but then you need to name your keys explicitly, see the \fB\fCkeys file\fR directive. + +.PP +A generated zone is written out in a file named \fB\fCdb..signed\fR in the directory named by the +\fB\fCdirectory\fR directive (which defaults to \fB\fC/var/lib/coredns\fR). + +.SH "SYNTAX" +.PP +.RS + +.nf +sign DBFILE [ZONES...] { + key file|directory KEY...|DIR... + directory DIR +} + +.fi +.RE + +.IP \(bu 4 +\fBDBFILE\fP the zone database file to read and parse. If the path is relative, the path from the +\fIroot\fP plugin will be prepended to it. +.IP \(bu 4 +\fBZONES\fP zones it should be sign for. If empty, the zones from the configuration block are +used. +.IP \(bu 4 +\fB\fCkey\fR specifies the key(s) (there can be multiple) to sign the zone. If \fB\fCfile\fR is +used the \fBKEY\fP's filenames are used as is. If \fB\fCdirectory\fR is used, \fIsign\fP will look in \fBDIR\fP +for \fB\fCK++\fR files. Any metadata in these files (Activate, Publish, etc.) is +\fIignored\fP. These keys must also be Key Signing Keys (KSK). +.IP \(bu 4 +\fB\fCdirectory\fR specifies the \fBDIR\fP where CoreDNS should save zones that have been signed. +If not given this defaults to \fB\fC/var/lib/coredns\fR. The zones are saved under the name +\fB\fCdb..signed\fR. If the path is relative the path from the \fIroot\fP plugin will be prepended +to it. + + +.PP +Keys can be generated with \fB\fCcoredns-keygen\fR, to create one for use in the \fIsign\fP plugin, use: +\fB\fCcoredns-keygen example.org\fR or \fB\fCdnssec-keygen -a ECDSAP256SHA256 -f KSK example.org\fR. + +.SH "EXAMPLES" +.PP +Sign the \fB\fCexample.org\fR zone contained in the file \fB\fCdb.example.org\fR and write the result to +\fB\fC./db.example.org.signed\fR to let the \fIfile\fP plugin pick it up and serve it. The keys used +are read from \fB\fC/etc/coredns/keys/Kexample.org.key\fR and \fB\fC/etc/coredns/keys/Kexample.org.private\fR. + +.PP +.RS + +.nf +example.org { + file db.example.org.signed + + sign db.example.org { + key file /etc/coredns/keys/Kexample.org + directory . + } +} + +.fi +.RE + +.PP +Running this leads to the following log output (note the timers in this example have been set to +shorter intervals). + +.PP +.RS + +.nf +[WARNING] plugin/file: Failed to open "open /tmp/db.example.org.signed: no such file or directory": trying again in 1m0s +[INFO] plugin/sign: Signing "example.org." because open /tmp/db.example.org.signed: no such file or directory +[INFO] plugin/sign: Successfully signed zone "example.org." in "/tmp/db.example.org.signed" with key tags "59725" and 1564766865 SOA serial, elapsed 9.357933ms, next: 2019\-08\-02T22:27:45.270Z +[INFO] plugin/file: Successfully reloaded zone "example.org." in "/tmp/db.example.org.signed" with serial 1564766865 + +.fi +.RE + +.PP +Or use a single zone file for \fImultiple\fP zones, note that the \fBZONES\fP are repeated for both plugins. +Also note this outputs \fImultiple\fP signed output files. Here we use the default output directory +\fB\fC/var/lib/coredns\fR. + +.PP +.RS + +.nf +\&. { + file /var/lib/coredns/db.example.org.signed example.org + file /var/lib/coredns/db.example.net.signed example.net + sign db.example.org example.org example.net { + key directory /etc/coredns/keys + } +} + +.fi +.RE + +.PP +This is the same configuration, but the zones are put in the server block, but note that you still +need to specify what file is served for what zone in the \fIfile\fP plugin: + +.PP +.RS + +.nf +example.org example.net { + file var/lib/coredns/db.example.org.signed example.org + file var/lib/coredns/db.example.net.signed example.net + sign db.example.org { + key directory /etc/coredns/keys + } +} + +.fi +.RE + +.PP +Be careful to fully list the origins you want to sign, if you don't: + +.PP +.RS + +.nf +example.org example.net { + sign plugin/sign/testdata/db.example.org miek.org { + key file /etc/coredns/keys/Kexample.org + } +} + +.fi +.RE + +.PP +This will lead to \fB\fCdb.example.org\fR be signed \fItwice\fP, as this entire section is parsed twice because +you have specified the origins \fB\fCexample.org\fR and \fB\fCexample.net\fR in the server block. + +.PP +Forcibly resigning a zone can be accomplished by removing the signed zone file (CoreDNS will keep +on serving it from memory), and sending SIGUSR1 to the process to make it reload and resign the zone +file. + +.SH "SEE ALSO" +.PP +The DNSSEC RFCs: RFC 4033, RFC 4034 and RFC 4035. And the BCP on DNSSEC, RFC 6781. Further more the +manual pages coredns-keygen(1) and dnssec-keygen(8). And the \fIfile\fP plugin's documentation. + +.PP +Coredns-keygen can be found at +https://github.com/coredns/coredns-utils +\[la]https://github.com/coredns/coredns-utils\[ra] in the +coredns-keygen directory. + +.PP +Other useful DNSSEC tools can be found in ldns +\[la]https://nlnetlabs.nl/projects/ldns/about/\[ra], e.g. +\fB\fCldns-key2ds\fR to create DS records from DNSKEYs. + +.SH "BUGS" +.PP +\fB\fCkeys directory\fR is not implemented. + diff --git a/ag_201_coredns/man/coredns-template.7 b/ag_201_coredns/man/coredns-template.7 new file mode 100644 index 0000000..e13354c --- /dev/null +++ b/ag_201_coredns/man/coredns-template.7 @@ -0,0 +1,359 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-TEMPLATE" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fItemplate\fP - allows for dynamic responses based on the incoming query. + +.SH "DESCRIPTION" +.PP +The \fItemplate\fP plugin allows you to dynamically respond to queries by just writing a (Go) template. + +.SH "SYNTAX" +.PP +.RS + +.nf +template CLASS TYPE [ZONE...] { + match REGEX... + answer RR + additional RR + authority RR + rcode CODE + fallthrough [ZONE...] +} + +.fi +.RE + +.IP \(bu 4 +\fBCLASS\fP the query class (usually IN or ANY). +.IP \(bu 4 +\fBTYPE\fP the query type (A, PTR, ... can be ANY to match all types). +.IP \(bu 4 +\fBZONE\fP the zone scope(s) for this template. Defaults to the server zones. +.IP \(bu 4 +\fBREGEX\fP Go regexp +\[la]https://golang.org/pkg/regexp/\[ra] that are matched against the incoming question name. Specifying no regex matches everything (default: \fB\fC.*\fR). First matching regex wins. +.IP \(bu 4 +\fB\fCanswer|additional|authority\fR \fBRR\fP A RFC 1035 +\[la]https://tools.ietf.org/html/rfc1035#section-5\[ra] style resource record fragment +built by a Go template +\[la]https://golang.org/pkg/text/template/\[ra] that contains the reply. +.IP \(bu 4 +\fB\fCrcode\fR \fBCODE\fP A response code (\fB\fCNXDOMAIN, SERVFAIL, ...\fR). The default is \fB\fCSUCCESS\fR. +.IP \(bu 4 +\fB\fCfallthrough\fR Continue with the next plugin if the zone matched but no regex matched. +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 +At least one \fB\fCanswer\fR or \fB\fCrcode\fR directive is needed (e.g. \fB\fCrcode NXDOMAIN\fR). + +.PP +Also see +\[la]#also-see\[ra] contains an additional reading list. + +.SH "TEMPLATES" +.PP +Each resource record is a full-featured Go template +\[la]https://golang.org/pkg/text/template/\[ra] with the following predefined data + +.IP \(bu 4 +\fB\fC.Zone\fR the matched zone string (e.g. \fB\fCexample.\fR). +.IP \(bu 4 +\fB\fC.Name\fR the query name, as a string (lowercased). +.IP \(bu 4 +\fB\fC.Class\fR the query class (usually \fB\fCIN\fR). +.IP \(bu 4 +\fB\fC.Type\fR the RR type requested (e.g. \fB\fCPTR\fR). +.IP \(bu 4 +\fB\fC.Match\fR an array of all matches. \fB\fCindex .Match 0\fR refers to the whole match. +.IP \(bu 4 +\fB\fC.Group\fR a map of the named capture groups. +.IP \(bu 4 +\fB\fC.Message\fR the complete incoming DNS message. +.IP \(bu 4 +\fB\fC.Question\fR the matched question section. +.IP \(bu 4 +\fB\fC.Remote\fR client’s IP address +.IP \(bu 4 +\fB\fC.Meta\fR a function that takes a metadata name and returns the value, if the +metadata plugin is enabled. For example, \fB\fC.Meta "kubernetes/client-namespace"\fR + + +.PP +The output of the template must be a RFC 1035 +\[la]https://tools.ietf.org/html/rfc1035\[ra] style resource record (commonly referred to as a "zone file"). + +.PP +\fBWARNING\fP there is a syntactical problem with Go templates and CoreDNS config files. Expressions + like \fB\fC{{$var}}\fR will be interpreted as a reference to an environment variable by CoreDNS (and + Caddy) while \fB\fC{{ $var }}\fR will work. See Bugs +\[la]#bugs\[ra] and corefile(5). + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metrics are exported: + +.IP \(bu 4 +\fB\fCcoredns_template_matches_total{server, regex}\fR the total number of matched requests by regex. +.IP \(bu 4 +\fB\fCcoredns_template_template_failures_total{server, regex,section,template}\fR the number of times the Go templating failed. Regex, section and template label values can be used to map the error back to the config file. +.IP \(bu 4 +\fB\fCcoredns_template_rr_failures_total{server, regex,section,template}\fR the number of times the templated resource record was invalid and could not be parsed. Regex, section and template label values can be used to map the error back to the config file. + + +.PP +Both failure cases indicate a problem with the template configuration. The \fB\fCserver\fR label indicates +the server incrementing the metric, see the \fImetrics\fP plugin for details. + +.SH "EXAMPLES" +.SS "RESOLVE EVERYTHING TO NXDOMAIN" +.PP +The most simplistic template is + +.PP +.RS + +.nf +\&. { + template ANY ANY { + rcode NXDOMAIN + } +} + +.fi +.RE + +.IP 1\. 4 +This template uses the default zone (\fB\fC.\fR or all queries) +.IP 2\. 4 +All queries will be answered (no \fB\fCfallthrough\fR) +.IP 3\. 4 +The answer is always NXDOMAIN + + +.SS "RESOLVE .INVALID AS NXDOMAIN" +.PP +The \fB\fC.invalid\fR domain is a reserved TLD (see RFC 2606 Reserved Top Level DNS Names +\[la]https://tools.ietf.org/html/rfc2606#section-2\[ra]) to indicate invalid domains. + +.PP +.RS + +.nf +\&. { + forward . 8.8.8.8 + + template ANY ANY invalid { + rcode NXDOMAIN + authority "invalid. 60 {{ .Class }} SOA ns.invalid. hostmaster.invalid. (1 60 60 60 60)" + } +} + +.fi +.RE + +.IP 1\. 4 +A query to .invalid will result in NXDOMAIN (rcode) +.IP 2\. 4 +A dummy SOA record is sent to hand out a TTL of 60s for caching purposes +.IP 3\. 4 +Querying \fB\fC.invalid\fR in the \fB\fCCH\fR class will also cause a NXDOMAIN/SOA response +.IP 4\. 4 +The default regex is \fB\fC.*\fR + + +.SS "BLOCK INVALID SEARCH DOMAIN COMPLETIONS" +.PP +Imagine you run \fB\fCexample.com\fR with a datacenter \fB\fCdc1.example.com\fR. The datacenter domain +is part of the DNS search domain. +However \fB\fCsomething.example.com.dc1.example.com\fR would indicate a fully qualified +domain name (\fB\fCsomething.example.com\fR) that inadvertently has the default domain or search +path (\fB\fCdc1.example.com\fR) added. + +.PP +.RS + +.nf +\&. { + forward . 8.8.8.8 + + template IN ANY example.com.dc1.example.com { + rcode NXDOMAIN + authority "{{ .Zone }} 60 IN SOA ns.example.com hostmaster.example.com (1 60 60 60 60)" + } +} + +.fi +.RE + +.PP +A more verbose regex based equivalent would be + +.PP +.RS + +.nf +\&. { + forward . 8.8.8.8 + + template IN ANY example.com { + match "example\\.com\\.(dc1\\.example\\.com\\.)$" + rcode NXDOMAIN + authority "{{ index .Match 1 }} 60 IN SOA ns.{{ index .Match 1 }} hostmaster.{{ index .Match 1 }} (1 60 60 60 60)" + fallthrough + } +} + +.fi +.RE + +.PP +The regex-based version can do more complex matching/templating while zone-based templating is easier to read and use. + +.SS "RESOLVE A/PTR FOR .EXAMPLE" +.PP +.RS + +.nf +\&. { + forward . 8.8.8.8 + + # ip\-a\-b\-c\-d.example A a.b.c.d + + template IN A example { + match (^|[.])ip\-(?P[0\-9]*)\-(?P[0\-9]*)\-(?P[0\-9]*)\-(?P[0\-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN A {{ .Group.a }}.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + fallthrough + } + + # d.c.b.a.in\-addr.arpa PTR ip\-a\-b\-c\-d.example + + template IN PTR in\-addr.arpa { + match ^(?P[0\-9]*)[.](?P[0\-9]*)[.](?P[0\-9]*)[.](?P[0\-9]*)[.]in\-addr[.]arpa[.]$ + answer "{{ .Name }} 60 IN PTR ip\-{{ .Group.a }}\-{{ .Group.b }}\-{{ .Group.c }}\-{{ .Group.d }}.example." + } +} + +.fi +.RE + +.PP +An IPv4 address consists of 4 bytes, \fB\fCa.b.c.d\fR. Named groups make it less error-prone to reverse the +IP address in the PTR case. Try to use named groups to explain what your regex and template are doing. + +.PP +Note that the A record is actually a wildcard: any subdomain of the IP address will resolve to the IP address. + +.PP +Having templates to map certain PTR/A pairs is a common pattern. + +.PP +Fallthrough is needed for mixed domains where only some responses are templated. + +.SS "RESOLVE MULTIPLE IP PATTERNS" +.PP +.RS + +.nf +\&. { + forward . 8.8.8.8 + + template IN A example { + match "^ip\-(?P10)\-(?P[0\-9]*)\-(?P[0\-9]*)\-(?P[0\-9]*)[.]dc[.]example[.]$" + match "^(?P[0\-9]*)[.](?P[0\-9]*)[.](?P[0\-9]*)[.](?P[0\-9]*)[.]ext[.]example[.]$" + answer "{{ .Name }} 60 IN A {{ .Group.a}}.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + fallthrough + } +} + +.fi +.RE + +.PP +Named capture groups can be used to template one response for multiple patterns. + +.SS "RESOLVE A AND MX RECORDS FOR IP TEMPLATES IN .EXAMPLE" +.PP +.RS + +.nf +\&. { + forward . 8.8.8.8 + + template IN A example { + match ^ip\-10\-(?P[0\-9]*)\-(?P[0\-9]*)\-(?P[0\-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + fallthrough + } + template IN MX example { + match ^ip\-10\-(?P[0\-9]*)\-(?P[0\-9]*)\-(?P[0\-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN MX 10 {{ .Name }}" + additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + fallthrough + } +} + +.fi +.RE + +.SS "ADDING AUTHORITATIVE NAMESERVERS TO THE RESPONSE" +.PP +.RS + +.nf +\&. { + forward . 8.8.8.8 + + template IN A example { + match ^ip\-10\-(?P[0\-9]*)\-(?P[0\-9]*)\-(?P[0\-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + authority "example. 60 IN NS ns0.example." + authority "example. 60 IN NS ns1.example." + additional "ns0.example. 60 IN A 203.0.113.8" + additional "ns1.example. 60 IN A 198.51.100.8" + fallthrough + } + template IN MX example { + match ^ip\-10\-(?P[0\-9]*)\-(?P[0\-9]*)\-(?P[0\-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN MX 10 {{ .Name }}" + additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + authority "example. 60 IN NS ns0.example." + authority "example. 60 IN NS ns1.example." + additional "ns0.example. 60 IN A 203.0.113.8" + additional "ns1.example. 60 IN A 198.51.100.8" + fallthrough + } +} + +.fi +.RE + +.SH "ALSO SEE" +.IP \(bu 4 +Go regexp +\[la]https://golang.org/pkg/regexp/\[ra] for details about the regex implementation +.IP \(bu 4 +RE2 syntax reference +\[la]https://github.com/google/re2/wiki/Syntax\[ra] for details about the regex syntax +.IP \(bu 4 +RFC 1034 +\[la]https://tools.ietf.org/html/rfc1034#section-3.6.1\[ra] and RFC 1035 +\[la]https://tools.ietf.org/html/rfc1035#section-5\[ra] for the resource record format +.IP \(bu 4 +Go template +\[la]https://golang.org/pkg/text/template/\[ra] for the template language reference + + +.SH "BUGS" +.PP +CoreDNS supports caddyfile environment variables +\[la]https://caddyserver.com/docs/caddyfile#env\[ra] +with notion of \fB\fC{$ENV_VAR}\fR. This parser feature will break Go template variables +\[la]https://golang.org/pkg/text/template/#hdr-Variables\[ra] notations like\fB\fC{{$variable}}\fR. +The equivalent notation \fB\fC{{ $variable }}\fR will work. +Try to avoid Go template variables in the context of this plugin. + diff --git a/ag_201_coredns/man/coredns-tls.7 b/ag_201_coredns/man/coredns-tls.7 new file mode 100644 index 0000000..9021fb9 --- /dev/null +++ b/ag_201_coredns/man/coredns-tls.7 @@ -0,0 +1,95 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-TLS" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fItls\fP - allows you to configure the server certificates for the TLS and gRPC servers. + +.SH "DESCRIPTION" +.PP +CoreDNS supports queries that are encrypted using TLS (DNS over Transport Layer Security, RFC 7858) +or are using gRPC (https://grpc.io/ +\[la]https://grpc.io/\[ra], not an IETF standard). Normally DNS traffic isn't encrypted at +all (DNSSEC only signs resource records). + +.PP +The \fItls\fP "plugin" allows you to configure the cryptographic keys that are needed for both +DNS-over-TLS and DNS-over-gRPC. If the \fItls\fP plugin is omitted, then no encryption takes place. + +.PP +The gRPC protobuffer is defined in \fB\fCpb/dns.proto\fR. It defines the proto as a simple wrapper for the +wire data of a DNS message. + +.SH "SYNTAX" +.PP +.RS + +.nf +tls CERT KEY [CA] + +.fi +.RE + +.PP +Parameter CA is optional. If not set, system CAs can be used to verify the client certificate + +.PP +.RS + +.nf +tls CERT KEY [CA] { + client\_auth nocert|request|require|verify\_if\_given|require\_and\_verify +} + +.fi +.RE + +.PP +If client_auth option is specified, it controls the client authentication policy. +The option value corresponds to the ClientAuthType values of the Go tls package +\[la]https://golang.org/pkg/crypto/tls/#ClientAuthType\[ra]: NoClientCert, RequestClientCert, RequireAnyClientCert, VerifyClientCertIfGiven, and RequireAndVerifyClientCert, respectively. +The default is "nocert". Note that it makes no sense to specify parameter CA unless this option is +set to verify_if_given or require_and_verify. + +.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. + +.PP +.RS + +.nf +tls://.:5553 { + tls cert.pem key.pem ca.pem + forward . /etc/resolv.conf +} + +.fi +.RE + +.PP +Start a DNS-over-gRPC server that is similar to the previous example, but using DNS-over-gRPC for +incoming queries. + +.PP +.RS + +.nf +grpc://. { + tls cert.pem key.pem ca.pem + forward . /etc/resolv.conf +} + +.fi +.RE + +.PP +Only Knot DNS' \fB\fCkdig\fR supports DNS-over-TLS queries, no command line client supports gRPC making +debugging these transports harder than it should be. + +.SH "SEE ALSO" +.PP +RFC 7858 and https://grpc.io +\[la]https://grpc.io\[ra]. + diff --git a/ag_201_coredns/man/coredns-trace.7 b/ag_201_coredns/man/coredns-trace.7 new file mode 100644 index 0000000..caf6854 --- /dev/null +++ b/ag_201_coredns/man/coredns-trace.7 @@ -0,0 +1,156 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-TRACE" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fItrace\fP - enables OpenTracing-based tracing of DNS requests as they go through the plugin chain. + +.SH "DESCRIPTION" +.PP +With \fItrace\fP you enable OpenTracing of how a request flows through CoreDNS. Enable the \fIdebug\fP +plugin to get logs from the trace plugin. + +.SH "SYNTAX" +.PP +The simplest form is just: + +.PP +.RS + +.nf +trace [ENDPOINT\-TYPE] [ENDPOINT] + +.fi +.RE + +.IP \(bu 4 +\fBENDPOINT-TYPE\fP is the type of tracing destination. Currently only \fB\fCzipkin\fR and \fB\fCdatadog\fR are supported. +Defaults to \fB\fCzipkin\fR. +.IP \(bu 4 +\fBENDPOINT\fP is the tracing destination, and defaults to \fB\fClocalhost:9411\fR. For Zipkin, if +\fBENDPOINT\fP does not begin with \fB\fChttp\fR, then it will be transformed to \fB\fChttp://ENDPOINT/api/v1/spans\fR. + + +.PP +With this form, all queries will be traced. + +.PP +Additional features can be enabled with this syntax: + +.PP +.RS + +.nf +trace [ENDPOINT\-TYPE] [ENDPOINT] { + every AMOUNT + service NAME + client\_server + datadog\_analytics\_rate RATE +} + +.fi +.RE + +.IP \(bu 4 +\fB\fCevery\fR \fBAMOUNT\fP will only trace one query of each AMOUNT queries. For example, to trace 1 in every +100 queries, use AMOUNT of 100. The default is 1. +.IP \(bu 4 +\fB\fCservice\fR \fBNAME\fP allows you to specify the service name reported to the tracing server. +Default is \fB\fCcoredns\fR. +.IP \(bu 4 +\fB\fCclient_server\fR will enable the \fB\fCClientServerSameSpan\fR OpenTracing feature. +.IP \(bu 4 +\fB\fCdatadog_analytics_rate\fR \fBRATE\fP will enable trace analytics +\[la]https://docs.datadoghq.com/tracing/app_analytics\[ra] on the traces sent +from \fI0\fP to \fI1\fP, \fI1\fP being every trace sent will be analyzed. This is a datadog only feature +(\fBENDPOINT-TYPE\fP needs to be \fB\fCdatadog\fR) + + +.SH "ZIPKIN" +.PP +You can run Zipkin on a Docker host like this: + +.PP +.RS + +.nf +docker run \-d \-p 9411:9411 openzipkin/zipkin + +.fi +.RE + +.PP +Note the zipkin provider does not support the v1 API since coredns 1.7.1. + +.SH "EXAMPLES" +.PP +Use an alternative Zipkin address: + +.PP +.RS + +.nf +trace tracinghost:9253 + +.fi +.RE + +.PP +or + +.PP +.RS + +.nf +\&. { + trace zipkin tracinghost:9253 +} + +.fi +.RE + +.PP +If for some reason you are using an API reverse proxy or something and need to remap +the standard Zipkin URL you can do something like: + +.PP +.RS + +.nf +trace http://tracinghost:9411/zipkin/api/v1/spans + +.fi +.RE + +.PP +Using DataDog: + +.PP +.RS + +.nf +trace datadog localhost:8126 + +.fi +.RE + +.PP +Trace one query every 10000 queries, rename the service, and enable same span: + +.PP +.RS + +.nf +trace tracinghost:9411 { + every 10000 + service dnsproxy + client\_server +} + +.fi +.RE + +.SH "SEE ALSO" +.PP +See the \fIdebug\fP plugin for more information about debug logging. + diff --git a/ag_201_coredns/man/coredns-transfer.7 b/ag_201_coredns/man/coredns-transfer.7 new file mode 100644 index 0000000..c7af522 --- /dev/null +++ b/ag_201_coredns/man/coredns-transfer.7 @@ -0,0 +1,50 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-TRANSFER" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fItransfer\fP - perform (outgoing) zone transfers for other plugins. + +.SH "DESCRIPTION" +.PP +This plugin answers zone transfers for authoritative plugins that implement \fB\fCtransfer.Transferer\fR. + +.PP +\fItransfer\fP answers full zone transfer (AXFR) requests and incremental zone transfer (IXFR) requests +with AXFR fallback if the zone has changed. + +.PP +When a plugin wants to notify it's secondaries it will call back into the \fItransfer\fP plugin. + +.PP +The following plugins implement zone transfers using this plugin: \fIfile\fP, \fIauto\fP, \fIsecondary\fP, and +\fIkubernetes\fP. See \fB\fCtransfer.go\fR for implementation details if you are a plugin author that wants to +use this plugin. + +.SH "SYNTAX" +.PP +.RS + +.nf +transfer [ZONE...] { + to ADDRESS... +} + +.fi +.RE + +.IP \(bu 4 +\fBZONE\fP The zones \fItransfer\fP will answer zone transfer requests for. If left blank, the zones +are inherited from the enclosing server block. To answer zone transfers for a given zone, +there must be another plugin in the same server block that serves the same zone, and implements +\fB\fCtransfer.Transferer\fR. +.IP \(bu 4 +\fB\fCto\fR \fBADDRESS...\fP The hosts \fItransfer\fP will transfer to. Use \fB\fC*\fR to permit transfers to all +addresses. \fBADDRESS\fP must be denoted in CIDR notation (e.g., 127.0.0.1/32) or just as plain +addresses. \fB\fCto\fR may be specified multiple times. + + +.SH "EXAMPLES" +.PP +See the specific plugins using this plugin for examples on it's usage. + diff --git a/ag_201_coredns/man/coredns-tsig.7 b/ag_201_coredns/man/coredns-tsig.7 new file mode 100644 index 0000000..9716515 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/man/coredns-view.7 b/ag_201_coredns/man/coredns-view.7 new file mode 100644 index 0000000..0eb2d88 --- /dev/null +++ b/ag_201_coredns/man/coredns-view.7 @@ -0,0 +1,184 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-VIEW" 7 "September 2022" "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 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 + } +} + +\&. { + 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 antonmedv/expr package (https://github.com/antonmedv/expr +\[la]https://github.com/antonmedv/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/antonmedv/expr/blob/master/docs/Language-Definition.md +\[la]https://github.com/antonmedv/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) +.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 : client'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/ag_201_coredns/man/coredns-whoami.7 b/ag_201_coredns/man/coredns-whoami.7 new file mode 100644 index 0000000..24694e7 --- /dev/null +++ b/ag_201_coredns/man/coredns-whoami.7 @@ -0,0 +1,82 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-WHOAMI" 7 "March 2021" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIwhoami\fP - returns your resolver's local IP address, port and transport. + +.SH "DESCRIPTION" +.PP +The \fIwhoami\fP plugin is not really that useful, but can be used for having a simple (fast) endpoint +to test clients against. When \fIwhoami\fP returns a response it will have your client's IP address in +the additional section as either an A or AAAA record. + +.PP +The reply always has an empty answer section. The port and transport are included in the additional +section as a SRV record, transport can be "tcp" or "udp". + +.PP +.RS + +.nf +\&.\_.qname. 0 IN SRV 0 0 . + +.fi +.RE + +.PP +The \fIwhoami\fP plugin will respond to every A or AAAA query, regardless of the query name. + +.PP +If CoreDNS can't find a Corefile on startup this is the \fIdefault\fP plugin that gets loaded. As such +it can be used to check that CoreDNS is responding to queries. Other than that this plugin is of +limited use in production. + +.SH "SYNTAX" +.PP +.RS + +.nf +whoami + +.fi +.RE + +.SH "EXAMPLES" +.PP +Start a server on the default port and load the \fIwhoami\fP plugin. + +.PP +.RS + +.nf +example.org { + whoami +} + +.fi +.RE + +.PP +When queried for "example.org A", CoreDNS will respond with: + +.PP +.RS + +.nf +;; QUESTION SECTION: +;example.org. IN A + +;; ADDITIONAL SECTION: +example.org. 0 IN A 10.240.0.1 +\_udp.example.org. 0 IN SRV 0 0 40212 + +.fi +.RE + +.SH "SEE ALSO" +.PP +Read the blog post +\[la]https://coredns.io/2017/03/01/how-to-add-plugins-to-coredns/\[ra] on how this plugin is built, or explore the source code +\[la]https://github.com/coredns/coredns/blob/master/plugin/whoami/\[ra]. + diff --git a/ag_201_coredns/man/coredns.1 b/ag_201_coredns/man/coredns.1 new file mode 100644 index 0000000..b495009 --- /dev/null +++ b/ag_201_coredns/man/coredns.1 @@ -0,0 +1,62 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS" 1 "May 2021" "CoreDNS" "CoreDNS" + +.SH "COREDNS" +.PP +\fIcoredns\fP - pluggable DNS nameserver optimized for service discovery and flexibility. + +.SH "SYNOPSIS" +.PP +\fIcoredns\fP \fB[-conf FILE]\fP \fB[-dns.port PORT}\fP \fB[OPTION]\fP... + +.SH "DESCRIPTION" +.PP +CoreDNS is a DNS server that chains plugins. Each plugin handles a DNS feature, like rewriting +queries, kubernetes service discovery or just exporting metrics. There are many other plugins, +each described on https://coredns.io/plugins +\[la]https://coredns.io/plugins\[ra] and their respective manual pages. Plugins not +bundled by default in CoreDNS are listed on https://coredns.io/explugins +\[la]https://coredns.io/explugins\[ra]. + +.PP +When started without options CoreDNS will look for a file named \fB\fCCorefile\fR in the current +directory, if found, it will parse its contents and start up accordingly. If no \fB\fCCorefile\fR is found +it will start with the \fIwhoami\fP plugin (coredns-whoami(7)) and start listening on port 53 (unless +overridden with \fB\fC-dns.port\fR). + +.PP +Available options: + +.TP +\fB-conf\fP \fBFILE\fP +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 or \fB-p\fP \fBPORT\fP +override default port (53) to listen on. +.TP +\fB-pidfile\fP \fBFILE\fP +write PID to \fBFILE\fP. +.TP +\fB-plugins\fP +list all plugins and quit. +.TP +\fB-quiet\fP +don't print any version and port information on startup. +.TP +\fB-version\fP +show version and quit. + + +.SH "AUTHORS" +.PP +CoreDNS Authors. + +.SH "COPYRIGHT" +.PP +Apache License 2.0 + +.SH "SEE ALSO" +.PP +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/ag_201_coredns/man/corefile.5 b/ag_201_coredns/man/corefile.5 new file mode 100644 index 0000000..d24a35e --- /dev/null +++ b/ag_201_coredns/man/corefile.5 @@ -0,0 +1,207 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREFILE" 5 "March 2021" "CoreDNS" "CoreDNS" + +.SH "NAME" +.PP +\fIcorefile\fP - configuration file for CoreDNS. + +.SH "DESCRIPTION" +.PP +A \fIcorefile\fP specifies the internal servers CoreDNS should run and what plugins each of these +should chain. The syntax is as follows: + +.PP +.RS + +.nf +[SCHEME://]ZONE [[SCHEME://]ZONE]...[:PORT] { + [PLUGIN]... +} + +.fi +.RE + +.PP +The \fBZONE\fP defines for which name this server should be called, multiple zones are allowed and +should be \fIwhite space\fP separated. You can use a "reverse" syntax to specify a reverse zone (i.e. +ip6.arpa and in-addr.arpa), by using an IP address in the CIDR notation. + +.PP +The optional \fBSCHEME\fP defaults to \fB\fCdns://\fR, but can also be \fB\fCtls://\fR (DNS over TLS), \fB\fCgrpc://\fR +(DNS over gRPC) or \fB\fChttps://\fR (DNS over HTTP/2). + +.PP +The optional \fBPORT\fP controls on which port the server will bind, this default to 53. If you use +a port number here, you \fIcan't\fP override it with \fB\fC-dns.port\fR (coredns(1)), also see coredns-bind(7). + +.PP +Specifying a \fBZONE\fP \fIand\fP \fBPORT\fP combination multiple times for \fIdifferent\fP servers will lead to +an error on startup. + +.PP +When a query comes in, it is matched again all zones for all servers, the server with the longest +match on the query name will receive the query. + +.PP +\fBPLUGIN\fP defines the plugin(s) we want to load into this server. This is optional as well, but as +server with no plugins will just return SERVFAIL for all queries. Each plugin can have a number of +properties than can have arguments, see the documentation for each plugin. + +.PP +Comments are allowed and begin with an unquoted hash \fB\fC#\fR and continue to the end of the line. +Comments may be started anywhere on a line. + +.PP +Environment variables are supported and either the Unix or Windows form may be used: \fB\fC{$ENV_VAR_1}\fR +or \fB\fC{%ENV_VAR_2%}\fR. + +.PP +You can use the \fB\fCimport\fR "plugin" (See coredns-import(7)) to include parts of other files. + +.PP +If CoreDNS can’t find a Corefile to load it loads the following builtin one: + +.PP +.RS + +.nf +\&. { + whoami + log +} + +.fi +.RE + +.SH "IMPORT" +.PP +You can use the \fB\fCimport\fR "plugin" to include parts of other files, see +https://coredns.io/plugins/import +\[la]https://coredns.io/plugins/import\[ra], and coredns-import(7). + +.SH "SNIPPETS" +.PP +If you want to reuse a snippet you can define one with and then use it with \fIimport\fP. + +.PP +.RS + +.nf +(mysnippet) { + log + whoami +} + +\&. { + import mysnippet +} + +.fi +.RE + +.SH "EXAMPLES" +.PP +The \fBZONE\fP is root zone \fB\fC.\fR, the \fBPLUGIN\fP is \fIchaos\fP. The \fIchaos\fP plugin takes an (optional) argument: +\fB\fCCoreDNS-001\fR. This text is returned on a CH class query: \fB\fCdig CH TXT version.bind @localhost\fR. + +.PP +.RS + +.nf +\&. { + chaos CoreDNS\-001 +} + +.fi +.RE + +.PP +When defining a new zone, you either create a new server, or add it to an existing one. Here we +define one server that handles two zones; that potentially chain different plugins: + +.PP +.RS + +.nf +example.org { + whoami +} +org { + whoami +} + +.fi +.RE + +.PP +Is identical to: + +.PP +.RS + +.nf +example.org org { + whoami +} + +.fi +.RE + +.PP +Reverse zones can be specified as domain names: + +.PP +.RS + +.nf +0.0.10.in\-addr.arpa { + whoami +} + +.fi +.RE + +.PP +or by just using the CIDR notation: + +.PP +.RS + +.nf +10.0.0.0/24 { + whoami +} + +.fi +.RE + +.PP +This also works on a non octet boundary: + +.PP +.RS + +.nf +10.0.0.0/27 { + whoami +} + +.fi +.RE + +.SH "AUTHORS" +.PP +CoreDNS Authors. + +.SH "COPYRIGHT" +.PP +Apache License 2.0 + +.SH "SEE ALSO" +.PP +The manual page for CoreDNS: coredns(1) and more documentation on https://coredns.io +\[la]https://coredns.io\[ra]. +Also see the \fIimport\fP +\[la]https://coredns.io/plugins/import\[ra]'s documentation and all the manual pages +for the plugins. + diff --git a/ag_201_coredns/notes/coredns-0.9.10.md b/ag_201_coredns/notes/coredns-0.9.10.md new file mode 100644 index 0000000..913dc06 --- /dev/null +++ b/ag_201_coredns/notes/coredns-0.9.10.md @@ -0,0 +1,50 @@ ++++ +title = "CoreDNS-0.9.10 Release" +description = "CoreDNS-0.9.10 Release Notes." +tags = ["Release", "0.9.10", "Notes"] +draft = false +release = "0.9.10" +date = "2017-11-03T20:45:43-00:00" +author = "coredns" ++++ + +CoreDNS-0.9.10 has been [released](https://github.com/coredns/coredns/releases/tag/v0.9.10)! + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +Release 0.9.10 is a minor release, with some fixes. + +## Core + +* The reverse zone syntax was extended to allow non-octet boundaries: + + ~~~ + 192.168.1.0/17 { + ... + } + ~~~ + + Will behave correctly. + +* Lots of documentation clean ups. +* More platforms have binaries for each release. + +## Plugins + +* *dnssec* will now insert DS records (and sign them) when it signs a delegation response. +* *host* now checks for /etc/hosts updates in a separate go-routine. + +## Contributors + +The following people helped with getting this release done: +Chris O'Haver, +Miek Gieben, +Pat Moroney, +Paul Hoffman, +Sandeep Rajan, +Yong Tang. + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-0.9.9.md b/ag_201_coredns/notes/coredns-0.9.9.md new file mode 100644 index 0000000..9f05a3a --- /dev/null +++ b/ag_201_coredns/notes/coredns-0.9.9.md @@ -0,0 +1,70 @@ ++++ +title = "CoreDNS-0.9.9 Release" +description = "CoreDNS-0.9.9 Release Notes." +tags = ["Release", "0.9.9", "Notes"] +draft = false +release = "0.9.9" +date = "2017-10-18T11:37:43-04:00" +author = "coredns" ++++ + +CoreDNS-0.9.9 has been [released](https://github.com/coredns/coredns/releases/tag/v0.9.9)! +(yes, we've moved to [semver](https://coredns.io/2017/09/16/semantic-versioning/)) + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +Release 0.9.9 is a major release, with lots of fixes. + +## Core + +* We've renamed `middleware.Middleware` to `plugin.Plugin`. This is backwards incompatible for external ~~middleware~~ plugins, but you can update your plugin by just replacing `[Mm]iddleware` with `[Pp]lugin`: + ~~~ + sed 's/Middleware/Plugin/'g -i *.go + sed 's/middleware/plugin/'g -i *.go + ~~~ +From now on we'll use the term *plugin* in our documentation and code. + +* We've sent a proposal to make CoreDNS the default in Kubernetes: https://github.com/kubernetes/community/pull/1100 + +## Plugins + +* *etcd*'s debug queries are removed. +* *hosts* gets inline host definitions that add or overwrite those from `/etc/hosts`. +* *auto*, *file* now poll every minute for on disk changes (inotify wasn't working). +* *rewrite* can chain rules and perform multiple changes on a message. +* *kubernetes* uses `protobuf` to communicate with the kubernetes API and +performance improvements when there are a large number of services. +* *dnstap* saw several fixes, including sending tap messages out-of-band. +* *cache* apply configured TTL to first answer returned. + * Don't cache TTL=0 messages. +* *proxy* smaller timeouts and the health check GET was given a timeout. + * Better metrics: add a request counter metrics and change the 'from' label to 'to' so we count/duration per upstream. +* *dnssec* now signs NODATA responses. + +## External Plugins + +Two new [external plugins](/explugins) were added: + +* *ipecho* parses the IP out of a subdomain and echos it back as an record. +* *forward* facilitates proxying DNS messages to upstream resolvers. + +## Contributors + +The following people helped with getting this release done: + +antonkyrylenko, +Chris O'Haver, +Chris West, +Damian Myerscough, +Isolus, +John Belamaric, +Miek Gieben, +Sandeep Rajan, +Thong Huynh, +varyoo, +Yong Tang. + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-001.md b/ag_201_coredns/notes/coredns-001.md new file mode 100644 index 0000000..c27839a --- /dev/null +++ b/ag_201_coredns/notes/coredns-001.md @@ -0,0 +1,57 @@ ++++ +date = "2016-09-18T11:40:37+01:00" +description = "CoreDNS-001 Release Notes." +release = "001" +tags = ["Release", "001", "Notes"] +title = "CoreDNS-001 Release" +author = "coredns" ++++ + +CoreDNS-001 has been [released](https://github.com/coredns/coredns/releases). This is the first +release! It provides a complete DNS server, that also does DNSSEC and is useful for service +discovery in cloud setups. + +# What is CoreDNS? + +[CoreDNS](https://coredns.io) is a DNS server that started its life as a fork of the [Caddy +web(!)server](https://caddyserver.com). + +It chains [plugin](https://github.com/coredns/coredns/tree/master/plugin), +where each plugin implements some DNS feature. CoreDNS is a complete replacement +(with more features, and maybe less bugs) for [SkyDNS](https://github.com/skynetservices/skydns). + +It is also useful as a normal DNS server, featuring DNSSEC, on-the-fly signing and zone transfers. + +# What is New + +CoreDNS is now a (the first!) server type plugin in Caddy - this means we can leverage a lot of code +from Caddy without having to fork (and maintain) it all. By doing so we were able to remove 9000 +lines of code from CoreDNS. + +The core (ghe!) of CoreDNS is now in a good shape. Future work will focus on making the +plugin better, e.g. the proxy implementation is slow and needs to be +[faster](https://github.com/coredns/coredns/issues/184). + +## New Plugins + +* There is now a [specific + plugin](https://github.com/coredns/coredns/tree/master/plugin/kubernetes) to deal with [Kubernetes](https://kubernetes.io). +* The *bind* [plugin](https://github.com/coredns/coredns/tree/master/plugin/bind) allows you to bind to a specific IP address, instead of using the wildcard + address. +* A *whoami* [plugin](https://github.com/coredns/coredns/tree/master/plugin/whoami) reports + back your address and port. +* All other plugins are reworked to fit in the new plugin framework from Caddy version 0.9 (and + up). + +The *whoami* plugin is also used when CoreDNS starts up and can't find a Corefile. + +# Contributors + +The following people helped with getting this release done: + +Cricket Liu, elcore, Félix Cantournet, Ilya Dmitrichenko, Joe Blow, Lee, Matt Layher, +Michael Richmond, Miek Gieben, pixelbender, Yong Tang. + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-002.md b/ag_201_coredns/notes/coredns-002.md new file mode 100644 index 0000000..660da85 --- /dev/null +++ b/ag_201_coredns/notes/coredns-002.md @@ -0,0 +1,108 @@ ++++ +date = "2016-10-19T19:09:32Z" +description = "CoreDNS-002 Release Notes." +release = "002" +tags = ["Release", "002", "Notes"] +title = "CoreDNS-002 Release" +author = "coredns" ++++ + +CoreDNS-002 has been [released](https://github.com/coredns/coredns/releases)! + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +## What is New + +* `-port` was renamed to `-dns.port` to avoid clashing with Caddy's `-port` (which was renamed to + `http.port`). +* Lumberjack logger was removed, this means no built in log rotation; use an external tool for that. +* Brushed up GoDoc for all packages. +* Brushed up all READMEs to be more standard and look like manual page. +* Golint-ed and go vet-ed the code - these can now (somewhat) useful tools before submitting PRs. +* Add more tests and show test coverage on submitting/PRs. +* Various Corefile parsing bugs fixed, better syntax error detection. + +## Plugin improvements: + +* plugin/root: a root plugin, same usage as in [Caddy](https://caddyserver.com/docs/root). + See plugin/root/README.md for its use in CoreDNS. + This makes stanzas like this shorter: + + ~~~ txt + .:53 { + file /etc/coredns/zones/db.example.net example.net + file /etc/coredns/zones/db.example.org example.org + file /etc/coredns/zones/db.example.com example.com + } + ~~~ + + Can be written as: + + ~~~ txt + .:53 { + root /etc/coredns/zones + file db.example.net example.net + file db.example.org example.org + file db.example.com example.com + } + ~~~ + +* plugin/auto: similar to the *file* plugin, but automatically picks up new zones. + The following Corefile will load all zones found under `/etc/coredns/org` and be authoritative + for `.org.`: + + ~~~ corefile + . { + auto org { + directory /etc/coredns/org + } + } + ~~~ +* plugin/file: handle wildcards better. +* plugin/kubernetes: TLS support for kubernetes and other improvements. +* plugin/cache: use an LRU cache to make it memory bounded. Added more option to have more + control on what is cached and for how long. The cache stanza was extended: + + ~~~ txt + . { + cache { + success CAPACITY [TTL] + denial CAPACITY [TTL] + } + } + ~~~ + + See plugin/cache/README.md for more details. + +* plugin/dnssec: replaced go-cache with golang-lru in dnssec. Also adds a `cache_capacity`. + option in dnssec plugin so that the capacity of the LRU cache could be specified in the config + file. +* plugin/logging: allow a response class to be specified on log on responses matching the name *and* + the response class. For instance only log denials for example.com: + + ~~~ corefile + . { + log example.com { + class denial + } + } + ~~~ + +* plugin/proxy: performance improvements. + +# Contributors + +The following people helped with getting this release done: + +Chris O'Haver, +Manuel de Brito Fontes, +Miek Gieben, +Shawn Smith, +Silas Baronda, +Yong Tang, +Zhipeng Jiang. + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-003.md b/ag_201_coredns/notes/coredns-003.md new file mode 100644 index 0000000..ce43b68 --- /dev/null +++ b/ag_201_coredns/notes/coredns-003.md @@ -0,0 +1,50 @@ ++++ +date = "2016-11-11T16:38:32Z" +description = "CoreDNS-003 Release Notes." +release = "003" +tags = ["Release", "003", "Notes"] +title = "CoreDNS-003 Release" +author = "coredns" ++++ + +CoreDNS-003 has been [released](https://github.com/coredns/coredns/releases)! + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +# What is New + +## Core + +Refused queries are properly logged and exported if metrics are enabled. + +## Plugin improvements + +* *proxy*: allow `/etc/resolv.conf` to be used in the configuration. +* *metrics*: add tests and normalize some of the metrics. Removed the AXFR size metrics. +* *cache*: Added size and capacity of the cache (for both `denial` and `success` cache types). + Don't cache meta data records and zone transfers. +* *dnssec*: metrics were unused, hooked them up: export size and capacity of the signature cache. +* *loadbalance*: balance MX records as well. +* *auto*: numerous bugfixes. +* *file*: fix data race in reload process and also reload a zone when it is `mv`ed (newly created) into place. + Also rewrite the zone lookup algorithm and be more standards compliant, esp. in the area of DNSSEC, wildcards and empty-non-terminals; handle secure delegations. +* *kubernetes*: vendor the k8s dependency and updates to be compatible with Kubernetes 1.4 and 1.5. + Multiple cleanups and fixes. Kubernetes services can now be resolved. + +# Contributors + +The following people helped with getting this release done: + +Ben Kochie, +Chris O'Haver, +John Belamaric, +Jonathan Dickinson, +Manuel Alejandro de Brito Fontes, +Michael Grosser, +Miek Gieben, +Yong Tang. + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-004.md b/ag_201_coredns/notes/coredns-004.md new file mode 100644 index 0000000..5360d13 --- /dev/null +++ b/ag_201_coredns/notes/coredns-004.md @@ -0,0 +1,45 @@ ++++ +date = "2017-01-01T10:30:31Z" +description = "CoreDNS-004 Release Notes." +release = "004" +tags = ["Release", "004", "Notes"] +title = "CoreDNS-004 Release" +author = "coredns" ++++ + +CoreDNS-004 has been [released](https://github.com/coredns/coredns/releases/tag/v004)! + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +# What is New + +## Core + +We are now also releasing an ARM build that can run on Raspberry Pi. + +## Plugin improvements + +* *file|auto*: resolve external CNAME when an upstream (new option) is specified. +* *file|auto*: allow port numbers for transfer from/to to be specified. +* *file|auto*: include zone's NSset in positive responses. +* *auto*: close files and don't leak file descriptors. +* *httpproxy*: new plugin that proxies to and resolves your requests over an encrypted connection. This plugin will probably be morphed into proxy at some point in the feature. Consider it experimental for the time being. +* *metrics*: `response_size_bytes` and `request_size_bytes` export the actual length of the packet, not the advertised bufsize. +* *log*: `{size}` is now the length in bytes of the request, `{rsize}` for the reply. Default logging is changed to show both. + +# Contributors + +The following people helped with getting this release done: + +Chris O'Haver, +Dmytro Kislov, +John Belamaric, +Mark Nevill, +Michael Grosser, +Miek Gieben, +Yong Tang. + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-005.md b/ag_201_coredns/notes/coredns-005.md new file mode 100644 index 0000000..aa6fa96 --- /dev/null +++ b/ag_201_coredns/notes/coredns-005.md @@ -0,0 +1,65 @@ ++++ +date = "2017-02-09T18:50:31Z" +description = "CoreDNS-005 Release Notes." +tags = ["Release", "005", "Notes"] +release = "005" +title = "CoreDNS-005 Release" +author = "coredns" ++++ + +CoreDNS-005 has been [released](https://github.com/coredns/coredns/releases/tag/v005)! + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +# What is New + +# Core + +A way to configure (external) plugin was added. Edit `plugin.cfg` and do a `go generate && go +build` and your plugin has been added. This allows for out-of-tree plugin to be easily +added. Documentation can be found in +[plugin.cfg](https://github.com/coredns/coredns/blob/master/plugin.cfg). + +## Plugin improvements + +### New + +* *erratic*: a new plugin that can drop queries, limited in the current functionality, but useful for testing. +* *trace*: a new plugin that implements OpenTracing-based tracing using Zipkin. + +### Improvements/changes + +* *proxy*: fix a bug when a connection hangs and never gets release (#467) +* *proxy*: Fold *httpproxy* into it, which is now a normal proxy with a special `protocol`. For + Monitoring an extra label was added: `proxy_proto` that shows the protocol used (`dns` or `https_google`). + See the [proxy README.md](https://github.com/coredns/coredns/blob/master/plugin/proxy/README.md) for details. +* *httpproxy*: removed because functionality is moved to *proxy*. +* *kubernetes*: Now implements the full + [Kubernetes DNS Specification](https://github.com/kubernetes/dns/blob/master/docs/specification.md), + including regular and headless services, endpoint hostnames, A, SRV, and PTR records. +* *kubernetes*: Implements the `pod` type for requests in both a Kube-DNS compatible mode + (`insecure`) and a mode which validates that the IP in question belongs to a pod in the specified + namespace (`verified`) +* *kubernetes*: Simplified the configuration of reverse zones. Instead of listing the zones in the + zone list, you can just add a list of CIDRs using the `cidrs` option. +* *rewrite*: allow rewriting more bits of the incoming packet. This required some backward + *incompatible* changes, e.g. a new **FIELD** keyword is now required. See the + [rewrite README.md](https://github.com/coredns/coredns/blob/master/plugin/rewrite/README.md) for details. + + +# Contributors + +The following people helped with getting this release done: + +Bob Wasniak, +Chris O'Haver, +devnev, +Dmytro Kislov, +John Belamaric, +Miek Gieben, +Yong Tang. + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-006.md b/ag_201_coredns/notes/coredns-006.md new file mode 100644 index 0000000..981949f --- /dev/null +++ b/ag_201_coredns/notes/coredns-006.md @@ -0,0 +1,55 @@ ++++ +date = "2017-02-22T21:26:11Z" +description = "CoreDNS-006 Release Notes." +tags = ["Release", "006", "Notes"] +release = "006" +title = "CoreDNS-006 Release" +author = "coredns" ++++ + +CoreDNS-006 has been [released](https://github.com/coredns/coredns/releases/tag/v006)! + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +# What is New + +# Core + +Move CoreDNS to together with several other repos. This will be +the new home for CoreDNS development. + +Fixed: + +* Fix hot-reloading. This would fail with `[ERROR] SIGUSR1: listen tcp :53: bind: address already in + use`. +* Allow removal of core plugin, see comments in + [plugin.cfg](https://github.com/miekg/coredns/blob/master/plugin.cfg). + +## Plugin improvements + +### New + +* *reverse* plugin: allows CoreDNS to respond dynamically to an PTR request and the related + A/AAAA request. + +### Improvements/changes + +* *proxy* a new `protocol`: `grpc`: speak DNS over gRPC. Server side impl. resides [in this out of + tree plugin](https://github.com/coredns/grpc). +* *file* additional section processing for MX and SRV queries. +* *prometheus* fix hot reloading +* *trace* various improvements + +# Contributors + +The following people helped with getting this release done: + +John Belamaric, +Miek Gieben, +Richard Hillmann, +Yong Tang, + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-007.md b/ag_201_coredns/notes/coredns-007.md new file mode 100644 index 0000000..d723c5f --- /dev/null +++ b/ag_201_coredns/notes/coredns-007.md @@ -0,0 +1,69 @@ ++++ +date = "2017-05-03T19:26:11Z" +description = "CoreDNS-007 Release Notes." +release = "007" +tags = ["Release", "007", "Notes"] +title = "CoreDNS-007 Release" +author = "coredns" ++++ + +CoreDNS-007 has been [released](https://github.com/coredns/coredns/releases/tag/v007)! + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +# News + +CoreDNS is accepted as an inception project by the [CNCF](https://cncf.io)! Which means a lot to us. +See [this blog post](/2017/03/02/why-cncf-for-coredns/) on why we wanted/did this. + +Because of this we moved repos: is the main overarching repo. There is +an automatic redirect in place from the old repo. + +And... We have a new logo! We're also discussion a website redesign for and +this blog. + +Back to the release. + +# Core + +* `ServeDNS` is extended to take a context. This allows (for instance) tracing to start at an earlier entrypoint. +* gRPC and TLS are made first class citizens. See [the zone + specification](https://github.com/coredns/coredns/blob/master/README.md#zone-specification) on how + to use it. TL;DR using `grpc://` makes the server talk gRPC. The `tls` directive is used to + specify TLS certificates. +* Zipkin tracing can be enabled for all plugin. + +# Plugins + +* *rewrite* now allows you to add or modify EDNS0 local or NSID options. The framework is in place to add additional EDNS0 types in the future. +* *etcd* when no upstreams are defined won't default to using 8.8.8.8, 8.8.4.4; it just does not resolve external names in that case. +* *erratic* now can also delay queries and send queries with Truncated set. +* *metrics* will happily start as many (different) listeners as you want (if you really need that). +* *startup* and *shutdown* allow for command to be run during startup or shutdown. These directly use the code from Caddy, see [Caddy's docs](https://caddyserver.com/docs/startup). +* *kubernetes* now implements a `fallthrough` option to pass queries that would result in NXDOMAIN + to the next plugin, even if the query is in the cluster domain. This enables custom DNS + entries in the cluster domain (as long as they do not overlap with a normal Kubernetes record). To + facilitate this the plugin ordering is also altered to put *kubernetes* in the chain before + other backends. +* *cache* will no longer cache RRSIGs that will expire while cached. + +# Contributors + +The following people helped with getting this release done: + +Chris Aniszczyk, +Chris O'Haver, +Christoph Görn, +Dominic, +John Belamaric, +Jonathan Boulle, +Michael, +Michael S. Fischer, +Miek Gieben, +Yong Tang, +Yue Ko. + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-008.md b/ag_201_coredns/notes/coredns-008.md new file mode 100644 index 0000000..e0c7ab3 --- /dev/null +++ b/ag_201_coredns/notes/coredns-008.md @@ -0,0 +1,66 @@ ++++ +date = "2017-06-14T22:52:11Z" +description = "CoreDNS-008 Release Notes." +release = "008" +tags = ["Release", "008", "Notes"] +title = "CoreDNS-008 Release" +author = "coredns" ++++ + +CoreDNS-008 has been [released](https://github.com/coredns/coredns/releases/tag/v008)! + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +Release v008 has a lot of content, with new plugin and major features added to existing plugin. + +Please note there is an *incompatible* change to the `log` directive - it now only logs to `stdout` and so +only allows `stdout` as the file name (which of course may be omitted). + +# Core + +* `-log` flag was changed into a boolean as all logging will be written to standard output. + +# Plugins + +## New + +* *hosts* allows CoreDNS to read a `/etc/hosts` styled file and generate responses from that. +* *debug* can disable the `panic/recover` that is enabled by default. Mostly useful for testing/non-prod use cases to generate stack traces. + +## Updates + +* *chaos* now returns the correct `version.bind` `TXT` record. +* *kubernetes* + * Now returns a proper NS record for the cluster domain + * Supports `ExternalName` services, which was an oversight in the 1.0.0 version of the [Kubernetes dns spec](https://github.com/kubernetes/dns/blob/master/docs/specification.md) + * Now supports federation records + * Has had some other bug fixes. +* *file* + * Now supports DNAME [RFC 6672](https://tools.ietf.org/html/rfc6672) + * Refuse to load a zone without a SOA record. +* *file, auto* don't reload a zone when the SOA's serial hasn't changed. +* *secondary* now behaves properly if queried before the zone has been transferred +* *log, errors* output everything to *stdout* and let `journald` or `docker` (or whatever) that care of further handling. This is backwards **incompatible** change wrt to the Corefile: `log query.log` will return an error. +* *cache* got a new cache implementation to be more scalable and a new `prefetch` option for fetching records before the TTL expires. +* *proxy* does not use `singleinflight` anymore, removing a potential bottleneck on the single mutex in that implementation; it now forwards *all* queries it get to the upstream nameserver. + + +# Contributors + +The following people helped with getting this release done: + +Chris Aniszczyk, +Chris O'Haver, +cricketliu, +Eric Yan, +John Belamaric, +Jonas Östanbäck, +Manuel Alejandro de Brito Fontes, +Miek Gieben, +Pat Moroney, +Yong Tang + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-009.md b/ag_201_coredns/notes/coredns-009.md new file mode 100644 index 0000000..cb0c981 --- /dev/null +++ b/ag_201_coredns/notes/coredns-009.md @@ -0,0 +1,47 @@ ++++ +date = "2017-07-13T22:52:11Z" +release = "009" +description = "CoreDNS-009 Release Notes." +tags = ["Release", "009", "Notes"] +title = "CoreDNS-009 Release" +author = "coredns" ++++ + +CoreDNS-009 has been [released](https://github.com/coredns/coredns/releases/tag/v009)! + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +Release v009 is mostly a bugfix release, with a few new features in the plugin. + +# Core + +No changes. + +# Plugins + +* *secondary*: fix functionality and improve matching of notify queries. +* *cache*: fix data race. +* *proxy*: async healthchecks. +* *reverse*: new option `wildcard` that also catches all subdomains of a template. +* *kubernetes*: experimental new option `autopath` that optimizes the search path and ndots + combinatorial explosion, so clients with a large search path and high ndots will get a reply on + the first query. + +# Contributors + +The following people helped with getting this release done: + +Athir Nuaimi, +Chris O'Haver, +ghostflame, +jremond, +Mia Boulay, +Miek Gieben, +Ning Xie, +Roman Mazur, +Yong Tang. + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-010.md b/ag_201_coredns/notes/coredns-010.md new file mode 100644 index 0000000..5db0e37 --- /dev/null +++ b/ag_201_coredns/notes/coredns-010.md @@ -0,0 +1,51 @@ ++++ +title = "CoreDNS-010 Release" +description = "CoreDNS-010 Release Notes." +tags = ["Release", "010", "Notes"] +draft = false +release = "010" +date = "2017-07-25T11:24:43-04:00" +author = "coredns" ++++ + +CoreDNS-010 has been [released](https://github.com/coredns/coredns/releases/tag/v010)! + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +Release v010 is mostly a bugfix release, with one new plugin - *dnstap*. + +# Core + +No changes. + +# Plugins + +## New + +* *dnstap* is a new plugin that allows you to get dnstap information from CoreDNS. + +## Updates + +* *file* now handles multiple wildcard below each other correctly, and handles wildcards at the apex. +* *hosts*, and *kubernetes* have been fixed to return success with no data in cases where records exist +but not of the requested type. This fixes an issue with getting NXDOMAIN for the AAAA record even when the +A record exists confusing some resolvers. + +# Documentation + +* Many updates to README files. + +# Contributors + +The following people helped with getting this release done: + +Antoine Debuisson, +Chris O'Haver, +John Belamaric, +Miek Gieben, +Pat Moroney + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-011.md b/ag_201_coredns/notes/coredns-011.md new file mode 100644 index 0000000..9e3d520 --- /dev/null +++ b/ag_201_coredns/notes/coredns-011.md @@ -0,0 +1,75 @@ ++++ +title = "CoreDNS-011 Release" +description = "CoreDNS-011 Release Notes." +tags = ["Release", "011", "Notes"] +draft = false +release = "011" +date = "2017-09-10T20:24:43-04:00" +author = "coredns" ++++ + +CoreDNS-011 has been [released](https://github.com/coredns/coredns/releases/tag/v011)! + +CoreDNS is a DNS server that chains plugins, where each plugin implements a DNS feature. + +Release v011 is a major release, with backwards incompatible changes in the *kubernetes* plugin. + +## Core + +**This release has backwards incompatible changes** for the *kubernetes* plugin. + +* Stop vendoring `github.com/miekg/dns` and `golang.org/x/net/context`. This enables external plugin to compile without tripping over vendored types that mismatch. +* Allow an easy way to specify reverse zones in the Corefile, just use (e.g) `10.0.0.0/24` as the zone name, + CoreDNS translates this to 0.0.10.in-addr.arpa. This is only done when the netmask is a multiple of 8 and for both IPv4 and IPv6. +* Bug and stability fixes. + +## Plugins + +Make *kubernetes*, *file*, *secondary*, *hosts*, *erratic* and *metrics* now fail on unknown properties in the Corefile. + +### New + +* *federation*: enables federation via kubernetes. +* *autopath*: enables autopath-ing. Can be used standalone, but its main use is with kubernetes. + +### Updates + +* *log* adds an `>rflags` replacer that shows the flags from the response - this has been enabled by default. +* *kubernetes* deprecates: + * `cidr`: use the reverse syntax in the Corefile + * `federation`: use the new *federation* plugin + * `autopath`: use the new *autopath* plugin +* *kubernetes*: + * add TTL option allowing to set minimal TTL for responses. + * Multiple k8s API endpoints could be specified, separated by `","`s, e.g. `endpoint http://k8s-endpoint1:8080,http://k8s-endpoint2:8080`. CoreDNS will automatically perform a healthcheck and proxy to the healthy k8s API endpoint. +* *rewrite*: + * allow for *dynamic* properties to be used, like client IP address in rewrite rules, i.e. +`rewrite edns0 local set 0xffee {client_ip}` + * add support for EDNS0 Client Subnet +* *dnstap* now reports messages proxied by *proxy*, and support remote IP endpoints by specifying `tcp://`. +* *dnssec* now warns if keys can't be used to sign the configured zones. +* *health* now allows for per plugin health status; no plugin makes use of this yet, though. +* *secondary* parses a secondary with a zone (`secondary example.org {...}`) correctly. + +## Contributors + +The following people helped with getting this release done: + +Brad Beam, +Chris O'Haver, +insomniac, +James Mills, +John Belamaric, +Markus Sommer, +Miek Gieben +Mohammed Naser, +Sandeep Rajan, +Thong Huynh, +varyoo, +Yong Tang, +张勋. + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.0.0.md b/ag_201_coredns/notes/coredns-1.0.0.md new file mode 100644 index 0000000..c1b9de5 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.0.0.md @@ -0,0 +1,75 @@ ++++ +title = "CoreDNS-1.0.0 Release" +description = "CoreDNS-1.0.0 Release Notes." +tags = ["Release", "1.0.0", "Notes"] +draft = false +release = "1.0.0" +date = "2017-12-01T22:43:43-00:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.0.0) of CoreDNS-1.0.0! + +Release 1.0.0 and other recent releases have focused on improving the performance and +functionality of the *kubernetes* plugin, since CoreDNS is now on track to eventually +[replace kube-dns](https://github.com/kubernetes/features/issues/427) as the default +cluster DNS in Kubernetes. + +As part of the [Kubernetes proposal](https://github.com/kubernetes/community/pull/1100), we have shown that CoreDNS +not only provides more functionality than kube-dns, but performs much better while using less memory. In our tests, +[CoreDNS](https://github.com/kubernetes/community/pull/1100#issuecomment-337747482) running against a cluster with 5000 +services was able to process 18,000 queries per second using 73MB of RAM, while +[kube-dns](https://github.com/kubernetes/community/pull/1100#issuecomment-338329100) achieved 7,000qps using 97MB of RAM. +This can be partial ascribed to CoreDNS simpler runtime - a single process instead of a combination of several processes. + +CoreDNS also implements a number of Kubernetes-related features that are not part of kube-dns, including: + +* Filtering of records by namespace +* Filtering of records by label selector +* `pods verified` mode, which ensures that a Pod exists before returning an answer for a `pod.cluster.local` query +* `endpoint_pod_names` which uses [Pod names](https://github.com/kubernetes/kubernetes/issues/47992) for service endpoint records if the hostname is not set +* `autopath` which provides a server-side implementation of the namespace-specific search path. This can cut down the query latency from pods dramatically. + +As a general-purpose DNS server, CoreDNS also enables many other use cases that would be difficult or impossible to +achieve with kube-dns, such as the ability to create [custom DNS entries](https://coredns.io/2017/05/08/custom-dns-entries-for-kubernetes/). + +We are excited to continue our contributions to the Kubernetes community, and CoreDNS is being incorporated as a 1.9 alpha feature into a variety +of Kubernetes deployment mechanisms, including upcoming versions of [kubeadm](https://github.com/kubernetes/kubeadm), [kops](https://github.com/kubernetes/kops), [minikube](https://github.com/kubernetes/minikube), and [kubespray](https://github.com/kubernetes-incubator/kubespray). + +Of course, there is more to 1.0.0 than just the Kubernetes work. See below for the details on all the changes. + +## Core + +* Fixed a bug in the gRPC server that prevented *dnstap* from working with it. +* Additional fuzz testing to ferret out obscure bugs. +* Documentation and configuration cleanups. + +## Plugins +* *log* no longer accepts `stdout` in the configuration (use of a file was removed in a previous release). All logging is always to STDOUT. This is a backwards **incompatible** change, so be sure to check your Corefile for this. +* *health* now checks plugins that support it for health and reflects that in the server health. +* *kubernetes* now shows healthy only after the initial API sync is complete. +* *kubernetes* has bug fixes and performance improvements. +* *kubernetes* now has an option to use pod names instead of IPs in service endpoint records when the `hostname` is not set. +* *metrics* have been revised to provide better histograms. You will need to change your Prometheus queries as metric names have changed to comply with Prometheus best practices. +* *erratic* now supports the health check. + +## Contributors + +The following people helped with getting this release done: +Andy Goldstein, +Ben Kochie, +Brian Akins, +Chris O'Haver, +Christian Nilsson, +John Belamaric, +Max Schmitt, +Michael Grosser, +Miek Gieben, +Ruslan Drozhdzh, +Uladzimir Trehubenka, +Yong Tang. + +If you want to help, please check out one of the [issues](https://github.com/coredns/coredns/issues/) +and start coding! + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.0.1.md b/ag_201_coredns/notes/coredns-1.0.1.md new file mode 100644 index 0000000..e3cb558 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.0.1.md @@ -0,0 +1,34 @@ ++++ +title = "CoreDNS-1.0.1 Release" +description = "CoreDNS-1.0.1 Release Notes." +tags = ["Release", "1.0.1", "Notes"] +draft = false +release = "1.0.1" +date = "2017-12-11T14:43:43-00:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.0.1) of CoreDNS-1.0.1! + +This release fixes a crash in the *file* plugin and has some minor bug fixes for other plugins. +One new plugin was added: *nsid*, that implements [RFC 5001](https://tools.ietf.org/html/rfc5001). + +## Plugins +* *file* fixes a crash when an request with a DO bit (pretty much the default) hits an unsigned zone. The default configuration should recover the go-routine, but this is nonetheless serious. *file* received some other fixes when returning (secure) delegations. +* *dnstap* plugin is now 50% faster. +* *metrics* fixed the start time bucket for the duration. + +## Contributors + +The following people helped with getting this release done: +Brad Beam, +James Hartig, +Miek Gieben, +Rene Treffer, +Ruslan Drozhdzh, +Seansean2, +Yong Tang. + +If you want to help, please check out one of the +[issues](https://github.com/coredns/coredns/issues/) and start coding! For documentation and help, +see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.0.2.md b/ag_201_coredns/notes/coredns-1.0.2.md new file mode 100644 index 0000000..413a660 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.0.2.md @@ -0,0 +1,44 @@ ++++ +title = "CoreDNS-1.0.2 Release" +description = "CoreDNS-1.0.2 Release Notes." +tags = ["Release", "1.0.2", "Notes"] +draft = false +release = "1.0.2" +date = "2017-12-31T09:06:29+00:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.0.2) of CoreDNS-1.0.2! +This release can be summarized as "help external plugin developers" as most changes are geared +towards exposing CoreDNS functionality to make this as easy as possible. Is also a fairly small +release. + +## Core + +Expose the directives list, so that external plugins can be easily added without mucking with +CoreDNS code, see the [pull request](https://github.com/coredns/coredns/pull/1315) for details. + +Fix crash when there are no handlers that can actually serve queries, i.e. a Corefile with only +*debug* and *pprof* for instance. + +## Plugins + +* [*metrics*](/plugins/metrics) has a New function to help external plugin developers. +* [*health*](/plugins/health) plugin now checks all plugins for a `health.Healther` implementation and will export health for those plugins that do. Again helps external plugin developers. +* [*rewrite*](/plugins/rewrite) gained regular expression and substring-matching support. + +## Contributors + +The following people helped with getting this release done: +Brad Beam, +Francois Tur, +Frederic Hemberger, +James Hartig, +Max Schmitt, +Miek Gieben, +Paul Greenberg, +Yong Tang. + +If you want to help, please check out one of the +[issues](https://github.com/coredns/coredns/issues/) and start coding! For documentation and help, +see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.0.3.md b/ag_201_coredns/notes/coredns-1.0.3.md new file mode 100644 index 0000000..a2103d4 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.0.3.md @@ -0,0 +1,43 @@ ++++ +title = "CoreDNS-1.0.3 Release" +description = "CoreDNS-1.0.3 Release Notes." +tags = ["Release", "1.0.3", "Notes"] +draft = false +release = "1.0.3" +date = "2018-01-10T19:38:29+00:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.0.3) of CoreDNS-1.0.3! +This is a small bugfix release, but we also have a new plugin: +[*template*](https://coredns.io/plugins/template). + +## Core + +Manual pages are now generated from the READMEs, you can find them in the man/ directory. +A coredns(1) and corefile(5) one where also added. + +## Plugins + +The `fallthrough` directive was overhauled and now allows a list of zones to be specified. It will +then only fallthrough for those zones, see `plugin/plugin.md`. + +A new plugin *template* was added. It allows you to use Go (text) templates to craft a response, see + for docs. + +* *dnssec* implements Cloudflares's NSEC blacklies better. +* *kubernetes*, adds a fix for `pod insecure` look ups for non-IP addresses. +* *health* adds a metrics for the duration it takes to GET /health. Useful for getting a sense of + overloadedness of the process. + +## Contributors + +The following people helped with getting this release done: +John Belamaric, +Miek Gieben, +Rene Treffer, +Yong Tang. + +If you want to help, please check out one of the +[issues](https://github.com/coredns/coredns/issues/) and start coding! For documentation and help, +see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.0.4.md b/ag_201_coredns/notes/coredns-1.0.4.md new file mode 100644 index 0000000..388b2b5 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.0.4.md @@ -0,0 +1,17 @@ ++++ +title = "CoreDNS-1.0.4 Release" +description = "CoreDNS-1.0.4 Release Notes." +tags = ["Release", "1.0.4", "Notes"] +draft = false +release = "1.0.4" +date = "2018-01-18T15:54:29+00:00" +author = "coredns" ++++ + +We are announcing the [release](https://github.com/coredns/coredns/releases/tag/v1.0.4) of CoreDNS-1.0.4! + +This is a release that fixes a vulnerability in the underlying DNS library. +See and the (still embargoed) CVE-2017-15133. +Thanks to Tom Thorogood for bringing this issue to our attention. + +CoreDNS-1.0.4 is [CoreDNS-1.0.3](https://coredns.io/2018/01/10/coredns-1.0.3-release/) recompiled with a patched DNS library. diff --git a/ag_201_coredns/notes/coredns-1.0.5.md b/ag_201_coredns/notes/coredns-1.0.5.md new file mode 100644 index 0000000..e02a261 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.0.5.md @@ -0,0 +1,48 @@ ++++ +title = "CoreDNS-1.0.5 Release" +description = "CoreDNS-1.0.5 Release Notes." +tags = ["Release", "1.0.5", "Notes"] +draft = false +release = "1.0.5" +date = "2018-01-25T11:10:29+00:00" +author = "coredns" +enabled = "default" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.0.5) of CoreDNS-1.0.5! +This release has bug fixes, documentation fixes, polish and new plugins. + +## Core + +Add ability to *really* compile out the default plugins. + +## Plugins + +* A new plugin *route53* was added that enables serving zone data from AWS route53, see the [documentation](https://coredns.io/plugins/route53). +* A new plugin *on* was added. This is an external Caddy [plugin](https://caddyserver.com/docs/on), that is now also available (by default) for CoreDNS; it allows you to run commands when an event is generated. + +* *cache* doesn't apply a 5s minimum TTL anymore. It fixes prefetching *and* correctly sets the metrics for cache hits and misses. +* *dnssec* fixes handing out *expired* signatures after 8 days and properly filters out the qtype in the NSEC bitmap for NXDOMAIN responses. +* *log* adds message ID `{>id}` to the default logging. +* *health* has gotten a lameduck option that will nack health, but will keep the server running for a configurable duration when CoreDNS is being shut down. If metrics are enabled *health* exports a metric that curls the local endpoint and exports the duration. Useful for getting a sense of overloadedness of the process. +* *rewrite* can now rewrite answers for `name regex` matches. This prevents DNS clients from ignoring the answers due to a mismatch with the original question. +* *secondary* saw a bunch of fixes. + +## Contributors + +The following people helped with getting this release done: + +Christian Nilsson, +cricketliu, +Francois Tur, +Ilya Galimyanov, +Miek Gieben, +Paul Greenberg, +Ruslan Drozhdzh, +Tobias Schmidt, +Yong Tang, +Yue Ko. + +If you want to help, please check out one of the +[issues](https://github.com/coredns/coredns/issues/) and start coding! For documentation and help, +see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.0.6.md b/ag_201_coredns/notes/coredns-1.0.6.md new file mode 100644 index 0000000..a2950b5 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.0.6.md @@ -0,0 +1,51 @@ ++++ +title = "CoreDNS-1.0.6 Release" +description = "CoreDNS-1.0.6 Release Notes." +tags = ["Release", "1.0.6", "Notes"] +draft = false +release = "1.0.6" +date = "2018-02-21T11:10:29+00:00" +author = "coredns" +enabled = "default" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.0.6) of CoreDNS-1.0.6! +This release has bug fixes, documentation fixes, polish and new plugins. + +## Core + +We've moved to a OWNERS model, where each plugin (and CoreDNS itself) now has an OWNERS file listing +people involved with this code. + +## Plugins + +* The *startup* and *shutdown* plugin are **deprecated** (but working and included) in this release in favor of the *on* + plugin. If you use them, this is the moment to move to [*on*](/explugins/on). +* A plugin called [*forward*](https://coredns.io/plugins/forward) has been included in CoreDNS, this + was, up until now, an external plugin. Supports DNS-over-TLS and has different way of health + checking an upstream. +* The [*proxy*](https://coredns.io/plugins/proxy) plugin has a new policy, *first* which always + chooses the first healthy upstream host. It also contains an important fix where + a non-health checked target could be mark unhealthy forever. +* We now support zone transfers in the [*kubernetes*](https://coredns.io/plugins/kubernetes) plugin. +* The [*bind*](https://coredns.io/plugins/bind) now supports multiple listening addresses. +* Bugfixes, improvements and documentation fixes in various other plugins. + +## Contributors + +The following people helped with getting this release done: + +Chris O'Haver, +Francois Tur, +Freddy, +Harshavardhana, +John Belamaric, +Miek Gieben, +Pat Moroney, +Paul Greenberg, +Sandeep Rajan, +Tobias Schmidt, +Uladzimir Trehubenka, +Yong Tang. + +For documentation and help, see our [community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.1.0.md b/ag_201_coredns/notes/coredns-1.1.0.md new file mode 100644 index 0000000..5213d1d --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.1.0.md @@ -0,0 +1,79 @@ ++++ +title = "CoreDNS-1.1.0 Release" +description = "CoreDNS-1.1.0 Release Notes." +tags = ["Release", "1.1.0", "Notes"] +draft = false +release = "1.1.0" +date = "2018-03-12T09:33:29+00:00" +author = "coredns" +enabled = "default" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.1.0) of +CoreDNS-1.1.0! + +CoreDNS has been promoted to the [incubating](https://www.cncf.io/projects/graduation-criteria/) +level in the [CNCF](https://www.cncf.io/projects/)! +This has been made possible by the work done by contributors, users and adopters. + +**Thank you all!** + +## Core + +Bump the version to 1.1.0, as we deprecate two plugins (*shutdown* and *startup*). + +In CoreDNS 1.0.6 the [*bind*](/plugins/bind) plugin was extended to allow binding to multiple +interfaces. This release adds the ability serve the same zone on different interfaces (we used to +block this for no good reason). I.e. this now works: + +``` +. { + bind 127.0.0.1 + # .. +} + +. { + bind 127.0.0.2 + # ... +} +``` + +## Plugins + +* The plugins *shutdown* and *startup* where marked deprecated in 1.0.6. This release removes them. You should use [*on*](/explugins/on) instead. +* A new plugin was added: *reload*, which watches for changes in your Corefile and then automatically will reload the process. This is not yet bullet proof, some plugins can fail to setup during a reload. See the discussion in [issue 1445](https://github.com/coredns/coredns/issues/1455). +* A number of plugins can only be used once in a server block, but didn't make this explicit. I.e. [*dnssec*](/plugins/dnssec) would silently overwrite earlier config. The following plugins now return an error when used multiple times **in a single Server Block**: +*cache*, +*dnssec*, +*errors*, +*forward*, +*hosts*, +*nsid*, +*metrics*, +*kubernetes*, +*pprof*, +*reload*, +*root*. +* [*Trace*](/plugins/trace) adds support for a Datadog endpoint. +* Some changes went into [*dnstap*](/plugins/dnstap), make it easier to use from other plugins. +* Small change in the [*log*](/plugin/log) plugin, the log default will now also log the client's + port number and IPv6 addresses are printed with brackets: `[::1]`. + +## Contributors + +The following people helped with getting this release done: + +Chris O'Haver, +Francois Tur, +John Belamaric, +Miek Gieben, +nogoegst, +Ricardo Katz, +Tobias Schmidt, +Uladzimir Trehubenka, +varyoo, +Yamil Asusta, +Yong Tang. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.1.1.md b/ag_201_coredns/notes/coredns-1.1.1.md new file mode 100644 index 0000000..01ab0a6 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.1.1.md @@ -0,0 +1,40 @@ ++++ +title = "CoreDNS-1.1.1 Release" +description = "CoreDNS-1.1.1 Release Notes." +tags = ["Release", "1.1.1", "Notes"] +release = "1.1.1" +date = "2018-03-25T18:04:29+01:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.1.1) of +CoreDNS-1.1.1! + +This release fixes a **critical bug** in the *cache* plugin found by [Cure53](/2018/03/15/cure53-security-assessment/). + +All users are encouraged to upgrade. + +## Core + +Fix a bug when scrubbing the reply to fit the request's buffer consumes 100% CPU and does not return +the reply. + +## Plugins + +* [*cache*](/plugins/cache) fixes the critical spoof vulnerability. +* [*route53*](/plugins/route53) adds support for PTR records. + +## Contributors + +The following people helped with getting this release done: + +Chris O'Haver, +Mario Kleinsasser, +Miek Gieben, +Yong Tang. + +And of course the people in [Cure53](https://cure53.de). Also special shout out to Mario Kleinsasser +for helping to debug the [scrubbing issue](https://github.com/coredns/coredns/issues/1625). + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.1.2.md b/ag_201_coredns/notes/coredns-1.1.2.md new file mode 100644 index 0000000..d5f0e65 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.1.2.md @@ -0,0 +1,40 @@ ++++ +title = "CoreDNS-1.1.2 Release" +description = "CoreDNS-1.1.2 Release Notes." +tags = ["Release", "1.1.2", "Notes"] +release = "1.1.2" +date = "2018-04-23T09:21:29+01:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.1.2) of +CoreDNS-1.1.2! + +This release has some fixes in the plugins and no core updates. + +# Plugins + +* [*forward*](/plugins/forward) has received a large pile of fixes and improvements. +* [*reload*](/plugins/reload): the *metrics* and *health* plugin saw fixes for this reload issue, still not 100% perfect, but a whole lot better than it was. +* [*log*](/plugins/log) now allows `OR`ing of log classes. +* [*metrics*](/plugins/metrics): add a server label to make each metric unique to the server handling it. + This impacts all plugins, currently *proxy* and *forward* have been updated to include a server label. +* [*debug*](/plugins/debug): when enabled plugins show their `log.Debug` output (none of the included plugins use this yet). +* [*kubernetes*](/plugins/kubernetes) has a small fix for apex queries. + +## Contributors + +The following people helped with getting this release done: + +Chris O'Haver, +Francois Tur, +Maksim Paramonau, +Miek Gieben, +Moto Ishizawa, +Ruslan Drozhdzh, +Scott Donovan, +Tobias Schmidt, +Uladzimir Trehubenka. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.1.3.md b/ag_201_coredns/notes/coredns-1.1.3.md new file mode 100644 index 0000000..4282f13 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.1.3.md @@ -0,0 +1,57 @@ ++++ +title = "CoreDNS-1.1.3 Release" +description = "CoreDNS-1.1.3 Release Notes." +tags = ["Release", "1.1.3", "Notes"] +release = "1.1.3" +date = "2018-05-24T09:43:29+01:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.1.3) of +CoreDNS-1.1.3! + +This release has fixes in the plugins, small core updates and experimental DNS over HTTPs support. +We also announce the deprecation of a few things. + +# Core + +*Experimental* DNS-over-HTTPS support was added in the server. Use `https://` as the server's scheme + in the configuration. + +The `-log` flag actually doesn't do anything, so this is a deprecation notice that this flag will be +removed in the next release. + +# Plugins + +* [*metrics*](/plugin/metrics) All in-tree plugins serve metrics with a `server` label. + * *cache* and *dnssec* drop the capacity metrics + * add a panic counter: `coredns_panic_count_total` +* [*etcd*](/plugins/etcd) supports A and AAAA record under the zone's apex. +* [*rewrite*](/plugins/rewrite) now handles `continue` in response rewrites. +* [*forward*](/plugin/forward) clean ups, esp when shutting it down. +* [*cache*](/plugin/cache) adds some optimization. +* [*kubernetes*](/plugin/kubernetes) adds option to `ignore` services without ready endpoints. +* Deprecation notice for the *reverse* plugin. +* Deprecation notice for the `https_google` protocol in *proxy*. + +## Contributors + +The following people helped with getting this release done: + +Ahmet Alp Balkan, +Anton Antonov, +Cem Türker, +Chris O'Haver, +darkweaver87, +Eugen Kleiner, +Francois Tur, +John Belamaric, +Mario Kleinsasser, +Miek Gieben, +Ruslan Drozhdzh, +Silver, +Tobias Schmidt, +Yong Tang. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.1.4.md b/ag_201_coredns/notes/coredns-1.1.4.md new file mode 100644 index 0000000..20c3403 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.1.4.md @@ -0,0 +1,54 @@ ++++ +title = "CoreDNS-1.1.4 Release" +description = "CoreDNS-1.1.4 Release Notes." +tags = ["Release", "1.1.4", "Notes"] +release = "1.1.4" +date = "2018-06-19T09:39:29+01:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.1.4) of +CoreDNS-1.1.4! + +This release has a few enhancements in the plugins, and a few (Docker) improvements. + +# Core + +As said in the [1.1.3 Release Notes](/2018/05/24/coredns-1.1.3-release/), we are making the `-log` +command line flag a noop. + +This is also a heads up that in the next release - 1.2.0 - the current *etcd* plugin will be +replaced by a new plugin that supports etcd3, see this [pull +request](https://github.com/coredns/coredns/pull/1702). + +The Docker image is now built using a multistage build. This means the final image is based on +`scratch` *and* all architectures now have certificates in the image (not just the amd64 one). + +# Plugins + +We are also deprecating: + +* the *reverse* plugin has been removed, but we allow it still in the configuration. +* the `google_https` protocol has been a noop in the *proxy* plugin. + +In the next release (1.2.0) this code will removed completely. + +Further more: + +* *file* now always queries local zones when trying to find a CNAME target. +* *log* will now always log in seconds (not micro, or milliseconds). +* *forward* erases expired connection after some time. + +## Contributors + +The following people helped with getting this release done: + +Francois Tur, +Malcolm Akinje, +Mario Kleinsasser, +Miek Gieben, +Ruslan Drozhdzh, +Yong Tang. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.10.0.md b/ag_201_coredns/notes/coredns-1.10.0.md new file mode 100644 index 0000000..145f2d4 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/notes/coredns-1.2.0.md b/ag_201_coredns/notes/coredns-1.2.0.md new file mode 100644 index 0000000..a929079 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.2.0.md @@ -0,0 +1,50 @@ ++++ +title = "CoreDNS-1.2.0 Release" +description = "CoreDNS-1.2.0 Release Notes." +tags = ["Release", "1.2.0", "Notes"] +release = "1.2.0" +date = "2018-07-11T11:13:29+01:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.2.0) of +CoreDNS-1.2.0! + +In this release we have a new plugin, bump etcd to version 3 and bugfixes. + +# Core + +Enable watch functionality when CoreDNS is used as a gRPC server (documented in the code - for now). + +# Plugins + +* A new plugin called [*metadata*](/plugins/metadata) was added. It adds metadata to a query, via the context. +* The [*etcd*](/plugins/etcd) plugin now supports etcd version 3 (only!). It was impossible to support v2 *and* v3 at + the same time (even as separate plugins); so we decided to drop v2 support. +* Fix a race/crash in the [*cache*](/plugins/cache) plugin when `prefetch` is enabled. +* The [*forward*](/plugins/forward) plugin has a `prefer_udp` option, that even when the incoming query is over TCP, the + outgoing one will be tried over UDP first. +* [*secondary*](/plugins/secondary) plugin has a bug fix for zone expiration: don't expire zones if we can reach the + primary, but see no zone changes. +* The [*auto*](/plugins/auto) plugin now works better with Kubernetes Configmaps. + +## Contributors + +The following people helped with getting this release done: +Chris O'Haver, +Eren Güven, +Eugen Kleiner, +Francois Tur, +Isolus, +Joey Espinosa, +John Belamaric, +Jun Li, +Marcus André, +Miek Gieben, +Nitish Tiwari, +Ruslan Drozhdzh, +Tobias Schmidt, +Yong Tang. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.2.1.md b/ag_201_coredns/notes/coredns-1.2.1.md new file mode 100644 index 0000000..c748926 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.2.1.md @@ -0,0 +1,41 @@ ++++ +title = "CoreDNS-1.2.1 Release" +description = "CoreDNS-1.2.1 Release Notes." +tags = ["Release", "1.2.1", "Notes"] +release = "1.2.1" +date = "2018-08-28T07:10:29+01:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.2.1) of +CoreDNS-1.2.1! + +This release features bugfixes (mostly in the [*kubernetes*](/plugins/kubernetes) plugin), +documentation improvements and one new plugin: [*loop*](/plugins/loop). + +# Plugins + +* A new plugin called [*loop*](/plugins/loop) was added. When starting up it detects resolver loops + and stops the process if one is detected. + +## Contributors + +The following people helped with getting this release done. Good to see a whole bunch of new names, +as well as the usual suspects: + +Bingshen Wang, +Chris O'Haver, +Eugen Kleiner, +Francois Tur, +Jiacheng Xu, +Karsten Weiss, +Lorenzo Fontana, +Miek Gieben, +Nitish Tiwari, +Stanislav Zapolsky, +varyoo, +Yong Tang, +Zach Eddy. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.2.2.md b/ag_201_coredns/notes/coredns-1.2.2.md new file mode 100644 index 0000000..2e50756 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.2.2.md @@ -0,0 +1,24 @@ ++++ +title = "CoreDNS-1.2.2 Release" +description = "CoreDNS-1.2.2 Release Notes." +tags = ["Release", "1.2.2", "Notes"] +release = "1.2.2" +date = "2018-08-29T07:10:29+01:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.2.2) of +CoreDNS-1.2.2! + +This is a (small) release that helps out our friends at +[kops](https://github.com/kubernetes/kops/issues/5652): make the default cache size smaller. + +## Contributors + +The following people helped with getting this release done: + +Chris O'Haver, +Miek Gieben. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.2.3.md b/ag_201_coredns/notes/coredns-1.2.3.md new file mode 100644 index 0000000..d169048 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.2.3.md @@ -0,0 +1,61 @@ ++++ +title = "CoreDNS-1.2.3 Release" +description = "CoreDNS-1.2.3 Release Notes." +tags = ["Release", "1.2.3", "Notes"] +release = "1.2.3" +date = "2018-10-16T11:37:29+01:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.2.3) of +CoreDNS-1.2.3! + +## Core + +This is a big release that spans almost 6 weeks of development, slightly longer than normal. You may +also have noticed that CoreDNS *wasn't* made the default in Kubernetes 1.12 due to increased memory +used compared to kube-dns. This release contains a fix for that. + +The underlying DNS library has seen multiple updates to improve throughput and memory and we have +enabled REUSE_PORT on the ports CoreDNS opens on \*nix. + +## Plugins + +* [*federation*](/plugins/federation) return a correct answer (SERVFAIL) if availability-zone or region labels are missing from a node. +* [*route53*](/plugins/route53) + * Refactor add-on to support batch querying of Route 53 along with all AWS record types (including `CNAME`). + * Add support for zones with overlapping domains (split config) + * Minor improvements (`fallthrough`, `upstream` options, AWS credentials file support) +* [*cache*](/plugin/cache) add a minttl option to set the minimal TTL for records being cached. The cache key moved to hash/fnv64. +* [*rewrite*](/plugin/rewrite) can now also rewrite TTLs +* [*kubernetes*](/plugin/kubernetes) + * Uses less memory (~30% less). + * Do not block on startup when connecting to the API server; returns SERVFAIL in the mean time. + * Support for using a `kubeconfig` file, including various auth providers (Azure not supported due to a compilation issue with that code). +* [*reload*](/plugin/reload) allows the reload interval to be configured. +* [*forward*](/plugin/forward) fix a crash when health checking is enabled in some circumstances. + +## Brought to you by: + +Aaron Riekenberg, +Billie Cleek, +Brad Beam, +Can Yucel, +Chris O'Haver, +dilyevsky, +Eugen Kleiner, +Francois Tur, +John Belamaric, +Manuel Alejandro de Brito Fontes, +Manuel Stocker, +marqc, +Miek Gieben, +Nic Cope, +Paul G, +Ruslan Drozhdzh, +Tom Thorogood, +Yong Tang, +Zach Eddy. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.2.4.md b/ag_201_coredns/notes/coredns-1.2.4.md new file mode 100644 index 0000000..aac2a2b --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.2.4.md @@ -0,0 +1,31 @@ ++++ +title = "CoreDNS-1.2.4 Release" +description = "CoreDNS-1.2.4 Release Notes." +tags = ["Release", "1.2.4", "Notes"] +release = "1.2.4" +date = "2018-10-17T20:01:29+01:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.2.4) of +CoreDNS-1.2.4! + +Remember we [said the 1.2.3 release](/2018/10/16/coredns-1.2.3-release/) was a big release and took +quite a while? Well, we've fixed that glitch; as 1.2.4 is here now. + +CoreDNS v1.2.3's *kubernetes* plugin **DOES NOT WORK IN KUBERNETES** and our testing that didn't catch +that regression, nor the Kubernetes scale testing which doesn't really exercise the *whole* API. + +## Plugins + +* [*cache*](/plugins/cache) use zero of the minimal negative TTL (if no suitable TTL was found in + the packet). +* [*kubernetes*](/plugins/kubernetes) fix a grave bug that made plugin **unusable** in Kubernetes. + +## Brought to you by: + +Chris O'Haver, +Miek Gieben. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.2.5.md b/ag_201_coredns/notes/coredns-1.2.5.md new file mode 100644 index 0000000..c5233ee --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.2.5.md @@ -0,0 +1,40 @@ ++++ +title = "CoreDNS-1.2.5 Release" +description = "CoreDNS-1.2.5 Release Notes." +tags = ["Release", "1.2.5", "Notes"] +release = "1.2.5" +date = "2018-10-24T20:40:29+01:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.2.5) of +CoreDNS-1.2.5! + +## Core + +Correctly make a reply fit in the client's buffer, *especially* when EDNS0 is not used. +This used to be the responsibility of a plugin, now the server will handle it. + +## Plugins + +Documentation and smaller updates for various plugins, as well as: + +* [*cache*](/plugins/cache) - resets min TTL default back to 5 second (instead of 0). +* [*dnssec*](/plugins/dnssec) - now allows aZSK/KSK split as well as a CSK setup. +* [*rewrite*](/plugins/rewrite) - answer rewrite is now automatic for _exact_ name rewrites. + +## Brought to you by + +Andrey Meshkov, +Chris O'Haver, +Francois Tur, +Kevin Nisbet, +Manuel Stocker, +Miek Gieben, +Paul G, +Ruslan Drozhdzh, +Sandeep Rajan, +Yong Tang. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.2.6.md b/ag_201_coredns/notes/coredns-1.2.6.md new file mode 100644 index 0000000..1e38301 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.2.6.md @@ -0,0 +1,49 @@ ++++ +title = "CoreDNS-1.2.6 Release" +description = "CoreDNS-1.2.6 Release Notes." +tags = ["Release", "1.2.6", "Notes"] +release = "1.2.6" +date = "2018-11-05T20:40:29+01:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.2.6) of +CoreDNS-1.2.6! + +## Core + +Ignore the error when setting SO_REUSEPORT on a socket fails; this makes CoreDNS work on older +kernels. + +## Plugins + +* [*etcd*](/plugins/etcd) has seen minor bugfixes. + +* [*loop*](/plugins/loop) fixes a bug when dealing with a failing upstream. + +* [*log*](/plugins/log) unifies all logging (done by this plugin and normal logs) and always use + RFC3339 timestamps (with millisecond accuracy). The `{when}` verb has been made a noop, it will + be removed in the next release. + +* [*cache*](/plugins/cache) got some minor optimizations. + +* [*errors*](/plugins/errors) (and *log*) gotten a new option (`consolidate`) to suppress logging. + +* [*hosts*](/plugins/hosts) will now read the `hosts` file without holding a write lock. + +* [*route53*](/plugins/route53) makes the upstream optional. + +## Brought to You By + +Carl-Magnus Björkell, +Chris O'Haver, +Dzmitry Razhanski, +Francois Tur, +Jiacheng Xu, +Matthias Lechner, +Miek Gieben, +Ruslan Drozhdzh, +Stuart Nelson. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.3.0.md b/ag_201_coredns/notes/coredns-1.3.0.md new file mode 100644 index 0000000..a526614 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.3.0.md @@ -0,0 +1,51 @@ ++++ +title = "CoreDNS-1.3.0 Release" +description = "CoreDNS-1.3.0 Release Notes." +tags = ["Release", "1.3.0", "Notes"] +release = "1.3.0" +date = "2018-12-15T16:14:29+00:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.3.0) of +CoreDNS-1.3.0! + +## Core + +In this release we do the EDNS0 handling in the server and make it compliant with +[https://dnsflagday.net/](https://dnsflagday.net/). This fits a theme where we move more and more +protocol details into the server to make life easier for plugin authors. + +# Plugins + +* [*k8s_external*](/plugins/k8s_external) a new plugin that allows external zones to point to + Kubernetes in-cluster services. + +* [*rewrite*](/plugins/rewrite) fixes a bug where a rule would eat the first character of a name + +* [*log*](/plugins/log) now supported the [*metadata*](/plugins/metadata) labels. It also fixes a + bug in the formatting of a plugin logging a info/failure/warning + +* [*forward*](/plugins/forward) removes the dynamic read timeout and uses a fixed value now. + +* [*kubernetes*](/plugins/kubernetes) now checks if a zone transfer is allowed. Also allow a TTL of + 0 to avoid caching in the cache plugin. + +## Brought to You By + +Chris O'Haver, +Cricket Liu, +Daniel Garcia, +DavadDi, +Francois Tur, +Jiacheng Xu, +John Belamaric, +Miek Gieben, +moredhel, +Sandeep Rajan, +StormXX, +stuart nelson, +Yong Tang. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.3.1.md b/ag_201_coredns/notes/coredns-1.3.1.md new file mode 100644 index 0000000..acab8bd --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.3.1.md @@ -0,0 +1,45 @@ ++++ +title = "CoreDNS-1.3.1 Release" +description = "CoreDNS-1.3.1 Release Notes." +tags = ["Release", "1.3.1", "Notes"] +release = "1.3.1" +date = "2019-01-13T15:00:29+00:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.3.1) +of CoreDNS-1.3.1! This is a fairly small release that allows us to announce some backwards +incompatible changes in the *next* (1.4.0) release: + + * The `upstream` directive used in various plugin will start to *default* to the coredns process + itself. This allow those resolutions to take advantage of other plugins (i.e. caching). The + *etcd*'s plugin StubDomain subsystem relied heavily on this functionality and as such will be + removed from that plugin. + + * Multiple endpoints in kubernetes will not be supported going forward. + + +# Plugins + +Mostly documentation updates in various plugins. Plus a small fix where we stop setting the RA +(recursion available) flag on responses in plugins that don't provide recursion. + + * [*log*](/plugins/log) now allows multiple names to be specified. + + * [*import*](/plugins/import) was added to give it a README.md to make its documentation more + discoverable. + + * [*kubernetes*](/plugins/kubernetes) `TTL` is also applied to negative responses (NXDOMAIN, etc). + +## Brought to You By + +Chris O'Haver, +ckcd, +Isolus, +jmpcyc, +Miek Gieben, +Taras Tsugrii, +Yong Tang. + +For documentation see our (in progress!) [manual](/manual). For help and other resources, see our +[community page](https://coredns.io/community/). diff --git a/ag_201_coredns/notes/coredns-1.4.0.md b/ag_201_coredns/notes/coredns-1.4.0.md new file mode 100644 index 0000000..4597b6d --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.4.0.md @@ -0,0 +1,88 @@ ++++ +title = "CoreDNS-1.4.0 Release" +description = "CoreDNS-1.4.0 Release Notes." +tags = ["Release", "1.4.0", "Notes"] +release = "1.4.0" +date = "2019-03-03T09:04:07+00:00" +author = "coredns" ++++ + +We are pleased to announce the [release](https://github.com/coredns/coredns/releases/tag/v1.4.0) +of CoreDNS-1.4.0! Our first release after we became a graduated project in +[CNCF](https://www.cncf.io/). + +Deprecation notice for the *next* release: + + * [*auto*](/plugins/auto) will deprecate **TIMEOUT** and recommends the use of RELOAD ([2516](https://github.com/coredns/coredns/issues/2516)). + * [*auto*](/plugins/file) and [*file*](/plugins/auto) will deprecate NO_RELOAD and recommends the use of RELOAD set to 0 ([2536](https://github.com/coredns/coredns/issues/2536)). + * [*health*](/plugins/health) will revert back to report process level health without plugin + status. A new *ready* plugin will make sure plugins have at least completed their startup + sequence. + * The [*proxy*](/plugins/proxy) will be moved to an external repository and as such be deprecated + from the default set of plugin; use the [*forward*](/plugins/forward) as a replacement. + +The [previous](/2019/01/13/coredns-1.3.1-release/) announced deprecations have been enacted. + +The (unused) gRPC watch functionally was removed from the server. + +Note we're actively working on two (probably related) bugs +([2593](https://github.com/coredns/coredns/issues/2593), +[2624](https://github.com/coredns/coredns/issues/2624)) which should hopefully result in a fix and +a new release fairly quickly. + +# Plugins + +Random updates in documentation and fixes in tests and various plugins. + + * [*kubernetes*](/plugins/kubernetes) fixes the logging now that kubernetes' client lib switched + to klog from glog. + + * [*hosts*](/plugins/hosts) fixes IPv4 addresses in IPV6 syntax. + + * [*etcd*](/plugins/etcd) adds credential support and a fix for the reply when the `host` field is + empty. + + * [*log*](/plugins/log) has been made more efficient. + + * [*forward*](/plugins/forward) drops out of order messages, this is solve occasionally FORMERRs + people saw. + +## Brought to You By + +Think we never had so many contributors for a single release. This is really nice to see. Thank you +all: + +AdamDang, +Anders Ingemann, +Andrey Meshkov, +Brian Bao, +Carl-Magnus Björkell, +Chris Aniszczyk, +Chris O'Haver, +Christophe de Carvalho, +ckcd, +Dan Kohn, +Darshan Chaudhary, +DO ANH TUAN, +Guillaume Gelin, +Guy Templeton, +JoeWrightss, +Kenjiro Nakayama, +LongKB, +Miek Gieben, +mrasu, +Nguyen Hai Truong, +Nguyen Phuong An, +Nguyen Quang Huy, +Nguyen Van Duc, +Nguyen Van Trung, +Rob Maas, +Ruslan Drozhdzh, +Sandeep Rajan, +Thomas Mangin, +tuanvcw, +Uladzimir Trehubenka, +Xiao An, +Xuanwo, +Ye Ben, +Yong Tang. diff --git a/ag_201_coredns/notes/coredns-1.5.0.md b/ag_201_coredns/notes/coredns-1.5.0.md new file mode 100644 index 0000000..808f07a --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.5.0.md @@ -0,0 +1,62 @@ ++++ +title = "CoreDNS-1.5.0 Release" +description = "CoreDNS-1.5.0 Release Notes." +tags = ["Release", "1.5.0", "Notes"] +release = "1.5.0" +date = 2019-04-06T08:24:07+00:00 +author = "coredns" ++++ + +A new [release](https://github.com/coredns/coredns/releases/tag/v1.5.0): CoreDNS-1.5.0. + +**Two** new plugins in this release: [*grpc*](/plugins/grpc), and [*ready*](/plugins/ready). And +some polish and simplifications in the core server code. + +The use of **TIMEOUT** and **no_reload** in [*file*](/plugins/file) and [*auto*](/plugins/auto) have +been fully deprecated. As is the [*proxy*](/explugins/proxy/) plugin. + +And a update on two important and active bugs: + +* [2593](https://github.com/coredns/coredns/issues/2593) seems to hone in on Docker and/or the + container environment being a contributing factor. + +* [2624](https://github.com/coredns/coredns/issues/2624) is because of TLS session negotiating in + the *forward* plugin. + +# Plugins + +* The [*ready*](/plugins/ready) plugin was added that signals a plugin is ready to receive queries. First user is the *kubernetes* plugin. +* The [*grpc*](/plugins/grpc) plugin was added to implement forwarding gRPC. If you were relying on this in the [*proxy*](/explugins/proxy) you can migrate to this one. +* The [*cancel*](/plugins/cancel) plugin was added that adds a context.WithTimeout to each context (but not + enabled - yet). +* The [*forward*](/plugins/forward) plugin adds dnstap support. +* The [*route53*](/plugins/route53) plugin now supports wildcards. +* The [*pprof*](/plugins/pprof) plugin adds a `block` option that enables the block profiling. +* The [*prometheus*](/plugins/metrics) plugin adds a `coredns_plugin_enabled` metric that shows which plugins are enabled. +* The [*chaos*](/plugins/chaos) plugin returns the maintainers when queried for "authors.bind TXT CH". +* The `resyncperiod` option in [*kubernetes*](/plugins/kubernetes) now defaults to zero seconds, which disables resyncing. + +# Deprecations + +* The `resyncperiod` option in [*kubernetes*](/plugins/kubernetes) is deprecated + and will be made a no-op in 1.6.0 and removed in 1.7.0. + +## Brought to You By + +Aleks, +Chris O'Haver, +David, +dilyevsky, +Francois Tur, +Iñigo, +Jiacheng Xu, +John Belamaric, +Matt Greenfield, +MengZeLee, +Miek Gieben, +peiranliushop, +Rajveer Malviya, +Ruslan Drozhdzh, +Stefan Budeanu, +Xiao An, +Yong Tang. diff --git a/ag_201_coredns/notes/coredns-1.5.1.md b/ag_201_coredns/notes/coredns-1.5.1.md new file mode 100644 index 0000000..124f179 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.5.1.md @@ -0,0 +1,75 @@ ++++ +title = "CoreDNS-1.5.1 Release" +description = "CoreDNS-1.5.1 Release Notes." +tags = ["Release", "1.5.1", "Notes"] +release = "1.5.1" +date = 2019-06-26T13:54:47+01:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.5.1](https://github.com/coredns/coredns/releases/tag/v1.5.1). + +Various bugfixes, better documentation and cleanups. + +The `-cpu` flag is somewhat redundant (cgroups/systemd/GOMAXPROCS are better ways to deal with +this) and we want to remove it; if you depend on it in some way please voice that in [this +PR](https://github.com/coredns/coredns/pull/2793) otherwise we'll remove it in the next release. + +# Plugins + +* A new plugin [*any*](/plugins/any) that block ANY queries according to [RFC 8482](https://tools.ietf.org/html/rfc8482) was added. +* Failed reload fixes for: [*ready*](/plugins/ready), [*health*](/plugins/health) and + [*prometheus*](/plugins/metrics) - when CoreDNS reloads and the Corefile is invalid these plugins + now keep on working. The [*reload*](/plugin/reload) also gained a metric that export failed + reloads. ([PR](https://github.com/coredns/coredns/pull/2922). +* [*tls*](/plugins/tls) now has a `client_auth` option that allows verification of client TLS certificates. Note that the default behavior continues to be to not require validation, however in version 1.6.0 this default will change to `required_and_verify` if the CA is provided. +* [*kubernetes*](/plugins/kubernetes) can now publish metadata about the request and, if `pods verified` is enabled, about the client Pod. To enable this, you must enable the [*metadata*](/plugins/metadata) plugin. + And also return pod IPs for running pods, instead of just the first + ([PR](https://github.com/coredns/coredns/pull/2846) and + [PR](https://github.com/coredns/coredns/pull/2853) + +* The [*cache*](/plugins/cache) now sets the Authoritative bit on replies + ([PR](https://github.com/coredns/coredns/pull/2885)). Further more it also caches DNS + failures ([PR](https://github.com/coredns/coredns/pull/2720)) + +## Brought to You By + +Alyx, +Andras Spitzer, +Andrey Meshkov, +Anshul Sharma, +Anurag Goel, +An Xiao, +Billie Cleek, +Chris O'Haver, +Cricket Liu, +Francois Tur, +JINMEI Tatuya, +John Belamaric, +Kun Chang, +Michael Grosser, +Miek Gieben, +Sandeep Rajan, +varyoo, +Yong Tang. + +## Noteworthy Changes + +* build: Add CircleCI for Integration testing (https://github.com/coredns/coredns/pull/2889) +* core: Add server instance to the context in ServerTLS and ServerHTTPS (https://github.com/coredns/coredns/pull/2840) +* plugin: Add any plugin (https://github.com/coredns/coredns/pull/2801) +* plugin/cache: cache failures (https://github.com/coredns/coredns/pull/2720) +* plugin/cache: remove item.Autoritative (https://github.com/coredns/coredns/pull/2885) +* plugin/chaos: randomize author list (https://github.com/coredns/coredns/pull/2794) +* plugin/health: add OnRestartFailed (https://github.com/coredns/coredns/pull/2812) +* plugin/kubernetes: make ignore empty work with ext svc types (https://github.com/coredns/coredns/pull/2823) + plugin/kubernetes: never respond with NXDOMAIN for authority label (https://github.com/coredns/coredns/pull/2769) +* plugin/kubernetes: Publish metadata from kubernetes plugin (https://github.com/coredns/coredns/pull/2829) +* plugin/kubernetes: skip deleting pods (https://github.com/coredns/coredns/pull/2853) +* plugin/loop: Update troubleshooting step (https://github.com/coredns/coredns/pull/2804) + plugin/metrcs: fix datarace on listeners (https://github.com/coredns/coredns/pull/2835) +* plugin/metrics: fix failed reload (https://github.com/coredns/coredns/pull/2816) +* plugin/ready: fix starts and restarts (https://github.com/coredns/coredns/pull/2814) +* plugin/template: Raise error if regexp and template are not specified together (https://github.com/coredns/coredns/pull/2884) +* tls: make sure client CA and auth type are set if CA is explicitly specified. (https://github.com/coredns/coredns/pull/2825) diff --git a/ag_201_coredns/notes/coredns-1.5.2.md b/ag_201_coredns/notes/coredns-1.5.2.md new file mode 100644 index 0000000..329328b --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.5.2.md @@ -0,0 +1,40 @@ ++++ +title = "CoreDNS-1.5.2 Release" +description = "CoreDNS-1.5.2 Release Notes." +tags = ["Release", "1.5.2", "Notes"] +release = "1.5.2" +date = 2019-07-03T07:35:47+01:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.5.2](https://github.com/coredns/coredns/releases/tag/v1.5.2). + +Small bugfixes and a change to Caddy's import path (mholt/caddy -> caddyserver/caddy). Doing +a release helps plugins deal with the change better. + +# Plugins + +* For all plugins that use the `upstream` directive it use removed from the documentation; it's still accepted + but is a noop. Currently these plugins use CoreDNS to resolve external queries. +* The [*template*](/plugins/template) plugin now supports meta data. +* The [*file*](/plugins/file) plugin closes the connection after an AXFR. It also loads secondary zones + lazily on startup. + +## Brought to You By + +bcebere, +John Belamaric, +JINMEI Tatuya, +Miek Gieben, +Timoses, +Yong Tang. + +## Noteworthy Changes + +* plugin/file: close correctly after AXFR (https://github.com/coredns/coredns/pull/2943) +* plugin/file: load secondary zones lazily on startup (https://github.com/coredns/coredns/pull/2944) +* plugin/template: support metadata (https://github.com/coredns/coredns/pull/2958) +* build: Update Caddy to 1.0.1, and update import path (https://github.com/coredns/coredns/pull/2961) +* plugins: set upstream unconditionally (https://github.com/coredns/coredns/pull/2956) +* tls: hardening (https://github.com/coredns/coredns/pull/2938) diff --git a/ag_201_coredns/notes/coredns-1.6.0.md b/ag_201_coredns/notes/coredns-1.6.0.md new file mode 100644 index 0000000..f5414fc --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.6.0.md @@ -0,0 +1,62 @@ ++++ +title = "CoreDNS-1.6.0 Release" +description = "CoreDNS-1.6.0 Release Notes." +tags = ["Release", "1.6.0", "Notes"] +release = "1.6.0" +date = 2019-07-28T14:35:47+01:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.6.0](https://github.com/coredns/coredns/releases/tag/v1.6.0). + +The `-cpu` flag is removed from this version. + +This release sports changes in the *file* plugin. A speed up in the *log* plugin and fixes in the +*cache* and *hosts* plugins. + +Upcoming deprecation: the kubernetes *federation* plugin [will be moved +to](https://github.com/coredns/coredns/issues/3041) github.com/coredns/federation. This is likely to +happen in CoreDNS 1.7.0. + +# Plugins + +* The [*file*](/plugins/file) got lot of bug fixes and it now loads zones lazily on start, i.e. if the zonefile + does not exist, it keeps trying with every reload period. +* The [*cache*](/plugins/cache) fixes a race. +* Multiple fixes in the [*route53*](/plugins/route53) plugin. +* And the [*kubernetes*](/plugins/kubernetes) removes the `resyncperiod` option. +* The [*host*](/plugins/host) appended entries from /etc/hosts on each (re-)parse, instead of + overwriting them. +* Speed ups in the [*log*](/plugins/log) plugin. + +## Brought to You By + +Anshul Sharma, +Charlie Vieth, +Chris O'Haver, +Christian Muehlhaeuser, +Erfan Besharat, +Jintao Zhang, +Mat Lowery, +Miek Gieben, +Ruslan Drozhdzh, +Yong Tang. + +## Noteworthy Changes + +* core: Scrub: TC bit is always set (https://github.com/coredns/coredns/pull/3001) +* pkg/cache: Fix race in Add() and Evict() (https://github.com/coredns/coredns/pull/3013) +* pkg/replacer: Evaluate format once and improve perf by ~3x (https://github.com/coredns/coredns/pull/3002) +* plugin/file: Fix setting ReloadInterval (https://github.com/coredns/coredns/pull/3017) +* plugin/file: Make non-existent file non-fatal (https://github.com/coredns/coredns/pull/2955) +* plugin/file: New zone should have zero records (https://github.com/coredns/coredns/pull/3025) +* plugin/file: Rename do to walk and cleanup and document (https://github.com/coredns/coredns/pull/2987) +* plugin/file: Simplify locking (https://github.com/coredns/coredns/pull/3024) +* plugin/host: don't append the names when reparsing hosts file (https://github.com/coredns/coredns/pull/3045) +* plugin/kubernetes: Remove resyncperiod (https://github.com/coredns/coredns/pull/2923) +* plugin/log: Fix log plugin benchmark and slightly improve performance (https://github.com/coredns/coredns/pull/3004) +* plugin/metrics: Fix response_rcode_count_total metric (https://github.com/coredns/coredns/pull/3029) +* plugin/rewrite: Fix domain length validation (https://github.com/coredns/coredns/pull/2995) +* plugin/route53: Fix IAM credential file (https://github.com/coredns/coredns/pull/2983) +* plugin/route53: Fix multiple credentials in route53 (https://github.com/coredns/coredns/pull/2859) diff --git a/ag_201_coredns/notes/coredns-1.6.1.md b/ag_201_coredns/notes/coredns-1.6.1.md new file mode 100644 index 0000000..76db2d1 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.6.1.md @@ -0,0 +1,37 @@ ++++ +title = "CoreDNS-1.6.1 Release" +description = "CoreDNS-1.6.1 Release Notes." +tags = ["Release", "1.6.1", "Notes"] +release = "1.6.1" +date = 2019-08-02T14:35:47+01:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.6.1](https://github.com/coredns/coredns/releases/tag/v1.6.1). + +This is a small (bug fix) release. + +# Plugins + +* Fix a panic in the [*hosts*](/plugins/hosts) plugin. +* The [*reload*](/plugins/reload) now detects changes in files imported from the main Corefile. +* [*route53*](/plugins/route53) increases the paging size when talking to the AWS API, this + decreases the chances of getting throttled. + +## Brought to You By + +Alan, +AllenZMC, +dzzg, +Erik Wilson, +Matt Kulka, +Miek Gieben, +Yong Tang. + +## Noteworthy Changes + +core: log panics (https://github.com/coredns/coredns/pull/3072) +plugin/hosts: create inline map in setup (https://github.com/coredns/coredns/pull/3071) +plugin/reload: Graceful reload of imported files (https://github.com/coredns/coredns/pull/3068) +plugin/route53: Increase ListResourceRecordSets paging size. (https://github.com/coredns/coredns/pull/3073) diff --git a/ag_201_coredns/notes/coredns-1.6.2.md b/ag_201_coredns/notes/coredns-1.6.2.md new file mode 100644 index 0000000..f942de7 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.6.2.md @@ -0,0 +1,41 @@ ++++ +title = "CoreDNS-1.6.2 Release" +description = "CoreDNS-1.6.2 Release Notes." +tags = ["Release", "1.6.2", "Notes"] +release = "1.6.2" +date = 2019-08-13T14:35:47+01:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.6.2](https://github.com/coredns/coredns/releases/tag/v1.6.2). + +This is a bug fix release, but it also features a new plugin called [*azure*](/plugins/azure). + +It's compiled with Go 1.12.8 that incorporates fixes for HTTP/2 that may impact you if you use +[DoH](https://tools.ietf.org/html/rfc8484). + +# Plugins + +* Add [*azure*](/plugins/azure) to facilitate serving records from Microsoft Azure. +* Make the refresh frequency adjustable in the [*route53*](/plugins/route53) plugin. +* Fix the handling of truncated responses in [*forward*](/plugins/forward). + +## Brought to You By + +Andrey Meshkov, +Chris O'Haver, +Darshan Chaudhary, +ethan, +Matt Kulka +and +Miek Gieben. + +## Noteworthy Changes + +* plugin/azure: Add plugin for Azure DNS (https://github.com/coredns/coredns/pull/2945) +* plugin/forward: Fix handling truncated responses in forward (https://github.com/coredns/coredns/pull/3110) +* plugin/kubernetes: Don't do a zone transfer for NS requests (https://github.com/coredns/coredns/pull/3098) +* plugin/kubernetes: fix NXDOMAIN/NODATA fallthough case (https://github.com/coredns/coredns/pull/3118) +* plugin/route53: make refresh frequency adjustable (https://github.com/coredns/coredns/pull/3083) +* plugin/route53: Various updates (https://github.com/coredns/coredns/pull/3108) diff --git a/ag_201_coredns/notes/coredns-1.6.3.md b/ag_201_coredns/notes/coredns-1.6.3.md new file mode 100644 index 0000000..10a9182 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.6.3.md @@ -0,0 +1,66 @@ ++++ +title = "CoreDNS-1.6.3 Release" +description = "CoreDNS-1.6.3 Release Notes." +tags = ["Release", "1.6.3", "Notes"] +release = "1.6.3" +date = 2019-08-31T07:30:47+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.6.3](https://github.com/coredns/coredns/releases/tag/v1.6.3). + +In this release we have moved the *federation* plugin to +[github.com/coredns/federation](https://github.com/coredns/federation), but it is still fully +functional in this version. In version 1.7.0 we expect to deprecate it. + +Further more a slew a spelling corrections, and other minor improvements and polish. **And** two new +plugins: *clouddns* and *sign*. + +# Plugins + +* [*clouddns*](/plugins/clouddns) to enable serving zone data from GCP Cloud DNS. +* [*sign*](/plugins/sign) that (DNSSEC) signs your zonefiles (in its most basic form). +* [*file*](/plugins/file) various update, plug a memory leak when doing zone transfers, among other + things. + +We've removed the time stamping from `pkg/log` as timestamps are *also* added by the logging +aggregators, like `journald` or inside Kubernetes. And a small ASCII art logo is now printed when +CoreDNS starts up. + +## Brought to You By + +AllenZMC, +Chris Aniszczyk, +Chris O'Haver, +Cricket Liu, +Guangming Wang, +Julien Garcia Gonzalez, +li mengyang, +Miek Gieben, +Muhammad Falak R Wani, +Palash Nigam, +Sakura, +wwgfhf, +xieyanker, +Xigang Wang, +Yevgeny Pats, +Yong Tang, +zhangguoyan, +陈谭军. + + +## Noteworthy Changes + +* fuzzing: Add Continuous Fuzzing Integration to Fuzzit (https://github.com/coredns/coredns/pull/3093) +* pkg/log: remove timestamp (https://github.com/coredns/coredns/pull/3218) +* plugin/clouddns: Add Google Cloud DNS plugin (https://github.com/coredns/coredns/pull/3011) +* plugin/federation: Move federation plugin to github.com/coredns/federation (https://github.com/coredns/coredns/pull/3139) +* plugin/file: close reader for reload (https://github.com/coredns/coredns/pull/3196) +* plugin/file: less notify logging spam (https://github.com/coredns/coredns/pull/3212) +* plugin/file: respond correctly to IXFR message (https://github.com/coredns/coredns/pull/3177) +* plugin/file: rework outgoing axfr (https://github.com/coredns/coredns/pull/3227) +* plugin/{health,ready}: return standardized text for ready and health endpoint (https://github.com/coredns/coredns/pull/3195) +* plugin/k8s_external handle NS records (https://github.com/coredns/coredns/pull/3160) +* plugin/kubernetes: handle NS records (https://github.com/coredns/coredns/pull/3160) +* startup: add logo (https://github.com/coredns/coredns/pull/3230) diff --git a/ag_201_coredns/notes/coredns-1.6.4.md b/ag_201_coredns/notes/coredns-1.6.4.md new file mode 100644 index 0000000..3a8a4bc --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.6.4.md @@ -0,0 +1,41 @@ ++++ +title = "CoreDNS-1.6.4 Release" +description = "CoreDNS-1.6.4 Release Notes." +tags = ["Release", "1.6.4", "Notes"] +release = "1.6.4" +date = 2019-09-27T10:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.6.4](https://github.com/coredns/coredns/releases/tag/v1.6.4). + +Various code cleanups and documentation improvements. We've added one new plugin: *acl*, that allows +blocking requests. + +# Plugins + +* [*acl*](/plugins/acl) block request from IPs or IP ranges. +* [*kubernetes*](/plugins/kubernetes) received some bug fixes, see below for specific PRs. +* [*hosts*](/plugins/hosts) exports metrics on the number of entries and last reload time. + +## Brought to You By + +An Xiao, +Chris O'Haver, +Cricket Liu, +Guangming Wang, +Kasisnu, +li mengyang, +Miek Gieben, +orangelynx, +xieyanker, +yeya24, +Yong Tang. + +## Noteworthy Changes + +* plugin/hosts: add host metrics (https://github.com/coredns/coredns/pull/3277) +* plugin/kubernetes: Don't duplicate service record for every port (https://github.com/coredns/coredns/pull/3240) +* plugin/kubernetes: Handle multiple local IPs and bind (https://github.com/coredns/coredns/pull/3208) +* Add plugin ACL for source IP filtering (https://github.com/coredns/coredns/pull/3103) diff --git a/ag_201_coredns/notes/coredns-1.6.5.md b/ag_201_coredns/notes/coredns-1.6.5.md new file mode 100644 index 0000000..bb426a9 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.6.5.md @@ -0,0 +1,52 @@ ++++ +title = "CoreDNS-1.6.5 Release" +description = "CoreDNS-1.6.5 Release Notes." +tags = ["Release", "1.6.5", "Notes"] +release = "1.6.5" +date = 2019-11-06T10:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.6.5](https://github.com/coredns/coredns/releases/tag/v1.6.5). + +A fairly small release that polishes various plugins and fixes a bunch of bugs. + +# Plugins + +A new plugin [*transfer*](/plugins/transfer) that encapsulates the zone transfer knowledge and code +in one place. This makes it easier to add this functionality to new plugins. Plugins that already +implement zone transfers are expected to move to it in the 1.7.0 release. + +* [*forward*](/plugins/forward) don't block on returning sockets; instead timeout and drop the + socket on the floor, this makes each go-routine guarantee to exit. +* [*kubernetes*](/plugins/kubernetes) adds metrics to measure kubernetes control plane latency, see + documentation for details. +* [*file*](/plugins/file) fixes a panic when comparing domains names. + +## Brought to You By + +Chris O'Haver, +Erfan Besharat, +Hauke Löffler, +Ingo Gottwald, +janluk, +Miek Gieben, +Uladzimir Trehubenka, +Yong Tang, +yuxiaobo96. + +## Noteworthy Changes + +* core: Make request.Request smaller (https://github.com/coredns/coredns/pull/3351) +* pkg/log: Add Clear to stop debug logging (https://github.com/coredns/coredns/pull/3372) +* plugin/cache: move goroutine closure to separate function to save memory (https://github.com/coredns/coredns/pull/3353) +* plugin/clouddns: remove initialization from init (https://github.com/coredns/coredns/pull/3349) +* plugin/erratic: doc and zone transfer (https://github.com/coredns/coredns/pull/3340) +* plugin/file: fix panic in miekg/dns.CompareDomainName() (https://github.com/coredns/coredns/pull/3337) +* plugin/forward: make Yield not block (https://github.com/coredns/coredns/pull/3336) +* plugin/forward: Move map to array (https://github.com/coredns/coredns/pull/3339) +* plugin/kubernetes: Measure and expose DNS programming latency from Kubernetes plugin. (https://github.com/coredns/coredns/pull/3171) +* plugin/route53: Remove amazon initialization from init (https://github.com/coredns/coredns/pull/3348) +* plugin/transfer: Zone transfer plugin (https://github.com/coredns/coredns/pull/3223) +* plugins: Add MustNormalize (https://github.com/coredns/coredns/pull/3385) diff --git a/ag_201_coredns/notes/coredns-1.6.6.md b/ag_201_coredns/notes/coredns-1.6.6.md new file mode 100644 index 0000000..5ede37c --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.6.6.md @@ -0,0 +1,50 @@ ++++ +title = "CoreDNS-1.6.6 Release" +description = "CoreDNS-1.6.6 Release Notes." +tags = ["Release", "1.6.6", "Notes"] +release = "1.6.6" +date = 2019-12-11T10:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.6.6](https://github.com/coredns/coredns/releases/tag/v1.6.6). + +A fairly small release that polishes various plugins and fixes a bunch of bugs. + +## Security + +The github.com/miekg/dns dependency has been updated +to v1.1.25 to fix a DNS related security vulnerability +([https://github.com/miekg/dns/issues/1043](https://github.com/miekg/dns/issues/1043)). + +## Plugins + +A new plugin [*bufsize*](/plugins/bufsize) has been added that prevents IP fragmentation +for the DNS Flag Day 2020 and to deal with DNS vulnerabilities. + +* [*cache*](/plugins/cache) added a `serve_stale` option similar to `unbound`'s `serve_expired`. +* [*sign*](/plugins/sign) fix signing of authoritative data that we are not authoritative for. +* [*transfer*](/plugins/transfer) fixed calling wg.Add in main goroutine to avoid race conditons. + +## Brought to You By + +Chris O'Haver +Gonzalo Paniagua Javier +Guangming Wang +Kohei Yoshida +Miciah Dashiel Butler Masters +Miek Gieben +Yong Tang +Zou Nengren + +## Noteworthy Changes + +* plugin/bufsize: A new bufsize plugin to prevent IP fragmentation and DNS Flag Day 2020 (https://github.com/coredns/coredns/pull/3401) +* plugin/transfer: Fixed calling wg.Add in main goroutine to avoid race conditions (https://github.com/coredns/coredns/pull/3433) +* plugin/pprof: Fixed a reloading issue (https://github.com/coredns/coredns/pull/3454) +* plugin/health: Fixed a reloading issue (https://github.com/coredns/coredns/pull/3473) +* plugin/redy: Fixed a reloading issue (https://github.com/coredns/coredns/pull/3473) +* plugin/cache: Added a `serve_stale` option similar to `unbound`'s `serve_expired` (https://github.com/coredns/coredns/pull/3468) +* plugin/sign: Fix signing of authoritative data (https://github.com/coredns/coredns/pull/3479) +* pkg/reuseport: Move the core server listening functions to a new package (https://github.com/coredns/coredns/pull/3455) diff --git a/ag_201_coredns/notes/coredns-1.6.7.md b/ag_201_coredns/notes/coredns-1.6.7.md new file mode 100644 index 0000000..98aaa4e --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.6.7.md @@ -0,0 +1,41 @@ ++++ +title = "CoreDNS-1.6.7 Release" +description = "CoreDNS-1.6.7 Release Notes." +tags = ["Release", "1.6.7", "Notes"] +release = "1.6.7" +date = 2020-01-28T10:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.6.7](https://github.com/coredns/coredns/releases/tag/v1.6.7). + +This is a fairly small release that resolves some nits and it adds mips64le to the set of +architectures that we create binaries for. See "Noteworthy Changes" for more detail. + +## Brought to You By + +Antonio Ojea, +Brad P. Crochet, +Dominic Yin, +DrmagicE, +Erfan Besharat, +Jonathan Nagy, +Kohei Yoshida, +Miek Gieben, +Yong Tang, +Zheng Xie, +Zou Nengren. + +## Noteworthy Changes + +* Add mips64le to released architectures (https://github.com/coredns/coredns/pull/3589) +* Fix HostPortOrFile to support IPv6 addresses with zone (https://github.com/coredns/coredns/pull/3527) +* plugin/acl: Document metrics in README (https://github.com/coredns/coredns/pull/3605) +* plugin/cache: Registry cache_miss logic (https://github.com/coredns/coredns/pull/3578) +* plugin/cache: Update comment to conform to the implementation (https://github.com/coredns/coredns/pull/3573) +* plugin/{forward, grpc}: Dedup policy implement between grpc and proxy plugin (https://github.com/coredns/coredns/pull/3537) +* plugin/kubernetes: Bump kubernetes plugin schema version (https://github.com/coredns/coredns/pull/3554) +* plugin/{kubernetes, etc}: Resolve TXT records via CNAME (https://github.com/coredns/coredns/pull/3557) +* plugin/logs: Docs: update README and log plugin (https://github.com/coredns/coredns/pull/3602) +* plugin/sign: Add expiration jitter (https://github.com/coredns/coredns/pull/3588) diff --git a/ag_201_coredns/notes/coredns-1.6.8.md b/ag_201_coredns/notes/coredns-1.6.8.md new file mode 100644 index 0000000..47052f0 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.6.8.md @@ -0,0 +1,44 @@ ++++ +title = "CoreDNS-1.6.8 Release" +description = "CoreDNS-1.6.8 Release Notes." +tags = ["Release", "1.6.8", "Notes"] +release = "1.6.8" +date = 2020-03-24T10:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.6.8](https://github.com/coredns/coredns/releases/tag/v1.6.8). + +Again a small release with some nice improvements in the *forward* plugin, and overall polish. See +"Noteworthy Changes" for more detail. + +Note that 1.7.0 will contain a bunch of backward incompatible changes: the *federation* plugin will +be full removed and the metrics name will be changed to inline with the naming recommendation from +the Prometheus project. + +## Brought to You By + +Andy Bursavich, +Chris O'Haver, +Christian Tryti, +Darshan Chaudhary, +Kohei Yoshida, +LongKB, +Miek Gieben, +Ricky S, +Sylvain Rabot, +Zou Nengren. + +## Noteworthy Changes + +* plugin/azure: Add private DNS support for azure plugin (https://github.com/coredns/coredns/pull/1516) +* plugin/cache: explain drop metric (https://github.com/coredns/coredns/pull/3706) +* plugin/forward: Add configuration flag to set if RecursionDesired should be set on health checks (https://github.com/coredns/coredns/pull/3679) +* plugin/forward: Add exponential backoff to healthcheck (https://github.com/coredns/coredns/pull/3643) +* plugin/forward: Add max_concurrent option (https://github.com/coredns/coredns/pull/3640) +* plugin/hosts: Modifies NODATA handling (https://github.com/coredns/coredns/pull/3536) +* plugin/kubernetes: fix metadata (https://github.com/coredns/coredns/pull/3642) +* plugin/kubernetes: Return all records with matching IP for reverse queries (https://github.com/coredns/coredns/pull/3687) +* plugin/metrics: Add query type to latency as well (https://github.com/coredns/coredns/pull/3685) +* plugin/pkg/up: Make default intervals shorter (https://github.com/coredns/coredns/pull/3651) diff --git a/ag_201_coredns/notes/coredns-1.6.9.md b/ag_201_coredns/notes/coredns-1.6.9.md new file mode 100644 index 0000000..3f70fc6 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.6.9.md @@ -0,0 +1,49 @@ ++++ +title = "CoreDNS-1.6.9 Release" +description = "CoreDNS-1.6.9 Release Notes." +tags = ["Release", "1.6.9", "Notes"] +release = "1.6.9" +date = 2020-03-24T10:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.6.9](https://github.com/coredns/coredns/releases/tag/v1.6.9). This release is identical +to 1.6.8. + +(Yes there was a [CoreDNS-1.6.8](https://github.com/coredns/coredns/releases/tag/v1.6.8), but our +automation broke after tagging it in Git - hence another bump in the minor version) + +Again a small release with some nice improvements in the *forward* plugin, and overall polish. See +"Noteworthy Changes" for more detail. + +Note that 1.7.0 will contain a bunch of backward incompatible changes: the *federation* plugin will +be full removed and the metrics name will be changed to inline with the naming recommendation from +the Prometheus project. + +## Brought to You By + +Andy Bursavich, +Chris O'Haver, +Christian Tryti, +Darshan Chaudhary, +Kohei Yoshida, +LongKB, +Miek Gieben, +Ricky S, +Sylvain Rabot, +Zou Nengren. + +## Noteworthy Changes + +* plugin/azure: Add private DNS support for azure plugin (https://github.com/coredns/coredns/pull/1516) +* plugin/cache: Fix negative cache masking cases (https://github.com/coredns/coredns/pull/3744) +* plugin/cache: explain drop metric (https://github.com/coredns/coredns/pull/3706) +* plugin/forward: Add configuration flag to set if RecursionDesired should be set on health checks (https://github.com/coredns/coredns/pull/3679) +* plugin/forward: Add exponential backoff to healthcheck (https://github.com/coredns/coredns/pull/3643) +* plugin/forward: Add max_concurrent option (https://github.com/coredns/coredns/pull/3640) +* plugin/hosts: Modifies NODATA handling (https://github.com/coredns/coredns/pull/3536) +* plugin/kubernetes: fix metadata (https://github.com/coredns/coredns/pull/3642) +* plugin/kubernetes: Return all records with matching IP for reverse queries (https://github.com/coredns/coredns/pull/3687) +* plugin/metrics: Add query type to latency as well (https://github.com/coredns/coredns/pull/3685) +* plugin/pkg/up: Make default intervals shorter (https://github.com/coredns/coredns/pull/3651) diff --git a/ag_201_coredns/notes/coredns-1.7.0.md b/ag_201_coredns/notes/coredns-1.7.0.md new file mode 100644 index 0000000..fddf715 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.7.0.md @@ -0,0 +1,93 @@ ++++ +title = "CoreDNS-1.7.0 Release" +description = "CoreDNS-1.7.0 Release Notes." +tags = ["Release", "1.7.0", "Notes"] +release = "1.7.0" +date = 2020-06-15T10:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.7.0](https://github.com/coredns/coredns/releases/tag/v1.7.0). + +This is a **backwards-incompatible release**. Major changes include: + +* Better [metrics names](https://github.com/coredns/coredns/pull/3776). +* The *federation* plugin (which allows for v1 Kubernetes federation) has been removed. We've also removed + some supporting code from the *kubernetes* plugin, so it will not build as an external plugin + (with this version of CoreDNS). + +As this was already backwards-incompatible release, we took the liberty of stuffing as much in +one release as possible to minimize the disruption going forward. + +A new plugin, [*dns64*](https://coredns.io/plugins/dns64), was promoted from external to a plugin that +is included by default. This plugin "enables DNS64 IPv6 transition mechanism." + +### Metric Changes + +We mostly dropped `count` from `_total` metrics names: + +* `coredns_request_block_count_total` -\> `coredns_dns_blocked_requests_total` +* `coredns_request_allow_count_total` -\> `coredns_dns_allowed_requests_total` + +* `coredns_dns_acl_request_block_count_total` -\> `coredns_acl_blocked_requests_total` +* `coredns_dns_acl_request_allow_count_total` -\> `coredns_acl_allowed_requests_total` + +* `coredns_autopath_success_count_total` -\> `coredns_autopath_success_total` + +* `coredns_forward_request_count_total` -\> `coredns_forward_requests_total` +* `coredns_forward_response_rcode_count_total` -\> `coredns_forward_responses_total` +* `coredns_forward_healthcheck_failure_count_total` -\> `coredns_forward_healthcheck_failures_total` +* `coredns_forward_healthcheck_broken_count_total` -\> `coredns_forward_healthcheck_broken_total` +* `coredns_forward_max_concurrent_reject_count_total` -\> `coredns_forward_max_concurrent_rejects_total` + +* `coredns_grpc_request_count_total` -\> `coredns_grpc_requests_total` +* `coredns_grpc_response_rcode_count_total` -\> `coredns_grpc_responses_total` + +* `coredns_panic_count_total` -\> `coredns_panics_total` +* `coredns_dns_request_count_total` -\> `coredns_dns_requests_total` +* `coredns_dns_request_do_count_total` -\> `coredns_dns_do_requests_total` +* `coredns_dns_response_rcode_count_total` -\> `coredns_dns_responses_total` + +* `coredns_reload_failed_count_total` -\> `coredns_reload_failed_total` + +* `coredns_cache_size` -\> `coredns_cache_entries` + +And note that +`coredns_dns_request_type_count_total` is now part of `coredns_dns_requests_total` . + +## Brought to You By + +Ambrose Chua, +Ben Kochie, +Catena cyber, +Chanakya-Ekbote, +Chris O'Haver, +Daisuke TASAKI, +Eli Lindsey, +Erfan Besharat, +Krzysztof Dąbrowski, +Michael Kashin, +Miek Gieben, +Mirek S, +Pablo Caderno, +Sandeep Rajan, +Tobias Schmidt, +Yang Bo, +Yong Tang, +Zou Nengren. + +## Noteworthy Changes + +* plugin/azure: Fix environment option overwrite (https://github.com/coredns/coredns/pull/3922) +* plugin/dns64: Add DNS64 plugin (https://github.com/coredns/coredns/pull/3534) +* plugin/federation: Remove already-deprecated federation plugin (https://github.com/coredns/coredns/pull/3794) +* plugin/forward: Fix only first upstream server is used in forward plugin (https://github.com/coredns/coredns/issues/3900) +* plugin/forward: Avoid https protocol (https://github.com/coredns/coredns/pull/3817) +* plugin/k8s_external: Add CNAME support for AWS ELB/NLB (https://github.com/coredns/coredns/pull/3916) +* plugin/kubernetes: Remove already-deprecated options `resyncperiod` and `upstream` (https://github.com/coredns/coredns/pull/3737) +* plugin/kubernetes: Populate client metadata for external queries (https://github.com/coredns/coredns/pull/3874) +* plugin/kubernetes: Fix 0 weight in SRV records with 100 or more records in answer (https://github.com/coredns/coredns/pull/3931) +* plugin/kubernetes: Handle tombstones in kubernetes plugin (https://github.com/coredns/coredns/pull/3887) and (https://github.com/coredns/coredns/pull/3890) +* plugin/nsid: Fix NSID not being set on cached responses (https://github.com/coredns/coredns/pull/3822) +* metrics: Better metrics names (https://github.com/coredns/coredns/pull/3776), (https://github.com/coredns/coredns/pull/3799), and (https://github.com/coredns/coredns/pull/3805) diff --git a/ag_201_coredns/notes/coredns-1.7.1.md b/ag_201_coredns/notes/coredns-1.7.1.md new file mode 100644 index 0000000..aafb8b8 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.7.1.md @@ -0,0 +1,53 @@ ++++ +title = "CoreDNS-1.7.1 Release" +description = "CoreDNS-1.7.1 Release Notes." +tags = ["Release", "1.7.1", "Notes"] +release = "1.7.1" +date = 2020-09-21T10:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.7.1](https://github.com/coredns/coredns/releases/tag/v1.7.1). + +This is a small, incremental release that adds some polish and fixes a bunch of bugs. + +## Brought to You By + +Ben Kochie, +Ben Ye, +Chris O'Haver, +Cricket Liu, +Grant Garrett-Grossman, +Hu Shuai, +Li Zhijian, +Maxime Guyot, +Miek Gieben, +milgradesec, +Oleg Atamanenko, +Olivier Lemasle, +Ricardo Katz, +Ruslan Drozhdzh, +Yong Tang, +Zhou Hao, +Zou Nengren. + +## Noteworthy Changes + +* backend: fix root zone usage (https://github.com/coredns/coredns/pull/4039) +* core: Add timeouts for http server (https://github.com/coredns/coredns/pull/3920) +* pkg/upstream: set edns0 and Do when required (https://github.com/coredns/coredns/pull/4055) +* plugin/cache: cache: default to DNSSEC (https://github.com/coredns/coredns/pull/4085) +* plugin/{clouddns,route53}: fix lingering goroutines after restart (https://github.com/coredns/coredns/pull/4096) +* plugin/debug: Enable debug globally if enabled in any server config (https://github.com/coredns/coredns/pull/4007) +* plugin/{etcd,kubernetes}: fix root zone usage (https://github.com/coredns/coredns/pull/4039) +* plugin/forward: add hit/miss metrics for connection cache (https://github.com/coredns/coredns/pull/4114) +* plugin/forward: fix panic when `expire` is configured as 0s (https://github.com/coredns/coredns/pull/4115) +* plugin/forward: init ClientSessionCache in tls.Config (https://github.com/coredns/coredns/pull/4108) +* plugin/forward: Register HealthcheckBrokenCount (https://github.com/coredns/coredns/pull/4021) +* plugin/grpc: Improve gRPC Plugin when backend is not available (https://github.com/coredns/coredns/pull/3966) +* plugins: Using promauto package to ensure all created metrics are properly registered (https://github.com/coredns/coredns/pull/4025). +* plugin/template: Add client IP data (https://github.com/coredns/coredns/pull/4034) +* plugin/trace: fix struct allignment (https://github.com/coredns/coredns/pull/4112) +* plugin/trace: Only with *debug* active enable debug mode for tracing - removes extra logging (https://github.com/coredns/coredns/pull/4016) +* project: Add DCO requirement in Contributing guidelines (https://github.com/coredns/coredns/pull/4008) diff --git a/ag_201_coredns/notes/coredns-1.8.0.md b/ag_201_coredns/notes/coredns-1.8.0.md new file mode 100644 index 0000000..071ce5f --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.8.0.md @@ -0,0 +1,68 @@ ++++ +title = "CoreDNS-1.8.0 Release" +description = "CoreDNS-1.8.0 Release Notes." +tags = ["Release", "1.8.0", "Notes"] +release = "1.8.0" +date = 2020-10-22T08:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.8.0](https://github.com/coredns/coredns/releases/tag/v1.8.0). + +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](/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 +need to fix the import path from: `github.com/caddyserver/caddy` to `github.com/coredns/caddy` (this +can thankfully be automated). + +And lastly, the `transfer` plugin is now made a first class citizen and plugins wanting to perform +outgoing zone transfers now use this plugin: *file*, *auto*, *secondary* and *kubernetes* are +converted. For this you must change your Corefile from (e.g.): + +``` txt +example.org { + file example.org.signed { + transfer to * + transfer to 10.240.1.1 + } +} +``` + +To + +``` txt +example.org { + file example.org.signed + transfer { + to * 10.240.1.1 + } +} +``` + +## Brought to You By + +Bob, +Chris O'Haver, +Johnny Bergström, +Macks, +Miek Gieben, +Yong Tang. + +## Noteworthy Changes +* core: doh support: fix alpn for http/2 upgrade when using DoH (https://github.com/coredns/coredns/pull/4182) +* core: doh support: make no TLS config fatal (https://github.com/coredns/coredns/pull/4162) +* core: fix crash with no plugins (https://github.com/coredns/coredns/pull/4184) +* core: Move caddy v1 in our GitHub org (https://github.com/coredns/coredns/pull/4018) +* plugin/auto: allow fallthrough if no zones match (https://github.com/coredns/coredns/pull/4166) +* plugin/cache: Fix filtering (https://github.com/coredns/coredns/pull/4148) +* plugin/cache: Fix removing OPT (https://github.com/coredns/coredns/pull/4190) +* plugin/dnstap: various cleanups (https://github.com/coredns/coredns/pull/4179) +* plugin/ready: don't return 200 during shutdown (https://github.com/coredns/coredns/pull/4167) +* plugin/trace: root span names no longer contain the query data (https://github.com/coredns/coredns/pull/4171) +* plugin/transfer: Implement notifies for transfer plugin (https://github.com/coredns/coredns/pull/3972) diff --git a/ag_201_coredns/notes/coredns-1.8.1.md b/ag_201_coredns/notes/coredns-1.8.1.md new file mode 100644 index 0000000..4d80c0d --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.8.1.md @@ -0,0 +1,49 @@ ++++ +title = "CoreDNS-1.8.1 Release" +description = "CoreDNS-1.8.1 Release Notes." +tags = ["Release", "1.8.1", "Notes"] +release = "1.8.1" +date = 2021-01-20T07:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.8.1](https://github.com/coredns/coredns/releases/tag/v1.8.1). + +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 +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 + +Blake Ryan, Bob, Chotiwat Chawannakul, Chris O'Haver, Guangwen Feng, Gunadhya, Jiang Biao, Johnny +Bergström, luanphantiki, Matt Kulka, mgugger, Miek Gieben, Serge, sschepens, Yong Tang, ZouYu. + +## Noteworthy Changes + +* plugin/{clouddns,azure,route53}: Use cancelable contexts for cloud provider plugin refreshes (https://github.com/coredns/coredns/pull/4226) +* plugin/azure: Iterate over all RecordSetListResultPage Pages (https://github.com/coredns/coredns/pull/4351) +* core: custom DoH request validation (https://github.com/coredns/coredns/pull/4329) +* pkg/tls: remove InsecureSkipVerify=true flag (https://github.com/coredns/coredns/pull/4265) +* plugin/azure: return FQDN as MNAME in SOA record (https://github.com/coredns/coredns/pull/4286) +* plugin/cache Prevent race from prefetching (https://github.com/coredns/coredns/pull/4368) +* plugin/dnssec: Change hash key input (https://github.com/coredns/coredns/pull/4372) +* plugin/dnstap: remove config struct (https://github.com/coredns/coredns/pull/4258) +* plugin/dnstap: remove custom encoder (https://github.com/coredns/coredns/pull/4242) +* plugin/file: Use NXDOMAIN response if CNAME target is NXDOMAIN (https://github.com/coredns/coredns/pull/4303) +* plugin/file: document wrong behavior in lookup fox Apex (https://github.com/coredns/coredns/pull/4376) +* plugin/file: guard against cname loops (https://github.com/coredns/coredns/pull/4387) +* plugin/forward: respond with REFUSED when max_concurrent is exceeded (https://github.com/coredns/coredns/pull/4326) +* plugin/forward: HC every 0.5 seconds, do not do exponential backoff (https://github.com/coredns/coredns/pull/4371) +* plugin/health: Fix health check endpoint (https://github.com/coredns/coredns/pull/4231) +* plugin/kubernetes: Add support for dual stack ClusterIP Services (https://github.com/coredns/coredns/pull/4339) +* plugin/kubernetes: Fix dns programming duration metric (https://github.com/coredns/coredns/pull/4255) +* plugin/kubernetes: Fix NPE issue (https://github.com/coredns/coredns/pull/4338) +* plugin/kubernetes: Watch EndpointSlices (https://github.com/coredns/coredns/pull/4209) +* plugin/local: add local plugin (https://github.com/coredns/coredns/pull/4262) +* plugin/trace: Fix zipkin json_v2 (https://github.com/coredns/coredns/pull/4180) +* plugin/transfer: Fix go-routine leak (https://github.com/coredns/coredns/pull/4380) diff --git a/ag_201_coredns/notes/coredns-1.8.2.md b/ag_201_coredns/notes/coredns-1.8.2.md new file mode 100644 index 0000000..4967f13 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.8.2.md @@ -0,0 +1,45 @@ ++++ +title = "CoreDNS-1.8.2 Release" +description = "CoreDNS-1.8.2 Release Notes." +tags = ["Release", "1.8.2", "Notes"] +release = "1.8.2" +date = 2021-02-23T07:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.8.2](https://github.com/coredns/coredns/releases/tag/v1.8.2). This release includes a +bunch of bugfixes and a few enhancements, see below. + +## Brought to You By + +Bob, +chantra, +Chris O'Haver, +Frank Riley, +George Shammas, +Johnny Bergström, +Jun Chen, +Lars Ekman, +Manuel Rüger, +Maxime Ginters, +Miek Gieben, +slick-nic, +TimYin. + +## Noteworthy Changes + +* core: Also clear `do` and `size` (https://github.com/coredns/coredns/pull/4465) +* core: Flag blacklisting not needed anymore (https://github.com/coredns/coredns/pull/4420) +* core: Set http request in writer (https://github.com/coredns/coredns/pull/4445) +* Makefile.release: Replace manifest-tool with docker manifest (https://github.com/coredns/coredns/pull/4421) +* plugin/acl: add the ability to filter records (https://github.com/coredns/coredns/pull/4389) +* plugin/dnstap: Fix out of order messages and fix forward perspective. (https://github.com/coredns/coredns/pull/4395) +* plugin/forward Add rcode and rtype to request_duration_seconds metric (https://github.com/coredns/coredns/pull/4391) +* plugin/kubernetes: Corrected detection of K8s minor version (https://github.com/coredns/coredns/pull/4430) +* plugin/kubernetes: make kubeconfig argument 'context' optional (https://github.com/coredns/coredns/pull/4451) +* plugin/rewrite: copy msg before rewriting (https://github.com/coredns/coredns/pull/4443) +* plugin/rewrite: SRV targets and additional names in response (https://github.com/coredns/coredns/pull/4287) +* plugin/sign: track zone file's mtime (https://github.com/coredns/coredns/pull/4431) +* plugin/trace: Use compatible tag name for datadog (https://github.com/coredns/coredns/pull/4408) +* plugin/transfer: only allow outgoing axfr over tcp (https://github.com/coredns/coredns/pull/4452) diff --git a/ag_201_coredns/notes/coredns-1.8.3.md b/ag_201_coredns/notes/coredns-1.8.3.md new file mode 100644 index 0000000..ec84267 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.8.3.md @@ -0,0 +1,49 @@ ++++ +title = "CoreDNS-1.8.3 Release" +description = "CoreDNS-1.8.3 Release Notes." +tags = ["Release", "1.8.3", "Notes"] +release = "1.8.3" +date = 2021-02-24T07:00:00+00:00 +author = "coredns" ++++ + +The CoreDNS team has released +[CoreDNS-1.8.3](https://github.com/coredns/coredns/releases/tag/v1.8.3). This release includes a +bunch of bugfixes and a few enhancements, see below. + +In case you're wondering, 1.8.2 didn't properly upload and tag the docker images, hence a quick +followup release with that fixed. + +## Brought to You By + +Bob, +chantra, +Chris O'Haver, +Frank Riley, +George Shammas, +Johnny Bergström, +Jun Chen, +Lars Ekman, +Manuel Rüger, +Maxime Ginters, +Miek Gieben, +slick-nic, +TimYin. + +## Noteworthy Changes + +* core: Also clear `do` and `size` (https://github.com/coredns/coredns/pull/4465) +* core: Flag blacklisting not needed anymore (https://github.com/coredns/coredns/pull/4420) +* core: Set http request in writer (https://github.com/coredns/coredns/pull/4445) +* Makefile.release: Replace manifest-tool with docker manifest (https://github.com/coredns/coredns/pull/4421) +* Makefile.release: Fix the Makefile (https://github.com/coredns/coredns/pull/4483) +* plugin/acl: add the ability to filter records (https://github.com/coredns/coredns/pull/4389) +* plugin/dnstap: Fix out of order messages and fix forward perspective. (https://github.com/coredns/coredns/pull/4395) +* plugin/forward Add rcode and rtype to request_duration_seconds metric (https://github.com/coredns/coredns/pull/4391) +* plugin/kubernetes: Corrected detection of K8s minor version (https://github.com/coredns/coredns/pull/4430) +* plugin/kubernetes: make kubeconfig argument 'context' optional (https://github.com/coredns/coredns/pull/4451) +* plugin/rewrite: copy msg before rewriting (https://github.com/coredns/coredns/pull/4443) +* plugin/rewrite: SRV targets and additional names in response (https://github.com/coredns/coredns/pull/4287) +* plugin/sign: track zone file's mtime (https://github.com/coredns/coredns/pull/4431) +* plugin/trace: Use compatible tag name for datadog (https://github.com/coredns/coredns/pull/4408) +* plugin/transfer: only allow outgoing axfr over tcp (https://github.com/coredns/coredns/pull/4452) diff --git a/ag_201_coredns/notes/coredns-1.8.4.md b/ag_201_coredns/notes/coredns-1.8.4.md new file mode 100644 index 0000000..462f840 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/notes/coredns-1.8.5.md b/ag_201_coredns/notes/coredns-1.8.5.md new file mode 100644 index 0000000..03f2e75 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/notes/coredns-1.8.6.md b/ag_201_coredns/notes/coredns-1.8.6.md new file mode 100644 index 0000000..aaf6f59 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/notes/coredns-1.8.7.md b/ag_201_coredns/notes/coredns-1.8.7.md new file mode 100644 index 0000000..f9e99ed --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/notes/coredns-1.9.0.md b/ag_201_coredns/notes/coredns-1.9.0.md new file mode 100644 index 0000000..4fb023a --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/notes/coredns-1.9.1.md b/ag_201_coredns/notes/coredns-1.9.1.md new file mode 100644 index 0000000..7912857 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/notes/coredns-1.9.2.md b/ag_201_coredns/notes/coredns-1.9.2.md new file mode 100644 index 0000000..3feba4b --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/notes/coredns-1.9.3.md b/ag_201_coredns/notes/coredns-1.9.3.md new file mode 100644 index 0000000..2601532 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/notes/coredns-1.9.4.md b/ag_201_coredns/notes/coredns-1.9.4.md new file mode 100644 index 0000000..b9b7f52 --- /dev/null +++ b/ag_201_coredns/notes/coredns-1.9.4.md @@ -0,0 +1,59 @@ ++++ +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 O'Haver +Christoph Heer +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/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/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/ag_201_coredns/owners_generate.go b/ag_201_coredns/owners_generate.go new file mode 100644 index 0000000..ebae010 --- /dev/null +++ b/ag_201_coredns/owners_generate.go @@ -0,0 +1,89 @@ +//go:build ignore + +// generates plugin/chaos/zowners.go. + +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "sort" + "strings" +) + +func main() { + // top-level OWNERS file + o, err := owners("CODEOWNERS") + if err != nil { + log.Fatal(err) + } + + golist := `package chaos + +// Owners are all GitHub handlers of all maintainers. +var Owners = []string{` + c := ", " + for i, a := range o { + if i == len(o)-1 { + c = "}" + } + golist += fmt.Sprintf("%q%s", a, c) + } + // to prevent `No newline at end of file` with gofmt + golist += "\n" + + if err := os.WriteFile("plugin/chaos/zowners.go", []byte(golist), 0644); err != nil { + log.Fatal(err) + } + return +} + +func owners(path string) ([]string, error) { + // simple line, by line based format + // + // # In this example, @doctocat owns any files in the build/logs + // # directory at the root of the repository and any of its + // # subdirectories. + // /build/logs/ @doctocat + f, err := os.Open(path) + if err != nil { + return nil, err + } + scanner := bufio.NewScanner(f) + users := map[string]struct{}{} + for scanner.Scan() { + text := scanner.Text() + if len(text) == 0 { + continue + } + if text[0] == '#' { + continue + } + ele := strings.Fields(text) + if len(ele) == 0 { + continue + } + + // ok ele[0] is the path, the rest are (in our case) github usernames prefixed with @ + for _, s := range ele[1:] { + if len(s) <= 1 { + continue + } + users[s[1:]] = struct{}{} + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + u := []string{} + for k := range users { + if strings.HasPrefix(k, "@") { + k = k[1:] + } + u = append(u, k) + } + sort.Strings(u) + return u, nil +} diff --git a/ag_201_coredns/pb/Makefile b/ag_201_coredns/pb/Makefile new file mode 100644 index 0000000..7e8cdaf --- /dev/null +++ b/ag_201_coredns/pb/Makefile @@ -0,0 +1,20 @@ +# Generate the Go files from the dns.proto protobuf, you need the utilities +# 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=. --go-grpc_out=. dns.proto + +.PHONY: clean +clean: + rm dns.pb.go diff --git a/ag_201_coredns/pb/dns.pb.go b/ag_201_coredns/pb/dns.pb.go new file mode 100644 index 0000000..e2a311c --- /dev/null +++ b/ag_201_coredns/pb/dns.pb.go @@ -0,0 +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 ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +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 { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Msg []byte `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` +} + +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 (x *DnsPacket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DnsPacket) ProtoMessage() {} + +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 mi.MessageOf(x) +} + +// Deprecated: Use DnsPacket.ProtoReflect.Descriptor instead. +func (*DnsPacket) Descriptor() ([]byte, []int) { + return file_dns_proto_rawDescGZIP(), []int{0} +} + +func (x *DnsPacket) GetMsg() []byte { + if x != nil { + return x.Msg + } + return nil +} + +var File_dns_proto protoreflect.FileDescriptor + +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, +} + +var ( + file_dns_proto_rawDescOnce sync.Once + file_dns_proto_rawDescData = file_dns_proto_rawDesc +) + +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 +} + +var file_dns_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_dns_proto_goTypes = []interface{}{ + (*DnsPacket)(nil), // 0: coredns.dns.DnsPacket +} +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 init() { file_dns_proto_init() } +func file_dns_proto_init() { + if File_dns_proto != nil { + return + } + 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 + } + } + } + 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, + }, + 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/ag_201_coredns/pb/dns.proto b/ag_201_coredns/pb/dns.proto new file mode 100644 index 0000000..ee24cb0 --- /dev/null +++ b/ag_201_coredns/pb/dns.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package coredns.dns; +option go_package = ".;pb"; + +message DnsPacket { + bytes msg = 1; +} + +service DnsService { + rpc Query (DnsPacket) returns (DnsPacket); +} diff --git a/ag_201_coredns/pb/dns_grpc.pb.go b/ag_201_coredns/pb/dns_grpc.pb.go new file mode 100644 index 0000000..6ff3faf --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/pkcs8.pem b/ag_201_coredns/pkcs8.pem new file mode 100644 index 0000000..74d1fd9 --- /dev/null +++ b/ag_201_coredns/pkcs8.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDbQkmFOXTGxHcr +DVFvGN8UaPI4MU9Rw7nylrx0+cb9hPEXraKh/pbrQGVnHAxs+cNZ3Zq73syrFeCV +TwaC5UHEyUj58WZtKYJnARN2L2gdw8ZVUloagZBF2Milmo/J4UYN4+0l9ydUSXkc +ZtyotqRYPLF+ERwHBipAB4vjiqdnYKzqg4kIHf+9wYv6RiMUjFGaWg9nAT/a1cBm +R5gJ9zbQBs3SsgNH2QagQbuqJB3TQEetC3ggrwYlt0UwSnMFSioNRPJVE/NJvSiS +6JLh8oDMS7gYrpBsUyeLYrWkEcy5944rUUadEbi1aMUWTueRzPAJECO9ApBRtlaB +9I8E6STHAgMBAAECggEAXtchbhsYRBIfaePs4Z1kgTOT8nKB2OBMwn4pLoAMbwTe +NFvLCT9SkJfeROeBCHJQ6cJNeA47gZWI+4yfSGdaN4DJrDObFoTp/xwcCvceliFk +3OsuRmDcwxmuUNO2dqCW6NM6dT2fKXpOhNaJhADcvb4jGGDWOsOT6vZYsCf9mV0O +6dTuHTVr5EZn2aHR5H5zpFIm2o57pL88unbDL9sMF5zxd8V/eAKHn8EGXsJcf/ke +0qhVA/Edsq2mZ4Tn+ufB61x6+CQdG4Ha3MpO4I30AhujnIrFlpzI66yNJl9epWQa +B52nXuUs1I1OimNcKFk95UAemzbTllc8cUv3EEUOAQKBgQDyn9hPORSIfER9wliI +tloxoaTOgn6DXiqqBmOFFJMZ6+Hf+VTjuiPoSytBq1Y7EWC7g9J5+ih2nX9kEB3U +OdlFeUjDehIAMe8DUumjSFvg5GDb2ETFo9rIjz2/1qbIledCL6CKChTKny/gTVHa +FfKPbKh+QEWixFaHxZoIieOnhwKBgQDnWK+Pnc5lSaGlf8K623cnWgF6t+Mp1jCe +br3p75dfAeZa7rizWmVBRz89WP7WopzXFL+ySnLAMOFItuItJ7M+D7gSNGtFfeWR +Tfb0tBhDXA3rdSFraEEuG2nEG4vXoZCALsU6KsUNEHWfr4FGKaAp72Voe380mDi7 +6UALVj9owQKBgHs9ODmdi9F8FaovcpsWzR+StMz0ueBuj4tHrXtmpQWwrE4BLt5x +qVWQAQvdRPLB8/mrjtjIeY8ulwuzqAMp0zQX1B7+vW/97SMqFWUMFydXud9RrjUZ +8sKXB82O8a7Hfe0/lfMoBcGaNE5h1+bH6SpzDe77JKP1yOI0O/aEW++3AoGBANC1 +XYz/HNib2Mzpuc/Bdnl15afEhZeUuD/2QDbwA2ue1yZp/w8vGfIOSKsbwqv3/+65 +tUcvit6AOn3TH7EFA9uVasZhq/UBYz33TDfu0YTTY2tsPD4dy8/Aw2Y8Q6jBBQ/f +Iecb9rGWi8cIGmQl4WxzoNTltRjJy0UbZL/Vi1cBAoGASef+WZCi0EzVcWmfZJSG +PbfIVSS5F0byHA304CmHqy38KfaNl/xrMmg0BCmgeQ5XR6FkntAp5yUfnLuRSaf+ +dHwnTfsmTtB+tcuZ6NnXJDEsVJ0IWaWzZq5b4osrBUaP7kRFjidbRd2FOuYtk9Ub +aISkOXEZdiCgSsgn5rps9q0= +-----END PRIVATE KEY----- diff --git a/ag_201_coredns/plugin.cfg b/ag_201_coredns/plugin.cfg new file mode 100644 index 0000000..542489f --- /dev/null +++ b/ag_201_coredns/plugin.cfg @@ -0,0 +1,73 @@ +# Directives are registered in the order they should be executed. +# +# Ordering is VERY important. Every plugin will feel the effects of all other +# plugin below (after) them during a request, but they must not care what plugin +# above them are doing. + +# How to rebuild with updated plugin configurations: Modify the list below and +# run `go generate && go build` + +# The parser takes the input format of: +# +# : +# Or +# : +# +# External plugin example: +# +# log:github.com/coredns/coredns/plugin/log +# Local plugin example: +# log:log + +metadata:metadata +geoip:geoip +cancel:cancel +tls:tls +reload:reload +nsid:nsid +bufsize:bufsize +root:root +bind:bind +debug:debug +trace:trace +ready:ready +health:health +pprof:pprof +prometheus:metrics +errors:errors +log:log +dnstap:dnstap +local:local +dns64:dns64 +acl:acl +any:any +chaos:chaos +loadbalance:loadbalance +tsig:tsig +cache:cache +rewrite:rewrite +header:header +dnssec:dnssec +autopath:autopath +minimal:minimal +template:template +transfer:transfer +hosts:hosts +route53:route53 +azure:azure +clouddns:clouddns +k8s_external:k8s_external +kubernetes:kubernetes +file:file +auto:auto +secondary:secondary +etcd:etcd +loop:loop +forward:forward +grpc:grpc +erratic:erratic +whoami:whoami +on:github.com/coredns/caddy/onevent +sign:sign +view:view +dnsovertor:dnsovertor diff --git a/ag_201_coredns/plugin.md b/ag_201_coredns/plugin.md new file mode 100644 index 0000000..1973729 --- /dev/null +++ b/ag_201_coredns/plugin.md @@ -0,0 +1,164 @@ +# Plugins + +## Writing Plugins + +The main method that gets called is `ServeDNS`. It has three parameters: + +* a `context.Context`; +* `dns.ResponseWriter` that is, basically, the client's connection; +* `*dns.Msg` the request from the client. + +`ServeDNS` returns two values, a response code and an error. If the error is not nil, CoreDNS +will return a SERVFAIL to the client. The response code tells CoreDNS if a *reply has been +written by the plugin chain or not*. In the latter case CoreDNS will take care of that. + +CoreDNS treats: + +* SERVFAIL (dns.RcodeServerFailure) +* REFUSED (dns.RcodeRefused) +* FORMERR (dns.RcodeFormatError) +* NOTIMP (dns.RcodeNotImplemented) + +as special and will then assume *nothing* has been written to the client. In all other cases it +assumes something has been written to the client (by the plugin). + +The [*example*](https://github.com/coredns/example) plugin shows a bare-bones implementation that +can be used as a starting point for your plugin. This plugin has tests and extensive comments in the +code. + +## Hooking It Up + +See a couple of blog posts on how to write and add plugin to CoreDNS: + +* +* , slightly older, but useful. + +## Logging + +If your plugin needs to output a log line you should use the `plugin/pkg/log` package. This package +implements log levels. The standard way of outputting is: `log.Info` for info level messages. The +levels available are `log.Info`, `log.Warning`, `log.Error`, `log.Debug`. Each of these also has +a `f` variant. The plugin's name should be included, by using the log package like so: + +~~~ go +import clog "github.com/coredns/coredns/plugin/pkg/log" + +var log = clog.NewWithPlugin("whoami") + +log.Info("message") // outputs: [INFO] plugin/whoami: message +~~~ + +In general, logging should be left to the higher layers by returning an error. However, if there is +a reason to consume the error and notify the user, then logging in the plugin itself can be +acceptable. The `Debug*` functions only output something when the *debug* plugin is loaded in the +server. + +## Metrics + +When exporting metrics the *Namespace* should be `plugin.Namespace` (="coredns"), and the +*Subsystem* must be the name of the plugin. The README.md for the plugin should then also contain +a *Metrics* section detailing the metrics. + +## Readiness + +If the plugin supports signalling readiness it should have a *Ready* section detailing how it +works, and implement the `ready.Readiness` interface. + +## Opening Sockets + +See the plugin/pkg/reuseport for `Listen` and `ListenPacket` functions. Using these functions makes +your plugin handle reload events better. + +## Context + +Every request get a context.Context these are pre-filled with 2 values: + +* `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. + +## Documentation + +Each plugin should have a README.md explaining what the plugin does and how it is configured. The +file should have the following layout: + +* Title: use the plugin's name +* Subsection titled: "Name" + with *PLUGIN* - one line description. +* Subsection titled: "Description" has a longer description. +* Subsection titled: "Syntax", syntax and supported directives. +* Subsection titled: "Examples" + +More sections are of course possible. + +### Style + +We use the Unix manual page style: + +* The name of plugin in the running text should be italic: *plugin*. +* all CAPITAL: user supplied argument, in the running text references this use strong text: `**`: + **EXAMPLE**. +* Optional text: in block quotes: `[optional]`. +* Use three dots to indicate multiple options are allowed: `arg...`. +* Item used literal: `literal`. + +### Example Domain Names + +Please be sure to use `example.org` or `example.net` in any examples and tests you provide. These +are the standard domain names created for this purpose. + +## Fallthrough + +In a perfect world the following would be true for plugin: "Either you are responsible for a zone or +not". If the answer is "not", the plugin should call the next plugin in the chain. If "yes" it +should handle *all* names that fall in this zone and the names below - i.e. it should handle the +entire domain and all sub domains. + +~~~ txt +. { + file example.org db.example +} +~~~ + +In this example the *file* plugin is handling all names below (and including) `example.org`. If +a query comes in that is not a subdomain (or equal to) `example.org` the next plugin is called. + +Now, the world isn't perfect, and there are may be reasons to "fallthrough" to the next plugin, +meaning a plugin is only responsible for a *subset* of names within the zone. + +The `fallthrough` directive should optionally accept a list of zones. Only queries for records +in one of those zones should be allowed to fallthrough. See `plugin/pkg/fallthrough` for the +implementation. + +## Mutating a Response + +Using a custom `ResponseWriter`, a plugin can mutate a response when another plugin further down the chain writes the response to the client. +If a plugin mutates a response it MUST make a copy of the entire response before doing so. A +response is a pointer to a `dns.Msg` and as such you will be manipulating the original response, +which could have been generated from a data store. E.g. the *file* plugin creates a response that +the *rewrite* plugin then rewrites; not copying the data, means it's **also** mutating the data of +the *file*'s data store. A response can be copied by using the `Copy()` method. + +## General Guidelines + +Some general guidelines: + +* logging time duration should be done in seconds (call the `Seconds()` method on any duration). +* keep logging to a minimum. +* call the main config parse function just `parse`. +* try to minimize the number of knobs in the configuration. +* use `plugin.Error()` to wrap errors returned from the `setup` function. + +## Qualifying for Main Repo + +Plugins for CoreDNS can live out-of-tree, `plugin.cfg` defaults to CoreDNS' repo but other +repos work just as well. So when do we consider the inclusion of a new plugin in the main repo? + +* First, the plugin should be useful for other people. "Useful" is a subjective term. We will + probably need to further refine this. +* It should be sufficiently different from other plugin to warrant inclusion. +* Current internet standards need be supported: IPv4 and IPv6, so A and AAAA records should be + handled (if your plugin is in the business of dealing with address records that is). +* It must have tests. +* It must have a README.md for documentation. diff --git a/ag_201_coredns/plugin/acl/README.md b/ag_201_coredns/plugin/acl/README.md new file mode 100644 index 0000000..6e5b827 --- /dev/null +++ b/ag_201_coredns/plugin/acl/README.md @@ -0,0 +1,98 @@ +# acl + +## Name + +*acl* - enforces access control policies on source ip and prevents unauthorized access to DNS servers. + +## 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 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. + +## Syntax + +``` +acl [ZONES...] { + ACTION [type QTYPE...] [net SOURCE...] +} +``` + +- **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* +- **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. + +## Examples + +To demonstrate the usage of plugin acl, here we provide some typical examples. + +Block all DNS queries with record type A from 192.168.0.0/16: + +~~~ corefile +. { + acl { + block type A net 192.168.0.0/16 + } +} +~~~ + +Filter all DNS queries with record type A from 192.168.0.0/16: + +~~~ corefile +. { + acl { + filter type A net 192.168.0.0/16 + } +} +~~~ + +Block all DNS queries from 192.168.0.0/16 except for 192.168.1.0/24: + +~~~ corefile +. { + acl { + allow net 192.168.1.0/24 + block net 192.168.0.0/16 + } +} +~~~ + +Allow only DNS queries from 192.168.0.0/24 and 192.168.1.0/24: + +~~~ corefile +. { + acl { + allow net 192.168.0.0/24 192.168.1.0/24 + block + } +} +~~~ + +Block all DNS queries from 192.168.1.0/24 towards a.example.org: + +~~~ corefile +example.org { + acl a.example.org { + block net 192.168.1.0/24 + } +} +~~~ + +## Metrics + +If monitoring is enabled (via the _prometheus_ plugin) then the following metrics are exported: + +- `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. + +The `server` and `zone` labels are explained in the _metrics_ plugin documentation. diff --git a/ag_201_coredns/plugin/acl/acl.go b/ag_201_coredns/plugin/acl/acl.go new file mode 100644 index 0000000..7d7b9d6 --- /dev/null +++ b/ag_201_coredns/plugin/acl/acl.go @@ -0,0 +1,144 @@ +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" + "github.com/miekg/dns" +) + +// ACL enforces access control policies on DNS queries. +type ACL struct { + Next plugin.Handler + + Rules []rule +} + +// rule defines a list of Zones and some ACL policies which will be +// enforced on them. +type rule struct { + zones []string + policies []policy +} + +// action defines the action against queries. +type action int + +// policy defines the ACL policy for DNS queries. +// A policy performs the specified action (block/allow) on all DNS queries +// matched by source IP or QTYPE. +type policy struct { + action action + qtypes map[uint16]struct{} + filter *iptree.Tree +} + +const ( + // actionNone does nothing on the queries. + actionNone = iota + // actionAllow allows authorized queries to recurse. + actionAllow + // actionBlock blocks unauthorized queries towards protected DNS zones. + actionBlock + // actionFilter returns empty sets for queries towards protected DNS zones. + actionFilter +) + +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} + +RulesCheckLoop: + for _, rule := range a.Rules { + // check zone. + zone := plugin.Zones(rule.zones).Matches(state.Name()) + if zone == "" { + continue + } + + action := matchWithPolicies(rule.policies, w, r) + switch action { + case actionBlock: + { + 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, metrics.WithView(ctx)).Inc() + return dns.RcodeSuccess, nil + } + case actionAllow: + { + break RulesCheckLoop + } + case actionFilter: + { + 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, metrics.WithView(ctx)).Inc() + return dns.RcodeSuccess, nil + } + } + } + + RequestAllowCount.WithLabelValues(metrics.WithServer(ctx), metrics.WithView(ctx)).Inc() + return plugin.NextOrFailure(state.Name(), a.Next, ctx, w, r) +} + +// matchWithPolicies matches the DNS query with a list of ACL polices and returns suitable +// action against the query. +func matchWithPolicies(policies []policy, w dns.ResponseWriter, r *dns.Msg) action { + state := request.Request{W: w, Req: r} + + 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. + _, matchAll := policy.qtypes[dns.TypeNone] + _, match := policy.qtypes[qtype] + if !matchAll && !match { + continue + } + + _, contained := policy.filter.GetByIP(ip) + if !contained { + continue + } + + // matched. + return policy.action + } + return actionNone +} + +// Name implements the plugin.Handler interface. +func (a ACL) Name() string { + return "acl" +} diff --git a/ag_201_coredns/plugin/acl/acl_test.go b/ag_201_coredns/plugin/acl/acl_test.go new file mode 100644 index 0000000..d947879 --- /dev/null +++ b/ag_201_coredns/plugin/acl/acl_test.go @@ -0,0 +1,449 @@ +package acl + +import ( + "context" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +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 +} + +func NewTestControllerWithZones(input string, zones []string) *caddy.Controller { + ctr := caddy.NewTestController("dns", input) + ctr.ServerBlockKeys = append(ctr.ServerBlockKeys, zones...) + return ctr +} + +func TestACLServeDNS(t *testing.T) { + type args struct { + domain string + sourceIP string + qtype uint16 + } + tests := []struct { + name string + config string + zones []string + args args + wantRcode int + wantErr bool + wantExtendedErrorCode uint16 + }{ + // IPv4 tests. + { + name: "Blacklist 1 BLOCKED", + config: `acl example.org { + block type A net 192.168.0.0/16 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.0.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Blacklist 1 ALLOWED", + config: `acl example.org { + block type A net 192.168.0.0/16 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.167.0.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + }, + { + name: "Blacklist 2 BLOCKED", + config: ` + acl example.org { + block type * net 192.168.0.0/16 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.0.2", + qtype: dns.TypeAAAA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Blacklist 3 BLOCKED", + config: `acl example.org { + block type A + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "10.1.0.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Blacklist 3 ALLOWED", + config: `acl example.org { + block type A + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "10.1.0.2", + qtype: dns.TypeAAAA, + }, + wantRcode: dns.RcodeSuccess, + }, + { + name: "Blacklist 4 Single IP BLOCKED", + config: `acl example.org { + block type A net 192.168.1.2 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Blacklist 4 Single IP ALLOWED", + config: `acl example.org { + block type A net 192.168.1.2 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.1.3", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + }, + { + name: "Filter 1 FILTERED", + config: `acl example.org { + filter type A 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, + wantExtendedErrorCode: dns.ExtendedErrorCodeFiltered, + }, + { + name: "Filter 1 ALLOWED", + config: `acl example.org { + filter type A net 192.168.0.0/16 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.167.0.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + }, + { + name: "Whitelist 1 ALLOWED", + config: `acl example.org { + allow net 192.168.0.0/16 + block + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.0.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + }, + { + name: "Whitelist 1 REFUSED", + config: `acl example.org { + allow type * net 192.168.0.0/16 + block + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "10.1.0.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Fine-Grained 1 REFUSED", + config: `acl a.example.org { + block type * net 192.168.1.0/24 + }`, + zones: []string{"example.org"}, + args: args{ + domain: "a.example.org.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Fine-Grained 1 ALLOWED", + config: `acl a.example.org { + block net 192.168.1.0/24 + }`, + zones: []string{"example.org"}, + args: args{ + domain: "www.example.org.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + }, + { + name: "Fine-Grained 2 REFUSED", + config: `acl example.org { + block net 192.168.1.0/24 + }`, + zones: []string{"example.org"}, + args: args{ + domain: "a.example.org.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Fine-Grained 2 ALLOWED", + config: `acl { + block net 192.168.1.0/24 + }`, + zones: []string{"example.org"}, + args: args{ + domain: "a.example.com.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + }, + { + 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 + }`, + zones: []string{"example.org"}, + args: args{ + domain: "b.example.org.", + sourceIP: "192.168.2.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + 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 + }`, + zones: []string{"example.org"}, + args: args{ + domain: "b.example.org.", + sourceIP: "192.168.1.2", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + }, + // IPv6 tests. + { + name: "Blacklist 1 BLOCKED IPv6", + config: `acl example.org { + block type A net 2001:db8:abcd:0012::0/64 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:db8:abcd:0012::1230", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Blacklist 1 ALLOWED IPv6", + config: `acl example.org { + block type A net 2001:db8:abcd:0012::0/64 + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:db8:abcd:0013::0", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + }, + { + name: "Blacklist 2 BLOCKED IPv6", + config: `acl example.org { + block type A + }`, + zones: []string{}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Blacklist 3 Single IP BLOCKED IPv6", + config: `acl example.org { + block type A 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.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Blacklist 3 Single IP ALLOWED IPv6", + config: `acl example.org { + block type A 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:7335", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeSuccess, + }, + { + name: "Fine-Grained 1 REFUSED IPv6", + config: `acl a.example.org { + block type * net 2001:db8:abcd:0012::0/64 + }`, + zones: []string{"example.org"}, + args: args{ + domain: "a.example.org.", + sourceIP: "2001:db8:abcd:0012:2019::0", + qtype: dns.TypeA, + }, + wantRcode: dns.RcodeRefused, + wantExtendedErrorCode: dns.ExtendedErrorCodeBlocked, + }, + { + name: "Fine-Grained 1 ALLOWED IPv6", + config: `acl a.example.org { + block net 2001:db8:abcd:0012::0/64 + }`, + zones: []string{"example.org"}, + args: args{ + domain: "www.example.org.", + sourceIP: "2001:db8:abcd:0012:2019::0", + qtype: dns.TypeA, + }, + 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, + }, + } + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctr := NewTestControllerWithZones(tt.config, tt.zones) + a, err := parse(ctr) + a.Next = test.NextHandler(dns.RcodeSuccess, nil) + if err != nil { + t.Errorf("Error: Cannot parse acl from config: %v", err) + return + } + + 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 { + t.Errorf("Error: acl.ServeDNS() error = %v, wantErr %v", err, tt.wantErr) + return + } + if w.Rcode != tt.wantRcode { + t.Errorf("Error: acl.ServeDNS() Rcode = %v, want %v", w.Rcode, tt.wantRcode) + } + 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/ag_201_coredns/plugin/acl/metrics.go b/ag_201_coredns/plugin/acl/metrics.go new file mode 100644 index 0000000..04d728b --- /dev/null +++ b/ag_201_coredns/plugin/acl/metrics.go @@ -0,0 +1,32 @@ +package acl + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // RequestBlockCount is the number of DNS requests being blocked. + RequestBlockCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: pluginName, + Name: "blocked_requests_total", + Help: "Counter of DNS requests being blocked.", + }, []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", "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", "view"}) +) diff --git a/ag_201_coredns/plugin/acl/setup.go b/ag_201_coredns/plugin/acl/setup.go new file mode 100644 index 0000000..3adde0a --- /dev/null +++ b/ag_201_coredns/plugin/acl/setup.go @@ -0,0 +1,152 @@ +package acl + +import ( + "net" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + + "github.com/infobloxopen/go-trees/iptree" + "github.com/miekg/dns" +) + +const pluginName = "acl" + +func init() { plugin.Register(pluginName, setup) } + +func newDefaultFilter() *iptree.Tree { + defaultFilter := iptree.NewTree() + _, IPv4All, _ := net.ParseCIDR("0.0.0.0/0") + _, IPv6All, _ := net.ParseCIDR("::/0") + defaultFilter.InplaceInsertNet(IPv4All, struct{}{}) + defaultFilter.InplaceInsertNet(IPv6All, struct{}{}) + return defaultFilter +} + +func setup(c *caddy.Controller) error { + a, err := parse(c) + if err != nil { + return plugin.Error(pluginName, err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + a.Next = next + return a + }) + + return nil +} + +func parse(c *caddy.Controller) (ACL, error) { + a := ACL{} + for c.Next() { + r := rule{} + args := c.RemainingArgs() + r.zones = plugin.OriginsFromArgsOrServerBlock(args, c.ServerBlockKeys) + + for c.NextBlock() { + p := policy{} + + action := strings.ToLower(c.Val()) + if action == "allow" { + p.action = actionAllow + } else if action == "block" { + p.action = actionBlock + } else if action == "filter" { + p.action = actionFilter + } else { + return a, c.Errf("unexpected token %q; expect 'allow', 'block', or 'filter'", c.Val()) + } + + p.qtypes = make(map[uint16]struct{}) + p.filter = iptree.NewTree() + + hasTypeSection := false + hasNetSection := false + + remainingTokens := c.RemainingArgs() + for len(remainingTokens) > 0 { + if !isPreservedIdentifier(remainingTokens[0]) { + return a, c.Errf("unexpected token %q; expect 'type | net'", remainingTokens[0]) + } + section := strings.ToLower(remainingTokens[0]) + + i := 1 + var tokens []string + for ; i < len(remainingTokens) && !isPreservedIdentifier(remainingTokens[i]); i++ { + tokens = append(tokens, remainingTokens[i]) + } + remainingTokens = remainingTokens[i:] + + if len(tokens) == 0 { + return a, c.Errf("no token specified in %q section", section) + } + + switch section { + case "type": + hasTypeSection = true + for _, token := range tokens { + if token == "*" { + p.qtypes[dns.TypeNone] = struct{}{} + break + } + qtype, ok := dns.StringToType[token] + if !ok { + return a, c.Errf("unexpected token %q; expect legal QTYPE", token) + } + p.qtypes[qtype] = struct{}{} + } + case "net": + hasNetSection = true + for _, token := range tokens { + if token == "*" { + p.filter = newDefaultFilter() + break + } + token = normalize(token) + _, source, err := net.ParseCIDR(token) + if err != nil { + return a, c.Errf("illegal CIDR notation %q", token) + } + p.filter.InplaceInsertNet(source, struct{}{}) + } + default: + return a, c.Errf("unexpected token %q; expect 'type | net'", section) + } + } + + // optional `type` section means all record types. + if !hasTypeSection { + p.qtypes[dns.TypeNone] = struct{}{} + } + + // optional `net` means all ip addresses. + if !hasNetSection { + p.filter = newDefaultFilter() + } + + r.policies = append(r.policies, p) + } + a.Rules = append(a.Rules, r) + } + return a, nil +} + +func isPreservedIdentifier(token string) bool { + identifier := strings.ToLower(token) + return identifier == "type" || identifier == "net" +} + +// normalize appends '/32' for any single IPv4 address and '/128' for IPv6. +func normalize(rawNet string) string { + if idx := strings.IndexAny(rawNet, "/"); idx >= 0 { + return rawNet + } + + if idx := strings.IndexAny(rawNet, ":"); idx >= 0 { + return rawNet + "/128" + } + return rawNet + "/32" +} diff --git a/ag_201_coredns/plugin/acl/setup_test.go b/ag_201_coredns/plugin/acl/setup_test.go new file mode 100644 index 0000000..1d25dd7 --- /dev/null +++ b/ag_201_coredns/plugin/acl/setup_test.go @@ -0,0 +1,259 @@ +package acl + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + tests := []struct { + name string + config string + wantErr bool + }{ + // IPv4 tests. + { + "Blacklist 1", + `acl { + block type A net 192.168.0.0/16 + }`, + false, + }, + { + "Blacklist 2", + `acl { + block type * net 192.168.0.0/16 + }`, + false, + }, + { + "Blacklist 3", + `acl { + block type A net * + }`, + false, + }, + { + "Blacklist 4", + `acl { + allow type * net 192.168.1.0/24 + block type * net 192.168.0.0/16 + }`, + false, + }, + { + "Filter 1", + `acl { + filter type A net 192.168.0.0/16 + }`, + false, + }, + { + "Whitelist 1", + `acl { + allow type * net 192.168.0.0/16 + block type * net * + }`, + false, + }, + { + "fine-grained 1", + `acl a.example.org { + block type * net 192.168.1.0/24 + }`, + false, + }, + { + "fine-grained 2", + `acl a.example.org { + block type * net 192.168.1.0/24 + } + acl b.example.org { + block type * net 192.168.2.0/24 + }`, + false, + }, + { + "Multiple Networks 1", + `acl example.org { + block type * net 192.168.1.0/24 192.168.3.0/24 + }`, + false, + }, + { + "Multiple Qtypes 1", + `acl example.org { + block type TXT ANY CNAME net 192.168.3.0/24 + }`, + false, + }, + { + "Missing argument 1", + `acl { + block A net 192.168.0.0/16 + }`, + true, + }, + { + "Missing argument 2", + `acl { + block type net 192.168.0.0/16 + }`, + true, + }, + { + "Illegal argument 1", + `acl { + block type ABC net 192.168.0.0/16 + }`, + true, + }, + { + "Illegal argument 2", + `acl { + blck type A net 192.168.0.0/16 + }`, + true, + }, + { + "Illegal argument 3", + `acl { + block type A net 192.168.0/16 + }`, + true, + }, + { + "Illegal argument 4", + `acl { + block type A net 192.168.0.0/33 + }`, + true, + }, + // IPv6 tests. + { + "Blacklist 1 IPv6", + `acl { + block type A net 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + }`, + false, + }, + { + "Blacklist 2 IPv6", + `acl { + block type * net 2001:db8:85a3::8a2e:370:7334 + }`, + false, + }, + { + "Blacklist 3 IPv6", + `acl { + block type A + }`, + false, + }, + { + "Blacklist 4 IPv6", + `acl { + allow net 2001:db8:abcd:0012::0/64 + block net 2001:db8:abcd:0012::0/48 + }`, + false, + }, + { + "Filter 1 IPv6", + `acl { + filter type A net 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + }`, + false, + }, + { + "Whitelist 1 IPv6", + `acl { + allow net 2001:db8:abcd:0012::0/64 + block + }`, + false, + }, + { + "fine-grained 1 IPv6", + `acl a.example.org { + block net 2001:db8:abcd:0012::0/64 + }`, + false, + }, + { + "fine-grained 2 IPv6", + `acl a.example.org { + block net 2001:db8:abcd:0012::0/64 + } + acl b.example.org { + block net 2001:db8:abcd:0013::0/64 + }`, + false, + }, + { + "Multiple Networks 1 IPv6", + `acl example.org { + block net 2001:db8:abcd:0012::0/64 2001:db8:85a3::8a2e:370:7334/64 + }`, + false, + }, + { + "Illegal argument 1 IPv6", + `acl { + block type A net 2001::85a3::8a2e:370:7334 + }`, + true, + }, + { + "Illegal argument 2 IPv6", + `acl { + block type A net 2001:db8:85a3:::8a2e:370:7334 + }`, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + 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) + } + }) + } +} + +func TestNormalize(t *testing.T) { + type args struct { + rawNet string + } + tests := []struct { + name string + args args + want string + }{ + { + "Network range 1", + args{"10.218.10.8/24"}, + "10.218.10.8/24", + }, + { + "IP address 1", + args{"10.218.10.8"}, + "10.218.10.8/32", + }, + { + "IPv6 address 1", + args{"2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, + "2001:0db8:85a3:0000:0000:8a2e:0370:7334/128", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := normalize(tt.args.rawNet); got != tt.want { + t.Errorf("Error: normalize() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/ag_201_coredns/plugin/any/README.md b/ag_201_coredns/plugin/any/README.md new file mode 100644 index 0000000..25e4ecf --- /dev/null +++ b/ag_201_coredns/plugin/any/README.md @@ -0,0 +1,36 @@ + +# any + +## Name + +*any* - gives a minimal response to ANY queries. + +## Description + +*any* basically blocks ANY queries by responding to them with a short HINFO reply. See [RFC +8482](https://tools.ietf.org/html/rfc8482) for details. + +## Syntax + +~~~ txt +any +~~~ + +## Examples + +~~~ corefile +example.org { + whoami + any +} +~~~ + +A `dig +nocmd ANY example.org +noall +answer` now returns: + +~~~ txt +example.org. 8482 IN HINFO "ANY obsoleted" "See RFC 8482" +~~~ + +## See Also + +[RFC 8482](https://tools.ietf.org/html/rfc8482). diff --git a/ag_201_coredns/plugin/any/any.go b/ag_201_coredns/plugin/any/any.go new file mode 100644 index 0000000..9a05e37 --- /dev/null +++ b/ag_201_coredns/plugin/any/any.go @@ -0,0 +1,32 @@ +package any + +import ( + "context" + + "github.com/coredns/coredns/plugin" + + "github.com/miekg/dns" +) + +// Any is a plugin that returns a HINFO reply to ANY queries. +type Any struct { + Next plugin.Handler +} + +// ServeDNS implements the plugin.Handler interface. +func (a Any) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if r.Question[0].Qtype != dns.TypeANY { + return plugin.NextOrFailure(a.Name(), a.Next, ctx, w, r) + } + + m := new(dns.Msg) + m.SetReply(r) + hdr := dns.RR_Header{Name: r.Question[0].Name, Ttl: 8482, Class: dns.ClassINET, Rrtype: dns.TypeHINFO} + m.Answer = []dns.RR{&dns.HINFO{Hdr: hdr, Cpu: "ANY obsoleted", Os: "See RFC 8482"}} + + w.WriteMsg(m) + return 0, nil +} + +// Name implements the Handler interface. +func (a Any) Name() string { return "any" } diff --git a/ag_201_coredns/plugin/any/any_test.go b/ag_201_coredns/plugin/any/any_test.go new file mode 100644 index 0000000..85df7d6 --- /dev/null +++ b/ag_201_coredns/plugin/any/any_test.go @@ -0,0 +1,28 @@ +package any + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestAny(t *testing.T) { + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeANY) + a := &Any{} + + 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 rec.Msg.Answer[0].(*dns.HINFO).Cpu != "ANY obsoleted" { + t.Errorf("Expected HINFO, but got %q", rec.Msg.Answer[0].(*dns.HINFO).Cpu) + } +} diff --git a/ag_201_coredns/plugin/any/setup.go b/ag_201_coredns/plugin/any/setup.go new file mode 100644 index 0000000..5c8a93b --- /dev/null +++ b/ag_201_coredns/plugin/any/setup.go @@ -0,0 +1,20 @@ +package any + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("any", setup) } + +func setup(c *caddy.Controller) error { + a := Any{} + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + a.Next = next + return a + }) + + return nil +} diff --git a/ag_201_coredns/plugin/auto/README.md b/ag_201_coredns/plugin/auto/README.md new file mode 100644 index 0000000..661e419 --- /dev/null +++ b/ag_201_coredns/plugin/auto/README.md @@ -0,0 +1,82 @@ +# auto + +## Name + +*auto* - enables serving zone data from an RFC 1035-style master file, which is automatically picked up from disk. + +## Description + +The *auto* plugin is used for an "old-style" DNS server. It serves from a preloaded file that exists +on disk. If the zone file contains signatures (i.e. is signed, i.e. using DNSSEC) correct DNSSEC answers +are returned. Only NSEC is supported! If you use this setup *you* are responsible for re-signing the +zonefile. New or changed zones are automatically picked up from disk only when SOA's serial changes. If the zones are not updated via a zone transfer, the serial must be manually changed. + +## Syntax + +~~~ +auto [ZONES...] { + directory DIR [REGEXP ORIGIN_TEMPLATE] + reload DURATION +} +~~~ + +**ZONES** zones it should be authoritative for. If empty, the zones from the configuration block +are used. + +* `directory` loads zones from the specified **DIR**. If a file name matches **REGEXP** it will be + used to extract the origin. **ORIGIN_TEMPLATE** will be used as a template for the origin. Strings + like `{}` are replaced with the respective matches in the file name, e.g. `{1}` is the + first match, `{2}` is the second. The default is: `db\.(.*) {1}` i.e. from a file with the + name `db.example.com`, the extracted origin will be `example.com`. +* `reload` interval to perform reloads of zones if SOA version changes and zonefiles. It specifies how often CoreDNS should scan the directory to watch for file removal and addition. Default is one minute. + Value of `0` means to not scan for changes and reload. eg. `30s` checks zonefile every 30 seconds + and reloads zone when serial changes. + +For enabling zone transfers look at the *transfer* plugin. + +All directives from the *file* plugin are supported. Note that *auto* will load all zones found, +even though the directive might only receive queries for a specific zone. I.e: + +~~~ corefile +. { + auto example.org { + directory /etc/coredns/zones + } +} +~~~ +Will happily pick up a zone for `example.COM`, except it will never be queried, because the *auto* +directive only is authoritative for `example.ORG`. + +## Examples + +Load `org` domains from `/etc/coredns/zones/org` and allow transfers to the internet, but send +notifies to 10.240.1.1 + +~~~ corefile +org { + auto { + directory /etc/coredns/zones/org + } + transfer { + to * + to 10.240.1.1 + } +} +~~~ + +Load `org` domains from `/etc/coredns/zones/org` and looks for file names as `www.db.example.org`, +where `example.org` is the origin. Scan every 45 seconds. + +~~~ corefile +org { + auto { + directory /etc/coredns/zones/org www\.db\.(.*) {1} + reload 45s + } +} +~~~ + +## Also + +Use the *root* plugin to help you specify the location of the zone files. See the *transfer* plugin +to enable outgoing zone transfers. diff --git a/ag_201_coredns/plugin/auto/auto.go b/ag_201_coredns/plugin/auto/auto.go new file mode 100644 index 0000000..581004b --- /dev/null +++ b/ag_201_coredns/plugin/auto/auto.go @@ -0,0 +1,100 @@ +// Package auto implements an on-the-fly loading file backend. +package auto + +import ( + "context" + "regexp" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/metrics" + "github.com/coredns/coredns/plugin/pkg/upstream" + "github.com/coredns/coredns/plugin/transfer" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +type ( + // Auto holds the zones and the loader configuration for automatically loading zones. + Auto struct { + Next plugin.Handler + *Zones + + metrics *metrics.Metrics + transfer *transfer.Transfer + loader + } + + loader struct { + directory string + template string + re *regexp.Regexp + + ReloadInterval time.Duration + upstream *upstream.Upstream // Upstream for looking up names during the resolution process. + } +) + +// ServeDNS implements the plugin.Handler interface. +func (a Auto) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.Name() + + // Precheck with the origins, i.e. are we allowed to look here? + zone := plugin.Zones(a.Zones.Origins()).Matches(qname) + if zone == "" { + return plugin.NextOrFailure(a.Name(), a.Next, ctx, w, r) + } + + // Now the real zone. + zone = plugin.Zones(a.Zones.Names()).Matches(qname) + if zone == "" { + return plugin.NextOrFailure(a.Name(), a.Next, ctx, w, r) + } + + a.Zones.RLock() + z, ok := a.Zones.Z[zone] + a.Zones.RUnlock() + + if !ok || z == nil { + return dns.RcodeServerFailure, nil + } + + // If transfer is not loaded, we'll see these, answer with refused (no transfer allowed). + if state.QType() == dns.TypeAXFR || state.QType() == dns.TypeIXFR { + return dns.RcodeRefused, nil + } + + answer, ns, extra, result := z.Lookup(ctx, state, qname) + + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative = true + m.Answer, m.Ns, m.Extra = answer, ns, extra + + switch result { + case file.Success: + case file.NoData: + case file.NameError: + m.Rcode = dns.RcodeNameError + case file.Delegation: + m.Authoritative = false + case file.ServerFailure: + // 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) + return dns.RcodeSuccess, nil +} + +// Name implements the Handler interface. +func (a Auto) Name() string { return "auto" } diff --git a/ag_201_coredns/plugin/auto/log_test.go b/ag_201_coredns/plugin/auto/log_test.go new file mode 100644 index 0000000..6047eeb --- /dev/null +++ b/ag_201_coredns/plugin/auto/log_test.go @@ -0,0 +1,5 @@ +package auto + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/auto/regexp.go b/ag_201_coredns/plugin/auto/regexp.go new file mode 100644 index 0000000..fa424ec --- /dev/null +++ b/ag_201_coredns/plugin/auto/regexp.go @@ -0,0 +1,20 @@ +package auto + +// 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 { + // Pretty dumb at the moment, every { will get a $ prefixed. + // Also wasteful as we build the string with +=. This is OKish + // as we do this during config parsing. + + copy := "" + + for _, c := range s { + if c == '{' { + copy += "$" + } + copy += string(c) + } + + return copy +} diff --git a/ag_201_coredns/plugin/auto/regexp_test.go b/ag_201_coredns/plugin/auto/regexp_test.go new file mode 100644 index 0000000..17c35eb --- /dev/null +++ b/ag_201_coredns/plugin/auto/regexp_test.go @@ -0,0 +1,20 @@ +package auto + +import "testing" + +func TestRewriteToExpand(t *testing.T) { + tests := []struct { + in string + expected string + }{ + {in: "", expected: ""}, + {in: "{1}", expected: "${1}"}, + {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) + } + } +} diff --git a/ag_201_coredns/plugin/auto/setup.go b/ag_201_coredns/plugin/auto/setup.go new file mode 100644 index 0000000..38e8d26 --- /dev/null +++ b/ag_201_coredns/plugin/auto/setup.go @@ -0,0 +1,165 @@ +package auto + +import ( + "errors" + "os" + "path/filepath" + "regexp" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metrics" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/upstream" + "github.com/coredns/coredns/plugin/transfer" +) + +var log = clog.NewWithPlugin("auto") + +func init() { plugin.Register("auto", setup) } + +func setup(c *caddy.Controller) error { + a, err := autoParse(c) + if err != nil { + return plugin.Error("auto", err) + } + + c.OnStartup(func() error { + m := dnsserver.GetConfig(c).Handler("prometheus") + if m != nil { + (&a).metrics = m.(*metrics.Metrics) + } + t := dnsserver.GetConfig(c).Handler("transfer") + if t != nil { + (&a).transfer = t.(*transfer.Transfer) + } + return nil + }) + + walkChan := make(chan bool) + + c.OnStartup(func() error { + err := a.Walk() + if err != nil { + return err + } + if a.loader.ReloadInterval == 0 { + return nil + } + go func() { + ticker := time.NewTicker(a.loader.ReloadInterval) + for { + select { + case <-walkChan: + return + case <-ticker.C: + a.Walk() + } + } + }() + return nil + }) + + c.OnShutdown(func() error { + close(walkChan) + return nil + }) + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + a.Next = next + return a + }) + + return nil +} + +func autoParse(c *caddy.Controller) (Auto, error) { + nilInterval := -1 * time.Second + var a = Auto{ + loader: loader{ + template: "${1}", + re: regexp.MustCompile(`db\.(.*)`), + ReloadInterval: nilInterval, + }, + Zones: &Zones{}, + } + + config := dnsserver.GetConfig(c) + + for c.Next() { + // auto [ZONES...] + args := c.RemainingArgs() + a.Zones.origins = plugin.OriginsFromArgsOrServerBlock(args, c.ServerBlockKeys) + a.loader.upstream = upstream.New() + + for c.NextBlock() { + switch c.Val() { + case "directory": // directory DIR [REGEXP TEMPLATE] + 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) + } + _, err := os.Stat(a.loader.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) + } + } + + // regexp template + if c.NextArg() { + a.loader.re, err = regexp.Compile(c.Val()) + if err != nil { + return a, err + } + if a.loader.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()) + } + + if c.NextArg() { + return Auto{}, c.ArgErr() + } + + case "reload": + 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 + + case "upstream": + // remove soon + c.RemainingArgs() // eat remaining args + + default: + return Auto{}, c.Errf("unknown property '%s'", c.Val()) + } + } + } + + if a.loader.ReloadInterval == nilInterval { + a.loader.ReloadInterval = 60 * time.Second + } + + return a, nil +} diff --git a/ag_201_coredns/plugin/auto/setup_test.go b/ag_201_coredns/plugin/auto/setup_test.go new file mode 100644 index 0000000..4fada6f --- /dev/null +++ b/ag_201_coredns/plugin/auto/setup_test.go @@ -0,0 +1,177 @@ +package auto + +import ( + "testing" + "time" + + "github.com/coredns/caddy" +) + +func TestAutoParse(t *testing.T) { + tests := []struct { + inputFileRules string + shouldErr bool + expectedDirectory string + expectedTempl string + expectedRe string + expectedReloadInterval time.Duration + }{ + { + `auto example.org { + directory /tmp + }`, + false, "/tmp", "${1}", `db\.(.*)`, 60 * time.Second, + }, + { + `auto 10.0.0.0/24 { + directory /tmp + }`, + false, "/tmp", "${1}", `db\.(.*)`, 60 * time.Second, + }, + { + `auto { + directory /tmp + reload 0 + }`, + false, "/tmp", "${1}", `db\.(.*)`, 0 * time.Second, + }, + { + `auto { + directory /tmp (.*) bliep + }`, + false, "/tmp", "bliep", `(.*)`, 60 * time.Second, + }, + { + `auto { + directory /tmp (.*) bliep + reload 10s + }`, + false, "/tmp", "bliep", `(.*)`, 10 * time.Second, + }, + // errors + // NO_RELOAD has been deprecated. + { + `auto { + directory /tmp + no_reload + }`, + true, "/tmp", "${1}", `db\.(.*)`, 0 * time.Second, + }, + // TIMEOUT has been deprecated. + { + `auto { + directory /tmp (.*) bliep 10 + }`, + true, "/tmp", "bliep", `(.*)`, 10 * time.Second, + }, + // TRANSFER has been deprecated. + { + `auto { + directory /tmp (.*) bliep 10 + transfer to 127.0.0.1 + }`, + true, "/tmp", "bliep", `(.*)`, 10 * time.Second, + }, + // no template specified. + { + `auto { + directory /tmp (.*) + }`, + true, "/tmp", "", `(.*)`, 60 * time.Second, + }, + // no directory specified. + { + `auto example.org { + directory + }`, + true, "", "${1}", `db\.(.*)`, 60 * time.Second, + }, + // illegal REGEXP. + { + `auto example.org { + directory /tmp * {1} + }`, + true, "/tmp", "${1}", ``, 60 * time.Second, + }, + // unexpected argument. + { + `auto example.org { + directory /tmp (.*) {1} aa + }`, + true, "/tmp", "${1}", ``, 60 * time.Second, + }, + } + + for i, test := range tests { + 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 a.loader.ReloadInterval != test.expectedReloadInterval { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedReloadInterval, a.loader.ReloadInterval) + } + } + } +} + +func TestSetupReload(t *testing.T) { + 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) { + 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/ag_201_coredns/plugin/auto/walk.go b/ag_201_coredns/plugin/auto/walk.go new file mode 100644 index 0000000..a50c5d8 --- /dev/null +++ b/ag_201_coredns/plugin/auto/walk.go @@ -0,0 +1,106 @@ +package auto + +import ( + "os" + "path/filepath" + "regexp" + + "github.com/coredns/coredns/plugin/file" + + "github.com/miekg/dns" +) + +// 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() { + toDelete[n] = true + } + + filepath.Walk(a.loader.directory, func(path string, info os.FileInfo, _ error) error { + if info == nil || info.IsDir() { + return nil + } + + match, origin := matches(a.loader.re, info.Name(), a.loader.template) + if !match { + return nil + } + + if z, ok := a.Zones.Z[origin]; ok { + // we already have this zone + toDelete[origin] = false + z.SetFile(path) + return nil + } + + reader, err := os.Open(filepath.Clean(path)) + if err != nil { + log.Warningf("Opening %s failed: %s", path, err) + return nil + } + defer reader.Close() + + // Serial for loading a zone is 0, because it is a new zone. + zo, err := file.Parse(reader, origin, path, 0) + if err != nil { + log.Warningf("Parse zone `%s': %v", origin, err) + return nil + } + + zo.ReloadInterval = a.loader.ReloadInterval + zo.Upstream = a.loader.upstream + + a.Zones.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 + + return nil + }) + + for origin, ok := range toDelete { + if !ok { + continue + } + + if a.metrics != nil { + a.metrics.RemoveZone(origin) + } + + a.Zones.Remove(origin) + + log.Infof("Deleting zone `%s'", origin) + } + + return nil +} + +// matches re to filename, if it is a match, the subexpression will be used to expand +// template to an origin. When match is true that origin is returned. Origin is fully qualified. +func matches(re *regexp.Regexp, filename, template string) (match bool, origin string) { + base := filepath.Base(filename) + + matches := re.FindStringSubmatchIndex(base) + if matches == nil { + return false, "" + } + + by := re.ExpandString(nil, template, base, matches) + if by == nil { + return false, "" + } + + origin = dns.Fqdn(string(by)) + + return true, origin +} diff --git a/ag_201_coredns/plugin/auto/walk_test.go b/ag_201_coredns/plugin/auto/walk_test.go new file mode 100644 index 0000000..bee955c --- /dev/null +++ b/ag_201_coredns/plugin/auto/walk_test.go @@ -0,0 +1,88 @@ +package auto + +import ( + "os" + "path/filepath" + "regexp" + "testing" +) + +var dbFiles = []string{"db.example.org", "aa.example.org"} + +const zoneContent = `; testzone +@ IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082534 7200 3600 1209600 3600 + NS a.iana-servers.net. + NS b.iana-servers.net. + +www IN A 127.0.0.1 +` + +func TestWalk(t *testing.T) { + tempdir, err := createFiles() + if err != nil { + if tempdir != "" { + os.RemoveAll(tempdir) + } + t.Fatal(err) + } + defer os.RemoveAll(tempdir) + + ldr := loader{ + directory: tempdir, + re: regexp.MustCompile(`db\.(.*)`), + template: `${1}`, + } + + a := Auto{ + loader: ldr, + Zones: &Zones{}, + } + + a.Walk() + + // 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 { + t.Errorf("%s should have been added", name) + } + } +} + +func TestWalkNonExistent(t *testing.T) { + nonExistingDir := "highly_unlikely_to_exist_dir" + + ldr := loader{ + directory: nonExistingDir, + re: regexp.MustCompile(`db\.(.*)`), + template: `${1}`, + } + + a := Auto{ + loader: ldr, + Zones: &Zones{}, + } + + a.Walk() +} + +func createFiles() (string, error) { + dir, err := os.MkdirTemp(os.TempDir(), "coredns") + if err != nil { + return dir, err + } + + for _, name := range dbFiles { + 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 { + return dir, err + } + if err = os.Symlink(filepath.Join(dir, "db.example.org"), filepath.Join(dir, "aa.example.com")); err != nil { + return dir, err + } + + return dir, nil +} diff --git a/ag_201_coredns/plugin/auto/watcher_test.go b/ag_201_coredns/plugin/auto/watcher_test.go new file mode 100644 index 0000000..43f1ff3 --- /dev/null +++ b/ag_201_coredns/plugin/auto/watcher_test.go @@ -0,0 +1,100 @@ +package auto + +import ( + "os" + "path/filepath" + "regexp" + "testing" +) + +func TestWatcher(t *testing.T) { + tempdir, err := createFiles() + if err != nil { + if tempdir != "" { + os.RemoveAll(tempdir) + } + t.Fatal(err) + } + defer os.RemoveAll(tempdir) + + ldr := loader{ + directory: tempdir, + re: regexp.MustCompile(`db\.(.*)`), + template: `${1}`, + } + + a := Auto{ + loader: ldr, + Zones: &Zones{}, + } + + 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 { + t.Fatalf("Expected 1 RRs, got %d", x) + } + if x := len(a.Zones.Z["example.com."].All()); x != 1 { + t.Fatalf("Expected 1 RRs, got %d", x) + } + + // Now remove one file, rescan and see if it's gone. + if err := os.Remove(filepath.Join(tempdir, "db.example.com")); err != nil { + t.Fatal(err) + } + + a.Walk() + + if _, ok := a.Zones.Z["example.com."]; ok { + t.Errorf("Expected %q to be gone.", "example.com.") + } + if _, ok := a.Zones.Z["example.org."]; !ok { + t.Errorf("Expected %q to still be there.", "example.org.") + } +} + +func TestSymlinks(t *testing.T) { + tempdir, err := createFiles() + if err != nil { + if tempdir != "" { + os.RemoveAll(tempdir) + } + t.Fatal(err) + } + defer os.RemoveAll(tempdir) + + ldr := loader{ + directory: tempdir, + re: regexp.MustCompile(`db\.(.*)`), + template: `${1}`, + } + + a := Auto{ + loader: ldr, + Zones: &Zones{}, + } + + a.Walk() + + // Now create a duplicate file in a subdirectory and repoint the symlink + if err := os.Remove(filepath.Join(tempdir, "db.example.com")); err != nil { + t.Fatal(err) + } + dataDir := filepath.Join(tempdir, "..data") + if err = os.Mkdir(dataDir, 0755); err != nil { + t.Fatal(err) + } + newFile := filepath.Join(dataDir, "db.example.com") + if err = os.Symlink(filepath.Join(tempdir, "db.example.org"), newFile); err != nil { + t.Fatal(err) + } + + a.Walk() + + if storedZone, ok := a.Zones.Z["example.com."]; ok { + storedFile := storedZone.File() + if storedFile != newFile { + t.Errorf("Expected %q to reflect new path %q", storedFile, newFile) + } + } +} diff --git a/ag_201_coredns/plugin/auto/xfr.go b/ag_201_coredns/plugin/auto/xfr.go new file mode 100644 index 0000000..6fef8b9 --- /dev/null +++ b/ag_201_coredns/plugin/auto/xfr.go @@ -0,0 +1,19 @@ +package auto + +import ( + "github.com/coredns/coredns/plugin/transfer" + + "github.com/miekg/dns" +) + +// 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() + + if !ok || z == nil { + return nil, transfer.ErrNotAuthoritative + } + return z.Transfer(serial) +} diff --git a/ag_201_coredns/plugin/auto/zone.go b/ag_201_coredns/plugin/auto/zone.go new file mode 100644 index 0000000..bb81186 --- /dev/null +++ b/ag_201_coredns/plugin/auto/zone.go @@ -0,0 +1,77 @@ +// Package auto implements a on-the-fly loading file backend. +package auto + +import ( + "sync" + + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/transfer" +) + +// Zones maps zone names to a *Zone. This keeps track of what zones we have loaded at +// any one time. +type Zones struct { + Z map[string]*file.Zone // A map mapping zone (origin) to the Zone's data. + names []string // All the keys from the map Z as a string slice. + + origins []string // Any origins from the server block. + + sync.RWMutex +} + +// Names returns the names from z. +func (z *Zones) Names() []string { + z.RLock() + n := z.names + z.RUnlock() + return n +} + +// Origins returns the origins from z. +func (z *Zones) Origins() []string { + // doesn't need locking, because there aren't multiple Go routines accessing it. + return z.origins +} + +// Zones returns a zone with origin name from z, nil when not found. +func (z *Zones) Zones(name string) *file.Zone { + z.RLock() + zo := z.Z[name] + z.RUnlock() + return zo +} + +// 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() + + if z.Z == nil { + z.Z = make(map[string]*file.Zone) + } + + z.Z[name] = zo + z.names = append(z.names, name) + zo.Reload(t) + + z.Unlock() +} + +// Remove removes the zone named name from z. It also stops the zone's reload goroutine. +func (z *Zones) Remove(name string) { + z.Lock() + + if zo, ok := z.Z[name]; ok { + zo.OnShutdown() + } + + delete(z.Z, name) + + // TODO(miek): just regenerate Names (might be bad if you have a lot of zones...) + z.names = []string{} + for n := range z.Z { + z.names = append(z.names, n) + } + + z.Unlock() +} diff --git a/ag_201_coredns/plugin/autopath/README.md b/ag_201_coredns/plugin/autopath/README.md new file mode 100644 index 0000000..eedbf5e --- /dev/null +++ b/ag_201_coredns/plugin/autopath/README.md @@ -0,0 +1,68 @@ +# autopath + +## Name + +*autopath* - allows for server-side search path completion. + +## Description + +If the *autopath* plugin sees a query that matches the first element of the configured search path, it will +follow the chain of search path elements and return the first reply that is not NXDOMAIN. On any +failures, the original reply is returned. Because *autopath* returns a reply for a name that wasn't +the original question, it will add a CNAME that points from the original name (with the search path +element in it) to the name of this answer. + +**Note**: There are several known issues, see the "Bugs" section below. + +## Syntax + +~~~ +autopath [ZONE...] RESOLV-CONF +~~~ + +* **ZONES** zones *autopath* should be authoritative for. +* **RESOLV-CONF** points to a `resolv.conf` like file or uses a special syntax to point to another + plugin. For instance `@kubernetes`, will call out to the kubernetes plugin (for each + query) to retrieve the search list it should use. + +If a plugin implements the `AutoPather` interface then it can be used by *autopath*. + +## Metrics + +If monitoring is enabled (via the *prometheus* plugin) then the following metric is exported: + +* `coredns_autopath_success_total{server}` - counter of successfully autopath-ed queries. + +The `server` label is explained in the *metrics* plugin documentation. + +## Examples + +~~~ +autopath my-resolv.conf +~~~ + +Use `my-resolv.conf` as the file to get the search path from. This file only needs to have one line: +`search domain1 domain2 ...` + +~~~ +autopath @kubernetes +~~~ + +Use the search path dynamically retrieved from the *kubernetes* plugin. + +## Bugs + +In Kubernetes, *autopath* can derive the wrong namespace of a client Pod (and therefore wrong search +path) in the following case. To properly build the search path of a client *autopath* needs to know +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 +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. + +In Kubernetes, *autopath* is not compatible with Pods running from Windows nodes. + +If the server side search ultimately results in a negative answer (e.g. `NXDOMAIN`), then the client +will fruitlessly search all paths manually, thus negating the *autopath* optimization. diff --git a/ag_201_coredns/plugin/autopath/autopath.go b/ag_201_coredns/plugin/autopath/autopath.go new file mode 100644 index 0000000..e5675e8 --- /dev/null +++ b/ag_201_coredns/plugin/autopath/autopath.go @@ -0,0 +1,157 @@ +/* +Package autopath implements autopathing. This is a hack; it shortcuts the +client's search path resolution by performing these lookups on the server... + +The server has a copy (via AutoPathFunc) of the client's search path and on +receiving a query it first establishes if the suffix matches the FIRST configured +element. If no match can be found the query will be forwarded up the plugin +chain without interference (if, and only if, 'fallthrough' has been set). + +If the query is deemed to fall in the search path the server will perform the +queries with each element of the search path appended in sequence until a +non-NXDOMAIN answer has been found. That reply will then be returned to the +client - with some CNAME hackery to let the client accept the reply. + +If all queries return NXDOMAIN we return the original as-is and let the client +continue searching. The client will go to the next element in the search path, +but we won’t do any more autopathing. It means that in the failure case, you do +more work, since the server looks it up, then the client still needs to go +through the search path. + +It is assume the search path ordering is identical between server and client. + +Plugins implementing autopath, must have a function called `AutoPath` of type +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", ""} +} +*/ +package autopath + +import ( + "context" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metrics" + "github.com/coredns/coredns/plugin/pkg/dnsutil" + "github.com/coredns/coredns/plugin/pkg/nonwriter" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Func defines the function plugin should implement to return a search +// path to the autopath plugin. The last element of the slice must be the empty string. +// If Func returns a nil slice, no autopathing will be done. +type Func func(request.Request) []string + +// AutoPather defines the interface that a plugin should implement in order to be +// used by AutoPath. +type AutoPather interface { + AutoPath(request.Request) []string +} + +// AutoPath performs autopath: service side search path completion. +type AutoPath struct { + Next plugin.Handler + Zones []string + + // Search always includes "" as the last element, so we try the base query with out any search paths added as well. + search []string + searchFunc Func +} + +// ServeDNS implements the plugin.Handle interface. +func (a *AutoPath) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + + zone := plugin.Zones(a.Zones).Matches(state.Name()) + if zone == "" { + return plugin.NextOrFailure(a.Name(), a.Next, ctx, w, r) + } + + // Check if autopath should be done, searchFunc takes precedence over the local configured search path. + var err error + searchpath := a.search + + if a.searchFunc != nil { + searchpath = a.searchFunc(state) + } + + if len(searchpath) == 0 { + return plugin.NextOrFailure(a.Name(), a.Next, ctx, w, r) + } + + if !firstInSearchPath(state.Name(), searchpath) { + return plugin.NextOrFailure(a.Name(), a.Next, ctx, w, r) + } + + origQName := state.QName() + + // Establish base name of the query. I.e what was originally asked. + base, err := dnsutil.TrimZone(state.QName(), searchpath[0]) + if err != nil { + return dns.RcodeServerFailure, err + } + + firstReply := new(dns.Msg) + firstRcode := 0 + var firstErr error + + ar := r.Copy() + // Walk the search path and see if we can get a non-nxdomain - if they all fail we return the first + // query we've done and return that as-is. This means the client will do the search path walk again... + for i, s := range searchpath { + newQName := base + "." + s + ar.Question[0].Name = newQName + nw := nonwriter.New(w) + + rcode, err := plugin.NextOrFailure(a.Name(), a.Next, ctx, nw, ar) + if err != nil { + // Return now - not sure if this is the best. We should also check if the write has happened. + return rcode, err + } + if i == 0 { + firstReply = nw.Msg + firstRcode = rcode + firstErr = err + } + + if !plugin.ClientWrite(rcode) { + continue + } + + if nw.Msg.Rcode == dns.RcodeNameError { + continue + } + + msg := nw.Msg + cnamer(msg, origQName) + + // Write whatever non-nxdomain answer we've found. + w.WriteMsg(msg) + autoPathCount.WithLabelValues(metrics.WithServer(ctx)).Add(1) + return rcode, err + } + if plugin.ClientWrite(firstRcode) { + w.WriteMsg(firstReply) + } + return firstRcode, firstErr +} + +// Name implements the Handler interface. +func (a *AutoPath) Name() string { return "autopath" } + +// firstInSearchPath checks if name is equal to are a sibling of the first element in the search path. +func firstInSearchPath(name string, searchpath []string) bool { + if name == searchpath[0] { + return true + } + if dns.IsSubDomain(searchpath[0], name) { + return true + } + return false +} diff --git a/ag_201_coredns/plugin/autopath/autopath_test.go b/ag_201_coredns/plugin/autopath/autopath_test.go new file mode 100644 index 0000000..5c4e554 --- /dev/null +++ b/ag_201_coredns/plugin/autopath/autopath_test.go @@ -0,0 +1,166 @@ +package autopath + +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" +) + +var autopathTestCases = []test.Case{ + { + // search path expansion. + Qname: "b.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("b.example.org. 3600 IN CNAME b.com."), + test.A("b.com." + defaultA), + }, + }, + { + // No search path expansion + Qname: "a.example.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("a.example.com." + defaultA), + }, + }, +} + +func newTestAutoPath() *AutoPath { + ap := new(AutoPath) + ap.Zones = []string{"."} + ap.Next = nextHandler(map[string]int{ + "b.example.org.": dns.RcodeNameError, + "b.com.": dns.RcodeSuccess, + "a.example.com.": dns.RcodeSuccess, + }) + + ap.search = []string{"example.org.", "example.com.", "com.", ""} + return ap +} + +func TestAutoPath(t *testing.T) { + ap := newTestAutoPath() + ctx := context.TODO() + + for _, tc := range autopathTestCases { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := ap.ServeDNS(ctx, rec, m) + if err != nil { + t.Errorf("Expected no error, got %v", err) + continue + } + + // No sorting here as we want to check if the CNAME sits *before* the + // test of the answer. + resp := rec.Msg + + if err := test.Header(tc, resp); err != nil { + t.Error(err) + continue + } + if err := test.Section(tc, test.Answer, resp.Answer); err != nil { + t.Error(err) + } + if err := test.Section(tc, test.Ns, resp.Ns); err != nil { + t.Error(err) + } + if err := test.Section(tc, test.Extra, resp.Extra); err != nil { + t.Error(err) + } + } +} + +var autopathNoAnswerTestCases = []test.Case{ + { + // search path expansion, no answer + Qname: "c.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("b.example.org. 3600 IN CNAME b.com."), + test.A("b.com." + defaultA), + }, + }, +} + +func TestAutoPathNoAnswer(t *testing.T) { + ap := newTestAutoPath() + ctx := context.TODO() + + for _, tc := range autopathNoAnswerTestCases { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + rcode, err := ap.ServeDNS(ctx, rec, m) + if err != nil { + t.Errorf("Expected no error, got %v", err) + continue + } + if plugin.ClientWrite(rcode) { + t.Fatalf("Expected no client write, got one for rcode %d", rcode) + } + } +} + +// nextHandler returns a Handler that returns an answer for the question in the +// request per the domain->answer map. On success an RR will be returned: "qname 3600 IN A 127.0.0.53" +func nextHandler(mm map[string]int) test.Handler { + return test.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + rcode, ok := mm[r.Question[0].Name] + if !ok { + return dns.RcodeServerFailure, nil + } + + m := new(dns.Msg) + m.SetReply(r) + + switch rcode { + case dns.RcodeNameError: + m.Rcode = rcode + m.Ns = []dns.RR{soa} + w.WriteMsg(m) + return m.Rcode, nil + + case dns.RcodeSuccess: + m.Rcode = rcode + a, _ := dns.NewRR(r.Question[0].Name + defaultA) + m.Answer = []dns.RR{a} + + w.WriteMsg(m) + return m.Rcode, nil + default: + panic("nextHandler: unhandled rcode") + } + }) +} + +const defaultA = " 3600 IN A 127.0.0.53" + +var soa = func() dns.RR { + s, _ := dns.NewRR("example.org. 1800 IN SOA example.org. example.org. 1502165581 14400 3600 604800 14400") + return s +}() + +func TestInSearchPath(t *testing.T) { + a := AutoPath{search: []string{"default.svc.cluster.local.", "svc.cluster.local.", "cluster.local."}} + + tests := []struct { + qname string + b bool + }{ + {"google.com", false}, + {"default.svc.cluster.local.", true}, + {"a.default.svc.cluster.local.", true}, + {"a.b.svc.cluster.local.", false}, + } + for i, tc := range tests { + got := firstInSearchPath(tc.qname, a.search) + if got != tc.b { + t.Errorf("Test %d, got %v, expected %v", i, got, tc.b) + } + } +} diff --git a/ag_201_coredns/plugin/autopath/cname.go b/ag_201_coredns/plugin/autopath/cname.go new file mode 100644 index 0000000..3b2c60f --- /dev/null +++ b/ag_201_coredns/plugin/autopath/cname.go @@ -0,0 +1,25 @@ +package autopath + +import ( + "strings" + + "github.com/miekg/dns" +) + +// cnamer will prefix the answer section with a cname that points from original qname to the +// name of the first RR. It will also update the question section and put original in there. +func cnamer(m *dns.Msg, original string) { + for _, a := range m.Answer { + if strings.EqualFold(original, a.Header().Name) { + continue + } + m.Answer = append(m.Answer, nil) + copy(m.Answer[1:], m.Answer) + m.Answer[0] = &dns.CNAME{ + Hdr: dns.RR_Header{Name: original, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: a.Header().Ttl}, + Target: a.Header().Name, + } + break + } + m.Question[0].Name = original +} diff --git a/ag_201_coredns/plugin/autopath/metrics.go b/ag_201_coredns/plugin/autopath/metrics.go new file mode 100644 index 0000000..65a6cbd --- /dev/null +++ b/ag_201_coredns/plugin/autopath/metrics.go @@ -0,0 +1,18 @@ +package autopath + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // autoPathCount is counter of successfully autopath-ed queries. + autoPathCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "autopath", + Name: "success_total", + Help: "Counter of requests that did autopath.", + }, []string{"server"}) +) diff --git a/ag_201_coredns/plugin/autopath/setup.go b/ag_201_coredns/plugin/autopath/setup.go new file mode 100644 index 0000000..a041e36 --- /dev/null +++ b/ag_201_coredns/plugin/autopath/setup.go @@ -0,0 +1,70 @@ +package autopath + +import ( + "fmt" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + + "github.com/miekg/dns" +) + +func init() { plugin.Register("autopath", setup) } + +func setup(c *caddy.Controller) error { + ap, mw, err := autoPathParse(c) + if err != nil { + return plugin.Error("autopath", err) + } + + // Do this in OnStartup, so all plugin has been initialized. + c.OnStartup(func() error { + m := dnsserver.GetConfig(c).Handler(mw) + if m == nil { + return nil + } + if x, ok := m.(AutoPather); ok { + ap.searchFunc = x.AutoPath + } else { + return plugin.Error("autopath", fmt.Errorf("%s does not implement the AutoPather interface", mw)) + } + return nil + }) + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + ap.Next = next + return ap + }) + + return nil +} + +func autoPathParse(c *caddy.Controller) (*AutoPath, string, error) { + ap := &AutoPath{} + mw := "" + + for c.Next() { + zoneAndresolv := c.RemainingArgs() + if len(zoneAndresolv) < 1 { + return ap, "", fmt.Errorf("no resolv-conf specified") + } + resolv := zoneAndresolv[len(zoneAndresolv)-1] + if strings.HasPrefix(resolv, "@") { + mw = resolv[1:] + } else { + // assume file on disk + rc, err := dns.ClientConfigFromFile(resolv) + if err != nil { + return ap, "", fmt.Errorf("failed to parse %q: %v", resolv, err) + } + ap.search = rc.Search + plugin.Zones(ap.search).Normalize() + ap.search = append(ap.search, "") // sentinel value as demanded. + } + zones := zoneAndresolv[:len(zoneAndresolv)-1] + ap.Zones = plugin.OriginsFromArgsOrServerBlock(zones, c.ServerBlockKeys) + } + return ap, mw, nil +} diff --git a/ag_201_coredns/plugin/autopath/setup_test.go b/ag_201_coredns/plugin/autopath/setup_test.go new file mode 100644 index 0000000..4644c7d --- /dev/null +++ b/ag_201_coredns/plugin/autopath/setup_test.go @@ -0,0 +1,77 @@ +package autopath + +import ( + "os" + "reflect" + "strings" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/test" +) + +func TestSetupAutoPath(t *testing.T) { + resolv, rm, err := test.TempFile(os.TempDir(), resolvConf) + if err != nil { + t.Fatalf("Could not create resolv.conf test file %s: %s", resolvConf, err) + } + defer rm() + + tests := []struct { + input string + shouldErr bool + expectedZone string + expectedMw string // expected plugin. + expectedSearch []string // expected search path + expectedErrContent string // substring from the expected error. Empty for positive cases. + }{ + // positive + {`autopath @kubernetes`, false, "", "kubernetes", nil, ""}, + {`autopath example.org @kubernetes`, false, "example.org.", "kubernetes", nil, ""}, + {`autopath 10.0.0.0/8 @kubernetes`, false, "10.in-addr.arpa.", "kubernetes", nil, ""}, + {`autopath ` + resolv, false, "", "", []string{"bar.com.", "baz.com.", ""}, ""}, + // 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 { + c := caddy.NewTestController("dns", test.input) + ap, mw, err := autoPathParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, 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) + } + } + + if !test.shouldErr && mw != test.expectedMw { + t.Errorf("Test %d, Plugin not correctly set for input %s. Expected: %s, actual: %s", i, test.input, test.expectedMw, mw) + } + if !test.shouldErr && ap.search != nil { + if !reflect.DeepEqual(test.expectedSearch, ap.search) { + t.Errorf("Test %d, wrong searchpath for input %s. Expected: '%v', actual: '%v'", i, test.input, test.expectedSearch, ap.search) + } + } + if !test.shouldErr && test.expectedZone != "" { + if test.expectedZone != ap.Zones[0] { + t.Errorf("Test %d, expected zone %q for input %s, got: %q", i, test.expectedZone, test.input, ap.Zones[0]) + } + } + } +} + +const resolvConf = `nameserver 1.2.3.4 +domain foo.com +search bar.com baz.com +options ndots:5 +` diff --git a/ag_201_coredns/plugin/azure/README.md b/ag_201_coredns/plugin/azure/README.md new file mode 100644 index 0000000..f5ed5ab --- /dev/null +++ b/ag_201_coredns/plugin/azure/README.md @@ -0,0 +1,60 @@ +# azure + +## Name + +*azure* - enables serving zone data from Microsoft Azure DNS service. + +## Description + +The azure plugin is useful for serving zones from Microsoft Azure DNS. The *azure* plugin supports +all the DNS records supported by Azure, viz. A, AAAA, CNAME, MX, NS, PTR, SOA, SRV, and TXT +record types. NS record type is not supported by azure private DNS. + +## Syntax + +~~~ txt +azure RESOURCE_GROUP:ZONE... { + tenant TENANT_ID + client CLIENT_ID + secret CLIENT_SECRET + subscription SUBSCRIPTION_ID + environment ENVIRONMENT + fallthrough [ZONES...] + access private +} +~~~ + +* **RESOURCE_GROUP:ZONE** is the resource group to which the hosted zones belongs on Azure, + and **ZONE** the zone that contains data. + +* **CLIENT_ID** and **CLIENT_SECRET** are the credentials for Azure, and `tenant` specifies the + **TENANT_ID** to be used. **SUBSCRIPTION_ID** is the subscription ID. All of these are needed + to access the data in Azure. + +* `environment` specifies the Azure **ENVIRONMENT**. + +* `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. + +* `access` specifies if the zone is `public` or `private`. Default is `public`. + +## Examples + +Enable the *azure* plugin with Azure credentials for private zones `example.org`, `example.private`: + +~~~ txt +example.org { + azure resource_group_foo:example.org resource_group_foo:example.private { + tenant 123abc-123abc-123abc-123abc + client 123abc-123abc-123abc-234xyz + subscription 123abc-123abc-123abc-563abc + secret mysecret + access private + } +} +~~~ + +## See Also + +The [Azure DNS Overview](https://docs.microsoft.com/en-us/azure/dns/dns-overview). diff --git a/ag_201_coredns/plugin/azure/azure.go b/ag_201_coredns/plugin/azure/azure.go new file mode 100644 index 0000000..e236a08 --- /dev/null +++ b/ag_201_coredns/plugin/azure/azure.go @@ -0,0 +1,352 @@ +package azure + +import ( + "context" + "fmt" + "net" + "sync" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/plugin/pkg/upstream" + "github.com/coredns/coredns/request" + + publicdns "github.com/Azure/azure-sdk-for-go/profiles/latest/dns/mgmt/dns" + privatedns "github.com/Azure/azure-sdk-for-go/profiles/latest/privatedns/mgmt/privatedns" + "github.com/miekg/dns" +) + +type zone struct { + id string + z *file.Zone + zone string + private bool +} + +type zones map[string][]*zone + +// Azure is the core struct of the azure plugin. +type Azure struct { + zoneNames []string + publicClient publicdns.RecordSetsClient + privateClient privatedns.RecordSetsClient + upstream *upstream.Upstream + zMu sync.RWMutex + zones zones + + Next plugin.Handler + Fall fall.F +} + +// New validates the input DNS zones and initializes the Azure struct. +func New(ctx context.Context, publicClient publicdns.RecordSetsClient, privateClient privatedns.RecordSetsClient, keys map[string][]string, accessMap map[string]string) (*Azure, error) { + zones := make(map[string][]*zone, len(keys)) + names := make([]string, len(keys)) + var private bool + + for resourceGroup, znames := range keys { + for _, name := range znames { + switch accessMap[resourceGroup+name] { + case "public": + if _, err := publicClient.ListAllByDNSZone(context.Background(), resourceGroup, name, nil, ""); err != nil { + return nil, err + } + private = false + case "private": + if _, err := privateClient.ListComplete(context.Background(), resourceGroup, name, nil, ""); err != nil { + return nil, err + } + private = true + } + + fqdn := dns.Fqdn(name) + if _, ok := zones[fqdn]; !ok { + names = append(names, fqdn) + } + zones[fqdn] = append(zones[fqdn], &zone{id: resourceGroup, zone: name, private: private, z: file.NewZone(fqdn, "")}) + } + } + + return &Azure{ + publicClient: publicClient, + privateClient: privateClient, + zones: zones, + zoneNames: names, + upstream: upstream.New(), + }, nil +} + +// Run updates the zone from azure. +func (h *Azure) Run(ctx context.Context) error { + if err := h.updateZones(ctx); err != nil { + 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 <-timer.C: + if err := h.updateZones(ctx); err != nil && ctx.Err() == nil { + log.Errorf("Failed to update zones %v: %v", h.zoneNames, err) + } + } + } + }() + return nil +} + +func (h *Azure) updateZones(ctx context.Context) error { + var err error + var publicSet publicdns.RecordSetListResultPage + var privateSet privatedns.RecordSetListResultPage + errs := make([]string, 0) + for zName, z := range h.zones { + for i, hostedZone := range z { + newZ := file.NewZone(zName, "") + if hostedZone.private { + for privateSet, err = h.privateClient.List(ctx, hostedZone.id, hostedZone.zone, nil, ""); privateSet.NotDone(); err = privateSet.NextWithContext(ctx) { + updateZoneFromPrivateResourceSet(privateSet, newZ) + } + } else { + for publicSet, err = h.publicClient.ListByDNSZone(ctx, hostedZone.id, hostedZone.zone, nil, ""); publicSet.NotDone(); err = publicSet.NextWithContext(ctx) { + updateZoneFromPublicResourceSet(publicSet, newZ) + } + } + if err != nil { + errs = append(errs, fmt.Sprintf("failed to list resource records for %v from azure: %v", hostedZone.zone, err)) + } + newZ.Upstream = h.upstream + h.zMu.Lock() + (*z[i]).z = newZ + h.zMu.Unlock() + } + } + + if len(errs) != 0 { + 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) { + 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) { + 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) { + 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))} + newZ.Insert(mx) + } + } + + if result.RecordSetProperties.PtrRecords != nil { + for _, PTR := range *(result.RecordSetProperties.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) { + 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)), + Port: uint16(*(SRV.Port)), + Target: dns.Fqdn(*(SRV.Target))} + newZ.Insert(srv) + } + } + + if result.RecordSetProperties.TxtRecords != nil { + for _, TXT := range *(result.RecordSetProperties.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) { + 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 + 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)), + Retry: uint32(*(SOA.RetryTime)), + Refresh: uint32(*(SOA.RefreshTime)), + Serial: uint32(*(SOA.SerialNumber)), + Mbox: dns.Fqdn(*(SOA.Email)), + Ns: *(SOA.Host)} + newZ.Insert(soa) + } + + if result.RecordSetProperties.CnameRecord != nil { + CNAME := result.RecordSetProperties.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) + } + } +} + +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) { + 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) { + 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) { + 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))} + newZ.Insert(mx) + } + } + + if result.RecordSetProperties.PtrRecords != nil { + for _, PTR := range *(result.RecordSetProperties.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) { + 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)), + Port: uint16(*(SRV.Port)), + Target: dns.Fqdn(*(SRV.Target))} + newZ.Insert(srv) + } + } + + if result.RecordSetProperties.TxtRecords != nil { + for _, TXT := range *(result.RecordSetProperties.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 + 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)), + Retry: uint32(*(SOA.RetryTime)), + Refresh: uint32(*(SOA.RefreshTime)), + Serial: uint32(*(SOA.SerialNumber)), + Mbox: dns.Fqdn(*(SOA.Email)), + Ns: dns.Fqdn(*(SOA.Host))} + newZ.Insert(soa) + } + + if result.RecordSetProperties.CnameRecord != nil { + CNAME := result.RecordSetProperties.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) + } + } +} + +// ServeDNS implements the plugin.Handler interface. +func (h *Azure) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.Name() + + zone := plugin.Zones(h.zoneNames).Matches(qname) + if zone == "" { + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + + zones, ok := h.zones[zone] // ok true if we are authoritative for the zone. + if !ok || zones == nil { + return dns.RcodeServerFailure, nil + } + + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative = true + var result file.Result + for _, z := range zones { + h.zMu.RLock() + m.Answer, m.Ns, m.Extra, result = z.z.Lookup(ctx, state, qname) + h.zMu.RUnlock() + + // record type exists for this name (NODATA). + if len(m.Answer) != 0 || result == file.NoData { + break + } + } + + if len(m.Answer) == 0 && result != file.NoData && h.Fall.Through(qname) { + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + + switch result { + case file.Success: + case file.NoData: + case file.NameError: + m.Rcode = dns.RcodeNameError + case file.Delegation: + m.Authoritative = false + case file.ServerFailure: + return dns.RcodeServerFailure, nil + } + + w.WriteMsg(m) + return dns.RcodeSuccess, nil +} + +// Name implements plugin.Handler.Name. +func (h *Azure) Name() string { return "azure" } diff --git a/ag_201_coredns/plugin/azure/azure_test.go b/ag_201_coredns/plugin/azure/azure_test.go new file mode 100644 index 0000000..d006f19 --- /dev/null +++ b/ag_201_coredns/plugin/azure/azure_test.go @@ -0,0 +1,180 @@ +package azure + +import ( + "context" + "reflect" + "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/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +var demoAzure = Azure{ + Next: testHandler(), + Fall: fall.Zero, + zoneNames: []string{"example.org.", "www.example.org.", "example.org.", "sample.example.org."}, + zones: testZones(), +} + +func testZones() zones { + zones := make(map[string][]*zone) + zones["example.org."] = append(zones["example.org."], &zone{zone: "example.org."}) + newZ := file.NewZone("example.org.", "") + + for _, rr := range []string{ + "example.org. 300 IN A 1.2.3.4", + "example.org. 300 IN AAAA 2001:db8:85a3::8a2e:370:7334", + "www.example.org. 300 IN A 1.2.3.4", + "www.example.org. 300 IN A 1.2.3.4", + "org. 172800 IN NS ns3-06.azure-dns.org.", + "org. 300 IN SOA ns1-06.azure-dns.com. azuredns-hostmaster.microsoft.com. 1 3600 300 2419200 300", + "cname.example.org. 300 IN CNAME example.org", + "mail.example.org. 300 IN MX 10 mailserver.example.com", + "ptr.example.org. 300 IN PTR www.ptr-example.com", + "example.org. 300 IN SRV 1 10 5269 srv-1.example.com.", + "example.org. 300 IN SRV 1 10 5269 srv-2.example.com.", + "txt.example.org. 300 IN TXT \"TXT for example.org\"", + } { + r, _ := dns.NewRR(rr) + newZ.Insert(r) + } + zones["example.org."][0].z = newZ + return zones +} + +func testHandler() test.HandlerFunc { + return func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.Name() + m := new(dns.Msg) + rcode := dns.RcodeServerFailure + if qname == "example.gov." { // No records match, test fallthrough. + m.SetReply(r) + rr := test.A("example.gov. 300 IN A 2.4.6.8") + m.Answer = []dns.RR{rr} + m.Authoritative = true + rcode = dns.RcodeSuccess + } + m.SetRcode(r, rcode) + w.WriteMsg(m) + return rcode, nil + } +} + +func TestAzure(t *testing.T) { + tests := []struct { + qname string + qtype uint16 + wantRetCode int + wantAnswer []string + wantMsgRCode int + wantNS []string + expectedErr error + }{ + { + qname: "example.org.", + qtype: dns.TypeA, + wantAnswer: []string{"example.org. 300 IN A 1.2.3.4"}, + }, + { + 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, + wantAnswer: []string{"org. 300 IN SOA ns1-06.azure-dns.com. azuredns-hostmaster.microsoft.com. 1 3600 300 2419200 300"}, + }, + { + qname: "badexample.com", + qtype: dns.TypeA, + wantRetCode: dns.RcodeServerFailure, + wantMsgRCode: dns.RcodeServerFailure, + }, + { + qname: "example.gov", + qtype: dns.TypeA, + wantAnswer: []string{"example.gov. 300 IN A 2.4.6.8"}, + }, + { + 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, + wantAnswer: []string{"cname.example.org. 300 IN CNAME example.org."}, + }, + { + 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, + wantAnswer: []string{"mail.example.org. 300 IN MX 10 mailserver.example.com."}, + }, + { + 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, + wantAnswer: []string{"txt.example.org. 300 IN TXT \"TXT for example.org\""}, + }, + } + + for ti, tc := range tests { + req := new(dns.Msg) + req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + code, err := demoAzure.ServeDNS(context.Background(), rec, req) + + if err != tc.expectedErr { + t.Fatalf("Test %d: Expected error %v, but got %v", ti, tc.expectedErr, err) + } + + if code != int(tc.wantRetCode) { + t.Fatalf("Test %d: Expected returned status code %s, but got %s", ti, dns.RcodeToString[tc.wantRetCode], dns.RcodeToString[code]) + } + + if tc.wantMsgRCode != rec.Msg.Rcode { + t.Errorf("Test %d: Unexpected msg status code. Want: %s, got: %s", ti, dns.RcodeToString[tc.wantMsgRCode], dns.RcodeToString[rec.Msg.Rcode]) + } + + if len(tc.wantAnswer) != len(rec.Msg.Answer) { + t.Errorf("Test %d: Unexpected number of Answers. Want: %d, got: %d", ti, len(tc.wantAnswer), len(rec.Msg.Answer)) + } else { + for i, gotAnswer := range rec.Msg.Answer { + if gotAnswer.String() != tc.wantAnswer[i] { + t.Errorf("Test %d: Unexpected answer.\nWant:\n\t%s\nGot:\n\t%s", ti, tc.wantAnswer[i], gotAnswer) + } + } + } + + if len(tc.wantNS) != len(rec.Msg.Ns) { + t.Errorf("Test %d: Unexpected NS number. Want: %d, got: %d", ti, len(tc.wantNS), len(rec.Msg.Ns)) + } else { + for i, ns := range rec.Msg.Ns { + got, ok := ns.(*dns.SOA) + if !ok { + t.Errorf("Test %d: Unexpected NS type. Want: SOA, got: %v", ti, reflect.TypeOf(got)) + } + if got.String() != tc.wantNS[i] { + t.Errorf("Test %d: Unexpected NS.\nWant: %v\nGot: %v", ti, tc.wantNS[i], got) + } + } + } + } +} diff --git a/ag_201_coredns/plugin/azure/setup.go b/ag_201_coredns/plugin/azure/setup.go new file mode 100644 index 0000000..6cabe05 --- /dev/null +++ b/ag_201_coredns/plugin/azure/setup.go @@ -0,0 +1,144 @@ +package azure + +import ( + "context" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/fall" + clog "github.com/coredns/coredns/plugin/pkg/log" + + publicAzureDNS "github.com/Azure/azure-sdk-for-go/profiles/latest/dns/mgmt/dns" + privateAzureDNS "github.com/Azure/azure-sdk-for-go/profiles/latest/privatedns/mgmt/privatedns" + azurerest "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/azure/auth" +) + +var log = clog.NewWithPlugin("azure") + +func init() { plugin.Register("azure", setup) } + +func setup(c *caddy.Controller) error { + env, keys, accessMap, fall, err := parse(c) + if err != nil { + return plugin.Error("azure", err) + } + ctx, cancel := context.WithCancel(context.Background()) + + 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) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + h.Next = next + return h + }) + c.OnShutdown(func() error { cancel(); return nil }) + return nil +} + +func parse(c *caddy.Controller) (auth.EnvironmentSettings, map[string][]string, map[string]string, fall.F, error) { + resourceGroupMapping := map[string][]string{} + accessMap := map[string]string{} + resourceGroupSet := map[string]struct{}{} + azureEnv := azurerest.PublicCloud + env := auth.EnvironmentSettings{Values: map[string]string{}} + + var fall fall.F + var access string + var resourceGroup string + var zoneName string + + for c.Next() { + args := c.RemainingArgs() + + for i := 0; i < len(args); i++ { + parts := strings.SplitN(args[i], ":", 2) + if len(parts) != 2 { + return env, resourceGroupMapping, accessMap, fall, c.Errf("invalid resource group/zone: %q", args[i]) + } + resourceGroup, zoneName = parts[0], parts[1] + if resourceGroup == "" || zoneName == "" { + return env, resourceGroupMapping, accessMap, fall, c.Errf("invalid resource group/zone: %q", args[i]) + } + if _, ok := resourceGroupSet[resourceGroup+zoneName]; ok { + return env, resourceGroupMapping, accessMap, fall, c.Errf("conflicting zone: %q", args[i]) + } + + resourceGroupSet[resourceGroup+zoneName] = struct{}{} + accessMap[resourceGroup+zoneName] = "public" + resourceGroupMapping[resourceGroup] = append(resourceGroupMapping[resourceGroup], zoneName) + } + + for c.NextBlock() { + switch c.Val() { + case "subscription": + if !c.NextArg() { + return env, resourceGroupMapping, accessMap, fall, c.ArgErr() + } + env.Values[auth.SubscriptionID] = c.Val() + case "tenant": + if !c.NextArg() { + return env, resourceGroupMapping, accessMap, fall, c.ArgErr() + } + env.Values[auth.TenantID] = c.Val() + case "client": + if !c.NextArg() { + return env, resourceGroupMapping, accessMap, fall, c.ArgErr() + } + env.Values[auth.ClientID] = c.Val() + case "secret": + if !c.NextArg() { + return env, resourceGroupMapping, accessMap, fall, c.ArgErr() + } + env.Values[auth.ClientSecret] = c.Val() + case "environment": + if !c.NextArg() { + return env, resourceGroupMapping, accessMap, fall, c.ArgErr() + } + var err error + if azureEnv, err = azurerest.EnvironmentFromName(c.Val()); err != nil { + return env, resourceGroupMapping, accessMap, fall, c.Errf("cannot set azure environment: %q", err.Error()) + } + case "fallthrough": + fall.SetZonesFromArgs(c.RemainingArgs()) + case "access": + if !c.NextArg() { + return env, resourceGroupMapping, accessMap, fall, c.ArgErr() + } + access = c.Val() + if access != "public" && access != "private" { + return env, resourceGroupMapping, accessMap, fall, c.Errf("invalid access value: can be public/private, found: %s", access) + } + accessMap[resourceGroup+zoneName] = access + default: + return env, resourceGroupMapping, accessMap, fall, c.Errf("unknown property: %q", c.Val()) + } + } + } + + env.Values[auth.Resource] = azureEnv.ResourceManagerEndpoint + env.Environment = azureEnv + return env, resourceGroupMapping, accessMap, fall, nil +} diff --git a/ag_201_coredns/plugin/azure/setup_test.go b/ag_201_coredns/plugin/azure/setup_test.go new file mode 100644 index 0000000..c6c26b1 --- /dev/null +++ b/ag_201_coredns/plugin/azure/setup_test.go @@ -0,0 +1,71 @@ +package azure + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + tests := []struct { + body string + expectedError bool + }{ + {`azure`, false}, + {`azure :`, true}, + {`azure resource_set:zone`, false}, + {`azure resource_set:zone { + tenant +}`, true}, + {`azure resource_set:zone { + tenant abc +}`, false}, + {`azure resource_set:zone { + client +}`, true}, + {`azure resource_set:zone { + client abc +}`, false}, + {`azure resource_set:zone { + subscription +}`, true}, + {`azure resource_set:zone { + subscription abc +}`, false}, + {`azure resource_set:zone { + foo +}`, true}, + {`azure resource_set:zone { + tenant tenant_id + client client_id + secret client_secret + subscription subscription_id + access public +}`, false}, + {`azure resource_set:zone { + fallthrough +}`, false}, + {`azure resource_set:zone { + environment AZUREPUBLICCLOUD + }`, false}, + {`azure resource_set:zone resource_set:zone { + fallthrough + }`, true}, + {`azure resource_set:zone,zone2 { + access private + }`, false}, + {`azure resource-set:zone { + access public + }`, false}, + {`azure resource-set:zone { + access foo + }`, true}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.body) + if _, _, _, _, err := parse(c); (err == nil) == test.expectedError { + t.Fatalf("Unexpected errors: %v in test: %d\n\t%s", err, i, test.body) + } + } +} diff --git a/ag_201_coredns/plugin/backend.go b/ag_201_coredns/plugin/backend.go new file mode 100644 index 0000000..a0217c9 --- /dev/null +++ b/ag_201_coredns/plugin/backend.go @@ -0,0 +1,40 @@ +package plugin + +import ( + "context" + + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// ServiceBackend defines a (dynamic) backend that returns a slice of service definitions. +type ServiceBackend interface { + // Services communicates with the backend to retrieve the service definitions. Exact indicates + // on exact match should be returned. + Services(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) + + // Reverse communicates with the backend to retrieve service definition based on a IP address + // instead of a name. I.e. a reverse DNS lookup. + Reverse(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error) + + // Lookup is used to find records else where. + Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) + + // Returns _all_ services that matches a certain name. + // Note: it does not implement a specific service. + Records(ctx context.Context, state request.Request, exact bool) ([]msg.Service, error) + + // IsNameError returns true if err indicated a record not found condition + IsNameError(err error) bool + + // Serial returns a SOA serial number to construct a SOA record. + Serial(state request.Request) uint32 + + // MinTTL returns the minimum TTL to be used in the SOA record. + MinTTL(state request.Request) uint32 +} + +// Options are extra options that can be specified for a lookup. +type Options struct{} diff --git a/ag_201_coredns/plugin/backend_lookup.go b/ag_201_coredns/plugin/backend_lookup.go new file mode 100644 index 0000000..0887bb4 --- /dev/null +++ b/ag_201_coredns/plugin/backend_lookup.go @@ -0,0 +1,560 @@ +package plugin + +import ( + "context" + "fmt" + "math" + "net" + + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/pkg/dnsutil" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// 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, truncated bool, err error) { + services, err := checkForApex(ctx, b, zone, state, opt) + if err != nil { + 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 { + // don't add it, and just continue + continue + } + if dnsutil.DuplicateCNAME(newRecord, previousRecords) { + continue + } + if dns.IsSubDomain(zone, dns.Fqdn(serv.Host)) { + state1 := state.NewWithQuestion(serv.Host, state.QType()) + state1.Zone = zone + 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. + if len(nextRecords) > 0 { + records = append(records, newRecord) + 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 { + continue + } + if m1.Truncated { + truncated = true + } + // Len(m1.Answer) > 0 here is well? + records = append(records, newRecord) + records = append(records, m1.Answer...) + continue + + case dns.TypeA: + if _, ok := dup[serv.Host]; !ok { + dup[serv.Host] = struct{}{} + records = append(records, serv.NewA(state.QName(), ip)) + } + + case dns.TypeAAAA: + // nada + } + } + 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, truncated bool, err error) { + services, err := checkForApex(ctx, b, zone, state, opt) + if err != nil { + return nil, false, err + } + + dup := make(map[string]struct{}) + + for _, serv := range services { + what, ip := serv.HostType() + + switch what { + case dns.TypeCNAME: + // 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 { + // don't add it, and just continue + continue + } + if dnsutil.DuplicateCNAME(newRecord, previousRecords) { + continue + } + if dns.IsSubDomain(zone, dns.Fqdn(serv.Host)) { + state1 := state.NewWithQuestion(serv.Host, state.QType()) + state1.Zone = zone + 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. + if len(nextRecords) > 0 { + records = append(records, newRecord) + 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 { + continue + } + if m1.Truncated { + truncated = true + } + // Len(m1.Answer) > 0 here is well? + records = append(records, newRecord) + records = append(records, m1.Answer...) + continue + // both here again + + case dns.TypeA: + // nada + + case dns.TypeAAAA: + if _, ok := dup[serv.Host]; !ok { + dup[serv.Host] = struct{}{} + records = append(records, serv.NewAAAA(state.QName(), ip)) + } + } + } + return records, truncated, nil +} + +// SRV returns SRV records from the Backend. +// If the Target is not a name but an IP address, a name is created on the fly. +func SRV(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, err error) { + services, err := b.Services(ctx, state, false, opt) + if err != nil { + return nil, nil, err + } + + dup := make(map[item]struct{}) + lookup := make(map[string]struct{}) + + // Looping twice to get the right weight vs priority. This might break because we may drop duplicate SRV records latter on. + w := make(map[int]int) + for _, serv := range services { + weight := 100 + if serv.Weight != 0 { + weight = serv.Weight + } + if _, ok := w[serv.Priority]; !ok { + w[serv.Priority] = weight + continue + } + w[serv.Priority] += weight + } + for _, serv := range services { + // Don't add the entry if the port is -1 (invalid). The kubernetes plugin uses port -1 when a service/endpoint + // does not have any declared ports. + if serv.Port == -1 { + continue + } + w1 := 100.0 / float64(w[serv.Priority]) + if serv.Weight == 0 { + w1 *= 100 + } else { + w1 *= float64(serv.Weight) + } + weight := uint16(math.Floor(w1)) + // weight should be at least 1 + if weight == 0 { + weight = 1 + } + + what, ip := serv.HostType() + + switch what { + case dns.TypeCNAME: + srv := serv.NewSRV(state.QName(), weight) + records = append(records, srv) + + if _, ok := lookup[srv.Target]; ok { + break + } + + lookup[srv.Target] = struct{}{} + + if !dns.IsSubDomain(zone, srv.Target) { + m1, e1 := b.Lookup(ctx, state, srv.Target, dns.TypeA) + if e1 == nil { + extra = append(extra, m1.Answer...) + } + + m1, e1 = b.Lookup(ctx, state, srv.Target, dns.TypeAAAA) + if e1 == 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 { + extra = append(extra, a) + } + } + } + break + } + // 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) + if e1 == nil { + extra = append(extra, addr...) + } + // TODO(miek): AAAA as well here. + + case dns.TypeA, dns.TypeAAAA: + addr := serv.Host + serv.Host = msg.Domain(serv.Key) + srv := serv.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 { + extra = append(extra, newAddress(serv, srv.Target, ip, what)) + } + } + } + return records, extra, nil +} + +// MX returns MX records from the Backend. If the Target is not a name but an IP address, a name is created on the fly. +func MX(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, err error) { + services, err := b.Services(ctx, state, false, opt) + if err != nil { + return nil, nil, err + } + + dup := make(map[item]struct{}) + lookup := make(map[string]struct{}) + for _, serv := range services { + if !serv.Mail { + continue + } + what, ip := serv.HostType() + switch what { + case dns.TypeCNAME: + mx := serv.NewMX(state.QName()) + records = append(records, mx) + if _, ok := lookup[mx.Mx]; ok { + break + } + + lookup[mx.Mx] = struct{}{} + + if !dns.IsSubDomain(zone, mx.Mx) { + m1, e1 := b.Lookup(ctx, state, mx.Mx, dns.TypeA) + if e1 == nil { + extra = append(extra, m1.Answer...) + } + + m1, e1 = b.Lookup(ctx, state, mx.Mx, dns.TypeAAAA) + if e1 == 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 { + extra = append(extra, a) + } + } + } + break + } + // Internal name + state1 := state.NewWithQuestion(mx.Mx, dns.TypeA) + addr, _, e1 := A(ctx, b, zone, state1, nil, opt) + if e1 == nil { + extra = append(extra, addr...) + } + // TODO(miek): AAAA as well here. + + case dns.TypeA, dns.TypeAAAA: + addr := serv.Host + serv.Host = msg.Domain(serv.Key) + mx := serv.NewMX(state.QName()) + + if ok := isDuplicate(dup, mx.Mx, "", mx.Preference); !ok { + records = append(records, mx) + } + // Fake port to be 0 for address... + if ok := isDuplicate(dup, serv.Host, addr, 0); !ok { + extra = append(extra, newAddress(serv, serv.Host, ip, what)) + } + } + } + return records, extra, nil +} + +// CNAME returns CNAME records from the backend or an error. +func CNAME(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) (records []dns.RR, err error) { + services, err := b.Services(ctx, state, true, opt) + if err != nil { + return nil, err + } + + if len(services) > 0 { + serv := services[0] + if ip := net.ParseIP(serv.Host); ip == nil { + records = append(records, serv.NewCNAME(state.QName(), serv.Host)) + } + } + return records, nil +} + +// 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, truncated bool, err error) { + services, err := b.Services(ctx, state, false, opt) + if err != nil { + 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 { + // don't add it, and just continue + continue + } + if dnsutil.DuplicateCNAME(newRecord, previousRecords) { + continue + } + if dns.IsSubDomain(zone, dns.Fqdn(serv.Host)) { + state1 := state.NewWithQuestion(serv.Host, state.QType()) + state1.Zone = zone + 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 { + records = append(records, newRecord) + records = append(records, nextRecords...) + } + } + 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 { + continue + } + // Len(m1.Answer) > 0 here is well? + records = append(records, newRecord) + records = append(records, m1.Answer...) + continue + + case dns.TypeTXT: + if _, ok := dup[serv.Text]; !ok { + dup[serv.Text] = struct{}{} + records = append(records, serv.NewTXT(state.QName())) + } + } + } + + return records, truncated, nil +} + +// PTR returns the PTR records from the backend, only services that have a domain name as host are included. +func PTR(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) (records []dns.RR, err error) { + services, err := b.Reverse(ctx, state, true, opt) + if err != nil { + return nil, err + } + + dup := make(map[string]struct{}) + + for _, serv := range services { + if ip := net.ParseIP(serv.Host); ip == nil { + if _, ok := dup[serv.Host]; !ok { + dup[serv.Host] = struct{}{} + records = append(records, serv.NewPTR(state.QName(), serv.Host)) + } + } + } + return records, nil +} + +// 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... + old := state.QName() + + state.Clear() + state.Req.Question[0].Name = dnsutil.Join("ns.dns.", zone) + services, err := b.Services(ctx, state, false, opt) + if err != nil { + return nil, nil, err + } + // ... and reset + state.Req.Question[0].Name = old + + seen := map[string]bool{} + + for _, serv := range services { + what, ip := serv.HostType() + switch what { + case dns.TypeCNAME: + return nil, nil, fmt.Errorf("NS record must be an IP address: %s", serv.Host) + + case dns.TypeA, dns.TypeAAAA: + serv.Host = msg.Domain(serv.Key) + ns := serv.NewNS(state.QName()) + extra = append(extra, newAddress(serv, ns.Ns, ip, what)) + if _, ok := seen[ns.Ns]; ok { + continue + } + seen[ns.Ns] = true + records = append(records, ns) + } + } + return records, extra, nil +} + +// 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 + } + + header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: ttl, Class: dns.ClassINET} + + Mbox := dnsutil.Join(hostmaster, zone) + Ns := dnsutil.Join("ns.dns", zone) + + soa := &dns.SOA{Hdr: header, + Mbox: Mbox, + Ns: Ns, + Serial: b.Serial(state), + Refresh: 7200, + Retry: 1800, + Expire: 86400, + Minttl: minTTL, + } + return []dns.RR{soa}, nil +} + +// BackendError writes an error response to the client. +func BackendError(ctx context.Context, b ServiceBackend, zone string, rcode int, state request.Request, err error, opt Options) (int, error) { + m := new(dns.Msg) + m.SetRcode(state.Req, rcode) + m.Authoritative = true + m.Ns, _ = SOA(ctx, b, zone, state, opt) + + state.W.WriteMsg(m) + // Return success as the rcode to signal we have written to the client. + return dns.RcodeSuccess, err +} + +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 { + return &dns.A{Hdr: hdr, A: ip} + } + // Should always be dns.TypeAAAA + return &dns.AAAA{Hdr: hdr, AAAA: ip} +} + +// checkForApex checks the special apex.dns directory for records that will be returned as A or AAAA. +func checkForApex(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) ([]msg.Service, error) { + if state.Name() != zone { + return b.Services(ctx, state, false, opt) + } + + // If the zone name itself is queried we fake the query to search for a special entry + // this is equivalent to the NS search code. + old := state.QName() + state.Clear() + state.Req.Question[0].Name = dnsutil.Join("apex.dns", zone) + + services, err := b.Services(ctx, state, false, opt) + if err == nil { + state.Req.Question[0].Name = old + return services, err + } + + state.Req.Question[0].Name = old + return b.Services(ctx, state, false, opt) +} + +// item holds records. +type item struct { + name string // name of the record (either owner or something else unique). + port uint16 // port of the record (used for address records, A and AAAA). + addr string // address of the record (A and AAAA). +} + +// isDuplicate uses m to see if the combo (name, addr, port) already exists. If it does +// not exist already IsDuplicate will also add the record to the map. +func isDuplicate(m map[item]struct{}, name, addr string, port uint16) bool { + if addr != "" { + _, ok := m[item{name, 0, addr}] + if !ok { + m[item{name, 0, addr}] = struct{}{} + } + return ok + } + _, ok := m[item{name, port, ""}] + if !ok { + m[item{name, port, ""}] = struct{}{} + } + return ok +} + +const hostmaster = "hostmaster" diff --git a/ag_201_coredns/plugin/bind/README.md b/ag_201_coredns/plugin/bind/README.md new file mode 100644 index 0000000..a911218 --- /dev/null +++ b/ag_201_coredns/plugin/bind/README.md @@ -0,0 +1,113 @@ +# bind + +## Name + +*bind* - overrides the host to which the server should bind. + +## Description + +Normally, the listener binds to the wildcard host. However, you may want the listener to bind to +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 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 serveral IP addresses, CoreDNS will listen on all of the interface IP addresses (including IPv4 and IPv6), except for IPv6 link-local addresses on that interface. + +## Syntax + +In its basic form, a simple bind uses this syntax: + +~~~ txt +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|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): + +~~~ corefile +. { + bind 127.0.0.1 +} +~~~ + +To allow processing DNS requests only local host on both IPv4 and IPv6 stacks, use the syntax: + +~~~ corefile +. { + bind 127.0.0.1 ::1 +} +~~~ + +If the configuration comes up with several *bind* plugins, all addresses are consolidated together: +The following sample is equivalent to the preceding: + +~~~ corefile +. { + bind 127.0.0.1 + bind ::1 +} +~~~ + +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 + +### 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`: + +``` +a.bad.example.com { + bind 127.0.0.1 + forward . 1.2.3.4 +} + +bad.example.com { + forward . 5.6.7.8 +} +``` + +Also on MacOS there is an (open) bug where this doesn't work properly. See + for details, but no solution. diff --git a/ag_201_coredns/plugin/bind/bind.go b/ag_201_coredns/plugin/bind/bind.go new file mode 100644 index 0000000..cada8fa --- /dev/null +++ b/ag_201_coredns/plugin/bind/bind.go @@ -0,0 +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" +) + +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/ag_201_coredns/plugin/bind/log_test.go b/ag_201_coredns/plugin/bind/log_test.go new file mode 100644 index 0000000..4ee3ffc --- /dev/null +++ b/ag_201_coredns/plugin/bind/log_test.go @@ -0,0 +1,5 @@ +package bind + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/bind/setup.go b/ag_201_coredns/plugin/bind/setup.go new file mode 100644 index 0000000..10fe4a9 --- /dev/null +++ b/ag_201_coredns/plugin/bind/setup.go @@ -0,0 +1,111 @@ +package bind + +import ( + "errors" + "fmt" + "net" + + "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() { + 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 _, ip := range ips { + if !isIn(ip, except) { + all = append(all, ip) + } + } + } + + 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 { + if ipnet.IP.To4() != nil || (!ipnet.IP.IsLinkLocalMulticast() && !ipnet.IP.IsLinkLocalUnicast()) { + all = append(all, ipnet.IP.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 +} + +// isIn checks if a string array contains an element +func isIn(s string, list []string) bool { + is := false + for _, l := range list { + if s == l { + is = true + } + } + return is +} diff --git a/ag_201_coredns/plugin/bind/setup_test.go b/ag_201_coredns/plugin/bind/setup_test.go new file mode 100644 index 0000000..e8c87b8 --- /dev/null +++ b/ag_201_coredns/plugin/bind/setup_test.go @@ -0,0 +1,47 @@ +package bind + +import ( + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" +) + +func TestSetup(t *testing.T) { + for i, test := range []struct { + config string + expected []string + failing bool + }{ + {`bind 1.2.3.4`, []string{"1.2.3.4"}, false}, + {`bind`, nil, true}, + {`bind 1.2.3.invalid`, nil, true}, + {`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) + if err != nil { + if !test.failing { + t.Fatalf("Test %d, expected no errors, but got: %v", i, err) + } + continue + } + if test.failing { + t.Fatalf("Test %d, expected to failed but did not, returned values", i) + } + cfg := dnsserver.GetConfig(c) + if len(cfg.ListenHosts) != len(test.expected) { + t.Errorf("Test %d : expected the config's ListenHosts size to be %d, was %d", i, len(test.expected), len(cfg.ListenHosts)) + continue + } + for i, v := range test.expected { + if got, want := cfg.ListenHosts[i], v; got != want { + t.Errorf("Test %d : expected the config's ListenHost to be %s, was %s", i, want, got) + } + } + } +} diff --git a/ag_201_coredns/plugin/bufsize/README.md b/ag_201_coredns/plugin/bufsize/README.md new file mode 100644 index 0000000..56a9ddd --- /dev/null +++ b/ag_201_coredns/plugin/bufsize/README.md @@ -0,0 +1,39 @@ +# bufsize +## Name +*bufsize* - sizes EDNS0 buffer size to prevent IP fragmentation. + +## Description +*bufsize* limits a requester's UDP payload size. +It prevents IP fragmentation, mitigating certain DNS vulnerabilities. +This will only affect queries that have an OPT RR. + +## Syntax +```txt +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. +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 + forward . 172.31.0.10 + log +} +``` + +Enable limiting the buffer size as an authoritative nameserver: +```corefile +. { + bufsize 512 + file db.example.org + log +} +``` + +## 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. diff --git a/ag_201_coredns/plugin/bufsize/bufsize.go b/ag_201_coredns/plugin/bufsize/bufsize.go new file mode 100644 index 0000000..00556c2 --- /dev/null +++ b/ag_201_coredns/plugin/bufsize/bufsize.go @@ -0,0 +1,27 @@ +// Package bufsize implements a plugin that clamps EDNS0 buffer size preventing packet fragmentation. +package bufsize + +import ( + "context" + + "github.com/coredns/coredns/plugin" + + "github.com/miekg/dns" +) + +// Bufsize implements bufsize plugin. +type Bufsize struct { + Next plugin.Handler + Size int +} + +// 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 && int(option.UDPSize()) > buf.Size { + option.SetUDPSize(uint16(buf.Size)) + } + return plugin.NextOrFailure(buf.Name(), buf.Next, ctx, w, r) +} + +// Name implements the Handler interface. +func (buf Bufsize) Name() string { return "bufsize" } diff --git a/ag_201_coredns/plugin/bufsize/bufsize_test.go b/ag_201_coredns/plugin/bufsize/bufsize_test.go new file mode 100644 index 0000000..eb267dd --- /dev/null +++ b/ag_201_coredns/plugin/bufsize/bufsize_test.go @@ -0,0 +1,102 @@ +package bufsize + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/plugin/whoami" + + "github.com/miekg/dns" +) + +func TestBufsize(t *testing.T) { + const maxBufSize = 1024 + + 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 + } + + 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) + + // 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) + + // WHEN + // request is processed + _, err := p.ServeDNS(context.Background(), &test.ResponseWriter{}, r) + + // 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) + + // 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/ag_201_coredns/plugin/bufsize/setup.go b/ag_201_coredns/plugin/bufsize/setup.go new file mode 100644 index 0000000..7ac602d --- /dev/null +++ b/ag_201_coredns/plugin/bufsize/setup.go @@ -0,0 +1,51 @@ +package bufsize + +import ( + "strconv" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("bufsize", setup) } + +func setup(c *caddy.Controller) error { + bufsize, err := parse(c) + if err != nil { + return plugin.Error("bufsize", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return Bufsize{Next: next, Size: bufsize} + }) + + return nil +} + +func parse(c *caddy.Controller) (int, error) { + const defaultBufSize = 512 + for c.Next() { + args := c.RemainingArgs() + switch len(args) { + case 0: + // Nothing specified; use 512 as default + return defaultBufSize, nil + case 1: + // Specified value is needed to verify + bufsize, err := strconv.Atoi(args[0]) + if err != nil { + return -1, plugin.Error("bufsize", c.ArgErr()) + } + // Follows RFC 6891 + if bufsize < 512 || bufsize > 4096 { + return -1, plugin.Error("bufsize", c.ArgErr()) + } + return bufsize, nil + default: + // Only 1 argument is acceptable + return -1, plugin.Error("bufsize", c.ArgErr()) + } + } + return -1, plugin.Error("bufsize", c.ArgErr()) +} diff --git a/ag_201_coredns/plugin/bufsize/setup_test.go b/ag_201_coredns/plugin/bufsize/setup_test.go new file mode 100644 index 0000000..bb10302 --- /dev/null +++ b/ag_201_coredns/plugin/bufsize/setup_test.go @@ -0,0 +1,46 @@ +package bufsize + +import ( + "strings" + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupBufsize(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedData int + expectedErrContent string // substring from the expected error. Empty for positive cases. + }{ + {`bufsize`, false, 512, ""}, + {`bufsize "1232"`, false, 1232, ""}, + {`bufsize "5000"`, true, -1, "plugin"}, + {`bufsize "512 512"`, true, -1, "plugin"}, + {`bufsize "abc123"`, true, -1, "plugin"}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + bufsize, err := parse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input) + } + + if err != nil { + if !test.shouldErr { + t.Errorf("Test %d: Error found for input %s. Error: %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) + } + } + + if !test.shouldErr && bufsize != test.expectedData { + t.Errorf("Test %d: Bufsize not correctly set for input %s. Expected: %d, actual: %d", i, test.input, test.expectedData, bufsize) + } + } +} diff --git a/ag_201_coredns/plugin/cache/README.md b/ag_201_coredns/plugin/cache/README.md new file mode 100644 index 0000000..562f5bd --- /dev/null +++ b/ag_201_coredns/plugin/cache/README.md @@ -0,0 +1,139 @@ +# cache + +## Name + +*cache* - enables a frontend cache. + +## Description + +With *cache* enabled, all records except zone transfers and metadata records will be cached for up to +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. + +This plugin can only be used once per Server Block. + +## Syntax + +~~~ txt +cache [TTL] [ZONES...] +~~~ + +* **TTL** max TTL in seconds. If not specified, the maximum TTL will be used, which is 3600 for + NOERROR responses and 1800 for denial of existence ones. + Setting a TTL of 300: `cache 300` would cache records up to 300 seconds. +* **ZONES** zones it should cache for. If empty, the zones from the configuration block are used. + +Each element in the cache is cached according to its TTL (with **TTL** as the max). +A cache is divided into 256 shards, each holding up to 39 items by default - for a total size +of 256 * 39 = 9984 items. + +If you want more control: + +~~~ txt +cache [TTL] [ZONES...] { + success CAPACITY [TTL] [MINTTL] + denial CAPACITY [TTL] [MINTTL] + prefetch AMOUNT [[DURATION] [PERCENTAGE%]] + serve_stale [DURATION] [REFRESH_MODE] + servfail DURATION + disable success|denial [ZONES...] +} +~~~ + +* **TTL** and **ZONES** as above. +* `success`, override the settings for caching successful responses. **CAPACITY** indicates the maximum + number of packets we cache before we start evicting (*randomly*). **TTL** overrides the cache maximum TTL. + **MINTTL** overrides the cache minimum TTL (default 5), which can be useful to limit queries to the backend. +* `denial`, override the settings for caching denial of existence responses. **CAPACITY** indicates the maximum + number of packets we cache before we start evicting (LRU). **TTL** overrides the cache maximum TTL. + **MINTTL** overrides the cache minimum TTL (default 5), which can be useful to limit queries to the backend. + There is a third category (`error`) but those responses are never cached. +* `prefetch` will prefetch popular items when they are about to be expunged from the cache. + Popular means **AMOUNT** queries have been seen with no gaps of **DURATION** or more between them. + **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 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. + +## Capacity and Eviction + +If **CAPACITY** _is not_ specified, the default cache size is 9984 per cache. The minimum allowed cache size is 1024. +If **CAPACITY** _is_ specified, the actual cache size used will be rounded down to the nearest number divisible by 256 (so all shards are equal in size). + +Eviction is done per shard. In effect, when a shard reaches capacity, items are evicted from that shard. +Since shards don't fill up perfectly evenly, evictions will occur before the entire cache reaches full capacity. +Each shard capacity is equal to the total cache size / number of shards (256). Eviction is random, not TTL based. +Entries with 0 TTL will remain in the cache until randomly evicted when the shard reaches capacity. + +## Metrics + +If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported: + +* `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. + +## Examples + +Enable caching for all zones, but cap everything to a TTL of 10 seconds: + +~~~ corefile +. { + cache 10 + whoami +} +~~~ + +Proxy to Google Public DNS and only cache responses for example.org (or below). + +~~~ corefile +. { + forward . 8.8.8.8:53 + cache example.org +} +~~~ + +Enable caching for `example.org`, keep a positive cache size of 5000 and a negative cache size of 2500: + +~~~ corefile +example.org { + cache { + success 5000 + denial 2500 + } +} +~~~ + +Enable caching for `example.org`, but do not cache denials in `sub.example.org`: + +~~~ corefile +example.org { + cache { + disable denial sub.example.org + } +} +~~~ \ No newline at end of file diff --git a/ag_201_coredns/plugin/cache/cache.go b/ag_201_coredns/plugin/cache/cache.go new file mode 100644 index 0000000..b476793 --- /dev/null +++ b/ag_201_coredns/plugin/cache/cache.go @@ -0,0 +1,299 @@ +// Package cache implements a cache. +package cache + +import ( + "hash/fnv" + "net" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/cache" + "github.com/coredns/coredns/plugin/pkg/dnsutil" + "github.com/coredns/coredns/plugin/pkg/response" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Cache is a plugin that looks up responses in a cache and caches replies. +// It has a success and a denial of existence cache. +type Cache struct { + Next plugin.Handler + Zones []string + + zonesMetricLabel string + viewMetricLabel string + + ncache *cache.Cache + ncap int + nttl time.Duration + minnttl time.Duration + + pcache *cache.Cache + pcap int + pttl time.Duration + minpttl time.Duration + failttl time.Duration // TTL for caching SERVFAIL responses + + // Prefetch. + prefetch int + duration time.Duration + percentage int + + // Stale serve + staleUpTo time.Duration + verifyStale bool + + // Positive/negative zone exceptions + pexcept []string + nexcept []string + + // Testing. + now func() time.Time +} + +// New returns an initialized Cache with default settings. It's up to the +// caller to set the Next handler. +func New() *Cache { + return &Cache{ + Zones: []string{"."}, + pcap: defaultCap, + pcache: cache.New(defaultCap), + pttl: maxTTL, + minpttl: minTTL, + ncap: defaultCap, + ncache: cache.New(defaultCap), + nttl: maxNTTL, + minnttl: minNTTL, + failttl: minNTTL, + prefetch: 0, + duration: 1 * time.Minute, + percentage: 10, + now: time.Now, + } +} + +// 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) { + // We don't store truncated responses. + if m.Truncated { + return false, 0 + } + // Nor errors or Meta or Update. + if t == response.OtherError || t == response.Meta || t == response.Update { + return false, 0 + } + + return true, hash(qname, m.Question[0].Qtype) +} + +func hash(qname string, qtype uint16) uint64 { + h := fnv.New64() + h.Write([]byte{byte(qtype >> 8)}) + h.Write([]byte{byte(qtype)}) + h.Write([]byte(qname)) + return h.Sum64() +} + +func computeTTL(msgTTL, minTTL, maxTTL time.Duration) time.Duration { + ttl := msgTTL + if ttl < minTTL { + ttl = minTTL + } + if ttl > maxTTL { + ttl = maxTTL + } + return ttl +} + +// ResponseWriter is a response writer that caches the reply message. +type ResponseWriter struct { + dns.ResponseWriter + *Cache + state request.Request + server string // Server handling the request. + + do bool // When true the original request had the DO 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 +// prefetch requests. It ensures RemoteAddr() can be called even after the +// original connection has already been closed. +func newPrefetchResponseWriter(server string, state request.Request, c *Cache) *ResponseWriter { + // Resolve the address now, the connection might be already closed when the + // actual prefetch request is made. + addr := state.W.RemoteAddr() + // The protocol of the client triggering a cache prefetch doesn't matter. + // The address type is used by request.Proto to determine the response size, + // and using TCP ensures the message isn't unnecessarily truncated. + if u, ok := addr.(*net.UDPAddr); ok { + addr = &net.TCPAddr{IP: u.IP, Port: u.Port, Zone: u.Zone} + } + + return &ResponseWriter{ + ResponseWriter: state.W, + Cache: c, + state: state, + server: server, + prefetch: true, + remoteAddr: addr, + } +} + +// RemoteAddr implements the dns.ResponseWriter interface. +func (w *ResponseWriter) RemoteAddr() net.Addr { + if w.remoteAddr != nil { + return w.remoteAddr + } + return w.ResponseWriter.RemoteAddr() +} + +// WriteMsg implements the dns.ResponseWriter interface. +func (w *ResponseWriter) WriteMsg(res *dns.Msg) error { + 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) + + msgTTL := dnsutil.MinimalTTL(res, mt) + var duration time.Duration + if mt == response.NameError || mt == response.NoData { + duration = computeTTL(msgTTL, w.minnttl, w.nttl) + } else if mt == response.ServerError { + duration = w.failttl + } else { + duration = computeTTL(msgTTL, w.minpttl, w.pttl) + } + + if hasKey && duration > 0 { + if w.state.Match(res) { + w.set(res, key, mt, duration) + 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, w.zonesMetricLabel, w.viewMetricLabel).Inc() + } + } + + if w.prefetch { + 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) + + 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 + } + + return w.ResponseWriter.WriteMsg(res) +} + +func (w *ResponseWriter) set(m *dns.Msg, key uint64, mt response.Type, duration time.Duration) { + // duration is expected > 0 + // 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) + 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) + 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 + default: + log.Warningf("Caching called with unknown classification: %d", mt) + } +} + +// Write implements the dns.ResponseWriter interface. +func (w *ResponseWriter) Write(buf []byte) (int, error) { + log.Warning("Caching called with Write: not caching reply") + if w.prefetch { + return 0, nil + } + n, err := w.ResponseWriter.Write(buf) + 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 + maxNTTL = dnsutil.MaximumDefaulTTL / 2 + minNTTL = dnsutil.MinimalDefaultTTL + + defaultCap = 10000 // default capacity of the cache. + + // Success is the class for caching positive caching. + Success = "success" + // Denial is the class defined for negative caching. + Denial = "denial" +) diff --git a/ag_201_coredns/plugin/cache/cache_test.go b/ag_201_coredns/plugin/cache/cache_test.go new file mode 100644 index 0000000..0328c84 --- /dev/null +++ b/ag_201_coredns/plugin/cache/cache_test.go @@ -0,0 +1,696 @@ +package cache + +import ( + "context" + "fmt" + "testing" + "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" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +type cacheTestCase struct { + test.Case // the expected message coming "out" of cache + in test.Case // the test message going "in" to cache + AuthenticatedData bool + RecursionAvailable bool + Truncated bool + shouldCache bool +} + +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."), + }, + }, + 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."), + }, + }, + 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."), + }, + }, + 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="), + }, + }, + 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.")}, + }, + 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"), + }, + }, + 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"), + }, + }, + shouldCache: true, + }, + { + RecursionAvailable: true, + Case: test.Case{ + Rcode: dns.RcodeServerFailure, + Qname: "example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{}, + }, + in: test.Case{ + Rcode: dns.RcodeServerFailure, + Qname: "example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{}, + }, + shouldCache: true, + }, + { + RecursionAvailable: true, + Case: test.Case{ + Rcode: dns.RcodeNotImplemented, + Qname: "example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{}, + }, + in: test.Case{ + Rcode: dns.RcodeNotImplemented, + Qname: "example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{}, + }, + 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="), + }, + }, + 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="), + }, + }, + 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="), + }, + }, + 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="), + }, + }, + shouldCache: true, + }, + { + 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"), + }, + }, + Case: test.Case{}, + shouldCache: false, + }, + { + 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"), + }, + }, + Case: test.Case{}, + shouldCache: false, + }, + { + 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"), + }, + }, + Case: 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{ + 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"), + }, + }, + Case: 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, + }, +} + +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} + crr.nexcept = []string{"neg-disabled.example.org."} + crr.pexcept = []string{"pos-disabled.example.org."} + + 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 n, tc := range cacheTestCases { + m := tc.in.Msg() + m = cacheMsg(m, tc) + + state := request.Request{W: &test.ResponseWriter{}, Req: m} + + mt, _ := response.Typify(m, utc) + valid, k := key(state.Name(), m, mt) + + if valid { + crr.set(m, k, mt, c.pttl) + } + + i := c.getIgnoreTTL(time.Now().UTC(), state, "dns://:53") + ok := i != nil + + if !tc.shouldCache && ok { + t.Errorf("Test %d: Cached message that should not have been cached: %s", n, state.Name()) + continue + } + if tc.shouldCache && !ok { + t.Errorf("Test %d: Did not cache message that should have been cached: %s", n, state.Name()) + continue + } + + if ok { + resp := i.toMsg(m, time.Now().UTC(), state.Do(), m.AuthenticatedData) + + if err := test.Header(tc.Case, resp); err != nil { + t.Logf("Cache %v", resp) + t.Error(err) + continue + } + + if err := test.Section(tc.Case, test.Answer, resp.Answer); err != nil { + t.Logf("Cache %v -- %v", test.Answer, resp.Answer) + t.Error(err) + } + if err := test.Section(tc.Case, test.Ns, resp.Ns); err != nil { + t.Error(err) + } + if err := test.Section(tc.Case, test.Extra, resp.Extra); err != nil { + t.Error(err) + } + } + } +} + +func TestCacheZeroTTL(t *testing.T) { + c := New() + c.minpttl = 0 + c.minnttl = 0 + c.Next = ttlBackend(0) + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + ctx := context.TODO() + + c.ServeDNS(ctx, &test.ResponseWriter{}, req) + if c.pcache.Len() != 0 { + t.Errorf("Msg with 0 TTL should not have been cached") + } + if c.ncache.Len() != 0 { + t.Errorf("Msg with 0 TTL should not have been cached") + } +} + +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) + + 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.staleUpTo = 1 * time.Hour + c.ServeDNS(ctx, rec, req) + if c.pcache.Len() != 1 { + t.Fatalf("Msg with > 0 TTL should have been cached") + } + + // No more backend resolutions, just from cache if available. + c.Next = plugin.HandlerFunc(func(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) { + return 255, nil // Below, a 255 means we tried querying upstream. + }) + + tests := []struct { + name string + futureMinutes int + expectedResult int + }{ + {"cached.org.", 30, 0}, + {"cached.org.", 60, 0}, + {"cached.org.", 70, 255}, + + {"notcached.org.", 30, 255}, + {"notcached.org.", 60, 255}, + {"notcached.org.", 70, 255}, + } + + 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) } + r := req.Copy() + r.SetQuestion(tt.name, dns.TypeA) + if ret, _ := c.ServeDNS(ctx, rec, r); ret != tt.expectedResult { + t.Errorf("Test %d: expecting %v; got %v", i, tt.expectedResult, ret) + } + } +} + +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) } + + if tt.upstreamRCode == dns.RcodeSuccess { + c.Next = ttlBackend(tt.upstreamTtl) + } else if tt.upstreamRCode == dns.RcodeServerFailure { + // Make upstream fail, should now rely on cache during the c.staleUpTo period + c.Next = servFailBackend(tt.upstreamTtl) + } else if tt.upstreamRCode == dns.RcodeNameError { + c.Next = nxDomainBackend(tt.upstreamTtl) + } else { + 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 + } + if ret == 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) + } + } else if ret == 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 + c.Next = nxDomainBackend(60) + + req := new(dns.Msg) + qname := "cached.org." + req.SetQuestion(qname, dns.TypeA) + ctx := context.TODO() + + // Add an entry to Negative Cache": cached.org. = NXDOMAIN + expectedResult := dns.RcodeNameError + if ret, _ := c.ServeDNS(ctx, &test.ResponseWriter{}, req); ret != expectedResult { + t.Errorf("Test 0 Negative Cache Population: expecting %v; got %v", expectedResult, ret) + } + + // Confirm item was added to negative cache and not to positive cache + if c.ncache.Len() == 0 { + t.Errorf("Test 0 Negative Cache Population: item not added to negative cache") + } + if c.pcache.Len() != 0 { + t.Errorf("Test 0 Negative Cache Population: item added to positive cache") + } + + // Set the Backend to return non-cachable errors only + c.Next = plugin.HandlerFunc(func(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) { + return 255, nil // Below, a 255 means we tried querying upstream. + }) + + // Confirm we get the NXDOMAIN from the negative cache, not the error form the backend + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + req = new(dns.Msg) + req.SetQuestion(qname, dns.TypeA) + expectedResult = dns.RcodeNameError + if c.ServeDNS(ctx, rec, req); rec.Rcode != expectedResult { + t.Errorf("Test 1 NXDOMAIN from Negative Cache: expecting %v; got %v", expectedResult, rec.Rcode) + } + + // Jump into the future beyond when the negative cache item would go stale + // but before the item goes rotten (exceeds serve stale time) + c.now = func() time.Time { return time.Now().Add(time.Duration(5) * time.Minute) } + + // Set Backend to return a positive NOERROR + A record response + c.Next = BackendHandler() + + // Make a query for the stale cache item + rec = dnstest.NewRecorder(&test.ResponseWriter{}) + req = new(dns.Msg) + req.SetQuestion(qname, dns.TypeA) + expectedResult = dns.RcodeNameError + if c.ServeDNS(ctx, rec, req); rec.Rcode != expectedResult { + t.Errorf("Test 2 NOERROR from Backend: expecting %v; got %v", expectedResult, rec.Rcode) + } + + // Confirm that prefetch removes the negative cache item. + waitFor := 3 + for i := 1; i <= waitFor; i++ { + if c.ncache.Len() != 0 { + if i == waitFor { + t.Errorf("Test 2 NOERROR from Backend: item still exists in negative cache") + } + time.Sleep(time.Second) + continue + } + } + + // Confirm that positive cache has the item + if c.pcache.Len() != 1 { + t.Errorf("Test 2 NOERROR from Backend: item missing from positive cache") + } + + // Backend - Give error only + c.Next = plugin.HandlerFunc(func(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) { + return 255, nil // Below, a 255 means we tried querying upstream. + }) + + // Query again, expect that positive cache entry is not masked by a negative cache entry + rec = dnstest.NewRecorder(&test.ResponseWriter{}) + req = new(dns.Msg) + req.SetQuestion(qname, dns.TypeA) + expectedResult = dns.RcodeSuccess + if ret, _ := c.ServeDNS(ctx, rec, req); ret != expectedResult { + t.Errorf("Test 3 NOERROR from Cache: expecting %v; got %v", expectedResult, ret) + } +} + +func BenchmarkCacheResponse(b *testing.B) { + c := New() + c.prefetch = 1 + c.Next = BackendHandler() + + ctx := context.TODO() + + reqs := make([]*dns.Msg, 5) + for i, q := range []string{"example1", "example2", "a", "b", "ddd"} { + reqs[i] = new(dns.Msg) + reqs[i].SetQuestion(q+".example.org.", dns.TypeA) + } + + b.StartTimer() + + j := 0 + for i := 0; i < b.N; i++ { + req := reqs[j] + c.ServeDNS(ctx, &test.ResponseWriter{}, req) + j = (j + 1) % 5 + } +} + +func BackendHandler() 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 = true + m.RecursionAvailable = true + + owner := m.Question[0].Name + m.Answer = []dns.RR{test.A(owner + " 303 IN A 127.0.0.53")} + + w.WriteMsg(m) + return dns.RcodeSuccess, nil + }) +} + +func nxDomainBackend(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.MsgHdr.Rcode = dns.RcodeNameError + w.WriteMsg(m) + return dns.RcodeNameError, nil + }) +} + +func ttlBackend(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.Answer = []dns.RR{test.A(fmt.Sprintf("example.org. %d IN A 127.0.0.53", ttl))} + w.WriteMsg(m) + return dns.RcodeSuccess, nil + }) +} + +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.MsgHdr.Rcode = dns.RcodeServerFailure + w.WriteMsg(m) + return dns.RcodeServerFailure, nil + }) +} + +func TestComputeTTL(t *testing.T) { + tests := []struct { + msgTTL time.Duration + minTTL time.Duration + maxTTL time.Duration + expectedTTL time.Duration + }{ + {1800 * time.Second, 300 * time.Second, 3600 * time.Second, 1800 * time.Second}, + {299 * time.Second, 300 * time.Second, 3600 * time.Second, 300 * time.Second}, + {299 * time.Second, 0 * time.Second, 3600 * time.Second, 299 * time.Second}, + {3601 * time.Second, 300 * time.Second, 3600 * time.Second, 3600 * time.Second}, + } + for i, test := range tests { + ttl := computeTTL(test.msgTTL, test.minTTL, test.maxTTL) + if ttl != test.expectedTTL { + t.Errorf("Test %v: Expected ttl %v but found: %v", i, test.expectedTTL, ttl) + } + } +} + +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) + + // 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) + i, _ := c.pcache.Get(k) + if i.(*item).wildcard != wildcard { + t.Errorf("expected wildcard reponse 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) + } +} + +// wildcardMetadataBackend mocks a backend that reponds 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/ag_201_coredns/plugin/cache/dnssec.go b/ag_201_coredns/plugin/cache/dnssec.go new file mode 100644 index 0000000..cf90803 --- /dev/null +++ b/ag_201_coredns/plugin/cache/dnssec.go @@ -0,0 +1,46 @@ +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 { + j := 0 + 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 + } + j++ + } + return rs[:j] +} diff --git a/ag_201_coredns/plugin/cache/dnssec_test.go b/ag_201_coredns/plugin/cache/dnssec_test.go new file mode 100644 index 0000000..a746387 --- /dev/null +++ b/ag_201_coredns/plugin/cache/dnssec_test.go @@ -0,0 +1,117 @@ +package cache + +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 TestResponseWithDNSSEC(t *testing.T) { + // We do 2 queries, one where we want non-dnssec and one with dnssec and check the responses in each of them + var tcs = []test.Case{ + { + Qname: "invent.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("invent.example.org. 1781 IN CNAME leptone.example.org."), + test.A("leptone.example.org. 1781 IN A 195.201.182.103"), + }, + }, + { + Qname: "invent.example.org.", Qtype: dns.TypeA, + 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+"), + test.A("leptone.example.org. 1781 IN A 195.201.182.103"), + test.RRSIG("leptone.example.org. 1781 IN RRSIG A 8 3 1800 20201012093630 20200912083827 57411 example.org. eLuSOkLAzm/WIOpaZD3/4TfvKP1HAFzjkis9LIJSRVpQt307dm9WY9"), + }, + }, + } + + c := New() + c.Next = dnssecHandler() + + for i, tc := range tcs { + 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) + } + } + + // now do the reverse + c = New() + c.Next = dnssecHandler() + + for i, tc := range []test.Case{tcs[1], tcs[0]} { + m := tc.Msg() + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + c.ServeDNS(context.TODO(), rec, m) + if err := test.Section(tc, test.Answer, rec.Msg.Answer); err != nil { + t.Errorf("Test %d, expected no error, got %s", i, err) + } + } +} + +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) + + m.AuthenticatedData = true + 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") + w.WriteMsg(m) + return dns.RcodeSuccess, nil + }) +} + +func TestFliterRRSlice(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+"), + test.A("leptone.example.org. 1781 IN A 195.201.182.103"), + 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) + if len(filter1) != 4 { + t.Errorf("Expected 4 RRs after filtering, got %d", len(filter1)) + } + rrsig := 0 + for _, f := range filter1 { + if f.Header().Rrtype == dns.TypeRRSIG { + rrsig++ + } + } + if rrsig != 2 { + 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)) + } + rrsig = 0 + for _, f := range filter2 { + if f.Header().Rrtype == dns.TypeRRSIG { + rrsig++ + } + } + if rrsig != 0 { + t.Errorf("Expected 0 RRSIGs after filtering, got %d", rrsig) + } +} diff --git a/ag_201_coredns/plugin/cache/error_test.go b/ag_201_coredns/plugin/cache/error_test.go new file mode 100644 index 0000000..cd18fda --- /dev/null +++ b/ag_201_coredns/plugin/cache/error_test.go @@ -0,0 +1,38 @@ +package cache + +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 TestFormErr(t *testing.T) { + c := New() + c.Next = formErrHandler() + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + c.ServeDNS(context.TODO(), rec, req) + + if c.pcache.Len() != 0 { + t.Errorf("Cached %s, while reply had %d", "example.org.", rec.Msg.Rcode) + } +} + +// formErrHandler is a fake plugin implementation which returns a FORMERR for a reply. +func formErrHandler() plugin.Handler { + return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + m := new(dns.Msg) + m.SetQuestion("example.net.", dns.TypeA) + m.Rcode = dns.RcodeFormatError + w.WriteMsg(m) + return dns.RcodeSuccess, nil + }) +} diff --git a/ag_201_coredns/plugin/cache/freq/freq.go b/ag_201_coredns/plugin/cache/freq/freq.go new file mode 100644 index 0000000..f545f22 --- /dev/null +++ b/ag_201_coredns/plugin/cache/freq/freq.go @@ -0,0 +1,55 @@ +// Package freq keeps track of last X seen events. The events themselves are not stored +// here. So the Freq type should be added next to the thing it is tracking. +package freq + +import ( + "sync" + "time" +) + +// Freq tracks the frequencies of things. +type Freq struct { + // Last time we saw a query for this element. + last time.Time + // Number of this in the last time slice. + hits int + + sync.RWMutex +} + +// New returns a new initialized Freq. +func New(t time.Time) *Freq { + return &Freq{last: t, hits: 0} +} + +// Update updates the number of hits. Last time seen will be set to now. +// If the last time we've seen this entity is within now - d, we increment hits, otherwise +// we reset hits to 1. It returns the number of hits. +func (f *Freq) Update(d time.Duration, now time.Time) int { + earliest := now.Add(-1 * d) + f.Lock() + defer f.Unlock() + if f.last.Before(earliest) { + f.last = now + f.hits = 1 + return f.hits + } + f.last = now + f.hits++ + return f.hits +} + +// Hits returns the number of hits that we have seen, according to the updates we have done to f. +func (f *Freq) Hits() int { + f.RLock() + defer f.RUnlock() + return f.hits +} + +// Reset resets f to time t and hits to hits. +func (f *Freq) Reset(t time.Time, hits int) { + f.Lock() + defer f.Unlock() + f.last = t + f.hits = hits +} diff --git a/ag_201_coredns/plugin/cache/freq/freq_test.go b/ag_201_coredns/plugin/cache/freq/freq_test.go new file mode 100644 index 0000000..740194c --- /dev/null +++ b/ag_201_coredns/plugin/cache/freq/freq_test.go @@ -0,0 +1,36 @@ +package freq + +import ( + "testing" + "time" +) + +func TestFreqUpdate(t *testing.T) { + now := time.Now().UTC() + f := New(now) + window := 1 * time.Minute + + f.Update(window, time.Now().UTC()) + f.Update(window, time.Now().UTC()) + f.Update(window, time.Now().UTC()) + hitsCheck(t, f, 3) + + f.Reset(now, 0) + history := time.Now().UTC().Add(-3 * time.Minute) + f.Update(window, history) + hitsCheck(t, f, 1) +} + +func TestReset(t *testing.T) { + f := New(time.Now().UTC()) + f.Update(1*time.Minute, time.Now().UTC()) + hitsCheck(t, f, 1) + f.Reset(time.Now().UTC(), 0) + hitsCheck(t, f, 0) +} + +func hitsCheck(t *testing.T, f *Freq, expected int) { + if x := f.Hits(); x != expected { + t.Fatalf("Expected hits to be %d, got %d", expected, x) + } +} diff --git a/ag_201_coredns/plugin/cache/fuzz.go b/ag_201_coredns/plugin/cache/fuzz.go new file mode 100644 index 0000000..43f4d26 --- /dev/null +++ b/ag_201_coredns/plugin/cache/fuzz.go @@ -0,0 +1,12 @@ +//go:build gofuzz + +package cache + +import ( + "github.com/coredns/coredns/plugin/pkg/fuzz" +) + +// Fuzz fuzzes cache. +func Fuzz(data []byte) int { + return fuzz.Do(New(), data) +} diff --git a/ag_201_coredns/plugin/cache/handler.go b/ag_201_coredns/plugin/cache/handler.go new file mode 100644 index 0000000..ec2135e --- /dev/null +++ b/ag_201_coredns/plugin/cache/handler.go @@ -0,0 +1,175 @@ +package cache + +import ( + "context" + "math" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/metrics" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// ServeDNS implements the plugin.Handler interface. +func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + 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() + ad := r.AuthenticatedData + + zone := plugin.Zones(c.Zones).Matches(state.Name()) + if zone == "" { + return plugin.NextOrFailure(c.Name(), c.Next, ctx, w, rc) + } + + 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? + + ttl := 0 + i := c.getIgnoreTTL(now, state, server) + if i == nil { + crr := &ResponseWriter{ResponseWriter: w, Cache: c, state: state, server: server, do: do, ad: ad, + nexcept: c.nexcept, pexcept: c.pexcept, wildcardFunc: wildcardFunc(ctx)} + return c.doRefresh(ctx, state, crr) + } + ttl = i.ttl(now) + if ttl < 0 { + // serve stale behavior + if c.verifyStale { + crr := &ResponseWriter{ResponseWriter: w, Cache: c, state: state, server: server, do: do} + 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) + 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) + } + + if i.wildcard != "" { + // Set wildcard source record name to metadata + metadata.SetValueFunc(ctx, "zone/wildcard", func() string { + return i.wildcard + }) + } + + 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, 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()) + } +} + +func (c *Cache) doRefresh(ctx context.Context, state request.Request, cw dns.ResponseWriter) (int, error) { + if !state.Do() { + setDo(state.Req) + } + return plugin.NextOrFailure(c.Name(), c.Next, ctx, cw, state.Req) +} + +func (c *Cache) shouldPrefetch(i *item, now time.Time) bool { + if c.prefetch <= 0 { + return false + } + i.Freq.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 +} + +// Name implements the Handler interface. +func (c *Cache) Name() string { return "cache" } + +// 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()) + cacheRequests.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc() + + if i, ok := c.ncache.Get(k); ok { + 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 { + 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, c.zonesMetricLabel, c.viewMetricLabel).Inc() + return nil +} + +func (c *Cache) exists(state request.Request) *item { + k := hash(state.Name(), state.QType()) + if i, ok := c.ncache.Get(k); ok { + return i.(*item) + } + if i, ok := c.pcache.Get(k); ok { + return i.(*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/ag_201_coredns/plugin/cache/item.go b/ag_201_coredns/plugin/cache/item.go new file mode 100644 index 0000000..6b51a5b --- /dev/null +++ b/ag_201_coredns/plugin/cache/item.go @@ -0,0 +1,107 @@ +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 + + *freq.Freq +} + +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 + i.Answer = m.Answer + i.Ns = m.Ns + i.Extra = make([]dns.RR, len(m.Extra)) + // Don't copy OPT records as these are hop-by-hop. + j := 0 + for _, e := range m.Extra { + if e.Header().Rrtype == dns.TypeOPT { + continue + } + i.Extra[j] = e + j++ + } + i.Extra = i.Extra[:j] + + i.origTTL = uint32(d.Seconds()) + i.stored = now.UTC() + + i.Freq = new(freq.Freq) + + return i +} + +// toMsg turns i into a message, it tailors the reply to m. +// The Authoritative bit should be set to 0, but some client stub resolver implementations, most notably, +// on some legacy systems(e.g. ubuntu 14.04 with glib version 2.20), low-level glibc function `getaddrinfo` +// useb by Python/Ruby/etc.. will discard answers that do not have this bit set. +// 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, ad bool) *dns.Msg { + m1 := new(dns.Msg) + m1.SetReply(m) + + // Set this to true as some DNS clients discard the *entire* packet when it's non-authoritative. + // This is probably not according to spec, but the bit itself is not super useful as this point, so + // just set it to true. + m1.Authoritative = true + m1.AuthenticatedData = i.AuthenticatedData + 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 + + m1.Answer = make([]dns.RR, len(i.Answer)) + m1.Ns = make([]dns.RR, len(i.Ns)) + 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) + + return m1 +} + +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/ag_201_coredns/plugin/cache/log_test.go b/ag_201_coredns/plugin/cache/log_test.go new file mode 100644 index 0000000..220b206 --- /dev/null +++ b/ag_201_coredns/plugin/cache/log_test.go @@ -0,0 +1,5 @@ +package cache + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/cache/metrics.go b/ag_201_coredns/plugin/cache/metrics.go new file mode 100644 index 0000000..77edb02 --- /dev/null +++ b/ag_201_coredns/plugin/cache/metrics.go @@ -0,0 +1,67 @@ +package cache + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // cacheSize is total elements in the cache by cache type. + cacheSize = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: plugin.Namespace, + Subsystem: "cache", + Name: "entries", + Help: "The number of elements in the cache.", + }, []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", "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. 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", "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", "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", "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/ag_201_coredns/plugin/cache/prefech_test.go b/ag_201_coredns/plugin/cache/prefech_test.go new file mode 100644 index 0000000..609956e --- /dev/null +++ b/ag_201_coredns/plugin/cache/prefech_test.go @@ -0,0 +1,163 @@ +package cache + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestPrefetch(t *testing.T) { + tests := []struct { + qname string + ttl int + prefetch int + verifications []verification + }{ + { + qname: "hits.reset.example.org.", + ttl: 80, + prefetch: 1, + verifications: []verification{ + { + after: 0 * time.Second, + answer: "hits.reset.example.org. 80 IN A 127.0.0.1", + fetch: true, + }, + { + after: 73 * time.Second, + answer: "hits.reset.example.org. 7 IN A 127.0.0.1", + fetch: true, + }, + { + after: 80 * time.Second, + answer: "hits.reset.example.org. 73 IN A 127.0.0.2", + }, + }, + }, + { + qname: "short.ttl.example.org.", + ttl: 5, + prefetch: 1, + verifications: []verification{ + { + after: 0 * time.Second, + answer: "short.ttl.example.org. 5 IN A 127.0.0.1", + fetch: true, + }, + { + after: 1 * time.Second, + answer: "short.ttl.example.org. 4 IN A 127.0.0.1", + }, + { + after: 4 * time.Second, + answer: "short.ttl.example.org. 1 IN A 127.0.0.1", + fetch: true, + }, + { + after: 5 * time.Second, + answer: "short.ttl.example.org. 4 IN A 127.0.0.2", + }, + }, + }, + { + qname: "no.prefetch.example.org.", + ttl: 30, + prefetch: 0, + verifications: []verification{ + { + after: 0 * time.Second, + answer: "no.prefetch.example.org. 30 IN A 127.0.0.1", + fetch: true, + }, + { + after: 15 * time.Second, + answer: "no.prefetch.example.org. 15 IN A 127.0.0.1", + }, + { + after: 29 * time.Second, + answer: "no.prefetch.example.org. 1 IN A 127.0.0.1", + }, + { + after: 30 * time.Second, + answer: "no.prefetch.example.org. 30 IN A 127.0.0.2", + fetch: true, + }, + }, + }, + } + + t0, err := time.Parse(time.RFC3339, "2018-01-01T14:00:00+00:00") + if err != nil { + t.Fatal(err) + } + for _, tt := range tests { + t.Run(tt.qname, func(t *testing.T) { + fetchc := make(chan struct{}, 1) + + c := New() + c.prefetch = tt.prefetch + c.Next = prefetchHandler(tt.qname, tt.ttl, fetchc) + + 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) } + + 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) + } + 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 { + t.Errorf("After %s: want rcode %d, got %d", v.after, want, got) + } + if want, got := 1, len(rec.Msg.Answer); want != got { + t.Errorf("After %s: want %d answer RR, got %d", v.after, want, got) + } + if want, got := test.A(v.answer).String(), rec.Msg.Answer[0].String(); want != got { + t.Errorf("After %s: want answer %s, got %s", v.after, want, got) + } + } + }) + } +} + +type verification struct { + after time.Duration + answer string + // fetch defines whether a request is sent to the next handler. + fetch bool +} + +// prefetchHandler is a fake plugin implementation which returns a single A +// record with the given qname and ttl. The returned IP address starts at +// 127.0.0.1 and is incremented on every request. +func prefetchHandler(qname string, ttl int, fetchc chan struct{}) plugin.Handler { + i := 0 + return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + i++ + m := new(dns.Msg) + m.SetQuestion(qname, dns.TypeA) + m.Response = true + m.Answer = append(m.Answer, test.A(fmt.Sprintf("%s %d IN A 127.0.0.%d", qname, ttl, i))) + + w.WriteMsg(m) + fetchc <- struct{}{} + return dns.RcodeSuccess, nil + }) +} diff --git a/ag_201_coredns/plugin/cache/setup.go b/ag_201_coredns/plugin/cache/setup.go new file mode 100644 index 0000000..6a537d9 --- /dev/null +++ b/ag_201_coredns/plugin/cache/setup.go @@ -0,0 +1,255 @@ +package cache + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/cache" + clog "github.com/coredns/coredns/plugin/pkg/log" +) + +var log = clog.NewWithPlugin("cache") + +func init() { plugin.Register("cache", setup) } + +func setup(c *caddy.Controller) error { + ca, err := cacheParse(c) + 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 + }) + + return nil +} + +func cacheParse(c *caddy.Controller) (*Cache, error) { + ca := New() + + j := 0 + for c.Next() { + if j > 0 { + return nil, plugin.ErrOnce + } + j++ + + // cache [ttl] [zones..] + 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]) + if err == nil { + // Reserve 0 (and smaller for future things) + if ttl <= 0 { + return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", ttl) + } + ca.pttl = time.Duration(ttl) * time.Second + ca.nttl = time.Duration(ttl) * time.Second + args = args[1:] + } + } + origins := plugin.OriginsFromArgsOrServerBlock(args, c.ServerBlockKeys) + + // Refinements? In an extra block. + for c.NextBlock() { + switch c.Val() { + // first number is cap, second is an new ttl + case Success: + args := c.RemainingArgs() + if len(args) == 0 { + return nil, c.ArgErr() + } + pcap, err := strconv.Atoi(args[0]) + if err != nil { + return nil, err + } + ca.pcap = pcap + if len(args) > 1 { + pttl, err := strconv.Atoi(args[1]) + if err != nil { + return nil, err + } + // Reserve 0 (and smaller for future things) + if pttl <= 0 { + return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", pttl) + } + ca.pttl = time.Duration(pttl) * time.Second + if len(args) > 2 { + minpttl, err := strconv.Atoi(args[2]) + if err != nil { + return nil, err + } + // Reserve < 0 + if minpttl < 0 { + return nil, fmt.Errorf("cache min TTL can not be negative: %d", minpttl) + } + ca.minpttl = time.Duration(minpttl) * time.Second + } + } + case Denial: + args := c.RemainingArgs() + if len(args) == 0 { + return nil, c.ArgErr() + } + ncap, err := strconv.Atoi(args[0]) + if err != nil { + return nil, err + } + ca.ncap = ncap + if len(args) > 1 { + nttl, err := strconv.Atoi(args[1]) + if err != nil { + return nil, err + } + // Reserve 0 (and smaller for future things) + if nttl <= 0 { + return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", nttl) + } + ca.nttl = time.Duration(nttl) * time.Second + if len(args) > 2 { + minnttl, err := strconv.Atoi(args[2]) + if err != nil { + return nil, err + } + // Reserve < 0 + if minnttl < 0 { + return nil, fmt.Errorf("cache min TTL can not be negative: %d", minnttl) + } + ca.minnttl = time.Duration(minnttl) * time.Second + } + } + case "prefetch": + args := c.RemainingArgs() + if len(args) == 0 || len(args) > 3 { + return nil, c.ArgErr() + } + amount, err := strconv.Atoi(args[0]) + if err != nil { + return nil, err + } + if amount < 0 { + return nil, fmt.Errorf("prefetch amount should be positive: %d", amount) + } + ca.prefetch = amount + + if len(args) > 1 { + dur, err := time.ParseDuration(args[1]) + if err != nil { + return nil, err + } + ca.duration = dur + } + if len(args) > 2 { + pct := args[2] + if x := pct[len(pct)-1]; x != '%' { + return nil, fmt.Errorf("last character of percentage should be `%%`, but is: %q", x) + } + pct = pct[:len(pct)-1] + + num, err := strconv.Atoi(pct) + if err != nil { + return nil, err + } + if num < 10 || num > 90 { + return nil, fmt.Errorf("percentage should fall in range [10, 90]: %d", num) + } + ca.percentage = num + } + + case "serve_stale": + args := c.RemainingArgs() + if len(args) > 2 { + return nil, c.ArgErr() + } + ca.staleUpTo = 1 * time.Hour + if len(args) > 0 { + d, err := time.ParseDuration(args[0]) + if err != nil { + return nil, err + } + if d < 0 { + return nil, errors.New("invalid negative duration for serve_stale") + } + 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) + } + default: + return nil, c.ArgErr() + } + } + + ca.Zones = origins + ca.zonesMetricLabel = strings.Join(origins, ",") + ca.pcache = cache.New(ca.pcap) + ca.ncache = cache.New(ca.ncap) + } + + return ca, nil +} diff --git a/ag_201_coredns/plugin/cache/setup_test.go b/ag_201_coredns/plugin/cache/setup_test.go new file mode 100644 index 0000000..5d8b965 --- /dev/null +++ b/ag_201_coredns/plugin/cache/setup_test.go @@ -0,0 +1,233 @@ +package cache + +import ( + "fmt" + "testing" + "time" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedNcap int + expectedPcap int + expectedNttl time.Duration + expectedMinNttl time.Duration + expectedPttl time.Duration + expectedMinPttl time.Duration + expectedPrefetch int + }{ + {`cache`, false, defaultCap, defaultCap, maxNTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache {}`, false, defaultCap, defaultCap, maxNTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache example.nl { + success 10 + }`, false, defaultCap, 10, maxNTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache example.nl { + success 10 1800 30 + }`, false, defaultCap, 10, maxNTTL, minNTTL, 1800 * time.Second, 30 * time.Second, 0}, + {`cache example.nl { + success 10 + denial 10 15 + }`, false, 10, 10, 15 * time.Second, minNTTL, maxTTL, minTTL, 0}, + {`cache example.nl { + success 10 + denial 10 15 2 + }`, false, 10, 10, 15 * time.Second, 2 * time.Second, maxTTL, minTTL, 0}, + {`cache 25 example.nl { + success 10 + denial 10 15 + }`, false, 10, 10, 15 * time.Second, minNTTL, 25 * time.Second, minTTL, 0}, + {`cache 25 example.nl { + success 10 + denial 10 15 5 + }`, false, 10, 10, 15 * time.Second, 5 * time.Second, 25 * time.Second, minTTL, 0}, + {`cache aaa example.nl`, false, defaultCap, defaultCap, maxNTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache { + prefetch 10 + }`, false, defaultCap, defaultCap, maxNTTL, minNTTL, maxTTL, minTTL, 10}, + + // fails + {`cache example.nl { + success + denial 10 15 + }`, true, defaultCap, defaultCap, maxTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache example.nl { + success 15 + denial aaa + }`, true, defaultCap, defaultCap, maxTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache example.nl { + positive 15 + negative aaa + }`, true, defaultCap, defaultCap, maxTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache 0 example.nl`, true, defaultCap, defaultCap, maxTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache -1 example.nl`, true, defaultCap, defaultCap, maxTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache 1 example.nl { + positive 0 + }`, true, defaultCap, defaultCap, maxTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache 1 example.nl { + positive 0 + prefetch -1 + }`, true, defaultCap, defaultCap, maxTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache 1 example.nl { + prefetch 0 blurp + }`, true, defaultCap, defaultCap, maxTTL, minNTTL, maxTTL, minTTL, 0}, + {`cache + cache`, true, defaultCap, defaultCap, maxTTL, minNTTL, maxTTL, minTTL, 0}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", 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.ncap != test.expectedNcap { + t.Errorf("Test %v: Expected ncap %v but found: %v", i, test.expectedNcap, ca.ncap) + } + if ca.pcap != test.expectedPcap { + t.Errorf("Test %v: Expected pcap %v but found: %v", i, test.expectedPcap, ca.pcap) + } + if ca.nttl != test.expectedNttl { + t.Errorf("Test %v: Expected nttl %v but found: %v", i, test.expectedNttl, ca.nttl) + } + if ca.minnttl != test.expectedMinNttl { + t.Errorf("Test %v: Expected minnttl %v but found: %v", i, test.expectedMinNttl, ca.minnttl) + } + if ca.pttl != test.expectedPttl { + t.Errorf("Test %v: Expected pttl %v but found: %v", i, test.expectedPttl, ca.pttl) + } + if ca.minpttl != test.expectedMinPttl { + t.Errorf("Test %v: Expected minpttl %v but found: %v", i, test.expectedMinPttl, ca.minpttl) + } + if ca.prefetch != test.expectedPrefetch { + t.Errorf("Test %v: Expected prefetch %v but found: %v", i, test.expectedPrefetch, ca.prefetch) + } + } +} + +func TestServeStale(t *testing.T) { + tests := []struct { + input string + shouldErr bool + staleUpTo time.Duration + verifyStale bool + }{ + {"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, 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)) + 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.staleUpTo != test.staleUpTo { + t.Errorf("Test %v: Expected stale %v but found: %v", i, test.staleUpTo, ca.staleUpTo) + } + } +} + +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) + } + } +} diff --git a/ag_201_coredns/plugin/cache/spoof_test.go b/ag_201_coredns/plugin/cache/spoof_test.go new file mode 100644 index 0000000..20d7e8d --- /dev/null +++ b/ag_201_coredns/plugin/cache/spoof_test.go @@ -0,0 +1,82 @@ +package cache + +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 TestSpoof(t *testing.T) { + // Send query for example.org, get reply for example.net; should not be cached. + c := New() + c.Next = spoofHandler(true) + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + c.ServeDNS(context.TODO(), rec, req) + + qname := rec.Msg.Question[0].Name + if c.pcache.Len() != 0 { + t.Errorf("Cached %s, while reply had %s", "example.org.", qname) + } + + // qtype + c.Next = spoofHandlerType() + req.SetQuestion("example.org.", dns.TypeMX) + + c.ServeDNS(context.TODO(), rec, req) + + qtype := rec.Msg.Question[0].Qtype + if c.pcache.Len() != 0 { + t.Errorf("Cached %s type %d, while reply had %d", "example.org.", dns.TypeMX, qtype) + } +} + +func TestResponse(t *testing.T) { + // Send query for example.org, get reply for example.net; should not be cached. + c := New() + c.Next = spoofHandler(false) + + req := new(dns.Msg) + req.SetQuestion("example.net.", dns.TypeA) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + c.ServeDNS(context.TODO(), rec, req) + + if c.pcache.Len() != 0 { + t.Errorf("Cached %s, while reply had response set to %t", "example.net.", rec.Msg.Response) + } +} + +// spoofHandler is a fake plugin implementation which returns a single A records for example.org. The qname in the +// question section is set to example.NET (i.e. they *don't* match). +func spoofHandler(response bool) plugin.Handler { + return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + m := new(dns.Msg) + m.SetQuestion("example.net.", dns.TypeA) + m.Response = response + m.Answer = []dns.RR{test.A("example.org. IN A 127.0.0.53")} + w.WriteMsg(m) + return dns.RcodeSuccess, nil + }) +} + +// spoofHandlerType is a fake plugin implementation which returns a single MX records for example.org. The qtype in the +// question section is set to A. +func spoofHandlerType() 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) + m.Response = true + m.Answer = []dns.RR{test.MX("example.org. IN MX 10 mail.example.org.")} + w.WriteMsg(m) + return dns.RcodeSuccess, nil + }) +} diff --git a/ag_201_coredns/plugin/cancel/README.md b/ag_201_coredns/plugin/cancel/README.md new file mode 100644 index 0000000..64f585a --- /dev/null +++ b/ag_201_coredns/plugin/cancel/README.md @@ -0,0 +1,47 @@ +# cancel + +## Name + +*cancel* - cancels a request's context after 5001 milliseconds. + +## Description + +The *cancel* plugin creates a canceling context for each request. It adds a timeout that gets +triggered after 5001 milliseconds. + +The 5001 number was chosen because the default timeout for DNS clients is 5 seconds, after that they +give up. + +A plugin interested in the cancellation status should call `plugin.Done()` on the context. If the +context was canceled due to a timeout the plugin should not write anything back to the client and +return a value indicating CoreDNS should not either; a zero return value should suffice for that. + +## Syntax + +~~~ txt +cancel [TIMEOUT] +~~~ + +* **TIMEOUT** allows setting a custom timeout. The default timeout is 5001 milliseconds (`5001 ms`) + +## Examples + +~~~ corefile +example.org { + cancel + whoami +} +~~~ + +Or with a custom timeout: + +~~~ corefile +example.org { + cancel 1s + whoami +} +~~~ + +## See Also + +The Go documentation for the context package. diff --git a/ag_201_coredns/plugin/cancel/cancel.go b/ag_201_coredns/plugin/cancel/cancel.go new file mode 100644 index 0000000..23f5de4 --- /dev/null +++ b/ag_201_coredns/plugin/cancel/cancel.go @@ -0,0 +1,66 @@ +// Package cancel implements a plugin adds a canceling context to each request. +package cancel + +import ( + "context" + "fmt" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + + "github.com/miekg/dns" +) + +func init() { plugin.Register("cancel", setup) } + +func setup(c *caddy.Controller) error { + ca := Cancel{} + + for c.Next() { + args := c.RemainingArgs() + switch len(args) { + case 0: + ca.timeout = 5001 * time.Millisecond + case 1: + dur, err := time.ParseDuration(args[0]) + if err != nil { + return plugin.Error("cancel", fmt.Errorf("invalid duration: %q", args[0])) + } + if dur <= 0 { + return plugin.Error("cancel", fmt.Errorf("invalid negative duration: %q", args[0])) + } + ca.timeout = dur + default: + return plugin.Error("cancel", c.ArgErr()) + } + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + ca.Next = next + return ca + }) + + return nil +} + +// Cancel is a plugin that adds a canceling context to each request's context. +type Cancel struct { + timeout time.Duration + Next plugin.Handler +} + +// ServeDNS implements the plugin.Handler interface. +func (c Cancel) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + ctx, cancel := context.WithTimeout(ctx, c.timeout) + + code, err := plugin.NextOrFailure(c.Name(), c.Next, ctx, w, r) + + cancel() + + return code, err +} + +// Name implements the Handler interface. +func (c Cancel) Name() string { return "cancel" } diff --git a/ag_201_coredns/plugin/cancel/cancel_test.go b/ag_201_coredns/plugin/cancel/cancel_test.go new file mode 100644 index 0000000..f775518 --- /dev/null +++ b/ag_201_coredns/plugin/cancel/cancel_test.go @@ -0,0 +1,51 @@ +package cancel + +import ( + "context" + "testing" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +type sleepPlugin struct{} + +func (s sleepPlugin) Name() string { return "sleep" } + +func (s sleepPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + i := 0 + m := new(dns.Msg) + m.SetReply(r) + for { + if plugin.Done(ctx) { + m.Rcode = dns.RcodeBadTime // use BadTime to return something time related + w.WriteMsg(m) + return 0, nil + } + time.Sleep(20 * time.Millisecond) + i++ + if i > 2 { + m.Rcode = dns.RcodeServerFailure + w.WriteMsg(m) + return 0, nil + } + } +} + +func TestCancel(t *testing.T) { + ca := Cancel{Next: sleepPlugin{}, timeout: 20 * time.Millisecond} + ctx := context.Background() + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + m := new(dns.Msg) + m.SetQuestion("aaa.example.com.", dns.TypeTXT) + + ca.ServeDNS(ctx, w, m) + if w.Rcode != dns.RcodeBadTime { + t.Error("Expected ServeDNS to be canceled by context") + } +} diff --git a/ag_201_coredns/plugin/cancel/setup_test.go b/ag_201_coredns/plugin/cancel/setup_test.go new file mode 100644 index 0000000..6079ff5 --- /dev/null +++ b/ag_201_coredns/plugin/cancel/setup_test.go @@ -0,0 +1,29 @@ +package cancel + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", `cancel`) + if err := setup(c); err != nil { + t.Errorf("Test 1, expected no errors, but got: %q", err) + } + + c = caddy.NewTestController("dns", `cancel 5s`) + if err := setup(c); err != nil { + t.Errorf("Test 2, expected no errors, but got: %q", err) + } + + c = caddy.NewTestController("dns", `cancel 5`) + if err := setup(c); err == nil { + t.Errorf("Test 3, expected errors, but got none") + } + + c = caddy.NewTestController("dns", `cancel -1s`) + if err := setup(c); err == nil { + t.Errorf("Test 4, expected errors, but got none") + } +} diff --git a/ag_201_coredns/plugin/chaos/README.md b/ag_201_coredns/plugin/chaos/README.md new file mode 100644 index 0000000..9ce5216 --- /dev/null +++ b/ag_201_coredns/plugin/chaos/README.md @@ -0,0 +1,51 @@ +# chaos + +## Name + +*chaos* - allows for responding to TXT queries in the CH class. + +## Description + +This is useful for retrieving version or author information from the server by querying a TXT record +for a special domain name in the CH class. + +## Syntax + +~~~ +chaos [VERSION] [AUTHORS...] +~~~ + +* **VERSION** is the version to return. Defaults to `CoreDNS-`, if not set. +* **AUTHORS** is what authors to return. This defaults to all GitHub handles in the OWNERS files. + +Note that you have to make sure that this plugin will get actual queries for the +following zones: `version.bind`, `version.server`, `authors.bind`, `hostname.bind` and +`id.server`. + +## Examples + +Specify all the zones in full. + +~~~ corefile +version.bind version.server authors.bind hostname.bind id.server { + chaos CoreDNS-001 info@coredns.io +} +~~~ + +Or just default to `.`: + +~~~ corefile +. { + chaos CoreDNS-001 info@coredns.io +} +~~~ + +And test with `dig`: + +~~~ txt +% dig @localhost CH TXT version.bind +... +;; ANSWER SECTION: +version.bind. 0 CH TXT "CoreDNS-001" +... +~~~ diff --git a/ag_201_coredns/plugin/chaos/chaos.go b/ag_201_coredns/plugin/chaos/chaos.go new file mode 100644 index 0000000..f4d758a --- /dev/null +++ b/ag_201_coredns/plugin/chaos/chaos.go @@ -0,0 +1,58 @@ +// Package chaos implements a plugin that answer to 'CH version.bind TXT' type queries. +package chaos + +import ( + "context" + "math/rand" + "os" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Chaos allows CoreDNS to reply to CH TXT queries and return author or +// version information. +type Chaos struct { + Next plugin.Handler + Version string + Authors []string +} + +// ServeDNS implements the plugin.Handler interface. +func (c Chaos) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + if state.QClass() != dns.ClassCHAOS || state.QType() != dns.TypeTXT { + return plugin.NextOrFailure(c.Name(), c.Next, ctx, w, r) + } + + m := new(dns.Msg) + m.SetReply(r) + + hdr := dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeTXT, Class: dns.ClassCHAOS, Ttl: 0} + switch state.Name() { + default: + return plugin.NextOrFailure(c.Name(), c.Next, ctx, w, r) + case "authors.bind.": + rnd := rand.New(rand.NewSource(time.Now().Unix())) + + for _, i := range rnd.Perm(len(c.Authors)) { + m.Answer = append(m.Answer, &dns.TXT{Hdr: hdr, Txt: []string{c.Authors[i]}}) + } + case "version.bind.", "version.server.": + m.Answer = []dns.RR{&dns.TXT{Hdr: hdr, Txt: []string{c.Version}}} + case "hostname.bind.", "id.server.": + hostname, err := os.Hostname() + if err != nil { + hostname = "localhost" + } + m.Answer = []dns.RR{&dns.TXT{Hdr: hdr, Txt: []string{trim(hostname)}}} + } + w.WriteMsg(m) + return 0, nil +} + +// Name implements the Handler interface. +func (c Chaos) Name() string { return "chaos" } diff --git a/ag_201_coredns/plugin/chaos/chaos_test.go b/ag_201_coredns/plugin/chaos/chaos_test.go new file mode 100644 index 0000000..12cc169 --- /dev/null +++ b/ag_201_coredns/plugin/chaos/chaos_test.go @@ -0,0 +1,80 @@ +package chaos + +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 TestChaos(t *testing.T) { + em := Chaos{ + Version: version, + Authors: []string{"Miek Gieben"}, + } + + tests := []struct { + next plugin.Handler + qname string + qtype uint16 + expectedCode int + expectedReply string + expectedErr error + }{ + { + next: test.NextHandler(dns.RcodeSuccess, nil), + qname: "version.bind", + expectedCode: dns.RcodeSuccess, + expectedReply: version, + expectedErr: nil, + }, + { + next: test.NextHandler(dns.RcodeSuccess, nil), + qname: "authors.bind", + expectedCode: dns.RcodeSuccess, + expectedReply: "Miek Gieben", + expectedErr: nil, + }, + { + next: test.NextHandler(dns.RcodeSuccess, nil), + qname: "authors.bind", + qtype: dns.TypeSRV, + expectedCode: dns.RcodeSuccess, + expectedErr: nil, + }, + } + + ctx := context.TODO() + + for i, tc := range tests { + req := new(dns.Msg) + if tc.qtype == 0 { + tc.qtype = dns.TypeTXT + } + req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype) + req.Question[0].Qclass = dns.ClassCHAOS + em.Next = tc.next + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + code, err := em.ServeDNS(ctx, rec, req) + + if err != tc.expectedErr { + t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expectedErr, err) + } + if code != int(tc.expectedCode) { + t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code) + } + if tc.expectedReply != "" { + answer := rec.Msg.Answer[0].(*dns.TXT).Txt[0] + if answer != tc.expectedReply { + t.Errorf("Test %d: Expected answer %s, but got %s", i, tc.expectedReply, answer) + } + } + } +} + +const version = "CoreDNS-001" diff --git a/ag_201_coredns/plugin/chaos/fuzz.go b/ag_201_coredns/plugin/chaos/fuzz.go new file mode 100644 index 0000000..001cf1d --- /dev/null +++ b/ag_201_coredns/plugin/chaos/fuzz.go @@ -0,0 +1,13 @@ +//go:build gofuzz + +package chaos + +import ( + "github.com/coredns/coredns/plugin/pkg/fuzz" +) + +// Fuzz fuzzes cache. +func Fuzz(data []byte) int { + c := Chaos{} + return fuzz.Do(c, data) +} diff --git a/ag_201_coredns/plugin/chaos/log_test.go b/ag_201_coredns/plugin/chaos/log_test.go new file mode 100644 index 0000000..92c98af --- /dev/null +++ b/ag_201_coredns/plugin/chaos/log_test.go @@ -0,0 +1,5 @@ +package chaos + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/chaos/setup.go b/ag_201_coredns/plugin/chaos/setup.go new file mode 100644 index 0000000..ce0eb7a --- /dev/null +++ b/ag_201_coredns/plugin/chaos/setup.go @@ -0,0 +1,66 @@ +//go:generate go run owners_generate.go + +package chaos + +import ( + "sort" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("chaos", setup) } + +func setup(c *caddy.Controller) error { + version, authors, err := parse(c) + if err != nil { + return plugin.Error("chaos", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return Chaos{Next: next, Version: version, Authors: authors} + }) + + return nil +} + +func parse(c *caddy.Controller) (string, []string, error) { + // Set here so we pick up AppName and AppVersion that get set in coremain's init(). + chaosVersion = caddy.AppName + "-" + caddy.AppVersion + version := "" + + if c.Next() { + args := c.RemainingArgs() + if len(args) == 0 { + return trim(chaosVersion), Owners, nil + } + if len(args) == 1 { + return trim(args[0]), Owners, nil + } + + version = args[0] + authors := make(map[string]struct{}) + for _, a := range args[1:] { + authors[a] = struct{}{} + } + list := []string{} + for k := range authors { + k = trim(k) // limit size to 255 chars + list = append(list, k) + } + sort.Strings(list) + return version, list, nil + } + + return version, Owners, nil +} + +func trim(s string) string { + if len(s) < 256 { + return s + } + return s[:255] +} + +var chaosVersion string diff --git a/ag_201_coredns/plugin/chaos/setup_test.go b/ag_201_coredns/plugin/chaos/setup_test.go new file mode 100644 index 0000000..2c45d86 --- /dev/null +++ b/ag_201_coredns/plugin/chaos/setup_test.go @@ -0,0 +1,54 @@ +package chaos + +import ( + "strings" + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupChaos(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedVersion string // expected version. + expectedAuthor string // expected author (string, although we get a slice). + expectedErrContent string // substring from the expected error. Empty for positive cases. + }{ + // positive + { + `chaos v2`, false, "v2", "", "", + }, + { + `chaos v3 "Miek Gieben"`, false, "v3", "Miek Gieben", "", + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + version, authors, err := parse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, 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) + } + } + + if !test.shouldErr && version != test.expectedVersion { + t.Errorf("Test %d: Chaos not correctly set for input %s. Expected: %s, actual: %s", i, test.input, test.expectedVersion, version) + } + if !test.shouldErr && authors != nil && test.expectedAuthor != "" { + if authors[0] != test.expectedAuthor { + t.Errorf("Test %d: Chaos not correctly set for input %s. Expected: '%s', actual: '%s'", i, test.input, test.expectedAuthor, authors[0]) + } + } + } +} diff --git a/ag_201_coredns/plugin/chaos/zowners.go b/ag_201_coredns/plugin/chaos/zowners.go new file mode 100644 index 0000000..419ca3c --- /dev/null +++ b/ag_201_coredns/plugin/chaos/zowners.go @@ -0,0 +1,4 @@ +package chaos + +// Owners are all GitHub handlers of all maintainers. +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/ag_201_coredns/plugin/clouddns/README.md b/ag_201_coredns/plugin/clouddns/README.md new file mode 100644 index 0000000..1e12281 --- /dev/null +++ b/ag_201_coredns/plugin/clouddns/README.md @@ -0,0 +1,73 @@ +# clouddns + +## Name + +*clouddns* - enables serving zone data from GCP Cloud DNS. + +## Description + +The *clouddns* plugin is useful for serving zones from resource record +sets in GCP Cloud DNS. This plugin supports all [Google Cloud DNS +records](https://cloud.google.com/dns/docs/overview#supported_dns_record_types). This plugin can +be used when CoreDNS is deployed on GCP or elsewhere. Note that this plugin accesses the resource +records through the Google Cloud API. For records in a privately hosted zone, it is not necessary to +place CoreDNS and this plugin in the associated VPC network. In fact the private hosted zone could +be created without any associated VPC and this plugin could still access the resource records under +the hosted zone. + +## Syntax + +~~~ txt +clouddns [ZONE:PROJECT_ID:HOSTED_ZONE_NAME...] { + credentials [FILENAME] + fallthrough [ZONES...] +} +~~~ + +* **ZONE** the name of the domain to be accessed. When there are multiple zones with overlapping + domains (private vs. public hosted zone), CoreDNS does the lookup in the given order here. + Therefore, for a non-existing resource record, SOA response will be from the rightmost zone. + +* **PROJECT\_ID** the project ID of the Google Cloud project. + +* **HOSTED\_ZONE\_NAME** the name of the hosted zone that contains the resource record sets to be + 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 + 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. + +## Examples + +Enable clouddns with implicit GCP credentials and resolve CNAMEs via 10.0.0.1: + +~~~ txt +example.org { + clouddns example.org.:gcp-example-project:example-zone + forward . 10.0.0.1 +} +~~~ + +Enable clouddns with fallthrough: + +~~~ txt +example.org { + clouddns example.org.:gcp-example-project:example-zone example.com.:gcp-example-project:example-zone-2 { + fallthrough example.gov. + } +} +~~~ + +Enable clouddns with multiple hosted zones with the same domain: + +~~~ txt +. { + clouddns example.org.:gcp-example-project:example-zone example.com.:gcp-example-project:other-example-zone +} +~~~ diff --git a/ag_201_coredns/plugin/clouddns/clouddns.go b/ag_201_coredns/plugin/clouddns/clouddns.go new file mode 100644 index 0000000..e09c247 --- /dev/null +++ b/ag_201_coredns/plugin/clouddns/clouddns.go @@ -0,0 +1,225 @@ +// Package clouddns implements a plugin that returns resource records +// from GCP Cloud DNS. +package clouddns + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/plugin/pkg/upstream" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + gcp "google.golang.org/api/dns/v1" +) + +// CloudDNS is a plugin that returns RR from GCP Cloud DNS. +type CloudDNS struct { + Next plugin.Handler + Fall fall.F + + zoneNames []string + client gcpDNS + upstream *upstream.Upstream + + zMu sync.RWMutex + zones zones +} + +type zone struct { + projectName string + zoneName string + z *file.Zone + dns string +} + +type zones map[string][]*zone + +// New reads from the keys map which uses domain names as its key and a colon separated +// string of project name and hosted zone name lists as its values, validates +// that each domain name/zone id pair does exist, and returns a new *CloudDNS. +// In addition to this, upstream is passed for doing recursive queries against CNAMEs. +// Returns error if it cannot verify any given domain name/zone id pair. +func New(ctx context.Context, c gcpDNS, keys map[string][]string, up *upstream.Upstream) (*CloudDNS, error) { + zones := make(map[string][]*zone, len(keys)) + zoneNames := make([]string, 0, len(keys)) + for dnsName, hostedZoneDetails := range keys { + for _, hostedZone := range hostedZoneDetails { + ss := strings.SplitN(hostedZone, ":", 2) + if len(ss) != 2 { + return nil, errors.New("either project or zone name missing") + } + err := c.zoneExists(ss[0], ss[1]) + if err != nil { + return nil, err + } + fqdnDNSName := dns.Fqdn(dnsName) + if _, ok := zones[fqdnDNSName]; !ok { + zoneNames = append(zoneNames, fqdnDNSName) + } + zones[fqdnDNSName] = append(zones[fqdnDNSName], &zone{projectName: ss[0], zoneName: ss[1], dns: fqdnDNSName, z: file.NewZone(fqdnDNSName, "")}) + } + } + return &CloudDNS{ + client: c, + zoneNames: zoneNames, + zones: zones, + upstream: up, + }, nil +} + +// Run executes first update, spins up an update forever-loop. +// Returns error if first update fails. +func (h *CloudDNS) Run(ctx context.Context) error { + if err := h.updateZones(ctx); err != nil { + 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 <-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) + } + } + } + }() + return nil +} + +// ServeDNS implements the plugin.Handler interface. +func (h *CloudDNS) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.Name() + + zName := plugin.Zones(h.zoneNames).Matches(qname) + if zName == "" { + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + + z, ok := h.zones[zName] // ok true if we are authoritative for the zone + if !ok || z == nil { + return dns.RcodeServerFailure, nil + } + + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative = true + var result file.Result + + for _, hostedZone := range z { + h.zMu.RLock() + m.Answer, m.Ns, m.Extra, result = hostedZone.z.Lookup(ctx, state, qname) + h.zMu.RUnlock() + + // Take the answer if it's non-empty OR if there is another + // record type exists for this name (NODATA). + if len(m.Answer) != 0 || result == file.NoData { + break + } + } + + if len(m.Answer) == 0 && result != file.NoData && h.Fall.Through(qname) { + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + + switch result { + case file.Success: + case file.NoData: + case file.NameError: + m.Rcode = dns.RcodeNameError + case file.Delegation: + m.Authoritative = false + case file.ServerFailure: + return dns.RcodeServerFailure, nil + } + + w.WriteMsg(m) + return dns.RcodeSuccess, nil +} + +func updateZoneFromRRS(rrs *gcp.ResourceRecordSetsListResponse, z *file.Zone) error { + for _, rr := range rrs.Rrsets { + var rfc1035 string + var r dns.RR + var err error + for _, value := range rr.Rrdatas { + 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) + } + return nil +} + +// updateZones re-queries resource record sets for each zone and updates the +// zone object. +// Returns error if any zones error'ed out, but waits for other zones to +// complete first. +func (h *CloudDNS) updateZones(ctx context.Context) error { + errc := make(chan error) + defer close(errc) + for zName, z := range h.zones { + go func(zName string, z []*zone) { + var err error + var rrListResponse *gcp.ResourceRecordSetsListResponse + defer func() { + errc <- err + }() + + for i, hostedZone := range z { + newZ := file.NewZone(zName, "") + newZ.Upstream = h.upstream + rrListResponse, err = h.client.listRRSets(ctx, hostedZone.projectName, hostedZone.zoneName) + if err != nil { + err = fmt.Errorf("failed to list resource records for %v:%v:%v from gcp: %v", zName, hostedZone.projectName, hostedZone.zoneName, err) + return + } + updateZoneFromRRS(rrListResponse, newZ) + + h.zMu.Lock() + (*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++ { + err := <-errc + if err != nil { + errs = append(errs, err.Error()) + } + } + if len(errs) != 0 { + return fmt.Errorf("errors updating zones: %v", errs) + } + return nil +} + +// Name implements the Handler interface. +func (h *CloudDNS) Name() string { return "clouddns" } diff --git a/ag_201_coredns/plugin/clouddns/clouddns_test.go b/ag_201_coredns/plugin/clouddns/clouddns_test.go new file mode 100644 index 0000000..e052bf2 --- /dev/null +++ b/ag_201_coredns/plugin/clouddns/clouddns_test.go @@ -0,0 +1,309 @@ +package clouddns + +import ( + "context" + "errors" + "reflect" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/plugin/pkg/upstream" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + gcp "google.golang.org/api/dns/v1" +) + +type fakeGCPClient struct { + *gcp.Service +} + +func (c fakeGCPClient) zoneExists(projectName, hostedZoneName string) error { + return nil +} + +func (c fakeGCPClient) listRRSets(ctx context.Context, projectName, hostedZoneName string) (*gcp.ResourceRecordSetsListResponse, error) { + if projectName == "bad-project" || hostedZoneName == "bad-zone" { + return nil, errors.New("the 'parameters.managedZone' resource named 'bad-zone' does not exist") + } + + var rr []*gcp.ResourceRecordSet + + if hostedZoneName == "sample-zone-1" { + rr = []*gcp.ResourceRecordSet{ + { + Name: "example.org.", + Ttl: 300, + Type: "A", + Rrdatas: []string{"1.2.3.4"}, + }, + { + Name: "www.example.org", + Ttl: 300, + Type: "A", + Rrdatas: []string{"1.2.3.4"}, + }, + { + Name: "*.www.example.org", + Ttl: 300, + Type: "CNAME", + Rrdatas: []string{"www.example.org"}, + }, + { + Name: "example.org.", + Ttl: 300, + Type: "AAAA", + Rrdatas: []string{"2001:db8:85a3::8a2e:370:7334"}, + }, + { + Name: "sample.example.org", + Ttl: 300, + Type: "CNAME", + Rrdatas: []string{"example.org"}, + }, + { + Name: "example.org.", + Ttl: 300, + Type: "PTR", + Rrdatas: []string{"ptr.example.org."}, + }, + { + Name: "org.", + Ttl: 300, + Type: "SOA", + Rrdatas: []string{"ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"}, + }, + { + Name: "com.", + Ttl: 300, + Type: "NS", + Rrdatas: []string{"ns-cloud-c4.googledomains.com."}, + }, + { + Name: "split-example.gov.", + Ttl: 300, + Type: "A", + Rrdatas: []string{"1.2.3.4"}, + }, + { + Name: "swag.", + Ttl: 300, + Type: "YOLO", + Rrdatas: []string{"foobar"}, + }, + } + } else { + rr = []*gcp.ResourceRecordSet{ + { + Name: "split-example.org.", + Ttl: 300, + Type: "A", + Rrdatas: []string{"1.2.3.4"}, + }, + { + Name: "other-example.org.", + Ttl: 300, + Type: "A", + Rrdatas: []string{"3.5.7.9"}, + }, + { + Name: "org.", + Ttl: 300, + Type: "SOA", + Rrdatas: []string{"ns-cloud-e1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 300 259200 300"}, + }, + } + } + + return &gcp.ResourceRecordSetsListResponse{Rrsets: rr}, nil +} + +func TestCloudDNS(t *testing.T) { + ctx := context.Background() + + r, err := New(ctx, fakeGCPClient{}, map[string][]string{"bad.": {"bad-project:bad-zone"}}, &upstream.Upstream{}) + if err != nil { + t.Fatalf("Failed to create Cloud DNS: %v", err) + } + if err = r.Run(ctx); err == nil { + t.Fatalf("Expected errors for zone bad.") + } + + r, err = New(ctx, fakeGCPClient{}, map[string][]string{"org.": {"sample-project-1:sample-zone-2", "sample-project-1:sample-zone-1"}, "gov.": {"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) + } + r.Fall = fall.Zero + r.Fall.SetZonesFromArgs([]string{"gov."}) + r.Next = test.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.Name() + m := new(dns.Msg) + rcode := dns.RcodeServerFailure + if qname == "example.gov." { + m.SetReply(r) + rr, err := dns.NewRR("example.gov. 300 IN A 2.4.6.8") + if err != nil { + t.Fatalf("Failed to create Resource Record: %v", err) + } + m.Answer = []dns.RR{rr} + + m.Authoritative = true + rcode = dns.RcodeSuccess + } + + m.SetRcode(r, rcode) + w.WriteMsg(m) + return rcode, nil + }) + err = r.Run(ctx) + if err != nil { + t.Fatalf("Failed to initialize Cloud DNS: %v", err) + } + + tests := []struct { + qname string + qtype uint16 + wantRetCode int + wantAnswer []string // ownernames for the records in the additional section. + wantMsgRCode int + wantNS []string + expectedErr error + }{ + // 0. example.org A found - success. + { + 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, + 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, + wantAnswer: []string{"example.org. 300 IN PTR ptr.example.org."}, + }, + // 3. sample.example.org points to example.org CNAME. + // Query must return both CNAME and A recs. + { + qname: "sample.example.org", + qtype: dns.TypeA, + wantAnswer: []string{ + "sample.example.org. 300 IN CNAME example.org.", + "example.org. 300 IN A 1.2.3.4", + }, + }, + // 4. Explicit CNAME query for sample.example.org. + // Query must return just CNAME. + { + 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, + 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. + { + 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"}, + }, + // 7. Zone not configured. + { + qname: "badexample.com", + qtype: dns.TypeA, + wantRetCode: dns.RcodeServerFailure, + wantMsgRCode: dns.RcodeServerFailure, + }, + // 8. No record found. Return SOA record. + { + qname: "bad.org", + 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"}, + }, + // 9. No record found. Fallthrough. + { + 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, + 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, + 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. + { + qname: "a.www.example.org", + qtype: dns.TypeA, + wantAnswer: []string{ + "a.www.example.org. 300 IN CNAME www.example.org.", + "www.example.org. 300 IN A 1.2.3.4", + }, + }, + } + + for ti, tc := range tests { + 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 != tc.expectedErr { + t.Fatalf("Test %d: Expected error %v, but got %v", ti, tc.expectedErr, err) + } + if code != int(tc.wantRetCode) { + t.Fatalf("Test %d: Expected returned status code %s, but got %s", ti, dns.RcodeToString[tc.wantRetCode], dns.RcodeToString[code]) + } + + if tc.wantMsgRCode != rec.Msg.Rcode { + t.Errorf("Test %d: Unexpected msg status code. Want: %s, got: %s", ti, dns.RcodeToString[tc.wantMsgRCode], dns.RcodeToString[rec.Msg.Rcode]) + } + + if len(tc.wantAnswer) != len(rec.Msg.Answer) { + t.Errorf("Test %d: Unexpected number of Answers. Want: %d, got: %d", ti, len(tc.wantAnswer), len(rec.Msg.Answer)) + } else { + for i, gotAnswer := range rec.Msg.Answer { + if gotAnswer.String() != tc.wantAnswer[i] { + t.Errorf("Test %d: Unexpected answer.\nWant:\n\t%s\nGot:\n\t%s", ti, tc.wantAnswer[i], gotAnswer) + } + } + } + + if len(tc.wantNS) != len(rec.Msg.Ns) { + t.Errorf("Test %d: Unexpected NS number. Want: %d, got: %d", ti, len(tc.wantNS), len(rec.Msg.Ns)) + } else { + for i, ns := range rec.Msg.Ns { + got, ok := ns.(*dns.SOA) + if !ok { + t.Errorf("Test %d: Unexpected NS type. Want: SOA, got: %v", ti, reflect.TypeOf(got)) + } + if got.String() != tc.wantNS[i] { + t.Errorf("Test %d: Unexpected NS.\nWant: %v\nGot: %v", ti, tc.wantNS[i], got) + } + } + } + } +} diff --git a/ag_201_coredns/plugin/clouddns/gcp.go b/ag_201_coredns/plugin/clouddns/gcp.go new file mode 100644 index 0000000..b02ab2b --- /dev/null +++ b/ag_201_coredns/plugin/clouddns/gcp.go @@ -0,0 +1,40 @@ +package clouddns + +import ( + "context" + + gcp "google.golang.org/api/dns/v1" +) + +type gcpDNS interface { + zoneExists(projectName, hostedZoneName string) error + listRRSets(ctx context.Context, projectName, hostedZoneName string) (*gcp.ResourceRecordSetsListResponse, error) +} + +type gcpClient struct { + *gcp.Service +} + +// zoneExists is a wrapper method around `gcp.Service.ManagedZones.Get` +// it checks if the provided zone name for a given project exists. +func (c gcpClient) zoneExists(projectName, hostedZoneName string) error { + _, err := c.ManagedZones.Get(projectName, hostedZoneName).Do() + if err != nil { + return err + } + return nil +} + +// listRRSets is a wrapper method around `gcp.Service.ResourceRecordSets.List` +// it fetches and returns the record sets for a hosted zone. +func (c gcpClient) listRRSets(ctx context.Context, projectName, hostedZoneName string) (*gcp.ResourceRecordSetsListResponse, error) { + req := c.ResourceRecordSets.List(projectName, hostedZoneName) + var rs []*gcp.ResourceRecordSet + if err := req.Pages(ctx, func(page *gcp.ResourceRecordSetsListResponse) error { + rs = append(rs, page.Rrsets...) + return nil + }); err != nil { + return nil, err + } + return &gcp.ResourceRecordSetsListResponse{Rrsets: rs}, nil +} diff --git a/ag_201_coredns/plugin/clouddns/log_test.go b/ag_201_coredns/plugin/clouddns/log_test.go new file mode 100644 index 0000000..148635b --- /dev/null +++ b/ag_201_coredns/plugin/clouddns/log_test.go @@ -0,0 +1,5 @@ +package clouddns + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/clouddns/setup.go b/ag_201_coredns/plugin/clouddns/setup.go new file mode 100644 index 0000000..cfd7eec --- /dev/null +++ b/ag_201_coredns/plugin/clouddns/setup.go @@ -0,0 +1,108 @@ +package clouddns + +import ( + "context" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "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/pkg/upstream" + + gcp "google.golang.org/api/dns/v1" + "google.golang.org/api/option" +) + +var log = clog.NewWithPlugin("clouddns") + +func init() { plugin.Register("clouddns", setup) } + +// exposed for testing +var f = func(ctx context.Context, opt option.ClientOption) (gcpDNS, error) { + var err error + var client *gcp.Service + if opt != nil { + client, err = gcp.NewService(ctx, opt) + } else { + // if credentials file is not provided in the Corefile + // authenticate the client using env variables + client, err = gcp.NewService(ctx) + } + return gcpClient{client}, err +} + +func setup(c *caddy.Controller) error { + for c.Next() { + keyPairs := map[string]struct{}{} + keys := map[string][]string{} + + var fall fall.F + up := upstream.New() + + args := c.RemainingArgs() + + for i := 0; i < len(args); i++ { + parts := strings.SplitN(args[i], ":", 3) + if len(parts) != 3 { + return plugin.Error("clouddns", c.Errf("invalid zone %q", args[i])) + } + dnsName, projectName, hostedZone := parts[0], parts[1], parts[2] + if dnsName == "" || projectName == "" || hostedZone == "" { + return plugin.Error("clouddns", c.Errf("invalid zone %q", args[i])) + } + if _, ok := keyPairs[args[i]]; ok { + return plugin.Error("clouddns", c.Errf("conflict zone %q", args[i])) + } + + keyPairs[args[i]] = struct{}{} + keys[dnsName] = append(keys[dnsName], projectName+":"+hostedZone) + } + + var opt option.ClientOption + for c.NextBlock() { + switch c.Val() { + case "upstream": + c.RemainingArgs() + case "credentials": + if c.NextArg() { + opt = option.WithCredentialsFile(c.Val()) + } else { + return plugin.Error("clouddns", c.ArgErr()) + } + case "fallthrough": + fall.SetZonesFromArgs(c.RemainingArgs()) + default: + return plugin.Error("clouddns", c.Errf("unknown property %q", c.Val())) + } + } + + 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)) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + h.Next = next + return h + }) + c.OnShutdown(func() error { cancel(); return nil }) + } + + return nil +} diff --git a/ag_201_coredns/plugin/clouddns/setup_test.go b/ag_201_coredns/plugin/clouddns/setup_test.go new file mode 100644 index 0000000..ae2262e --- /dev/null +++ b/ag_201_coredns/plugin/clouddns/setup_test.go @@ -0,0 +1,49 @@ +package clouddns + +import ( + "context" + "testing" + + "github.com/coredns/caddy" + + "google.golang.org/api/option" +) + +func TestSetupCloudDNS(t *testing.T) { + f = func(ctx context.Context, opt option.ClientOption) (gcpDNS, error) { + return fakeGCPClient{}, nil + } + + tests := []struct { + body string + expectedError bool + }{ + {`clouddns`, false}, + {`clouddns :`, true}, + {`clouddns ::`, true}, + {`clouddns example.org.:example-project:zone-name`, false}, + {`clouddns example.org.:example-project:zone-name { }`, false}, + {`clouddns example.org.:example-project: { }`, true}, + {`clouddns example.org.:example-project:zone-name { }`, false}, + {`clouddns example.org.:example-project:zone-name { wat +}`, true}, + {`clouddns example.org.:example-project:zone-name { + fallthrough +}`, false}, + {`clouddns example.org.:example-project:zone-name { + credentials +}`, true}, + {`clouddns example.org.:example-project:zone-name example.org.:example-project:zone-name { + }`, true}, + + {`clouddns example.org { + }`, true}, + } + + for _, test := range tests { + c := caddy.NewTestController("dns", test.body) + if err := setup(c); (err == nil) == test.expectedError { + t.Errorf("Unexpected errors: %v", err) + } + } +} diff --git a/ag_201_coredns/plugin/debug/README.md b/ag_201_coredns/plugin/debug/README.md new file mode 100644 index 0000000..4376723 --- /dev/null +++ b/ag_201_coredns/plugin/debug/README.md @@ -0,0 +1,51 @@ +# debug + +## Name + +*debug* - disables the automatic recovery upon a crash so that you'll get a nice stack trace. + +## Description + +Normally CoreDNS will recover from panics; using *debug* inhibits this. The main use of *debug* is +to help in testing. A side effect of using *debug* is that `log.Debug` and `log.Debugf` messages +will be printed to standard output. + +Note that the *errors* plugin (if loaded) will also set a `recover`, negating this setting. + +Enabling this plugin is process-wide: enabling *debug* in at least one server block enables +debug mode globally. + +## Syntax + +~~~ txt +debug +~~~ + +Some plugins will send debug log DNS messages. This is done in the following format: + +~~~ +debug: 000000 00 0a 01 00 00 01 00 00 00 00 00 01 07 65 78 61 +debug: 000010 6d 70 6c 65 05 6c 6f 63 61 6c 00 00 01 00 01 00 +debug: 000020 00 29 10 00 00 00 80 00 00 00 +debug: 00002a +~~~ + +Using `text2pcap` (part of Wireshark), this can be converted back to binary, with the following +command line: `text2pcap -i 17 -u 53,53`, where 17 is the protocol (UDP) and 53 are the ports. These +ports allow Wireshark to detect these packets as DNS messages. + +Each plugin can decide whether to dump messages to aid in debugging. + +## Examples + +Disable the ability to recover from crashes and show debug logging: + +~~~ corefile +. { + debug +} +~~~ + +## See Also + +. diff --git a/ag_201_coredns/plugin/debug/debug.go b/ag_201_coredns/plugin/debug/debug.go new file mode 100644 index 0000000..7fb6861 --- /dev/null +++ b/ag_201_coredns/plugin/debug/debug.go @@ -0,0 +1,22 @@ +package debug + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("debug", setup) } + +func setup(c *caddy.Controller) error { + config := dnsserver.GetConfig(c) + + for c.Next() { + if c.NextArg() { + return plugin.Error("debug", c.ArgErr()) + } + config.Debug = true + } + + return nil +} diff --git a/ag_201_coredns/plugin/debug/debug_test.go b/ag_201_coredns/plugin/debug/debug_test.go new file mode 100644 index 0000000..71ebf37 --- /dev/null +++ b/ag_201_coredns/plugin/debug/debug_test.go @@ -0,0 +1,44 @@ +package debug + +import ( + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" +) + +func TestDebug(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedDebug bool + }{ + // positive + { + `debug`, false, true, + }, + // negative + { + `debug off`, true, false, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + err := setup(c) + cfg := dnsserver.GetConfig(c) + + if test.shouldErr && err == nil { + t.Fatalf("Test %d: Expected error but found %s for input %s", i, err, test.input) + } + + if err != nil { + if !test.shouldErr { + t.Fatalf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) + } + } + if cfg.Debug != test.expectedDebug { + t.Fatalf("Test %d: Expected debug to be: %t, but got: %t, input: %s", i, test.expectedDebug, cfg.Debug, test.input) + } + } +} diff --git a/ag_201_coredns/plugin/debug/log_test.go b/ag_201_coredns/plugin/debug/log_test.go new file mode 100644 index 0000000..6e256db --- /dev/null +++ b/ag_201_coredns/plugin/debug/log_test.go @@ -0,0 +1,5 @@ +package debug + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/debug/pcap.go b/ag_201_coredns/plugin/debug/pcap.go new file mode 100644 index 0000000..493478a --- /dev/null +++ b/ag_201_coredns/plugin/debug/pcap.go @@ -0,0 +1,72 @@ +package debug + +import ( + "bytes" + "fmt" + + "github.com/coredns/coredns/plugin/pkg/log" + + "github.com/miekg/dns" +) + +// Hexdump converts the dns message m to a hex dump Wireshark can import. +// See https://www.wireshark.org/docs/man-pages/text2pcap.html. +// This output looks like this: +// +// 00000 dc bd 01 00 00 01 00 00 00 00 00 01 07 65 78 61 +// 000010 6d 70 6c 65 05 6c 6f 63 61 6c 00 00 01 00 01 00 +// 000020 00 29 10 00 00 00 80 00 00 00 +// 00002a +// +// Hexdump will use log.Debug to write the dump to the log, each line +// is prefixed with 'debug: ' so the data can be easily extracted. +// +// msg will prefix the pcap dump. +func Hexdump(m *dns.Msg, v ...interface{}) { + if !log.D.Value() { + return + } + + buf, _ := m.Pack() + if len(buf) == 0 { + return + } + + out := "\n" + string(hexdump(buf)) + v = append(v, out) + log.Debug(v...) +} + +// Hexdumpf dumps a DNS message as Hexdump, but allows a format string. +func Hexdumpf(m *dns.Msg, format string, v ...interface{}) { + if !log.D.Value() { + return + } + + buf, _ := m.Pack() + if len(buf) == 0 { + return + } + + format += "\n%s" + v = append(v, hexdump(buf)) + log.Debugf(format, v...) +} + +func hexdump(data []byte) []byte { + b := new(bytes.Buffer) + + newline := "" + for i := 0; i < len(data); i++ { + if i%16 == 0 { + fmt.Fprintf(b, "%s%s%06x", newline, prefix, i) + newline = "\n" + } + fmt.Fprintf(b, " %02x", data[i]) + } + fmt.Fprintf(b, "\n%s%06x", prefix, len(data)) + + return b.Bytes() +} + +const prefix = "debug: " diff --git a/ag_201_coredns/plugin/debug/pcap_test.go b/ag_201_coredns/plugin/debug/pcap_test.go new file mode 100644 index 0000000..6b263c8 --- /dev/null +++ b/ag_201_coredns/plugin/debug/pcap_test.go @@ -0,0 +1,73 @@ +package debug + +import ( + "bytes" + "fmt" + golog "log" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/log" + + "github.com/miekg/dns" +) + +func msg() *dns.Msg { + m := new(dns.Msg) + m.SetQuestion("example.local.", dns.TypeA) + m.SetEdns0(4096, true) + m.Id = 10 + return m +} + +func TestNoDebug(t *testing.T) { + // Must come first, because set log.D.Set() which is impossible to undo. + var f bytes.Buffer + golog.SetOutput(&f) + + str := "Hi There!" + Hexdumpf(msg(), "%s %d", str, 10) + if len(f.Bytes()) != 0 { + t.Errorf("Expected no output, got %d bytes", len(f.Bytes())) + } +} + +func ExampleHexdump() { + buf, _ := msg().Pack() + h := hexdump(buf) + fmt.Println(string(h)) + + // Output: + // debug: 000000 00 0a 01 00 00 01 00 00 00 00 00 01 07 65 78 61 + // debug: 000010 6d 70 6c 65 05 6c 6f 63 61 6c 00 00 01 00 01 00 + // debug: 000020 00 29 10 00 00 00 80 00 00 00 + // debug: 00002a +} + +func TestHexdump(t *testing.T) { + var f bytes.Buffer + golog.SetOutput(&f) + log.D.Set() + + str := "Hi There!" + Hexdump(msg(), str) + logged := f.String() + + if !strings.Contains(logged, "[DEBUG] "+str) { + t.Errorf("The string %s, is not contained in the logged output: %s", str, logged) + } +} + +func TestHexdumpf(t *testing.T) { + var f bytes.Buffer + golog.SetOutput(&f) + log.D.Set() + + str := "Hi There!" + Hexdumpf(msg(), "%s %d", str, 10) + logged := f.String() + + if !strings.Contains(logged, "[DEBUG] "+fmt.Sprintf("%s %d", str, 10)) { + t.Errorf("The string %s %d, is not contained in the logged output: %s", str, 10, logged) + } +} diff --git a/ag_201_coredns/plugin/deprecated/setup.go b/ag_201_coredns/plugin/deprecated/setup.go new file mode 100644 index 0000000..64caa0c --- /dev/null +++ b/ag_201_coredns/plugin/deprecated/setup.go @@ -0,0 +1,34 @@ +// Package deprecated is used when we deprecated plugin. In plugin.cfg just go from +// +// startup:github.com/coredns/caddy/startupshutdown +// +// To: +// +// startup:deprecated +// +// And things should work as expected. This means starting CoreDNS will fail with an error. We can only +// point to the release notes to details what next steps a user should take. I.e. there is no way to add this +// to the error generated. +package deprecated + +import ( + "errors" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin" +) + +// removed has the names of the plugins that need to error on startup. +var removed = []string{""} + +func setup(c *caddy.Controller) error { + c.Next() + x := c.Val() + return plugin.Error(x, errors.New("this plugin has been deprecated")) +} + +func init() { + for _, plug := range removed { + plugin.Register(plug, setup) + } +} diff --git a/ag_201_coredns/plugin/dns64/README.md b/ag_201_coredns/plugin/dns64/README.md new file mode 100644 index 0000000..7975574 --- /dev/null +++ b/ag_201_coredns/plugin/dns64/README.md @@ -0,0 +1,106 @@ +# dns64 + +## Name + +*dns64* - enables DNS64 IPv6 transition mechanism. + +## Description + +The *dns64* plugin will when asked for a domain's AAAA records, but only finds A records, +synthesizes the AAAA records from the A records. + +The synthesis is *only* performed **if the query came in via IPv6**. + +This translation is for IPv6-only networks that have [NAT64](https://en.wikipedia.org/wiki/NAT64). + +## Syntax + +~~~ +dns64 [PREFIX] +~~~ + +* **PREFIX** defines a custom prefix instead of the default `64:ff9b::/96`. + +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 + +Translate with the default well known prefix. Applies to all queries (if they came in over IPv6). + +~~~ +. { + dns64 +} +~~~ + +Use a custom prefix. + +~~~ corefile +. { + dns64 64:1337::/96 +} +~~~ + +Or +~~~ corefile +. { + dns64 { + prefix 64:1337::/96 + } +} +~~~ + +Enable translation even if an existing AAAA record is present. + +~~~ corefile +. { + dns64 { + translate_all + } +} +~~~ + +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: + +- `coredns_dns64_requests_translated_total{server}` - counter of DNS requests translated + +The `server` label is explained in the _prometheus_ plugin documentation. + +## Bugs + +Not all features required by DNS64 are implemented, only basic AAAA synthesis. + +* Support "mapping of separate IPv4 ranges to separate IPv6 prefixes" +* Resolve PTR records +* Make resolver DNSSEC aware. See: [RFC 6147 Section 3](https://tools.ietf.org/html/rfc6147#section-3) + +## See Also + +See [RFC 6147](https://tools.ietf.org/html/rfc6147) for more information on the DNS64 mechanism. diff --git a/ag_201_coredns/plugin/dns64/dns64.go b/ag_201_coredns/plugin/dns64/dns64.go new file mode 100644 index 0000000..9f426eb --- /dev/null +++ b/ag_201_coredns/plugin/dns64/dns64.go @@ -0,0 +1,208 @@ +// Package dns64 implements a plugin that performs DNS64. +// +// See: RFC 6147 (https://tools.ietf.org/html/rfc6147) +package dns64 + +import ( + "context" + "errors" + "net" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metrics" + "github.com/coredns/coredns/plugin/pkg/nonwriter" + "github.com/coredns/coredns/plugin/pkg/response" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// UpstreamInt wraps the Upstream API for dependency injection during testing +type UpstreamInt interface { + Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) +} + +// DNS64 performs DNS64. +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 !d.requestShouldIntercept(&request.Request{W: w, Req: r}) { + return d.Next.ServeDNS(ctx, w, r) + } + + // Pass the request to the next plugin in the chain, but intercept the response. + nw := nonwriter.New(w) + origRc, origErr := d.Next.ServeDNS(ctx, nw, r) + if nw.Msg == nil { // somehow we didn't get a response (or raw bytes were written) + return origRc, origErr + } + + // If the response doesn't need DNS64, short-circuit. + if !d.responseShouldDNS64(nw.Msg) { + w.WriteMsg(nw.Msg) + return origRc, origErr + } + + // otherwise do the actual DNS64 request and response synthesis + msg, err := d.DoDNS64(ctx, w, r, nw.Msg) + if err != nil { + // err means we weren't able to even issue the A request + // to CoreDNS upstream + return dns.RcodeServerFailure, err + } + + RequestsTranslatedCount.WithLabelValues(metrics.WithServer(ctx)).Inc() + w.WriteMsg(msg) + return msg.MsgHdr.Rcode, nil +} + +// Name implements the Handler interface. +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 or the 'allow_ipv4' option is set +// 2. The request is of type AAAA +// 3. The request is of class INET +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 + } + + // Do not modify if question is not AAAA or not of class IN. See RFC 6147 5.1 + return req.QType() == dns.TypeAAAA && req.QClass() == dns.ClassINET +} + +// responseShouldDNS64 returns true if the response indicates we should attempt +// DNS64 rewriting: +// 1. The response has no valid (RFC 5.1.4) AAAA records (RFC 5.1.1) +// 2. The response code (RCODE) is not 3 (Name Error) (RFC 5.1.2) +// +// Note that requestShouldIntercept must also have been true, so the request +// is known to be of type AAAA. +func (d *DNS64) responseShouldDNS64(origResponse *dns.Msg) bool { + ty, _ := response.Typify(origResponse, time.Now().UTC()) + + // Handle NameError normally. See RFC 6147 5.1.2 + // All other error types are "equivalent" to empty response + if ty == response.NameError { + return false + } + + // If we've configured to always translate, well, then always translate. + if d.TranslateAll { + return true + } + + // if response includes AAAA record, no need to rewrite + for _, rr := range origResponse.Answer { + if rr.Header().Rrtype == dns.TypeAAAA { + return false + } + } + return true +} + +// DoDNS64 takes an (empty) response to an AAAA question, issues the A request, +// and synthesizes the answer. Returns the response message, or error on internal failure. +func (d *DNS64) DoDNS64(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, origResponse *dns.Msg) (*dns.Msg, error) { + req := request.Request{W: w, Req: r} // req is unused + resp, err := d.Upstream.Lookup(ctx, req, req.Name(), dns.TypeA) + if err != nil { + return nil, err + } + out := d.Synthesize(r, origResponse, resp) + return out, nil +} + +// Synthesize merges the AAAA response and the records from the A response +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 + + // 5.1.7: The TTL is the minimum of the A RR and the SOA RR. If SOA is + // unknown, then the TTL is the minimum of A TTL and 600 + SOATtl := uint32(600) // Default NS record TTL + for _, ns := range origResponse.Ns { + if ns.Header().Rrtype == dns.TypeSOA { + SOATtl = ns.Header().Ttl + } + } + + ret.Answer = make([]dns.RR, 0, len(resp.Answer)) + // convert A records to AAAA records + for _, rr := range resp.Answer { + header := rr.Header() + // 5.3.3: All other RR's MUST be returned unchanged + if header.Rrtype != dns.TypeA { + ret.Answer = append(ret.Answer, rr) + continue + } + + 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 + } + + // Replace A answer with a DNS64 AAAA answer + ret.Answer = append(ret.Answer, &dns.AAAA{ + Hdr: dns.RR_Header{ + Name: header.Name, + Rrtype: dns.TypeAAAA, + Class: header.Class, + Ttl: ttl, + }, + AAAA: aaaa, + }) + } + return &ret +} + +// to6 takes a prefix and IPv4 address and returns an IPv6 address according to RFC 6052. +func to6(prefix *net.IPNet, addr net.IP) (net.IP, error) { + addr = addr.To4() + if addr == nil { + return nil, errors.New("not a valid IPv4 address") + } + + n, _ := prefix.Mask.Size() + // Assumes prefix has been validated during setup + v6 := make([]byte, 16) + i, j := 0, 0 + + for ; i < n/8; i++ { + v6[i] = prefix.IP[i] + } + for ; i < 8; i, j = i+1, j+1 { + v6[i] = addr[j] + } + if i == 8 { + i++ + } + for ; j < 4; i, j = i+1, j+1 { + v6[i] = addr[j] + } + + return v6, nil +} diff --git a/ag_201_coredns/plugin/dns64/dns64_test.go b/ag_201_coredns/plugin/dns64/dns64_test.go new file mode 100644 index 0000000..a294721 --- /dev/null +++ b/ag_201_coredns/plugin/dns64/dns64_test.go @@ -0,0 +1,556 @@ +package dns64 + +import ( + "context" + "fmt" + "net" + "reflect" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +func To6(prefix, address string) (net.IP, error) { + _, pref, _ := net.ParseCIDR(prefix) + addr := net.ParseIP(address) + + return to6(pref, addr) +} + +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) + } + if v6.String() != "64:ff9b::4040:4040" { + t.Errorf("%d", v6) + } + + v6, err = To6("64:ff9b::/64", "64.64.64.64") + if err != nil { + t.Error(err) + } + if v6.String() != "64:ff9b::40:4040:4000:0" { + t.Errorf("%d", v6) + } + + v6, err = To6("64:ff9b::/56", "64.64.64.64") + if err != nil { + t.Error(err) + } + if v6.String() != "64:ff9b:0:40:40:4040::" { + t.Errorf("%d", v6) + } + + v6, err = To6("64::/32", "64.64.64.64") + if err != nil { + t.Error(err) + } + if v6.String() != "64:0:4040:4040::" { + t.Errorf("%d", v6) + } +} + +func TestResponseShould(t *testing.T) { + var tests = []struct { + resp dns.Msg + translateAll bool + expected bool + }{ + // If there's an AAAA record, then no + { + resp: dns.Msg{ + MsgHdr: dns.MsgHdr{ + Rcode: dns.RcodeSuccess, + }, + Answer: []dns.RR{ + test.AAAA("example.com. IN AAAA ::1"), + }, + }, + expected: false, + }, + // If there's no AAAA, then true + { + resp: dns.Msg{ + MsgHdr: dns.MsgHdr{ + Rcode: dns.RcodeSuccess, + }, + Ns: []dns.RR{ + test.SOA("example.com. IN SOA foo bar 1 1 1 1 1"), + }, + }, + expected: true, + }, + // Failure, except NameError, should be true + { + resp: dns.Msg{ + MsgHdr: dns.MsgHdr{ + Rcode: dns.RcodeNotImplemented, + }, + Ns: []dns.RR{ + test.SOA("example.com. IN SOA foo bar 1 1 1 1 1"), + }, + }, + expected: true, + }, + // NameError should be false + { + resp: dns.Msg{ + MsgHdr: dns.MsgHdr{ + Rcode: dns.RcodeNameError, + }, + Ns: []dns.RR{ + test.SOA("example.com. IN SOA foo bar 1 1 1 1 1"), + }, + }, + expected: false, + }, + // If there's an AAAA record, but translate_all is configured, then yes + { + resp: dns.Msg{ + MsgHdr: dns.MsgHdr{ + Rcode: dns.RcodeSuccess, + }, + Answer: []dns.RR{ + test.AAAA("example.com. IN AAAA ::1"), + }, + }, + translateAll: true, + expected: true, + }, + } + + d := DNS64{} + + for idx, tc := range tests { + t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) { + d.TranslateAll = tc.translateAll + actual := d.responseShouldDNS64(&tc.resp) + if actual != tc.expected { + t.Fatalf("Expected %v got %v", tc.expected, actual) + } + }) + } +} + +func TestDNS64(t *testing.T) { + var cases = []struct { + // a brief summary of the test case + name string + + // the request + req *dns.Msg + + // the initial response from the "downstream" server + initResp *dns.Msg + + // A response to provide + aResp *dns.Msg + + // the expected ultimate result + resp *dns.Msg + }{ + { + // no AAAA record, yes A record. Do DNS64 + name: "standard flow", + 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, + 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, + 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"), + }, + }, + }, + { + // name exists, but has neither A nor AAAA record + name: "a empty", + 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. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, + }, + aResp: &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: 43, + Opcode: dns.OpcodeQuery, + RecursionDesired: true, + Rcode: dns.RcodeSuccess, + Response: true, + }, + 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")}, + }, + + resp: &dns.Msg{ + 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. 3600 IN SOA foo bar 1 7200 900 1209600 86400")}, + Answer: []dns.RR{}, // just to make comparison happy + }, + }, + { + // Query error other than NameError + name: "non-nxdomain error", + 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{ // failure + MsgHdr: dns.MsgHdr{ + Id: 42, + Opcode: dns.OpcodeQuery, + RecursionDesired: true, + Rcode: dns.RcodeRefused, + Response: true, + }, + Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}}, + }, + aResp: &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: 43, + Opcode: dns.OpcodeQuery, + RecursionDesired: 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, + 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"), + test.AAAA("example.com. 600 IN AAAA 64:ff9b::192.0.2.43"), + }, + }, + }, + { + // nxdomain (NameError): don't even try an A request. + name: "nxdomain", + 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{ // failure + MsgHdr: dns.MsgHdr{ + Id: 42, + Opcode: dns.OpcodeQuery, + RecursionDesired: true, + Rcode: dns.RcodeNameError, + Response: true, + }, + 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{ + MsgHdr: dns.MsgHdr{ + Id: 42, + Opcode: dns.OpcodeQuery, + RecursionDesired: true, + Rcode: dns.RcodeNameError, + Response: true, + }, + 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")}, + }, + }, + { + // AAAA record exists + name: "AAAA record", + 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{ + 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}}, + Answer: []dns.RR{ + test.AAAA("example.com. 60 IN AAAA ::1"), + test.AAAA("example.com. 5000 IN AAAA ::2"), + }, + }, + + resp: &dns.Msg{ + 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}}, + 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") + + for idx, tc := range cases { + t.Run(fmt.Sprintf("%d_%s", idx, tc.name), func(t *testing.T) { + d := DNS64{ + Next: &fakeHandler{t, tc.initResp}, + Prefix: pfx, + Upstream: &fakeUpstream{t, tc.req.Question[0].Name, tc.aResp}, + } + + rec := dnstest.NewRecorder(&test.ResponseWriter{RemoteIP: "::1"}) + rc, err := d.ServeDNS(context.Background(), rec, tc.req) + if err != nil { + t.Fatal(err) + } + actual := rec.Msg + if actual.Rcode != rc { + t.Fatalf("ServeDNS should return real result code %q != %q", actual.Rcode, rc) + } + + if !reflect.DeepEqual(actual, tc.resp) { + t.Fatalf("Final answer should match expected %q != %q", actual, tc.resp) + } + }) + } +} + +type fakeHandler struct { + t *testing.T + reply *dns.Msg +} + +func (fh *fakeHandler) ServeDNS(_ context.Context, w dns.ResponseWriter, _ *dns.Msg) (int, error) { + if fh.reply == nil { + panic("fakeHandler ServeDNS with nil reply") + } + w.WriteMsg(fh.reply) + + return fh.reply.Rcode, nil +} +func (fh *fakeHandler) Name() string { + return "fake" +} + +type fakeUpstream struct { + t *testing.T + qname string + resp *dns.Msg +} + +func (fu *fakeUpstream) Lookup(_ context.Context, _ request.Request, name string, typ uint16) (*dns.Msg, error) { + if fu.qname == "" { + fu.t.Fatalf("Unexpected A lookup for %s", name) + } + if name != fu.qname { + fu.t.Fatalf("Wrong A lookup for %s, expected %s", name, fu.qname) + } + + if typ != dns.TypeA { + fu.t.Fatalf("Wrong lookup type %d, expected %d", typ, dns.TypeA) + } + + return fu.resp, nil +} diff --git a/ag_201_coredns/plugin/dns64/metrics.go b/ag_201_coredns/plugin/dns64/metrics.go new file mode 100644 index 0000000..9552316 --- /dev/null +++ b/ag_201_coredns/plugin/dns64/metrics.go @@ -0,0 +1,18 @@ +package dns64 + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // RequestsTranslatedCount is the number of DNS requests translated by dns64. + RequestsTranslatedCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: pluginName, + Name: "requests_translated_total", + Help: "Counter of DNS requests translated by dns64.", + }, []string{"server"}) +) diff --git a/ag_201_coredns/plugin/dns64/setup.go b/ag_201_coredns/plugin/dns64/setup.go new file mode 100644 index 0000000..5e06187 --- /dev/null +++ b/ag_201_coredns/plugin/dns64/setup.go @@ -0,0 +1,92 @@ +package dns64 + +import ( + "net" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/upstream" +) + +const pluginName = "dns64" + +func init() { plugin.Register(pluginName, setup) } + +func setup(c *caddy.Controller) error { + dns64, err := dns64Parse(c) + if err != nil { + return plugin.Error(pluginName, err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + dns64.Next = next + return dns64 + }) + + return nil +} + +func dns64Parse(c *caddy.Controller) (*DNS64, error) { + _, defaultPref, _ := net.ParseCIDR("64:ff9b::/96") + dns64 := &DNS64{ + Upstream: upstream.New(), + Prefix: defaultPref, + } + + for c.Next() { + args := c.RemainingArgs() + if len(args) == 1 { + pref, err := parsePrefix(c, args[0]) + + if err != nil { + return nil, err + } + dns64.Prefix = pref + continue + } + if len(args) > 0 { + return nil, c.ArgErr() + } + + for c.NextBlock() { + switch c.Val() { + case "prefix": + if !c.NextArg() { + return nil, c.ArgErr() + } + pref, err := parsePrefix(c, c.Val()) + + if err != nil { + return nil, err + } + 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()) + } + } + } + return dns64, nil +} + +func parsePrefix(c *caddy.Controller, addr string) (*net.IPNet, error) { + _, pref, err := net.ParseCIDR(addr) + if err != nil { + return nil, err + } + + // Test for valid prefix + n, total := pref.Mask.Size() + if total != 128 { + return nil, c.Errf("invalid netmask %d IPv6 address: %q", total, pref) + } + if n%8 != 0 || n < 32 || n > 96 { + return nil, c.Errf("invalid prefix length %q", pref) + } + + return pref, nil +} diff --git a/ag_201_coredns/plugin/dns64/setup_test.go b/ag_201_coredns/plugin/dns64/setup_test.go new file mode 100644 index 0000000..e7d13f4 --- /dev/null +++ b/ag_201_coredns/plugin/dns64/setup_test.go @@ -0,0 +1,153 @@ +package dns64 + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupDns64(t *testing.T) { + tests := []struct { + inputUpstreams string + shouldErr bool + wantPrefix string + wantAllowIpv4 bool + }{ + { + `dns64`, + false, + "64:ff9b::/96", + false, + }, + { + `dns64 64:dead::/96`, + false, + "64:dead::/96", + false, + }, + { + `dns64 { + translate_all + }`, + false, + "64:ff9b::/96", + false, + }, + { + `dns64`, + false, + "64:ff9b::/96", + false, + }, + { + `dns64 { + prefix 64:ff9b::/96 + }`, + false, + "64:ff9b::/96", + false, + }, + { + `dns64 { + prefix 64:ff9b::/32 + }`, + false, + "64:ff9b::/32", + false, + }, + { + `dns64 { + prefix 64:ff9b::/52 + }`, + true, + "64:ff9b::/52", + false, + }, + { + `dns64 { + prefix 64:ff9b::/104 + }`, + true, + "64:ff9b::/104", + false, + }, + { + `dns64 { + prefix 8.8.8.8/24 + }`, + true, + "8.8.9.9/24", + false, + }, + { + `dns64 { + prefix 64:ff9b::/96 + }`, + false, + "64:ff9b::/96", + false, + }, + { + `dns64 { + prefix 2002:ac12:b083::/96 + }`, + false, + "2002:ac12:b083::/96", + false, + }, + { + `dns64 { + prefix 2002:c0a8:a88a::/48 + }`, + false, + "2002:c0a8:a88a::/48", + false, + }, + { + `dns64 foobar { + prefix 64:ff9b::/96 + }`, + true, + "64:ff9b::/96", + false, + }, + { + `dns64 foobar`, + true, + "64:ff9b::/96", + false, + }, + { + `dns64 { + foobar + }`, + true, + "64:ff9b::/96", + false, + }, + { + `dns64 { + allow_ipv4 + }`, + false, + "64:ff9b::/96", + true, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputUpstreams) + dns64, err := dns64Parse(c) + if (err != nil) != test.shouldErr { + 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.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/ag_201_coredns/plugin/dnsovertor.zip b/ag_201_coredns/plugin/dnsovertor.zip new file mode 100644 index 0000000..7abc687 Binary files /dev/null and b/ag_201_coredns/plugin/dnsovertor.zip differ diff --git a/ag_201_coredns/plugin/dnsovertor/dnsovertor.go b/ag_201_coredns/plugin/dnsovertor/dnsovertor.go new file mode 100644 index 0000000..7d9a42f --- /dev/null +++ b/ag_201_coredns/plugin/dnsovertor/dnsovertor.go @@ -0,0 +1,135 @@ +// Package dnsovertor implements a plugin. +package dnsovertor + +import ( + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "io/ioutil" + "net/http" + "os" + "time" + + "github.com/coredns/coredns/request" + "github.com/miekg/dns" + "golang.org/x/net/proxy" + + "github.com/coredns/coredns/plugin" + + clog "github.com/coredns/coredns/plugin/pkg/log" +) + +var log = clog.NewWithPlugin("dnsovertor") + +// Dnsovertor is a plugin in CoreDNS +type Dnsovertor struct { + Next plugin.Handler + Hsaddr string + TorCtrlPort string +} + +// default tor control port is 9050 +func NewDnsovertorPlugin(next plugin.Handler, Hsaddr string, TorCtrlPort string) *Dnsovertor { + + log.Debugf( + "Creating dnsovertor plugin with %s", + Hsaddr, + ) + + p := &Dnsovertor{ + Hsaddr: Hsaddr, + Next: next, + TorCtrlPort: TorCtrlPort, + } + + return p +} + +// ServeDNS implements the plugin.Handler interface. +func (p Dnsovertor) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.Name() + + t0 := time.Now() + timeStr := time.Now().Format("2006-01-02 15:04:05") //当前时间的字符串,2006-01-02 15:04:05据说是golang的诞生时间,固定写法 + fmt.Printf("dnsovertor解析\n接收请求时间:" + timeStr) + fmt.Printf("接收请求域名:" + qname + "\n") + + // create a socks5 dialer + dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:"+p.TorCtrlPort, nil, proxy.Direct) + if err != nil { + fmt.Fprintln(os.Stderr, "can't connect to the proxy:", err) + os.Exit(1) + } + // setup a http client + httpTransport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + httpTransport.DisableKeepAlives = false + httpClient := &http.Client{ + Transport: httpTransport, + Timeout: 10 * time.Second, + } + + // set our socks5 as the dialer + httpTransport.Dial = dialer.Dial + + query := dns.Msg{} + query.SetQuestion(qname, dns.TypeA) + timeStr1 := time.Now().Format("2006-01-02 15:04:05") //当前时间的字符串,2006-01-02 15:04:05据说是golang的诞生时间,固定写法 + fmt.Printf("发送请求时间:" + timeStr1) + fmt.Printf("发送请求域名:" + qname + "\n") + msg, _ := query.Pack() + b64 := base64.RawURLEncoding.EncodeToString(msg) + + resp, err := httpClient.Get("https://" + p.Hsaddr + "/dns-query?dns=" + b64) + if err != nil { + fmt.Printf("Send query error, err:%v\n", err) + //os.Exit(1) 怎么好像没见过print上面的 + } + defer resp.Body.Close() + bodyBytes, _ := ioutil.ReadAll(resp.Body) + + response := dns.Msg{} + response.Unpack(bodyBytes) + timeStr2 := time.Now().Format("2006-01-02 15:04:05") //当前时间的字符串,2006-01-02 15:04:05据说是golang的诞生时间,固定写法 + fmt.Println("接收响应时间:" + timeStr2) + fmt.Println("接收响应信息:") + for i := 0; i < len(response.Answer); i++ { + //a := response.Answer[i].Header() + b := response.Answer[i].String() + //fmt.Printf("Dns answer name is :" + a.Name + "\n") + fmt.Printf(b + "\t") + } + fmt.Printf("\n") + //fmt.Printf("Dns answer is :%v\n", response.String()) + + //m := new(dns.Msg) + response.SetReply(r) //m是r的reply + //m.Authoritative = true + //m.Answer = answers + var msg22 *dns.Msg + msg22 = &response + timeStr3 := time.Now().Format("2006-01-02 15:04:05") //当前时间的字符串,2006-01-02 15:04:05据说是golang的诞生时间,固定写法 + fmt.Println("发送响应时间:" + timeStr3) + fmt.Printf("发送响应信息gogogogo:") + for i := 0; i < len(response.Answer); i++ { + //a := response.Answer[i].Header() + b := response.Answer[i].String() + //fmt.Printf("Dns answer name is :" + a.Name + "\n") + fmt.Printf(b + "\t") + } + t1 := time.Now() + d := t1.Sub(t0) + fmt.Printf("处理时间:") + fmt.Println(d) + + w.WriteMsg(msg22) + return dns.RcodeSuccess, nil +} + +// Name implements the Handler interface. +func (p Dnsovertor) Name() string { return "dnsovertor" } + + diff --git a/ag_201_coredns/plugin/dnsovertor/metrics.go b/ag_201_coredns/plugin/dnsovertor/metrics.go new file mode 100644 index 0000000..f76043e --- /dev/null +++ b/ag_201_coredns/plugin/dnsovertor/metrics.go @@ -0,0 +1,18 @@ +package dnsovertor + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// 向dns隐藏服务转发的请求的数量 +var dotorCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "dnsovertor", + Name: "dnsovertor_hits_total", + Help: "Counter of the number of requests made to warnlisted domains.", +}, []string{"server", "requestor", "domain"}) + +// 服务时间应该怎么记录? diff --git a/ag_201_coredns/plugin/dnsovertor/ready.go b/ag_201_coredns/plugin/dnsovertor/ready.go new file mode 100644 index 0000000..e478081 --- /dev/null +++ b/ag_201_coredns/plugin/dnsovertor/ready.go @@ -0,0 +1,5 @@ +package dnsovertor + +// Ready implements the ready.Readiness interface, once this flips to true CoreDNS +// assumes this plugin is ready for queries; it is not checked again. +func (p Dnsovertor) Ready() bool { return true } diff --git a/ag_201_coredns/plugin/dnsovertor/setup.go b/ag_201_coredns/plugin/dnsovertor/setup.go new file mode 100644 index 0000000..ba19ed9 --- /dev/null +++ b/ag_201_coredns/plugin/dnsovertor/setup.go @@ -0,0 +1,55 @@ +package dnsovertor + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("dnsovertor", setup) } + +func setup(c *caddy.Controller) error { + + var torsocksport string + var hsaddr string + torsocksport = "9050" + hsaddr="dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion" + for c.Next() { + for c.NextBlock() { + option := c.Val() + switch option { + case "hsaddr": + if !c.NextArg() { + return c.ArgErr() + } + hsaddr = c.Val() + log.Debugf("Setting hidden service address to %s", hsaddr) + log.Infof("Setting hidden service address to %s", hsaddr) + + case "torsocksport": + if !c.NextArg() { + return c.ArgErr() + } + torsocksport = c.Val() + + default: + return plugin.Error("dnsovertor", c.Errf("unexpected '%v' command", option)) + } + } + // Default (The most common) tor socks port is 9050 + + log.Debugf("Setting Tor socks port to %s", torsocksport) + log.Infof("Setting Tor socks port to %s", torsocksport) + if c.NextArg() { + return plugin.Error("dnsovertor", c.ArgErr()) + } + + } // 'dnsovertor' + //这里怎么回事,没定义的hsaddr + dnsserver.GetConfig(c). + AddPlugin(func(next plugin.Handler) plugin.Handler { + return NewDnsovertorPlugin(next, hsaddr, torsocksport) //'Dnsovertor' + }) + + return nil +} diff --git a/ag_201_coredns/plugin/dnsovertor/setup_test.go b/ag_201_coredns/plugin/dnsovertor/setup_test.go new file mode 100644 index 0000000..5336725 --- /dev/null +++ b/ag_201_coredns/plugin/dnsovertor/setup_test.go @@ -0,0 +1,52 @@ +// 注意:测试文件Torsocksport时需要相应端口的tor运行 +package dnsovertor + +import ( + "reflect" + "testing" + + "github.com/coredns/caddy" + "github.com/stretchr/testify/assert" +) + +func TestValidHsaddr(t *testing.T) { + cfg := `dnsovertor { + hsaddr dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion + }` + c := caddy.NewTestController("dns", cfg) + + err := setup(c) + assert.NoError(t, err) +} + +func TestValidTorsocksport(t *testing.T) { + cfg := `dnsovertor { + hsaddr dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion + torsocksport 9050 + }` + c := caddy.NewTestController("dns", cfg) + + err := setup(c) + assert.NoError(t, err) +} + +func TestEmptyHsaddr(t *testing.T) { + cfg := `dnsovertor { + hsaddr + }` + c := caddy.NewTestController("dns", cfg) + + err := setup(c) + t.Fatalf("Emplty Hsaddr is not supported: %v", err) +} + +func TestEmptyTorsocksport(t *testing.T) { + cfg := `dnsovertor { + hsaddr dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion + torsocksport + }` + c := caddy.NewTestController("dns", cfg) + + err := setup(c) + t.Fatalf("Emplty torsocksport is not supported: %v", err) +} diff --git a/ag_201_coredns/plugin/dnssec/README.md b/ag_201_coredns/plugin/dnssec/README.md new file mode 100644 index 0000000..00766a1 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/README.md @@ -0,0 +1,87 @@ +# dnssec + +## Name + +*dnssec* - enables on-the-fly DNSSEC signing of served data. + +## Description + +With *dnssec*, any reply that doesn't (or can't) do DNSSEC will get signed on the fly. 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. + +This plugin can only be used once per Server Block. + +## Syntax + +~~~ +dnssec [ZONES... ] { + key file KEY... + cache_capacity CAPACITY +} +~~~ + +The signing behavior depends on the keys specified. If multiple keys are specified of which there is +at least one key with the SEP bit set and at least one key with the SEP bit unset, signing will happen +in split ZSK/KSK mode. DNSKEY records will be signed with all keys that have the SEP bit set. All other +records will be signed with all keys that do not have the SEP bit set. + +In any other case, each specified key will be treated as a CSK (common signing key), forgoing the +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. + +* **ZONES** zones that should be signed. If empty, the zones from the configuration block + are used. + +* `key file` indicates that **KEY** file(s) should be read from disk. When multiple keys are specified, RRsets + will be signed with all keys. Generating a key can be done with `dnssec-keygen`: `dnssec-keygen -a + ECDSAP256SHA256 `. A key created for zone *A* can be safely used for zone *B*. The name of the + key file can be specified in one of the following formats + + * basename of the generated key `Kexample.org+013+45330` + * generated public key `Kexample.org+013+45330.key` + * generated private key `Kexample.org+013+45330.private` + +* `cache_capacity` indicates the capacity of the cache. The dnssec plugin uses a cache to store + RRSIGs. The default for **CAPACITY** is 10000. + +## Metrics + +If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported: + +* `coredns_dnssec_cache_entries{server, type}` - total elements in the cache, type is "signature". +* `coredns_dnssec_cache_hits_total{server}` - Counter of cache hits. +* `coredns_dnssec_cache_misses_total{server}` - Counter of cache misses. + +The label `server` indicated the server handling the request, see the *metrics* plugin for details. + +## Examples + +Sign responses for `example.org` with the key "Kexample.org.+013+45330.key". + +~~~ corefile +example.org { + dnssec { + key file Kexample.org.+013+45330 + } + whoami +} +~~~ + +Sign responses for a kubernetes zone with the key "Kcluster.local+013+45129.key". + +~~~ +cluster.local { + kubernetes + dnssec { + key file Kcluster.local+013+45129 + } +} +~~~ diff --git a/ag_201_coredns/plugin/dnssec/black_lies.go b/ag_201_coredns/plugin/dnssec/black_lies.go new file mode 100644 index 0000000..68fae22 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/black_lies.go @@ -0,0 +1,64 @@ +package dnssec + +import ( + "github.com/coredns/coredns/plugin/pkg/response" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// nsec returns an NSEC useful for NXDOMAIN responses. +// 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.Name() == state.Zone { + nsec.TypeBitMap = filter18(state.QType(), apexBitmap, mt) + } else { + nsec.TypeBitMap = filter14(state.QType(), zoneBitmap, mt) + } + + sigs, err := d.sign([]dns.RR{nsec}, state.Zone, ttl, incep, expir, server) + if err != nil { + return nil, err + } + + return append(sigs, nsec), nil +} + +// 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} +) + +// 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 { + return zoneBitmap[:] + } + for i := range bitmap { + if bitmap[i] == t { + return append(bitmap[:i], bitmap[i+1:]...) + } + } + return zoneBitmap[:] // make a slice +} + +func filter18(t uint16, bitmap [18]uint16, mt response.Type) []uint16 { + if mt != response.NoData && mt != response.NameError { + return apexBitmap[:] + } + for i := range bitmap { + if bitmap[i] == t { + return append(bitmap[:i], bitmap[i+1:]...) + } + } + return apexBitmap[:] // make a slice +} diff --git a/ag_201_coredns/plugin/dnssec/black_lies_bitmap_test.go b/ag_201_coredns/plugin/dnssec/black_lies_bitmap_test.go new file mode 100644 index 0000000..a4a487f --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/black_lies_bitmap_test.go @@ -0,0 +1,64 @@ +package dnssec + +import ( + "testing" + "time" + + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +const server = "dns//." + +func TestBlackLiesBitmapNoData(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"example.org."}) + defer rm1() + defer rm2() + + m := testTLSAMsg() + state := request.Request{Req: m, Zone: "example.org."} + m = d.Sign(state, time.Now().UTC(), server) + + var nsec *dns.NSEC + for _, r := range m.Ns { + if r.Header().Rrtype == dns.TypeNSEC { + nsec = r.(*dns.NSEC) + } + } + for _, b := range nsec.TypeBitMap { + if uint16(b) == dns.TypeTLSA { + t.Errorf("Type TLSA should not be present in the type bitmap: %v", nsec.TypeBitMap) + } + } +} +func TestBlackLiesBitmapNameError(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"example.org."}) + defer rm1() + defer rm2() + + m := testTLSAMsg() + m.Rcode = dns.RcodeNameError // change to name error + state := request.Request{Req: m, Zone: "example.org."} + m = d.Sign(state, time.Now().UTC(), server) + + var nsec *dns.NSEC + for _, r := range m.Ns { + if r.Header().Rrtype == dns.TypeNSEC { + nsec = r.(*dns.NSEC) + } + } + for _, b := range nsec.TypeBitMap { + if uint16(b) == dns.TypeTLSA { + t.Errorf("Type TLSA should not be present in the type bitmap: %v", nsec.TypeBitMap) + } + } +} + +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")}, + } +} diff --git a/ag_201_coredns/plugin/dnssec/black_lies_test.go b/ag_201_coredns/plugin/dnssec/black_lies_test.go new file mode 100644 index 0000000..a9a2902 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/black_lies_test.go @@ -0,0 +1,86 @@ +package dnssec + +import ( + "testing" + "time" + + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +func TestZoneSigningBlackLies(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testNxdomainMsg() + 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 m.Rcode != dns.RcodeSuccess { + t.Errorf("Expected rcode %d, got %d", dns.RcodeSuccess, m.Rcode) + } + if nsec == nil { + t.Fatalf("Expected NSEC, got none") + } + if nsec.Hdr.Name != "ww.miek.nl." { + t.Errorf("Expected %s, got %s", "ww.miek.nl.", nsec.Hdr.Name) + } + if nsec.NextDomain != "\\000.ww.miek.nl." { + t.Errorf("Expected %s, got %s", "\\000.ww.miek.nl.", nsec.NextDomain) + } +} + +func TestBlackLiesNoError(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testSuccessMsg() + state := request.Request{Req: m, Zone: "miek.nl."} + m = d.Sign(state, time.Now().UTC(), server) + + if m.Rcode != dns.RcodeSuccess { + t.Errorf("Expected rcode %d, got %d", dns.RcodeSuccess, m.Rcode) + } + + if len(m.Answer) != 2 { + t.Errorf("Answer section should have 2 RRs") + } + sig, txt := false, false + for _, rr := range m.Answer { + if _, ok := rr.(*dns.RRSIG); ok { + sig = true + } + if _, ok := rr.(*dns.TXT); ok { + txt = true + } + } + if !sig || !txt { + t.Errorf("Expected RRSIG and TXT in answer section") + } +} + +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")}, + } +} + +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"`)}, + } +} diff --git a/ag_201_coredns/plugin/dnssec/cache.go b/ag_201_coredns/plugin/dnssec/cache.go new file mode 100644 index 0000000..d80f5c1 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/cache.go @@ -0,0 +1,48 @@ +package dnssec + +import ( + "hash/fnv" + "io" + "time" + + "github.com/coredns/coredns/plugin/pkg/cache" + + "github.com/miekg/dns" +) + +// hash serializes the RRset and returns a signature cache key. +func hash(rrs []dns.RR) uint64 { + h := fnv.New64() + // 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]interface{}, key uint64) bool { + for _, rr := range items[key].([]dns.RR) { + if !rr.(*dns.RRSIG).ValidityPeriod(is75) { + delete(items, key) + } + } + return true + }) + + case <-stop: + return + } + } +} diff --git a/ag_201_coredns/plugin/dnssec/cache_test.go b/ag_201_coredns/plugin/dnssec/cache_test.go new file mode 100644 index 0000000..8d5ea88 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/cache_test.go @@ -0,0 +1,82 @@ +package dnssec + +import ( + "testing" + "time" + + "github.com/coredns/coredns/plugin/pkg/cache" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" +) + +func TestCacheSet(t *testing.T) { + fPriv, rmPriv, _ := test.TempFile(".", privKey) + fPub, rmPub, _ := test.TempFile(".", pubKey) + defer rmPriv() + defer rmPub() + + dnskey, err := ParseKeyFile(fPub, fPriv) + if err != nil { + t.Fatalf("Failed to parse key: %v\n", err) + } + + c := cache.New(defaultCap) + m := testMsg() + state := request.Request{Req: m, Zone: "miek.nl."} + k := hash(m.Answer) // calculate *before* we add the sig + d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, false, nil, c) + d.Sign(state, time.Now().UTC(), server) + + _, ok := d.get(k, server) + if !ok { + t.Errorf("Signature was not added to the cache") + } +} + +func TestCacheNotValidExpired(t *testing.T) { + fPriv, rmPriv, _ := test.TempFile(".", privKey) + fPub, rmPub, _ := test.TempFile(".", pubKey) + defer rmPriv() + defer rmPub() + + dnskey, err := ParseKeyFile(fPub, fPriv) + if err != nil { + t.Fatalf("Failed to parse key: %v\n", err) + } + + c := cache.New(defaultCap) + m := testMsg() + state := request.Request{Req: m, Zone: "miek.nl."} + k := hash(m.Answer) // calculate *before* we add the sig + d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, false, nil, c) + d.Sign(state, time.Now().UTC().AddDate(0, 0, -9), server) + + _, ok := d.get(k, server) + if ok { + t.Errorf("Signature was added to the cache even though not valid") + } +} + +func TestCacheNotValidYet(t *testing.T) { + fPriv, rmPriv, _ := test.TempFile(".", privKey) + fPub, rmPub, _ := test.TempFile(".", pubKey) + defer rmPriv() + defer rmPub() + + dnskey, err := ParseKeyFile(fPub, fPriv) + if err != nil { + t.Fatalf("Failed to parse key: %v\n", err) + } + + c := cache.New(defaultCap) + m := testMsg() + state := request.Request{Req: m, Zone: "miek.nl."} + k := hash(m.Answer) // calculate *before* we add the sig + d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, false, nil, c) + d.Sign(state, time.Now().UTC().AddDate(0, 0, +9), server) + + _, ok := d.get(k, server) + if ok { + t.Errorf("Signature was added to the cache even though not valid yet") + } +} diff --git a/ag_201_coredns/plugin/dnssec/dnskey.go b/ag_201_coredns/plugin/dnssec/dnskey.go new file mode 100644 index 0000000..161db94 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/dnskey.go @@ -0,0 +1,95 @@ +package dnssec + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "errors" + "os" + "path/filepath" + "time" + + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + "golang.org/x/crypto/ed25519" +) + +// DNSKEY holds a DNSSEC public and private key used for on-the-fly signing. +type DNSKEY struct { + K *dns.DNSKEY + D *dns.DS + s crypto.Signer + tag uint16 +} + +// 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(filepath.Clean(pubFile)) + if e != nil { + return nil, e + } + defer f.Close() + k, e := dns.ReadRR(f, pubFile) + if e != nil { + return nil, e + } + + f, e = os.Open(filepath.Clean(privFile)) + if e != nil { + return nil, e + } + defer f.Close() + + dk, ok := k.(*dns.DNSKEY) + if !ok { + return nil, errors.New("no public key found") + } + p, e := dk.ReadPrivateKey(f, privFile) + if e != nil { + return nil, e + } + + if s, ok := p.(*rsa.PrivateKey); ok { + return &DNSKEY{K: dk, D: dk.ToDS(dns.SHA256), s: s, tag: dk.KeyTag()}, nil + } + if s, ok := p.(*ecdsa.PrivateKey); ok { + return &DNSKEY{K: dk, D: dk.ToDS(dns.SHA256), s: s, tag: dk.KeyTag()}, nil + } + if s, ok := p.(ed25519.PrivateKey); ok { + return &DNSKEY{K: dk, D: dk.ToDS(dns.SHA256), s: s, tag: dk.KeyTag()}, nil + } + return &DNSKEY{K: dk, D: dk.ToDS(dns.SHA256), s: nil, tag: 0}, errors.New("no private key found") +} + +// 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)) + for i, k := range d.keys { + keys[i] = dns.Copy(k.K) + keys[i].Header().Name = zone + } + m := new(dns.Msg) + m.SetReply(state.Req) + m.Answer = keys + if !do { + return m + } + + incep, expir := incepExpir(time.Now().UTC()) + if sigs, err := d.sign(keys, zone, 3600, incep, expir, server); err == nil { + m.Answer = append(m.Answer, sigs...) + } + return m +} + +// Return true if, and only if, this is a zone key with the SEP bit unset. This implies a ZSK (rfc4034 2.1.1). +func (k DNSKEY) isZSK() bool { + return k.K.Flags&(1<<8) == (1<<8) && k.K.Flags&1 == 0 +} + +// Return true if, and only if, this is a zone key with the SEP bit set. This implies a KSK (rfc4034 2.1.1). +func (k DNSKEY) isKSK() bool { + return k.K.Flags&(1<<8) == (1<<8) && k.K.Flags&1 == 1 +} diff --git a/ag_201_coredns/plugin/dnssec/dnssec.go b/ag_201_coredns/plugin/dnssec/dnssec.go new file mode 100644 index 0000000..9e050f5 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/dnssec.go @@ -0,0 +1,159 @@ +// Package dnssec implements a plugin that signs responses on-the-fly using +// NSEC black lies. +package dnssec + +import ( + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/cache" + "github.com/coredns/coredns/plugin/pkg/response" + "github.com/coredns/coredns/plugin/pkg/singleflight" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Dnssec signs the reply on-the-fly. +type Dnssec struct { + Next plugin.Handler + + zones []string + keys []*DNSKEY + splitkeys bool + inflight *singleflight.Group + cache *cache.Cache +} + +// New returns a new Dnssec. +func New(zones []string, keys []*DNSKEY, splitkeys bool, next plugin.Handler, c *cache.Cache) Dnssec { + return Dnssec{Next: next, + zones: zones, + keys: keys, + splitkeys: splitkeys, + cache: c, + inflight: new(singleflight.Group), + } +} + +// Sign signs the message in state. it takes care of negative or nodata responses. It +// uses NSEC black lies for authenticated denial of existence. For delegations it +// will insert DS records and sign those. +// Signatures will be cached for a short while. By default we sign for 8 days, +// starting 3 hours ago. +func (d Dnssec) Sign(state request.Request, now time.Time, server string) *dns.Msg { + req := state.Req + + incep, expir := incepExpir(now) + + mt, _ := response.Typify(req, time.Now().UTC()) // TODO(miek): need opt record here? + if mt == response.Delegation { + return req + } + + if mt == response.NameError || mt == response.NoData { + if req.Ns[0].Header().Rrtype != dns.TypeSOA || len(req.Ns) > 1 { + return req + } + + ttl := req.Ns[0].Header().Ttl + + if sigs, err := d.sign(req.Ns, state.Zone, ttl, incep, expir, server); err == nil { + req.Ns = append(req.Ns, sigs...) + } + if sigs, err := d.nsec(state, mt, ttl, incep, expir, server); err == nil { + req.Ns = append(req.Ns, sigs...) + } + if len(req.Ns) > 1 { // actually added nsec and sigs, reset the rcode + req.Rcode = dns.RcodeSuccess + } + return req + } + + for _, r := range rrSets(req.Answer) { + ttl := r[0].Header().Ttl + if sigs, err := d.sign(r, state.Zone, ttl, incep, expir, server); err == nil { + req.Answer = append(req.Answer, sigs...) + } + } + for _, r := range rrSets(req.Ns) { + ttl := r[0].Header().Ttl + if sigs, err := d.sign(r, state.Zone, ttl, incep, expir, server); err == nil { + req.Ns = append(req.Ns, sigs...) + } + } + for _, r := range rrSets(req.Extra) { + ttl := r[0].Header().Ttl + if sigs, err := d.sign(r, state.Zone, ttl, incep, expir, server); err == nil { + req.Extra = append(req.Extra, sigs...) + } + } + return req +} + +func (d Dnssec) sign(rrs []dns.RR, signerName string, ttl, incep, expir uint32, server string) ([]dns.RR, error) { + k := hash(rrs) + sgs, ok := d.get(k, server) + if ok { + return sgs, nil + } + + sigs, err := d.inflight.Do(k, func() (interface{}, error) { + var sigs []dns.RR + for _, k := range d.keys { + if d.splitkeys { + if len(rrs) > 0 && rrs[0].Header().Rrtype == dns.TypeDNSKEY { + // We are signing a DNSKEY RRSet. With split keys, we need to use a KSK here. + if !k.isKSK() { + continue + } + } else { + // For non-DNSKEY RRSets, we want to use a ZSK. + if !k.isZSK() { + continue + } + } + } + sig := k.newRRSIG(signerName, ttl, incep, expir) + if e := sig.Sign(k.s, rrs); e != nil { + return sigs, e + } + sigs = append(sigs, sig) + } + d.set(k, sigs) + return sigs, nil + }) + return sigs.([]dns.RR), err +} + +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(twoDays) + for _, rr := range s.([]dns.RR) { + if !rr.(*dns.RRSIG).ValidityPeriod(is75) { + cacheMisses.WithLabelValues(server).Inc() + return nil, false + } + } + + cacheHits.WithLabelValues(server).Inc() + return s.([]dns.RR), true + } + cacheMisses.WithLabelValues(server).Inc() + return nil, false +} + +func incepExpir(now time.Time) (uint32, uint32) { + incep := uint32(now.Add(-3 * time.Hour).Unix()) // -(2+1) hours, be sure to catch daylight saving time and such + expir := uint32(now.Add(eightDays).Unix()) // sign for 8 days + return incep, expir +} + +const ( + eightDays = 8 * 24 * time.Hour + twoDays = 2 * 24 * time.Hour + defaultCap = 10000 // default capacity of the cache. +) diff --git a/ag_201_coredns/plugin/dnssec/dnssec_test.go b/ag_201_coredns/plugin/dnssec/dnssec_test.go new file mode 100644 index 0000000..fb8a128 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/dnssec_test.go @@ -0,0 +1,208 @@ +package dnssec + +import ( + "testing" + "time" + + "github.com/coredns/coredns/plugin/pkg/cache" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +func TestZoneSigning(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testMsg() + state := request.Request{Req: m, Zone: "miek.nl."} + + m = d.Sign(state, time.Now().UTC(), server) + if !section(m.Answer, 1) { + t.Errorf("Answer section should have 1 RRSIG") + } + if !section(m.Ns, 1) { + t.Errorf("Authority section should have 1 RRSIG") + } +} + +func TestZoneSigningDouble(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + fPriv1, rmPriv1, _ := test.TempFile(".", privKey1) + fPub1, rmPub1, _ := test.TempFile(".", pubKey1) + defer rmPriv1() + defer rmPub1() + + key1, err := ParseKeyFile(fPub1, fPriv1) + if err != nil { + t.Fatalf("Failed to parse key: %v\n", err) + } + d.keys = append(d.keys, key1) + + m := testMsg() + state := request.Request{Req: m, Zone: "miek.nl."} + m = d.Sign(state, time.Now().UTC(), server) + if !section(m.Answer, 2) { + t.Errorf("Answer section should have 1 RRSIG") + } + if !section(m.Ns, 2) { + t.Errorf("Authority section should have 1 RRSIG") + } +} + +// TestSigningDifferentZone tests if a key for miek.nl and be used for example.org. +func TestSigningDifferentZone(t *testing.T) { + fPriv, rmPriv, _ := test.TempFile(".", privKey) + fPub, rmPub, _ := test.TempFile(".", pubKey) + defer rmPriv() + defer rmPub() + + key, err := ParseKeyFile(fPub, fPriv) + if err != nil { + t.Fatalf("Failed to parse key: %v\n", err) + } + + m := testMsgEx() + state := request.Request{Req: m, Zone: "example.org."} + c := cache.New(defaultCap) + d := New([]string{"example.org."}, []*DNSKEY{key}, false, nil, c) + m = d.Sign(state, time.Now().UTC(), server) + if !section(m.Answer, 1) { + t.Errorf("Answer section should have 1 RRSIG") + t.Logf("%+v\n", m) + } + if !section(m.Ns, 1) { + t.Errorf("Authority section should have 1 RRSIG") + t.Logf("%+v\n", m) + } +} + +func TestSigningCname(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testMsgCname() + state := request.Request{Req: m, Zone: "miek.nl."} + m = d.Sign(state, time.Now().UTC(), server) + if !section(m.Answer, 1) { + t.Errorf("Answer section should have 1 RRSIG") + } +} + +func TestSigningDname(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testMsgDname() + state := request.Request{Req: m, Zone: "miek.nl."} + // We sign *everything* we see, also the synthesized CNAME. + m = d.Sign(state, time.Now().UTC(), server) + if !section(m.Answer, 3) { + t.Errorf("Answer section should have 3 RRSIGs") + } +} + +func TestSigningEmpty(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testEmptyMsg() + m.SetQuestion("a.miek.nl.", dns.TypeA) + 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 RRSIGs") + } +} + +func section(rss []dns.RR, nrSigs int) bool { + i := 0 + for _, r := range rss { + if r.Header().Rrtype == dns.TypeRRSIG { + i++ + } + } + return nrSigs == i +} + +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.")}, + } +} +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.")}, + } +} + +func testMsgCname() *dns.Msg { + return &dns.Msg{ + Answer: []dns.RR{test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl.")}, + } +} + +func testMsgDname() *dns.Msg { + return &dns.Msg{ + Answer: []dns.RR{ + test.CNAME("a.dname.miek.nl. 1800 IN CNAME a.test.miek.nl."), + test.A("a.test.miek.nl. 1800 IN A 139.162.196.78"), + test.DNAME("dname.miek.nl. 1800 IN DNAME test.miek.nl."), + }, + } +} + +func testEmptyMsg() *dns.Msg { + // don't care about the message header + return &dns.Msg{ + Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA ns.miek.nl. dnsmaster.miek.nl. 2017100301 200 100 604800 3600")}, + } +} + +func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) { + k, rm1, rm2 := newKey(t) + c := cache.New(defaultCap) + d := New(zones, []*DNSKEY{k}, false, nil, c) + return d, rm1, rm2 +} + +func newKey(t *testing.T) (*DNSKEY, func(), func()) { + fPriv, rmPriv, _ := test.TempFile(".", privKey) + fPub, rmPub, _ := test.TempFile(".", pubKey) + + key, err := ParseKeyFile(fPub, fPriv) + if err != nil { + t.Fatalf("Failed to parse key: %v\n", err) + } + return key, rmPriv, rmPub +} + +const ( + pubKey = `miek.nl. IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4BXPP3gwhetiOUMnGA+x09nqzgF5IY OyjWB7N3rXqQbnOSILhH1hnuyh7mmA==` + privKey = `Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: /4BZk8AFvyW5hL3cOLSVxIp1RTqHSAEloWUxj86p3gs= +Created: 20160423195532 +Publish: 20160423195532 +Activate: 20160423195532 +` + pubKey1 = `example.org. IN DNSKEY 257 3 13 tVRWNSGpHZbCi7Pr7OmbADVUO3MxJ0Lb8Lk3o/HBHqCxf5K/J50lFqRa 98lkdAIiFOVRy8LyMvjwmxZKwB5MNw==` + privKey1 = `Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: i8j4OfDGT8CQt24SDwLz2hg9yx4qKOEOh1LvbAuSp1c= +Created: 20160423211746 +Publish: 20160423211746 +Activate: 20160423211746 +` +) diff --git a/ag_201_coredns/plugin/dnssec/handler.go b/ag_201_coredns/plugin/dnssec/handler.go new file mode 100644 index 0000000..1ab70ab --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/handler.go @@ -0,0 +1,50 @@ +package dnssec + +import ( + "context" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metrics" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// ServeDNS implements the plugin.Handler interface. +func (d Dnssec) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + + do := state.Do() + qname := state.Name() + qtype := state.QType() + zone := plugin.Zones(d.zones).Matches(qname) + if zone == "" { + return plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r) + } + + state.Zone = zone + server := metrics.WithServer(ctx) + + // Intercept queries for DNSKEY, but only if one of the zones matches the qname, otherwise we let + // the query through. + if qtype == dns.TypeDNSKEY { + for _, z := range d.zones { + if qname == z { + resp := d.getDNSKEY(state, z, do, server) + resp.Authoritative = true + w.WriteMsg(resp) + return dns.RcodeSuccess, nil + } + } + } + + if do { + drr := &ResponseWriter{w, d, server} + return plugin.NextOrFailure(d.Name(), d.Next, ctx, drr, r) + } + + return plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r) +} + +// Name implements the Handler interface. +func (d Dnssec) Name() string { return "dnssec" } diff --git a/ag_201_coredns/plugin/dnssec/handler_test.go b/ag_201_coredns/plugin/dnssec/handler_test.go new file mode 100644 index 0000000..a1e24b7 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/handler_test.go @@ -0,0 +1,182 @@ +package dnssec + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/pkg/cache" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var dnssecTestCases = []test.Case{ + { + Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, + Answer: []dns.RR{ + test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, Do: true, + Answer: []dns.RR{ + test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"), + test.RRSIG("miek.nl. 3600 IN RRSIG DNSKEY 13 2 3600 20160503150844 20160425120844 18512 miek.nl. Iw/kNOyM"), + }, + /* Extra: []dns.RR{test.OPT(4096, true)}, this has moved to the server and can't be test here */ + }, +} + +var dnsTestCases = []test.Case{ + { + Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, + Answer: []dns.RR{ + test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."), + }, + Ns: []dns.RR{ + test.NS("miek.nl. 1800 IN NS linode.atoom.net."), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeMX, Do: true, + Answer: []dns.RR{ + test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."), + test.RRSIG("miek.nl. 1800 IN RRSIG MX 13 2 3600 20160503192428 20160425162428 18512 miek.nl. 4nxuGKitXjPVA9zP1JIUvA09"), + }, + 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: "www.miek.nl.", Qtype: dns.TypeAAAA, Do: true, + Answer: []dns.RR{ + test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + test.RRSIG("a.miek.nl. 1800 IN RRSIG AAAA 13 3 3600 20160503193047 20160425163047 18512 miek.nl. UAyMG+gcnoXW3"), + test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."), + test.RRSIG("www.miek.nl. 1800 IN RRSIG CNAME 13 3 3600 20160503193047 20160425163047 18512 miek.nl. E3qGZn"), + }, + 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: "wwwww.miek.nl.", Qtype: dns.TypeAAAA, Do: true, + Ns: []dns.RR{ + test.RRSIG("miek.nl. 1800 IN RRSIG SOA 13 2 3600 20171220135446 20171212105446 18512 miek.nl. hCRzzjYz6w=="), + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + test.NSEC("wwwww.miek.nl. 1800 IN NSEC \\000.wwwww.miek.nl. A HINFO TXT LOC SRV CERT SSHFP RRSIG NSEC TLSA HIP OPENPGPKEY SPF"), + test.RRSIG("wwwww.miek.nl. 1800 IN RRSIG NSEC 13 3 3600 20171220135446 20171212105446 18512 miek.nl. cVUQWs8xw=="), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeHINFO, Do: true, + Ns: []dns.RR{ + test.NSEC("miek.nl. 1800 IN NSEC \\000.miek.nl. A 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 3600 20171220141741 20171212111741 18512 miek.nl. GuXROL7Uu+UiPcg=="), + 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: "www.example.org.", Qtype: dns.TypeAAAA, Do: true, + Rcode: dns.RcodeServerFailure, + }, +} + +func TestLookupZone(t *testing.T) { + zone, err := file.Parse(strings.NewReader(dbMiekNL), "miek.nl.", "stdin", 0) + if err != nil { + return + } + fm := file.File{Next: test.ErrorHandler(), Zones: file.Zones{Z: map[string]*file.Zone{"miek.nl.": zone}, Names: []string{"miek.nl."}}} + dnskey, rm1, rm2 := newKey(t) + defer rm1() + defer rm2() + c := cache.New(defaultCap) + dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, false, fm, c) + + for _, tc := range dnsTestCases { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := dh.ServeDNS(context.TODO(), rec, m) + if err != nil { + t.Errorf("Expected no error, got %v", err) + return + } + + if err := test.SortAndCheck(rec.Msg, tc); err != nil { + t.Error(err) + } + } +} + +func TestLookupDNSKEY(t *testing.T) { + dnskey, rm1, rm2 := newKey(t) + defer rm1() + defer rm2() + c := cache.New(defaultCap) + dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, false, test.ErrorHandler(), c) + + for _, tc := range dnssecTestCases { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := dh.ServeDNS(context.TODO(), rec, m) + if err != nil { + t.Errorf("Expected no error, got %v", err) + return + } + + resp := rec.Msg + if !resp.Authoritative { + t.Errorf("Authoritative Answer should be true, got false") + } + + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + + // If there is an NSEC present in authority section check if the bitmap does not have the qtype set. + for _, rr := range resp.Ns { + if n, ok := rr.(*dns.NSEC); ok { + for i := range n.TypeBitMap { + if n.TypeBitMap[i] == tc.Qtype { + t.Errorf("Bitmap contains qtype: %d", tc.Qtype) + } + } + } + } + } +} + +const dbMiekNL = ` +$TTL 30M +$ORIGIN miek.nl. +@ IN SOA linode.atoom.net. miek.miek.nl. ( + 1282630057 ; Serial + 4H ; Refresh + 1H ; Retry + 7D ; Expire + 4H ) ; Negative Cache TTL + IN NS linode.atoom.net. + + IN MX 1 aspmx.l.google.com. + + IN A 139.162.196.78 + IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 + +a IN A 139.162.196.78 + IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 +www IN CNAME a` diff --git a/ag_201_coredns/plugin/dnssec/log_test.go b/ag_201_coredns/plugin/dnssec/log_test.go new file mode 100644 index 0000000..e8f3a1d --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/log_test.go @@ -0,0 +1,5 @@ +package dnssec + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/dnssec/metrics.go b/ag_201_coredns/plugin/dnssec/metrics.go new file mode 100644 index 0000000..e69dbf5 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/metrics.go @@ -0,0 +1,32 @@ +package dnssec + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // cacheSize is the number of elements in the dnssec cache. + cacheSize = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: plugin.Namespace, + Subsystem: "dnssec", + Name: "cache_entries", + Help: "The number of elements in the dnssec cache.", + }, []string{"server", "type"}) + // cacheHits is the count of cache hits. + cacheHits = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "dnssec", + Name: "cache_hits_total", + Help: "The count of cache hits.", + }, []string{"server"}) + // cacheMisses is the count of cache misses. + cacheMisses = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "dnssec", + Name: "cache_misses_total", + Help: "The count of cache misses.", + }, []string{"server"}) +) diff --git a/ag_201_coredns/plugin/dnssec/responsewriter.go b/ag_201_coredns/plugin/dnssec/responsewriter.go new file mode 100644 index 0000000..355b317 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/responsewriter.go @@ -0,0 +1,43 @@ +package dnssec + +import ( + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// ResponseWriter signs the response on the fly. +type ResponseWriter struct { + dns.ResponseWriter + d Dnssec + server string // server label for metrics. +} + +// WriteMsg implements the dns.ResponseWriter interface. +func (d *ResponseWriter) WriteMsg(res *dns.Msg) error { + // By definition we should sign anything that comes back, we should still figure out for + // which zone it should be. + state := request.Request{W: d.ResponseWriter, Req: res} + + zone := plugin.Zones(d.d.zones).Matches(state.Name()) + if zone == "" { + return d.ResponseWriter.WriteMsg(res) + } + state.Zone = zone + + res = d.d.Sign(state, time.Now().UTC(), d.server) + cacheSize.WithLabelValues(d.server, "signature").Set(float64(d.d.cache.Len())) + // No need for EDNS0 trickery, as that is handled by the server. + + return d.ResponseWriter.WriteMsg(res) +} + +// Write implements the dns.ResponseWriter interface. +func (d *ResponseWriter) Write(buf []byte) (int, error) { + log.Warning("Dnssec called with Write: not signing reply") + n, err := d.ResponseWriter.Write(buf) + return n, err +} diff --git a/ag_201_coredns/plugin/dnssec/rrsig.go b/ag_201_coredns/plugin/dnssec/rrsig.go new file mode 100644 index 0000000..250a603 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/rrsig.go @@ -0,0 +1,53 @@ +package dnssec + +import "github.com/miekg/dns" + +// newRRSIG returns a new RRSIG, with all fields filled out, except the signed data. +func (k *DNSKEY) newRRSIG(signerName string, ttl, incep, expir uint32) *dns.RRSIG { + sig := new(dns.RRSIG) + + sig.Hdr.Rrtype = dns.TypeRRSIG + sig.Algorithm = k.K.Algorithm + sig.KeyTag = k.tag + sig.SignerName = signerName + sig.Hdr.Ttl = ttl + sig.OrigTtl = origTTL + + sig.Inception = incep + sig.Expiration = expir + + return sig +} + +type rrset struct { + qname string + qtype uint16 +} + +// rrSets returns rrs as a map of RRsets. It skips RRSIG and OPT records as those don't need to be signed. +func rrSets(rrs []dns.RR) map[rrset][]dns.RR { + m := make(map[rrset][]dns.RR) + + for _, r := range rrs { + if r.Header().Rrtype == dns.TypeRRSIG || r.Header().Rrtype == dns.TypeOPT { + continue + } + + if s, ok := m[rrset{r.Header().Name, r.Header().Rrtype}]; ok { + s = append(s, r) + m[rrset{r.Header().Name, r.Header().Rrtype}] = s + continue + } + + s := make([]dns.RR, 1, 3) + s[0] = r + m[rrset{r.Header().Name, r.Header().Rrtype}] = s + } + + if len(m) > 0 { + return m + } + return nil +} + +const origTTL = 3600 diff --git a/ag_201_coredns/plugin/dnssec/setup.go b/ag_201_coredns/plugin/dnssec/setup.go new file mode 100644 index 0000000..7820e93 --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/setup.go @@ -0,0 +1,146 @@ +package dnssec + +import ( + "fmt" + "path/filepath" + "strconv" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/cache" + clog "github.com/coredns/coredns/plugin/pkg/log" +) + +var log = clog.NewWithPlugin("dnssec") + +func init() { plugin.Register("dnssec", setup) } + +func setup(c *caddy.Controller) error { + zones, keys, capacity, splitkeys, err := dnssecParse(c) + if err != nil { + return plugin.Error("dnssec", err) + } + + 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) + }) + + return nil +} + +func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, bool, error) { + zones := []string{} + keys := []*DNSKEY{} + capacity := defaultCap + + i := 0 + for c.Next() { + if i > 0 { + return nil, nil, 0, false, plugin.ErrOnce + } + i++ + + // dnssec [zones...] + zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) + + for c.NextBlock() { + switch x := c.Val(); x { + case "key": + k, e := keyParse(c) + if e != nil { + return nil, nil, 0, false, e + } + keys = append(keys, k...) + case "cache_capacity": + if !c.NextArg() { + return nil, nil, 0, false, c.ArgErr() + } + value := c.Val() + cacheCap, err := strconv.Atoi(value) + if err != nil { + return nil, nil, 0, false, err + } + capacity = cacheCap + default: + return nil, nil, 0, false, c.Errf("unknown property '%s'", x) + } + } + } + // Check if we have both KSKs and ZSKs. + zsk, ksk := 0, 0 + for _, k := range keys { + if k.isKSK() { + ksk++ + } else if k.isZSK() { + zsk++ + } + } + splitkeys := zsk > 0 && ksk > 0 + + // 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 + } + } + if !ok { + return zones, keys, capacity, splitkeys, fmt.Errorf("key %s (keyid: %d) can not sign any of the zones", string(kname), k.tag) + } + } + + return zones, keys, capacity, splitkeys, nil +} + +func keyParse(c *caddy.Controller) ([]*DNSKEY, error) { + keys := []*DNSKEY{} + config := dnsserver.GetConfig(c) + + if !c.NextArg() { + return nil, c.ArgErr() + } + value := c.Val() + if value == "file" { + ks := c.RemainingArgs() + if len(ks) == 0 { + return nil, c.ArgErr() + } + + for _, k := range ks { + base := k + // Kmiek.nl.+013+26205.key, handle .private or without extension: Kmiek.nl.+013+26205 + if strings.HasSuffix(k, ".key") { + base = k[:len(k)-4] + } + if strings.HasSuffix(k, ".private") { + base = k[:len(k)-8] + } + if !filepath.IsAbs(base) && config.Root != "" { + base = filepath.Join(config.Root, base) + } + k, err := ParseKeyFile(base+".key", base+".private") + if err != nil { + return nil, err + } + keys = append(keys, k) + } + } + return keys, nil +} diff --git a/ag_201_coredns/plugin/dnssec/setup_test.go b/ag_201_coredns/plugin/dnssec/setup_test.go new file mode 100644 index 0000000..66ff45f --- /dev/null +++ b/ag_201_coredns/plugin/dnssec/setup_test.go @@ -0,0 +1,160 @@ +package dnssec + +import ( + "os" + "strings" + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupDnssec(t *testing.T) { + 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 := 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 := 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 := 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") }() + + tests := []struct { + input string + shouldErr bool + expectedZones []string + expectedKeys []string + expectedSplitkeys bool + expectedCapacity int + expectedErrContent string + }{ + {`dnssec`, false, nil, nil, false, defaultCap, ""}, + {`dnssec example.org`, false, []string{"example.org."}, nil, false, defaultCap, ""}, + {`dnssec 10.0.0.0/8`, false, []string{"10.in-addr.arpa."}, nil, false, defaultCap, ""}, + { + `dnssec example.org { + cache_capacity 100 + }`, false, []string{"example.org."}, nil, false, 100, "", + }, + { + `dnssec cluster.local { + key file Kcluster.local + }`, false, []string{"cluster.local."}, nil, false, defaultCap, "", + }, + { + `dnssec example.org cluster.local { + key file Kcluster.local + }`, false, []string{"example.org.", "cluster.local."}, nil, false, defaultCap, "", + }, + // fails + { + `dnssec example.org { + key file Kcluster.local + }`, true, []string{"example.org."}, nil, false, defaultCap, "can not sign any", + }, + { + `dnssec example.org { + key + }`, true, []string{"example.org."}, nil, false, defaultCap, "argument count", + }, + { + `dnssec example.org { + key file + }`, true, []string{"example.org."}, nil, false, defaultCap, "argument count", + }, + {`dnssec + dnssec`, true, nil, nil, false, defaultCap, ""}, + { + `dnssec cluster.local { + key file Kcluster.local + key file ksk_Kcluster.local + }`, false, []string{"cluster.local."}, nil, true, defaultCap, "", + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + zones, keys, capacity, splitkeys, err := dnssecParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, 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) + } + } + if !test.shouldErr { + for i, z := range test.expectedZones { + if zones[i] != z { + t.Errorf("Dnssec not correctly set for input %s. Expected: %s, actual: %s", test.input, z, zones[i]) + } + } + for i, k := range test.expectedKeys { + if k != keys[i].K.Header().Name { + t.Errorf("Dnssec not correctly set for input %s. Expected: '%s', actual: '%s'", test.input, k, keys[i].K.Header().Name) + } + } + if splitkeys != test.expectedSplitkeys { + t.Errorf("Detected split keys does not match. Expected: %t, actual %t", test.expectedSplitkeys, splitkeys) + } + if capacity != test.expectedCapacity { + t.Errorf("Dnssec not correctly set capacity for input '%s' Expected: '%d', actual: '%d'", test.input, capacity, test.expectedCapacity) + } + } + } +} + +const keypub = `; This is a zone-signing key, keyid 45330, for cluster.local. +; Created: 20170901060531 (Fri Sep 1 08:05:31 2017) +; Publish: 20170901060531 (Fri Sep 1 08:05:31 2017) +; Activate: 20170901060531 (Fri Sep 1 08:05:31 2017) +cluster.local. IN DNSKEY 256 3 5 AwEAAcFpDv+Cb23kFJowu+VU++b2N1uEHi6Ll9H0BzLasFOdJjEEclCO q/KlD4682vOMXxJNN8ZwOyiCa7Y0TEYqSwWvhHyn3bHCwuy4I6fss4Wd 7Y9dU+6QTgJ8LimGG40Iizjc9zqoU8Q+q81vIukpYWOHioHoY7hsWBvS RSlzDJk3` + +const keypriv = `Private-key-format: v1.3 +Algorithm: 5 (RSASHA1) +Modulus: wWkO/4JvbeQUmjC75VT75vY3W4QeLouX0fQHMtqwU50mMQRyUI6r8qUPjrza84xfEk03xnA7KIJrtjRMRipLBa+EfKfdscLC7Lgjp+yzhZ3tj11T7pBOAnwuKYYbjQiLONz3OqhTxD6rzW8i6SlhY4eKgehjuGxYG9JFKXMMmTc= +PublicExponent: AQAB +PrivateExponent: K5XyZFBPrjMVFX5gCZlyPyVDamNGrfSVXSIiMSqpS96BSdCXtmHAjCj4bZFPwkzi6+vs4tJN8p4ZifEVM0a6qwPZyENBrc2qbsweOXE6l8BaPVWFX30xvVRzGXuNtXxlBXE17zoHty5r5mRyRou1bc2HUS5otdkEjE30RiocQVk= +Prime1: 7RRFUxaZkVNVH1DaT/SV5Sb8kABB389qLwU++argeDCVf+Wm9BBlTrsz2U6bKlfpaUmYZKtCCd+CVxqzMyuu0w== +Prime2: 0NiY3d7Fa08IGY9L4TaFc02A721YcDNBBf95BP31qGvwnYsLFM/1xZwaEsIjohg8g+m/GpyIlvNMbK6pywIVjQ== +Exponent1: XjXO8pype9mMmvwrNNix9DTQ6nxfsQugW30PMHGZ78kGr6NX++bEC0xS50jYWjRDGcbYGzD+9iNujSScD3qNZw== +Exponent2: wkoOhLIfhUIj7etikyUup2Ld5WAbW15DSrotstg0NrgcQ+Q7reP96BXeJ79WeREFE09cyvv/EjdLzPv81/CbbQ== +Coefficient: ah4LL0KLTO8kSKHK+X9Ud8grYi94QSNdbX11ge/eFcS/41QhDuZRTAFv4y0+IG+VWd+XzojLsQs+jzLe5GzINg== +Created: 20170901060531 +Publish: 20170901060531 +Activate: 20170901060531 +` + +const kskpub = `; This is a zone-signing key, keyid 45330, for cluster.local. +; Created: 20170901060531 (Fri Sep 1 08:05:31 2017) +; Publish: 20170901060531 (Fri Sep 1 08:05:31 2017) +; Activate: 20170901060531 (Fri Sep 1 08:05:31 2017) +cluster.local. IN DNSKEY 257 3 5 AwEAAcFpDv+Cb23kFJowu+VU++b2N1uEHi6Ll9H0BzLasFOdJjEEclCO q/KlD4682vOMXxJNN8ZwOyiCa7Y0TEYqSwWvhHyn3bHCwuy4I6fss4Wd 7Y9dU+6QTgJ8LimGG40Iizjc9zqoU8Q+q81vIukpYWOHioHoY7hsWBvS RSlzDJk3` + +const kskpriv = `Private-key-format: v1.3 +Algorithm: 5 (RSASHA1) +Modulus: wWkO/4JvbeQUmjC75VT75vY3W4QeLouX0fQHMtqwU50mMQRyUI6r8qUPjrza84xfEk03xnA7KIJrtjRMRipLBa+EfKfdscLC7Lgjp+yzhZ3tj11T7pBOAnwuKYYbjQiLONz3OqhTxD6rzW8i6SlhY4eKgehjuGxYG9JFKXMMmTc= +PublicExponent: AQAB +PrivateExponent: K5XyZFBPrjMVFX5gCZlyPyVDamNGrfSVXSIiMSqpS96BSdCXtmHAjCj4bZFPwkzi6+vs4tJN8p4ZifEVM0a6qwPZyENBrc2qbsweOXE6l8BaPVWFX30xvVRzGXuNtXxlBXE17zoHty5r5mRyRou1bc2HUS5otdkEjE30RiocQVk= +Prime1: 7RRFUxaZkVNVH1DaT/SV5Sb8kABB389qLwU++argeDCVf+Wm9BBlTrsz2U6bKlfpaUmYZKtCCd+CVxqzMyuu0w== +Prime2: 0NiY3d7Fa08IGY9L4TaFc02A721YcDNBBf95BP31qGvwnYsLFM/1xZwaEsIjohg8g+m/GpyIlvNMbK6pywIVjQ== +Exponent1: XjXO8pype9mMmvwrNNix9DTQ6nxfsQugW30PMHGZ78kGr6NX++bEC0xS50jYWjRDGcbYGzD+9iNujSScD3qNZw== +Exponent2: wkoOhLIfhUIj7etikyUup2Ld5WAbW15DSrotstg0NrgcQ+Q7reP96BXeJ79WeREFE09cyvv/EjdLzPv81/CbbQ== +Coefficient: ah4LL0KLTO8kSKHK+X9Ud8grYi94QSNdbX11ge/eFcS/41QhDuZRTAFv4y0+IG+VWd+XzojLsQs+jzLe5GzINg== +Created: 20170901060531 +Publish: 20170901060531 +Activate: 20170901060531 +` diff --git a/ag_201_coredns/plugin/dnstap/README.md b/ag_201_coredns/plugin/dnstap/README.md new file mode 100644 index 0000000..8e56157 --- /dev/null +++ b/ag_201_coredns/plugin/dnstap/README.md @@ -0,0 +1,124 @@ +# dnstap + +## Name + +*dnstap* - enables logging to dnstap. + +## Description + +dnstap is a flexible, structured binary log format for DNS software; see https://dnstap.info. With this +plugin you make CoreDNS output dnstap logging. + +Every message is sent to the socket as soon as it comes in, the *dnstap* plugin has a buffer of +10000 messages, above that number dnstap messages will be dropped (this is logged). + +## Syntax + +~~~ txt +dnstap SOCKET [full] { + [identity IDENTITY] + [version VERSION] +} +~~~ + +* **SOCKET** is the socket (path) supplied to the dnstap command line tool. +* `full` to include the wire-format DNS message. +* **IDENTITY** to override the identity of the server. Defaults to the hostname. +* **VERSION** to override the version field. Defaults to the CoreDNS version. + +## Examples + +Log information about client requests and responses to */tmp/dnstap.sock*. + +~~~ txt +dnstap /tmp/dnstap.sock +~~~ + +Log information including the wire-format DNS message about client requests and responses to */tmp/dnstap.sock*. + +~~~ txt +dnstap unix:///tmp/dnstap.sock full +~~~ + +Log to a remote endpoint. + +~~~ txt +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 +} +~~~ + +## 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. + +The following command listens on the given socket and decodes messages to stdout. + +~~~ sh +$ dnstap -u /tmp/dnstap.sock +~~~ + +The following command listens on the given socket and saves message payloads to a binary dnstap-format log file. + +~~~ sh +$ dnstap -u /tmp/dnstap.sock -w /tmp/test.dnstap +~~~ + +Listen for dnstap messages on port 6000. + +~~~ sh +$ 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: + +~~~ go +c.OnStartup(func() error { + if taph := dnsserver.GetConfig(c).Handler("dnstap"); taph != nil { + if tapPlugin, ok := taph.(dnstap.Dnstap); ok { + f.tapPlugin = &tapPlugin + } + } + return nil +}) +~~~ + +And then in your plugin: + +~~~ go +func (x RandomPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if tapPlugin != nil { + q := new(msg.Msg) + msg.SetQueryTime(q, time.Now()) + msg.SetQueryAddress(q, w.RemoteAddr()) + if tapPlugin.IncludeRawMessage { + buf, _ := r.Pack() // r has been seen packed/unpacked before, this should not fail + q.QueryMessage = buf + } + msg.SetType(q, tap.Message_CLIENT_QUERY) + tapPlugin.TapMessage(q) + } + // ... +} +~~~ + +## See Also + +The website [dnstap.info](https://dnstap.info) has info on the dnstap protocol. The *forward* +plugin's `dnstap.go` uses dnstap to tap messages sent to an upstream. diff --git a/ag_201_coredns/plugin/dnstap/encoder.go b/ag_201_coredns/plugin/dnstap/encoder.go new file mode 100644 index 0000000..93d3e73 --- /dev/null +++ b/ag_201_coredns/plugin/dnstap/encoder.go @@ -0,0 +1,40 @@ +package dnstap + +import ( + "io" + "time" + + tap "github.com/dnstap/golang-dnstap" + fs "github.com/farsightsec/golang-framestream" + "google.golang.org/protobuf/proto" +) + +// encoder wraps a golang-framestream.Encoder. +type encoder struct { + fs *fs.Encoder +} + +func newEncoder(w io.Writer, timeout time.Duration) (*encoder, error) { + fs, err := fs.NewEncoder(w, &fs.EncoderOptions{ + ContentType: []byte("protobuf:dnstap.Dnstap"), + Bidirectional: true, + Timeout: timeout, + }) + if err != nil { + return nil, err + } + return &encoder{fs}, nil +} + +func (e *encoder) writeMsg(msg *tap.Dnstap) error { + buf, err := proto.Marshal(msg) + if err != nil { + return err + } + + _, err = e.fs.Write(buf) // n < len(buf) should return an error? + return err +} + +func (e *encoder) flush() error { return e.fs.Flush() } +func (e *encoder) close() error { return e.fs.Close() } diff --git a/ag_201_coredns/plugin/dnstap/handler.go b/ag_201_coredns/plugin/dnstap/handler.go new file mode 100644 index 0000000..a4b1acf --- /dev/null +++ b/ag_201_coredns/plugin/dnstap/handler.go @@ -0,0 +1,61 @@ +package dnstap + +import ( + "context" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/dnstap/msg" + + tap "github.com/dnstap/golang-dnstap" + "github.com/miekg/dns" +) + +// Dnstap is the dnstap handler. +type Dnstap struct { + Next plugin.Handler + io tapper + + // IncludeRawMessage will include the raw DNS message into the dnstap messages if true. + IncludeRawMessage bool + Identity []byte + Version []byte +} + +// TapMessage sends the message m to the dnstap interface. +func (h Dnstap) TapMessage(m *tap.Message) { + t := tap.Dnstap_MESSAGE + h.io.Dnstap(&tap.Dnstap{Type: &t, Message: m, Identity: h.Identity, Version: h.Version}) +} + +func (h Dnstap) tapQuery(w dns.ResponseWriter, query *dns.Msg, queryTime time.Time) { + q := new(tap.Message) + msg.SetQueryTime(q, queryTime) + msg.SetQueryAddress(q, w.RemoteAddr()) + + if h.IncludeRawMessage { + buf, _ := query.Pack() + q.QueryMessage = buf + } + msg.SetType(q, tap.Message_CLIENT_QUERY) + h.TapMessage(q) +} + +// 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) { + rw := &ResponseWriter{ + ResponseWriter: w, + Dnstap: h, + query: r, + 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) + + return plugin.NextOrFailure(h.Name(), h.Next, ctx, rw, r) +} + +// Name implements the plugin.Plugin interface. +func (h Dnstap) Name() string { return "dnstap" } diff --git a/ag_201_coredns/plugin/dnstap/handler_test.go b/ag_201_coredns/plugin/dnstap/handler_test.go new file mode 100644 index 0000000..2c54f70 --- /dev/null +++ b/ag_201_coredns/plugin/dnstap/handler_test.go @@ -0,0 +1,84 @@ +package dnstap + +import ( + "context" + "net" + "testing" + + "github.com/coredns/coredns/plugin/dnstap/msg" + 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) { + 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, + } + _, err := h.ServeDNS(context.TODO(), &test.ResponseWriter{}, q) + if err != nil { + t.Fatal(err) + } +} + +type writer struct { + t *testing.T + queue []*tap.Message +} + +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 + + if string(ex.QueryAddress) != string(got.QueryAddress) { + w.t.Errorf("Expected source address %s, got %s", ex.QueryAddress, got.QueryAddress) + } + if string(ex.ResponseAddress) != string(got.ResponseAddress) { + w.t.Errorf("Expected response address %s, got %s", ex.ResponseAddress, got.ResponseAddress) + } + if *ex.QueryPort != *got.QueryPort { + w.t.Errorf("Expected port %d, got %d", *ex.QueryPort, *got.QueryPort) + } + if *ex.SocketFamily != *got.SocketFamily { + w.t.Errorf("Expected socket family %d, got %d", *ex.SocketFamily, *got.SocketFamily) + } + w.queue = w.queue[1:] +} + +func TestDnstap(t *testing.T) { + q := test.Case{Qname: "example.org", Qtype: dns.TypeA}.Msg() + r := test.Case{ + Qname: "example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + 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) +} + +func testMessage() *tap.Message { + inet := tap.SocketFamily_INET + udp := tap.SocketProtocol_UDP + port := uint32(40212) + return &tap.Message{ + SocketFamily: &inet, + SocketProtocol: &udp, + QueryAddress: net.ParseIP("10.240.0.1"), + QueryPort: &port, + } +} diff --git a/ag_201_coredns/plugin/dnstap/io.go b/ag_201_coredns/plugin/dnstap/io.go new file mode 100644 index 0000000..d15d566 --- /dev/null +++ b/ag_201_coredns/plugin/dnstap/io.go @@ -0,0 +1,121 @@ +package dnstap + +import ( + "net" + "sync/atomic" + "time" + + tap "github.com/dnstap/golang-dnstap" +) + +const ( + tcpWriteBufSize = 1024 * 1024 // there is no good explanation for why this number has this value. + queueSize = 10000 // idem. + + tcpTimeout = 4 * time.Second + flushTimeout = 1 * time.Second +) + +// tapper interface is used in testing to mock the Dnstap method. +type tapper interface { + Dnstap(*tap.Dnstap) +} + +// dio implements the Tapper interface. +type dio struct { + endpoint string + proto string + enc *encoder + queue chan *tap.Dnstap + dropped uint32 + quit chan struct{} + flushTimeout time.Duration + tcpTimeout time.Duration +} + +// newIO returns a new and initialized pointer to a dio. +func newIO(proto, endpoint string) *dio { + return &dio{ + endpoint: endpoint, + proto: proto, + queue: make(chan *tap.Dnstap, queueSize), + quit: make(chan struct{}), + flushTimeout: flushTimeout, + tcpTimeout: tcpTimeout, + } +} + +func (d *dio) dial() error { + 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.SetNoDelay(false) + } + + d.enc, err = newEncoder(conn, d.tcpTimeout) + return err +} + +// Connect connects to the dnstap endpoint. +func (d *dio) connect() error { + err := d.dial() + go d.serve() + return err +} + +// Dnstap enqueues the payload for log. +func (d *dio) Dnstap(payload *tap.Dnstap) { + select { + case d.queue <- payload: + default: + atomic.AddUint32(&d.dropped, 1) + } +} + +// close waits until the I/O routine is finished to return. +func (d *dio) close() { close(d.quit) } + +func (d *dio) write(payload *tap.Dnstap) error { + if d.enc == nil { + atomic.AddUint32(&d.dropped, 1) + return nil + } + if err := d.enc.writeMsg(payload); err != nil { + atomic.AddUint32(&d.dropped, 1) + return err + } + return nil +} + +func (d *dio) serve() { + timeout := time.NewTimer(d.flushTimeout) + defer timeout.Stop() + for { + timeout.Reset(d.flushTimeout) + select { + case <-d.quit: + if d.enc == nil { + return + } + d.enc.flush() + d.enc.close() + return + case payload := <-d.queue: + if err := d.write(payload); err != nil { + d.dial() + } + case <-timeout.C: + if dropped := atomic.SwapUint32(&d.dropped, 0); dropped > 0 { + log.Warningf("Dropped dnstap messages: %d", dropped) + } + if d.enc == nil { + d.dial() + } else { + d.enc.flush() + } + } + } +} diff --git a/ag_201_coredns/plugin/dnstap/io_test.go b/ag_201_coredns/plugin/dnstap/io_test.go new file mode 100644 index 0000000..3e94f05 --- /dev/null +++ b/ag_201_coredns/plugin/dnstap/io_test.go @@ -0,0 +1,155 @@ +package dnstap + +import ( + "net" + "sync" + "testing" + "time" + + "github.com/coredns/coredns/plugin/pkg/reuseport" + + tap "github.com/dnstap/golang-dnstap" + fs "github.com/farsightsec/golang-framestream" +) + +var ( + msgType = tap.Dnstap_MESSAGE + tmsg = tap.Dnstap{Type: &msgType} +) + +func accept(t *testing.T, l net.Listener, count int) { + server, err := l.Accept() + if err != nil { + t.Fatalf("Server accepted: %s", err) + } + dec, err := fs.NewDecoder(server, &fs.DecoderOptions{ + ContentType: []byte("protobuf:dnstap.Dnstap"), + Bidirectional: true, + }) + if err != nil { + t.Fatalf("Server decoder: %s", err) + } + + for i := 0; i < count; i++ { + if _, err := dec.Decode(); err != nil { + t.Errorf("Server decode: %s", err) + } + } + + if err := server.Close(); err != nil { + t.Error(err) + } +} + +func TestTransport(t *testing.T) { + transport := [2][2]string{ + {"tcp", ":0"}, + {"unix", "dnstap.sock"}, + } + + for _, param := range transport { + l, err := reuseport.Listen(param[0], param[1]) + if err != nil { + t.Fatalf("Cannot start listener: %s", err) + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + accept(t, l, 1) + wg.Done() + }() + + dio := newIO(param[0], l.Addr().String()) + dio.tcpTimeout = 10 * time.Millisecond + dio.flushTimeout = 30 * time.Millisecond + dio.connect() + + dio.Dnstap(&tmsg) + + wg.Wait() + l.Close() + dio.close() + } +} + +func TestRace(t *testing.T) { + count := 10 + + l, err := reuseport.Listen("tcp", ":0") + if err != nil { + t.Fatalf("Cannot start listener: %s", err) + } + defer l.Close() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + accept(t, l, count) + wg.Done() + }() + + dio := newIO("tcp", l.Addr().String()) + dio.tcpTimeout = 10 * time.Millisecond + dio.flushTimeout = 30 * time.Millisecond + dio.connect() + defer dio.close() + + wg.Add(count) + for i := 0; i < count; i++ { + go func() { + tmsg := tap.Dnstap_MESSAGE + dio.Dnstap(&tap.Dnstap{Type: &tmsg}) + wg.Done() + }() + } + wg.Wait() +} + +func TestReconnect(t *testing.T) { + count := 5 + + l, err := reuseport.Listen("tcp", ":0") + if err != nil { + t.Fatalf("Cannot start listener: %s", err) + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + accept(t, l, 1) + wg.Done() + }() + + addr := l.Addr().String() + dio := newIO("tcp", addr) + dio.tcpTimeout = 10 * time.Millisecond + dio.flushTimeout = 30 * time.Millisecond + dio.connect() + defer dio.close() + + dio.Dnstap(&tmsg) + + wg.Wait() + + // Close listener + l.Close() + // And start TCP listener again on the same port + l, err = reuseport.Listen("tcp", addr) + if err != nil { + t.Fatalf("Cannot start listener: %s", err) + } + defer l.Close() + + wg.Add(1) + go func() { + accept(t, l, 1) + wg.Done() + }() + + for i := 0; i < count; i++ { + time.Sleep(100 * time.Millisecond) + dio.Dnstap(&tmsg) + } + wg.Wait() +} diff --git a/ag_201_coredns/plugin/dnstap/log_test.go b/ag_201_coredns/plugin/dnstap/log_test.go new file mode 100644 index 0000000..145aa1d --- /dev/null +++ b/ag_201_coredns/plugin/dnstap/log_test.go @@ -0,0 +1,5 @@ +package dnstap + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/dnstap/msg/msg.go b/ag_201_coredns/plugin/dnstap/msg/msg.go new file mode 100644 index 0000000..f9d84c4 --- /dev/null +++ b/ag_201_coredns/plugin/dnstap/msg/msg.go @@ -0,0 +1,97 @@ +package msg + +import ( + "fmt" + "net" + "time" + + tap "github.com/dnstap/golang-dnstap" +) + +var ( + protoUDP = tap.SocketProtocol_UDP + protoTCP = tap.SocketProtocol_TCP + familyINET = tap.SocketFamily_INET + familyINET6 = tap.SocketFamily_INET6 +) + +// SetQueryAddress adds the query address to the message. This also sets the SocketFamily and SocketProtocol. +func SetQueryAddress(t *tap.Message, addr net.Addr) error { + t.SocketFamily = &familyINET + switch a := addr.(type) { + case *net.TCPAddr: + t.SocketProtocol = &protoTCP + t.QueryAddress = a.IP + + p := uint32(a.Port) + t.QueryPort = &p + + if a.IP.To4() == nil { + t.SocketFamily = &familyINET6 + } + return nil + case *net.UDPAddr: + t.SocketProtocol = &protoUDP + t.QueryAddress = a.IP + + p := uint32(a.Port) + t.QueryPort = &p + + if a.IP.To4() == nil { + t.SocketFamily = &familyINET6 + } + return nil + default: + return fmt.Errorf("unknown address type: %T", a) + } +} + +// SetResponseAddress the response address to the message. This also sets the SocketFamily and SocketProtocol. +func SetResponseAddress(t *tap.Message, addr net.Addr) error { + t.SocketFamily = &familyINET + switch a := addr.(type) { + case *net.TCPAddr: + t.SocketProtocol = &protoTCP + t.ResponseAddress = a.IP + + p := uint32(a.Port) + t.ResponsePort = &p + + if a.IP.To4() == nil { + t.SocketFamily = &familyINET6 + } + return nil + case *net.UDPAddr: + t.SocketProtocol = &protoUDP + t.ResponseAddress = a.IP + + p := uint32(a.Port) + t.ResponsePort = &p + + if a.IP.To4() == nil { + t.SocketFamily = &familyINET6 + } + return nil + default: + return fmt.Errorf("unknown address type: %T", a) + } +} + +// SetQueryTime sets the time of the query in t. +func SetQueryTime(t *tap.Message, ti time.Time) { + qts := uint64(ti.Unix()) + qtn := uint32(ti.Nanosecond()) + t.QueryTimeSec = &qts + t.QueryTimeNsec = &qtn +} + +// SetResponseTime sets the time of the response in t. +func SetResponseTime(t *tap.Message, ti time.Time) { + rts := uint64(ti.Unix()) + rtn := uint32(ti.Nanosecond()) + t.ResponseTimeSec = &rts + t.ResponseTimeNsec = &rtn +} + +// SetType sets the type in t. +func SetType(t *tap.Message, typ tap.Message_Type) { t.Type = &typ } diff --git a/ag_201_coredns/plugin/dnstap/setup.go b/ag_201_coredns/plugin/dnstap/setup.go new file mode 100644 index 0000000..d7d1cdc --- /dev/null +++ b/ag_201_coredns/plugin/dnstap/setup.go @@ -0,0 +1,103 @@ +package dnstap + +import ( + "net/url" + "os" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" +) + +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 := "" + + args := c.RemainingArgs() + + if len(args) == 0 { + return d, c.ArgErr() + } + + endpoint = args[0] + + if strings.HasPrefix(endpoint, "tcp://") { + // remote network endpoint + endpointURL, err := url.Parse(endpoint) + if err != nil { + return d, c.ArgErr() + } + dio := newIO("tcp", endpointURL.Host) + d = Dnstap{io: dio} + } else { + endpoint = strings.TrimPrefix(endpoint, "unix://") + dio := newIO("unix", endpoint) + d = Dnstap{io: dio} + } + + d.IncludeRawMessage = len(args) == 2 && args[1] == "full" + + hostname, _ := os.Hostname() + d.Identity = []byte(hostname) + d.Version = []byte(caddy.AppName + "-" + caddy.AppVersion) + + for c.NextBlock() { + switch c.Val() { + case "identity": + { + if !c.NextArg() { + return d, c.ArgErr() + } + d.Identity = []byte(c.Val()) + } + case "version": + { + if !c.NextArg() { + return d, c.ArgErr() + } + d.Version = []byte(c.Val()) + } + } + } + + return d, nil +} + +func setup(c *caddy.Controller) error { + dnstap, 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 + }) + + return nil +} diff --git a/ag_201_coredns/plugin/dnstap/setup_test.go b/ag_201_coredns/plugin/dnstap/setup_test.go new file mode 100644 index 0000000..9d5f20a --- /dev/null +++ b/ag_201_coredns/plugin/dnstap/setup_test.go @@ -0,0 +1,60 @@ +package dnstap + +import ( + "os" + "testing" + + "github.com/coredns/caddy" +) + +func TestConfig(t *testing.T) { + hostname, _ := os.Hostname() + tests := []struct { + in string + endpoint string + full bool + proto string + fail bool + identity []byte + version []byte + }{ + {"dnstap dnstap.sock full", "dnstap.sock", true, "unix", false, []byte(hostname), []byte("-")}, + {"dnstap unix://dnstap.sock", "dnstap.sock", false, "unix", false, []byte(hostname), []byte("-")}, + {"dnstap tcp://127.0.0.1:6000", "127.0.0.1:6000", false, "tcp", false, []byte(hostname), []byte("-")}, + {"dnstap tcp://[::1]:6000", "[::1]:6000", false, "tcp", false, []byte(hostname), []byte("-")}, + {"dnstap tcp://example.com:6000", "example.com:6000", false, "tcp", false, []byte(hostname), []byte("-")}, + {"dnstap", "fail", false, "tcp", true, []byte(hostname), []byte("-")}, + {"dnstap dnstap.sock full {\nidentity NAME\nversion VER\n}\n", "dnstap.sock", true, "unix", false, []byte("NAME"), []byte("VER")}, + {"dnstap dnstap.sock {\nidentity NAME\nversion VER\n}\n", "dnstap.sock", false, "unix", false, []byte("NAME"), []byte("VER")}, + {"dnstap {\nidentity NAME\nversion VER\n}\n", "fail", false, "tcp", true, []byte("NAME"), []byte("VER")}, + } + for i, tc := range tests { + c := caddy.NewTestController("dns", tc.in) + tap, err := parseConfig(c) + if tc.fail && err == nil { + t.Fatalf("Test %d: expected test to fail: %s: %s", i, tc.in, err) + } + if tc.fail { + continue + } + + 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) + } + if x := string(tap.Identity); x != string(tc.identity) { + t.Errorf("Test %d: expected identity %s, got %s", i, tc.identity, x) + } + if x := string(tap.Version); x != string(tc.version) { + t.Errorf("Test %d: expected version %s, got %s", i, tc.version, x) + } + } +} diff --git a/ag_201_coredns/plugin/dnstap/writer.go b/ag_201_coredns/plugin/dnstap/writer.go new file mode 100644 index 0000000..1772634 --- /dev/null +++ b/ag_201_coredns/plugin/dnstap/writer.go @@ -0,0 +1,40 @@ +package dnstap + +import ( + "time" + + "github.com/coredns/coredns/plugin/dnstap/msg" + + tap "github.com/dnstap/golang-dnstap" + "github.com/miekg/dns" +) + +// ResponseWriter captures the client response and logs the query to dnstap. +type ResponseWriter struct { + queryTime time.Time + query *dns.Msg + dns.ResponseWriter + Dnstap +} + +// WriteMsg writes back the response to the client and THEN works on logging the request and response to dnstap. +func (w *ResponseWriter) WriteMsg(resp *dns.Msg) error { + err := w.ResponseWriter.WriteMsg(resp) + if err != nil { + return err + } + + r := new(tap.Message) + msg.SetQueryTime(r, w.queryTime) + msg.SetResponseTime(r, time.Now()) + msg.SetQueryAddress(r, w.RemoteAddr()) + + if w.IncludeRawMessage { + buf, _ := resp.Pack() + r.ResponseMessage = buf + } + + msg.SetType(r, tap.Message_CLIENT_RESPONSE) + w.TapMessage(r) + return nil +} diff --git a/ag_201_coredns/plugin/done.go b/ag_201_coredns/plugin/done.go new file mode 100644 index 0000000..c6ff863 --- /dev/null +++ b/ag_201_coredns/plugin/done.go @@ -0,0 +1,13 @@ +package plugin + +import "context" + +// Done is a non-blocking function that returns true if the context has been canceled. +func Done(ctx context.Context) bool { + select { + case <-ctx.Done(): + return true + default: + return false + } +} diff --git a/ag_201_coredns/plugin/erratic/README.md b/ag_201_coredns/plugin/erratic/README.md new file mode 100644 index 0000000..5e2b06b --- /dev/null +++ b/ag_201_coredns/plugin/erratic/README.md @@ -0,0 +1,89 @@ +# erratic + +## Name + +*erratic* - a plugin useful for testing client behavior. + +## Description + +*erratic* returns a static response to all queries, but the responses can be delayed, +dropped or truncated. The *erratic* plugin will respond to every A or AAAA query. For +any other type it will return a SERVFAIL response (except AXFR). The reply for A will return +192.0.2.53 ([RFC 5737](https://tools.ietf.org/html/rfc5737)), for AAAA it returns 2001:DB8::53 ([RFC +3849](https://tools.ietf.org/html/rfc3849)). For an AXFR request it will respond with a small +zone transfer. + +## Syntax + +~~~ txt +erratic { + drop [AMOUNT] + truncate [AMOUNT] + delay [AMOUNT [DURATION]] +} +~~~ + +* `drop`: drop 1 per **AMOUNT** of queries, the default is 2. +* `truncate`: truncate 1 per **AMOUNT** of queries, the default is 2. +* `delay`: delay 1 per **AMOUNT** of queries for **DURATION**, the default for **AMOUNT** is 2 and + the default for **DURATION** is 100ms. + +In case of a zone transfer and truncate the final SOA record *isn't* added to the response. + +## Ready + +This plugin reports readiness to the ready plugin. + +## Examples + +~~~ corefile +example.org { + erratic { + drop 3 + } +} +~~~ + +Or even shorter if the defaults suit you. Note this only drops queries, it does not delay them. + +~~~ corefile +example.org { + erratic +} +~~~ + +Delay 1 in 3 queries for 50ms + +~~~ corefile +example.org { + erratic { + delay 3 50ms + } +} +~~~ + +Delay 1 in 3 and truncate 1 in 5. + +~~~ corefile +example.org { + erratic { + delay 3 5ms + truncate 5 + } +} +~~~ + +Drop every second query. + +~~~ corefile +example.org { + erratic { + drop 2 + truncate 2 + } +} +~~~ + +## See Also + +[RFC 3849](https://tools.ietf.org/html/rfc3849) and [RFC 5737](https://tools.ietf.org/html/rfc5737). diff --git a/ag_201_coredns/plugin/erratic/autopath.go b/ag_201_coredns/plugin/erratic/autopath.go new file mode 100644 index 0000000..0e29fff --- /dev/null +++ b/ag_201_coredns/plugin/erratic/autopath.go @@ -0,0 +1,8 @@ +package erratic + +import "github.com/coredns/coredns/request" + +// AutoPath implements the AutoPathFunc call from the autopath plugin. +func (e *Erratic) AutoPath(state request.Request) []string { + return []string{"a.example.org.", "b.example.org.", ""} +} diff --git a/ag_201_coredns/plugin/erratic/erratic.go b/ag_201_coredns/plugin/erratic/erratic.go new file mode 100644 index 0000000..da7f68a --- /dev/null +++ b/ag_201_coredns/plugin/erratic/erratic.go @@ -0,0 +1,109 @@ +// Package erratic implements a plugin that returns erratic answers (delayed, dropped). +package erratic + +import ( + "context" + "sync/atomic" + "time" + + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Erratic is a plugin that returns erratic responses to each client. +type Erratic struct { + q uint64 // counter of queries + drop uint64 + delay uint64 + truncate uint64 + + duration time.Duration + large bool // undocumented feature; return large responses for A request (>512B, to test compression). +} + +// ServeDNS implements the plugin.Handler interface. +func (e *Erratic) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + drop := false + delay := false + trunc := false + + queryNr := atomic.LoadUint64(&e.q) + atomic.AddUint64(&e.q, 1) + + if e.drop > 0 && queryNr%e.drop == 0 { + drop = true + } + if e.delay > 0 && queryNr%e.delay == 0 { + delay = true + } + if e.truncate > 0 && queryNr&e.truncate == 0 { + trunc = true + } + + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative = true + if trunc { + m.Truncated = true + } + + // small dance to copy rrA or rrAAAA into a non-pointer var that allows us to overwrite the ownername + // in a non-racy way. + switch state.QType() { + case dns.TypeA: + rr := *(rrA.(*dns.A)) + rr.Header().Name = state.QName() + m.Answer = append(m.Answer, &rr) + if e.large { + for i := 0; i < 29; i++ { + m.Answer = append(m.Answer, &rr) + } + } + case dns.TypeAAAA: + rr := *(rrAAAA.(*dns.AAAA)) + rr.Header().Name = state.QName() + m.Answer = append(m.Answer, &rr) + case dns.TypeAXFR: + if drop { + return 0, nil + } + if delay { + time.Sleep(e.duration) + } + + xfr(state, trunc) + return 0, nil + + default: + if drop { + return 0, nil + } + if delay { + time.Sleep(e.duration) + } + // coredns will return error. + return dns.RcodeServerFailure, nil + } + + if drop { + return 0, nil + } + + if delay { + time.Sleep(e.duration) + } + + w.WriteMsg(m) + + return 0, nil +} + +// Name implements the Handler interface. +func (e *Erratic) Name() string { return "erratic" } + +var ( + rrA, _ = dns.NewRR(". IN 0 A 192.0.2.53") + rrAAAA, _ = dns.NewRR(". IN 0 AAAA 2001:DB8::53") +) diff --git a/ag_201_coredns/plugin/erratic/erratic_test.go b/ag_201_coredns/plugin/erratic/erratic_test.go new file mode 100644 index 0000000..ec2ec5c --- /dev/null +++ b/ag_201_coredns/plugin/erratic/erratic_test.go @@ -0,0 +1,116 @@ +package erratic + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestErraticDrop(t *testing.T) { + e := &Erratic{drop: 2} // 50% drops + + tests := []struct { + rrtype uint16 + expectedCode int + expectedErr error + drop bool + }{ + {rrtype: dns.TypeA, expectedCode: dns.RcodeSuccess, expectedErr: nil, drop: true}, + {rrtype: dns.TypeA, expectedCode: dns.RcodeSuccess, expectedErr: nil, drop: false}, + {rrtype: dns.TypeAAAA, expectedCode: dns.RcodeSuccess, expectedErr: nil, drop: true}, + {rrtype: dns.TypeHINFO, expectedCode: dns.RcodeServerFailure, expectedErr: nil, drop: false}, + } + + ctx := context.TODO() + + for i, tc := range tests { + req := new(dns.Msg) + req.SetQuestion("example.org.", tc.rrtype) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + code, err := e.ServeDNS(ctx, rec, req) + + if err != tc.expectedErr { + t.Errorf("Test %d: Expected error %q, but got %q", i, tc.expectedErr, err) + } + if code != int(tc.expectedCode) { + t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code) + } + + if tc.drop && rec.Msg != nil { + t.Errorf("Test %d: Expected dropped message, but got %q", i, rec.Msg.Question[0].Name) + } + } +} + +func TestErraticTruncate(t *testing.T) { + e := &Erratic{truncate: 2} // 50% drops + + tests := []struct { + expectedCode int + expectedErr error + truncate bool + }{ + {expectedCode: dns.RcodeSuccess, expectedErr: nil, truncate: true}, + {expectedCode: dns.RcodeSuccess, expectedErr: nil, truncate: false}, + } + + ctx := context.TODO() + + for i, tc := range tests { + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + code, err := e.ServeDNS(ctx, rec, req) + + if err != tc.expectedErr { + t.Errorf("Test %d: Expected error %q, but got %q", i, tc.expectedErr, err) + } + if code != int(tc.expectedCode) { + t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code) + } + + if tc.truncate && !rec.Msg.Truncated { + t.Errorf("Test %d: Expected truncated message, but got %q", i, rec.Msg.Question[0].Name) + } + } +} + +func TestAxfr(t *testing.T) { + e := &Erratic{truncate: 0} // nothing, just check if we can get an axfr + + ctx := context.TODO() + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeAXFR) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := e.ServeDNS(ctx, rec, req) + if err != nil { + t.Errorf("Failed to set up AXFR: %s", err) + } + if x := rec.Msg.Answer[0].Header().Rrtype; x != dns.TypeSOA { + t.Errorf("Expected for record to be %d, got %d", dns.TypeSOA, x) + } +} + +func TestErratic(t *testing.T) { + e := &Erratic{drop: 0, delay: 0} + + ctx := context.TODO() + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + e.ServeDNS(ctx, rec, req) + + if rec.Msg.Answer[0].Header().Rrtype != dns.TypeA { + t.Errorf("Expected A response, got %d type", rec.Msg.Answer[0].Header().Rrtype) + } +} diff --git a/ag_201_coredns/plugin/erratic/log_test.go b/ag_201_coredns/plugin/erratic/log_test.go new file mode 100644 index 0000000..f6fb4bf --- /dev/null +++ b/ag_201_coredns/plugin/erratic/log_test.go @@ -0,0 +1,5 @@ +package erratic + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/erratic/ready.go b/ag_201_coredns/plugin/erratic/ready.go new file mode 100644 index 0000000..d5f18a6 --- /dev/null +++ b/ag_201_coredns/plugin/erratic/ready.go @@ -0,0 +1,13 @@ +package erratic + +import "sync/atomic" + +// Ready returns true if the number of received queries is in the range [3, 5). All other values return false. +// To aid in testing we want to this flip between ready and not ready. +func (e *Erratic) Ready() bool { + q := atomic.LoadUint64(&e.q) + if q >= 3 && q < 5 { + return true + } + return false +} diff --git a/ag_201_coredns/plugin/erratic/setup.go b/ag_201_coredns/plugin/erratic/setup.go new file mode 100644 index 0000000..524473c --- /dev/null +++ b/ag_201_coredns/plugin/erratic/setup.go @@ -0,0 +1,113 @@ +package erratic + +import ( + "fmt" + "strconv" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("erratic", setup) } + +func setup(c *caddy.Controller) error { + e, err := parseErratic(c) + if err != nil { + return plugin.Error("erratic", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return e + }) + + return nil +} + +func parseErratic(c *caddy.Controller) (*Erratic, error) { + e := &Erratic{drop: 2} + drop := false // true if we've seen the drop keyword + + for c.Next() { // 'erratic' + for c.NextBlock() { + switch c.Val() { + case "drop": + args := c.RemainingArgs() + if len(args) > 1 { + return nil, c.ArgErr() + } + + if len(args) == 0 { + continue + } + + amount, err := strconv.ParseInt(args[0], 10, 32) + if err != nil { + return nil, err + } + if amount < 0 { + return nil, fmt.Errorf("illegal amount value given %q", args[0]) + } + e.drop = uint64(amount) + drop = true + case "delay": + args := c.RemainingArgs() + if len(args) > 2 { + return nil, c.ArgErr() + } + + // Defaults. + e.delay = 2 + e.duration = 100 * time.Millisecond + if len(args) == 0 { + continue + } + + amount, err := strconv.ParseInt(args[0], 10, 32) + if err != nil { + return nil, err + } + if amount < 0 { + return nil, fmt.Errorf("illegal amount value given %q", args[0]) + } + e.delay = uint64(amount) + + if len(args) > 1 { + duration, err := time.ParseDuration(args[1]) + if err != nil { + return nil, err + } + e.duration = duration + } + case "truncate": + args := c.RemainingArgs() + if len(args) > 1 { + return nil, c.ArgErr() + } + + if len(args) == 0 { + continue + } + + amount, err := strconv.ParseInt(args[0], 10, 32) + if err != nil { + return nil, err + } + if amount < 0 { + return nil, fmt.Errorf("illegal amount value given %q", args[0]) + } + e.truncate = uint64(amount) + case "large": + e.large = true + default: + return nil, c.Errf("unknown property '%s'", c.Val()) + } + } + } + if (e.delay > 0 || e.truncate > 0) && !drop { // delay is set, but we've haven't seen a drop keyword, remove default drop stuff + e.drop = 0 + } + + return e, nil +} diff --git a/ag_201_coredns/plugin/erratic/setup_test.go b/ag_201_coredns/plugin/erratic/setup_test.go new file mode 100644 index 0000000..9d2ff51 --- /dev/null +++ b/ag_201_coredns/plugin/erratic/setup_test.go @@ -0,0 +1,103 @@ +package erratic + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", `erratic { + drop + }`) + if err := setup(c); err != nil { + t.Fatalf("Test 1, expected no errors, but got: %q", err) + } + + c = caddy.NewTestController("dns", `erratic`) + if err := setup(c); err != nil { + t.Fatalf("Test 2, expected no errors, but got: %q", err) + } + + c = caddy.NewTestController("dns", `erratic { + drop -1 + }`) + if err := setup(c); err == nil { + t.Fatalf("Test 4, expected errors, but got: %q", err) + } +} + +func TestParseErratic(t *testing.T) { + tests := []struct { + input string + shouldErr bool + drop uint64 + delay uint64 + truncate uint64 + }{ + // oks + {`erratic`, false, 2, 0, 0}, + {`erratic { + drop 2 + delay 3 1ms + + }`, false, 2, 3, 0}, + {`erratic { + truncate 2 + delay 3 1ms + + }`, false, 0, 3, 2}, + {`erraric { + drop 3 + delay + }`, false, 3, 2, 0}, + // fails + {`erratic { + drop -1 + }`, true, 0, 0, 0}, + {`erratic { + delay -1 + }`, true, 0, 0, 0}, + {`erratic { + delay 1 2 4 + }`, true, 0, 0, 0}, + {`erratic { + delay 15.a + }`, true, 0, 0, 0}, + {`erraric { + drop 3 + delay 3 bla + }`, true, 0, 0, 0}, + {`erraric { + truncate 15.a + }`, true, 0, 0, 0}, + {`erraric { + something-else + }`, true, 0, 0, 0}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + e, err := parseErratic(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 test.delay != e.delay { + t.Errorf("Test %v: Expected delay %d but found: %d", i, test.delay, e.delay) + } + if test.drop != e.drop { + t.Errorf("Test %v: Expected drop %d but found: %d", i, test.drop, e.drop) + } + if test.truncate != e.truncate { + t.Errorf("Test %v: Expected truncate %d but found: %d", i, test.truncate, e.truncate) + } + } +} diff --git a/ag_201_coredns/plugin/erratic/xfr.go b/ag_201_coredns/plugin/erratic/xfr.go new file mode 100644 index 0000000..e1ec77e --- /dev/null +++ b/ag_201_coredns/plugin/erratic/xfr.go @@ -0,0 +1,57 @@ +package erratic + +import ( + "strings" + "sync" + + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// allRecords returns a small zone file. The first RR must be a SOA. +func allRecords(name string) []dns.RR { + var rrs = []dns.RR{ + test.SOA("xx. 0 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2018050825 7200 3600 1209600 3600"), + test.NS("xx. 0 IN NS b.xx."), + test.NS("xx. 0 IN NS a.xx."), + test.AAAA("a.xx. 0 IN AAAA 2001:bd8::53"), + test.AAAA("b.xx. 0 IN AAAA 2001:500::54"), + } + + for _, r := range rrs { + r.Header().Name = strings.Replace(r.Header().Name, "xx.", name, 1) + + if n, ok := r.(*dns.NS); ok { + n.Ns = strings.Replace(n.Ns, "xx.", name, 1) + } + } + return rrs +} + +func xfr(state request.Request, truncate bool) { + rrs := allRecords(state.QName()) + + ch := make(chan *dns.Envelope) + tr := new(dns.Transfer) + + go func() { + // So the rrs we have don't have a closing SOA, only add that when truncate is false, + // so we send an incomplete AXFR. + if !truncate { + rrs = append(rrs, rrs[0]) + } + + ch <- &dns.Envelope{RR: rrs} + close(ch) + }() + + wg := new(sync.WaitGroup) + wg.Add(1) + go func() { + tr.Out(state.W, state.Req, ch) + wg.Done() + }() + wg.Wait() +} diff --git a/ag_201_coredns/plugin/errors/README.md b/ag_201_coredns/plugin/errors/README.md new file mode 100644 index 0000000..27ba105 --- /dev/null +++ b/ag_201_coredns/plugin/errors/README.md @@ -0,0 +1,65 @@ +# errors + +## Name + +*errors* - enables error logging. + +## Description + +Any errors encountered during the query processing will be printed to standard output. The errors of particular type can be consolidated and printed once per some period of time. + +This plugin can only be used once per Server Block. + +## Syntax + +The basic syntax is: + +~~~ +errors +~~~ + +Extra knobs are available with an expanded syntax: + +~~~ +errors { + stacktrace + consolidate DURATION REGEXP [LEVEL] +} +~~~ + +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 +~~~ + +Multiple `consolidate` options with different **DURATION** and **REGEXP** are allowed. In case if some error message corresponds to several defined regular expressions the message will be associated with the first appropriate **REGEXP**. + +For better performance, it's recommended to use the `^` or `$` metacharacters in regular expression when filtering error messages by prefix or suffix, e.g. `^failed to .*`, or `.* timeout$`. + +## Examples + +Use the *whoami* to respond to queries in the example.org domain and Log errors to standard output. + +~~~ corefile +example.org { + whoami + errors +} +~~~ + +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$" warning + consolidate 30s "^Failed to .+" + } +} +~~~ diff --git a/ag_201_coredns/plugin/errors/benchmark_test.go b/ag_201_coredns/plugin/errors/benchmark_test.go new file mode 100644 index 0000000..04e6433 --- /dev/null +++ b/ag_201_coredns/plugin/errors/benchmark_test.go @@ -0,0 +1,27 @@ +package errors + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func BenchmarkServeDNS(b *testing.B) { + h := &errorHandler{} + h.Next = test.ErrorHandler() + + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeA) + w := &test.ResponseWriter{} + ctx := context.TODO() + + for i := 0; i < b.N; i++ { + _, err := h.ServeDNS(ctx, w, r) + if err != nil { + b.Errorf("ServeDNS returned error: %s", err) + } + } +} diff --git a/ag_201_coredns/plugin/errors/errors.go b/ag_201_coredns/plugin/errors/errors.go new file mode 100644 index 0000000..c045f69 --- /dev/null +++ b/ag_201_coredns/plugin/errors/errors.go @@ -0,0 +1,104 @@ +// Package errors implements an error handling plugin. +package errors + +import ( + "context" + "regexp" + "sync/atomic" + "time" + "unsafe" + + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +var log = clog.NewWithPlugin("errors") + +type pattern struct { + ptimer unsafe.Pointer + count uint32 + period time.Duration + pattern *regexp.Regexp + logCallback func(format string, v ...interface{}) +} + +func (p *pattern) timer() *time.Timer { + return (*time.Timer)(atomic.LoadPointer(&p.ptimer)) +} + +func (p *pattern) setTimer(t *time.Timer) { + atomic.StorePointer(&p.ptimer, unsafe.Pointer(t)) +} + +// errorHandler handles DNS errors (and errors from other plugin). +type errorHandler struct { + patterns []*pattern + stopFlag uint32 + Next plugin.Handler +} + +func newErrorHandler() *errorHandler { + return &errorHandler{} +} + +func (h *errorHandler) logPattern(i int) { + cnt := atomic.SwapUint32(&h.patterns[i].count, 0) + if cnt > 0 { + h.patterns[i].logCallback("%d errors like '%s' occurred in last %s", + cnt, h.patterns[i].pattern.String(), h.patterns[i].period) + } +} + +func (h *errorHandler) inc(i int) bool { + if atomic.LoadUint32(&h.stopFlag) > 0 { + return false + } + if atomic.AddUint32(&h.patterns[i].count, 1) == 1 { + ind := i + t := time.AfterFunc(h.patterns[ind].period, func() { + h.logPattern(ind) + }) + h.patterns[ind].setTimer(t) + if atomic.LoadUint32(&h.stopFlag) > 0 && t.Stop() { + h.logPattern(ind) + } + } + return true +} + +func (h *errorHandler) stop() { + atomic.StoreUint32(&h.stopFlag, 1) + for i := range h.patterns { + t := h.patterns[i].timer() + if t != nil && t.Stop() { + h.logPattern(i) + } + } +} + +// ServeDNS implements the plugin.Handler interface. +func (h *errorHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + rcode, err := plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + + if err != nil { + strErr := err.Error() + for i := range h.patterns { + if h.patterns[i].pattern.MatchString(strErr) { + if h.inc(i) { + return rcode, err + } + break + } + } + state := request.Request{W: w, Req: r} + log.Errorf("%d %s %s: %s", rcode, state.Name(), state.Type(), strErr) + } + + return rcode, err +} + +// Name implements the plugin.Handler interface. +func (h *errorHandler) Name() string { return "errors" } diff --git a/ag_201_coredns/plugin/errors/errors_test.go b/ag_201_coredns/plugin/errors/errors_test.go new file mode 100644 index 0000000..1cd42b4 --- /dev/null +++ b/ag_201_coredns/plugin/errors/errors_test.go @@ -0,0 +1,237 @@ +package errors + +import ( + "bytes" + "context" + "errors" + "fmt" + golog "log" + "regexp" + "strings" + "sync/atomic" + "testing" + "time" + + "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" +) + +func TestErrors(t *testing.T) { + buf := bytes.Buffer{} + golog.SetOutput(&buf) + em := errorHandler{} + + testErr := errors.New("test error") + tests := []struct { + next plugin.Handler + expectedCode int + expectedLog string + expectedErr error + }{ + { + next: genErrorHandler(dns.RcodeSuccess, nil), + expectedCode: dns.RcodeSuccess, + expectedLog: "", + expectedErr: nil, + }, + { + next: genErrorHandler(dns.RcodeNotAuth, testErr), + expectedCode: dns.RcodeNotAuth, + expectedLog: fmt.Sprintf("%d %s: %v\n", dns.RcodeNotAuth, "example.org. A", testErr), + expectedErr: testErr, + }, + } + + ctx := context.TODO() + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + + for i, tc := range tests { + em.Next = tc.next + buf.Reset() + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + code, err := em.ServeDNS(ctx, rec, req) + + if err != tc.expectedErr { + t.Errorf("Test %d: Expected error %v, but got %v", + i, tc.expectedErr, err) + } + if code != tc.expectedCode { + t.Errorf("Test %d: Expected status code %d, but got %d", + i, tc.expectedCode, code) + } + if log := buf.String(); !strings.Contains(log, tc.expectedLog) { + t.Errorf("Test %d: Expected log %q, but got %q", + i, tc.expectedLog, log) + } + } +} + +func TestLogPattern(t *testing.T) { + type args struct { + logCallback func(format string, v ...interface{}) + } + 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", + }, + } + + 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) + } + }) + } +} + +func TestInc(t *testing.T) { + h := &errorHandler{ + stopFlag: 1, + patterns: []*pattern{{ + period: 2 * time.Second, + pattern: regexp.MustCompile("^error.*!$"), + }}, + } + + ret := h.inc(0) + if ret { + t.Error("Unexpected return value, expected false, actual true") + } + + h.stopFlag = 0 + ret = h.inc(0) + if !ret { + t.Error("Unexpected return value, expected true, actual false") + } + + expCnt := uint32(1) + actCnt := atomic.LoadUint32(&h.patterns[0].count) + if actCnt != expCnt { + t.Errorf("Unexpected 'count', expected %d, actual %d", expCnt, actCnt) + } + + t1 := h.patterns[0].timer() + if t1 == nil { + t.Error("Unexpected 'timer', expected not nil") + } + + ret = h.inc(0) + if !ret { + t.Error("Unexpected return value, expected true, actual false") + } + + expCnt = uint32(2) + actCnt = atomic.LoadUint32(&h.patterns[0].count) + if actCnt != expCnt { + t.Errorf("Unexpected 'count', expected %d, actual %d", expCnt, actCnt) + } + + t2 := h.patterns[0].timer() + if t2 != t1 { + t.Error("Unexpected 'timer', expected the same") + } + + ret = t1.Stop() + if !ret { + t.Error("Timer was unexpectedly stopped before") + } + ret = t2.Stop() + if ret { + t.Error("Timer was unexpectedly not stopped before") + } +} + +func TestStop(t *testing.T) { + buf := bytes.Buffer{} + golog.SetOutput(&buf) + + h := &errorHandler{ + patterns: []*pattern{{ + period: 2 * time.Second, + pattern: regexp.MustCompile("^error.*!$"), + logCallback: log.Errorf, + }}, + } + + h.inc(0) + h.inc(0) + h.inc(0) + expCnt := uint32(3) + actCnt := atomic.LoadUint32(&h.patterns[0].count) + if actCnt != expCnt { + t.Fatalf("Unexpected initial 'count', expected %d, actual %d", expCnt, actCnt) + } + + h.stop() + + expCnt = uint32(0) + actCnt = atomic.LoadUint32(&h.patterns[0].count) + if actCnt != expCnt { + t.Errorf("Unexpected 'count', expected %d, actual %d", expCnt, actCnt) + } + + expStop := uint32(1) + actStop := h.stopFlag + if actStop != expStop { + t.Errorf("Unexpected 'stop', expected %d, actual %d", expStop, actStop) + } + + t1 := h.patterns[0].timer() + if t1 == nil { + t.Error("Unexpected 'timer', expected not nil") + } else if t1.Stop() { + t.Error("Timer was unexpectedly not stopped before") + } + + expLog := "3 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) + } +} + +func genErrorHandler(rcode int, err error) plugin.Handler { + return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + return rcode, err + }) +} diff --git a/ag_201_coredns/plugin/errors/log_test.go b/ag_201_coredns/plugin/errors/log_test.go new file mode 100644 index 0000000..643c16a --- /dev/null +++ b/ag_201_coredns/plugin/errors/log_test.go @@ -0,0 +1,5 @@ +package errors + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/errors/setup.go b/ag_201_coredns/plugin/errors/setup.go new file mode 100644 index 0000000..c040e10 --- /dev/null +++ b/ag_201_coredns/plugin/errors/setup.go @@ -0,0 +1,109 @@ +package errors + +import ( + "regexp" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("errors", setup) } + +func setup(c *caddy.Controller) error { + handler, err := errorsParse(c) + if err != nil { + return plugin.Error("errors", err) + } + + c.OnShutdown(func() error { + handler.stop() + return nil + }) + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + handler.Next = next + return handler + }) + + return nil +} + +func errorsParse(c *caddy.Controller) (*errorHandler, error) { + handler := newErrorHandler() + + i := 0 + for c.Next() { + if i > 0 { + return nil, plugin.ErrOnce + } + i++ + + args := c.RemainingArgs() + switch len(args) { + case 0: + case 1: + if args[0] != "stdout" { + return nil, c.Errf("invalid log file: %s", args[0]) + } + default: + return nil, c.ArgErr() + } + + for c.NextBlock() { + 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 parseConsolidate(c *caddy.Controller) (*pattern, error) { + args := c.RemainingArgs() + if len(args) < 2 || len(args) > 3 { + return nil, c.ArgErr() + } + p, err := time.ParseDuration(args[0]) + if err != nil { + return nil, c.Err(err.Error()) + } + re, err := regexp.Compile(args[1]) + if err != nil { + return nil, c.Err(err.Error()) + } + lc, err := parseLogLevel(c, args) + if err != nil { + return nil, err + } + return &pattern{period: p, pattern: re, logCallback: lc}, nil +} + +func parseLogLevel(c *caddy.Controller, args []string) (func(format string, v ...interface{}), 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/ag_201_coredns/plugin/errors/setup_test.go b/ag_201_coredns/plugin/errors/setup_test.go new file mode 100644 index 0000000..5dbc9ec --- /dev/null +++ b/ag_201_coredns/plugin/errors/setup_test.go @@ -0,0 +1,148 @@ +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) { + tests := []struct { + inputErrorsRules string + shouldErr bool + optCount int + stacktrace bool + }{ + {`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, false}, + {`errors a b`, true, 0, false}, + + {`errors { + consolidate + }`, true, 0, false}, + {`errors { + consolidate 1m + }`, true, 0, false}, + {`errors { + consolidate 1m .* extra + }`, true, 0, false}, + {`errors { + consolidate abc .* + }`, true, 0, false}, + {`errors { + consolidate 1 .* + }`, true, 0, false}, + {`errors { + consolidate 1m ()) + }`, true, 0, false}, + {`errors { + stacktrace + }`, false, 0, true}, + {`errors { + stacktrace + consolidate 1m ^exact$ + }`, false, 1, true}, + {`errors { + consolidate 1m ^exact$ + }`, false, 1, false}, + {`errors { + consolidate 1m error + }`, false, 1, false}, + {`errors { + consolidate 1m "format error" + }`, false, 1, false}, + {`errors { + consolidate 1m error1 + consolidate 5s error2 + }`, false, 2, false}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputErrorsRules) + h, err := errorsParse(c) + + if err == nil && test.shouldErr { + t.Errorf("Test %d didn't error, but it should have", i) + } else if err != nil && !test.shouldErr { + t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) + } else if h != nil && len(h.patterns) != test.optCount { + 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/ag_201_coredns/plugin/etcd/README.md b/ag_201_coredns/plugin/etcd/README.md new file mode 100644 index 0000000..6e83f23 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/README.md @@ -0,0 +1,236 @@ +# etcd + +## Name + +*etcd* - enables SkyDNS service discovery from etcd. + +## Description + +The *etcd* plugin implements the (older) SkyDNS service discovery service. It is *not* suitable as +a generic DNS zone data plugin. Only a subset of DNS record types are implemented, and subdomains +and delegations are not handled at all. The plugin will also recursively descend the tree and return +all records found, see "Special Behavior" below for details. + +The data in the etcd instance has to be encoded as +a [message](https://github.com/skynetservices/skydns/blob/2fcff74cdc9f9a7dd64189a447ef27ac354b725f/msg/service.go#L26) +like [SkyDNS](https://github.com/skynetservices/skydns). It works just like SkyDNS. + +The *etcd* plugin makes extensive use of the *forward* plugin to forward and query other servers in the +network - if that plugin has been enabled as well. + +## Syntax + +~~~ +etcd [ZONES...] +~~~ + +* **ZONES** zones *etcd* should be authoritative for. + +The path will default to `/skydns` the local etcd3 proxy (http://localhost:2379). If no zones are +specified the block's zone will be used as the zone. + + +~~~ +etcd [ZONES...] { + fallthrough [ZONES...] + path PATH + endpoint ENDPOINT... + credentials USERNAME PASSWORD + tls CERT KEY CACERT +} +~~~ + +* `fallthrough` If zone matches but 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. +* **PATH** the path inside etcd. Defaults to "/skydns". +* **ENDPOINT** the etcd endpoints. Defaults to "http://localhost:2379". +* `credentials` is used to set the **USERNAME** and **PASSWORD** for accessing the etcd cluster. +* `tls` followed by: + + * no arguments, if the server certificate is signed by a system-installed CA and no client cert is needed + * a single argument that is the CA PEM file, if the server cert is not signed by a system CA and no client cert is needed + * two arguments - path to cert PEM file, the path to private key PEM file - if the server certificate is signed by a system-installed CA and a client certificate is needed + * 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. + +## Special Behaviour + +The *etcd* plugin leverages directory structure to look for related entries. For example +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. +If the directory extends deeper those records are returned as well. + +With etcd3, support for [hierarchical keys are +dropped](https://coreos.com/etcd/docs/latest/learning/api.html). This means there are no directories +but only flat keys with prefixes in etcd3. To accommodate lookups, the *etcd* plugin now does a lookup +on prefix `/skydns/test/skydns/mx/` to search for entries like `/skydns/test/skydns/mx/a` etc, and +if there is nothing found on `/skydns/test/skydns/mx/`, it looks for `/skydns/test/skydns/mx` to +find entries like `/skydns/test/skydns/mx1`. + +This causes two lookups from CoreDNS to etcd in certain cases. + +## Examples + +This is the default SkyDNS setup, with everything specified in full: + +~~~ corefile +skydns.local { + etcd { + path /skydns + endpoint http://localhost:2379 + } + prometheus + cache + loadbalance +} + +. { + forward . 8.8.8.8:53 8.8.4.4:53 + cache +} +~~~ + +Or a setup where we use `/etc/resolv.conf` as the basis for the proxy and the upstream +when resolving external pointing CNAMEs. + +~~~ corefile +skydns.local { + etcd { + path /skydns + } + cache +} + +. { + forward . /etc/resolv.conf + cache +} +~~~ + +Multiple endpoints are supported as well. + +~~~ +etcd skydns.local { + endpoint http://localhost:2379 http://localhost:4001 +... +~~~ +Before getting started with these examples, please setup `etcdctl` (with `etcdv3` API) as explained +[here](https://coreos.com/etcd/docs/latest/dev-guide/interacting_v3.html). This will help you to put +sample keys in your etcd server. + +If you prefer, you can use `curl` to populate the `etcd` server, but with `curl` the +endpoint URL depends on the version of `etcd`. For instance, `etcd v3.2` or before uses only +[CLIENT-URL]/v3alpha/* while `etcd v3.5` or later uses [CLIENT-URL]/v3/* . Also, Key and Value must +be base64 encoded in the JSON payload. With `etcdctl` these details are automatically taken care +of. You can check [this document](https://github.com/coreos/etcd/blob/master/Documentation/dev-guide/api_grpc_gateway.md#notes) +for details. + +### Reverse zones + +Reverse zones are supported. You need to make CoreDNS aware of the fact that you are also +authoritative for the reverse. For instance if you want to add the reverse for 10.0.0.0/24, you'll +need to add the zone `0.0.10.in-addr.arpa` to the list of zones. Showing a snippet of a Corefile: + +~~~ +etcd skydns.local 10.0.0.0/24 { +... +~~~ + +Next you'll need to populate the zone with reverse records, here we add a reverse for +10.0.0.127 pointing to reverse.skydns.local. + +~~~ +% etcdctl put /skydns/arpa/in-addr/10/0/0/127 '{"host":"reverse.skydns.local."}' +~~~ + +Querying with dig: + +~~~ sh +% dig @localhost -x 10.0.0.127 +short +reverse.skydns.local. +~~~ + +### Zone name as A record + +The zone name itself can be used as an `A` record. This behavior can be achieved by writing special +entries to the ETCD path of your zone. If your zone is named `skydns.local` for example, you can +create an `A` record for this zone as follows: + +~~~ +% etcdctl put /skydns/local/skydns/ '{"host":"1.1.1.1","ttl":60}' +~~~ + +If you query the zone name itself, you will receive the created `A` record: + +~~~ sh +% dig +short skydns.local @localhost +1.1.1.1 +~~~ + +If you would like to use DNS RR for the zone name, you can set the following: +~~~ +% etcdctl put /skydns/local/skydns/x1 '{"host":"1.1.1.1","ttl":60}' +% etcdctl put /skydns/local/skydns/x2 '{"host":"1.1.1.2","ttl":60}' +~~~ + +If you query the zone name now, you will get the following response: + +~~~ sh +% dig +short skydns.local @localhost +1.1.1.1 +1.1.1.2 +~~~ + +### Zone name as AAAA record + +If you would like to use `AAAA` records for the zone name too, you can set the following: +~~~ +% etcdctl put /skydns/local/skydns/x3 '{"host":"2003::8:1","ttl":60}' +% etcdctl put /skydns/local/skydns/x4 '{"host":"2003::8:2","ttl":60}' +~~~ + +If you query the zone name for `AAAA` now, you will get the following response: +~~~ sh +% dig +short skydns.local AAAA @localhost +2003::8:1 +2003::8:2 +~~~ + +### SRV record + +If you would like to use `SRV` records, you can set the following: +~~~ +% etcdctl put /skydns/local/skydns/x5 '{"host":"skydns-local.server","ttl":60,"priority":10,"port":8080}' +~~~ +Please notice that the key `host` is the `target` in `SRV`, so it should be a domain name. + +If you query the zone name for `SRV` now, you will get the following response: + +~~~ sh +% dig +short skydns.local SRV @localhost +10 100 8080 skydns-local.server. +~~~ + +### TXT record + +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 + +If you want to `round robin` A and AAAA responses look at the *loadbalance* plugin. diff --git a/ag_201_coredns/plugin/etcd/cname_test.go b/ag_201_coredns/plugin/etcd/cname_test.go new file mode 100644 index 0000000..1e64d6d --- /dev/null +++ b/ag_201_coredns/plugin/etcd/cname_test.go @@ -0,0 +1,108 @@ +//go:build etcd + +package etcd + +// etcd needs to be running on http://localhost:2379 + +import ( + "testing" + + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +// Check the ordering of returned cname. +func TestCnameLookup(t *testing.T) { + etc := newEtcdPlugin() + + for _, serv := range servicesCname { + set(t, etc, serv.Key, 0, serv) + defer delete(t, etc, serv.Key) + } + for i, tc := range dnsTestCasesCname { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := etc.ServeDNS(ctxt, rec, m) + if err != nil { + t.Errorf("Expected no error, got %v", err) + return + } + + resp := rec.Msg + if err := test.Header(tc, resp); err != nil { + t.Errorf("Test %d: %v", i, err) + continue + } + if err := test.Section(tc, test.Answer, resp.Answer); err != nil { + t.Errorf("Test %d: %v", i, err) + } + if err := test.Section(tc, test.Ns, resp.Ns); err != nil { + t.Errorf("Test %d: %v", i, err) + } + if err := test.Section(tc, test.Extra, resp.Extra); err != nil { + t.Errorf("Test %d: %v", i, err) + } + } +} + +var servicesCname = []*msg.Service{ + {Host: "cname1.region2.skydns.test", Key: "a.server1.dev.region1.skydns.test."}, + {Host: "cname2.region2.skydns.test", Key: "cname1.region2.skydns.test."}, + {Host: "cname3.region2.skydns.test", Key: "cname2.region2.skydns.test."}, + {Host: "cname4.region2.skydns.test", Key: "cname3.region2.skydns.test."}, + {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: "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."), + }, + Extra: []dns.RR{ + test.CNAME("cname1.region2.skydns.test. 300 IN CNAME cname2.region2.skydns.test."), + test.CNAME("cname2.region2.skydns.test. 300 IN CNAME cname3.region2.skydns.test."), + test.CNAME("cname3.region2.skydns.test. 300 IN CNAME cname4.region2.skydns.test."), + test.CNAME("cname4.region2.skydns.test. 300 IN CNAME cname5.region2.skydns.test."), + test.CNAME("cname5.region2.skydns.test. 300 IN CNAME cname6.region2.skydns.test."), + test.CNAME("cname6.region2.skydns.test. 300 IN CNAME endpoint.region2.skydns.test."), + 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."), + }, + }, + { // 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 endpoint.region3.skydns.test."), + test.TXT("endpoint.region3.skydns.test. 300 IN TXT \"SOME-RECORD-TEXT\""), + }, + }, +} diff --git a/ag_201_coredns/plugin/etcd/etcd.go b/ag_201_coredns/plugin/etcd/etcd.go new file mode 100644 index 0000000..077e490 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/etcd.go @@ -0,0 +1,185 @@ +// Package etcd provides the etcd version 3 backend plugin. +package etcd + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "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" + + "github.com/miekg/dns" + "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 +) + +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 + + endpoints []string // Stored here as well, to aid in testing. +} + +// Services implements the ServiceBackend interface. +func (e *Etcd) Services(ctx context.Context, state request.Request, exact bool, opt plugin.Options) (services []msg.Service, err error) { + services, err = e.Records(ctx, state, exact) + if err != nil { + return + } + + services = msg.Group(services) + return +} + +// Reverse implements the ServiceBackend interface. +func (e *Etcd) Reverse(ctx context.Context, state request.Request, exact bool, opt plugin.Options) (services []msg.Service, err error) { + return e.Services(ctx, state, exact, opt) +} + +// Lookup implements the ServiceBackend interface. +func (e *Etcd) Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) { + return e.Upstream.Lookup(ctx, state, name, typ) +} + +// IsNameError implements the ServiceBackend interface. +func (e *Etcd) IsNameError(err error) bool { + return err == errKeyNotFound +} + +// Records looks up records in etcd. If exact is true, it will lookup just this +// name. This is used when find matches when completing SRV lookups for instance. +func (e *Etcd) Records(ctx context.Context, state request.Request, exact bool) ([]msg.Service, error) { + name := state.Name() + + path, star := msg.PathWithWildcard(name, e.PathPrefix) + r, err := e.get(ctx, path, !exact) + if err != nil { + return nil, err + } + segments := strings.Split(msg.Path(name, e.PathPrefix), "/") + return e.loopNodes(r.Kvs, segments, star, state.QType()) +} + +func (e *Etcd) get(ctx context.Context, path string, recursive bool) (*etcdcv3.GetResponse, error) { + ctx, cancel := context.WithTimeout(ctx, etcdTimeout) + defer cancel() + if recursive { + if !strings.HasSuffix(path, "/") { + path = path + "/" + } + r, err := e.Client.Get(ctx, path, etcdcv3.WithPrefix()) + if err != nil { + return nil, err + } + if r.Count == 0 { + path = strings.TrimSuffix(path, "/") + r, err = e.Client.Get(ctx, path) + if err != nil { + return nil, err + } + if r.Count == 0 { + return nil, errKeyNotFound + } + } + return r, nil + } + + r, err := e.Client.Get(ctx, path) + if err != nil { + return nil, err + } + if r.Count == 0 { + return nil, errKeyNotFound + } + return r, nil +} + +func (e *Etcd) loopNodes(kv []*mvccpb.KeyValue, nameParts []string, star bool, qType uint16) (sx []msg.Service, err error) { + bx := make(map[msg.Service]struct{}) +Nodes: + for _, n := range kv { + if star { + s := string(n.Key) + keyParts := strings.Split(s, "/") + for i, n := range nameParts { + if i > len(keyParts)-1 { + // name is longer than key + continue Nodes + } + if n == "*" || n == "any" { + continue + } + if keyParts[i] != n { + continue Nodes + } + } + } + serv := new(msg.Service) + if err := json.Unmarshal(n.Value, serv); err != nil { + return nil, fmt.Errorf("%s: %s", n.Key, err.Error()) + } + serv.Key = string(n.Key) + if _, ok := bx[*serv]; ok { + continue + } + bx[*serv] = struct{}{} + + serv.TTL = e.TTL(n, serv) + if serv.Priority == 0 { + serv.Priority = priority + } + + if shouldInclude(serv, qType) { + sx = append(sx, *serv) + } + } + return sx, nil +} + +// 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) + + if etcdTTL == 0 && serv.TTL == 0 { + return ttl + } + if etcdTTL == 0 { + return serv.TTL + } + if serv.TTL == 0 { + return etcdTTL + } + if etcdTTL < serv.TTL { + return etcdTTL + } + return serv.TTL +} + +// shouldInclude returns true if the service should be included in a list of records, given the qType. For all the +// currently supported lookup types, the only one to allow for an empty Host field in the service are TXT records +// which resolve directly. If a TXT record is being resolved by CNAME, then we expect the Host field to have a +// value while the TXT field will be empty. +func shouldInclude(serv *msg.Service, qType uint16) bool { + return (qType == dns.TypeTXT && serv.Text != "") || serv.Host != "" +} diff --git a/ag_201_coredns/plugin/etcd/group_test.go b/ag_201_coredns/plugin/etcd/group_test.go new file mode 100644 index 0000000..2620bf2 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/group_test.go @@ -0,0 +1,87 @@ +//go:build etcd + +package etcd + +import ( + "testing" + + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestGroupLookup(t *testing.T) { + etc := newEtcdPlugin() + + for _, serv := range servicesGroup { + set(t, etc, serv.Key, 0, serv) + defer delete(t, etc, serv.Key) + } + for _, tc := range dnsTestCasesGroup { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := etc.ServeDNS(ctxt, rec, m) + if err != nil { + t.Errorf("Expected no error, got %v", err) + continue + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +// Note the key is encoded as DNS name, while in "reality" it is a etcd path. +var servicesGroup = []*msg.Service{ + {Host: "127.0.0.1", Key: "a.dom.skydns.test.", Group: "g1"}, + {Host: "127.0.0.2", Key: "b.sub.dom.skydns.test.", Group: "g1"}, + + {Host: "127.0.0.1", Key: "a.dom2.skydns.test.", Group: "g1"}, + {Host: "127.0.0.2", Key: "b.sub.dom2.skydns.test.", Group: ""}, + + {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 A records + Qname: "dom.skydns.test.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("dom.skydns.test. 300 IN A 127.0.0.1"), + test.A("dom.skydns.test. 300 IN A 127.0.0.2"), + }, + }, + { + // One has group, the other has not... Include the non-group always. + Qname: "dom2.skydns.test.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("dom2.skydns.test. 300 IN A 127.0.0.1"), + test.A("dom2.skydns.test. 300 IN A 127.0.0.2"), + }, + }, + { + // The groups differ. + Qname: "dom1.skydns.test.", Qtype: dns.TypeA, + Answer: []dns.RR{ + 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/ag_201_coredns/plugin/etcd/handler.go b/ag_201_coredns/plugin/etcd/handler.go new file mode 100644 index 0000000..5a99753 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/handler.go @@ -0,0 +1,82 @@ +package etcd + +import ( + "context" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// ServeDNS implements the plugin.Handler interface. +func (e *Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + opt := plugin.Options{} + state := request.Request{W: w, Req: r} + + zone := plugin.Zones(e.Zones).Matches(state.Name()) + if zone == "" { + return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r) + } + + var ( + records, extra []dns.RR + truncated bool + err error + ) + + switch state.QType() { + case dns.TypeA: + records, truncated, err = plugin.A(ctx, e, zone, state, nil, opt) + case dns.TypeAAAA: + records, truncated, err = plugin.AAAA(ctx, e, zone, state, nil, opt) + case dns.TypeTXT: + 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: + records, err = plugin.PTR(ctx, e, zone, state, opt) + case dns.TypeMX: + records, extra, err = plugin.MX(ctx, e, zone, state, opt) + case dns.TypeSRV: + records, extra, err = plugin.SRV(ctx, e, zone, state, opt) + case dns.TypeSOA: + records, err = plugin.SOA(ctx, e, zone, state, opt) + case dns.TypeNS: + if state.Name() == zone { + records, extra, err = plugin.NS(ctx, e, zone, state, opt) + break + } + fallthrough + default: + // Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN + _, _, err = plugin.A(ctx, e, zone, state, nil, opt) + } + if err != nil && e.IsNameError(err) { + if e.Fall.Through(state.Name()) { + return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r) + } + // Make err nil when returning here, so we don't log spam for NXDOMAIN. + return plugin.BackendError(ctx, e, zone, dns.RcodeNameError, state, nil /* err */, opt) + } + if err != nil { + return plugin.BackendError(ctx, e, zone, dns.RcodeServerFailure, state, err, opt) + } + + if len(records) == 0 { + return plugin.BackendError(ctx, e, zone, dns.RcodeSuccess, state, err, opt) + } + + 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...) + + w.WriteMsg(m) + return dns.RcodeSuccess, nil +} + +// Name implements the Handler interface. +func (e *Etcd) Name() string { return "etcd" } diff --git a/ag_201_coredns/plugin/etcd/log_test.go b/ag_201_coredns/plugin/etcd/log_test.go new file mode 100644 index 0000000..57735be --- /dev/null +++ b/ag_201_coredns/plugin/etcd/log_test.go @@ -0,0 +1,5 @@ +package etcd + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/etcd/lookup_test.go b/ag_201_coredns/plugin/etcd/lookup_test.go new file mode 100644 index 0000000..0b689b0 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/lookup_test.go @@ -0,0 +1,355 @@ +//go:build etcd + +package etcd + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/tls" + "github.com/coredns/coredns/plugin/pkg/upstream" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func init() { + ctxt = context.TODO() +} + +// Note the key is encoded as DNS name, while in "reality" it is a etcd path. +var services = []*msg.Service{ + {Host: "dev.server1", Port: 8080, Key: "a.server1.dev.region1.skydns.test."}, + {Host: "10.0.0.1", Port: 8080, Key: "a.server1.prod.region1.skydns.test."}, + {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. + {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. + {Host: "priority.server1", Priority: 333, Port: 8080, Key: "priority.skydns.test."}, + // Subdomain. + {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."}, + // Zone name as A (single entry). + {Host: "10.0.0.2", Key: "x.skydns_zoneb.test."}, + {Host: "10.0.0.3", Key: "y.skydns_zoneb.test."}, + {Host: "10.0.0.4", Key: "apex.dns.skydns_zoneb.test."}, + // A zone record (rr multiple entries). + {Host: "10.0.0.2", Key: "x.skydns_zonec.test."}, + {Host: "10.0.0.3", Key: "y.skydns_zonec.test."}, + {Host: "10.0.0.4", Key: "a1.apex.dns.skydns_zonec.test."}, + {Host: "10.0.0.5", Key: "a2.apex.dns.skydns_zonec.test."}, + // AAAA zone record (rr multiple entries mixed with A). + {Host: "10.0.0.2", Key: "x.skydns_zoned.test."}, + {Host: "10.0.0.3", Key: "y.skydns_zoned.test."}, + {Host: "10.0.0.4", Key: "a1.apex.dns.skydns_zoned.test."}, + {Host: "10.0.0.5", Key: "a2.apex.dns.skydns_zoned.test."}, + {Host: "2003::8:1", Key: "a3.apex.dns.skydns_zoned.test."}, + {Host: "2003::8:2", Key: "a4.apex.dns.skydns_zoned.test."}, + // Reverse. + {Host: "reverse.example.com", Key: "1.0.0.10.in-addr.arpa."}, // 10.0.0.1 +} + +var dnsTestCases = []test.Case{ + // SRV Test + { + Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeSRV, + Answer: []dns.RR{test.SRV("a.server1.dev.region1.skydns.test. 300 SRV 10 100 8080 dev.server1.")}, + }, + // SRV Test (case test) + { + Qname: "a.SERVer1.dEv.region1.skydns.tEst.", Qtype: dns.TypeSRV, + Answer: []dns.RR{test.SRV("a.SERVer1.dEv.region1.skydns.tEst. 300 SRV 10 100 8080 dev.server1.")}, + }, + // NXDOMAIN Test + { + Qname: "doesnotexist.skydns.test.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("skydns.test. 30 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0"), + }, + }, + // A Test + { + Qname: "a.server1.prod.region1.skydns.test.", Qtype: dns.TypeA, + Answer: []dns.RR{test.A("a.server1.prod.region1.skydns.test. 300 A 10.0.0.1")}, + }, + // SRV Test where target is IP address + { + Qname: "a.server1.prod.region1.skydns.test.", Qtype: dns.TypeSRV, + Answer: []dns.RR{test.SRV("a.server1.prod.region1.skydns.test. 300 SRV 10 100 8080 a.server1.prod.region1.skydns.test.")}, + Extra: []dns.RR{test.A("a.server1.prod.region1.skydns.test. 300 A 10.0.0.1")}, + }, + // AAAA Test + { + Qname: "b.server6.prod.region1.skydns.test.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{test.AAAA("b.server6.prod.region1.skydns.test. 300 AAAA ::1")}, + }, + // Multiple A Record Test + { + Qname: "server1.prod.region1.skydns.test.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("server1.prod.region1.skydns.test. 300 A 10.0.0.1"), + test.A("server1.prod.region1.skydns.test. 300 A 10.0.0.2"), + }, + }, + // Priority Test + { + Qname: "priority.skydns.test.", Qtype: dns.TypeSRV, + Answer: []dns.RR{test.SRV("priority.skydns.test. 300 SRV 333 100 8080 priority.server1.")}, + }, + // Subdomain Test + { + Qname: "sub.region1.skydns.test.", Qtype: dns.TypeSRV, + Answer: []dns.RR{ + test.SRV("sub.region1.skydns.test. 300 IN SRV 10 33 0 sub.server1."), + test.SRV("sub.region1.skydns.test. 300 IN SRV 10 33 80 sub.server2."), + test.SRV("sub.region1.skydns.test. 300 IN SRV 10 33 8080 c.sub.region1.skydns.test."), + }, + 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, + Ns: []dns.RR{test.SOA("skydns.test. 30 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")}, + }, + // TXT Test + { + 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"), + }, + }, + // Wildcard Test + { + Qname: "*.region1.skydns.test.", Qtype: dns.TypeSRV, + Answer: []dns.RR{ + test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 0 sub.server1."), + test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 0 unresolvable.skydns.test."), + test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 80 sub.server2."), + test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 a.server1.prod.region1.skydns.test."), + test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 b.server1.prod.region1.skydns.test."), + test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 b.server6.prod.region1.skydns.test."), + test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 c.sub.region1.skydns.test."), + test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 dev.server1."), + }, + Extra: []dns.RR{ + test.A("a.server1.prod.region1.skydns.test. 300 IN A 10.0.0.1"), + test.A("b.server1.prod.region1.skydns.test. 300 IN A 10.0.0.2"), + test.AAAA("b.server6.prod.region1.skydns.test. 300 IN AAAA ::1"), + test.A("c.sub.region1.skydns.test. 300 IN A 10.0.0.1"), + }, + }, + // Wildcard Test + { + Qname: "prod.*.skydns.test.", Qtype: dns.TypeSRV, + Answer: []dns.RR{ + + test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 0 unresolvable.skydns.test."), + test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 8080 a.server1.prod.region1.skydns.test."), + test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 8080 b.server1.prod.region1.skydns.test."), + test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 8080 b.server6.prod.region1.skydns.test."), + }, + Extra: []dns.RR{ + test.A("a.server1.prod.region1.skydns.test. 300 IN A 10.0.0.1"), + test.A("b.server1.prod.region1.skydns.test. 300 IN A 10.0.0.2"), + test.AAAA("b.server6.prod.region1.skydns.test. 300 IN AAAA ::1"), + }, + }, + // Wildcard Test + { + Qname: "prod.any.skydns.test.", Qtype: dns.TypeSRV, + Answer: []dns.RR{ + test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 0 unresolvable.skydns.test."), + test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 8080 a.server1.prod.region1.skydns.test."), + test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 8080 b.server1.prod.region1.skydns.test."), + test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 8080 b.server6.prod.region1.skydns.test."), + }, + Extra: []dns.RR{ + test.A("a.server1.prod.region1.skydns.test. 300 IN A 10.0.0.1"), + test.A("b.server1.prod.region1.skydns.test. 300 IN A 10.0.0.2"), + test.AAAA("b.server6.prod.region1.skydns.test. 300 IN AAAA ::1"), + }, + }, + // CNAME loop detection + { + Qname: "a.cname.skydns.test.", Qtype: dns.TypeA, + Ns: []dns.RR{test.SOA("skydns.test. 30 SOA ns.dns.skydns.test. hostmaster.skydns.test. 1407441600 28800 7200 604800 60")}, + }, + // NODATA Test + { + Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeTXT, + Ns: []dns.RR{test.SOA("skydns.test. 30 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")}, + }, + // NODATA Test + { + Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeHINFO, + Ns: []dns.RR{test.SOA("skydns.test. 30 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")}, + }, + // NXDOMAIN Test + { + Qname: "a.server1.nonexistent.region1.skydns.test.", Qtype: dns.TypeHINFO, Rcode: dns.RcodeNameError, + Ns: []dns.RR{test.SOA("skydns.test. 30 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")}, + }, + { + Qname: "skydns.test.", Qtype: dns.TypeSOA, + Answer: []dns.RR{test.SOA("skydns.test. 30 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1460498836 14400 3600 604800 60")}, + }, + // NS Record Test + { + Qname: "skydns.test.", Qtype: dns.TypeNS, + 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 + { + Qname: "a.skydns.test.", Qtype: dns.TypeNS, Rcode: dns.RcodeNameError, + Ns: []dns.RR{test.SOA("skydns.test. 30 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1460498836 14400 3600 604800 60")}, + }, + // A Record For NS Record Test + { + Qname: "ns.dns.skydns.test.", Qtype: dns.TypeA, + 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"), + }, + }, + { + Qname: "skydns_extra.test.", Qtype: dns.TypeSOA, + Answer: []dns.RR{test.SOA("skydns_extra.test. 30 IN SOA ns.dns.skydns_extra.test. hostmaster.skydns_extra.test. 1460498836 14400 3600 604800 60")}, + }, + // A Record Test for backward compatibility for zone records + { + Qname: "skydns_zonea.test.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("skydns_zonea.test. 300 A 10.0.0.2"), + test.A("skydns_zonea.test. 300 A 10.0.0.3"), + }, + }, + // A Record Test for single A zone record + { + Qname: "skydns_zoneb.test.", Qtype: dns.TypeA, + Answer: []dns.RR{test.A("skydns_zoneb.test. 300 A 10.0.0.4")}, + }, + // A Record Test for multiple A zone records + { + Qname: "skydns_zonec.test.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("skydns_zonec.test. 300 A 10.0.0.4"), + test.A("skydns_zonec.test. 300 A 10.0.0.5"), + }, + }, + // A Record Test for multiple mixed A and AAAA records + { + Qname: "skydns_zoned.test.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("skydns_zoned.test. 300 A 10.0.0.4"), + test.A("skydns_zoned.test. 300 A 10.0.0.5"), + }, + }, + // AAAA Record Test for multiple mixed A and AAAA records + { + Qname: "skydns_zoned.test.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.AAAA("skydns_zoned.test. 300 AAAA 2003::8:1"), + test.AAAA("skydns_zoned.test. 300 AAAA 2003::8:2"), + }, + }, + // Reverse lookup + { + Qname: "1.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR, + Answer: []dns.RR{test.PTR("1.0.0.10.in-addr.arpa. 300 PTR reverse.example.com.")}, + }, +} + +func newEtcdPlugin() *Etcd { + ctxt = context.TODO() + + endpoints := []string{"http://localhost:2379"} + tlsc, _ := tls.NewTLSConfigFromArgs() + client, _ := newEtcdClient(endpoints, tlsc, "", "") + + return &Etcd{ + Upstream: upstream.New(), + PathPrefix: "skydns", + Zones: []string{"skydns.test.", "skydns_extra.test.", "skydns_zonea.test.", "skydns_zoneb.test.", "skydns_zonec.test.", "skydns_zoned.test.", "in-addr.arpa."}, + Client: client, + } +} + +func set(t *testing.T, e *Etcd, k string, ttl time.Duration, m *msg.Service) { + b, err := json.Marshal(m) + if err != nil { + t.Fatal(err) + } + path, _ := msg.PathWithWildcard(k, e.PathPrefix) + e.Client.KV.Put(ctxt, path, string(b)) +} + +func delete(t *testing.T, e *Etcd, k string) { + path, _ := msg.PathWithWildcard(k, e.PathPrefix) + e.Client.Delete(ctxt, path) +} + +func TestLookup(t *testing.T) { + etc := newEtcdPlugin() + for _, serv := range services { + set(t, etc, serv.Key, 0, serv) + defer delete(t, etc, serv.Key) + } + + for i, tc := range dnsTestCases { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + etc.ServeDNS(ctxt, rec, m) + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Errorf("Test %d: %v", i, err) + } + } +} + +var ctxt context.Context diff --git a/ag_201_coredns/plugin/etcd/msg/path.go b/ag_201_coredns/plugin/etcd/msg/path.go new file mode 100644 index 0000000..2c6cbff --- /dev/null +++ b/ag_201_coredns/plugin/etcd/msg/path.go @@ -0,0 +1,51 @@ +package msg + +import ( + "path" + "strings" + + "github.com/coredns/coredns/plugin/pkg/dnsutil" + + "github.com/miekg/dns" +) + +// Path converts a domainname to an etcd path. If s looks like service.staging.skydns.local., +// the resulting key will be /skydns/local/skydns/staging/service . +func Path(s, prefix string) string { + l := dns.SplitDomainName(s) + for i, j := 0, len(l)-1; i < j; i, j = i+1, j-1 { + l[i], l[j] = l[j], l[i] + } + return path.Join(append([]string{"/" + prefix + "/"}, l...)...) +} + +// 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] + } + return dnsutil.Join(l[1 : len(l)-1]...) +} + +// PathWithWildcard acts as Path, but if a name contains wildcards (* or any), the name will be +// chopped of before the (first) wildcard, and we do a higher level search and +// later find the matching names. So service.*.skydns.local, will look for all +// services under skydns.local and will later check for names that match +// service.*.skydns.local. If a wildcard is found the returned bool is true. +func PathWithWildcard(s, prefix string) (string, bool) { + l := dns.SplitDomainName(s) + for i, j := 0, len(l)-1; i < j; i, j = i+1, j-1 { + l[i], l[j] = l[j], l[i] + } + for i, k := range l { + if k == "*" || k == "any" { + return path.Join(append([]string{"/" + prefix + "/"}, l[:i]...)...), true + } + } + return path.Join(append([]string{"/" + prefix + "/"}, l...)...), false +} diff --git a/ag_201_coredns/plugin/etcd/msg/path_test.go b/ag_201_coredns/plugin/etcd/msg/path_test.go new file mode 100644 index 0000000..a20d783 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/msg/path_test.go @@ -0,0 +1,24 @@ +package msg + +import "testing" + +func TestPath(t *testing.T) { + for _, path := range []string{"mydns", "skydns"} { + result := Path("service.staging.skydns.local.", path) + if result != "/"+path+"/local/skydns/staging/service" { + t.Errorf("Failure to get domain's path with prefix: %s", result) + } + } +} + +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/ag_201_coredns/plugin/etcd/msg/service.go b/ag_201_coredns/plugin/etcd/msg/service.go new file mode 100644 index 0000000..759a862 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/msg/service.go @@ -0,0 +1,176 @@ +// Package msg defines the Service structure which is used for service discovery. +package msg + +import ( + "net" + "strings" + + "github.com/miekg/dns" +) + +// Service defines a discoverable service in etcd. It is the rdata from a SRV +// record, but with a twist. Host (Target in SRV) must be a domain name, but +// if it looks like an IP address (4/6), we will treat it like an IP address. +type Service struct { + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + Priority int `json:"priority,omitempty"` + Weight int `json:"weight,omitempty"` + Text string `json:"text,omitempty"` + Mail bool `json:"mail,omitempty"` // Be an MX record. Priority becomes Preference. + TTL uint32 `json:"ttl,omitempty"` + + // When a SRV record with a "Host: IP-address" is added, we synthesize + // a srv.Target domain name. Normally we convert the full Key where + // the record lives to a DNS name and use this as the srv.Target. When + // TargetStrip > 0 we strip the left most TargetStrip labels from the + // DNS name. + TargetStrip int `json:"targetstrip,omitempty"` + + // Group is used to group (or *not* to group) different services + // together. Services with an identical Group are returned in the same + // answer. + Group string `json:"group,omitempty"` + + // Etcd key where we found this service and ignored from json un-/marshalling + Key string `json:"-"` +} + +// NewSRV returns a new SRV record based on the Service. +func (s *Service) NewSRV(name string, weight uint16) *dns.SRV { + host := dns.Fqdn(s.Host) + if s.TargetStrip > 0 { + host = targetStrip(host, s.TargetStrip) + } + + return &dns.SRV{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: s.TTL}, + Priority: uint16(s.Priority), Weight: weight, Port: uint16(s.Port), Target: host} +} + +// NewMX returns a new MX record based on the Service. +func (s *Service) NewMX(name string) *dns.MX { + host := dns.Fqdn(s.Host) + if s.TargetStrip > 0 { + host = targetStrip(host, s.TargetStrip) + } + + return &dns.MX{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: s.TTL}, + Preference: uint16(s.Priority), Mx: host} +} + +// NewA returns a new A record based on the Service. +func (s *Service) NewA(name string, ip net.IP) *dns.A { + return &dns.A{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: s.TTL}, A: ip} +} + +// NewAAAA returns a new AAAA record based on the Service. +func (s *Service) NewAAAA(name string, ip net.IP) *dns.AAAA { + return &dns.AAAA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: s.TTL}, AAAA: ip} +} + +// NewCNAME returns a new CNAME record based on the Service. +func (s *Service) NewCNAME(name string, target string) *dns.CNAME { + return &dns.CNAME{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: s.TTL}, Target: dns.Fqdn(target)} +} + +// NewTXT returns a new TXT record based on the Service. +func (s *Service) NewTXT(name string) *dns.TXT { + return &dns.TXT{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: s.TTL}, Txt: split255(s.Text)} +} + +// NewPTR returns a new PTR record based on the Service. +func (s *Service) NewPTR(name string, target string) *dns.PTR { + return &dns.PTR{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: s.TTL}, Ptr: dns.Fqdn(target)} +} + +// NewNS returns a new NS record based on the Service. +func (s *Service) NewNS(name string) *dns.NS { + host := dns.Fqdn(s.Host) + if s.TargetStrip > 0 { + host = targetStrip(host, s.TargetStrip) + } + return &dns.NS{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: s.TTL}, Ns: host} +} + +// Group checks the services in sx, it looks for a Group attribute on the shortest +// keys. If there are multiple shortest keys *and* the group attribute disagrees (and +// is not empty), we don't consider it a group. +// If a group is found, only services with *that* group (or no group) will be returned. +func Group(sx []Service) []Service { + if len(sx) == 0 { + return sx + } + + // Shortest key with group attribute sets the group for this set. + group := sx[0].Group + slashes := strings.Count(sx[0].Key, "/") + length := make([]int, len(sx)) + for i, s := range sx { + x := strings.Count(s.Key, "/") + length[i] = x + if x < slashes { + if s.Group == "" { + break + } + slashes = x + group = s.Group + } + } + + if group == "" { + return sx + } + + ret := []Service{} // with slice-tricks in sx we can prolly save this allocation (TODO) + + for i, s := range sx { + if s.Group == "" { + ret = append(ret, s) + continue + } + + // Disagreement on the same level + if length[i] == slashes && s.Group != group { + return sx + } + + if s.Group == group { + ret = append(ret, s) + } + } + return ret +} + +// Split255 splits a string into 255 byte chunks. +func split255(s string) []string { + if len(s) < 255 { + return []string{s} + } + sx := []string{} + p, i := 0, 255 + for { + if i <= len(s) { + sx = append(sx, s[p:i]) + } else { + sx = append(sx, s[p:]) + break + } + p, i = p+255, i+255 + } + + return sx +} + +// 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++ { + offset, end = dns.NextLabel(name, offset) + } + if end { + // We overshot the name, use the original one. + offset = 0 + } + name = name[offset:] + return name +} diff --git a/ag_201_coredns/plugin/etcd/msg/service_test.go b/ag_201_coredns/plugin/etcd/msg/service_test.go new file mode 100644 index 0000000..f334aa5 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/msg/service_test.go @@ -0,0 +1,125 @@ +package msg + +import "testing" + +func TestSplit255(t *testing.T) { + xs := split255("abc") + if len(xs) != 1 && xs[0] != "abc" { + t.Errorf("Failure to split abc") + } + s := "" + for i := 0; i < 255; i++ { + s += "a" + } + xs = split255(s) + if len(xs) != 1 && xs[0] != s { + t.Errorf("Failure to split 255 char long string") + } + s += "b" + xs = split255(s) + 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++ { + s += "a" + } + xs = split255(s) + if len(xs) != 3 || xs[2] != "a" { + t.Errorf("Failure to split 510 char long string: %d", len(xs)) + } +} + +func TestGroup(t *testing.T) { + // Key are in the wrong order, but for this test it does not matter. + sx := Group( + []Service{ + {Host: "127.0.0.1", Group: "g1", Key: "b/sub/dom1/skydns/test"}, + {Host: "127.0.0.2", Group: "g2", Key: "a/dom1/skydns/test"}, + }, + ) + // Expecting to return the shortest key with a Group attribute. + if len(sx) != 1 { + t.Fatalf("Failure to group zeroth set: %v", sx) + } + if sx[0].Key != "a/dom1/skydns/test" { + t.Fatalf("Failure to group zeroth set: %v, wrong Key", sx) + } + + // Groups disagree, so we will not do anything. + sx = Group( + []Service{ + {Host: "server1", Group: "g1", Key: "region1/skydns/test"}, + {Host: "server2", Group: "g2", Key: "region1/skydns/test"}, + }, + ) + if len(sx) != 2 { + t.Fatalf("Failure to group first set: %v", sx) + } + + // Group is g1, include only the top-level one. + sx = Group( + []Service{ + {Host: "server1", Group: "g1", Key: "a/dom/region1/skydns/test"}, + {Host: "server2", Group: "g2", Key: "a/subdom/dom/region1/skydns/test"}, + }, + ) + if len(sx) != 1 { + t.Fatalf("Failure to group second set: %v", sx) + } + + // Groupless services must be included. + sx = Group( + []Service{ + {Host: "server1", Group: "g1", Key: "a/dom/region1/skydns/test"}, + {Host: "server2", Group: "g2", Key: "a/subdom/dom/region1/skydns/test"}, + {Host: "server2", Group: "", Key: "b/subdom/dom/region1/skydns/test"}, + }, + ) + if len(sx) != 2 { + t.Fatalf("Failure to group third set: %v", sx) + } + + // Empty group on the highest level: include that one also. + sx = Group( + []Service{ + {Host: "server1", Group: "g1", Key: "a/dom/region1/skydns/test"}, + {Host: "server1", Group: "", Key: "b/dom/region1/skydns/test"}, + {Host: "server2", Group: "g2", Key: "a/subdom/dom/region1/skydns/test"}, + }, + ) + if len(sx) != 2 { + t.Fatalf("Failure to group fourth set: %v", sx) + } + + // Empty group on the highest level: include that one also, and the rest. + sx = Group( + []Service{ + {Host: "server1", Group: "g5", Key: "a/dom/region1/skydns/test"}, + {Host: "server1", Group: "", Key: "b/dom/region1/skydns/test"}, + {Host: "server2", Group: "g5", Key: "a/subdom/dom/region1/skydns/test"}, + }, + ) + if len(sx) != 3 { + t.Fatalf("Failure to group fifth set: %v", sx) + } + + // One group. + sx = Group( + []Service{ + {Host: "server1", Group: "g6", Key: "a/dom/region1/skydns/test"}, + }, + ) + if len(sx) != 1 { + t.Fatalf("Failure to group sixth set: %v", sx) + } + + // No group, once service + sx = Group( + []Service{ + {Host: "server1", Key: "a/dom/region1/skydns/test"}, + }, + ) + if len(sx) != 1 { + t.Fatalf("Failure to group seventh set: %v", sx) + } +} diff --git a/ag_201_coredns/plugin/etcd/msg/type.go b/ag_201_coredns/plugin/etcd/msg/type.go new file mode 100644 index 0000000..a300eac --- /dev/null +++ b/ag_201_coredns/plugin/etcd/msg/type.go @@ -0,0 +1,35 @@ +package msg + +import ( + "net" + + "github.com/miekg/dns" +) + +// HostType returns the DNS type of what is encoded in the Service Host field. We're reusing +// dns.TypeXXX to not reinvent a new set of identifiers. +// +// dns.TypeA: the service's Host field contains an A record. +// dns.TypeAAAA: the service's Host field contains an AAAA record. +// dns.TypeCNAME: the service's Host field contains a name. +// +// 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 + } + return dns.TypeTXT, nil + + case ip.To4() != nil: + return dns.TypeA, ip.To4() + + case ip.To4() == nil: + return dns.TypeAAAA, ip.To16() + } + // This should never be reached. + return dns.TypeNone, nil +} diff --git a/ag_201_coredns/plugin/etcd/msg/type_test.go b/ag_201_coredns/plugin/etcd/msg/type_test.go new file mode 100644 index 0000000..721f5a8 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/msg/type_test.go @@ -0,0 +1,30 @@ +package msg + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestType(t *testing.T) { + tests := []struct { + serv Service + expectedType uint16 + }{ + {Service{Host: "example.org"}, dns.TypeCNAME}, + {Service{Host: "127.0.0.1"}, dns.TypeA}, + {Service{Host: "2000::3"}, dns.TypeAAAA}, + {Service{Host: "2000..3"}, dns.TypeCNAME}, + {Service{Host: "127.0.0.257"}, dns.TypeCNAME}, + {Service{Host: "127.0.0.252", Mail: true}, dns.TypeA}, + {Service{Host: "127.0.0.252", Mail: true, Text: "a"}, dns.TypeA}, + {Service{Host: "127.0.0.254", Mail: false, Text: "a"}, dns.TypeA}, + } + + for i, tc := range tests { + what, _ := tc.serv.HostType() + if what != tc.expectedType { + t.Errorf("Test %d: Expected what %v, but got %v", i, tc.expectedType, what) + } + } +} diff --git a/ag_201_coredns/plugin/etcd/multi_test.go b/ag_201_coredns/plugin/etcd/multi_test.go new file mode 100644 index 0000000..7993a25 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/multi_test.go @@ -0,0 +1,60 @@ +//go:build etcd + +package etcd + +import ( + "testing" + + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestMultiLookup(t *testing.T) { + etc := newEtcdPlugin() + etc.Zones = []string{"skydns.test.", "miek.nl."} + etc.Next = test.ErrorHandler() + + for _, serv := range servicesMulti { + set(t, etc, serv.Key, 0, serv) + defer delete(t, etc, serv.Key) + } + for _, tc := range dnsTestCasesMulti { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := etc.ServeDNS(ctxt, rec, m) + if err != nil { + t.Errorf("Expected no error, got %v", err) + return + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +// Note the key is encoded as DNS name, while in "reality" it is a etcd path. +var servicesMulti = []*msg.Service{ + {Host: "dev.server1", Port: 8080, Key: "a.server1.dev.region1.skydns.test."}, + {Host: "dev.server1", Port: 8080, Key: "a.server1.dev.region1.miek.nl."}, + {Host: "dev.server1", Port: 8080, Key: "a.server1.dev.region1.example.org."}, +} + +var dnsTestCasesMulti = []test.Case{ + { + Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeSRV, + Answer: []dns.RR{test.SRV("a.server1.dev.region1.skydns.test. 300 SRV 10 100 8080 dev.server1.")}, + }, + { + Qname: "a.server1.dev.region1.miek.nl.", Qtype: dns.TypeSRV, + Answer: []dns.RR{test.SRV("a.server1.dev.region1.miek.nl. 300 SRV 10 100 8080 dev.server1.")}, + }, + { + Qname: "a.server1.dev.region1.example.org.", Qtype: dns.TypeSRV, Rcode: dns.RcodeServerFailure, + }, +} diff --git a/ag_201_coredns/plugin/etcd/other_test.go b/ag_201_coredns/plugin/etcd/other_test.go new file mode 100644 index 0000000..a71260f --- /dev/null +++ b/ag_201_coredns/plugin/etcd/other_test.go @@ -0,0 +1,138 @@ +//go:build etcd + +// tests mx and txt records + +package etcd + +import ( + "fmt" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestOtherLookup(t *testing.T) { + etc := newEtcdPlugin() + + for _, serv := range servicesOther { + set(t, etc, serv.Key, 0, serv) + defer delete(t, etc, serv.Key) + } + for _, tc := range dnsTestCasesOther { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := etc.ServeDNS(ctxt, rec, m) + if err != nil { + t.Errorf("Expected no error, got %v", err) + continue + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +// Note the key is encoded as DNS name, while in "reality" it is a etcd path. +var servicesOther = []*msg.Service{ + {Host: "dev.server1", Port: 8080, Key: "a.server1.dev.region1.skydns.test."}, + + // mx + {Host: "mx.skydns.test", Priority: 50, Mail: true, Key: "a.mail.skydns.test."}, + {Host: "mx.miek.nl", Priority: 50, Mail: true, Key: "b.mail.skydns.test."}, + {Host: "a.ipaddr.skydns.test", Priority: 30, Mail: true, Key: "a.mx.skydns.test."}, + + {Host: "a.ipaddr.skydns.test", Mail: true, Key: "a.mx2.skydns.test."}, + {Host: "b.ipaddr.skydns.test", Mail: true, Key: "b.mx2.skydns.test."}, + + {Host: "a.ipaddr.skydns.test", Priority: 20, Mail: true, Key: "a.mx3.skydns.test."}, + {Host: "a.ipaddr.skydns.test", Priority: 30, Mail: true, Key: "b.mx3.skydns.test."}, + + {Host: "172.16.1.1", Key: "a.ipaddr.skydns.test."}, + {Host: "172.16.1.2", Key: "b.ipaddr.skydns.test."}, + + // txt + {Text: "abc", Key: "a1.txt.skydns.test."}, + {Text: "abc abc", Key: "a2.txt.skydns.test."}, + // txt sizes + {Text: strings.Repeat("0", 400), Key: "large400.skydns.test."}, + {Text: strings.Repeat("0", 600), Key: "large600.skydns.test."}, + {Text: strings.Repeat("0", 2000), Key: "large2000.skydns.test."}, + + // duplicate ip address + {Host: "10.11.11.10", Key: "http.multiport.http.skydns.test.", Port: 80}, + {Host: "10.11.11.10", Key: "https.multiport.http.skydns.test.", Port: 443}, +} + +var dnsTestCasesOther = []test.Case{ + // MX Tests + { + // NODATA as this is not an Mail: true record. + Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeMX, + Ns: []dns.RR{ + test.SOA("skydns.test. 30 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0"), + }, + }, + { + Qname: "a.mail.skydns.test.", Qtype: dns.TypeMX, + Answer: []dns.RR{test.MX("a.mail.skydns.test. 300 IN MX 50 mx.skydns.test.")}, + Extra: []dns.RR{ + test.A("a.ipaddr.skydns.test. 300 IN A 172.16.1.1"), + test.CNAME("mx.skydns.test. 300 IN CNAME a.ipaddr.skydns.test."), + }, + }, + { + Qname: "mx2.skydns.test.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("mx2.skydns.test. 300 IN MX 10 a.ipaddr.skydns.test."), + test.MX("mx2.skydns.test. 300 IN MX 10 b.ipaddr.skydns.test."), + }, + Extra: []dns.RR{ + test.A("a.ipaddr.skydns.test. 300 A 172.16.1.1"), + test.A("b.ipaddr.skydns.test. 300 A 172.16.1.2"), + }, + }, + // different priority, same host + { + Qname: "mx3.skydns.test.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("mx3.skydns.test. 300 IN MX 20 a.ipaddr.skydns.test."), + test.MX("mx3.skydns.test. 300 IN MX 30 a.ipaddr.skydns.test."), + }, + Extra: []dns.RR{ + test.A("a.ipaddr.skydns.test. 300 A 172.16.1.1"), + }, + }, + // Txt + { + Qname: "a1.txt.skydns.test.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT("a1.txt.skydns.test. 300 IN TXT \"abc\""), + }, + }, + { + Qname: "a2.txt.skydns.test.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT("a2.txt.skydns.test. 300 IN TXT \"abc abc\""), + }, + }, + // Large txt less than 512 + { + Qname: "large400.skydns.test.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT(fmt.Sprintf("large400.skydns.test. 300 IN TXT \"%s\"", strings.Repeat("0", 400))), + }, + }, + // Duplicate IP address test + { + Qname: "multiport.http.skydns.test.", Qtype: dns.TypeA, + Answer: []dns.RR{test.A("multiport.http.skydns.test. 300 IN A 10.11.11.10")}, + }, +} diff --git a/ag_201_coredns/plugin/etcd/setup.go b/ag_201_coredns/plugin/etcd/setup.go new file mode 100644 index 0000000..bd81af5 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/setup.go @@ -0,0 +1,116 @@ +package etcd + +import ( + "crypto/tls" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + mwtls "github.com/coredns/coredns/plugin/pkg/tls" + "github.com/coredns/coredns/plugin/pkg/upstream" + + etcdcv3 "go.etcd.io/etcd/client/v3" +) + +func init() { plugin.Register("etcd", setup) } + +func setup(c *caddy.Controller) error { + e, err := etcdParse(c) + if err != nil { + return plugin.Error("etcd", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + e.Next = next + return e + }) + + return nil +} + +func etcdParse(c *caddy.Controller) (*Etcd, error) { + etc := Etcd{PathPrefix: "skydns"} + var ( + tlsConfig *tls.Config + err error + endpoints = []string{defaultEndpoint} + username string + password string + ) + + etc.Upstream = upstream.New() + + if c.Next() { + etc.Zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) + for c.NextBlock() { + switch c.Val() { + case "stubzones": + // ignored, remove later. + case "fallthrough": + etc.Fall.SetZonesFromArgs(c.RemainingArgs()) + case "debug": + /* it is a noop now */ + case "path": + if !c.NextArg() { + return &Etcd{}, c.ArgErr() + } + etc.PathPrefix = c.Val() + case "endpoint": + args := c.RemainingArgs() + if len(args) == 0 { + return &Etcd{}, c.ArgErr() + } + endpoints = args + case "upstream": + // remove soon + c.RemainingArgs() + case "tls": // cert key cacertfile + args := c.RemainingArgs() + tlsConfig, err = mwtls.NewTLSConfigFromArgs(args...) + if err != nil { + return &Etcd{}, err + } + case "credentials": + args := c.RemainingArgs() + if len(args) == 0 { + return &Etcd{}, c.ArgErr() + } + if len(args) != 2 { + return &Etcd{}, c.Errf("credentials requires 2 arguments, username and password") + } + username, password = args[0], args[1] + default: + if c.Val() != "}" { + return &Etcd{}, c.Errf("unknown property '%s'", c.Val()) + } + } + } + client, err := newEtcdClient(endpoints, tlsConfig, username, password) + if err != nil { + return &Etcd{}, err + } + etc.Client = client + etc.endpoints = endpoints + + return &etc, nil + } + return &Etcd{}, nil +} + +func newEtcdClient(endpoints []string, cc *tls.Config, username, password string) (*etcdcv3.Client, error) { + etcdCfg := etcdcv3.Config{ + Endpoints: endpoints, + TLS: cc, + } + if username != "" && password != "" { + etcdCfg.Username = username + etcdCfg.Password = password + } + cli, err := etcdcv3.New(etcdCfg) + if err != nil { + return nil, err + } + return cli, nil +} + +const defaultEndpoint = "http://localhost:2379" diff --git a/ag_201_coredns/plugin/etcd/setup_test.go b/ag_201_coredns/plugin/etcd/setup_test.go new file mode 100644 index 0000000..4922641 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/setup_test.go @@ -0,0 +1,118 @@ +//go:build etcd + +package etcd + +import ( + "strings" + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupEtcd(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedPath string + expectedEndpoint []string + expectedErrContent string // substring from the expected error. Empty for positive cases. + username string + password string + }{ + // positive + { + `etcd`, false, "skydns", []string{"http://localhost:2379"}, "", "", "", + }, + { + `etcd { + endpoint http://localhost:2379 http://localhost:3379 http://localhost:4379 + +}`, false, "skydns", []string{"http://localhost:2379", "http://localhost:3379", "http://localhost:4379"}, "", "", "", + }, + { + `etcd skydns.local { + endpoint localhost:300 +} +`, false, "skydns", []string{"localhost:300"}, "", "", "", + }, + // negative + { + `etcd { + endpoints localhost:300 +} +`, true, "", []string{""}, "unknown property 'endpoints'", "", "", + }, + // with valid credentials + { + `etcd { + endpoint http://localhost:2379 + credentials username password + } + `, false, "skydns", []string{"http://localhost:2379"}, "", "username", "password", + }, + // with credentials, missing password + { + `etcd { + endpoint http://localhost:2379 + credentials username + } + `, true, "skydns", []string{"http://localhost:2379"}, "credentials requires 2 arguments", "username", "", + }, + // with credentials, missing username and password + { + `etcd { + endpoint http://localhost:2379 + credentials + } + `, true, "skydns", []string{"http://localhost:2379"}, "Wrong argument count", "", "", + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + etcd, err := etcdParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, 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) + continue + } + + 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.Error(), test.input) + continue + } + } + + if !test.shouldErr && etcd.PathPrefix != test.expectedPath { + t.Errorf("Etcd not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedPath, etcd.PathPrefix) + } + if !test.shouldErr { + if len(etcd.endpoints) != len(test.expectedEndpoint) { + t.Errorf("Etcd not correctly set for input %s. Expected: '%+v', actual: '%+v'", test.input, test.expectedEndpoint, etcd.endpoints) + } + for i, endpoint := range etcd.endpoints { + if endpoint != test.expectedEndpoint[i] { + t.Errorf("Etcd not correctly set for input %s. Expected: '%+v', actual: '%+v'", test.input, test.expectedEndpoint, etcd.endpoints) + } + } + } + + if !test.shouldErr { + if test.username != "" { + if etcd.Client.Username != test.username { + t.Errorf("Etcd username not correctly set for input %s. Expected: '%+v', actual: '%+v'", test.input, test.username, etcd.Client.Username) + } + } + if test.password != "" { + if etcd.Client.Password != test.password { + t.Errorf("Etcd password not correctly set for input %s. Expected: '%+v', actual: '%+v'", test.input, test.password, etcd.Client.Password) + } + } + } + } +} diff --git a/ag_201_coredns/plugin/etcd/xfr.go b/ag_201_coredns/plugin/etcd/xfr.go new file mode 100644 index 0000000..87a4d78 --- /dev/null +++ b/ag_201_coredns/plugin/etcd/xfr.go @@ -0,0 +1,17 @@ +package etcd + +import ( + "time" + + "github.com/coredns/coredns/request" +) + +// Serial returns the serial number to use. +func (e *Etcd) Serial(state request.Request) uint32 { + return uint32(time.Now().Unix()) +} + +// MinTTL returns the minimal TTL. +func (e *Etcd) MinTTL(state request.Request) uint32 { + return 30 +} diff --git a/ag_201_coredns/plugin/file/README.md b/ag_201_coredns/plugin/file/README.md new file mode 100644 index 0000000..d1bd425 --- /dev/null +++ b/ag_201_coredns/plugin/file/README.md @@ -0,0 +1,112 @@ +# file + +## Name + +*file* - enables serving zone data from an RFC 1035-style master file. + +## Description + +The *file* plugin is used for an "old-style" DNS server. It serves from a preloaded file that exists +on disk contained RFC 1035 styled data. If the zone file contains signatures (i.e., is signed using +DNSSEC), correct DNSSEC answers are returned. Only NSEC is supported! If you use this setup *you* +are responsible for re-signing the zonefile. + +## Syntax + +~~~ +file DBFILE [ZONES...] +~~~ + +* **DBFILE** the database file to read and parse. If the path is relative, the path from the *root* + plugin will be prepended to it. +* **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block + are used. + +If you want to round-robin A and AAAA responses look at the *loadbalance* plugin. + +~~~ +file DBFILE [ZONES... ] { + reload DURATION +} +~~~ + +* `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. + +If you need outgoing zone transfers, take a look at the *transfer* plugin. + +## Examples + +Load the `example.org` zone from `db.example.org` and allow transfers to the internet, but send +notifies to 10.240.1.1 + +~~~ corefile +example.org { + file db.example.org + transfer { + to * 10.240.1.1 + } +} +~~~ + +Where `db.example.org` would contain RRSets () in the +(text) presentation format from RFC 1035: + +~~~ +$ORIGIN example.org. +@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2017042745 7200 3600 1209600 3600 + 3600 IN NS a.iana-servers.net. + 3600 IN NS b.iana-servers.net. + +www IN A 127.0.0.1 + IN AAAA ::1 +~~~ + + +Or use a single zone file for multiple zones: + +~~~ corefile +. { + file example.org.signed example.org example.net + transfer example.org example.net { + to * 10.240.1.1 + } +} +~~~ + +Note that if you have a configuration like the following you may run into a problem of the origin +not being correctly recognized: + +~~~ corefile +. { + file db.example.org +} +~~~ + +We omit the origin for the file `db.example.org`, so this references the zone in the server block, +which, in this case, is the root zone. Any contents of `db.example.org` will then read with that +origin set; this may or may not do what you want. +It's better to be explicit here and specify the correct origin. This can be done in two ways: + +~~~ corefile +. { + file db.example.org example.org +} +~~~ + +Or + +~~~ corefile +example.org { + file db.example.org +} +~~~ + +## See Also + +See the *loadbalance* plugin if you need simple record shuffling. And the *transfer* plugin for zone +transfers. Lastly the *root* plugin can help you specify the location of the zone files. + +See [RFC 1035](https://www.rfc-editor.org/rfc/rfc1035.txt) for more info on how to structure zone +files. diff --git a/ag_201_coredns/plugin/file/apex_test.go b/ag_201_coredns/plugin/file/apex_test.go new file mode 100644 index 0000000..2108543 --- /dev/null +++ b/ag_201_coredns/plugin/file/apex_test.go @@ -0,0 +1,45 @@ +package file + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +const exampleApexOnly = `$ORIGIN example.com. +@ IN SOA ns1.example.com. admin.example.com. ( + 2005011437 ; Serial + 1200 ; Refresh + 144 ; Retry + 1814400 ; Expire + 2h ) ; Minimum +@ IN NS ns1.example.com. +` + +func TestLookupApex(t *testing.T) { + // this tests a zone with *only* an apex. The behavior here is wrong, we should return NODATA, but we do a NXDOMAIN. + // Adding this test to document this. Note a zone that doesn't have any data is pretty useless anyway, so rather than + // fix this with an entirely new branch in lookup.go, just live with it. + zone, err := Parse(strings.NewReader(exampleApexOnly), "example.com.", "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{"example.com.": zone}, Names: []string{"example.com."}}} + ctx := context.TODO() + + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + if _, err := fm.ServeDNS(ctx, rec, m); err != nil { + t.Errorf("Expected no error, got %v", err) + } + if rec.Msg.Rcode != dns.RcodeNameError { // Should be RcodeSuccess in a perfect world. + t.Errorf("Expected rcode %d, got %d", dns.RcodeNameError, rec.Msg.Rcode) + } +} diff --git a/ag_201_coredns/plugin/file/closest.go b/ag_201_coredns/plugin/file/closest.go new file mode 100644 index 0000000..5059194 --- /dev/null +++ b/ag_201_coredns/plugin/file/closest.go @@ -0,0 +1,23 @@ +package file + +import ( + "github.com/coredns/coredns/plugin/file/tree" + + "github.com/miekg/dns" +) + +// 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) + if elem != nil { + return elem, true + } + qname = qname[offset:] + + offset, end = dns.NextLabel(qname, offset) + } + + return z.Tree.Search(z.origin) +} diff --git a/ag_201_coredns/plugin/file/closest_test.go b/ag_201_coredns/plugin/file/closest_test.go new file mode 100644 index 0000000..40c04ff --- /dev/null +++ b/ag_201_coredns/plugin/file/closest_test.go @@ -0,0 +1,38 @@ +package file + +import ( + "strings" + "testing" +) + +func TestClosestEncloser(t *testing.T) { + z, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0) + if err != nil { + t.Fatalf("Expect no error when reading zone, got %q", err) + } + + tests := []struct { + in, out string + }{ + {"miek.nl.", "miek.nl."}, + {"www.miek.nl.", "www.miek.nl."}, + + {"blaat.miek.nl.", "miek.nl."}, + {"blaat.www.miek.nl.", "www.miek.nl."}, + {"www.blaat.miek.nl.", "miek.nl."}, + {"blaat.a.miek.nl.", "a.miek.nl."}, + } + + for _, tc := range tests { + ce, _ := z.ClosestEncloser(tc.in) + if ce == nil { + if z.origin != tc.out { + t.Errorf("Expected ce to be %s for %s, got %s", tc.out, tc.in, ce.Name()) + } + continue + } + if ce.Name() != tc.out { + t.Errorf("Expected ce to be %s for %s, got %s", tc.out, tc.in, ce.Name()) + } + } +} diff --git a/ag_201_coredns/plugin/file/delegation_test.go b/ag_201_coredns/plugin/file/delegation_test.go new file mode 100644 index 0000000..a6da621 --- /dev/null +++ b/ag_201_coredns/plugin/file/delegation_test.go @@ -0,0 +1,228 @@ +package file + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var delegationTestCases = []test.Case{ + { + Qname: "a.delegated.miek.nl.", Qtype: dns.TypeTXT, + Ns: []dns.RR{ + test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."), + test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."), + }, + Extra: []dns.RR{ + test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"), + test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + }, + { + Qname: "delegated.miek.nl.", Qtype: dns.TypeNS, + Ns: []dns.RR{ + test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."), + test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."), + }, + Extra: []dns.RR{ + test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"), + test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + }, + { + Qname: "foo.delegated.miek.nl.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."), + test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."), + }, + Extra: []dns.RR{ + test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"), + test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + }, + { + Qname: "foo.delegated.miek.nl.", Qtype: dns.TypeTXT, + Ns: []dns.RR{ + test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."), + test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."), + }, + Extra: []dns.RR{ + test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"), + test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + }, + { + Qname: "foo.delegated.miek.nl.", Qtype: dns.TypeSOA, + Ns: []dns.RR{ + test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."), + test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."), + }, + Extra: []dns.RR{ + test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"), + test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeSOA, + Answer: []dns.RR{ + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + Ns: miekAuth, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeAAAA, + Ns: []dns.RR{ + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, +} + +var secureDelegationTestCases = []test.Case{ + { + Qname: "a.delegated.example.org.", Qtype: dns.TypeTXT, Do: true, + Ns: []dns.RR{ + test.DS("delegated.example.org. 1800 IN DS 10056 5 1 EE72CABD1927759CDDA92A10DBF431504B9E1F13"), + test.DS("delegated.example.org. 1800 IN DS 10056 5 2 E4B05F87725FA86D9A64F1E53C3D0E6250946599DFE639C45955B0ED416CDDFA"), + test.NS("delegated.example.org. 1800 IN NS a.delegated.example.org."), + test.NS("delegated.example.org. 1800 IN NS ns-ext.nlnetlabs.nl."), + test.RRSIG("delegated.example.org. 1800 IN RRSIG DS 13 3 1800 20161129153240 20161030153240 49035 example.org. rlNNzcUmtbjLSl02ZzQGUbWX75yCUx0Mug1jHtKVqRq1hpPE2S3863tIWSlz+W9wz4o19OI4jbznKKqk+DGKog=="), + }, + Extra: []dns.RR{ + test.A("a.delegated.example.org. 1800 IN A 139.162.196.78"), + test.AAAA("a.delegated.example.org. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + }, + { + Qname: "delegated.example.org.", Qtype: dns.TypeNS, Do: true, + Ns: []dns.RR{ + test.DS("delegated.example.org. 1800 IN DS 10056 5 1 EE72CABD1927759CDDA92A10DBF431504B9E1F13"), + test.DS("delegated.example.org. 1800 IN DS 10056 5 2 E4B05F87725FA86D9A64F1E53C3D0E6250946599DFE639C45955B0ED416CDDFA"), + test.NS("delegated.example.org. 1800 IN NS a.delegated.example.org."), + test.NS("delegated.example.org. 1800 IN NS ns-ext.nlnetlabs.nl."), + test.RRSIG("delegated.example.org. 1800 IN RRSIG DS 13 3 1800 20161129153240 20161030153240 49035 example.org. rlNNzcUmtbjLSl02ZzQGUbWX75yCUx0Mug1jHtKVqRq1hpPE2S3863tIWSlz+W9wz4o19OI4jbznKKqk+DGKog=="), + }, + Extra: []dns.RR{ + test.A("a.delegated.example.org. 1800 IN A 139.162.196.78"), + test.AAAA("a.delegated.example.org. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + }, + { + Qname: "foo.delegated.example.org.", Qtype: dns.TypeA, Do: true, + Ns: []dns.RR{ + test.DS("delegated.example.org. 1800 IN DS 10056 5 1 EE72CABD1927759CDDA92A10DBF431504B9E1F13"), + test.DS("delegated.example.org. 1800 IN DS 10056 5 2 E4B05F87725FA86D9A64F1E53C3D0E6250946599DFE639C45955B0ED416CDDFA"), + test.NS("delegated.example.org. 1800 IN NS a.delegated.example.org."), + test.NS("delegated.example.org. 1800 IN NS ns-ext.nlnetlabs.nl."), + test.RRSIG("delegated.example.org. 1800 IN RRSIG DS 13 3 1800 20161129153240 20161030153240 49035 example.org. rlNNzcUmtbjLSl02ZzQGUbWX75yCUx0Mug1jHtKVqRq1hpPE2S3863tIWSlz+W9wz4o19OI4jbznKKqk+DGKog=="), + }, + Extra: []dns.RR{ + test.A("a.delegated.example.org. 1800 IN A 139.162.196.78"), + test.AAAA("a.delegated.example.org. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + }, + { + Qname: "foo.delegated.example.org.", Qtype: dns.TypeDS, Do: true, + Ns: []dns.RR{ + test.DS("delegated.example.org. 1800 IN DS 10056 5 1 EE72CABD1927759CDDA92A10DBF431504B9E1F13"), + test.DS("delegated.example.org. 1800 IN DS 10056 5 2 E4B05F87725FA86D9A64F1E53C3D0E6250946599DFE639C45955B0ED416CDDFA"), + test.NS("delegated.example.org. 1800 IN NS a.delegated.example.org."), + test.NS("delegated.example.org. 1800 IN NS ns-ext.nlnetlabs.nl."), + test.RRSIG("delegated.example.org. 1800 IN RRSIG DS 13 3 1800 20161129153240 20161030153240 49035 example.org. rlNNzcUmtbjLSl02ZzQGUbWX75yCUx0Mug1jHtKVqRq1hpPE2S3863tIWSlz+W9wz4o19OI4jbznKKqk+DGKog=="), + }, + Extra: []dns.RR{ + test.A("a.delegated.example.org. 1800 IN A 139.162.196.78"), + test.AAAA("a.delegated.example.org. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + }, + { + Qname: "delegated.example.org.", Qtype: dns.TypeDS, Do: true, + Answer: []dns.RR{ + test.DS("delegated.example.org. 1800 IN DS 10056 5 1 EE72CABD1927759CDDA92A10DBF431504B9E1F13"), + test.DS("delegated.example.org. 1800 IN DS 10056 5 2 E4B05F87725FA86D9A64F1E53C3D0E6250946599DFE639C45955B0ED416CDDFA"), + test.RRSIG("delegated.example.org. 1800 IN RRSIG DS 13 3 1800 20161129153240 20161030153240 49035 example.org. rlNNzcUmtbjLSl02ZzQGUbWX75yCUx0Mug1jHtKVqRq1hpPE2S3863tIWSlz+W9wz4o19OI4jbznKKqk+DGKog=="), + }, + Ns: []dns.RR{ + test.NS("example.org. 1800 IN NS a.iana-servers.net."), + test.NS("example.org. 1800 IN NS b.iana-servers.net."), + test.RRSIG("example.org. 1800 IN RRSIG NS 13 2 1800 20161129153240 20161030153240 49035 example.org. llrHoIuw="), + }, + }, +} + +var miekAuth = []dns.RR{ + test.NS("miek.nl. 1800 IN NS ext.ns.whyscream.net."), + test.NS("miek.nl. 1800 IN NS linode.atoom.net."), + test.NS("miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."), + test.NS("miek.nl. 1800 IN NS omval.tednet.nl."), +} + +func TestLookupDelegation(t *testing.T) { + testDelegation(t, dbMiekNLDelegation, testzone, delegationTestCases) +} + +func TestLookupSecureDelegation(t *testing.T) { + testDelegation(t, exampleOrgSigned, "example.org.", secureDelegationTestCases) +} + +func testDelegation(t *testing.T, z, origin string, testcases []test.Case) { + zone, err := Parse(strings.NewReader(z), origin, "stdin", 0) + if err != nil { + t.Fatalf("Expect no error when reading zone, got %q", err) + } + + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{origin: zone}, Names: []string{origin}}} + ctx := context.TODO() + + for _, tc := range testcases { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := fm.ServeDNS(ctx, rec, m) + if err != nil { + t.Errorf("Expected no error, got %q", err) + return + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +const dbMiekNLDelegation = ` +$TTL 30M +$ORIGIN miek.nl. +@ IN SOA linode.atoom.net. miek.miek.nl. ( + 1282630057 ; Serial + 4H ; Refresh + 1H ; Retry + 7D ; Expire + 4H ) ; Negative Cache TTL + IN NS linode.atoom.net. + IN NS ns-ext.nlnetlabs.nl. + IN NS omval.tednet.nl. + IN NS ext.ns.whyscream.net. + + IN MX 1 aspmx.l.google.com. + IN MX 5 alt1.aspmx.l.google.com. + IN MX 5 alt2.aspmx.l.google.com. + IN MX 10 aspmx2.googlemail.com. + IN MX 10 aspmx3.googlemail.com. + +delegated IN NS a.delegated + IN NS ns-ext.nlnetlabs.nl. + +a.delegated IN TXT "obscured" + IN A 139.162.196.78 + IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 + +a IN A 139.162.196.78 + IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 +www IN CNAME a +archive IN CNAME a` diff --git a/ag_201_coredns/plugin/file/delete_test.go b/ag_201_coredns/plugin/file/delete_test.go new file mode 100644 index 0000000..26ee64e --- /dev/null +++ b/ag_201_coredns/plugin/file/delete_test.go @@ -0,0 +1,65 @@ +package file + +import ( + "bytes" + "fmt" + "testing" + + "github.com/coredns/coredns/plugin/file/tree" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +/* +Create a zone with: + + apex + / + a MX + a A + +Test that: we create the proper tree and that delete +deletes the correct elements +*/ + +var tz = NewZone("example.org.", "db.example.org.") + +type treebuf struct { + *bytes.Buffer +} + +func (t *treebuf) printFunc(e *tree.Elem, rrs map[uint16][]dns.RR) error { + fmt.Fprintf(t.Buffer, "%v\n", rrs) // should be fixed order in new go versions. + return nil +} + +func TestZoneInsertAndDelete(t *testing.T) { + tz.Insert(test.SOA("example.org. IN SOA 1 2 3 4 5")) + + if x := tz.Apex.SOA.Header().Name; x != "example.org." { + t.Errorf("Failed to insert SOA, expected %s, git %s", "example.org.", x) + } + + // Insert two RRs and then remove one. + tz.Insert(test.A("a.example.org. IN A 127.0.0.1")) + tz.Insert(test.MX("a.example.org. IN MX 10 mx.example.org.")) + + tz.Delete(test.MX("a.example.org. IN MX 10 mx.example.org.")) + + tb := treebuf{new(bytes.Buffer)} + + tz.Walk(tb.printFunc) + if tb.String() != "map[1:[a.example.org.\t3600\tIN\tA\t127.0.0.1]]\n" { + t.Errorf("Expected 1 A record in tree, got %s", tb.String()) + } + + tz.Delete(test.A("a.example.org. IN A 127.0.0.1")) + + tb.Reset() + + tz.Walk(tb.printFunc) + if tb.String() != "" { + t.Errorf("Expected no record in tree, got %s", tb.String()) + } +} diff --git a/ag_201_coredns/plugin/file/dname.go b/ag_201_coredns/plugin/file/dname.go new file mode 100644 index 0000000..58351a3 --- /dev/null +++ b/ag_201_coredns/plugin/file/dname.go @@ -0,0 +1,44 @@ +package file + +import ( + "github.com/coredns/coredns/plugin/pkg/dnsutil" + + "github.com/miekg/dns" +) + +// substituteDNAME performs the DNAME substitution defined by RFC 6672, +// assuming the QTYPE of the query is not DNAME. It returns an empty +// string if there is no match. +func substituteDNAME(qname, owner, target string) string { + if dns.IsSubDomain(owner, qname) && qname != owner { + labels := dns.SplitDomainName(qname) + labels = append(labels[0:len(labels)-dns.CountLabel(owner)], dns.SplitDomainName(target)...) + + return dnsutil.Join(labels...) + } + + return "" +} + +// synthesizeCNAME returns a CNAME RR pointing to the resulting name of +// the DNAME substitution. The owner name of the CNAME is the QNAME of +// the query and the TTL is the same as the corresponding DNAME RR. +// +// It returns nil if the DNAME substitution has no match. +func synthesizeCNAME(qname string, d *dns.DNAME) *dns.CNAME { + target := substituteDNAME(qname, d.Header().Name, d.Target) + if target == "" { + return nil + } + + r := new(dns.CNAME) + r.Hdr = dns.RR_Header{ + Name: qname, + Rrtype: dns.TypeCNAME, + Class: dns.ClassINET, + Ttl: d.Header().Ttl, + } + r.Target = target + + return r +} diff --git a/ag_201_coredns/plugin/file/dname_test.go b/ag_201_coredns/plugin/file/dname_test.go new file mode 100644 index 0000000..cc70bb5 --- /dev/null +++ b/ag_201_coredns/plugin/file/dname_test.go @@ -0,0 +1,300 @@ +package file + +/* +TODO(miek): move to test/ for full server testing + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +// RFC 6672, Section 2.2. Assuming QTYPE != DNAME. +var dnameSubstitutionTestCases = []struct { + qname string + owner string + target string + expected string +}{ + {"com.", "example.com.", "example.net.", ""}, + {"example.com.", "example.com.", "example.net.", ""}, + {"a.example.com.", "example.com.", "example.net.", "a.example.net."}, + {"a.b.example.com.", "example.com.", "example.net.", "a.b.example.net."}, + {"ab.example.com.", "b.example.com.", "example.net.", ""}, + {"foo.example.com.", "example.com.", "example.net.", "foo.example.net."}, + {"a.x.example.com.", "x.example.com.", "example.net.", "a.example.net."}, + {"a.example.com.", "example.com.", "y.example.net.", "a.y.example.net."}, + {"cyc.example.com.", "example.com.", "example.com.", "cyc.example.com."}, + {"cyc.example.com.", "example.com.", "c.example.com.", "cyc.c.example.com."}, + {"shortloop.x.x.", "x.", ".", "shortloop.x."}, + {"shortloop.x.", "x.", ".", "shortloop."}, +} + +func TestDNAMESubstitution(t *testing.T) { + for i, tc := range dnameSubstitutionTestCases { + result := substituteDNAME(tc.qname, tc.owner, tc.target) + if result != tc.expected { + if result == "" { + result = "" + } + + t.Errorf("Case %d: Expected %s -> %s, got %v", i, tc.qname, tc.expected, result) + return + } + } +} + +var dnameTestCases = []test.Case{ + { + Qname: "dname.miek.nl.", Qtype: dns.TypeDNAME, + Answer: []dns.RR{ + test.DNAME("dname.miek.nl. 1800 IN DNAME test.miek.nl."), + }, + Ns: miekAuth, + }, + { + Qname: "dname.miek.nl.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("dname.miek.nl. 1800 IN A 127.0.0.1"), + }, + Ns: miekAuth, + }, + { + Qname: "dname.miek.nl.", Qtype: dns.TypeMX, + Answer: []dns.RR{}, + Ns: []dns.RR{ + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, + { + Qname: "a.dname.miek.nl.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("a.dname.miek.nl. 1800 IN CNAME a.test.miek.nl."), + test.A("a.test.miek.nl. 1800 IN A 139.162.196.78"), + test.DNAME("dname.miek.nl. 1800 IN DNAME test.miek.nl."), + }, + Ns: miekAuth, + }, + { + Qname: "www.dname.miek.nl.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("a.test.miek.nl. 1800 IN A 139.162.196.78"), + test.DNAME("dname.miek.nl. 1800 IN DNAME test.miek.nl."), + test.CNAME("www.dname.miek.nl. 1800 IN CNAME www.test.miek.nl."), + test.CNAME("www.test.miek.nl. 1800 IN CNAME a.test.miek.nl."), + }, + Ns: miekAuth, + }, +} + +func TestLookupDNAME(t *testing.T) { + zone, err := Parse(strings.NewReader(dbMiekNLDNAME), testzone, "stdin", 0) + if err != nil { + t.Fatalf("Expect 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() + + for _, tc := range dnameTestCases { + 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 + } + + resp := rec.Msg + test.SortAndCheck(t, resp, tc) + } +} + +var dnameDnssecTestCases = []test.Case{ + { + // We have no auth section, because the test zone does not have nameservers. + Qname: "ns.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ns.example.org. 1800 IN A 127.0.0.1"), + }, + }, + { + Qname: "dname.example.org.", Qtype: dns.TypeDNAME, Do: true, + Answer: []dns.RR{ + test.DNAME("dname.example.org. 1800 IN DNAME test.example.org."), + test.RRSIG("dname.example.org. 1800 IN RRSIG DNAME 5 3 1800 20170702091734 20170602091734 54282 example.org. HvXtiBM="), + }, + }, + { + Qname: "a.dname.example.org.", Qtype: dns.TypeA, Do: true, + Answer: []dns.RR{ + test.CNAME("a.dname.example.org. 1800 IN CNAME a.test.example.org."), + test.DNAME("dname.example.org. 1800 IN DNAME test.example.org."), + test.RRSIG("dname.example.org. 1800 IN RRSIG DNAME 5 3 1800 20170702091734 20170602091734 54282 example.org. HvXtiBM="), + }, + }, +} + +func TestLookupDNAMEDNSSEC(t *testing.T) { + zone, err := Parse(strings.NewReader(dbExampleDNAMESigned), testzone, "stdin", 0) + if err != nil { + t.Fatalf("Expect no error when reading zone, got %q", err) + } + + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{"example.org.": zone}, Names: []string{"example.org."}}} + ctx := context.TODO() + + for _, tc := range dnameDnssecTestCases { + 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 + } + + resp := rec.Msg + test.SortAndCheck(t, resp, tc) + } +} + +const dbMiekNLDNAME = ` +$TTL 30M +$ORIGIN miek.nl. +@ IN SOA linode.atoom.net. miek.miek.nl. ( + 1282630057 ; Serial + 4H ; Refresh + 1H ; Retry + 7D ; Expire + 4H ) ; Negative Cache TTL + IN NS linode.atoom.net. + IN NS ns-ext.nlnetlabs.nl. + IN NS omval.tednet.nl. + IN NS ext.ns.whyscream.net. + +test IN MX 1 aspmx.l.google.com. + IN MX 5 alt1.aspmx.l.google.com. + IN MX 5 alt2.aspmx.l.google.com. + IN MX 10 aspmx2.googlemail.com. + IN MX 10 aspmx3.googlemail.com. +a.test IN A 139.162.196.78 + IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 +www.test IN CNAME a.test + +dname IN DNAME test +dname IN A 127.0.0.1 +a.dname IN A 127.0.0.1 +` + +const dbExampleDNAMESigned = ` +; File written on Fri Jun 2 10:17:34 2017 +; dnssec_signzone version 9.10.3-P4-Debian +example.org. 1800 IN SOA a.example.org. b.example.org. ( + 1282630057 ; serial + 14400 ; refresh (4 hours) + 3600 ; retry (1 hour) + 604800 ; expire (1 week) + 14400 ; minimum (4 hours) + ) + 1800 RRSIG SOA 5 2 1800 ( + 20170702091734 20170602091734 54282 example.org. + mr5eQtFs1GubgwaCcqrpiF6Cgi822OkESPeV + X0OJYq3JzthJjHw8TfYAJWQ2yGqhlePHir9h + FT/uFZdYyytHq+qgIUbJ9IVCrq0gZISZdHML + Ry1DNffMR9CpD77KocOAUABfopcvH/3UGOHn + TFxkAr447zPaaoC68JYGxYLfZk8= ) + 1800 NS ns.example.org. + 1800 RRSIG NS 5 2 1800 ( + 20170702091734 20170602091734 54282 example.org. + McM4UdMxkscVQkJnnEbdqwyjpPgq5a/EuOLA + r2MvG43/cwOaWULiZoNzLi5Rjzhf+GTeVTan + jw6EsL3gEuYI1nznwlLQ04/G0XAHjbq5VvJc + rlscBD+dzf774yfaTjRNoeo2xTem6S7nyYPW + Y+1f6xkrsQPLYJfZ6VZ9QqyupBw= ) + 14400 NSEC dname.example.org. NS SOA RRSIG NSEC DNSKEY + 14400 RRSIG NSEC 5 2 14400 ( + 20170702091734 20170602091734 54282 example.org. + VT+IbjDFajM0doMKFipdX3+UXfCn3iHIxg5x + LElp4Q/YddTbX+6tZf53+EO+G8Kye3JDLwEl + o8VceijNeF3igZ+LiZuXCei5Qg/TJ7IAUnAO + xd85IWwEYwyKkKd6Z2kXbAN2pdcHE8EmboQd + wfTr9oyWhpZk1Z+pN8vdejPrG0M= ) + 1800 DNSKEY 256 3 5 ( + AwEAAczLlmTk5bMXUzpBo/Jta6MWSZYy3Nfw + gz8t/pkfSh4IlFF6vyXZhEqCeQsCBdD7ltkD + h5qd4A+nFrYOMwsi5XIjoHMlJN15xwFS9EgS + ZrZmuxePIEiYB5KccEf9JQMgM1t07Iu1FnrY + 02OuAqGWcO4tuyTLaK3QP4MLQOfAgKqf + ) ; ZSK; alg = RSASHA1; key id = 54282 + 1800 RRSIG DNSKEY 5 2 1800 ( + 20170702091734 20170602091734 54282 example.org. + MBgSRtZ6idJblLIHxZWpWL/1oqIwImb1mkl7 + hDFxqV6Hw19yLX06P7gcJEWiisdZBkVEfcOK + LeMJly05vgKfrMzLgIu2Ry4bL8AMKc8NMXBG + b1VDCEBW69P2omogj2KnORHDCZQr/BX9+wBU + 5rIMTTKlMSI5sT6ecJHHEymtiac= ) +dname.example.org. 1800 IN A 127.0.0.1 + 1800 RRSIG A 5 3 1800 ( + 20170702091734 20170602091734 54282 example.org. + LPCK2nLyDdGwvmzGLkUO2atEUjoc+aEspkC3 + keZCdXZaLnAwBH7dNAjvvXzzy0WrgWeiyDb4 + +rJ2N0oaKEZicM4QQDHKhugJblKbU5G4qTey + LSEaV3vvQnzGd0S6dCqnwfPj9czagFN7Zlf5 + DmLtdxx0aiDPCUpqT0+H/vuGPfk= ) + 1800 DNAME test.example.org. + 1800 RRSIG DNAME 5 3 1800 ( + 20170702091734 20170602091734 54282 example.org. + HvX79T1flWJ8H9/1XZjX6gz8rP/o2jbfPXJ9 + vC7ids/ZJilSReabLru4DCqcw1IV2DM/CZdE + tBnED/T2PJXvMut9tnYMrz+ZFPxoV6XyA3Z7 + bok3B0OuxizzAN2EXdol04VdbMHoWUzjQCzi + 0Ri12zLGRPzDepZ7FolgD+JtiBM= ) + 14400 NSEC a.dname.example.org. A DNAME RRSIG NSEC + 14400 RRSIG NSEC 5 3 14400 ( + 20170702091734 20170602091734 54282 example.org. + U3ZPYMUBJl3wF2SazQv/kBf6ec0CH+7n0Hr9 + w6lBKkiXz7P9WQzJDVnTHEZOrbDI6UetFGyC + 6qcaADCASZ9Wxc+riyK1Hl4ox+Y/CHJ97WHy + oS2X//vEf6qmbHQXin0WQtFdU/VCRYF40X5v + 8VfqOmrr8iKiEqXND8XNVf58mTw= ) +a.dname.example.org. 1800 IN A 127.0.0.1 + 1800 RRSIG A 5 4 1800 ( + 20170702091734 20170602091734 54282 example.org. + y7RHBWZwli8SJQ4BgTmdXmYS3KGHZ7AitJCx + zXFksMQtNoOfVEQBwnFqjAb8ezcV5u92h1gN + i1EcuxCFiElML1XFT8dK2GnlPAga9w3oIwd5 + wzW/YHcnR0P9lF56Sl7RoIt6+jJqOdRfixS6 + TDoLoXsNbOxQ+qV3B8pU2Tam204= ) + 14400 NSEC ns.example.org. A RRSIG NSEC + 14400 RRSIG NSEC 5 4 14400 ( + 20170702091734 20170602091734 54282 example.org. + Tmu27q3+xfONSZZtZLhejBUVtEw+83ZU1AFb + Rsxctjry/x5r2JSxw/sgSAExxX/7tx/okZ8J + oJqtChpsr91Kiw3eEBgINi2lCYIpMJlW4cWz + 8bYlHfR81VsKYgy/cRgrq1RRvBoJnw+nwSty + mKPIvUtt67LAvLxJheSCEMZLCKI= ) +ns.example.org. 1800 IN A 127.0.0.1 + 1800 RRSIG A 5 3 1800 ( + 20170702091734 20170602091734 54282 example.org. + mhi1SGaaAt+ndQEg5uKWKCH0HMzaqh/9dUK3 + p2wWMBrLbTZrcWyz10zRnvehicXDCasbBrer + ZpDQnz5AgxYYBURvdPfUzx1XbNuRJRE4l5PN + CEUTlTWcqCXnlSoPKEJE5HRf7v0xg2BrBUfM + 4mZnW2bFLwjrRQ5mm/mAmHmTROk= ) + 14400 NSEC example.org. A RRSIG NSEC + 14400 RRSIG NSEC 5 3 14400 ( + 20170702091734 20170602091734 54282 example.org. + loHcdjX+NIWLAkUDfPSy2371wrfUvrBQTfMO + 17eO2Y9E/6PE935NF5bjQtZBRRghyxzrFJhm + vY1Ad5ZTb+NLHvdSWbJQJog+eCc7QWp64WzR + RXpMdvaE6ZDwalWldLjC3h8QDywDoFdndoRY + eHOsmTvvtWWqtO6Fa5A8gmHT5HA= ) +` +*/ diff --git a/ag_201_coredns/plugin/file/dnssec_test.go b/ag_201_coredns/plugin/file/dnssec_test.go new file mode 100644 index 0000000..7292523 --- /dev/null +++ b/ag_201_coredns/plugin/file/dnssec_test.go @@ -0,0 +1,350 @@ +package file + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +// All OPT RR are added in server.go, so we don't specify them in the unit tests. +var dnssecTestCases = []test.Case{ + { + Qname: "miek.nl.", Qtype: dns.TypeSOA, Do: true, + Answer: []dns.RR{ + test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="), + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + Ns: auth, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeAAAA, Do: true, + Answer: []dns.RR{ + test.AAAA("miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + test.RRSIG("miek.nl. 1800 IN RRSIG AAAA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. SsRT="), + }, + Ns: auth, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeNS, Do: true, + Answer: []dns.RR{ + test.NS("miek.nl. 1800 IN NS ext.ns.whyscream.net."), + test.NS("miek.nl. 1800 IN NS linode.atoom.net."), + test.NS("miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."), + test.NS("miek.nl. 1800 IN NS omval.tednet.nl."), + test.RRSIG("miek.nl. 1800 IN RRSIG NS 8 2 1800 20160426031301 20160327031301 12051 miek.nl. ZLtsQhwaz+lHfNpztFoR1Vxs="), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeMX, Do: true, + Answer: []dns.RR{ + test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."), + test.MX("miek.nl. 1800 IN MX 10 aspmx2.googlemail.com."), + test.MX("miek.nl. 1800 IN MX 10 aspmx3.googlemail.com."), + test.MX("miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com."), + test.MX("miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com."), + test.RRSIG("miek.nl. 1800 IN RRSIG MX 8 2 1800 20160426031301 20160327031301 12051 miek.nl. kLqG+iOr="), + }, + Ns: auth, + }, + { + Qname: "www.miek.nl.", Qtype: dns.TypeA, Do: true, + Answer: []dns.RR{ + test.A("a.miek.nl. 1800 IN A 139.162.196.78"), + test.RRSIG("a.miek.nl. 1800 IN RRSIG A 8 3 1800 20160426031301 20160327031301 12051 miek.nl. lxLotCjWZ3kihTxk="), + test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."), + test.RRSIG("www.miek.nl. 1800 RRSIG CNAME 8 3 1800 20160426031301 20160327031301 12051 miek.nl. NVZmMJaypS+wDL2Lar4Zw1zF"), + }, + Ns: auth, + }, + { + // NoData + Qname: "a.miek.nl.", Qtype: dns.TypeSRV, Do: true, + Ns: []dns.RR{ + test.NSEC("a.miek.nl. 14400 IN NSEC archive.miek.nl. A AAAA RRSIG NSEC"), + test.RRSIG("a.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160426031301 20160327031301 12051 miek.nl. GqnF6cutipmSHEao="), + test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="), + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, + { + Qname: "b.miek.nl.", Qtype: dns.TypeA, Do: true, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.NSEC("archive.miek.nl. 14400 IN NSEC go.dns.miek.nl. CNAME RRSIG NSEC"), + test.RRSIG("archive.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160426031301 20160327031301 12051 miek.nl. jEpx8lcp4do5fWXg="), + test.NSEC("miek.nl. 14400 IN NSEC a.miek.nl. A NS SOA MX AAAA RRSIG NSEC DNSKEY"), + test.RRSIG("miek.nl. 14400 IN RRSIG NSEC 8 2 14400 20160426031301 20160327031301 12051 miek.nl. mFfc3r/9PSC1H6oSpdC"), + test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="), + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, + { + Qname: "b.blaat.miek.nl.", Qtype: dns.TypeA, Do: true, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.NSEC("archive.miek.nl. 14400 IN NSEC go.dns.miek.nl. CNAME RRSIG NSEC"), + test.RRSIG("archive.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160426031301 20160327031301 12051 miek.nl. jEpx8lcp4do5fWXg="), + test.NSEC("miek.nl. 14400 IN NSEC a.miek.nl. A NS SOA MX AAAA RRSIG NSEC DNSKEY"), + test.RRSIG("miek.nl. 14400 IN RRSIG NSEC 8 2 14400 20160426031301 20160327031301 12051 miek.nl. mFfc3r/9PSC1H6oSpdC"), + test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="), + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, + { + Qname: "b.a.miek.nl.", Qtype: dns.TypeA, Do: true, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + // dedupped NSEC, because 1 nsec tells all + test.NSEC("a.miek.nl. 14400 IN NSEC archive.miek.nl. A AAAA RRSIG NSEC"), + test.RRSIG("a.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160426031301 20160327031301 12051 miek.nl. GqnF6cut/RRGPQ1QGQE1ipmSHEao="), + test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="), + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, +} + +var auth = []dns.RR{ + test.NS("miek.nl. 1800 IN NS ext.ns.whyscream.net."), + test.NS("miek.nl. 1800 IN NS linode.atoom.net."), + test.NS("miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."), + test.NS("miek.nl. 1800 IN NS omval.tednet.nl."), + test.RRSIG("miek.nl. 1800 IN RRSIG NS 8 2 1800 20160426031301 20160327031301 12051 miek.nl. ZLtsQhwazbqSpztFoR1Vxs="), +} + +func TestLookupDNSSEC(t *testing.T) { + zone, err := Parse(strings.NewReader(dbMiekNLSigned), 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() + + for _, tc := range dnssecTestCases { + 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 + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +func BenchmarkFileLookupDNSSEC(b *testing.B) { + zone, err := Parse(strings.NewReader(dbMiekNLSigned), testzone, "stdin", 0) + if err != nil { + return + } + + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}} + ctx := context.TODO() + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + tc := test.Case{ + Qname: "b.miek.nl.", Qtype: dns.TypeA, Do: true, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.NSEC("archive.miek.nl. 14400 IN NSEC go.dns.miek.nl. CNAME RRSIG NSEC"), + test.RRSIG("archive.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160426031301 20160327031301 12051 miek.nl. jEpx8lcp4do5fWXg="), + test.NSEC("miek.nl. 14400 IN NSEC a.miek.nl. A NS SOA MX AAAA RRSIG NSEC DNSKEY"), + test.RRSIG("miek.nl. 14400 IN RRSIG NSEC 8 2 14400 20160426031301 20160327031301 12051 miek.nl. mFfc3r/9PSC1H6oSpdC"), + test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="), + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + } + + m := tc.Msg() + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + fm.ServeDNS(ctx, rec, m) + } +} + +const dbMiekNLSigned = ` +; File written on Sun Mar 27 04:13:01 2016 +; dnssec_signzone version 9.10.3-P4-Ubuntu +miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. ( + 1459051981 ; serial + 14400 ; refresh (4 hours) + 3600 ; retry (1 hour) + 604800 ; expire (1 week) + 14400 ; minimum (4 hours) + ) + 1800 RRSIG SOA 8 2 1800 ( + 20160426031301 20160327031301 12051 miek.nl. + FIrzy07acBzrf6kNW13Ypmq/ahojoMqOj0qJ + ixTevTvwOEcVuw9GlJoYIHTYg+hm1sZHtx9K + RiVmYsm8SHKsJA1WzixtT4K7vQvM+T+qbeOJ + xA6YTivKUcGRWRXQlOTUAlHS/KqBEfmxKgRS + 68G4oOEClFDSJKh7RbtyQczy1dc= ) + 1800 NS ext.ns.whyscream.net. + 1800 NS omval.tednet.nl. + 1800 NS linode.atoom.net. + 1800 NS ns-ext.nlnetlabs.nl. + 1800 RRSIG NS 8 2 1800 ( + 20160426031301 20160327031301 12051 miek.nl. + ZLtsQhwaz+CwrgzgFiEAqbqS/JH65MYjziA3 + 6EXwlGDy41lcfGm71PpxA7cDzFhWNkJNk4QF + q48wtpP4IGPPpHbnJHKDUXj6se7S+ylAGbS+ + VgVJ4YaVcE6xA9ZVhVpz8CSSjeH34vmqq9xj + zmFjofuDvraZflHfNpztFoR1Vxs= ) + 1800 A 139.162.196.78 + 1800 RRSIG A 8 2 1800 ( + 20160426031301 20160327031301 12051 miek.nl. + hl+6Q075tsCkxIqbop8zZ6U8rlFvooz7Izzx + MgCZYVLcg75El28EXKIhBfRb1dPaKbd+v+AD + wrJMHL131pY5sU2Ly05K+7CqmmyaXgDaVsKS + rSw/TbhGDIItBemeseeuXGAKAbY2+gE7kNN9 + mZoQ9hRB3SrxE2jhctv66DzYYQQ= ) + 1800 MX 1 aspmx.l.google.com. + 1800 MX 5 alt1.aspmx.l.google.com. + 1800 MX 5 alt2.aspmx.l.google.com. + 1800 MX 10 aspmx2.googlemail.com. + 1800 MX 10 aspmx3.googlemail.com. + 1800 RRSIG MX 8 2 1800 ( + 20160426031301 20160327031301 12051 miek.nl. + kLqG+iOrKSzms1H9Et9me8Zts1rbyeCFSVQD + G9is/u6ec3Lqg2vwJddf/yRsjVpVgadWSAkc + GSDuD2dK8oBeP24axWc3Z1OY2gdMI7w+PKWT + Z+pjHVjbjM47Ii/a6jk5SYeOwpGMsdEwhtTP + vk2O2WGljifqV3uE7GshF5WNR10= ) + 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735 + 1800 RRSIG AAAA 8 2 1800 ( + 20160426031301 20160327031301 12051 miek.nl. + SsRTHytW4YTAuHovHQgfIMhNwMtMp4gaAU/Z + lgTO+IkBb9y9F8uHrf25gG6RqA1bnGV/gezV + NU5negXm50bf1BNcyn3aCwEbA0rCGYIL+nLJ + szlBVbBu6me/Ym9bbJlfgfHRDfsVy2ZkNL+B + jfNQtGCSDoJwshjcqJlfIVSardo= ) + 14400 NSEC a.miek.nl. A NS SOA MX AAAA RRSIG NSEC DNSKEY + 14400 RRSIG NSEC 8 2 14400 ( + 20160426031301 20160327031301 12051 miek.nl. + mFfc3r/9PSC1H6oSpdC+FDy/Iu02W2Tf0x+b + n6Lpe1gCC1uvcSUrrmBNlyAWRr5Zm+ZXssEb + cKddRGiu/5sf0bUWrs4tqokL/HUl10X/sBxb + HfwNAeD7R7+CkpMv67li5AhsDgmQzpX2r3P6 + /6oZyLvODGobysbmzeWM6ckE8IE= ) + 1800 DNSKEY 256 3 8 ( + AwEAAcNEU67LJI5GEgF9QLNqLO1SMq1EdoQ6 + E9f85ha0k0ewQGCblyW2836GiVsm6k8Kr5EC + IoMJ6fZWf3CQSQ9ycWfTyOHfmI3eQ/1Covhb + 2y4bAmL/07PhrL7ozWBW3wBfM335Ft9xjtXH + Py7ztCbV9qZ4TVDTW/Iyg0PiwgoXVesz + ) ; ZSK; alg = RSASHA256; key id = 12051 + 1800 DNSKEY 257 3 8 ( + AwEAAcWdjBl4W4wh/hPxMDcBytmNCvEngIgB + 9Ut3C2+QI0oVz78/WK9KPoQF7B74JQ/mjO4f + vIncBmPp6mFNxs9/WQX0IXf7oKviEVOXLjct + R4D1KQLX0wprvtUIsQFIGdXaO6suTT5eDbSd + 6tTwu5xIkGkDmQhhH8OQydoEuCwV245ZwF/8 + AIsqBYDNQtQ6zhd6jDC+uZJXg/9LuPOxFHbi + MTjp6j3CCW0kHbfM/YHZErWWtjPj3U3Z7knQ + SIm5PO5FRKBEYDdr5UxWJ/1/20SrzI3iztvP + wHDsA2rdHm/4YRzq7CvG4N0t9ac/T0a0Sxba + /BUX2UVPWaIVBdTRBtgHi0s= + ) ; KSK; alg = RSASHA256; key id = 33694 + 1800 RRSIG DNSKEY 8 2 1800 ( + 20160426031301 20160327031301 12051 miek.nl. + o/D6o8+/bNGQyyRvwZ2hM0BJ+3HirvNjZoko + yGhGe9sPSrYU39WF3JVIQvNJFK6W3/iwlKir + TPOeYlN6QilnztFq1vpCxwj2kxJaIJhZecig + LsKxY/fOHwZlIbBLZZadQG6JoGRLHnImSzpf + xtyVaXQtfnJFC07HHt9np3kICfE= ) + 1800 RRSIG DNSKEY 8 2 1800 ( + 20160426031301 20160327031301 33694 miek.nl. + Ak/mbbQVQV+nUgw5Sw/c+TSoYqIwbLARzuNE + QJvJNoRR4tKVOY6qSxQv+j5S7vzyORZ+yeDp + NlEa1T9kxZVBMABoOtLX5kRqZncgijuH8fxb + L57Sv2IzINI9+DOcy9Q9p9ygtwYzQKrYoNi1 + 0hwHi6emGkVG2gGghruMinwOJASGgQy487Yd + eIpcEKJRw73nxd2le/4/Vafy+mBpKWOczfYi + 5m9MSSxcK56NFYjPG7TvdIw0m70F/smY9KBP + pGWEdzRQDlqfZ4fpDaTAFGyRX0mPFzMbs1DD + 3hQ4LHUSi/NgQakdH9eF42EVEDeL4cI69K98 + 6NNk6X9TRslO694HKw== ) +a.miek.nl. 1800 IN A 139.162.196.78 + 1800 RRSIG A 8 3 1800 ( + 20160426031301 20160327031301 12051 miek.nl. + lxLotCjWZ3kikNNcePu6HOCqMHDINKFRJRD8 + laz2KQ9DKtgXPdnRw5RJvVITSj8GUVzw1ec1 + CYVEKu/eMw/rc953Zns528QBypGPeMNLe2vu + C6a6UhZnGHA48dSd9EX33eSJs0MP9xsC9csv + LGdzYmv++eslkKxkhSOk2j/hTxk= ) + 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735 + 1800 RRSIG AAAA 8 3 1800 ( + 20160426031301 20160327031301 12051 miek.nl. + ji3QMlaUzlK85ppB5Pc+y2WnfqOi6qrm6dm1 + bXgsEov/5UV1Lmcv8+Y5NBbTbBlXGlWcpqNp + uWpf9z3lbguDWznpnasN2MM8t7yxo/Cr7WRf + QCzui7ewpWiA5hq7j0kVbM4nnDc6cO+U93hO + mMhVbeVI70HM2m0HaHkziEyzVZk= ) + 14400 NSEC archive.miek.nl. A AAAA RRSIG NSEC + 14400 RRSIG NSEC 8 3 14400 ( + 20160426031301 20160327031301 12051 miek.nl. + GqnF6cut/KCxbnJj27MCjjVGkjObV0hLhHOP + E1/GXAUTEKG6BWxJq8hidS3p/yrOmP5PEL9T + 4FjBp0/REdVmGpuLaiHyMselES82p/uMMdY5 + QqRM6LHhZdO1zsRbyzOZbm5MsW6GR7K2kHlX + 9TdBIULiRRGPQ1QGQE1ipmSHEao= ) +archive.miek.nl. 1800 IN CNAME a.miek.nl. + 1800 RRSIG CNAME 8 3 1800 ( + 20160426031301 20160327031301 12051 miek.nl. + s4zVJiDrVuUiUFr8CNQLuXYYfpqpl8rovL50 + BYsub/xK756NENiOTAOjYH6KYg7RSzsygJjV + YQwXolZly2/KXAr48SCtxzkGFxLexxiKcFaj + vm7ZDl7Btoa5l68qmBcxOX5E/W0IKITi4PNK + mhBs7dlaf0IbPGNgMxae72RosxM= ) + 14400 NSEC go.dns.miek.nl. CNAME RRSIG NSEC + 14400 RRSIG NSEC 8 3 14400 ( + 20160426031301 20160327031301 12051 miek.nl. + jEp7LsoK++/PRFh2HieLzasA1jXBpp90NyDf + RfpfOxdM69yRKfvXMc2bazIiMuDhxht79dGI + Gj02cn1cvX60SlaHkeFtqTdJcHdK9rbI65EK + YHFZFzGh9XVnuMJKpUsm/xS1dnUSAnXN8q+0 + xBlUDlQpsAFv/cx8lcp4do5fWXg= ) +go.dns.miek.nl. 1800 IN TXT "Hello!" + 1800 RRSIG TXT 8 4 1800 ( + 20160426031301 20160327031301 12051 miek.nl. + O0uo1NsXTq2TTfgOmGbHQQEchrcpllaDAMMX + dTDizw3t+vZ5SR32qJ8W7y6VXLgUqJgcdRxS + Fou1pp+t5juRZSQ0LKgxMpZAgHorkzPvRf1b + E9eBKrDSuLGagsQRwHeldFGFgsXtCbf07vVH + zoKR8ynuG4/cAoY0JzMhCts+56U= ) + 14400 NSEC www.miek.nl. TXT RRSIG NSEC + 14400 RRSIG NSEC 8 4 14400 ( + 20160426031301 20160327031301 12051 miek.nl. + BW6qo7kYe3Z+Y0ebaVTWTy1c3bpdf8WUEoXq + WDQxLDEj2fFiuEBDaSN5lTWRg3wj8kZmr6Uk + LvX0P29lbATFarIgkyiAdbOEdaf88nMfqBW8 + z2T5xrPQcN0F13uehmv395yAJs4tebRxErMl + KdkVF0dskaDvw8Wo3YgjHUf6TXM= ) +www.miek.nl. 1800 IN CNAME a.miek.nl. + 1800 RRSIG CNAME 8 3 1800 ( + 20160426031301 20160327031301 12051 miek.nl. + MiQQh2lScoNiNVZmMJaypS+wDL2Lar4Zw1zF + Uo4tL16BfQOt7yl8gXdAH2JMFqoKAoIdM2K6 + XwFOwKTOGSW0oNCOcaE7ts+1Z1U0H3O2tHfq + FAzfg1s9pQ5zxk8J/bJgkVIkw2/cyB0y1/PK + EmIqvChBSb4NchTuMCSqo63LJM8= ) + 14400 NSEC miek.nl. CNAME RRSIG NSEC + 14400 RRSIG NSEC 8 3 14400 ( + 20160426031301 20160327031301 12051 miek.nl. + OPPZ8iaUPrVKEP4cqeCiiv1WLRAY30GRIhc/ + me0gBwFkbmTEnvB+rUp831OJZDZBNKv4QdZj + Uyc26wKUOQeUyMJqv4IRDgxH7nq9GB5JRjYZ + IVxtGD1aqWLXz+8aMaf9ARJjtYUd3K4lt8Wz + LbJSo5Wdq7GOWqhgkY5n3XD0/FA= )` diff --git a/ag_201_coredns/plugin/file/dnssex_test.go b/ag_201_coredns/plugin/file/dnssex_test.go new file mode 100644 index 0000000..d9a0a45 --- /dev/null +++ b/ag_201_coredns/plugin/file/dnssex_test.go @@ -0,0 +1,145 @@ +package file + +const dbDnssexNLSigned = ` +; File written on Tue Mar 29 21:02:24 2016 +; dnssec_signzone version 9.10.3-P4-Ubuntu +dnssex.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. ( + 1459281744 ; serial + 14400 ; refresh (4 hours) + 3600 ; retry (1 hour) + 604800 ; expire (1 week) + 14400 ; minimum (4 hours) + ) + 1800 RRSIG SOA 8 2 1800 ( + 20160428190224 20160329190224 14460 dnssex.nl. + CA/Y3m9hCOiKC/8ieSOv8SeP964BUdG/8MC3 + WtKljUosK9Z9bBGrVizDjjqgq++lyH8BZJcT + aabAsERs4xj5PRtcxicwQXZACX5VYjXHQeZm + CyytFU5wq2gcXSmvUH86zZzftx3RGPvn1aOo + TlcvoC3iF8fYUCpROlUS0YR8Cdw= ) + 1800 NS omval.tednet.nl. + 1800 NS linode.atoom.net. + 1800 NS ns-ext.nlnetlabs.nl. + 1800 RRSIG NS 8 2 1800 ( + 20160428190224 20160329190224 14460 dnssex.nl. + dLIeEvP86jj5nd3orv9bH7hTvkblF4Na0sbl + k6fJA6ha+FPN1d6Pig3NNEEVQ/+wlOp/JTs2 + v07L7roEEUCbBprI8gMSld2gFDwNLW3DAB4M + WD/oayYdAnumekcLzhgvWixTABjWAGRTGQsP + sVDFXsGMf9TGGC9FEomgkCVeNC0= ) + 1800 A 139.162.196.78 + 1800 RRSIG A 8 2 1800 ( + 20160428190224 20160329190224 14460 dnssex.nl. + LKJKLzPiSEDWOLAag2YpfD5EJCuDcEAJu+FZ + Xy+4VyOv9YvRHCTL4vbrevOo5+XymY2RxU1q + j+6leR/Fe7nlreSj2wzAAk2bIYn4m6r7hqeO + aKZsUFfpX8cNcFtGEywfHndCPELbRxFeEziP + utqHFLPNMX5nYCpS28w4oJ5sAnM= ) + 1800 TXT "Doing It Safe Is Better" + 1800 RRSIG TXT 8 2 1800 ( + 20160428190224 20160329190224 14460 dnssex.nl. + f6S+DUfJK1UYdOb3AHgUXzFTTtu+yLp/Fv7S + Hv0CAGhXAVw+nBbK719igFvBtObS33WKwzxD + 1pQNMaJcS6zeevtD+4PKB1KDC4fyJffeEZT6 + E30jGR8Y29/xA+Fa4lqDNnj9zP3b8TiABCle + ascY5abkgWCALLocFAzFJQ/27YQ= ) + 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735 + 1800 RRSIG AAAA 8 2 1800 ( + 20160428190224 20160329190224 14460 dnssex.nl. + PWcPSawEUBAfCuv0liEOQ8RYe7tfNW4rubIJ + LE+dbrub1DUer3cWrDoCYFtOufvcbkYJQ2CQ + AGjJmAQ5J2aqYDOPMrKa615V0KT3ifbZJcGC + gkIic4U/EXjaQpRoLdDzR9MyVXOmbA6sKYzj + ju1cNkLqM8D7Uunjl4pIr6rdSFo= ) + 14400 NSEC *.dnssex.nl. A NS SOA TXT AAAA RRSIG NSEC DNSKEY + 14400 RRSIG NSEC 8 2 14400 ( + 20160428190224 20160329190224 14460 dnssex.nl. + oIvM6JZIlNc1aNKGTxv58ApSnDr1nDPPgnD9 + 9oJZRIn7eb5WnpeDz2H3z5+x6Bhlp5hJJaUp + KJ3Ss6Jg/IDnrmIvKmgq6L6gHj1Y1IiHmmU8 + VeZTRzdTsDx/27OsN23roIvsytjveNSEMfIm + iLZ23x5kg1kBdJ9p3xjYHm5lR+8= ) + 1800 DNSKEY 256 3 8 ( + AwEAAazSO6uvLPEVknDA8yxjFe8nnAMU7txp + wb19k55hQ81WV3G4bpBM1NdN6sbYHrkXaTNx + 2bQWAkvX6pz0XFx3z/MPhW+vkakIWFYpyQ7R + AT5LIJfToVfiCDiyhhF0zVobKBInO9eoGjd9 + BAW3TUt+LmNAO/Ak5D5BX7R3CuA7v9k7 + ) ; ZSK; alg = RSASHA256; key id = 14460 + 1800 DNSKEY 257 3 8 ( + AwEAAbyeaV9zg0IqdtgYoqK5jJ239anzwG2i + gvH1DxSazLyaoNvEkCIvPgMLW/JWfy7Z1mQp + SMy9DtzL5pzRyQgw7kIeXLbi6jufUFd9pxN+ + xnzKLf9mY5AcnGToTrbSL+jnMT67wG+c34+Q + PeVfucHNUePBxsbz2+4xbXiViSQyCQGv + ) ; KSK; alg = RSASHA256; key id = 18772 + 1800 RRSIG DNSKEY 8 2 1800 ( + 20160428190224 20160329190224 14460 dnssex.nl. + cFSFtJE+DBGNxb52AweFaVHBe5Ue5MDpqNdC + TIneUnEhP2m+vK4zJ/TraK0WdQFpsX63pod8 + PZ9y03vHUfewivyonCCBD3DcNdoU9subhN22 + tez9Ct8Z5/9E4RAz7orXal4M1VUEhRcXSEH8 + SJW20mfVsqJAiKqqNeGB/pAj23I= ) + 1800 RRSIG DNSKEY 8 2 1800 ( + 20160428190224 20160329190224 18772 dnssex.nl. + oiiwo/7NYacePqohEp50261elhm6Dieh4j2S + VZGAHU5gqLIQeW9CxKJKtSCkBVgUo4cvO4Rn + 2tzArAuclDvBrMXRIoct8u7f96moeFE+x5FI + DYqICiV6k449ljj9o4t/5G7q2CRsEfxZKpTI + A/L0+uDk0RwVVzL45+TnilcsmZs= ) +*.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better" + 1800 RRSIG TXT 8 2 1800 ( + 20160428190224 20160329190224 14460 dnssex.nl. + FUZSTyvZfeuuOpCmNzVKOfITRHJ6/ygjmnnb + XGBxVUyQjoLuYXwD5XqZWGw4iKH6QeSDfGCx + 4MPqA4qQmW7Wwth7mat9yMfA4+p2sO84bysl + 7/BG9+W2G+q1uQiM9bX9V42P2X/XuW5Y/t9Y + 8u1sljQ7D8WwS6naH/vbaJxnDBw= ) + 14400 NSEC a.dnssex.nl. TXT RRSIG NSEC + 14400 RRSIG NSEC 8 2 14400 ( + 20160428190224 20160329190224 14460 dnssex.nl. + os6INm6q2eXknD5z8TpfbK00uxVbQefMvHcR + /RNX/kh0xXvzAaaDOV+Ge/Ko+2dXnKP+J1LY + G9ffXNpdbaQy5ygzH5F041GJst4566GdG/jt + 7Z7vLHYxEBTpZfxo+PLsXQXH3VTemZyuWyDf + qJzafXJVH1F0nDrcXmMlR6jlBHA= ) +www.dnssex.nl. 1800 IN CNAME a.dnssex.nl. + 1800 RRSIG CNAME 8 3 1800 ( + 20160428190224 20160329190224 14460 dnssex.nl. + Omv42q/uVvdNsWQoSrQ6m6w6U7r7Abga7uF4 + 25b3gZlse0C+WyMyGFMGUbapQm7azvBpreeo + uKJHjzd+ufoG+Oul6vU9vyoj+ejgHzGLGbJQ + HftfP+UqP5SWvAaipP/LULTWKPuiBcLDLiBI + PGTfsq0DB6R+qCDTV0fNnkgxEBQ= ) + 14400 NSEC dnssex.nl. CNAME RRSIG NSEC + 14400 RRSIG NSEC 8 3 14400 ( + 20160428190224 20160329190224 14460 dnssex.nl. + TBN3ddfZW+kC84/g3QlNNJMeLZoyCalPQylt + KXXLPGuxfGpl3RYRY8KaHbP+5a8MnHjqjuMB + Lofb7yKMFxpSzMh8E36vnOqry1mvkSakNj9y + 9jM8PwDjcpYUwn/ql76MsmNgEV5CLeQ7lyH4 + AOrL79yOSQVI3JHJIjKSiz88iSw= ) +a.dnssex.nl. 1800 IN A 139.162.196.78 + 1800 RRSIG A 8 3 1800 ( + 20160428190224 20160329190224 14460 dnssex.nl. + OXHpFj9nSpKi5yA/ULH7MOpGAWfyJ2yC/2xa + Pw0fqSY4QvcRt+V3adcFA4H9+P1b32GpxEjB + lXmCJID+H4lYkhUR4r4IOZBVtKG2SJEBZXip + pH00UkOIBiXxbGzfX8VL04v2G/YxUgLW57kA + aknaeTOkJsO20Y+8wmR9EtzaRFI= ) + 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735 + 1800 RRSIG AAAA 8 3 1800 ( + 20160428190224 20160329190224 14460 dnssex.nl. + jrepc/VnRzJypnrG0WDEqaAr3HMjWrPxJNX0 + 86gbFjZG07QxBmrA1rj0jM9YEWTjjyWb2tT7 + lQhzKDYX/0XdOVUeeOM4FoSks80V+pWR8fvj + AZ5HmX69g36tLosMDKNR4lXcrpv89QovG4Hr + /r58fxEKEFJqrLDjMo6aOrg+uKA= ) + 14400 NSEC www.dnssex.nl. A AAAA RRSIG NSEC + 14400 RRSIG NSEC 8 3 14400 ( + 20160428190224 20160329190224 14460 dnssex.nl. + S+UM62wXRNNFN3QDWK5YFWUbHBXC4aqaqinZ + A2ZDeC+IQgyw7vazPz7cLI5T0YXXks0HTMlr + soEjKnnRZsqSO9EuUavPNE1hh11Jjm0fB+5+ + +Uro0EmA5Dhgc0Z2VpbXVQEhNDf/pI1gem15 + RffN2tBYNykZn4Has2ySgRaaRYQ= )` diff --git a/ag_201_coredns/plugin/file/ds_test.go b/ag_201_coredns/plugin/file/ds_test.go new file mode 100644 index 0000000..74f7bbd --- /dev/null +++ b/ag_201_coredns/plugin/file/ds_test.go @@ -0,0 +1,77 @@ +package file + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var dsTestCases = []test.Case{ + { + Qname: "a.delegated.miek.nl.", Qtype: dns.TypeDS, + Ns: []dns.RR{ + test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."), + test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."), + }, + Extra: []dns.RR{ + test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"), + test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + }, + { + Qname: "_udp.delegated.miek.nl.", Qtype: dns.TypeDS, + Ns: []dns.RR{ + test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."), + test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."), + }, + Extra: []dns.RR{ + test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"), + test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + }, + { + // This works *here* because we skip the server routing for DS in core/dnsserver/server.go + Qname: "_udp.miek.nl.", Qtype: dns.TypeDS, + 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: "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"), + }, + }, +} + +func TestLookupDS(t *testing.T) { + zone, err := Parse(strings.NewReader(dbMiekNLDelegation), 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() + + for _, tc := range dsTestCases { + 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 + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} diff --git a/ag_201_coredns/plugin/file/ent_test.go b/ag_201_coredns/plugin/file/ent_test.go new file mode 100644 index 0000000..73f5085 --- /dev/null +++ b/ag_201_coredns/plugin/file/ent_test.go @@ -0,0 +1,159 @@ +package file + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var entTestCases = []test.Case{ + { + Qname: "b.c.miek.nl.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, + { + Qname: "b.c.miek.nl.", Qtype: dns.TypeA, Do: true, + Ns: []dns.RR{ + test.NSEC("a.miek.nl. 14400 IN NSEC a.b.c.miek.nl. A RRSIG NSEC"), + test.RRSIG("a.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160502144311 20160402144311 12051 miek.nl. d5XZEy6SUpq98ZKUlzqhAfkLI9pQPc="), + test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160502144311 20160402144311 12051 miek.nl. KegoBxA3Tbrhlc4cEdkRiteIkOfsq"), + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, +} + +func TestLookupEnt(t *testing.T) { + zone, err := Parse(strings.NewReader(dbMiekENTNL), testzone, "stdin", 0) + if err != nil { + t.Fatalf("Expect 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() + + for _, tc := range entTestCases { + 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 + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +const dbMiekENTNL = `; File written on Sat Apr 2 16:43:11 2016 +; dnssec_signzone version 9.10.3-P4-Ubuntu +miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. ( + 1282630057 ; serial + 14400 ; refresh (4 hours) + 3600 ; retry (1 hour) + 604800 ; expire (1 week) + 14400 ; minimum (4 hours) + ) + 1800 RRSIG SOA 8 2 1800 ( + 20160502144311 20160402144311 12051 miek.nl. + KegoBxA3Tbrhlc4cEdkRiteIkOfsqD4oCLLM + ISJ5bChWy00LGHUlAnHVu5Ti96hUjVNmGSxa + xtGSuAAMFCr52W8pAB8LBIlu9B6QZUPHMccr + SuzxAX3ioawk2uTjm+k8AGPT4RoQdXemGLAp + zJTASolTVmeMTh5J0sZTZJrtvZ0= ) + 1800 NS linode.atoom.net. + 1800 RRSIG NS 8 2 1800 ( + 20160502144311 20160402144311 12051 miek.nl. + m0cOHL6Rre/0jZPXe+0IUjs/8AFASRCvDbSx + ZQsRDSlZgS6RoMP3OC77cnrKDVlfZ2Vhq3Ce + nYPoGe0/atB92XXsilmstx4HTSU64gsV9iLN + Xkzk36617t7zGOl/qumqfaUXeA9tihItzEim + 6SGnufVZI4o8xeyaVCNDDuN0bvY= ) + 14400 NSEC a.miek.nl. NS SOA RRSIG NSEC DNSKEY + 14400 RRSIG NSEC 8 2 14400 ( + 20160502144311 20160402144311 12051 miek.nl. + BCWVgwxWrs4tBjS9QXKkftCUbiLi40NyH1yA + nbFy1wCKQ2jDH00810+ia4b66QrjlAKgxE9z + 9U7MKSMV86sNkyAtlCi+2OnjtWF6sxPdJO7k + CHeg46XBjrQuiJRY8CneQX56+IEPdufLeqPR + l+ocBQ2UkGhXmQdWp3CFDn2/eqU= ) + 1800 DNSKEY 256 3 8 ( + AwEAAcNEU67LJI5GEgF9QLNqLO1SMq1EdoQ6 + E9f85ha0k0ewQGCblyW2836GiVsm6k8Kr5EC + IoMJ6fZWf3CQSQ9ycWfTyOHfmI3eQ/1Covhb + 2y4bAmL/07PhrL7ozWBW3wBfM335Ft9xjtXH + Py7ztCbV9qZ4TVDTW/Iyg0PiwgoXVesz + ) ; ZSK; alg = RSASHA256; key id = 12051 + 1800 DNSKEY 257 3 8 ( + AwEAAcWdjBl4W4wh/hPxMDcBytmNCvEngIgB + 9Ut3C2+QI0oVz78/WK9KPoQF7B74JQ/mjO4f + vIncBmPp6mFNxs9/WQX0IXf7oKviEVOXLjct + R4D1KQLX0wprvtUIsQFIGdXaO6suTT5eDbSd + 6tTwu5xIkGkDmQhhH8OQydoEuCwV245ZwF/8 + AIsqBYDNQtQ6zhd6jDC+uZJXg/9LuPOxFHbi + MTjp6j3CCW0kHbfM/YHZErWWtjPj3U3Z7knQ + SIm5PO5FRKBEYDdr5UxWJ/1/20SrzI3iztvP + wHDsA2rdHm/4YRzq7CvG4N0t9ac/T0a0Sxba + /BUX2UVPWaIVBdTRBtgHi0s= + ) ; KSK; alg = RSASHA256; key id = 33694 + 1800 RRSIG DNSKEY 8 2 1800 ( + 20160502144311 20160402144311 12051 miek.nl. + YNpi1jRDQKpnsQEjIjxqy+kJGaYnV16e8Iug + 40c82y4pee7kIojFUllSKP44qiJpCArxF557 + tfjfwBd6c4hkqCScGPZXJ06LMyG4u//rhVMh + 4hyKcxzQFKxmrFlj3oQGksCI8lxGX6RxiZuR + qv2ol2lUWrqetpAL+Zzwt71884E= ) + 1800 RRSIG DNSKEY 8 2 1800 ( + 20160502144311 20160402144311 33694 miek.nl. + jKpLDEeyadgM0wDgzEk6sBBdWr2/aCrkAOU/ + w6dYIafN98f21oIYQfscV1gc7CTsA0vwzzUu + x0QgwxoNLMvSxxjOiW/2MzF8eozczImeCWbl + ad/pVCYH6Jn5UBrZ5RCWMVcs2RP5KDXWeXKs + jEN/0EmQg5qNd4zqtlPIQinA9I1HquJAnS56 + pFvYyGIbZmGEbhR18sXVBeTWYr+zOMHn2quX + 0kkrx2udz+sPg7i4yRsLdhw138gPRy1qvbaC + 8ELs1xo1mC9pTlDOhz24Q3iXpVAU1lXLYOh9 + nUP1/4UvZEYXHBUQk/XPRciojniWjAF825x3 + QoSivMHblBwRdAKJSg== ) +a.miek.nl. 1800 IN A 127.0.0.1 + 1800 RRSIG A 8 3 1800 ( + 20160502144311 20160402144311 12051 miek.nl. + lUOYdSxScjyYz+Ebc+nb6iTNgCohqj7K+Dat + 97KE7haV2nP3LxdYuDCJYZpeyhsXDLHd4bFI + bInYPwJiC6DUCxPCuCWy0KYlZOWW8KCLX3Ia + BOPQbvIwLsJhnX+/tyMD9mXortoqATO79/6p + nNxvFeM8pFDwaih17fXMuFR/BsI= ) + 14400 NSEC a.b.c.miek.nl. A RRSIG NSEC + 14400 RRSIG NSEC 8 3 14400 ( + 20160502144311 20160402144311 12051 miek.nl. + d5XZEy6SUp+TPRJQED+0R65zf2Yeo/1dlEA2 + jYYvkXGSHXke4sg9nH8U3nr1rLcuqA1DsQgH + uMIjdENvXuZ+WCSwvIbhC+JEI6AyQ6Gfaf/D + I3mfu60C730IRByTrKM5C2rt11lwRQlbdaUY + h23/nn/q98ZKUlzqhAfkLI9pQPc= ) +a.b.c.miek.nl. 1800 IN A 127.0.0.1 + 1800 RRSIG A 8 5 1800 ( + 20160502144311 20160402144311 12051 miek.nl. + FwgU5+fFD4hEebco3gvKQt3PXfY+dcOJr8dl + Ky4WLsONIdhP+4e9oprPisSLxImErY21BcrW + xzu1IZrYDsS8XBVV44lBx5WXEKvAOrUcut/S + OWhFZW7ncdIQCp32ZBIatiLRJEqXUjx+guHs + noFLiHix35wJWsRKwjGLIhH1fbs= ) + 14400 NSEC miek.nl. A RRSIG NSEC + 14400 RRSIG NSEC 8 5 14400 ( + 20160502144311 20160402144311 12051 miek.nl. + lXgOqm9/jRRYvaG5jC1CDvTtGYxMroTzf4t4 + jeYGb60+qI0q9sHQKfAJvoQ5o8o1qfR7OuiF + f544ipYT9eTcJRyGAOoJ37yMie7ZIoVJ91tB + r8YdzZ9Q6x3v1cbwTaQiacwhPZhGYOw63qIs + q5IQErIPos2sNk+y9D8BEce2DO4= )` diff --git a/ag_201_coredns/plugin/file/example_org.go b/ag_201_coredns/plugin/file/example_org.go new file mode 100644 index 0000000..eba18e0 --- /dev/null +++ b/ag_201_coredns/plugin/file/example_org.go @@ -0,0 +1,113 @@ +package file + +// exampleOrgSigned is a fake signed example.org zone with two delegations, +// one signed (with DSs) and one "normal". +const exampleOrgSigned = ` +example.org. 1800 IN SOA a.iana-servers.net. devnull.example.org. ( + 1282630057 ; serial + 14400 ; refresh (4 hours) + 3600 ; retry (1 hour) + 604800 ; expire (1 week) + 14400 ; minimum (4 hours) + ) + 1800 RRSIG SOA 13 2 1800 ( + 20161129153240 20161030153240 49035 example.org. + GVnMpFmN+6PDdgCtlYDEYBsnBNDgYmEJNvos + Bk9+PNTPNWNst+BXCpDadTeqRwrr1RHEAQ7j + YWzNwqn81pN+IA== ) + 1800 NS a.iana-servers.net. + 1800 NS b.iana-servers.net. + 1800 RRSIG NS 13 2 1800 ( + 20161129153240 20161030153240 49035 example.org. + llrHoIuwjnbo28LOt4p5zWAs98XGqrXicKVI + Qxyaf/ORM8boJvW2XrKr3nj6Y8FKMhzd287D + 5PBzVCL6MZyjQg== ) + 14400 NSEC a.example.org. NS SOA RRSIG NSEC DNSKEY + 14400 RRSIG NSEC 13 2 14400 ( + 20161129153240 20161030153240 49035 example.org. + BQROf1swrmYi3GqpP5M/h5vTB8jmJ/RFnlaX + 7fjxvV7aMvXCsr3ekWeB2S7L6wWFihDYcKJg + 9BxVPqxzBKeaqg== ) + 1800 DNSKEY 256 3 13 ( + UNTqlHbC51EbXuY0rshW19Iz8SkCuGVS+L0e + bQj53dvtNlaKfWmtTauC797FoyVLbQwoMy/P + G68SXgLCx8g+9g== + ) ; ZSK; alg = ECDSAP256SHA256; key id = 49035 + 1800 RRSIG DNSKEY 13 2 1800 ( + 20161129153240 20161030153240 49035 example.org. + LnLHyqYJaCMOt7EHB4GZxzAzWLwEGCTFiEhC + jj1X1VuQSjJcN42Zd3yF+jihSW6huknrig0Z + Mqv0FM6mJ/qPKg== ) +a.delegated.example.org. 1800 IN A 139.162.196.78 + 1800 TXT "obscured" + 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735 +archive.example.org. 1800 IN CNAME a.example.org. + 1800 RRSIG CNAME 13 3 1800 ( + 20161129153240 20161030153240 49035 example.org. + SDFW1z/PN9knzH8BwBvmWK0qdIwMVtGrMgRw + 7lgy4utRrdrRdCSLZy3xpkmkh1wehuGc4R0S + 05Z3DPhB0Fg5BA== ) + 14400 NSEC delegated.example.org. CNAME RRSIG NSEC + 14400 RRSIG NSEC 13 3 14400 ( + 20161129153240 20161030153240 49035 example.org. + DQqLSVNl8F6v1K09wRU6/M6hbHy2VUddnOwn + JusJjMlrAOmoOctCZ/N/BwqCXXBA+d9yFGdH + knYumXp+BVPBAQ== ) +www.example.org. 1800 IN CNAME a.example.org. + 1800 RRSIG CNAME 13 3 1800 ( + 20161129153240 20161030153240 49035 example.org. + adzujOxCV0uBV4OayPGfR11iWBLiiSAnZB1R + slmhBFaDKOKSNYijGtiVPeaF+EuZs63pzd4y + 6Nm2Iq9cQhAwAA== ) + 14400 NSEC example.org. CNAME RRSIG NSEC + 14400 RRSIG NSEC 13 3 14400 ( + 20161129153240 20161030153240 49035 example.org. + jy3f96GZGBaRuQQjuqsoP1YN8ObZF37o+WkV + PL7TruzI7iNl0AjrUDy9FplP8Mqk/HWyvlPe + N3cU+W8NYlfDDQ== ) +a.example.org. 1800 IN A 139.162.196.78 + 1800 RRSIG A 13 3 1800 ( + 20161129153240 20161030153240 49035 example.org. + 41jFz0Dr8tZBN4Kv25S5dD4vTmviFiLx7xSA + qMIuLFm0qibKL07perKpxqgLqM0H1wreT4xz + I9Y4Dgp1nsOuMA== ) + 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735 + 1800 RRSIG AAAA 13 3 1800 ( + 20161129153240 20161030153240 49035 example.org. + brHizDxYCxCHrSKIu+J+XQbodRcb7KNRdN4q + VOWw8wHqeBsFNRzvFF6jwPQYphGP7kZh1KAb + VuY5ZVVhM2kHjw== ) + 14400 NSEC archive.example.org. A AAAA RRSIG NSEC + 14400 RRSIG NSEC 13 3 14400 ( + 20161129153240 20161030153240 49035 example.org. + zIenVlg5ScLr157EWigrTGUgrv7W/1s49Fic + i2k+OVjZfT50zw+q5X6DPKkzfAiUhIuqs53r + hZUzZwV/1Wew9Q== ) +delegated.example.org. 1800 IN NS a.delegated.example.org. + 1800 IN NS ns-ext.nlnetlabs.nl. + 1800 DS 10056 5 1 ( + EE72CABD1927759CDDA92A10DBF431504B9E + 1F13 ) + 1800 DS 10056 5 2 ( + E4B05F87725FA86D9A64F1E53C3D0E625094 + 6599DFE639C45955B0ED416CDDFA ) + 1800 RRSIG DS 13 3 1800 ( + 20161129153240 20161030153240 49035 example.org. + rlNNzcUmtbjLSl02ZzQGUbWX75yCUx0Mug1j + HtKVqRq1hpPE2S3863tIWSlz+W9wz4o19OI4 + jbznKKqk+DGKog== ) + 14400 NSEC sub.example.org. NS DS RRSIG NSEC + 14400 RRSIG NSEC 13 3 14400 ( + 20161129153240 20161030153240 49035 example.org. + lNQ5kRTB26yvZU5bFn84LYFCjwWTmBcRCDbD + cqWZvCSw4LFOcqbz1/wJKIRjIXIqnWIrfIHe + fZ9QD5xZsrPgUQ== ) +sub.example.org. 1800 IN NS sub1.example.net. + 1800 IN NS sub2.example.net. + 14400 NSEC www.example.org. NS RRSIG NSEC + 14400 RRSIG NSEC 13 3 14400 ( + 20161129153240 20161030153240 49035 example.org. + VYjahdV+TTkA3RBdnUI0hwXDm6U5k/weeZZr + ix1znORpOELbeLBMJW56cnaG+LGwOQfw9qqj + bOuULDst84s4+g== ) +` diff --git a/ag_201_coredns/plugin/file/file.go b/ag_201_coredns/plugin/file/file.go new file mode 100644 index 0000000..f50c3d0 --- /dev/null +++ b/ag_201_coredns/plugin/file/file.go @@ -0,0 +1,164 @@ +// Package file implements a file backend. +package file + +import ( + "context" + "fmt" + "io" + + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/transfer" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +var log = clog.NewWithPlugin("file") + +type ( + // File is the plugin that reads zone data from disk. + File struct { + Next plugin.Handler + Zones + transfer *transfer.Transfer + } + + // Zones maps zone names to a *Zone. + Zones struct { + Z map[string]*Zone // A map mapping zone (origin) to the Zone's data + Names []string // All the keys from the map Z as a string slice. + } +) + +// ServeDNS implements the plugin.Handle interface. +func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + + qname := state.Name() + // TODO(miek): match the qname better in the map + zone := plugin.Zones(f.Zones.Names).Matches(qname) + if zone == "" { + return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r) + } + + z, ok := f.Zones.Z[zone] + if !ok || z == nil { + return dns.RcodeServerFailure, nil + } + + // If transfer is not loaded, we'll see these, answer with refused (no transfer allowed). + if state.QType() == dns.TypeAXFR || state.QType() == dns.TypeIXFR { + return dns.RcodeRefused, nil + } + + // This is only for when we are a secondary zones. + if r.Opcode == dns.OpcodeNotify { + if z.isNotify(state) { + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative = true + w.WriteMsg(m) + + log.Infof("Notify from %s for %s: checking transfer", state.IP(), zone) + ok, err := z.shouldTransfer() + if ok { + z.TransferIn() + } else { + log.Infof("Notify from %s for %s: no SOA serial increase seen", state.IP(), zone) + } + if err != nil { + log.Warningf("Notify from %s for %s: failed primary check: %s", state.IP(), zone, err) + } + return dns.RcodeSuccess, nil + } + log.Infof("Dropping notify from %s for %s", state.IP(), zone) + return dns.RcodeSuccess, nil + } + + z.RLock() + exp := z.Expired + z.RUnlock() + if exp { + log.Errorf("Zone %s is expired", zone) + return dns.RcodeServerFailure, nil + } + + answer, ns, extra, result := z.Lookup(ctx, state, qname) + + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative = true + m.Answer, m.Ns, m.Extra = answer, ns, extra + + switch result { + case Success: + case NoData: + case NameError: + m.Rcode = dns.RcodeNameError + case Delegation: + m.Authoritative = false + case ServerFailure: + // 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) + return dns.RcodeSuccess, nil +} + +// Name implements the Handler interface. +func (f File) Name() string { return "file" } + +type serialErr struct { + err string + zone string + origin string + serial int64 +} + +func (s *serialErr) Error() string { + return fmt.Sprintf("%s for origin %s in file %s, with %d SOA serial", s.err, s.origin, s.zone, s.serial) +} + +// Parse parses the zone in filename and returns a new Zone or an error. +// If serial >= 0 it will reload the zone, if the SOA hasn't changed +// it returns an error indicating nothing was read. +func Parse(f io.Reader, origin, fileName string, serial int64) (*Zone, error) { + zp := dns.NewZoneParser(f, dns.Fqdn(origin), fileName) + zp.SetIncludeAllowed(true) + 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 + + // -1 is valid serial is we failed to load the file on startup. + + if serial >= 0 && s.Serial == uint32(serial) { // same serial + return nil, &serialErr{err: "no change in SOA serial", origin: origin, zone: fileName, serial: serial} + } + } + } + + if err := z.Insert(rr); err != nil { + return nil, err + } + } + if !seenSOA { + return nil, fmt.Errorf("file %q has no SOA record for origin %s", fileName, origin) + } + + return z, nil +} diff --git a/ag_201_coredns/plugin/file/file_test.go b/ag_201_coredns/plugin/file/file_test.go new file mode 100644 index 0000000..0e4050e --- /dev/null +++ b/ag_201_coredns/plugin/file/file_test.go @@ -0,0 +1,31 @@ +package file + +import ( + "strings" + "testing" +) + +func BenchmarkFileParseInsert(b *testing.B) { + for i := 0; i < b.N; i++ { + Parse(strings.NewReader(dbMiekENTNL), testzone, "stdin", 0) + } +} + +func TestParseNoSOA(t *testing.T) { + _, err := Parse(strings.NewReader(dbNoSOA), "example.org.", "stdin", 0) + if err == nil { + t.Fatalf("Zone %q should have failed to load", "example.org.") + } + if !strings.Contains(err.Error(), "no SOA record") { + t.Fatalf("Zone %q should have failed to load with no soa error: %s", "example.org.", err) + } +} + +const dbNoSOA = ` +$TTL 1M +$ORIGIN example.org. + +www IN A 192.168.0.14 +mail IN A 192.168.0.15 +imap IN CNAME mail +` diff --git a/ag_201_coredns/plugin/file/fuzz.go b/ag_201_coredns/plugin/file/fuzz.go new file mode 100644 index 0000000..9c59ab8 --- /dev/null +++ b/ag_201_coredns/plugin/file/fuzz.go @@ -0,0 +1,50 @@ +//go:build gofuzz + +package file + +import ( + "strings" + + "github.com/coredns/coredns/plugin/pkg/fuzz" + "github.com/coredns/coredns/plugin/test" +) + +// Fuzz fuzzes file. +func Fuzz(data []byte) int { + name := "miek.nl." + zone, _ := Parse(strings.NewReader(fuzzMiekNL), name, "stdin", 0) + f := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{name: zone}, Names: []string{name}}} + + return fuzz.Do(f, data) +} + +const fuzzMiekNL = ` +$TTL 30M +$ORIGIN miek.nl. +@ IN SOA linode.atoom.net. miek.miek.nl. ( + 1282630057 ; Serial + 4H ; Refresh + 1H ; Retry + 7D ; Expire + 4H ) ; Negative Cache TTL + IN NS linode.atoom.net. + IN NS ns-ext.nlnetlabs.nl. + IN NS omval.tednet.nl. + IN NS ext.ns.whyscream.net. + + IN MX 1 aspmx.l.google.com. + IN MX 5 alt1.aspmx.l.google.com. + IN MX 5 alt2.aspmx.l.google.com. + IN MX 10 aspmx2.googlemail.com. + IN MX 10 aspmx3.googlemail.com. + + IN A 139.162.196.78 + IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 + +a IN A 139.162.196.78 + IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 +www IN CNAME a +archive IN CNAME a + +srv IN SRV 10 10 8080 a.miek.nl. +mx IN MX 10 a.miek.nl.` diff --git a/ag_201_coredns/plugin/file/glue_test.go b/ag_201_coredns/plugin/file/glue_test.go new file mode 100644 index 0000000..eeddc4e --- /dev/null +++ b/ag_201_coredns/plugin/file/glue_test.go @@ -0,0 +1,254 @@ +package file + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +// another personal zone (helps in testing as my secondary is NSD, atoom = atom in English. +var atoomTestCases = []test.Case{ + { + Qname: atoom, Qtype: dns.TypeNS, Do: true, + Answer: []dns.RR{ + test.NS("atoom.net. 1800 IN NS linode.atoom.net."), + test.NS("atoom.net. 1800 IN NS ns-ext.nlnetlabs.nl."), + test.NS("atoom.net. 1800 IN NS omval.tednet.nl."), + test.RRSIG("atoom.net. 1800 IN RRSIG NS 8 2 1800 20170112031301 20161213031301 53289 atoom.net. DLe+G1 jlw="), + }, + Extra: []dns.RR{ + // test.OPT(4096, true), // added by server, not test in this unit test. + test.A("linode.atoom.net. 1800 IN A 176.58.119.54"), + test.AAAA("linode.atoom.net. 1800 IN AAAA 2a01:7e00::f03c:91ff:fe79:234c"), + test.RRSIG("linode.atoom.net. 1800 IN RRSIG A 8 3 1800 20170112031301 20161213031301 53289 atoom.net. Z4Ka4OLDoyxj72CL vkI="), + test.RRSIG("linode.atoom.net. 1800 IN RRSIG AAAA 8 3 1800 20170112031301 20161213031301 53289 atoom.net. l+9Qc914zFH/okG2fzJ1q olQ="), + }, + }, +} + +func TestLookupGlue(t *testing.T) { + zone, err := Parse(strings.NewReader(dbAtoomNetSigned), atoom, "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{atoom: zone}, Names: []string{atoom}}} + ctx := context.TODO() + + for _, tc := range atoomTestCases { + 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 + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +const dbAtoomNetSigned = ` +; File written on Tue Dec 13 04:13:01 2016 +; dnssec_signzone version 9.10.3-P4-Debian +atoom.net. 1800 IN SOA linode.atoom.net. miek.miek.nl. ( + 1481602381 ; serial + 14400 ; refresh (4 hours) + 3600 ; retry (1 hour) + 604800 ; expire (1 week) + 14400 ; minimum (4 hours) + ) + 1800 RRSIG SOA 8 2 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + GZ30uFuGATKzwHXgpEwK70qjdXSAqmbB5d4z + e7WTibvJDPLa1ptZBI7Zuod2KMOkT1ocSvhL + U7makhdv0BQx+5RSaP25mAmPIzfU7/T7R+DJ + 5q1GLlDSvOprfyMUlwOgZKZinesSdUa9gRmu + 8E+XnPNJ/jcTrGzzaDjn1/irrM0= ) + 1800 NS omval.tednet.nl. + 1800 NS linode.atoom.net. + 1800 NS ns-ext.nlnetlabs.nl. + 1800 RRSIG NS 8 2 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + D8Sd9JpXIOxOrUF5Hi1ASutyQwP7JNu8XZxA + rse86A6L01O8H8sCNib2VEoJjHuZ/dDEogng + OgmfqeFy04cpSX19GAk3bkx8Lr6aEat3nqIC + XA/xsCCfXy0NKZpI05zntHPbbP5tF/NvpE7n + 0+oLtlHSPEg1ZnEgwNoLe+G1jlw= ) + 1800 A 176.58.119.54 + 1800 RRSIG A 8 2 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + mrjiUFNCqDgCW8TuhjzcMh0V841uC224QvwH + 0+OvYhcve9twbX3Y12PSFmz77Xz3Jg9WAj4I + qhh3iHUac4dzUXyC702DT62yMF/9CMUO0+Ee + b6wRtvPHr2Tt0i/xV/BTbArInIvurXJrvKvo + LsZHOfsg7dZs6Mvdpe/CgwRExpk= ) + 1800 AAAA 2a01:7e00::f03c:91ff:fe79:234c + 1800 RRSIG AAAA 8 2 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + EkMxX2vUaP4h0qbWlHaT4yNhm8MrPMZTn/3R + zNw+i3oF2cLMWKh6GCfuIX/x5ID706o8kfum + bxTYwuTe1LJ+GoZHWEiH8VCa1laTlh8l3qSi + PZKU8339rr5cCYluk6p9PbAuRkYYOEruNg42 + wPOx46dsAlvp2XpOaOeJtU64QGQ= ) + 14400 NSEC deb.atoom.net. A NS SOA AAAA RRSIG NSEC DNSKEY + 14400 RRSIG NSEC 8 2 14400 ( + 20170112031301 20161213031301 53289 atoom.net. + P7Stx7lqRKl8tbTAAaJ0W6UhgJwZz3cjpM8z + eplbhXEVohKtyJ9xgptKt1vreH6lkhzciar5 + EB9Nj0VOmcthiht/+As8aEKmf8UlcJ2EbLII + NT7NUaasxsrLE2rjjX5mEtzOZ1uQAGiU8Hnk + XdGweTgIVFuiCcMCgaKpC2TRrMw= ) + 1800 DNSKEY 256 3 8 ( + AwEAAeDZTH9YT9qLMPlq4VrxX7H3GbWcqCrC + tXc9RT/hf96GN+ttnnEQVaJY8Gbly3IZpYQW + MwaCi0t30UULXE3s9FUQtl4AMbplyiz9EF8L + /XoBS1yhGm5WV5u608ihoPaRkYNyVV3egb5Y + hA5EXWy2vfsa1XWPpxvSAhlqM0YENtP3 + ) ; ZSK; alg = RSASHA256; key id = 53289 + 1800 DNSKEY 257 3 8 ( + AwEAAepN7Vo8enDCruVduVlGxTDIv7QG0wJQ + fTL1hMy4k0Yf/7dXzrn5bZT4ytBvH1hoBImH + mtTrQo6DQlBBVXDJXTyQjQozaHpN1HhTJJTz + IXl8UrdbkLWvz6QSeJPmBBYQRAqylUA2KE29 + nxyiNboheDLiIWyQ7Q/Op7lYaKMdb555kQAs + b/XT4Tb3/3BhAjcofNofNBjDjPq2i8pAo8HU + 5mW5/Pl+ZT/S0aqQPnCkHk/iofSRu3ZdBzkH + 54eoC+BdyXb7gTbPGRr+1gMbf/rzhRiZ4vnX + NoEzGAXmorKzJHANNb6KQ/932V9UDHm9wbln + 6y3s7IBvsMX5KF8vo81Stkc= + ) ; KSK; alg = RSASHA256; key id = 19114 + 1800 RRSIG DNSKEY 8 2 1800 ( + 20170112031301 20161213031301 19114 atoom.net. + IEjViubKdef8RWB5bcnirqVcqDk16irkywJZ + sBjMyNs03/a+sl0UHEGAB7qCC+Rn+RDaM5It + WF+Gha6BwRIN9NuSg3BwB2h1nJtHw61pMVU9 + 2j9Q3pq7X1xoTBAcwY95t5a1xlw0iTCaLu1L + Iu/PbVp1gj1o8BF/PiYilvZJGUjaTgsi+YNi + 2kiWpp6afO78/W4nfVx+lQBmpyfX1lwL5PEC + 9f5PMbzRmOapvUBc2XdddGywLdmlNsLHimGV + t7kkHZHOWQR1TvvMbU3dsC0bFCrBVGDhEuxC + hATR+X5YV0AyDSyrew7fOGJKrapwMWS3yRLr + FAt0Vcxno5lwQImbCQ== ) + 1800 RRSIG DNSKEY 8 2 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + sSxdgPT+gFZPN0ot6lZRGqOwvONUEsg0uEbf + kh19JlWHu/qvq5HOOK2VOW/UnswpVmtpFk0W + z/jiCNHifjpCCVn5tfCMZDLGekmPOjdobw24 + swBuGjnn0NHvxHoN6S+mb+AR6V/dLjquNUda + yzBc2Ua+XtQ7SCLKIvEhcNg9H3o= ) +deb.atoom.net. 1800 IN A 176.58.119.54 + 1800 RRSIG A 8 3 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + ZW7jm/VDa/I9DxWlE7Cm+HHymiVv4Wk5UGYI + Uf/g0EfxLCBR6SwL5QKuV1z7xoWKaiNqqrmc + gg35xgskKyS8QHgCCODhDzcIKe+MSsBXbY04 + AtrC5dV3JJQoA65Ng/48hwcyghAjXKrA2Yyq + GXf2DSvWeIV9Jmk0CsOELP24dpk= ) + 1800 TXT "v=spf1 a ip6:2a01:7e00::f03c:91ff:fe79:234c ~all" + 1800 RRSIG TXT 8 3 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + fpvVJ+Z6tzSd9yETn/PhLSCRISwRD1c3ET80 + 8twnx3XfAPQfV2R8dw7pz8Vw4TSxvf19bAZc + PWRjW682gb7gAxoJshCXBYabMfqExrBc9V1S + ezwm3D93xNMyegxzHx2b/H8qp3ZWdsMLTvvN + Azu7P4iyO+WRWT0R7bJGrdTwRz8= ) + 1800 AAAA 2a01:7e00::f03c:91ff:fe79:234c + 1800 RRSIG AAAA 8 3 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + aaPF6NqXfWamzi+xUDVeYa7StJUVM1tDsL34 + w5uozFRZ0f4K/Z88Kk5CgztxmtpNNKGdLWa0 + iryUJsbVWAbSQfrZNkNckBtczMNxGgjqn97A + 2//F6ajH/qrR3dWcCm+VJMgu3UPqAxLiCaYO + GQUx6Y8JA1VIM/RJAM6BhgNxjD0= ) + 14400 NSEC lafhart.atoom.net. A TXT AAAA RRSIG NSEC + 14400 RRSIG NSEC 8 3 14400 ( + 20170112031301 20161213031301 53289 atoom.net. + 1Llad64NDWcz8CyBu2TsyANrJ9Tpfm5257sY + FPYF579p3c9Imwp9kYEO1zMEKgNoXBN/sQnd + YCugq3r2GAI6bfJj8sV5bt6GKuZcGHMESug4 + uh2gU0NDcCA4GPdBYGdusePwV0RNpcRnVCFA + fsACp+22j3uwRUbCh0re0ufbAs4= ) +lafhart.atoom.net. 1800 IN A 178.79.160.171 + 1800 RRSIG A 8 3 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + fruP6cvMVICXEV8NcheS73NWLCEKlO1FgW6B + 35D2GhtfYZe+M23V5YBRtlVCCrAdS0etdCOf + xH9yt3u2kVvDXuMRiQr1zJPRDEq3cScYumpd + bOO8cjHiCic5lEcRVWNNHXyGtpqTvrp9CxOu + IQw1WgAlZyKj43zGg3WZi6OTKLg= ) + 14400 NSEC linode.atoom.net. A RRSIG NSEC + 14400 RRSIG NSEC 8 3 14400 ( + 20170112031301 20161213031301 53289 atoom.net. + 2AUWXbScL0jIJ7G6UsJAlUs+bgSprZ1zY6v/ + iVB5BAYwZD6pPky7LZdzvPEHh0aNLGIFbbU8 + SDJI7u/e4RUTlE+8yyjl6obZNfNKyJFqE5xN + 1BJ8sjFrVn6KaHIDKEOZunNb1MlMfCRkLg9O + 94zg04XEgVUfaYCPxvLs3fCEgzw= ) +voordeur.atoom.net. 1800 IN A 77.249.87.46 + 1800 RRSIG A 8 3 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + SzJz0NaKLRA/lW4CxgMHgeuQLp5QqFEjQv3I + zfPtY4joQsZn8RN8RLECcpcPKjbC8Dj6mxIJ + dd2vwhsCVlZKMNcZUOfpB7eGx1TR9HnzMkY9 + OdTt30a9+tktagrJEoy31vAhj1hJqLbSgvOa + pRr1P4ZpQ53/qH8JX/LOmqfWTdg= ) + 14400 NSEC www.atoom.net. A RRSIG NSEC + 14400 RRSIG NSEC 8 3 14400 ( + 20170112031301 20161213031301 53289 atoom.net. + CETJhUJy1rKjVj9wsW1549gth+/Z37//BI6S + nxJ+2Oq63jEjlbznmyo5hvFW54DbVUod+cLo + N9PdlNQDr1XsRBgWhkKW37RkuoRVEPwqRykv + xzn9i7CgYKAAHFyWMGihBLkV9ByPp8GDR8Zr + DEkrG3ErDlBcwi3FqGZFsSOW2xg= ) +www.atoom.net. 1800 IN CNAME deb.atoom.net. + 1800 RRSIG CNAME 8 3 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + 1lhG6iTtbeesBCVOrA8a7+V2gogCuXzKgSi8 + 6K0Pzq2CwqTScdNcZvcDOIbLq45Am5p09PIj + lXnd2fw6WAxphwvRhmwCve3uTZMUt5STw7oi + 0rED7GMuFUSC/BX0XVly7NET3ECa1vaK6RhO + hDSsKPWFI7to4d1z6tQ9j9Kvm4Y= ) + 14400 NSEC atoom.net. CNAME RRSIG NSEC + 14400 RRSIG NSEC 8 3 14400 ( + 20170112031301 20161213031301 53289 atoom.net. + CC4yCYP1q75/gTmPz+mVM6Lam2foPP5oTccY + RtROuTkgbt8DtAoPe304vmNazWBlGidnWJeD + YyAAe3znIHP0CgrxjD/hRL9FUzMnVrvB3mnx + 4W13wP1rE97RqJxV1kk22Wl3uCkVGy7LCjb0 + JLFvzCe2fuMe7YcTzI+t1rioTP0= ) +linode.atoom.net. 1800 IN A 176.58.119.54 + 1800 RRSIG A 8 3 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + Z4Ka4OLDha4eQNWs3GtUd1Cumr48RUnH523I + nZzGXtpQNou70qsm5Jt8n/HmsZ4L5DoxomRz + rgZTGnrqj43+A16UUGfVEk6SfUUHOgxgspQW + zoaqk5/5mQO1ROsLKY8RqaRqzvbToHvqeZEh + VkTPVA02JK9UFlKqoyxj72CLvkI= ) + 1800 AAAA 2a01:7e00::f03c:91ff:fe79:234c + 1800 RRSIG AAAA 8 3 1800 ( + 20170112031301 20161213031301 53289 atoom.net. + l+9Qce/EQyKrTJVKLv7iatjuCO285ckd5Oie + P2LzWVsL4tW04oHzieKZwIuNBRE+px8g5qrT + LIK2TikCGL1xHAd7CT7gbCtDcZ7jHmSTmMTJ + 405nOV3G3xWelreLI5Fn5ck8noEsF64kiw1y + XfkyQn2B914zFH/okG2fzJ1qolQ= ) + 14400 NSEC voordeur.atoom.net. A AAAA RRSIG NSEC + 14400 RRSIG NSEC 8 3 14400 ( + 20170112031301 20161213031301 53289 atoom.net. + Owzmz7QrVL2Gw2njEsUVEknMl2amx1HG9X3K + tO+Ihyy4tApiUFxUjAu3P/30QdqbB85h7s// + ipwX/AmQJNoxTScR3nHt9qDqJ044DPmiuh0l + NuIjguyZRANApmKCTA6AoxXIUqToIIjfVzi/ + PxXE6T3YIPlK7Bxgv1lcCBJ1fmE= )` + +const atoom = "atoom.net." diff --git a/ag_201_coredns/plugin/file/include_test.go b/ag_201_coredns/plugin/file/include_test.go new file mode 100644 index 0000000..490f05a --- /dev/null +++ b/ag_201_coredns/plugin/file/include_test.go @@ -0,0 +1,31 @@ +package file + +import ( + "strings" + "testing" + + "github.com/coredns/coredns/plugin/test" +) + +// 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) + } + defer rm() + + zone := `$ORIGIN example.org. +@ IN SOA sns.dns.icann.org. noc.dns.icann.org. 2017042766 7200 3600 1209600 3600 +$INCLUDE ` + name + "\n" + + z, err := Parse(strings.NewReader(zone), "example.org.", "test", 0) + if err != nil { + t.Errorf("Unable to parse zone %q: %s", "example.org.", err) + } + + if _, ok := z.Search("foo.example.org."); !ok { + t.Errorf("Failed to find %q in parsed zone", "foo.example.org.") + } +} diff --git a/ag_201_coredns/plugin/file/log_test.go b/ag_201_coredns/plugin/file/log_test.go new file mode 100644 index 0000000..c9609ee --- /dev/null +++ b/ag_201_coredns/plugin/file/log_test.go @@ -0,0 +1,5 @@ +package file + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/file/lookup.go b/ag_201_coredns/plugin/file/lookup.go new file mode 100644 index 0000000..3f69299 --- /dev/null +++ b/ag_201_coredns/plugin/file/lookup.go @@ -0,0 +1,435 @@ +package file + +import ( + "context" + + "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" +) + +// Result is the result of a Lookup +type Result int + +const ( + // Success is a successful lookup. + Success Result = iota + // NameError indicates a nameerror + NameError + // Delegation indicates the lookup resulted in a delegation. + Delegation + // NoData indicates the lookup resulted in a NODATA. + NoData + // ServerFailure indicates a server failure during the lookup. + ServerFailure +) + +// Lookup looks up qname and qtype in the zone. When do is true DNSSEC records are included. +// Three sets of records are returned, one for the answer, one for authority and one for the additional section. +func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) ([]dns.RR, []dns.RR, []dns.RR, Result) { + qtype := state.QType() + do := state.Do() + + // If z is a secondary zone we might not have transferred it, meaning we have + // all zone context setup, except the actual record. This means (for one thing) the apex + // is empty and we don't have a SOA record. + z.RLock() + ap := z.Apex + tr := z.Tree + z.RUnlock() + if ap.SOA == nil { + return nil, nil, nil, ServerFailure + } + + if qname == z.origin { + switch qtype { + case dns.TypeSOA: + return ap.soa(do), ap.ns(do), nil, Success + case dns.TypeNS: + nsrrs := ap.ns(do) + glue := tr.Glue(nsrrs, do) // technically this isn't glue + return nsrrs, nil, glue, Success + } + } + + var ( + found, shot bool + parts string + i int + elem, wildElem *tree.Elem + ) + + loop, _ := ctx.Value(dnsserver.LoopKey{}).(int) + if loop > 8 { + // We're back here for the 9th time; we have a loop and need to bail out. + // Note the answer we're returning will be incomplete (more cnames to be followed) or + // illegal (wildcard cname with multiple identical records). For now it's more important + // to protect ourselves then to give the client a valid answer. We return with an error + // to let the server handle what to do. + return nil, nil, nil, ServerFailure + } + + // Lookup: + // * Per label from the right, look if it exists. We do this to find potential + // delegation records. + // * If the per-label search finds nothing, we will look for the wildcard at the + // level. If found we keep it around. If we don't find the complete name we will + // use the wildcard. + // + // Main for-loop handles delegation and finding or not finding the qname. + // If found we check if it is a CNAME/DNAME and do CNAME processing + // We also check if we have type and do a nodata response. + // + // If not found, we check the potential wildcard, and use that for further processing. + // If not found and no wildcard we will process this as an NXDOMAIN response. + for { + parts, shot = z.nameFromRight(qname, i) + // We overshot the name, break and check if we previously found something. + if shot { + break + } + + elem, found = tr.Search(parts) + if !found { + // Apex will always be found, when we are here we can search for a wildcard + // and save the result of that search. So when nothing match, but we have a + // wildcard we should expand the wildcard. + + wildcard := replaceWithAsteriskLabel(parts) + if wild, found := tr.Search(wildcard); found { + wildElem = wild + } + + // Keep on searching, because maybe we hit an empty-non-terminal (which aren't + // stored in the tree. Only when we have match the full qname (and possible wildcard + // we can be confident that we didn't find anything. + i++ + continue + } + + // If we see DNAME records, we should return those. + if dnamerrs := elem.Type(dns.TypeDNAME); dnamerrs != nil { + // 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 { + 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) + sigs = rrutil.SubTypeSignature(sigs, dns.TypeDNAME) + dnamerrs = append(dnamerrs, sigs...) + } + + // The relevant DNAME RR should be included in the answer section, + // if the DNAME is being employed as a substitution instruction. + answer = append(dnamerrs, answer...) + + return answer, ns, extra, rcode + } + // The domain name that owns a DNAME record is allowed to have other RR types + // at that domain name, except those have restrictions on what they can coexist + // with (e.g. another DNAME). So there is nothing special left here. + } + + // 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. + if qtype == dns.TypeDS && elem.Name() == qname { + i++ + continue + } + + glue := tr.Glue(nsrrs, do) + if do { + dss := typeFromElem(elem, dns.TypeDS, do) + nsrrs = append(nsrrs, dss...) + } + + return nil, nsrrs, glue, Delegation + } + + i++ + } + + // What does found and !shot mean - do we ever hit it? + if found && !shot { + return nil, nil, nil, ServerFailure + } + + // 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) + } + + rrs := elem.Type(qtype) + + // NODATA + if len(rrs) == 0 { + ret := ap.soa(do) + if do { + nsec := typeFromElem(elem, dns.TypeNSEC, do) + ret = append(ret, nsec...) + } + return nil, ret, nil, NoData + } + + // Additional section processing for MX, SRV. Check response and see if any of the names are in bailiwick - + // if so add IP addresses to the additional section. + additional := z.additionalProcessing(rrs, do) + + if do { + sigs := elem.Type(dns.TypeRRSIG) + sigs = rrutil.SubTypeSignature(sigs, qtype) + rrs = append(rrs, sigs...) + } + + return rrs, ap.ns(do), additional, Success + } + + // Haven't found the original name. + + // Found wildcard. + if wildElem != nil { + // 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 && qtype != dns.TypeCNAME { + ctx = context.WithValue(ctx, dnsserver.LoopKey{}, loop+1) + return z.externalLookup(ctx, state, wildElem, rrs) + } + + rrs := wildElem.TypeForWildcard(qtype, qname) + + // NODATA response. + if len(rrs) == 0 { + ret := ap.soa(do) + if do { + nsec := typeFromElem(wildElem, dns.TypeNSEC, do) + ret = append(ret, nsec...) + } + 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 { + nsec := typeFromElem(deny, dns.TypeNSEC, do) + auth = append(auth, nsec...) + } + + sigs := wildElem.TypeForWildcard(dns.TypeRRSIG, qname) + sigs = rrutil.SubTypeSignature(sigs, qtype) + rrs = append(rrs, sigs...) + } + return rrs, auth, nil, Success + } + + rcode := NameError + + // Hacky way to get around empty-non-terminals. If a longer name does exist, but this qname, does not, it + // must be an empty-non-terminal. If so, we do the proper NXDOMAIN handling, but set the rcode to be success. + if x, found := tr.Next(qname); found { + if dns.IsSubDomain(qname, x.Name()) { + rcode = Success + } + } + + ret := ap.soa(do) + if do { + deny, found := tr.Prev(qname) + if !found { + goto Out + } + nsec := typeFromElem(deny, dns.TypeNSEC, do) + ret = append(ret, nsec...) + + if rcode != NameError { + goto Out + } + + ce, found := z.ClosestEncloser(qname) + + // wildcard denial only for NXDOMAIN + if found { + // wildcard denial + wildcard := "*." + ce.Name() + if ss, found := tr.Prev(wildcard); found { + // Only add this nsec if it is different than the one already added + if ss.Name() != deny.Name() { + nsec := typeFromElem(ss, dns.TypeNSEC, do) + ret = append(ret, nsec...) + } + } + } + } +Out: + return nil, ret, nil, rcode +} + +// typeFromElem returns the type tp from e and adds signatures (if they exist) and do is true. +func typeFromElem(elem *tree.Elem, tp uint16, do bool) []dns.RR { + rrs := elem.Type(tp) + if do { + sigs := elem.Type(dns.TypeRRSIG) + sigs = rrutil.SubTypeSignature(sigs, tp) + rrs = append(rrs, sigs...) + } + return rrs +} + +func (a Apex) soa(do bool) []dns.RR { + if do { + ret := append([]dns.RR{a.SOA}, a.SIGSOA...) + return ret + } + return []dns.RR{a.SOA} +} + +func (a Apex) ns(do bool) []dns.RR { + if do { + ret := append(a.NS, a.SIGNS...) + return ret + } + return a.NS +} + +// 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() + + if do { + sigs := elem.Type(dns.TypeRRSIG) + sigs = rrutil.SubTypeSignature(sigs, dns.TypeCNAME) + rrs = append(rrs, sigs...) + } + + targetName := rrs[0].(*dns.CNAME).Target + elem, _ = z.Tree.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 + } + + i := 0 + +Redo: + cname := elem.Type(dns.TypeCNAME) + if len(cname) > 0 { + rrs = append(rrs, cname...) + + if do { + sigs := elem.Type(dns.TypeRRSIG) + sigs = rrutil.SubTypeSignature(sigs, dns.TypeCNAME) + rrs = append(rrs, sigs...) + } + targetName := cname[0].(*dns.CNAME).Target + elem, _ = z.Tree.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 + } + + i++ + if i > 8 { + return rrs, z.Apex.ns(do), nil, Success + } + + goto Redo + } + + targets := elem.Type(qtype) + if len(targets) > 0 { + rrs = append(rrs, targets...) + + if do { + sigs := elem.Type(dns.TypeRRSIG) + sigs = rrutil.SubTypeSignature(sigs, qtype) + rrs = append(rrs, sigs...) + } + } + + return rrs, z.Apex.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, ServerFailure + } + if m == nil { + return nil, Success + } + if m.Rcode == dns.RcodeNameError { + return m.Answer, NameError + } + if m.Rcode == dns.RcodeServerFailure { + return m.Answer, ServerFailure + } + if m.Rcode == dns.RcodeSuccess && len(m.Answer) == 0 { + return m.Answer, NoData + } + return m.Answer, Success +} + +// additionalProcessing checks the current answer section and retrieves A or AAAA records +// (and possible SIGs) to need to be put in the additional section. +func (z *Zone) additionalProcessing(answer []dns.RR, do bool) (extra []dns.RR) { + for _, rr := range answer { + name := "" + switch x := rr.(type) { + case *dns.SRV: + name = x.Target + case *dns.MX: + name = x.Mx + } + if len(name) == 0 || !dns.IsSubDomain(z.origin, name) { + continue + } + + elem, _ := z.Tree.Search(name) + if elem == nil { + continue + } + + sigs := elem.Type(dns.TypeRRSIG) + for _, addr := range []uint16{dns.TypeA, dns.TypeAAAA} { + if a := elem.Type(addr); a != nil { + extra = append(extra, a...) + if do { + sig := rrutil.SubTypeSignature(sigs, addr) + extra = append(extra, sig...) + } + } + } + } + + return extra +} diff --git a/ag_201_coredns/plugin/file/lookup_test.go b/ag_201_coredns/plugin/file/lookup_test.go new file mode 100644 index 0000000..79e5604 --- /dev/null +++ b/ag_201_coredns/plugin/file/lookup_test.go @@ -0,0 +1,284 @@ +package file + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +var dnsTestCases = []test.Case{ + { + Qname: "www.miek.nl.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("a.miek.nl. 1800 IN A 139.162.196.78"), + test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."), + }, + Ns: miekAuth, + }, + { + Qname: "www.miek.nl.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."), + }, + Ns: miekAuth, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeSOA, + Answer: []dns.RR{ + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + Ns: miekAuth, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.AAAA("miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + Ns: miekAuth, + }, + { + Qname: "mIeK.NL.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.AAAA("miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + Ns: miekAuth, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."), + test.MX("miek.nl. 1800 IN MX 10 aspmx2.googlemail.com."), + test.MX("miek.nl. 1800 IN MX 10 aspmx3.googlemail.com."), + test.MX("miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com."), + test.MX("miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com."), + }, + Ns: miekAuth, + }, + { + Qname: "a.miek.nl.", Qtype: dns.TypeSRV, + Ns: []dns.RR{ + test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, + { + Qname: "b.miek.nl.", Qtype: dns.TypeA, + 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: "srv.miek.nl.", Qtype: dns.TypeSRV, + Answer: []dns.RR{ + test.SRV("srv.miek.nl. 1800 IN SRV 10 10 8080 a.miek.nl."), + }, + Extra: []dns.RR{ + test.A("a.miek.nl. 1800 IN A 139.162.196.78"), + test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + Ns: miekAuth, + }, + { + Qname: "mx.miek.nl.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("mx.miek.nl. 1800 IN MX 10 a.miek.nl."), + }, + Extra: []dns.RR{ + test.A("a.miek.nl. 1800 IN A 139.162.196.78"), + test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + }, + 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 ( + testzone = "miek.nl." + testzone1 = "dnssex.nl." +) + +func TestLookup(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() + + for _, tc := range dnsTestCases { + 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 + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +func TestLookupNil(t *testing.T) { + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: nil}, Names: []string{testzone}}} + ctx := context.TODO() + + m := dnsTestCases[0].Msg() + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + 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 BenchmarkFileLookup(b *testing.B) { + zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0) + if err != nil { + return + } + + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}} + ctx := context.TODO() + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + tc := test.Case{ + Qname: "www.miek.nl.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."), + test.A("a.miek.nl. 1800 IN A 139.162.196.78"), + }, + } + + m := tc.Msg() + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + fm.ServeDNS(ctx, rec, m) + } +} + +const dbMiekNL = ` +$TTL 30M +$ORIGIN miek.nl. +@ IN SOA linode.atoom.net. miek.miek.nl. ( + 1282630057 ; Serial + 4H ; Refresh + 1H ; Retry + 7D ; Expire + 4H ) ; Negative Cache TTL + IN NS linode.atoom.net. + IN NS ns-ext.nlnetlabs.nl. + IN NS omval.tednet.nl. + IN NS ext.ns.whyscream.net. + + IN MX 1 aspmx.l.google.com. + IN MX 5 alt1.aspmx.l.google.com. + IN MX 5 alt2.aspmx.l.google.com. + IN MX 10 aspmx2.googlemail.com. + IN MX 10 aspmx3.googlemail.com. + + IN A 139.162.196.78 + IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 + +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. + +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/ag_201_coredns/plugin/file/notify.go b/ag_201_coredns/plugin/file/notify.go new file mode 100644 index 0000000..7d4e35c --- /dev/null +++ b/ag_201_coredns/plugin/file/notify.go @@ -0,0 +1,33 @@ +package file + +import ( + "net" + + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// isNotify checks if state is a notify message and if so, will *also* check if it +// is from one of the configured masters. If not it will not be a valid notify +// message. If the zone z is not a secondary zone the message will also be ignored. +func (z *Zone) isNotify(state request.Request) bool { + if state.Req.Opcode != dns.OpcodeNotify { + return false + } + if len(z.TransferFrom) == 0 { + return false + } + // If remote IP matches we accept. + remote := state.IP() + for _, f := range z.TransferFrom { + from, _, err := net.SplitHostPort(f) + if err != nil { + continue + } + if from == remote { + return true + } + } + return false +} diff --git a/ag_201_coredns/plugin/file/nsec3_test.go b/ag_201_coredns/plugin/file/nsec3_test.go new file mode 100644 index 0000000..ed9f74f --- /dev/null +++ b/ag_201_coredns/plugin/file/nsec3_test.go @@ -0,0 +1,28 @@ +package file + +import ( + "strings" + "testing" +) + +func TestParseNSEC3PARAM(t *testing.T) { + _, err := Parse(strings.NewReader(nsec3paramTest), "miek.nl", "stdin", 0) + if err == nil { + t.Fatalf("Expected error when reading zone, got nothing") + } +} + +func TestParseNSEC3(t *testing.T) { + _, err := Parse(strings.NewReader(nsec3Test), "miek.nl", "stdin", 0) + if err == nil { + t.Fatalf("Expected error when reading zone, got nothing") + } +} + +const nsec3paramTest = `miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1460175181 14400 3600 604800 14400 +miek.nl. 1800 IN NS omval.tednet.nl. +miek.nl. 0 IN NSEC3PARAM 1 0 5 A3DEBC9CC4F695C7` + +const nsec3Test = `example.org. 1800 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082508 7200 3600 1209600 3600 +aub8v9ce95ie18spjubsr058h41n7pa5.example.org. 284 IN NSEC3 1 1 5 D0CBEAAF0AC77314 AUB95P93VPKP55G6U5S4SGS7LS61ND85 NS SOA TXT RRSIG DNSKEY NSEC3PARAM +aub8v9ce95ie18spjubsr058h41n7pa5.example.org. 284 IN RRSIG NSEC3 8 2 600 20160910232502 20160827231002 14028 example.org. XBNpA7KAIjorPbXvTinOHrc1f630aHic2U716GHLHA4QMx9cl9ss4QjR Wj2UpDM9zBW/jNYb1xb0yjQoez/Jv200w0taSWjRci5aUnRpOi9bmcrz STHb6wIUjUsbJ+NstQsUwVkj6679UviF1FqNwr4GlJnWG3ZrhYhE+NI6 s0k=` diff --git a/ag_201_coredns/plugin/file/reload.go b/ag_201_coredns/plugin/file/reload.go new file mode 100644 index 0000000..cdb50f4 --- /dev/null +++ b/ag_201_coredns/plugin/file/reload.go @@ -0,0 +1,69 @@ +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.ReloadInterval is zero, no reloading will be done. +func (z *Zone) Reload(t *transfer.Transfer) error { + if z.ReloadInterval == 0 { + return nil + } + tick := time.NewTicker(z.ReloadInterval) + + go func() { + for { + select { + case <-tick.C: + zFile := z.File() + 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 + } + + serial := z.SOASerialIfDefined() + zone, err := Parse(reader, z.origin, zFile, serial) + reader.Close() + if err != nil { + if _, ok := err.(*serialErr); !ok { + log.Errorf("Parsing zone %q: %v", z.origin, err) + } + continue + } + + // copy elements we need + z.Lock() + z.Apex = zone.Apex + 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) + if t != nil { + if err := t.Notify(z.origin); err != nil { + log.Warningf("Failed sending notifies: %s", err) + } + } + + case <-z.reloadShutdown: + tick.Stop() + return + } + } + }() + return nil +} + +// SOASerialIfDefined returns the SOA's serial if the zone has a SOA record in the Apex, or -1 otherwise. +func (z *Zone) SOASerialIfDefined() int64 { + z.RLock() + defer z.RUnlock() + if z.Apex.SOA != nil { + return int64(z.Apex.SOA.Serial) + } + return -1 +} diff --git a/ag_201_coredns/plugin/file/reload_test.go b/ag_201_coredns/plugin/file/reload_test.go new file mode 100644 index 0000000..c404bc4 --- /dev/null +++ b/ag_201_coredns/plugin/file/reload_test.go @@ -0,0 +1,90 @@ +package file + +import ( + "context" + "os" + "strings" + "testing" + "time" + + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/plugin/transfer" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +func TestZoneReload(t *testing.T) { + fileName, rm, err := test.TempFile(".", reloadZoneTest) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + reader, err := os.Open(fileName) + if err != nil { + t.Fatalf("Failed to open zone: %s", err) + } + z, err := Parse(reader, "miek.nl", fileName, 0) + if err != nil { + t.Fatalf("Failed to parse zone: %s", err) + } + + z.ReloadInterval = 10 * time.Millisecond + z.Reload(&transfer.Transfer{}) + time.Sleep(20 * time.Millisecond) + + ctx := context.TODO() + r := new(dns.Msg) + r.SetQuestion("miek.nl", dns.TypeSOA) + state := request.Request{W: &test.ResponseWriter{}, Req: r} + if _, _, _, res := z.Lookup(ctx, state, "miek.nl."); res != Success { + t.Fatalf("Failed to lookup, got %d", res) + } + + r = new(dns.Msg) + r.SetQuestion("miek.nl", dns.TypeNS) + state = request.Request{W: &test.ResponseWriter{}, Req: r} + if _, _, _, res := z.Lookup(ctx, state, "miek.nl."); res != Success { + t.Fatalf("Failed to lookup, got %d", res) + } + + rrs, err := z.ApexIfDefined() // all apex records. + if err != nil { + t.Fatal(err) + } + if len(rrs) != 5 { + t.Fatalf("Expected 5 RRs, got %d", len(rrs)) + } + 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 + time.Sleep(30 * time.Millisecond) + + rrs, err = z.ApexIfDefined() + if err != nil { + t.Fatal(err) + } + if len(rrs) != 3 { + t.Fatalf("Expected 3 RRs, got %d", len(rrs)) + } +} + +func TestZoneReloadSOAChange(t *testing.T) { + _, err := Parse(strings.NewReader(reloadZoneTest), "miek.nl.", "stdin", 1460175181) + if err == nil { + t.Fatalf("Zone should not have been re-parsed") + } +} + +const reloadZoneTest = `miek.nl. 1627 IN SOA linode.atoom.net. miek.miek.nl. 1460175181 14400 3600 604800 14400 +miek.nl. 1627 IN NS ext.ns.whyscream.net. +miek.nl. 1627 IN NS omval.tednet.nl. +miek.nl. 1627 IN NS linode.atoom.net. +miek.nl. 1627 IN NS ns-ext.nlnetlabs.nl. +` + +const reloadZone2Test = `miek.nl. 1627 IN SOA linode.atoom.net. miek.miek.nl. 1460175182 14400 3600 604800 14400 +miek.nl. 1627 IN NS ext.ns.whyscream.net. +miek.nl. 1627 IN NS omval.tednet.nl. +` diff --git a/ag_201_coredns/plugin/file/rrutil/util.go b/ag_201_coredns/plugin/file/rrutil/util.go new file mode 100644 index 0000000..564b82c --- /dev/null +++ b/ag_201_coredns/plugin/file/rrutil/util.go @@ -0,0 +1,18 @@ +// Package rrutil provides function to find certain RRs in slices. +package rrutil + +import "github.com/miekg/dns" + +// SubTypeSignature returns the RRSIG for the subtype. +func SubTypeSignature(rrs []dns.RR, subtype uint16) []dns.RR { + sigs := []dns.RR{} + // there may be multiple keys that have signed this subtype + for _, sig := range rrs { + if s, ok := sig.(*dns.RRSIG); ok { + if s.TypeCovered == subtype { + sigs = append(sigs, s) + } + } + } + return sigs +} diff --git a/ag_201_coredns/plugin/file/secondary.go b/ag_201_coredns/plugin/file/secondary.go new file mode 100644 index 0000000..932916b --- /dev/null +++ b/ag_201_coredns/plugin/file/secondary.go @@ -0,0 +1,198 @@ +package file + +import ( + "math/rand" + "time" + + "github.com/miekg/dns" +) + +// TransferIn retrieves the zone from the masters, parses it and sets it live. +func (z *Zone) TransferIn() error { + if len(z.TransferFrom) == 0 { + return nil + } + m := new(dns.Msg) + m.SetAxfr(z.origin) + + z1 := z.CopyWithoutApex() + var ( + Err error + tr string + ) + +Transfer: + for _, tr = range z.TransferFrom { + t := new(dns.Transfer) + c, err := t.In(m, tr) + if err != nil { + log.Errorf("Failed to setup transfer `%s' with `%q': %v", z.origin, tr, err) + Err = err + continue Transfer + } + for env := range c { + if env.Error != nil { + log.Errorf("Failed to transfer `%s' from %q: %v", z.origin, tr, env.Error) + Err = env.Error + continue Transfer + } + for _, rr := range env.RR { + if err := z1.Insert(rr); err != nil { + log.Errorf("Failed to parse transfer `%s' from: %q: %v", z.origin, tr, err) + Err = err + continue Transfer + } + } + } + Err = nil + break + } + if Err != nil { + return Err + } + + z.Lock() + z.Tree = z1.Tree + z.Apex = z1.Apex + z.Expired = false + z.Unlock() + log.Infof("Transferred: %s from %s", z.origin, tr) + return nil +} + +// shouldTransfer checks the primaries of zone, retrieves the SOA record, checks the current serial +// and the remote serial and will return true if the remote one is higher than the locally configured one. +func (z *Zone) shouldTransfer() (bool, error) { + c := new(dns.Client) + c.Net = "tcp" // do this query over TCP to minimize spoofing + m := new(dns.Msg) + m.SetQuestion(z.origin, dns.TypeSOA) + + var Err error + serial := -1 + +Transfer: + for _, tr := range z.TransferFrom { + Err = nil + ret, _, err := c.Exchange(m, tr) + if err != nil || ret.Rcode != dns.RcodeSuccess { + Err = err + continue + } + for _, a := range ret.Answer { + if a.Header().Rrtype == dns.TypeSOA { + serial = int(a.(*dns.SOA).Serial) + break Transfer + } + } + } + if serial == -1 { + return false, Err + } + if z.Apex.SOA == nil { + return true, Err + } + return less(z.Apex.SOA.Serial, uint32(serial)), Err +} + +// less returns true of a is smaller than b when taking RFC 1982 serial arithmetic into account. +func less(a, b uint32) bool { + if a < b { + return (b - a) <= MaxSerialIncrement + } + return (a - b) > MaxSerialIncrement +} + +// Update updates the secondary zone according to its SOA. It will run for the life time of the server +// and uses the SOA parameters. Every refresh it will check for a new SOA number. If that fails (for all +// server) it will retry every retry interval. If the zone failed to transfer before the expire, the zone +// 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 { + 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) + + refreshTicker := time.NewTicker(refresh) + retryTicker := time.NewTicker(retry) + expireTicker := time.NewTicker(expire) + + for { + select { + case <-expireTicker.C: + if !retryActive { + break + } + z.Expired = true + + case <-retryTicker.C: + if !retryActive { + break + } + + time.Sleep(jitter(2000)) // 2s randomize + + ok, err := z.shouldTransfer() + if err != nil { + log.Warningf("Failed retry check %s", err) + continue + } + + if ok { + if err := z.TransferIn(); err != nil { + // transfer failed, leave retryActive true + break + } + } + + // no errors, stop timers and restart + retryActive = false + refreshTicker.Stop() + retryTicker.Stop() + expireTicker.Stop() + goto Restart + + case <-refreshTicker.C: + + time.Sleep(jitter(5000)) // 5s randomize + + ok, err := z.shouldTransfer() + if err != nil { + log.Warningf("Failed refresh check %s", err) + retryActive = true + continue + } + + if ok { + if err := z.TransferIn(); err != nil { + // transfer failed + retryActive = true + break + } + } + + // no errors, stop timers and restart + retryActive = false + refreshTicker.Stop() + retryTicker.Stop() + expireTicker.Stop() + goto Restart + } + } +} + +// jitter returns a random duration between [0,n) * time.Millisecond +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 +// two serials is greater than this number, the smaller one is considered greater. +const MaxSerialIncrement uint32 = 2147483647 diff --git a/ag_201_coredns/plugin/file/secondary_test.go b/ag_201_coredns/plugin/file/secondary_test.go new file mode 100644 index 0000000..67d151e --- /dev/null +++ b/ag_201_coredns/plugin/file/secondary_test.go @@ -0,0 +1,146 @@ +package file + +import ( + "fmt" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +func TestLess(t *testing.T) { + const ( + min = 0 + max = 4294967295 + low = 12345 + high = 4000000000 + ) + + if less(min, max) { + t.Fatalf("Less: should be false") + } + if !less(max, min) { + t.Fatalf("Less: should be true") + } + if !less(high, low) { + t.Fatalf("Less: should be true") + } + if !less(7, 9) { + t.Fatalf("Less; should be true") + } +} + +type soa struct { + serial uint32 +} + +func (s *soa) Handler(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetReply(req) + switch req.Question[0].Qtype { + case dns.TypeSOA: + m.Answer = make([]dns.RR, 1) + m.Answer[0] = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, s.serial)) + w.WriteMsg(m) + case dns.TypeAXFR: + m.Answer = make([]dns.RR, 4) + m.Answer[0] = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, s.serial)) + m.Answer[1] = test.A(fmt.Sprintf("%s IN A 127.0.0.1", testZone)) + m.Answer[2] = test.A(fmt.Sprintf("%s IN A 127.0.0.1", testZone)) + m.Answer[3] = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, s.serial)) + w.WriteMsg(m) + } +} + +func (s *soa) TransferHandler(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetReply(req) + m.Answer = make([]dns.RR, 1) + m.Answer[0] = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, s.serial)) + w.WriteMsg(m) +} + +const testZone = "secondary.miek.nl." + +func TestShouldTransfer(t *testing.T) { + soa := soa{250} + + s := dnstest.NewServer(soa.Handler) + defer s.Close() + + z := NewZone("testzone", "test") + z.origin = testZone + z.TransferFrom = []string{s.Addr} + + // when we have a nil SOA (initial state) + should, err := z.shouldTransfer() + if err != nil { + t.Fatalf("Unable to run shouldTransfer: %v", err) + } + if !should { + 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)) + should, err = z.shouldTransfer() + if err != nil { + t.Fatalf("Unable to run shouldTransfer: %v", err) + } + if !should { + 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)) + should, err = z.shouldTransfer() + if err != nil { + t.Fatalf("Unable to run shouldTransfer: %v", err) + } + if should { + t.Fatalf("ShouldTransfer should return false for serial: %d", soa.serial) + } +} + +func TestTransferIn(t *testing.T) { + soa := soa{250} + + s := dnstest.NewServer(soa.Handler) + defer s.Close() + + z := new(Zone) + z.origin = testZone + z.TransferFrom = []string{s.Addr} + + 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) { + t.Fatalf("Unknown SOA transferred") + } +} + +func TestIsNotify(t *testing.T) { + z := new(Zone) + z.origin = testZone + state := newRequest(testZone, dns.TypeSOA) + // need to set opcode + state.Req.Opcode = dns.OpcodeNotify + + z.TransferFrom = []string{"10.240.0.1:53"} // IP from testing/responseWriter + if !z.isNotify(state) { + t.Fatal("Should have been valid notify") + } + z.TransferFrom = []string{"10.240.0.2:53"} + if z.isNotify(state) { + t.Fatal("Should have been invalid notify") + } +} + +func newRequest(zone string, qtype uint16) request.Request { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + m.SetEdns0(4097, true) + return request.Request{W: &test.ResponseWriter{}, Req: m} +} diff --git a/ag_201_coredns/plugin/file/setup.go b/ag_201_coredns/plugin/file/setup.go new file mode 100644 index 0000000..95e5d72 --- /dev/null +++ b/ag_201_coredns/plugin/file/setup.go @@ -0,0 +1,144 @@ +package file + +import ( + "errors" + "os" + "path/filepath" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/upstream" + "github.com/coredns/coredns/plugin/transfer" +) + +func init() { plugin.Register("file", setup) } + +func setup(c *caddy.Controller) error { + zones, err := fileParse(c) + if err != nil { + return plugin.Error("file", err) + } + + f := File{Zones: zones} + // 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") + if t == nil { + return nil + } + f.transfer = t.(*transfer.Transfer) // if found this must be OK. + go func() { + for _, n := range zones.Names { + f.transfer.Notify(n) + } + }() + return nil + }) + + c.OnRestartFailed(func() error { + t := dnsserver.GetConfig(c).Handler("transfer") + if t == nil { + return nil + } + go func() { + for _, n := range zones.Names { + f.transfer.Notify(n) + } + }() + return nil + }) + + for _, n := range zones.Names { + z := zones.Z[n] + c.OnShutdown(z.OnShutdown) + c.OnStartup(func() error { + z.StartupOnce.Do(func() { z.Reload(f.transfer) }) + return nil + }) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + f.Next = next + return f + }) + + return nil +} + +func fileParse(c *caddy.Controller) (Zones, error) { + z := make(map[string]*Zone) + names := []string{} + + config := dnsserver.GetConfig(c) + + var openErr error + reload := 1 * time.Minute + + for c.Next() { + // file db.file [zones...] + if !c.NextArg() { + return Zones{}, c.ArgErr() + } + fileName := c.Val() + + origins := plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) + if !filepath.IsAbs(fileName) && config.Root != "" { + fileName = filepath.Join(config.Root, fileName) + } + + reader, err := os.Open(filepath.Clean(fileName)) + if err != nil { + openErr = err + } + + 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 Zones{}, err + } + z[origins[i]] = zone + } + names = append(names, origins[i]) + } + + for c.NextBlock() { + switch c.Val() { + case "reload": + t := c.RemainingArgs() + if len(t) < 1 { + return Zones{}, errors.New("reload duration value is expected") + } + d, err := time.ParseDuration(t[0]) + if err != nil { + return Zones{}, plugin.Error("file", err) + } + reload = d + case "upstream": + // remove soon + c.RemainingArgs() + + default: + return Zones{}, c.Errf("unknown property '%s'", c.Val()) + } + } + + 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) + } + log.Warningf("Failed to open %q: trying again in %s", openErr, reload) + } + return Zones{Z: z, Names: names}, nil +} diff --git a/ag_201_coredns/plugin/file/setup_test.go b/ag_201_coredns/plugin/file/setup_test.go new file mode 100644 index 0000000..1d3b8dc --- /dev/null +++ b/ag_201_coredns/plugin/file/setup_test.go @@ -0,0 +1,124 @@ +package file + +import ( + "testing" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/test" +) + +func TestFileParse(t *testing.T) { + zoneFileName1, rm, err := test.TempFile(".", dbMiekNL) + if err != nil { + t.Fatal(err) + } + defer rm() + + zoneFileName2, rm, err := test.TempFile(".", dbDnssexNLSigned) + if err != nil { + t.Fatal(err) + } + defer rm() + + tests := []struct { + inputFileRules string + shouldErr bool + expectedZones Zones + }{ + { + `file ` + zoneFileName1 + ` miek.nl.`, + false, + Zones{Names: []string{"miek.nl."}}, + }, + { + `file ` + zoneFileName2 + ` dnssex.nl.`, + false, + Zones{Names: []string{"dnssex.nl."}}, + }, + { + `file ` + zoneFileName2 + ` 10.0.0.0/8`, + false, + Zones{Names: []string{"10.in-addr.arpa."}}, + }, + // errors. + { + `file ` + zoneFileName1 + ` miek.nl { + transfer from 127.0.0.1 + }`, + true, + Zones{}, + }, + { + `file`, + true, + Zones{}, + }, + { + `file ` + zoneFileName1 + ` example.net. { + no_reload + }`, + true, + Zones{}, + }, + { + `file ` + zoneFileName1 + ` example.net. { + no_rebloat + }`, + true, + Zones{}, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputFileRules) + actualZones, err := fileParse(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 len(actualZones.Names) != len(test.expectedZones.Names) { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedZones.Names, actualZones.Names) + } + for j, name := range test.expectedZones.Names { + if actualZones.Names[j] != name { + t.Fatalf("Test %d expected %v for %d th zone, got %v", i, name, j, actualZones.Names[j]) + } + } + } + } +} + +func TestParseReload(t *testing.T) { + name, rm, err := test.TempFile(".", dbMiekNL) + if err != nil { + t.Fatal(err) + } + defer rm() + + tests := []struct { + input string + reload time.Duration + }{ + { + `file ` + name + ` example.org.`, + 1 * time.Minute, + }, + { + `file ` + name + ` example.org. { + reload 5s + }`, + 5 * time.Second, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + 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/ag_201_coredns/plugin/file/shutdown.go b/ag_201_coredns/plugin/file/shutdown.go new file mode 100644 index 0000000..9aa5989 --- /dev/null +++ b/ag_201_coredns/plugin/file/shutdown.go @@ -0,0 +1,9 @@ +package file + +// OnShutdown shuts down any running go-routines for this zone. +func (z *Zone) OnShutdown() error { + if 0 < z.ReloadInterval { + z.reloadShutdown <- true + } + return nil +} diff --git a/ag_201_coredns/plugin/file/tree/all.go b/ag_201_coredns/plugin/file/tree/all.go new file mode 100644 index 0000000..e1fc5b3 --- /dev/null +++ b/ag_201_coredns/plugin/file/tree/all.go @@ -0,0 +1,21 @@ +package tree + +// All traverses tree and returns all elements. +func (t *Tree) All() []*Elem { + if t.Root == nil { + return nil + } + found := t.Root.all(nil) + return found +} + +func (n *Node) all(found []*Elem) []*Elem { + if n.Left != nil { + found = n.Left.all(found) + } + found = append(found, n.Elem) + if n.Right != nil { + found = n.Right.all(found) + } + return found +} diff --git a/ag_201_coredns/plugin/file/tree/auth_walk.go b/ag_201_coredns/plugin/file/tree/auth_walk.go new file mode 100644 index 0000000..1f43671 --- /dev/null +++ b/ag_201_coredns/plugin/file/tree/auth_walk.go @@ -0,0 +1,58 @@ +package tree + +import ( + "github.com/miekg/dns" +) + +// AuthWalk performs fn on all authoritative values stored in the tree in +// pre-order depth first. If a non-nil error is returned the AuthWalk was interrupted +// by an fn returning that error. If fn alters stored values' sort +// relationships, future tree operation behaviors are undefined. +// +// The fn function will be called with 3 arguments, the current element, a map containing all +// the RRs for this element and a boolean if this name is considered authoritative. +func (t *Tree) AuthWalk(fn func(*Elem, map[uint16][]dns.RR, bool) error) error { + if t.Root == nil { + return nil + } + return t.Root.authwalk(make(map[string]struct{}), fn) +} + +func (n *Node) authwalk(ns map[string]struct{}, fn func(*Elem, map[uint16][]dns.RR, bool) error) error { + if n.Left != nil { + if err := n.Left.authwalk(ns, fn); err != nil { + return err + } + } + + // Check if the current name is a subdomain of *any* of the delegated names we've seen, if so, skip this name. + // The ordering of the tree and how we walk if guarantees we see parents first. + if n.Elem.Type(dns.TypeNS) != nil { + ns[n.Elem.Name()] = struct{}{} + } + + auth := true + i := 0 + for { + j, end := dns.NextLabel(n.Elem.Name(), i) + if end { + break + } + if _, ok := ns[n.Elem.Name()[j:]]; ok { + auth = false + break + } + i++ + } + + if err := fn(n.Elem, n.Elem.m, auth); err != nil { + return err + } + + if n.Right != nil { + if err := n.Right.authwalk(ns, fn); err != nil { + return err + } + } + return nil +} diff --git a/ag_201_coredns/plugin/file/tree/elem.go b/ag_201_coredns/plugin/file/tree/elem.go new file mode 100644 index 0000000..c190964 --- /dev/null +++ b/ag_201_coredns/plugin/file/tree/elem.go @@ -0,0 +1,101 @@ +package tree + +import "github.com/miekg/dns" + +// Elem is an element in the tree. +type Elem struct { + m map[uint16][]dns.RR + name string // owner name +} + +// newElem returns a new elem. +func newElem(rr dns.RR) *Elem { + e := Elem{m: make(map[uint16][]dns.RR)} + e.m[rr.Header().Rrtype] = []dns.RR{rr} + return &e +} + +// Types returns the types of the records in e. The returned list is not sorted. +func (e *Elem) Types() []uint16 { + t := make([]uint16, len(e.m)) + i := 0 + for ty := range e.m { + t[i] = ty + i++ + } + return t +} + +// Type returns the RRs with type qtype from e. +func (e *Elem) Type(qtype uint16) []dns.RR { return e.m[qtype] } + +// TypeForWildcard returns the RRs with type qtype from e. The ownername returned is set to qname. +func (e *Elem) TypeForWildcard(qtype uint16, qname string) []dns.RR { + rrs := e.m[qtype] + + if rrs == nil { + return nil + } + + copied := make([]dns.RR, len(rrs)) + for i := range rrs { + copied[i] = dns.Copy(rrs[i]) + copied[i].Header().Name = qname + } + return copied +} + +// All returns all RRs from e, regardless of type. +func (e *Elem) All() []dns.RR { + list := []dns.RR{} + for _, rrs := range e.m { + list = append(list, rrs...) + } + return list +} + +// Name returns the name for this node. +func (e *Elem) Name() string { + if e.name != "" { + return e.name + } + for _, rrs := range e.m { + e.name = rrs[0].Header().Name + return e.name + } + return "" +} + +// Empty returns true is e does not contain any RRs, i.e. is an empty-non-terminal. +func (e *Elem) Empty() bool { return len(e.m) == 0 } + +// Insert inserts rr into e. If rr is equal to existing RRs, the RR will be added anyway. +func (e *Elem) Insert(rr dns.RR) { + t := rr.Header().Rrtype + if e.m == nil { + e.m = make(map[uint16][]dns.RR) + e.m[t] = []dns.RR{rr} + return + } + rrs, ok := e.m[t] + if !ok { + e.m[t] = []dns.RR{rr} + return + } + + rrs = append(rrs, rr) + e.m[t] = rrs +} + +// Delete removes all RRs of type rr.Header().Rrtype from e. +func (e *Elem) Delete(rr dns.RR) { + if e.m == nil { + return + } + + t := rr.Header().Rrtype + delete(e.m, t) +} + +// Less is a tree helper function that calls less. +func Less(a *Elem, name string) int { return less(name, a.Name()) } diff --git a/ag_201_coredns/plugin/file/tree/glue.go b/ag_201_coredns/plugin/file/tree/glue.go new file mode 100644 index 0000000..937ae54 --- /dev/null +++ b/ag_201_coredns/plugin/file/tree/glue.go @@ -0,0 +1,44 @@ +package tree + +import ( + "github.com/coredns/coredns/plugin/file/rrutil" + + "github.com/miekg/dns" +) + +// Glue returns any potential glue records for nsrrs. +func (t *Tree) Glue(nsrrs []dns.RR, do bool) []dns.RR { + glue := []dns.RR{} + for _, rr := range nsrrs { + if ns, ok := rr.(*dns.NS); ok && dns.IsSubDomain(ns.Header().Name, ns.Ns) { + glue = append(glue, t.searchGlue(ns.Ns, do)...) + } + } + return glue +} + +// searchGlue looks up A and AAAA for name. +func (t *Tree) searchGlue(name string, do bool) []dns.RR { + glue := []dns.RR{} + + // A + if elem, found := t.Search(name); found { + glue = append(glue, elem.Type(dns.TypeA)...) + if do { + sigs := elem.Type(dns.TypeRRSIG) + sigs = rrutil.SubTypeSignature(sigs, dns.TypeA) + glue = append(glue, sigs...) + } + } + + // AAAA + if elem, found := t.Search(name); found { + glue = append(glue, elem.Type(dns.TypeAAAA)...) + if do { + sigs := elem.Type(dns.TypeRRSIG) + sigs = rrutil.SubTypeSignature(sigs, dns.TypeAAAA) + glue = append(glue, sigs...) + } + } + return glue +} diff --git a/ag_201_coredns/plugin/file/tree/less.go b/ag_201_coredns/plugin/file/tree/less.go new file mode 100644 index 0000000..7421cf0 --- /dev/null +++ b/ag_201_coredns/plugin/file/tree/less.go @@ -0,0 +1,59 @@ +package tree + +import ( + "bytes" + + "github.com/miekg/dns" +) + +// less returns <0 when a is less than b, 0 when they are equal and +// >0 when a is larger than b. +// The function orders names in DNSSEC canonical order: RFC 4034s section-6.1 +// +// See https://bert-hubert.blogspot.co.uk/2015/10/how-to-do-fast-canonical-ordering-of.html +// for a blog article on this implementation, although here we still go label by label. +// +// The values of a and b are *not* lowercased before the comparison! +func less(a, b string) int { + i := 1 + aj := len(a) + bj := len(b) + for { + ai, oka := dns.PrevLabel(a, i) + bi, okb := dns.PrevLabel(b, i) + if oka && okb { + return 0 + } + + // 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]) + doDDD(ab) + doDDD(bb) + + res := bytes.Compare(ab, bb) + if res != 0 { + return res + } + + i++ + aj, bj = ai, bi + } +} + +func doDDD(b []byte) { + lb := len(b) + for i := 0; i < lb; i++ { + if i+3 < lb && b[i] == '\\' && isDigit(b[i+1]) && isDigit(b[i+2]) && isDigit(b[i+3]) { + b[i] = dddToByte(b[i:]) + for j := i + 1; j < lb-3; j++ { + b[j] = b[j+3] + } + lb -= 3 + } + } +} + +func isDigit(b byte) bool { return b >= '0' && b <= '9' } +func dddToByte(s []byte) byte { return (s[1]-'0')*100 + (s[2]-'0')*10 + (s[3] - '0') } diff --git a/ag_201_coredns/plugin/file/tree/less_test.go b/ag_201_coredns/plugin/file/tree/less_test.go new file mode 100644 index 0000000..f2559de --- /dev/null +++ b/ag_201_coredns/plugin/file/tree/less_test.go @@ -0,0 +1,80 @@ +package tree + +import ( + "sort" + "strings" + "testing" +) + +type set []string + +func (p set) Len() int { return len(p) } +func (p set) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p set) Less(i, j int) bool { d := less(p[i], p[j]); return d <= 0 } + +func TestLess(t *testing.T) { + tests := []struct { + in []string + out []string + }{ + { + []string{"aaa.powerdns.de", "bbb.powerdns.net.", "xxx.powerdns.com."}, + []string{"xxx.powerdns.com.", "aaa.powerdns.de", "bbb.powerdns.net."}, + }, + { + []string{"aaa.POWERDNS.de", "bbb.PoweRdnS.net.", "xxx.powerdns.com."}, + []string{"xxx.powerdns.com.", "aaa.POWERDNS.de", "bbb.PoweRdnS.net."}, + }, + { + []string{"aaa.aaaa.aa.", "aa.aaa.a.", "bbb.bbbb.bb."}, + []string{"aa.aaa.a.", "aaa.aaaa.aa.", "bbb.bbbb.bb."}, + }, + { + []string{"aaaaa.", "aaa.", "bbb."}, + []string{"aaa.", "aaaaa.", "bbb."}, + }, + { + []string{"a.a.a.a.", "a.a.", "a.a.a."}, + []string{"a.a.", "a.a.a.", "a.a.a.a."}, + }, + { + []string{"example.", "z.example.", "a.example."}, + []string{"example.", "a.example.", "z.example."}, + }, + { + []string{"a.example.", "Z.a.example.", "z.example.", "yljkjljk.a.example.", "\\001.z.example.", "example.", "*.z.example.", "\\200.z.example.", "zABC.a.EXAMPLE."}, + []string{"example.", "a.example.", "yljkjljk.a.example.", "Z.a.example.", "zABC.a.EXAMPLE.", "z.example.", "\\001.z.example.", "*.z.example.", "\\200.z.example."}, + }, + { + // RFC3034 example. + []string{"a.example.", "Z.a.example.", "z.example.", "yljkjljk.a.example.", "example.", "*.z.example.", "zABC.a.EXAMPLE."}, + []string{"example.", "a.example.", "yljkjljk.a.example.", "Z.a.example.", "zABC.a.EXAMPLE.", "z.example.", "*.z.example."}, + }, + } + +Tests: + for j, test := range tests { + // Need to lowercase these example as the Less function does lowercase for us anymore. + for i, b := range test.in { + test.in[i] = strings.ToLower(b) + } + for i, b := range test.out { + test.out[i] = strings.ToLower(b) + } + + sort.Sort(set(test.in)) + for i := 0; i < len(test.in); i++ { + if test.in[i] != test.out[i] { + t.Errorf("Test %d: expected %s, got %s", j, test.out[i], test.in[i]) + n := "" + for k, in := range test.in { + if k+1 == len(test.in) { + n = "\n" + } + t.Logf("%s <-> %s\n%s", in, test.out[k], n) + } + continue Tests + } + } + } +} diff --git a/ag_201_coredns/plugin/file/tree/print.go b/ag_201_coredns/plugin/file/tree/print.go new file mode 100644 index 0000000..b2df70e --- /dev/null +++ b/ag_201_coredns/plugin/file/tree/print.go @@ -0,0 +1,62 @@ +package tree + +import "fmt" + +// Print prints a Tree. Main use is to aid in debugging. +func (t *Tree) Print() { + if t.Root == nil { + fmt.Println("") + } + t.Root.print() +} + +func (n *Node) print() { + q := newQueue() + q.push(n) + + nodesInCurrentLevel := 1 + nodesInNextLevel := 0 + + for !q.empty() { + do := q.pop() + nodesInCurrentLevel-- + + if do != nil { + fmt.Print(do.Elem.Name(), " ") + q.push(do.Left) + q.push(do.Right) + nodesInNextLevel += 2 + } + if nodesInCurrentLevel == 0 { + fmt.Println() + nodesInCurrentLevel = nodesInNextLevel + nodesInNextLevel = 0 + } + } + fmt.Println() +} + +type queue []*Node + +// newQueue returns a new queue. +func newQueue() queue { + q := queue([]*Node{}) + return q +} + +// push pushes n to the end of the queue. +func (q *queue) push(n *Node) { + *q = append(*q, n) +} + +// pop pops the first element off the queue. +func (q *queue) pop() *Node { + n := (*q)[0] + *q = (*q)[1:] + return n +} + +// empty returns true when the queue contains zero nodes. +func (q *queue) empty() bool { + return len(*q) == 0 +} diff --git a/ag_201_coredns/plugin/file/tree/print_test.go b/ag_201_coredns/plugin/file/tree/print_test.go new file mode 100644 index 0000000..2ab5527 --- /dev/null +++ b/ag_201_coredns/plugin/file/tree/print_test.go @@ -0,0 +1,102 @@ +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.Create("tmp") + if err != nil { + t.Error(err) + } + //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) + _, er := f.Read(buf) + if er != nil { + 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 { + f.Close() + os.Remove("tmp") + t.Fatal("The number of rows is inconsistent with the actual number of rows in the tree itself.") + } + f.Close() + os.Remove("tmp") +} diff --git a/ag_201_coredns/plugin/file/tree/tree.go b/ag_201_coredns/plugin/file/tree/tree.go new file mode 100644 index 0000000..a6caafe --- /dev/null +++ b/ag_201_coredns/plugin/file/tree/tree.go @@ -0,0 +1,453 @@ +// Copyright ©2012 The bíogo Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found at the end of this file. + +// Package tree implements Left-Leaning Red Black trees as described by Robert Sedgewick. +// +// More details relating to the implementation are available at the following locations: +// +// http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf +// http://www.cs.princeton.edu/~rs/talks/LLRB/Java/RedBlackBST.java +// http://www.teachsolaisgames.com/articles/balanced_left_leaning.html +// +// Heavily modified by Miek Gieben for use in DNS zones. +package tree + +import "github.com/miekg/dns" + +const ( + td234 = iota + bu23 +) + +// Operation mode of the LLRB tree. +const mode = bu23 + +func init() { + if mode != td234 && mode != bu23 { + panic("tree: unknown mode") + } +} + +// A Color represents the color of a Node. +type Color bool + +const ( + // Red as false give us the defined behaviour that new nodes are red. Although this + // is incorrect for the root node, that is resolved on the first insertion. + red Color = false + black Color = true +) + +// A Node represents a node in the LLRB tree. +type Node struct { + Elem *Elem + Left, Right *Node + Color Color +} + +// A Tree manages the root node of an LLRB tree. Public methods are exposed through this type. +type Tree struct { + Root *Node // Root node of the tree. + Count int // Number of elements stored. +} + +// Helper methods + +// color returns the effect color of a Node. A nil node returns black. +func (n *Node) color() Color { + if n == nil { + return black + } + return n.Color +} + +// (a,c)b -rotL-> ((a,)b,)c +func (n *Node) rotateLeft() (root *Node) { + // Assumes: n has two children. + root = n.Right + n.Right = root.Left + root.Left = n + root.Color = n.Color + n.Color = red + return +} + +// (a,c)b -rotR-> (,(,c)b)a +func (n *Node) rotateRight() (root *Node) { + // Assumes: n has two children. + root = n.Left + n.Left = root.Right + root.Right = n + root.Color = n.Color + n.Color = red + return +} + +// (aR,cR)bB -flipC-> (aB,cB)bR | (aB,cB)bR -flipC-> (aR,cR)bB +func (n *Node) flipColors() { + // Assumes: n has two children. + n.Color = !n.Color + n.Left.Color = !n.Left.Color + n.Right.Color = !n.Right.Color +} + +// fixUp ensures that black link balance is correct, that red nodes lean left, +// and that 4 nodes are split in the case of BU23 and properly balanced in TD234. +func (n *Node) fixUp() *Node { + if n.Right.color() == red { + if mode == td234 && n.Right.Left.color() == red { + n.Right = n.Right.rotateRight() + } + n = n.rotateLeft() + } + if n.Left.color() == red && n.Left.Left.color() == red { + n = n.rotateRight() + } + if mode == bu23 && n.Left.color() == red && n.Right.color() == red { + n.flipColors() + } + return n +} + +func (n *Node) moveRedLeft() *Node { + n.flipColors() + if n.Right.Left.color() == red { + n.Right = n.Right.rotateRight() + n = n.rotateLeft() + n.flipColors() + if mode == td234 && n.Right.Right.color() == red { + n.Right = n.Right.rotateLeft() + } + } + return n +} + +func (n *Node) moveRedRight() *Node { + n.flipColors() + if n.Left.Left.color() == red { + n = n.rotateRight() + n.flipColors() + } + return n +} + +// Len returns the number of elements stored in the Tree. +func (t *Tree) Len() int { + return t.Count +} + +// Search returns the first match of qname in the Tree. +func (t *Tree) Search(qname string) (*Elem, bool) { + if t.Root == nil { + return nil, false + } + n, res := t.Root.search(qname) + if n == nil { + return nil, res + } + return n.Elem, res +} + +// search searches the tree for qname and type. +func (n *Node) search(qname string) (*Node, bool) { + for n != nil { + switch c := Less(n.Elem, qname); { + case c == 0: + return n, true + case c < 0: + n = n.Left + default: + n = n.Right + } + } + + return n, false +} + +// Insert inserts rr into the Tree at the first match found +// with e or when a nil node is reached. +func (t *Tree) Insert(rr dns.RR) { + var d int + t.Root, d = t.Root.insert(rr) + t.Count += d + t.Root.Color = black +} + +// insert inserts rr in to the tree. +func (n *Node) insert(rr dns.RR) (root *Node, d int) { + if n == nil { + return &Node{Elem: newElem(rr)}, 1 + } else if n.Elem == nil { + n.Elem = newElem(rr) + return n, 1 + } + + if mode == td234 { + if n.Left.color() == red && n.Right.color() == red { + n.flipColors() + } + } + + switch c := Less(n.Elem, rr.Header().Name); { + case c == 0: + n.Elem.Insert(rr) + case c < 0: + n.Left, d = n.Left.insert(rr) + default: + n.Right, d = n.Right.insert(rr) + } + + if n.Right.color() == red && n.Left.color() == black { + n = n.rotateLeft() + } + if n.Left.color() == red && n.Left.Left.color() == red { + n = n.rotateRight() + } + + if mode == bu23 { + if n.Left.color() == red && n.Right.color() == red { + n.flipColors() + } + } + + root = n + + return +} + +// DeleteMin deletes the node with the minimum value in the tree. +func (t *Tree) DeleteMin() { + if t.Root == nil { + return + } + var d int + t.Root, d = t.Root.deleteMin() + t.Count += d + if t.Root == nil { + return + } + t.Root.Color = black +} + +func (n *Node) deleteMin() (root *Node, d int) { + if n.Left == nil { + return nil, -1 + } + if n.Left.color() == black && n.Left.Left.color() == black { + n = n.moveRedLeft() + } + n.Left, d = n.Left.deleteMin() + + root = n.fixUp() + + return +} + +// DeleteMax deletes the node with the maximum value in the tree. +func (t *Tree) DeleteMax() { + if t.Root == nil { + return + } + var d int + t.Root, d = t.Root.deleteMax() + t.Count += d + if t.Root == nil { + return + } + t.Root.Color = black +} + +func (n *Node) deleteMax() (root *Node, d int) { + if n.Left != nil && n.Left.color() == red { + n = n.rotateRight() + } + if n.Right == nil { + return nil, -1 + } + if n.Right.color() == black && n.Right.Left.color() == black { + n = n.moveRedRight() + } + n.Right, d = n.Right.deleteMax() + + root = n.fixUp() + + return +} + +// Delete removes all RRs of type rr.Header().Rrtype from e. If after the deletion of rr the node is empty the +// entire node is deleted. +func (t *Tree) Delete(rr dns.RR) { + if t.Root == nil { + return + } + + el, _ := t.Search(rr.Header().Name) + if el == nil { + return + } + el.Delete(rr) + if el.Empty() { + t.deleteNode(rr) + } +} + +// DeleteNode deletes the node that matches rr according to Less(). +func (t *Tree) deleteNode(rr dns.RR) { + if t.Root == nil { + return + } + var d int + t.Root, d = t.Root.delete(rr) + t.Count += d + if t.Root == nil { + return + } + t.Root.Color = black +} + +func (n *Node) delete(rr dns.RR) (root *Node, d int) { + if Less(n.Elem, rr.Header().Name) < 0 { + if n.Left != nil { + if n.Left.color() == black && n.Left.Left.color() == black { + n = n.moveRedLeft() + } + n.Left, d = n.Left.delete(rr) + } + } else { + if n.Left.color() == red { + n = n.rotateRight() + } + if n.Right == nil && Less(n.Elem, rr.Header().Name) == 0 { + return nil, -1 + } + if n.Right != nil { + if n.Right.color() == black && n.Right.Left.color() == black { + n = n.moveRedRight() + } + if Less(n.Elem, rr.Header().Name) == 0 { + n.Elem = n.Right.min().Elem + n.Right, d = n.Right.deleteMin() + } else { + n.Right, d = n.Right.delete(rr) + } + } + } + + root = n.fixUp() + return +} + +// Min returns the minimum value stored in the tree. +func (t *Tree) Min() *Elem { + if t.Root == nil { + return nil + } + return t.Root.min().Elem +} + +func (n *Node) min() *Node { + for ; n.Left != nil; n = n.Left { + } + return n +} + +// Max returns the maximum value stored in the tree. +func (t *Tree) Max() *Elem { + if t.Root == nil { + return nil + } + return t.Root.max().Elem +} + +func (n *Node) max() *Node { + for ; n.Right != nil; n = n.Right { + } + return n +} + +// Prev returns the greatest value equal to or less than the qname according to Less(). +func (t *Tree) Prev(qname string) (*Elem, bool) { + if t.Root == nil { + return nil, false + } + + n := t.Root.floor(qname) + if n == nil { + return nil, false + } + return n.Elem, true +} + +func (n *Node) floor(qname string) *Node { + if n == nil { + return nil + } + switch c := Less(n.Elem, qname); { + case c == 0: + return n + case c <= 0: + return n.Left.floor(qname) + default: + if r := n.Right.floor(qname); r != nil { + return r + } + } + return n +} + +// Next returns the smallest value equal to or greater than the qname according to Less(). +func (t *Tree) Next(qname string) (*Elem, bool) { + if t.Root == nil { + return nil, false + } + n := t.Root.ceil(qname) + if n == nil { + return nil, false + } + return n.Elem, true +} + +func (n *Node) ceil(qname string) *Node { + if n == nil { + return nil + } + switch c := Less(n.Elem, qname); { + case c == 0: + return n + case c > 0: + return n.Right.ceil(qname) + default: + if l := n.Left.ceil(qname); l != nil { + return l + } + } + return n +} + +/* +Copyright ©2012 The bíogo Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the bíogo project nor the names of its authors and + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ diff --git a/ag_201_coredns/plugin/file/tree/walk.go b/ag_201_coredns/plugin/file/tree/walk.go new file mode 100644 index 0000000..e315eb0 --- /dev/null +++ b/ag_201_coredns/plugin/file/tree/walk.go @@ -0,0 +1,33 @@ +package tree + +import "github.com/miekg/dns" + +// Walk performs fn on all authoritative values stored in the tree in +// in-order depth first. If a non-nil error is returned the Walk was interrupted +// by an fn returning that error. If fn alters stored values' sort +// relationships, future tree operation behaviors are undefined. +func (t *Tree) Walk(fn func(*Elem, map[uint16][]dns.RR) error) error { + if t.Root == nil { + return nil + } + return t.Root.walk(fn) +} + +func (n *Node) walk(fn func(*Elem, map[uint16][]dns.RR) error) error { + if n.Left != nil { + if err := n.Left.walk(fn); err != nil { + return err + } + } + + if err := fn(n.Elem, n.Elem.m); err != nil { + return err + } + + if n.Right != nil { + if err := n.Right.walk(fn); err != nil { + return err + } + } + return nil +} diff --git a/ag_201_coredns/plugin/file/wildcard.go b/ag_201_coredns/plugin/file/wildcard.go new file mode 100644 index 0000000..9526cb5 --- /dev/null +++ b/ag_201_coredns/plugin/file/wildcard.go @@ -0,0 +1,13 @@ +package file + +import "github.com/miekg/dns" + +// replaceWithWildcard replaces the left most label with '*'. +func replaceWithAsteriskLabel(qname string) (wildcard string) { + i, shot := dns.NextLabel(qname, 0) + if shot { + return "" + } + + return "*." + qname[i:] +} diff --git a/ag_201_coredns/plugin/file/wildcard_test.go b/ag_201_coredns/plugin/file/wildcard_test.go new file mode 100644 index 0000000..894a088 --- /dev/null +++ b/ag_201_coredns/plugin/file/wildcard_test.go @@ -0,0 +1,298 @@ +package file + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +// these examples don't have an additional opt RR set, because that's gets added by the server. +var wildcardTestCases = []test.Case{ + { + Qname: "wild.dnssex.nl.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT(`wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`), + }, + Ns: dnssexAuth[:len(dnssexAuth)-1], // remove RRSIG on the end + }, + { + Qname: "a.wild.dnssex.nl.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT(`a.wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`), + }, + Ns: dnssexAuth[:len(dnssexAuth)-1], // remove RRSIG on the end + }, + { + Qname: "wild.dnssex.nl.", Qtype: dns.TypeTXT, Do: true, + Answer: []dns.RR{ + test.RRSIG("wild.dnssex.nl. 1800 IN RRSIG TXT 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. FUZSTyvZfeuuOpCm"), + test.TXT(`wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`), + }, + Ns: append([]dns.RR{ + test.NSEC("a.dnssex.nl. 14400 IN NSEC www.dnssex.nl. A AAAA RRSIG NSEC"), + test.RRSIG("a.dnssex.nl. 14400 IN RRSIG NSEC 8 3 14400 20160428190224 20160329190224 14460 dnssex.nl. S+UMs2ySgRaaRY"), + }, dnssexAuth...), + }, + { + Qname: "a.wild.dnssex.nl.", Qtype: dns.TypeTXT, Do: true, + Answer: []dns.RR{ + test.RRSIG("a.wild.dnssex.nl. 1800 IN RRSIG TXT 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. FUZSTyvZfeuuOpCm"), + test.TXT(`a.wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`), + }, + Ns: append([]dns.RR{ + test.NSEC("a.dnssex.nl. 14400 IN NSEC www.dnssex.nl. A AAAA RRSIG NSEC"), + test.RRSIG("a.dnssex.nl. 14400 IN RRSIG NSEC 8 3 14400 20160428190224 20160329190224 14460 dnssex.nl. S+UMs2ySgRaaRY"), + }, dnssexAuth...), + }, + // nodata responses + { + Qname: "wild.dnssex.nl.", Qtype: dns.TypeSRV, + Ns: []dns.RR{ + test.SOA(`dnssex.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1459281744 14400 3600 604800 14400`), + }, + }, + { + Qname: "wild.dnssex.nl.", Qtype: dns.TypeSRV, Do: true, + Ns: []dns.RR{ + // TODO(miek): needs closest encloser proof as well? This is the wrong answer + test.NSEC(`*.dnssex.nl. 14400 IN NSEC a.dnssex.nl. TXT RRSIG NSEC`), + test.RRSIG(`*.dnssex.nl. 14400 IN RRSIG NSEC 8 2 14400 20160428190224 20160329190224 14460 dnssex.nl. os6INm6q2eXknD5z8TaaDOV+Ge/Ko+2dXnKP+J1fqJzafXJVH1F0nDrcXmMlR6jlBHA=`), + test.RRSIG(`dnssex.nl. 1800 IN RRSIG SOA 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. CA/Y3m9hCOiKC/8ieSOv8SeP964Bq++lyH8BZJcTaabAsERs4xj5PRtcxicwQXZiF8fYUCpROlUS0YR8Cdw=`), + test.SOA(`dnssex.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1459281744 14400 3600 604800 14400`), + }, + }, +} + +var dnssexAuth = []dns.RR{ + test.NS("dnssex.nl. 1800 IN NS linode.atoom.net."), + test.NS("dnssex.nl. 1800 IN NS ns-ext.nlnetlabs.nl."), + test.NS("dnssex.nl. 1800 IN NS omval.tednet.nl."), + test.RRSIG("dnssex.nl. 1800 IN RRSIG NS 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. dLIeEvP86jj5ndkcLzhgvWixTABjWAGRTGQsPsVDFXsGMf9TGGC9FEomgkCVeNC0="), +} + +func TestLookupWildcard(t *testing.T) { + zone, err := Parse(strings.NewReader(dbDnssexNLSigned), testzone1, "stdin", 0) + if err != nil { + t.Fatalf("Expect no error when reading zone, got %q", err) + } + + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone1: zone}, Names: []string{testzone1}}} + ctx := context.TODO() + + for _, tc := range wildcardTestCases { + 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 + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +var wildcardDoubleTestCases = []test.Case{ + { + Qname: "wild.w.example.org.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT(`wild.w.example.org. IN TXT "Wildcard"`), + }, + Ns: exampleAuth, + }, + { + Qname: "wild.c.example.org.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT(`wild.c.example.org. IN TXT "c Wildcard"`), + }, + Ns: exampleAuth, + }, + { + Qname: "wild.d.example.org.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT(`alias.example.org. IN TXT "Wildcard CNAME expansion"`), + test.CNAME(`wild.d.example.org. IN CNAME alias.example.org`), + }, + Ns: exampleAuth, + }, + { + Qname: "alias.example.org.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT(`alias.example.org. IN TXT "Wildcard CNAME expansion"`), + }, + Ns: exampleAuth, + }, +} + +var exampleAuth = []dns.RR{ + test.NS("example.org. 3600 IN NS a.iana-servers.net."), + test.NS("example.org. 3600 IN NS b.iana-servers.net."), +} + +func TestLookupDoubleWildcard(t *testing.T) { + zone, err := Parse(strings.NewReader(exampleOrg), "example.org.", "stdin", 0) + if err != nil { + t.Fatalf("Expect no error when reading zone, got %q", err) + } + + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{"example.org.": zone}, Names: []string{"example.org."}}} + ctx := context.TODO() + + for _, tc := range wildcardDoubleTestCases { + 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 + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +func TestReplaceWithAsteriskLabel(t *testing.T) { + tests := []struct { + in, out string + }{ + {".", ""}, + {"miek.nl.", "*.nl."}, + {"www.miek.nl.", "*.miek.nl."}, + } + + for _, tc := range tests { + got := replaceWithAsteriskLabel(tc.in) + if got != tc.out { + t.Errorf("Expected to be %s, got %s", tc.out, got) + } + } +} + +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.`)}, + }, + { + 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.`)}, + }, +} + +func TestLookupApexWildcard(t *testing.T) { + const name = "example.org." + zone, err := Parse(strings.NewReader(apexWildcard), name, "stdin", 0) + if err != nil { + t.Fatalf("Expect no error when reading zone, got %q", err) + } + + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{name: zone}, Names: []string{name}}} + ctx := context.TODO() + + for _, tc := range apexWildcardTestCases { + 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 + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +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.`)}, + }, + { + 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.`)}, + }, + { + 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.`)}, + }, +} + +func TestLookupMultiWildcard(t *testing.T) { + const name = "example.org." + zone, err := Parse(strings.NewReader(doubleWildcard), name, "stdin", 0) + if err != nil { + t.Fatalf("Expect no error when reading zone, got %q", err) + } + + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{name: zone}, Names: []string{name}}} + ctx := context.TODO() + + for _, tc := range multiWildcardTestCases { + 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 + } + + resp := rec.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +const exampleOrg = `; example.org test file +$TTL 3600 +example.org. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600 +example.org. IN NS b.iana-servers.net. +example.org. IN NS a.iana-servers.net. +example.org. IN A 127.0.0.1 +example.org. IN A 127.0.0.2 +*.w.example.org. IN TXT "Wildcard" +a.b.c.w.example.org. IN TXT "Not a wildcard" +*.c.example.org. IN TXT "c Wildcard" +*.d.example.org. IN CNAME alias.example.org. +alias.example.org. IN TXT "Wildcard CNAME expansion" +` + +const apexWildcard = `; example.org test file with wildcard at apex +$TTL 3600 +example.org. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600 +example.org. IN NS b.iana-servers.net. +*.example.org. IN A 127.0.0.53 +foo.example.org. IN A 127.0.0.54 +` + +const doubleWildcard = `; example.org test file with wildcard at apex +$TTL 3600 +example.org. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600 +example.org. IN NS b.iana-servers.net. +*.example.org. IN A 127.0.0.53 +*.intern.example.org. IN A 127.0.1.52 +foo.example.org. IN A 127.0.0.54 +` diff --git a/ag_201_coredns/plugin/file/xfr.go b/ag_201_coredns/plugin/file/xfr.go new file mode 100644 index 0000000..28c3a3a --- /dev/null +++ b/ag_201_coredns/plugin/file/xfr.go @@ -0,0 +1,45 @@ +package file + +import ( + "github.com/coredns/coredns/plugin/file/tree" + "github.com/coredns/coredns/plugin/transfer" + + "github.com/miekg/dns" +) + +// Transfer implements the transfer.Transfer interface. +func (f File) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) { + z, ok := f.Zones.Z[zone] + if !ok || z == nil { + return nil, transfer.ErrNotAuthoritative + } + return z.Transfer(serial) +} + +// Transfer transfers a zone with serial in the returned channel and implements IXFR fallback, by just +// sending a single SOA record. +func (z *Zone) Transfer(serial uint32) (<-chan []dns.RR, error) { + // get soa and apex + apex, err := z.ApexIfDefined() + if err != nil { + return nil, err + } + + ch := make(chan []dns.RR) + go func() { + if serial != 0 && apex[0].(*dns.SOA).Serial == serial { // ixfr fallback, only send SOA + ch <- []dns.RR{apex[0]} + + close(ch) + return + } + + ch <- apex + z.Walk(func(e *tree.Elem, _ map[uint16][]dns.RR) error { ch <- e.All(); return nil }) + ch <- []dns.RR{apex[0]} + + close(ch) + }() + + return ch, nil +} diff --git a/ag_201_coredns/plugin/file/xfr_test.go b/ag_201_coredns/plugin/file/xfr_test.go new file mode 100644 index 0000000..f8d4caf --- /dev/null +++ b/ag_201_coredns/plugin/file/xfr_test.go @@ -0,0 +1,72 @@ +package file + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func ExampleZone_All() { + zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0) + if err != nil { + return + } + records := zone.All() + for _, r := range records { + fmt.Printf("%+v\n", r) + } + // Output + // xfr_test.go:15: miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400 + // xfr_test.go:15: www.miek.nl. 1800 IN CNAME a.miek.nl. + // xfr_test.go:15: miek.nl. 1800 IN NS linode.atoom.net. + // xfr_test.go:15: miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl. + // xfr_test.go:15: miek.nl. 1800 IN NS omval.tednet.nl. + // xfr_test.go:15: miek.nl. 1800 IN NS ext.ns.whyscream.net. + // xfr_test.go:15: miek.nl. 1800 IN MX 1 aspmx.l.google.com. + // xfr_test.go:15: miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com. + // xfr_test.go:15: miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com. + // xfr_test.go:15: miek.nl. 1800 IN MX 10 aspmx2.googlemail.com. + // xfr_test.go:15: miek.nl. 1800 IN MX 10 aspmx3.googlemail.com. + // xfr_test.go:15: miek.nl. 1800 IN A 139.162.196.78 + // xfr_test.go:15: miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 + // xfr_test.go:15: archive.miek.nl. 1800 IN CNAME a.miek.nl. + // xfr_test.go:15: a.miek.nl. 1800 IN A 139.162.196.78 + // xfr_test.go:15: a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 +} + +func TestAllNewZone(t *testing.T) { + zone := NewZone("example.org.", "stdin") + records := zone.All() + if len(records) != 0 { + t.Errorf("Expected %d records in empty zone, got %d", 0, len(records)) + } +} + +func TestAXFRWithOutTransferPlugin(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() + + m := new(dns.Msg) + m.SetQuestion("miek.nl.", dns.TypeAXFR) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + code, err := fm.ServeDNS(ctx, rec, m) + if err != nil { + t.Errorf("Expected no error, got %v", err) + return + } + if code != dns.RcodeRefused { + t.Errorf("Expecting REFUSED, got %d", code) + } +} diff --git a/ag_201_coredns/plugin/file/zone.go b/ag_201_coredns/plugin/file/zone.go new file mode 100644 index 0000000..aa5f3ca --- /dev/null +++ b/ag_201_coredns/plugin/file/zone.go @@ -0,0 +1,178 @@ +package file + +import ( + "fmt" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/coredns/coredns/plugin/file/tree" + "github.com/coredns/coredns/plugin/pkg/upstream" + + "github.com/miekg/dns" +) + +// Zone is a structure that contains all data related to a DNS zone. +type Zone struct { + origin string + origLen int + file string + *tree.Tree + Apex + Expired bool + + sync.RWMutex + + StartupOnce sync.Once + TransferFrom []string + + ReloadInterval time.Duration + reloadShutdown chan bool + + Upstream *upstream.Upstream // Upstream for looking up external names during the resolution process. +} + +// Apex contains the apex records of a zone: SOA, NS and their potential signatures. +type Apex struct { + SOA *dns.SOA + NS []dns.RR + SIGSOA []dns.RR + SIGNS []dns.RR +} + +// NewZone returns a new zone. +func NewZone(name, file string) *Zone { + return &Zone{ + origin: dns.Fqdn(name), + origLen: dns.CountLabel(dns.Fqdn(name)), + file: filepath.Clean(file), + Tree: &tree.Tree{}, + reloadShutdown: make(chan bool), + } +} + +// Copy copies a zone. +func (z *Zone) Copy() *Zone { + z1 := NewZone(z.origin, z.file) + z1.TransferFrom = z.TransferFrom + z1.Expired = z.Expired + + z1.Apex = z.Apex + return z1 +} + +// CopyWithoutApex copies zone z without the Apex records. +func (z *Zone) CopyWithoutApex() *Zone { + z1 := NewZone(z.origin, z.file) + z1.TransferFrom = z.TransferFrom + z1.Expired = z.Expired + + return z1 +} + +// Insert inserts r into z. +func (z *Zone) Insert(r dns.RR) error { + 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) + 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) + 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) + case dns.TypeRRSIG: + x := r.(*dns.RRSIG) + switch x.TypeCovered { + case dns.TypeSOA: + z.Apex.SIGSOA = append(z.Apex.SIGSOA, x) + return nil + case dns.TypeNS: + if r.Header().Name == z.origin { + z.Apex.SIGNS = append(z.Apex.SIGNS, x) + return nil + } + } + case dns.TypeCNAME: + r.(*dns.CNAME).Target = strings.ToLower(r.(*dns.CNAME).Target) + 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) + } + + z.Tree.Insert(r) + return nil +} + +// File retrieves the file path in a safe way. +func (z *Zone) File() string { + z.RLock() + defer z.RUnlock() + return z.file +} + +// SetFile updates the file path in a safe way. +func (z *Zone) SetFile(path string) { + z.Lock() + z.file = path + z.Unlock() +} + +// ApexIfDefined returns the apex nodes from z. The SOA record is the first record, if it does not exist, an error is returned. +func (z *Zone) ApexIfDefined() ([]dns.RR, error) { + z.RLock() + defer z.RUnlock() + if z.Apex.SOA == nil { + return nil, fmt.Errorf("no SOA") + } + + rrs := []dns.RR{z.Apex.SOA} + + if len(z.Apex.SIGSOA) > 0 { + rrs = append(rrs, z.Apex.SIGSOA...) + } + if len(z.Apex.NS) > 0 { + rrs = append(rrs, z.Apex.NS...) + } + if len(z.Apex.SIGNS) > 0 { + rrs = append(rrs, z.Apex.SIGNS...) + } + + return rrs, nil +} + +// NameFromRight returns the labels from the right, staring with the +// origin and then i labels extra. When we are overshooting the name +// the returned boolean is set to true. +func (z *Zone) nameFromRight(qname string, i int) (string, bool) { + if i <= 0 { + return z.origin, false + } + + for j := 1; j <= z.origLen; j++ { + if _, shot := dns.PrevLabel(qname, j); shot { + return qname, shot + } + } + + k := 0 + var shot bool + for j := 1; j <= i; j++ { + k, shot = dns.PrevLabel(qname, j+z.origLen) + if shot { + return qname, shot + } + } + return qname[k:], false +} diff --git a/ag_201_coredns/plugin/file/zone_test.go b/ag_201_coredns/plugin/file/zone_test.go new file mode 100644 index 0000000..aa42fd8 --- /dev/null +++ b/ag_201_coredns/plugin/file/zone_test.go @@ -0,0 +1,30 @@ +package file + +import "testing" + +func TestNameFromRight(t *testing.T) { + z := NewZone("example.org.", "stdin") + + tests := []struct { + in string + labels int + shot bool + expected string + }{ + {"example.org.", 0, false, "example.org."}, + {"a.example.org.", 0, false, "example.org."}, + {"a.example.org.", 1, false, "a.example.org."}, + {"a.example.org.", 2, true, "a.example.org."}, + {"a.b.example.org.", 2, false, "a.b.example.org."}, + } + + for i, tc := range tests { + got, shot := z.nameFromRight(tc.in, tc.labels) + if got != tc.expected { + t.Errorf("Test %d: expected %s, got %s", i, tc.expected, got) + } + if shot != tc.shot { + t.Errorf("Test %d: expected shot to be %t, got %t", i, tc.shot, shot) + } + } +} diff --git a/ag_201_coredns/plugin/forward/README.md b/ag_201_coredns/plugin/forward/README.md new file mode 100644 index 0000000..0088c9c --- /dev/null +++ b/ag_201_coredns/plugin/forward/README.md @@ -0,0 +1,265 @@ +# forward + +## Name + +*forward* - facilitates proxying DNS messages to upstream resolvers. + +## Description + +The *forward* plugin re-uses already opened sockets to the upstreams. It supports UDP, TCP and +DNS-over-TLS and uses in band health checking. + +When it detects an error a health check is performed. This checks runs in a loop, performing each +check at a *0.5s* interval for as long as the upstream reports unhealthy. Once healthy we stop +health checking (until the next error). The health checks use a recursive DNS query (`. IN NS`) +to get upstream health. Any response that is not a network error (REFUSED, NOTIMPL, SERVFAIL, etc) +is taken as a healthy upstream. The health check uses the same protocol as specified in **TO**. If +`max_fails` is set to 0, no checking is performed and upstreams will always be considered healthy. + +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). + +## Syntax + +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. 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. + +Multiple upstreams are randomized (see `policy`) on first use. When a healthy proxy returns an error +during the exchange the next upstream in the list is tried. + +Extra knobs are available with an expanded syntax: + +~~~ +forward FROM TO... { + except IGNORED_NAMES... + force_tcp + prefer_udp + expire DURATION + max_fails INTEGER + tls CERT KEY CA + tls_servername NAME + policy random|round_robin|sequential + health_check DURATION [no_rec] [domain FQDN] + max_concurrent MAX +} +~~~ + +* **FROM** and **TO...** as above. +* **IGNORED_NAMES** in `except` is a space-separated list of domains to exclude from forwarding. + Requests that match none of these names will be passed through. +* `force_tcp`, use TCP even when the request comes in over UDP. +* `prefer_udp`, try first using UDP even when the request comes in over TCP. If response is truncated + (TC flag set in response) then do another attempt over TCP. In case if both `force_tcp` and + `prefer_udp` options specified the `force_tcp` takes precedence. +* `max_fails` is the number of subsequent failed health checks that are needed before considering + an upstream to be down. If 0, the upstream will never be marked as down (nor health checked). + Default is 2. +* `expire` **DURATION**, expire (cached) connections after this time, the default is 10s. +* `tls` **CERT** **KEY** **CA** define the TLS properties for TLS connection. From 0 to 3 arguments can be + provided with the meaning as described below + + * `tls` - no client authentication is used, and the system CAs are used to verify the server certificate + * `tls` **CA** - no client authentication is used, and the file CA is used to verify the server certificate + * `tls` **CERT** **KEY** - client authentication is used with the specified cert/key pair. + The server certificate is verified with the system CAs + * `tls` **CERT** **KEY** **CA** - client authentication is used with the specified cert/key pair. + The server certificate is verified using the specified CA file + +* `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. 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. + * `sequential` is a policy that selects hosts based on sequential ordering. +* `health_check` configure the behaviour of health checking of the upstream servers + * `` - 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. + +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. + +On each endpoint, the timeouts for communication are set as follows: + +* 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 + +The forward plugin will publish the following metadata, if the *metadata* +plugin is also enabled: + +* `forward/upstream`: the upstream used to forward the request + +## Metrics + +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, + 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 + 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. +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`. + +## Examples + +Proxy all requests within `example.org.` to a nameserver running on a different port: + +~~~ corefile +example.org { + forward . 127.0.0.1:9005 +} +~~~ + +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 +. { + forward . 10.0.0.10:53 10.0.0.11:1053 [2003::1]:53 +} +~~~ + +Forward everything except requests to `example.org` + +~~~ corefile +. { + forward . 10.0.0.10:1234 { + except example.org + } +} +~~~ + +Proxy everything except `example.org` using the host's `resolv.conf`'s nameservers: + +~~~ corefile +. { + forward . /etc/resolv.conf { + except example.org + } +} +~~~ + +Proxy all requests to 9.9.9.9 using the DNS-over-TLS (DoT) protocol, and cache every answer for up to 30 +seconds. Note the `tls_servername` is mandatory if you want a working setup, as 9.9.9.9 can't be +used in the TLS negotiation. Also set the health check duration to 5s to not completely swamp the +service with health checks. + +~~~ corefile +. { + forward . tls://9.9.9.9 { + tls_servername dns.quad9.net + health_check 5s + } + cache 30 +} +~~~ + +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 +. { + forward . tls://1.1.1.1 tls://1.0.0.1 { + tls_servername cloudflare-dns.com + health_check 5s + } + cache 30 +} +~~~ + +Or when you have multiple DoT upstreams with different `tls_servername`s, you can do the following: + +~~~ corefile +. { + forward . 127.0.0.1:5301 127.0.0.1:5302 +} + +.:5301 { + forward . 8.8.8.8 8.8.4.4 { + tls_servername dns.google + } +} + +.:5302 { + forward . 1.1.1.1 1.0.0.1 { + tls_servername cloudflare-dns.com + } +} +~~~ + +## See Also + +[RFC 7858](https://tools.ietf.org/html/rfc7858) for DNS over TLS. diff --git a/ag_201_coredns/plugin/forward/connect.go b/ag_201_coredns/plugin/forward/connect.go new file mode 100644 index 0000000..3d53044 --- /dev/null +++ b/ag_201_coredns/plugin/forward/connect.go @@ -0,0 +1,152 @@ +// 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/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)) + // records the origin Id before upstream. + originId := state.Req.Id + state.Req.Id = dns.Id() + defer func() { + state.Req.Id = originId + }() + + 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 + } + // recovery the origin Id after upstream. + if ret != nil { + ret.Id = originId + } + return ret, err + } + // drop out-of-order responses + if state.Req.Id == ret.Id { + break + } + } + // recovery the origin Id after upstream. + ret.Id = originId + + p.transport.Yield(pc) + + rc, ok := dns.RcodeToString[ret.Rcode] + if !ok { + rc = strconv.Itoa(ret.Rcode) + } + + RequestCount.WithLabelValues(p.addr).Add(1) + RcodeCount.WithLabelValues(rc, p.addr).Add(1) + RequestDuration.WithLabelValues(p.addr, rc).Observe(time.Since(start).Seconds()) + + return ret, nil +} + +const cumulativeAvgWeight = 4 diff --git a/ag_201_coredns/plugin/forward/dnstap.go b/ag_201_coredns/plugin/forward/dnstap.go new file mode 100644 index 0000000..4e06ac1 --- /dev/null +++ b/ag_201_coredns/plugin/forward/dnstap.go @@ -0,0 +1,63 @@ +package forward + +import ( + "net" + "strconv" + "time" + + "github.com/coredns/coredns/plugin/dnstap/msg" + "github.com/coredns/coredns/request" + + tap "github.com/dnstap/golang-dnstap" + "github.com/miekg/dns" +) + +// 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)} + t := state.Proto() + switch { + case opts.forceTCP: + t = "tcp" + case opts.preferUDP: + t = "udp" + } + + if t == "tcp" { + ta = &net.TCPAddr{IP: ip, Port: int(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) + + 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 + } + 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/ag_201_coredns/plugin/forward/forward.go b/ag_201_coredns/plugin/forward/forward.go new file mode 100644 index 0000000..90ae1ae --- /dev/null +++ b/ag_201_coredns/plugin/forward/forward.go @@ -0,0 +1,239 @@ +// 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" + "crypto/tls" + "errors" + "sync/atomic" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/debug" + "github.com/coredns/coredns/plugin/dnstap" + "github.com/coredns/coredns/plugin/metadata" + clog "github.com/coredns/coredns/plugin/pkg/log" + "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") + +// 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 + p Policy + hcInterval time.Duration + + from string + ignored []string + + tlsConfig *tls.Config + tlsServerName string + maxfails uint32 + expire time.Duration + maxConcurrent int64 + + opts 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. + + 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, hcDomain: "."}} + return f +} + +// SetProxy appends p to the proxy list and starts healthchecking. +func (f *Forward) SetProxy(p *Proxy) { + f.proxies = append(f.proxies, p) + p.start(f.hcInterval) +} + +// Len returns the number of configured proxies. +func (f *Forward) Len() int { return len(f.proxies) } + +// Name implements plugin.Handler. +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) + } + + if f.maxConcurrent > 0 { + count := atomic.AddInt64(&(f.concurrent), 1) + defer atomic.AddInt64(&(f.concurrent), -1) + if count > f.maxConcurrent { + MaxConcurrentRejectCount.Add(1) + return dns.RcodeRefused, f.ErrLimitExceeded + } + } + + fails := 0 + var span, child ot.Span + var upstreamErr error + span = ot.SpanFromContext(ctx) + i := 0 + list := f.List() + deadline := time.Now().Add(defaultTimeout) + start := time.Now() + for time.Now().Before(deadline) { + if i >= len(list) { + // reached the end of list, reset to begin + i = 0 + fails = 0 + } + + proxy := list[i] + i++ + if proxy.Down(f.maxfails) { + fails++ + if fails < len(f.proxies) { + continue + } + // All upstream proxies are dead, 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 + }) + + var ( + ret *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. + continue + } + // Retry with TCP if truncated and prefer_udp configured. + if ret != nil && ret.Truncated && !opts.forceTCP && opts.preferUDP { + opts.forceTCP = true + continue + } + break + } + + if child != nil { + child.Finish() + } + + if f.tapPlugin != nil { + toDnstap(f, proxy.addr, state, opts, ret, start) + } + + upstreamErr = err + + if err != nil { + // Kick off health check to see if *our* upstream is broken. + if f.maxfails != 0 { + proxy.Healthcheck() + } + + if fails < len(f.proxies) { + continue + } + break + } + + // Check if the reply is correct; if not return FormErr. + if !state.Match(ret) { + debug.Hexdumpf(ret, "Wrong reply for id: %d, %s %d", ret.Id, state.QName(), state.QType()) + + formerr := new(dns.Msg) + formerr.SetRcode(state.Req, dns.RcodeFormatError) + w.WriteMsg(formerr) + return 0, nil + } + + w.WriteMsg(ret) + return 0, nil + } + + if upstreamErr != nil { + return dns.RcodeServerFailure, upstreamErr + } + + return dns.RcodeServerFailure, ErrNoHealthy +} + +func (f *Forward) match(state request.Request) bool { + if !plugin.Name(f.from).Matches(state.Name()) || !f.isAllowedDomain(state.Name()) { + return false + } + + return true +} + +func (f *Forward) isAllowedDomain(name string) bool { + if dns.Name(name) == dns.Name(f.from) { + return true + } + + for _, ignore := range f.ignored { + if plugin.Name(ignore).Matches(name) { + return false + } + } + return true +} + +// 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 } + +// 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 } + +// 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) } + +var ( + // ErrNoHealthy means no healthy proxies left. + ErrNoHealthy = errors.New("no healthy proxies") + // ErrNoForward means no forwarder defined. + ErrNoForward = errors.New("no forwarder defined") + // ErrCachedClosed means cached connection was closed by peer. + 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 + hcDomain string +} + +var defaultTimeout = 5 * time.Second diff --git a/ag_201_coredns/plugin/forward/forward_test.go b/ag_201_coredns/plugin/forward/forward_test.go new file mode 100644 index 0000000..b0ef47b --- /dev/null +++ b/ag_201_coredns/plugin/forward/forward_test.go @@ -0,0 +1,24 @@ +package forward + +import ( + "testing" +) + +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{}, + } + + expect := []*Proxy{{addr: "2.2.2.2:53"}, {addr: "1.1.1.1:53"}, {addr: "3.3.3.3:53"}} + 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) + } + } +} diff --git a/ag_201_coredns/plugin/forward/fuzz.go b/ag_201_coredns/plugin/forward/fuzz.go new file mode 100644 index 0000000..bec573e --- /dev/null +++ b/ag_201_coredns/plugin/forward/fuzz.go @@ -0,0 +1,34 @@ +//go:build gofuzz + +package forward + +import ( + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/fuzz" + + "github.com/miekg/dns" +) + +var f *Forward + +// abuse init to setup an environment to test against. This start another server to that will +// reflect responses. +func init() { + f = New() + s := dnstest.NewServer(r{}.reflectHandler) + f.SetProxy(NewProxy(s.Addr, "tcp")) + f.SetProxy(NewProxy(s.Addr, "udp")) +} + +// Fuzz fuzzes forward. +func Fuzz(data []byte) int { + return fuzz.Do(f, data) +} + +type r struct{} + +func (r r) reflectHandler(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetReply(req) + w.WriteMsg(m) +} diff --git a/ag_201_coredns/plugin/forward/health.go b/ag_201_coredns/plugin/forward/health.go new file mode 100644 index 0000000..ec0b481 --- /dev/null +++ b/ag_201_coredns/plugin/forward/health.go @@ -0,0 +1,106 @@ +package forward + +import ( + "crypto/tls" + "sync/atomic" + "time" + + "github.com/coredns/coredns/plugin/pkg/transport" + + "github.com/miekg/dns" +) + +// HealthChecker checks the upstream health. +type HealthChecker interface { + Check(*Proxy) error + SetTLSConfig(*tls.Config) + SetRecursionDesired(bool) + GetRecursionDesired() bool + SetDomain(domain string) + GetDomain() string + SetTCPTransport() +} + +// dnsHc is a health checker for a DNS endpoint (DNS, and DoT). +type dnsHc struct { + c *dns.Client + recursionDesired bool + domain string +} + +var ( + hcReadTimeout = 1 * time.Second + hcWriteTimeout = 1 * time.Second +) + +// NewHealthChecker returns a new HealthChecker based on transport. +func NewHealthChecker(trans string, recursionDesired bool, domain string) HealthChecker { + switch trans { + case transport.DNS, transport.TLS: + c := new(dns.Client) + c.Net = "udp" + c.ReadTimeout = hcReadTimeout + c.WriteTimeout = hcWriteTimeout + + return &dnsHc{c: c, recursionDesired: recursionDesired, domain: domain} + } + + log.Warningf("No healthchecker for transport %q", trans) + return nil +} + +func (h *dnsHc) SetTLSConfig(cfg *tls.Config) { + h.c.Net = "tcp-tls" + h.c.TLSConfig = cfg +} + +func (h *dnsHc) SetRecursionDesired(recursionDesired bool) { + h.recursionDesired = recursionDesired +} +func (h *dnsHc) GetRecursionDesired() bool { + return h.recursionDesired +} + +func (h *dnsHc) SetDomain(domain string) { + h.domain = domain +} +func (h *dnsHc) GetDomain() string { + return h.domain +} + +func (h *dnsHc) SetTCPTransport() { + h.c.Net = "tcp" +} + +// For HC we send to . IN NS +[no]rec message to the upstream. Dial timeouts and empty +// replies are considered fails, basically anything else constitutes a healthy upstream. + +// Check is used as the up.Func in the up.Probe. +func (h *dnsHc) Check(p *Proxy) error { + err := h.send(p.addr) + if err != nil { + HealthcheckFailureCount.WithLabelValues(p.addr).Add(1) + atomic.AddUint32(&p.fails, 1) + return err + } + + atomic.StoreUint32(&p.fails, 0) + return nil +} + +func (h *dnsHc) send(addr string) error { + ping := new(dns.Msg) + ping.SetQuestion(h.domain, dns.TypeNS) + ping.MsgHdr.RecursionDesired = h.recursionDesired + + m, _, err := h.c.Exchange(ping, addr) + // If we got a header, we're alright, basically only care about I/O errors 'n stuff. + if err != nil && m != nil { + // Silly check, something sane came back. + if m.Response || m.Opcode == dns.OpcodeQuery { + err = nil + } + } + + return err +} diff --git a/ag_201_coredns/plugin/forward/health_test.go b/ag_201_coredns/plugin/forward/health_test.go new file mode 100644 index 0000000..9917b3a --- /dev/null +++ b/ag_201_coredns/plugin/forward/health_test.go @@ -0,0 +1,283 @@ +package forward + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/transport" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestHealth(t *testing.T) { + hcReadTimeout = 10 * time.Millisecond + hcWriteTimeout = 10 * time.Millisecond + readTimeout = 10 * time.Millisecond + 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 := NewProxy(s.Addr, transport.DNS) + f := New() + f.SetProxy(p) + defer f.OnShutdown() + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + + 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 RecursionDesired==true to be %d, got %d", 1, i1) + } +} + +func TestHealthTCP(t *testing.T) { + hcReadTimeout = 10 * time.Millisecond + hcWriteTimeout = 10 * time.Millisecond + readTimeout = 10 * time.Millisecond + 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 := NewProxy(s.Addr, transport.DNS) + p.health.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) + 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 == false { + atomic.AddUint32(&i, 1) + } + ret := new(dns.Msg) + ret.SetReply(r) + w.WriteMsg(ret) + }) + defer s.Close() + + p := NewProxy(s.Addr, transport.DNS) + p.health.SetRecursionDesired(false) + f := New() + f.SetProxy(p) + defer f.OnShutdown() + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + + 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 RecursionDesired==false to be %d, got %d", 1, i1) + } +} + +func TestHealthTimeout(t *testing.T) { + hcReadTimeout = 10 * time.Millisecond + hcWriteTimeout = 10 * time.Millisecond + readTimeout = 10 * time.Millisecond + defaultTimeout = 10 * time.Millisecond + + i := uint32(0) + q := uint32(0) + s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { + if r.Question[0].Name == "." { + // health check, answer + atomic.AddUint32(&i, 1) + ret := new(dns.Msg) + ret.SetReply(r) + w.WriteMsg(ret) + return + } + if atomic.LoadUint32(&q) == 0 { //drop only first query + atomic.AddUint32(&q, 1) + return + } + ret := new(dns.Msg) + ret.SetReply(r) + w.WriteMsg(ret) + }) + defer s.Close() + + p := NewProxy(s.Addr, transport.DNS) + f := New() + f.SetProxy(p) + defer f.OnShutdown() + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + + 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 to be %d, got %d", 1, i1) + } +} + +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 + + s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { + // timeout + }) + defer s.Close() + + p := NewProxy(s.Addr, transport.DNS) + f := New() + f.maxfails = 2 + f.SetProxy(p) + defer f.OnShutdown() + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + + f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req) + + time.Sleep(100 * time.Millisecond) + fails := atomic.LoadUint32(&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) { + if r.Question[0].Name == "." { + // health check, answer + atomic.AddUint32(&i, 1) + ret := new(dns.Msg) + ret.SetReply(r) + w.WriteMsg(ret) + } + }) + defer s.Close() + + p := NewProxy(s.Addr, transport.DNS) + f := New() + f.maxfails = 0 + f.SetProxy(p) + defer f.OnShutdown() + + req := new(dns.Msg) + req.SetQuestion("example.org.", dns.TypeA) + + f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req) + + time.Sleep(20 * time.Millisecond) + i1 := atomic.LoadUint32(&i) + if i1 != 0 { + t.Errorf("Expected number of health checks to be %d, got %d", 0, i1) + } +} + +func TestHealthDomain(t *testing.T) { + hcReadTimeout = 10 * time.Millisecond + readTimeout = 10 * time.Millisecond + defaultTimeout = 10 * time.Millisecond + hcWriteTimeout = 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 := NewProxy(s.Addr, transport.DNS) + p.health.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) + } +} diff --git a/ag_201_coredns/plugin/forward/log_test.go b/ag_201_coredns/plugin/forward/log_test.go new file mode 100644 index 0000000..a7f0a85 --- /dev/null +++ b/ag_201_coredns/plugin/forward/log_test.go @@ -0,0 +1,5 @@ +package forward + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/forward/metrics.go b/ag_201_coredns/plugin/forward/metrics.go new file mode 100644 index 0000000..f1f0c48 --- /dev/null +++ b/ag_201_coredns/plugin/forward/metrics.go @@ -0,0 +1,61 @@ +package forward + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// 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"}) + 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{ + Namespace: plugin.Namespace, + Subsystem: "forward", + Name: "healthcheck_broken_total", + Help: "Counter of the number of complete failures of the healthchecks.", + }) + 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/ag_201_coredns/plugin/forward/persistent.go b/ag_201_coredns/plugin/forward/persistent.go new file mode 100644 index 0000000..95d08e1 --- /dev/null +++ b/ag_201_coredns/plugin/forward/persistent.go @@ -0,0 +1,161 @@ +package forward + +import ( + "crypto/tls" + "sort" + "time" + + "github.com/miekg/dns" +) + +// a persistConn hold the dns.Conn and the last used time. +type persistConn struct { + c *dns.Conn + used time.Time +} + +// Transport hold the persistent cache. +type Transport struct { + avgDialTime int64 // kind of average time of dial time + conns [typeTotalCount][]*persistConn // Buckets for udp, tcp and tcp-tls. + expire time.Duration // After this duration a connection is expired. + addr string + tlsConfig *tls.Config + + dial chan string + yield chan *persistConn + ret chan *persistConn + stop chan bool +} + +func newTransport(addr string) *Transport { + t := &Transport{ + avgDialTime: int64(maxDialTimeout / 2), + conns: [typeTotalCount][]*persistConn{}, + expire: defaultExpire, + addr: addr, + dial: make(chan string), + yield: make(chan *persistConn), + ret: make(chan *persistConn), + stop: make(chan bool), + } + return t +} + +// connManagers manages the persistent connection cache for UDP and TCP. +func (t *Transport) connManager() { + ticker := time.NewTicker(defaultExpire) +Wait: + for { + select { + case proto := <-t.dial: + transtype := stringToTransportType(proto) + // take the last used conn - complexity O(1) + if stack := t.conns[transtype]; len(stack) > 0 { + pc := stack[len(stack)-1] + if time.Since(pc.used) < t.expire { + // Found one, remove from pool and return this conn. + t.conns[transtype] = stack[:len(stack)-1] + t.ret <- pc + continue Wait + } + // clear entire cache if the last conn is expired + t.conns[transtype] = nil + // now, the connections being passed to closeConns() are not reachable from + // transport methods anymore. So, it's safe to close them in a separate goroutine + go closeConns(stack) + } + t.ret <- nil + + case pc := <-t.yield: + transtype := t.transportTypeFromConn(pc) + t.conns[transtype] = append(t.conns[transtype], pc) + + case <-ticker.C: + t.cleanup(false) + + case <-t.stop: + t.cleanup(true) + close(t.ret) + return + } + } +} + +// closeConns closes connections. +func closeConns(conns []*persistConn) { + for _, pc := range conns { + pc.c.Close() + } +} + +// cleanup removes connections from cache. +func (t *Transport) cleanup(all bool) { + staleTime := time.Now().Add(-t.expire) + for transtype, stack := range t.conns { + if len(stack) == 0 { + continue + } + if all { + t.conns[transtype] = nil + // now, the connections being passed to closeConns() are not reachable from + // transport methods anymore. So, it's safe to close them in a separate goroutine + go closeConns(stack) + continue + } + if stack[0].used.After(staleTime) { + continue + } + + // connections in stack are sorted by "used" + good := sort.Search(len(stack), func(i int) bool { + return stack[i].used.After(staleTime) + }) + t.conns[transtype] = stack[good:] + // now, the connections being passed to closeConns() are not reachable from + // transport methods anymore. So, it's safe to close them in a separate goroutine + go closeConns(stack[:good]) + } +} + +// It is hard to pin a value to this, the import thing is to no block forever, losing at cached connection is not terrible. +const yieldTimeout = 25 * time.Millisecond + +// Yield returns the connection to transport for reuse. +func (t *Transport) Yield(pc *persistConn) { + pc.used = time.Now() // update used time + + // Make this non-blocking, because in the case of a very busy forwarder we will *block* on this yield. This + // blocks the outer go-routine and stuff will just pile up. We timeout when the send fails to as returning + // these connection is an optimization anyway. + select { + case t.yield <- pc: + return + case <-time.After(yieldTimeout): + return + } +} + +// Start starts the transport's connection manager. +func (t *Transport) Start() { go t.connManager() } + +// Stop stops the transport's connection manager. +func (t *Transport) Stop() { close(t.stop) } + +// SetExpire sets the connection expire time in transport. +func (t *Transport) SetExpire(expire time.Duration) { t.expire = expire } + +// SetTLSConfig sets the TLS config in transport. +func (t *Transport) SetTLSConfig(cfg *tls.Config) { t.tlsConfig = cfg } + +const ( + defaultExpire = 10 * time.Second + minDialTimeout = 1 * time.Second + maxDialTimeout = 30 * time.Second +) + +// Make a var for minimizing this value in tests. +var ( + // Some resolves might take quite a while, usually (cached) responses are fast. Set to 2s to give us some time to retry a different upstream. + readTimeout = 2 * time.Second +) diff --git a/ag_201_coredns/plugin/forward/persistent_test.go b/ag_201_coredns/plugin/forward/persistent_test.go new file mode 100644 index 0000000..633696a --- /dev/null +++ b/ag_201_coredns/plugin/forward/persistent_test.go @@ -0,0 +1,109 @@ +package forward + +import ( + "testing" + "time" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + + "github.com/miekg/dns" +) + +func TestCached(t *testing.T) { + s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { + ret := new(dns.Msg) + ret.SetReply(r) + w.WriteMsg(ret) + }) + defer s.Close() + + tr := newTransport(s.Addr) + tr.Start() + defer tr.Stop() + + c1, cache1, _ := tr.Dial("udp") + c2, cache2, _ := tr.Dial("udp") + + if cache1 || cache2 { + t.Errorf("Expected non-cached connection") + } + + tr.Yield(c1) + tr.Yield(c2) + c3, cached3, _ := tr.Dial("udp") + if !cached3 { + t.Error("Expected cached connection (c3)") + } + if c2 != c3 { + t.Error("Expected c2 == c3") + } + + tr.Yield(c3) + + // dial another protocol + c4, cached4, _ := tr.Dial("tcp") + if cached4 { + t.Errorf("Expected non-cached connection (c4)") + } + tr.Yield(c4) +} + +func TestCleanupByTimer(t *testing.T) { + s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { + ret := new(dns.Msg) + ret.SetReply(r) + w.WriteMsg(ret) + }) + defer s.Close() + + tr := newTransport(s.Addr) + tr.SetExpire(100 * time.Millisecond) + tr.Start() + defer tr.Stop() + + c1, _, _ := tr.Dial("udp") + c2, _, _ := tr.Dial("udp") + tr.Yield(c1) + time.Sleep(10 * time.Millisecond) + tr.Yield(c2) + + time.Sleep(120 * time.Millisecond) + c3, cached, _ := tr.Dial("udp") + if cached { + t.Error("Expected non-cached connection (c3)") + } + tr.Yield(c3) + + time.Sleep(120 * time.Millisecond) + c4, cached, _ := tr.Dial("udp") + if cached { + t.Error("Expected non-cached connection (c4)") + } + tr.Yield(c4) +} + +func TestCleanupAll(t *testing.T) { + s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { + ret := new(dns.Msg) + ret.SetReply(r) + w.WriteMsg(ret) + }) + defer s.Close() + + tr := newTransport(s.Addr) + + c1, _ := dns.DialTimeout("udp", tr.addr, maxDialTimeout) + c2, _ := dns.DialTimeout("udp", tr.addr, maxDialTimeout) + c3, _ := dns.DialTimeout("udp", tr.addr, maxDialTimeout) + + tr.conns[typeUDP] = []*persistConn{{c1, time.Now()}, {c2, time.Now()}, {c3, time.Now()}} + + if len(tr.conns[typeUDP]) != 3 { + t.Error("Expected 3 connections") + } + tr.cleanup(true) + + if len(tr.conns[typeUDP]) > 0 { + t.Error("Expected no cached connections") + } +} diff --git a/ag_201_coredns/plugin/forward/policy.go b/ag_201_coredns/plugin/forward/policy.go new file mode 100644 index 0000000..e81e4ab --- /dev/null +++ b/ag_201_coredns/plugin/forward/policy.go @@ -0,0 +1,68 @@ +package forward + +import ( + "sync/atomic" + "time" + + "github.com/coredns/coredns/plugin/pkg/rand" +) + +// Policy defines a policy we use for selecting upstreams. +type Policy interface { + List([]*Proxy) []*Proxy + String() string +} + +// random is a policy that implements random upstream selection. +type random struct{} + +func (r *random) String() string { return "random" } + +func (r *random) List(p []*Proxy) []*Proxy { + switch len(p) { + case 1: + return p + case 2: + if rn.Int()%2 == 0 { + return []*Proxy{p[1], p[0]} // swap + } + return p + } + + perms := rn.Perm(len(p)) + rnd := make([]*Proxy, len(p)) + + for i, p1 := range perms { + rnd[i] = p[p1] + } + return rnd +} + +// roundRobin is a policy that selects hosts based on round robin ordering. +type roundRobin struct { + robin uint32 +} + +func (r *roundRobin) String() string { return "round_robin" } + +func (r *roundRobin) List(p []*Proxy) []*Proxy { + poolLen := uint32(len(p)) + i := atomic.AddUint32(&r.robin, 1) % poolLen + + robin := []*Proxy{p[i]} + robin = append(robin, p[:i]...) + robin = append(robin, p[i+1:]...) + + return robin +} + +// sequential is a policy that selects hosts based on sequential ordering. +type sequential struct{} + +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/ag_201_coredns/plugin/forward/proxy.go b/ag_201_coredns/plugin/forward/proxy.go new file mode 100644 index 0000000..6a4b569 --- /dev/null +++ b/ag_201_coredns/plugin/forward/proxy.go @@ -0,0 +1,82 @@ +package forward + +import ( + "crypto/tls" + "runtime" + "sync/atomic" + "time" + + "github.com/coredns/coredns/plugin/pkg/up" +) + +// Proxy defines an upstream host. +type Proxy struct { + fails uint32 + addr string + + transport *Transport + + // health checking + probe *up.Probe + health HealthChecker +} + +// NewProxy returns a new proxy. +func NewProxy(addr, trans string) *Proxy { + p := &Proxy{ + addr: addr, + fails: 0, + probe: up.New(), + transport: newTransport(addr), + } + p.health = NewHealthChecker(trans, true, ".") + runtime.SetFinalizer(p, (*Proxy).finalizer) + return p +} + +// SetTLSConfig sets the TLS config in the lower p.transport and in the healthchecking client. +func (p *Proxy) SetTLSConfig(cfg *tls.Config) { + p.transport.SetTLSConfig(cfg) + p.health.SetTLSConfig(cfg) +} + +// SetExpire sets the expire duration in the lower p.transport. +func (p *Proxy) SetExpire(expire time.Duration) { p.transport.SetExpire(expire) } + +// Healthcheck kicks of a round of health checks for this proxy. +func (p *Proxy) Healthcheck() { + if p.health == nil { + log.Warning("No healthchecker") + return + } + + p.probe.Do(func() error { + return p.health.Check(p) + }) +} + +// Down returns true if this proxy is down, i.e. has *more* fails than maxfails. +func (p *Proxy) Down(maxfails uint32) bool { + if maxfails == 0 { + return false + } + + fails := atomic.LoadUint32(&p.fails) + return fails > maxfails +} + +// close stops the health checking goroutine. +func (p *Proxy) stop() { p.probe.Stop() } +func (p *Proxy) finalizer() { p.transport.Stop() } + +// start starts the proxy's healthchecking. +func (p *Proxy) start(duration time.Duration) { + p.probe.Start(duration) + p.transport.Start() +} + +const ( + maxTimeout = 2 * time.Second +) + +var hcInterval = 500 * time.Millisecond diff --git a/ag_201_coredns/plugin/forward/proxy_test.go b/ag_201_coredns/plugin/forward/proxy_test.go new file mode 100644 index 0000000..74a0b5c --- /dev/null +++ b/ag_201_coredns/plugin/forward/proxy_test.go @@ -0,0 +1,99 @@ +package forward + +import ( + "context" + "testing" + + "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" +) + +func TestProxy(t *testing.T) { + s := dnstest.NewServer(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() + + c := caddy.NewTestController("dns", "forward . "+s.Addr) + fs, err := parseForward(c) + f := fs[0] + if err != nil { + t.Errorf("Failed to create forwarder: %s", err) + } + f.OnStartup() + defer f.OnShutdown() + + 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 x := rec.Msg.Answer[0].Header().Name; x != "example.org." { + t.Errorf("Expected %s, got %s", "example.org.", x) + } +} + +func TestProxyTLSFail(t *testing.T) { + // This is an udp/tcp test server, so we shouldn't reach it with TLS. + s := dnstest.NewServer(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() + + c := caddy.NewTestController("dns", "forward . tls://"+s.Addr) + fs, err := parseForward(c) + f := fs[0] + if err != nil { + t.Errorf("Failed to create forwarder: %s", err) + } + f.OnStartup() + defer f.OnShutdown() + + 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 *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/ag_201_coredns/plugin/forward/setup.go b/ag_201_coredns/plugin/forward/setup.go new file mode 100644 index 0000000..dfae70d --- /dev/null +++ b/ag_201_coredns/plugin/forward/setup.go @@ -0,0 +1,292 @@ +package forward + +import ( + "crypto/tls" + "errors" + "fmt" + "strconv" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/dnstap" + "github.com/coredns/coredns/plugin/pkg/parse" + 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 setup(c *caddy.Controller) error { + fs, err := parseForward(c) + if err != nil { + return plugin.Error("forward", err) + } + 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())) + } + + 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 + }) + } + + 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 + } + } + return nil + }) + + c.OnShutdown(func() error { + return f.OnShutdown() + }) + } + + return nil +} + +// OnStartup starts a goroutines for all proxies. +func (f *Forward) OnStartup() (err error) { + for _, p := range f.proxies { + p.start(f.hcInterval) + } + return nil +} + +// OnShutdown stops all configured proxies. +func (f *Forward) OnShutdown() error { + for _, p := range f.proxies { + p.stop() + } + return nil +} + +func parseForward(c *caddy.Controller) ([]*Forward, error) { + var fs = []*Forward{} + for c.Next() { + f, err := parseStanza(c) + if err != nil { + return nil, err + } + fs = append(fs, f) + } + return fs, nil +} + +func parseStanza(c *caddy.Controller) (*Forward, error) { + f := New() + + if !c.Args(&f.from) { + return f, c.ArgErr() + } + 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 { + return f, c.ArgErr() + } + + toHosts, err := parse.HostPortOrFile(to...) + if err != nil { + return f, err + } + + transports := make([]string, len(toHosts)) + allowedTrans := map[string]bool{"dns": true, "tls": true} + for i, host := range toHosts { + trans, h := parse.Transport(host) + + if !allowedTrans[trans] { + return f, fmt.Errorf("'%s' is not supported as a destination protocol in forward: %s", trans, host) + } + p := NewProxy(h, trans) + f.proxies = append(f.proxies, p) + transports[i] = trans + } + + for c.NextBlock() { + if err := parseBlock(c, f); err != nil { + return f, err + } + } + + if f.tlsServerName != "" { + f.tlsConfig.ServerName = f.tlsServerName + } + + // Initialize ClientSessionCache in tls.Config. This may speed up a TLS handshake + // in upcoming connections to the same TLS server. + f.tlsConfig.ClientSessionCache = tls.NewLRUClientSessionCache(len(f.proxies)) + + for i := range f.proxies { + // Only set this for proxies that need it. + if transports[i] == transport.TLS { + f.proxies[i].SetTLSConfig(f.tlsConfig) + } + f.proxies[i].SetExpire(f.expire) + f.proxies[i].health.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].health.SetTCPTransport() + } + f.proxies[i].health.SetDomain(f.opts.hcDomain) + } + + return f, nil +} + +func parseBlock(c *caddy.Controller, f *Forward) error { + switch c.Val() { + case "except": + ignore := c.RemainingArgs() + if len(ignore) == 0 { + return c.ArgErr() + } + for i := 0; i < len(ignore); i++ { + f.ignored = append(f.ignored, plugin.Host(ignore[i]).NormalizeExact()...) + } + case "max_fails": + if !c.NextArg() { + return c.ArgErr() + } + n, err := strconv.ParseUint(c.Val(), 10, 32) + if err != nil { + return err + } + f.maxfails = uint32(n) + case "health_check": + if !c.NextArg() { + return c.ArgErr() + } + dur, err := time.ParseDuration(c.Val()) + if err != nil { + return err + } + if dur < 0 { + 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 + 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) + } + } + + case "force_tcp": + if c.NextArg() { + return c.ArgErr() + } + f.opts.forceTCP = true + case "prefer_udp": + if c.NextArg() { + return c.ArgErr() + } + f.opts.preferUDP = true + case "tls": + args := c.RemainingArgs() + if len(args) > 3 { + return c.ArgErr() + } + + tlsConfig, err := pkgtls.NewTLSConfigFromArgs(args...) + if err != nil { + return err + } + f.tlsConfig = tlsConfig + case "tls_servername": + if !c.NextArg() { + return c.ArgErr() + } + f.tlsServerName = c.Val() + case "expire": + if !c.NextArg() { + return c.ArgErr() + } + dur, err := time.ParseDuration(c.Val()) + if err != nil { + return err + } + if dur < 0 { + return fmt.Errorf("expire can't be negative: %s", dur) + } + f.expire = dur + case "policy": + if !c.NextArg() { + return c.ArgErr() + } + switch x := c.Val(); x { + case "random": + f.p = &random{} + case "round_robin": + f.p = &roundRobin{} + case "sequential": + f.p = &sequential{} + default: + return c.Errf("unknown policy '%s'", x) + } + case "max_concurrent": + if !c.NextArg() { + return c.ArgErr() + } + n, err := strconv.Atoi(c.Val()) + if err != nil { + return err + } + if n < 0 { + return fmt.Errorf("max_concurrent can't be negative: %d", n) + } + f.ErrLimitExceeded = errors.New("concurrent queries exceeded maximum " + c.Val()) + f.maxConcurrent = int64(n) + + default: + return c.Errf("unknown property '%s'", c.Val()) + } + + return nil +} + +const max = 15 // Maximum number of upstreams. diff --git a/ag_201_coredns/plugin/forward/setup_policy_test.go b/ag_201_coredns/plugin/forward/setup_policy_test.go new file mode 100644 index 0000000..13466d7 --- /dev/null +++ b/ag_201_coredns/plugin/forward/setup_policy_test.go @@ -0,0 +1,47 @@ +package forward + +import ( + "strings" + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupPolicy(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedPolicy string + expectedErr string + }{ + // positive + {"forward . 127.0.0.1 {\npolicy random\n}\n", false, "random", ""}, + {"forward . 127.0.0.1 {\npolicy round_robin\n}\n", false, "round_robin", ""}, + {"forward . 127.0.0.1 {\npolicy sequential\n}\n", false, "sequential", ""}, + // negative + {"forward . 127.0.0.1 {\npolicy random2\n}\n", true, "random", "unknown policy"}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + 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) + } + + 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 && (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/ag_201_coredns/plugin/forward/setup_test.go b/ag_201_coredns/plugin/forward/setup_test.go new file mode 100644 index 0000000..4b17430 --- /dev/null +++ b/ag_201_coredns/plugin/forward/setup_test.go @@ -0,0 +1,334 @@ +package forward + +import ( + "os" + "reflect" + "strings" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + + "github.com/miekg/dns" +) + +func TestSetup(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedFrom string + expectedIgnored []string + expectedFails uint32 + expectedOpts options + expectedErr string + }{ + // positive + {"forward . 127.0.0.1", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain example.org\n}\n", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "example.org."}, ""}, + {"forward . 127.0.0.1 {\nexcept miek.nl\n}\n", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nmax_fails 3\n}\n", false, ".", nil, 3, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nforce_tcp\n}\n", false, ".", nil, 2, options{forceTCP: true, hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true, hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nforce_tcp\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true, forceTCP: true, hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1:53", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1:8080", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . [::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . [2003::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 \n", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward 10.9.3.0/18 127.0.0.1", false, "0.9.10.in-addr.arpa.", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {`forward . ::1 + forward com ::2`, false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, "plugin"}, + // negative + {"forward . a27.0.0.1", true, "", nil, 0, options{hcRecursionDesired: true, hcDomain: "."}, "not an IP"}, + {"forward . 127.0.0.1 {\nblaatl\n}\n", true, "", nil, 0, options{hcRecursionDesired: true, hcDomain: "."}, "unknown property"}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain\n}\n", true, "", nil, 0, options{hcRecursionDesired: true, hcDomain: "."}, "Wrong argument count or unexpected line ending after 'domain'"}, + {"forward . https://127.0.0.1 \n", true, ".", nil, 2, 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, options{hcRecursionDesired: true, hcDomain: "."}, "unable to normalize 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'"}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + 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) + } + + if err != nil { + if !test.shouldErr { + 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) { + t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input) + } + } + + 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) + } + } + } +} + +func TestSetupTLS(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedServerName string + expectedErr string + }{ + // positive + {`forward . tls://127.0.0.1 { + tls_servername dns + }`, false, "dns", ""}, + {`forward . 127.0.0.1 { + tls_servername dns + }`, false, "", ""}, + {`forward . 127.0.0.1 { + tls + }`, false, "", ""}, + {`forward . tls://127.0.0.1`, false, "", ""}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + fs, err := parseForward(c) + f := fs[0] + + if test.shouldErr && err == nil { + t.Errorf("Test %d: expected error but found %s for input %s", i, err, 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 && 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) + } + } +} + +func TestSetupResolvconf(t *testing.T) { + const resolv = "resolv.conf" + 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) + + tests := []struct { + input string + shouldErr bool + expectedErr string + expectedNames []string + }{ + // pass + {`forward . ` + resolv, false, "", []string{"10.10.255.252:53", "10.10.255.253:53"}}, + // fail + {`forward . /dev/null`, true, "no nameservers", nil}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + 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) + continue + } + + 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 { + 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 + } + } +} + +func TestSetupMaxConcurrent(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedVal int64 + expectedErr string + }{ + // positive + {"forward . 127.0.0.1 {\nmax_concurrent 1000\n}\n", false, 1000, ""}, + // negative + {"forward . 127.0.0.1 {\nmax_concurrent many\n}\n", true, 0, "invalid"}, + {"forward . 127.0.0.1 {\nmax_concurrent -4\n}\n", true, 0, "negative"}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + 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) + } + + 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 { + continue + } + f := fs[0] + if f.maxConcurrent != test.expectedVal { + t.Errorf("Test %d: expected: %d, got: %d", i, test.expectedVal, f.maxConcurrent) + } + } +} + +func TestSetupHealthCheck(t *testing.T) { + tests := []struct { + 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 {\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 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) + 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) + } + + 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 { + continue + } + + f := fs[0] + if f.opts.hcRecursionDesired != test.expectedRecVal || f.proxies[0].health.GetRecursionDesired() != test.expectedRecVal || + f.opts.hcDomain != test.expectedDomain || f.proxies[0].health.GetDomain() != test.expectedDomain || !dns.IsFqdn(f.proxies[0].health.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(f1.Next)) + } + + 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") + } +} diff --git a/ag_201_coredns/plugin/forward/type.go b/ag_201_coredns/plugin/forward/type.go new file mode 100644 index 0000000..9de842f --- /dev/null +++ b/ag_201_coredns/plugin/forward/type.go @@ -0,0 +1,37 @@ +package forward + +import "net" + +type transportType int + +const ( + typeUDP transportType = iota + typeTCP + typeTLS + typeTotalCount // keep this last +) + +func stringToTransportType(s string) transportType { + switch s { + case "udp": + return typeUDP + case "tcp": + return typeTCP + case "tcp-tls": + return typeTLS + } + + return typeUDP +} + +func (t *Transport) transportTypeFromConn(pc *persistConn) transportType { + if _, ok := pc.c.Conn.(*net.UDPConn); ok { + return typeUDP + } + + if t.tlsConfig == nil { + return typeTCP + } + + return typeTLS +} diff --git a/ag_201_coredns/plugin/geoip/README.md b/ag_201_coredns/plugin/geoip/README.md new file mode 100644 index 0000000..9c9a943 --- /dev/null +++ b/ag_201_coredns/plugin/geoip/README.md @@ -0,0 +1,96 @@ +# 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. +} +``` + +## 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/ag_201_coredns/plugin/geoip/city.go b/ag_201_coredns/plugin/geoip/city.go new file mode 100644 index 0000000..2e5d9f7 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/plugin/geoip/geoip.go b/ag_201_coredns/plugin/geoip/geoip.go new file mode 100644 index 0000000..3451c82 --- /dev/null +++ b/ag_201_coredns/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 { + case g.db.provides&city == 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/ag_201_coredns/plugin/geoip/geoip_test.go b/ag_201_coredns/plugin/geoip/geoip_test.go new file mode 100644 index 0000000..b11fc8b --- /dev/null +++ b/ag_201_coredns/plugin/geoip/geoip_test.go @@ -0,0 +1,90 @@ +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 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) { + 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/ag_201_coredns/plugin/geoip/setup.go b/ag_201_coredns/plugin/geoip/setup.go new file mode 100644 index 0000000..7f6e16f --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/plugin/geoip/setup_test.go b/ag_201_coredns/plugin/geoip/setup_test.go new file mode 100644 index 0000000..b9b0030 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/plugin/geoip/testdata/GeoLite2-City.mmdb b/ag_201_coredns/plugin/geoip/testdata/GeoLite2-City.mmdb new file mode 100644 index 0000000..cd79ed9 Binary files /dev/null and b/ag_201_coredns/plugin/geoip/testdata/GeoLite2-City.mmdb differ diff --git a/ag_201_coredns/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb b/ag_201_coredns/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb new file mode 100644 index 0000000..23efbf3 Binary files /dev/null and b/ag_201_coredns/plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb differ diff --git a/ag_201_coredns/plugin/geoip/testdata/README.md b/ag_201_coredns/plugin/geoip/testdata/README.md new file mode 100644 index 0000000..2f6f884 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/plugin/grpc/README.md b/ag_201_coredns/plugin/grpc/README.md new file mode 100644 index 0000000..5e6148d --- /dev/null +++ b/ag_201_coredns/plugin/grpc/README.md @@ -0,0 +1,135 @@ +# grpc + +## Name + +*grpc* - facilitates proxying DNS messages to upstream resolvers via gRPC protocol. + +## Description + +The *grpc* plugin supports gRPC and TLS. + +This plugin can only be used once per Server Block. + +## Syntax + +In its most basic form: + +~~~ +grpc FROM TO... +~~~ + +* **FROM** is the base domain to match for the request to be proxied. +* **TO...** are the destination endpoints to proxy to. The number of upstreams is + limited to 15. + +Multiple upstreams are randomized (see `policy`) on first use. When a proxy returns an error +the next upstream in the list is tried. + +Extra knobs are available with an expanded syntax: + +~~~ +grpc FROM TO... { + except IGNORED_NAMES... + tls CERT KEY CA + tls_servername NAME + policy random|round_robin|sequential +} +~~~ + +* **FROM** and **TO...** as above. +* **IGNORED_NAMES** in `except` is a space-separated list of domains to exclude from proxying. + Requests that match none of these names will be passed through. +* `tls` **CERT** **KEY** **CA** define the TLS properties for TLS connection. From 0 to 3 arguments can be + provided with the meaning as described below + + * `tls` - no client authentication is used, and the system CAs are used to verify the server certificate + * `tls` **CA** - no client authentication is used, and the file CA is used to verify the server certificate + * `tls` **CERT** **KEY** - client authentication is used with the specified cert/key pair. + The server certificate is verified with the system CAs + * `tls` **CERT** **KEY** **CA** - client authentication is used with the specified cert/key pair. + The server certificate is verified using the specified CA file + +* `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. +* `policy` specifies the policy to use for selecting upstream servers. The default is `random`. + +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. + +## Metrics + +If monitoring is enabled (via the *prometheus* plugin) then the following metric are exported: + +* `coredns_grpc_request_duration_seconds{to}` - duration per upstream interaction. +* `coredns_grpc_requests_total{to}` - query count per upstream. +* `coredns_grpc_responses_total{to, rcode}` - count of RCODEs per upstream. + and we are randomly (this always uses the `random` policy) spraying to an upstream. + +## Examples + +Proxy all requests within `example.org.` to a nameserver running on a different port: + +~~~ corefile +example.org { + grpc . 127.0.0.1:9005 +} +~~~ + +Load balance all requests between three resolvers, one of which has a IPv6 address. + +~~~ corefile +. { + grpc . 10.0.0.10:53 10.0.0.11:1053 [2003::1]:53 +} +~~~ + +Forward everything except requests to `example.org` + +~~~ corefile +. { + grpc . 10.0.0.10:1234 { + except example.org + } +} +~~~ + +Proxy everything except `example.org` using the host's `resolv.conf`'s nameservers: + +~~~ corefile +. { + grpc . /etc/resolv.conf { + except example.org + } +} +~~~ + +Proxy all requests to 9.9.9.9 using the TLS protocol, and cache every answer for up to 30 +seconds. Note the `tls_servername` is mandatory if you want a working setup, as 9.9.9.9 can't be +used in the TLS negotiation. + +~~~ corefile +. { + grpc . 9.9.9.9 { + tls_servername dns.quad9.net + } + cache 30 +} +~~~ + +Or with multiple upstreams from the same provider + +~~~ corefile +. { + grpc . 1.1.1.1 1.0.0.1 { + tls_servername cloudflare-dns.com + } + cache 30 +} +~~~ + +## 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. diff --git a/ag_201_coredns/plugin/grpc/grpc.go b/ag_201_coredns/plugin/grpc/grpc.go new file mode 100644 index 0000000..c2911ed --- /dev/null +++ b/ag_201_coredns/plugin/grpc/grpc.go @@ -0,0 +1,143 @@ +package grpc + +import ( + "context" + "crypto/tls" + "errors" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/debug" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + ot "github.com/opentracing/opentracing-go" +) + +// GRPC represents a plugin instance that can proxy requests to another (DNS) server via gRPC protocol. +// It has a list of proxies each representing one upstream proxy. +type GRPC struct { + proxies []*Proxy + p Policy + + from string + ignored []string + + tlsConfig *tls.Config + tlsServerName string + + Next plugin.Handler +} + +// ServeDNS implements the plugin.Handler interface. +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) + } + + var ( + span, child ot.Span + ret *dns.Msg + err error + i int + ) + span = ot.SpanFromContext(ctx) + list := g.list() + deadline := time.Now().Add(defaultTimeout) + + for time.Now().Before(deadline) { + if i >= len(list) { + // reached the end of list without any answer + if ret != nil { + // write empty response and finish + w.WriteMsg(ret) + } + break + } + + proxy := list[i] + i++ + + if span != nil { + child = span.Tracer().StartSpan("query", ot.ChildOf(span.Context())) + ctx = ot.ContextWithSpan(ctx, child) + } + + ret, err = proxy.query(ctx, r) + if err != nil { + // Continue with the next proxy + continue + } + + if child != nil { + child.Finish() + } + + // Check if the reply is correct; if not return FormErr. + if !state.Match(ret) { + debug.Hexdumpf(ret, "Wrong reply for id: %d, %s %d", ret.Id, state.QName(), state.QType()) + + formerr := new(dns.Msg) + formerr.SetRcode(state.Req, dns.RcodeFormatError) + w.WriteMsg(formerr) + return 0, nil + } + + w.WriteMsg(ret) + return 0, nil + } + + // SERVFAIL if all healthy proxys returned errors. + if err != nil { + // just return the last error received + return dns.RcodeServerFailure, err + } + + return dns.RcodeServerFailure, ErrNoHealthy +} + +// NewGRPC returns a new GRPC. +func newGRPC() *GRPC { + g := &GRPC{ + p: new(random), + } + return g +} + +// Name implements the Handler interface. +func (g *GRPC) Name() string { return "grpc" } + +// Len returns the number of configured proxies. +func (g *GRPC) len() int { return len(g.proxies) } + +func (g *GRPC) match(state request.Request) bool { + if !plugin.Name(g.from).Matches(state.Name()) || !g.isAllowedDomain(state.Name()) { + return false + } + + return true +} + +func (g *GRPC) isAllowedDomain(name string) bool { + if dns.Name(name) == dns.Name(g.from) { + return true + } + + for _, ignore := range g.ignored { + if plugin.Name(ignore).Matches(name) { + return false + } + } + return true +} + +// List returns a set of proxies to be used for this client depending on the policy in p. +func (g *GRPC) list() []*Proxy { return g.p.List(g.proxies) } + +const defaultTimeout = 5 * time.Second + +var ( + // ErrNoHealthy means no healthy proxies left. + ErrNoHealthy = errors.New("no healthy gRPC proxies") +) diff --git a/ag_201_coredns/plugin/grpc/grpc_test.go b/ag_201_coredns/plugin/grpc/grpc_test.go new file mode 100644 index 0000000..06375ec --- /dev/null +++ b/ag_201_coredns/plugin/grpc/grpc_test.go @@ -0,0 +1,75 @@ +package grpc + +import ( + "context" + "errors" + "testing" + + "github.com/coredns/coredns/pb" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestGRPC(t *testing.T) { + m := &dns.Msg{} + msg, err := m.Pack() + if err != nil { + t.Fatalf("Error packing response: %s", err.Error()) + } + dnsPacket := &pb.DnsPacket{Msg: msg} + tests := map[string]struct { + proxies []*Proxy + wantErr bool + }{ + "single_proxy_ok": { + proxies: []*Proxy{ + {client: &testServiceClient{dnsPacket: dnsPacket, err: nil}}, + }, + wantErr: false, + }, + "multiple_proxies_ok": { + proxies: []*Proxy{ + {client: &testServiceClient{dnsPacket: dnsPacket, err: nil}}, + {client: &testServiceClient{dnsPacket: dnsPacket, err: nil}}, + {client: &testServiceClient{dnsPacket: dnsPacket, err: nil}}, + }, + wantErr: false, + }, + "single_proxy_ko": { + proxies: []*Proxy{ + {client: &testServiceClient{dnsPacket: nil, err: errors.New("")}}, + }, + wantErr: true, + }, + "multiple_proxies_one_ko": { + proxies: []*Proxy{ + {client: &testServiceClient{dnsPacket: dnsPacket, err: nil}}, + {client: &testServiceClient{dnsPacket: nil, err: errors.New("")}}, + {client: &testServiceClient{dnsPacket: dnsPacket, err: nil}}, + }, + wantErr: false, + }, + "multiple_proxies_ko": { + proxies: []*Proxy{ + {client: &testServiceClient{dnsPacket: nil, err: errors.New("")}}, + {client: &testServiceClient{dnsPacket: nil, err: errors.New("")}}, + {client: &testServiceClient{dnsPacket: nil, err: errors.New("")}}, + }, + wantErr: true, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + g := newGRPC() + g.from = "." + g.proxies = tt.proxies + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + if _, err := g.ServeDNS(context.TODO(), rec, m); err != nil && !tt.wantErr { + t.Fatal("Expected to receive reply, but didn't") + } + }) + } +} diff --git a/ag_201_coredns/plugin/grpc/metrics.go b/ag_201_coredns/plugin/grpc/metrics.go new file mode 100644 index 0000000..2857042 --- /dev/null +++ b/ag_201_coredns/plugin/grpc/metrics.go @@ -0,0 +1,31 @@ +package grpc + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// Variables declared for monitoring. +var ( + RequestCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "grpc", + Name: "requests_total", + Help: "Counter of requests made per upstream.", + }, []string{"to"}) + RcodeCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "grpc", + Name: "responses_total", + 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.", + }, []string{"to"}) +) diff --git a/ag_201_coredns/plugin/grpc/policy.go b/ag_201_coredns/plugin/grpc/policy.go new file mode 100644 index 0000000..686b2eb --- /dev/null +++ b/ag_201_coredns/plugin/grpc/policy.go @@ -0,0 +1,68 @@ +package grpc + +import ( + "sync/atomic" + "time" + + "github.com/coredns/coredns/plugin/pkg/rand" +) + +// Policy defines a policy we use for selecting upstreams. +type Policy interface { + List([]*Proxy) []*Proxy + String() string +} + +// random is a policy that implements random upstream selection. +type random struct{} + +func (r *random) String() string { return "random" } + +func (r *random) List(p []*Proxy) []*Proxy { + switch len(p) { + case 1: + return p + case 2: + if rn.Int()%2 == 0 { + return []*Proxy{p[1], p[0]} // swap + } + return p + } + + perms := rn.Perm(len(p)) + rnd := make([]*Proxy, len(p)) + + for i, p1 := range perms { + rnd[i] = p[p1] + } + return rnd +} + +// roundRobin is a policy that selects hosts based on round robin ordering. +type roundRobin struct { + robin uint32 +} + +func (r *roundRobin) String() string { return "round_robin" } + +func (r *roundRobin) List(p []*Proxy) []*Proxy { + poolLen := uint32(len(p)) + i := atomic.AddUint32(&r.robin, 1) % poolLen + + robin := []*Proxy{p[i]} + robin = append(robin, p[:i]...) + robin = append(robin, p[i+1:]...) + + return robin +} + +// sequential is a policy that selects hosts based on sequential ordering. +type sequential struct{} + +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/ag_201_coredns/plugin/grpc/proxy.go b/ag_201_coredns/plugin/grpc/proxy.go new file mode 100644 index 0000000..9a96e95 --- /dev/null +++ b/ag_201_coredns/plugin/grpc/proxy.go @@ -0,0 +1,82 @@ +package grpc + +import ( + "context" + "crypto/tls" + "strconv" + "time" + + "github.com/coredns/coredns/pb" + + "github.com/miekg/dns" + "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" +) + +// Proxy defines an upstream host. +type Proxy struct { + addr string + + // connection + client pb.DnsServiceClient + dialOpts []grpc.DialOption +} + +// newProxy returns a new proxy. +func newProxy(addr string, tlsConfig *tls.Config) (*Proxy, error) { + p := &Proxy{ + addr: addr, + } + + if tlsConfig != nil { + p.dialOpts = append(p.dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) + } else { + p.dialOpts = append(p.dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + + conn, err := grpc.Dial(p.addr, p.dialOpts...) + if err != nil { + return nil, err + } + p.client = pb.NewDnsServiceClient(conn) + + return p, nil +} + +// query sends the request and waits for a response. +func (p *Proxy) query(ctx context.Context, req *dns.Msg) (*dns.Msg, error) { + start := time.Now() + + msg, err := req.Pack() + if 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 + if status.Code(err) == codes.NotFound { + m := new(dns.Msg).SetRcode(req, dns.RcodeNameError) + return m, nil + } + return nil, err + } + ret := new(dns.Msg) + if err := ret.Unpack(reply.Msg); err != nil { + return nil, err + } + + rc, ok := dns.RcodeToString[ret.Rcode] + if !ok { + rc = strconv.Itoa(ret.Rcode) + } + + RequestCount.WithLabelValues(p.addr).Add(1) + RcodeCount.WithLabelValues(rc, p.addr).Add(1) + RequestDuration.WithLabelValues(p.addr).Observe(time.Since(start).Seconds()) + + return ret, nil +} diff --git a/ag_201_coredns/plugin/grpc/proxy_test.go b/ag_201_coredns/plugin/grpc/proxy_test.go new file mode 100644 index 0000000..cc4ebec --- /dev/null +++ b/ag_201_coredns/plugin/grpc/proxy_test.go @@ -0,0 +1,66 @@ +package grpc + +import ( + "context" + "errors" + "testing" + + "github.com/coredns/coredns/pb" + + "github.com/miekg/dns" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestProxy(t *testing.T) { + tests := map[string]struct { + p *Proxy + res *dns.Msg + wantErr bool + }{ + "response_ok": { + p: &Proxy{}, + res: &dns.Msg{}, + wantErr: false, + }, + "nil_response": { + p: &Proxy{}, + res: nil, + wantErr: true, + }, + "tls": { + p: &Proxy{dialOpts: []grpc.DialOption{grpc.WithTransportCredentials(credentials.NewTLS(nil))}}, + res: &dns.Msg{}, + wantErr: false, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + var mock *testServiceClient + if tt.res != nil { + msg, err := tt.res.Pack() + if err != nil { + t.Fatalf("Error packing response: %s", err.Error()) + } + mock = &testServiceClient{&pb.DnsPacket{Msg: msg}, nil} + } else { + mock = &testServiceClient{nil, errors.New("server error")} + } + tt.p.client = mock + + _, err := tt.p.query(context.TODO(), new(dns.Msg)) + if err != nil && !tt.wantErr { + t.Fatalf("Error query(): %s", err.Error()) + } + }) + } +} + +type testServiceClient struct { + dnsPacket *pb.DnsPacket + err error +} + +func (m testServiceClient) Query(ctx context.Context, in *pb.DnsPacket, opts ...grpc.CallOption) (*pb.DnsPacket, error) { + return m.dnsPacket, m.err +} diff --git a/ag_201_coredns/plugin/grpc/setup.go b/ag_201_coredns/plugin/grpc/setup.go new file mode 100644 index 0000000..48a3d2c --- /dev/null +++ b/ag_201_coredns/plugin/grpc/setup.go @@ -0,0 +1,147 @@ +package grpc + +import ( + "crypto/tls" + "fmt" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/parse" + pkgtls "github.com/coredns/coredns/plugin/pkg/tls" +) + +func init() { plugin.Register("grpc", setup) } + +func setup(c *caddy.Controller) error { + g, err := parseGRPC(c) + if err != nil { + return plugin.Error("grpc", err) + } + + if g.len() > max { + return plugin.Error("grpc", fmt.Errorf("more than %d TOs configured: %d", max, g.len())) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + g.Next = next // Set the Next field, so the plugin chaining works. + return g + }) + + return nil +} + +func parseGRPC(c *caddy.Controller) (*GRPC, error) { + var ( + g *GRPC + err error + i int + ) + for c.Next() { + if i > 0 { + return nil, plugin.ErrOnce + } + i++ + g, err = parseStanza(c) + if err != nil { + return nil, err + } + } + return g, nil +} + +func parseStanza(c *caddy.Controller) (*GRPC, error) { + g := newGRPC() + + if !c.Args(&g.from) { + return g, c.ArgErr() + } + 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 { + return g, c.ArgErr() + } + + toHosts, err := parse.HostPortOrFile(to...) + if err != nil { + return g, err + } + + for c.NextBlock() { + if err := parseBlock(c, g); err != nil { + return g, err + } + } + + if g.tlsServerName != "" { + if g.tlsConfig == nil { + g.tlsConfig = new(tls.Config) + } + g.tlsConfig.ServerName = g.tlsServerName + } + for _, host := range toHosts { + pr, err := newProxy(host, g.tlsConfig) + if err != nil { + return nil, err + } + g.proxies = append(g.proxies, pr) + } + + return g, nil +} + +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++ { + g.ignored = append(g.ignored, plugin.Host(ignore[i]).NormalizeExact()...) + } + case "tls": + args := c.RemainingArgs() + if len(args) > 3 { + return c.ArgErr() + } + + tlsConfig, err := pkgtls.NewTLSConfigFromArgs(args...) + if err != nil { + return err + } + g.tlsConfig = tlsConfig + case "tls_servername": + if !c.NextArg() { + return c.ArgErr() + } + g.tlsServerName = c.Val() + case "policy": + if !c.NextArg() { + return c.ArgErr() + } + switch x := c.Val(); x { + case "random": + g.p = &random{} + case "round_robin": + g.p = &roundRobin{} + case "sequential": + g.p = &sequential{} + default: + return c.Errf("unknown policy '%s'", x) + } + default: + if c.Val() != "}" { + return c.Errf("unknown property '%s'", c.Val()) + } + } + + return nil +} + +const max = 15 // Maximum number of upstreams. diff --git a/ag_201_coredns/plugin/grpc/setup_policy_test.go b/ag_201_coredns/plugin/grpc/setup_policy_test.go new file mode 100644 index 0000000..c13339d --- /dev/null +++ b/ag_201_coredns/plugin/grpc/setup_policy_test.go @@ -0,0 +1,47 @@ +package grpc + +import ( + "strings" + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupPolicy(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedPolicy string + expectedErr string + }{ + // positive + {"grpc . 127.0.0.1 {\npolicy random\n}\n", false, "random", ""}, + {"grpc . 127.0.0.1 {\npolicy round_robin\n}\n", false, "round_robin", ""}, + {"grpc . 127.0.0.1 {\npolicy sequential\n}\n", false, "sequential", ""}, + // negative + {"grpc . 127.0.0.1 {\npolicy random2\n}\n", true, "random", "unknown policy"}, + } + + 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 %s for input %s", i, err, 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.p.String() != test.expectedPolicy { + t.Errorf("Test %d: expected: %s, got: %s", i, test.expectedPolicy, g.p.String()) + } + } +} diff --git a/ag_201_coredns/plugin/grpc/setup_test.go b/ag_201_coredns/plugin/grpc/setup_test.go new file mode 100644 index 0000000..1d9e93b --- /dev/null +++ b/ag_201_coredns/plugin/grpc/setup_test.go @@ -0,0 +1,153 @@ +package grpc + +import ( + "os" + "reflect" + "strings" + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedFrom string + expectedIgnored []string + expectedErr string + }{ + // positive + {"grpc . 127.0.0.1", false, ".", nil, ""}, + {"grpc . 127.0.0.1 {\nexcept miek.nl\n}\n", false, ".", nil, ""}, + {"grpc . 127.0.0.1", false, ".", nil, ""}, + {"grpc . 127.0.0.1:53", false, ".", nil, ""}, + {"grpc . 127.0.0.1:8080", false, ".", nil, ""}, + {"grpc . [::1]:53", false, ".", nil, ""}, + {"grpc . [2003::1]:53", 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 { + c := caddy.NewTestController("grpc", test.input) + g, err := parseGRPC(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: expected error but found %s for input %s", i, err, 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.from != test.expectedFrom { + t.Errorf("Test %d: expected: %s, got: %s", i, test.expectedFrom, g.from) + } + if !test.shouldErr && test.expectedIgnored != nil { + if !reflect.DeepEqual(g.ignored, test.expectedIgnored) { + t.Errorf("Test %d: expected: %q, actual: %q", i, test.expectedIgnored, g.ignored) + } + } + } +} + +func TestSetupTLS(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedServerName string + expectedErr string + }{ + // positive + {`grpc . 127.0.0.1 { +tls_servername dns +}`, false, "dns", ""}, + {`grpc . 127.0.0.1 { +tls +}`, false, "", ""}, + {`grpc . 127.0.0.1`, false, "", ""}, + } + + 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 %s for input %s", i, err, 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 && test.expectedServerName != "" && g.tlsConfig != nil && test.expectedServerName != g.tlsConfig.ServerName { + t.Errorf("Test %d: expected: %q, actual: %q", i, test.expectedServerName, g.tlsConfig.ServerName) + } + } +} + +func TestSetupResolvconf(t *testing.T) { + const resolv = "resolv.conf" + 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) + + tests := []struct { + input string + shouldErr bool + expectedErr string + expectedNames []string + }{ + // pass + {`grpc . ` + resolv, false, "", []string{"10.10.255.252:53", "10.10.255.253:53"}}, + } + + for i, test := range tests { + c := caddy.NewTestController("grpc", test.input) + f, err := parseGRPC(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input) + continue + } + + 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 { + 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) + } + } + } + } +} diff --git a/ag_201_coredns/plugin/header/README.md b/ag_201_coredns/plugin/header/README.md new file mode 100644 index 0000000..9a855c7 --- /dev/null +++ b/ag_201_coredns/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`. In future CoreDNS version the selector will be mandatory. For backwards compatibility the action will be applied on `response` if the selector is undefined. + +* **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/ag_201_coredns/plugin/header/handler.go b/ag_201_coredns/plugin/header/handler.go new file mode 100644 index 0000000..e11eb03 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/plugin/header/header.go b/ag_201_coredns/plugin/header/header.go new file mode 100644 index 0000000..830587d --- /dev/null +++ b/ag_201_coredns/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) + } + + var rules []Rule + 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/ag_201_coredns/plugin/header/header_test.go b/ag_201_coredns/plugin/header/header_test.go new file mode 100644 index 0000000..1182654 --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/plugin/header/setup.go b/ag_201_coredns/plugin/header/setup.go new file mode 100644 index 0000000..3d6facf --- /dev/null +++ b/ag_201_coredns/plugin/header/setup.go @@ -0,0 +1,74 @@ +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 + if selector == "set" || selector == "clear" { + log.Warningf("The selector for header rule in line %d isn't explicit defined. "+ + "Assume rule applies for selector 'response'. This syntax is deprecated. "+ + "In future versions of CoreDNS the selector must be explicit defined.", + c.Line()) + + action = selector + selector = "response" + } else if selector == "query" || selector == "response" { + if c.NextArg() { + action = c.Val() + } + } else { + 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/ag_201_coredns/plugin/header/setup_test.go b/ag_201_coredns/plugin/header/setup_test.go new file mode 100644 index 0000000..36b7995 --- /dev/null +++ b/ag_201_coredns/plugin/header/setup_test.go @@ -0,0 +1,65 @@ +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 { + set +}`, true, "invalid length for flags, at least one should be provided"}, + {`header { + foo +}`, true, "invalid selector=foo should be query or response"}, + {`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 { + set ra +}`, false, ""}, + {`header { + clear ra + }`, false, ""}, + {`header { + query set rd + }`, false, ""}, + {`header { + response set aa + }`, false, ""}, + {`header { + set ra aa + 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/ag_201_coredns/plugin/health/README.md b/ag_201_coredns/plugin/health/README.md new file mode 100644 index 0000000..b18d2ec --- /dev/null +++ b/ag_201_coredns/plugin/health/README.md @@ -0,0 +1,80 @@ +# health + +## Name + +*health* - enables a health check endpoint. + +## Description + +Enabled process wide health endpoint. When CoreDNS is up and running this returns a 200 OK HTTP +status code. The health is exported, by default, on port 8080/health. + +## Syntax + +~~~ +health [ADDRESS] +~~~ + +Optionally takes an address; the default is `:8080`. The health path is fixed to `/health`. The +health endpoint returns a 200 response code and the word "OK" when this server is healthy. + +An extra option can be set with this extended syntax: + +~~~ +health [ADDRESS] { + lameduck DURATION +} +~~~ + +* 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 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: + +~~~ corefile +com { + whoami + health :8080 +} + +net { + erratic + health :8081 +} +~~~ + +Doing this is supported but both endpoints ":8080" and ":8081" will export the exact same health. + +## Metrics + +If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported: + + * `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 these metrics *do not* have a `server` label, because being overloaded is a symptom of +the running process, *not* a specific server. + +## Examples + +Run another health endpoint on http://localhost:8091. + +~~~ corefile +. { + health localhost:8091 +} +~~~ + +Set a lame duck duration of 1 second: + +~~~ corefile +. { + health localhost:8092 { + lameduck 1s + } +} +~~~ diff --git a/ag_201_coredns/plugin/health/health.go b/ag_201_coredns/plugin/health/health.go new file mode 100644 index 0000000..c69b221 --- /dev/null +++ b/ag_201_coredns/plugin/health/health.go @@ -0,0 +1,84 @@ +// Package health implements an HTTP handler that responds to health checks. +package health + +import ( + "context" + "io" + "net" + "net/http" + "time" + + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/reuseport" +) + +var log = clog.NewWithPlugin("health") + +// Health implements healthchecks by exporting a HTTP endpoint. +type health struct { + Addr string + lameduck time.Duration + + ln net.Listener + nlSetup bool + mux *http.ServeMux + + stop context.CancelFunc +} + +func (h *health) OnStartup() error { + if h.Addr == "" { + h.Addr = ":8080" + } + ln, err := reuseport.Listen("tcp", h.Addr) + if err != nil { + return err + } + + h.ln = ln + h.mux = http.NewServeMux() + h.nlSetup = true + + h.mux.HandleFunc("/health", 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(ctx) }() + + return nil +} + +func (h *health) OnFinalShutdown() error { + if !h.nlSetup { + return nil + } + + if h.lameduck > 0 { + log.Infof("Going into lameduck mode for %s", h.lameduck) + 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 + return nil +} diff --git a/ag_201_coredns/plugin/health/health_test.go b/ag_201_coredns/plugin/health/health_test.go new file mode 100644 index 0000000..ba6b14c --- /dev/null +++ b/ag_201_coredns/plugin/health/health_test.go @@ -0,0 +1,47 @@ +package health + +import ( + "fmt" + "io" + "net/http" + "testing" + "time" +) + +func TestHealth(t *testing.T) { + h := &health{Addr: ":0"} + + if err := h.OnStartup(); err != nil { + t.Fatalf("Unable to startup the health server: %v", err) + } + defer h.OnFinalShutdown() + + address := fmt.Sprintf("http://%s%s", h.ln.Addr().String(), "/health") + + response, err := http.Get(address) + if err != nil { + t.Fatalf("Unable to query %s: %v", address, err) + } + if response.StatusCode != 200 { + t.Errorf("Invalid status code: expecting '200', got '%d'", response.StatusCode) + } + content, err := io.ReadAll(response.Body) + if err != nil { + t.Fatalf("Unable to get response body from %s: %v", address, err) + } + response.Body.Close() + + if string(content) != http.StatusText(http.StatusOK) { + t.Errorf("Invalid response body: expecting 'OK', got '%s'", string(content)) + } +} + +func TestHealthLameduck(t *testing.T) { + h := &health{Addr: ":0", lameduck: 250 * time.Millisecond} + + if err := h.OnStartup(); err != nil { + t.Fatalf("Unable to startup the health server: %v", err) + } + + h.OnFinalShutdown() +} diff --git a/ag_201_coredns/plugin/health/log_test.go b/ag_201_coredns/plugin/health/log_test.go new file mode 100644 index 0000000..7e6c97b --- /dev/null +++ b/ag_201_coredns/plugin/health/log_test.go @@ -0,0 +1,5 @@ +package health + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/health/overloaded.go b/ag_201_coredns/plugin/health/overloaded.go new file mode 100644 index 0000000..57b9ca2 --- /dev/null +++ b/ag_201_coredns/plugin/health/overloaded.go @@ -0,0 +1,84 @@ +package health + +import ( + "context" + "net" + "net/http" + "time" + + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// overloaded queries the health end point and updates a metrics showing how long it took. +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, + Transport: bypassProxy, + } + + url := "http://" + h.Addr + "/health" + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + tick := time.NewTicker(1 * time.Second) + defer tick.Stop() + + for { + select { + case <-tick.C: + start := time.Now() + resp, err := client.Do(req) + if err != nil && ctx.Err() == context.Canceled { + // request was cancelled by parent goroutine + return + } + if err != nil { + HealthDuration.Observe(time.Since(start).Seconds()) + HealthFailures.Inc() + log.Warningf("Local health request to %q failed: %s", url, err) + continue + } + resp.Body.Close() + 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", url, elapsed) + } + + case <-ctx.Done(): + return + } + } +} + +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, + 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_failures_total", + Help: "The number of times the health check failed.", + }) +) diff --git a/ag_201_coredns/plugin/health/overloaded_test.go b/ag_201_coredns/plugin/health/overloaded_test.go new file mode 100644 index 0000000..c927f13 --- /dev/null +++ b/ag_201_coredns/plugin/health/overloaded_test.go @@ -0,0 +1,41 @@ +package health + +import ( + "context" + "net/http" + "net/http/httptest" + "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, + } + + 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/ag_201_coredns/plugin/health/setup.go b/ag_201_coredns/plugin/health/setup.go new file mode 100644 index 0000000..e9163ad --- /dev/null +++ b/ag_201_coredns/plugin/health/setup.go @@ -0,0 +1,66 @@ +package health + +import ( + "fmt" + "net" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("health", setup) } + +func setup(c *caddy.Controller) error { + addr, lame, err := parse(c) + if err != nil { + return plugin.Error("health", err) + } + + h := &health{Addr: addr, lameduck: lame} + + c.OnStartup(h.OnStartup) + c.OnRestart(h.OnReload) + c.OnFinalShutdown(h.OnFinalShutdown) + c.OnRestartFailed(h.OnStartup) + + // Don't do AddPlugin, as health is not *really* a plugin just a separate webserver running. + return nil +} + +func parse(c *caddy.Controller) (string, time.Duration, error) { + addr := "" + dur := time.Duration(0) + for c.Next() { + args := c.RemainingArgs() + + switch len(args) { + case 0: + case 1: + addr = args[0] + if _, _, e := net.SplitHostPort(addr); e != nil { + return "", 0, e + } + default: + return "", 0, c.ArgErr() + } + + for c.NextBlock() { + switch c.Val() { + case "lameduck": + args := c.RemainingArgs() + if len(args) != 1 { + return "", 0, c.ArgErr() + } + l, err := time.ParseDuration(args[0]) + if err != nil { + return "", 0, fmt.Errorf("unable to parse lameduck duration value: '%v' : %v", args[0], err) + } + dur = l + default: + return "", 0, c.ArgErr() + } + } + } + return addr, dur, nil +} diff --git a/ag_201_coredns/plugin/health/setup_test.go b/ag_201_coredns/plugin/health/setup_test.go new file mode 100644 index 0000000..7bb2132 --- /dev/null +++ b/ag_201_coredns/plugin/health/setup_test.go @@ -0,0 +1,45 @@ +package health + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupHealth(t *testing.T) { + tests := []struct { + input string + shouldErr bool + }{ + {`health`, false}, + {`health localhost:1234`, false}, + {`health localhost:1234 { + lameduck 4s +}`, false}, + {`health bla:a`, false}, + + {`health bla`, true}, + {`health bla bla`, true}, + {`health localhost:1234 { + lameduck a +}`, true}, + {`health localhost:1234 { + lamedudk 4 +} `, true}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + _, _, err := parse(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) + } + } + } +} diff --git a/ag_201_coredns/plugin/hosts/README.md b/ag_201_coredns/plugin/hosts/README.md new file mode 100644 index 0000000..dde6259 --- /dev/null +++ b/ag_201_coredns/plugin/hosts/README.md @@ -0,0 +1,125 @@ +# hosts + +## Name + +*hosts* - enables serving zone data from a `/etc/hosts` style file. + +## Description + +The *hosts* plugin is useful for serving zones from a `/etc/hosts` file. It serves from a preloaded +file that exists on disk. It checks the file for changes and updates the zones accordingly. This +plugin only supports A, AAAA, and PTR records. The hosts plugin can be used with readily +available hosts files that block access to advertising servers. + +The plugin reloads the content of the hosts file every 5 seconds. Upon reload, CoreDNS will use the +new definitions. Should the file be deleted, any inlined content will continue to be served. When +the file is restored, it will then again be used. + +If you want to pass the request to the rest of the plugin chain if there is no match in the *hosts* +plugin, you must specify the `fallthrough` option. + +This plugin can only be used once per Server Block. + +## The hosts file + +Commonly the entries are of the form `IP_address canonical_hostname [aliases...]` as explained by +the hosts(5) man page. + +Examples: + +~~~ +# The following lines are desirable for IPv4 capable hosts +127.0.0.1 localhost +192.168.1.10 example.com example + +# The following lines are desirable for IPv6 capable hosts +::1 localhost ip6-localhost ip6-loopback +fdfc:a744:27b5:3b0e::1 example.com example +~~~ + +### PTR records + +PTR records for reverse lookups are generated automatically by CoreDNS (based on the hosts file +entries) and cannot be created manually. + +## Syntax + +~~~ +hosts [FILE [ZONES...]] { + [INLINE] + ttl SECONDS + no_reverse + reload DURATION + fallthrough [ZONES...] +} +~~~ + +* **FILE** the hosts file to read and parse. If the path is relative the path from the *root* + plugin will be prepended to it. Defaults to /etc/hosts if omitted. We scan the file for changes + every 5 seconds. +* **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block + are used. +* **INLINE** the hosts file contents inlined in Corefile. If there are any lines before fallthrough + then all of them will be treated as the additional content for hosts file. The specified hosts + file path will still be read but entries will be overridden. +* `ttl` change the DNS TTL of the records generated (forward and reverse). The default is 3600 seconds (1 hour). +* `reload` change the period between each hostsfile reload. A time of zero seconds disables the + feature. Examples of valid durations: "300ms", "1.5h" or "2h45m". See Go's + [time](https://godoc.org/time). package. +* `no_reverse` disable the automatic generation of the `in-addr.arpa` or `ip6.arpa` entries for the hosts +* `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. + +## Metrics + +If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported: + +- `coredns_hosts_entries{}` - The combined number of entries in hosts and Corefile. +- `coredns_hosts_reload_timestamp_seconds{}` - The timestamp of the last reload of hosts file. + +## Examples + +Load `/etc/hosts` file. + +~~~ corefile +. { + hosts +} +~~~ + +Load `example.hosts` file in the current directory. + +~~~ +. { + hosts example.hosts +} +~~~ + +Load example.hosts file and only serve example.org and example.net from it and fall through to the +next plugin if query doesn't match. + +~~~ +. { + hosts example.hosts example.org example.net { + fallthrough + } +} +~~~ + +Load hosts file inlined in Corefile. + +~~~ +example.hosts example.org { + hosts { + 10.0.0.1 example.org + fallthrough + } + whoami +} +~~~ + +## See also + +The form of the entries in the `/etc/hosts` file are based on IETF [RFC 952](https://tools.ietf.org/html/rfc952) which was updated by IETF [RFC 1123](https://tools.ietf.org/html/rfc1123). diff --git a/ag_201_coredns/plugin/hosts/hosts.go b/ag_201_coredns/plugin/hosts/hosts.go new file mode 100644 index 0000000..5c644e7 --- /dev/null +++ b/ag_201_coredns/plugin/hosts/hosts.go @@ -0,0 +1,122 @@ +package hosts + +import ( + "context" + "net" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnsutil" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Hosts is the plugin handler +type Hosts struct { + Next plugin.Handler + *Hostsfile + + Fall fall.F +} + +// ServeDNS implements the plugin.Handle interface. +func (h Hosts) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.Name() + + answers := []dns.RR{} + + zone := plugin.Zones(h.Origins).Matches(qname) + if zone == "" { + // PTR zones don't need to be specified in Origins. + if state.QType() != dns.TypePTR { + // if this doesn't match we need to fall through regardless of h.Fallthrough + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + } + + switch state.QType() { + case dns.TypePTR: + names := h.LookupStaticAddr(dnsutil.ExtractAddressFromReverse(qname)) + if len(names) == 0 { + // If this doesn't match we need to fall through regardless of h.Fallthrough + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + answers = h.ptr(qname, h.options.ttl, names) + case dns.TypeA: + ips := h.LookupStaticHostV4(qname) + answers = a(qname, h.options.ttl, ips) + case dns.TypeAAAA: + ips := h.LookupStaticHostV6(qname) + answers = aaaa(qname, h.options.ttl, ips) + } + + // Only on NXDOMAIN we will fallthrough. + if len(answers) == 0 && !h.otherRecordsExist(qname) { + if h.Fall.Through(qname) { + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + + // We want to send an NXDOMAIN, but because of /etc/hosts' setup we don't have a SOA, so we make it SERVFAIL + // to at least give an answer back to signals we're having problems resolving this. + return dns.RcodeServerFailure, nil + } + + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative = true + m.Answer = answers + + w.WriteMsg(m) + return dns.RcodeSuccess, nil +} + +func (h Hosts) otherRecordsExist(qname string) bool { + if len(h.LookupStaticHostV4(qname)) > 0 { + return true + } + if len(h.LookupStaticHostV6(qname)) > 0 { + return true + } + return false +} + +// Name implements the plugin.Handle interface. +func (h Hosts) Name() string { return "hosts" } + +// a takes a slice of net.IPs and returns a slice of A RRs. +func a(zone string, ttl uint32, ips []net.IP) []dns.RR { + answers := make([]dns.RR, len(ips)) + for i, ip := range ips { + r := new(dns.A) + r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: ttl} + r.A = ip + answers[i] = r + } + return answers +} + +// aaaa takes a slice of net.IPs and returns a slice of AAAA RRs. +func aaaa(zone string, ttl uint32, ips []net.IP) []dns.RR { + answers := make([]dns.RR, len(ips)) + for i, ip := range ips { + r := new(dns.AAAA) + r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: ttl} + r.AAAA = ip + answers[i] = r + } + return answers +} + +// ptr takes a slice of host names and filters out the ones that aren't in Origins, if specified, and returns a slice of PTR RRs. +func (h *Hosts) ptr(zone string, ttl uint32, names []string) []dns.RR { + answers := make([]dns.RR, len(names)) + for i, n := range names { + r := new(dns.PTR) + r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: ttl} + r.Ptr = dns.Fqdn(n) + answers[i] = r + } + return answers +} diff --git a/ag_201_coredns/plugin/hosts/hosts_test.go b/ag_201_coredns/plugin/hosts/hosts_test.go new file mode 100644 index 0000000..320655a --- /dev/null +++ b/ag_201_coredns/plugin/hosts/hosts_test.go @@ -0,0 +1,120 @@ +package hosts + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestLookupA(t *testing.T) { + for _, tc := range hostsTestCases { + m := tc.Msg() + + var tcFall fall.F + isFall := tc.Qname == "fallthrough-example.org." + if isFall { + tcFall = fall.Root + } else { + tcFall = fall.Zero + } + + h := Hosts{ + Next: test.NextHandler(dns.RcodeNameError, nil), + Hostsfile: &Hostsfile{ + Origins: []string{"."}, + hmap: newMap(), + inline: newMap(), + options: newOptions(), + }, + Fall: tcFall, + } + h.hmap = h.parse(strings.NewReader(hostsExample)) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + rcode, err := h.ServeDNS(context.Background(), rec, m) + if err != nil { + t.Errorf("Expected no error, got %v", err) + return + } + + if isFall && tc.Rcode != rcode { + t.Errorf("Expected rcode is %d, but got %d", tc.Rcode, rcode) + return + } + + if resp := rec.Msg; rec.Msg != nil { + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } + } +} + +var hostsTestCases = []test.Case{ + { + Qname: "example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("example.org. 3600 IN A 10.0.0.1"), + }, + }, + { + Qname: "example.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("example.com. 3600 IN A 10.0.0.2"), + }, + }, + { + Qname: "localhost.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.AAAA("localhost. 3600 IN AAAA ::1"), + }, + }, + { + Qname: "1.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR, + Answer: []dns.RR{ + test.PTR("1.0.0.10.in-addr.arpa. 3600 PTR example.org."), + }, + }, + { + Qname: "2.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR, + Answer: []dns.RR{ + test.PTR("2.0.0.10.in-addr.arpa. 3600 PTR example.com."), + }, + }, + { + Qname: "1.0.0.127.in-addr.arpa.", Qtype: dns.TypePTR, + Answer: []dns.RR{ + test.PTR("1.0.0.127.in-addr.arpa. 3600 PTR localhost."), + test.PTR("1.0.0.127.in-addr.arpa. 3600 PTR localhost.domain."), + }, + }, + { + Qname: "example.org.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{}, + }, + { + Qname: "example.org.", Qtype: dns.TypeMX, + Answer: []dns.RR{}, + }, + { + Qname: "fallthrough-example.org.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{}, Rcode: dns.RcodeSuccess, + }, +} + +const hostsExample = ` +127.0.0.1 localhost localhost.domain +::1 localhost localhost.domain +10.0.0.1 example.org +::FFFF:10.0.0.2 example.com +10.0.0.3 fallthrough-example.org +reload 5s +timeout 3600 +` diff --git a/ag_201_coredns/plugin/hosts/hostsfile.go b/ag_201_coredns/plugin/hosts/hostsfile.go new file mode 100644 index 0000000..cf3c43c --- /dev/null +++ b/ag_201_coredns/plugin/hosts/hostsfile.go @@ -0,0 +1,259 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file is a modified version of net/hosts.go from the golang repo + +package hosts + +import ( + "bufio" + "bytes" + "io" + "net" + "os" + "strings" + "sync" + "time" + + "github.com/coredns/coredns/plugin" +) + +// parseIP calls discards any v6 zone info, before calling net.ParseIP. +func parseIP(addr string) net.IP { + if i := strings.Index(addr, "%"); i >= 0 { + // discard ipv6 zone + addr = addr[0:i] + } + + return net.ParseIP(addr) +} + +type options struct { + // automatically generate IP to Hostname PTR entries + // for host entries we parse + autoReverse bool + + // The TTL of the record we generate + ttl uint32 + + // The time between two reload of the configuration + reload time.Duration +} + +func newOptions() *options { + return &options{ + autoReverse: true, + ttl: 3600, + reload: time.Duration(5 * time.Second), + } +} + +// Map contains the IPv4/IPv6 and reverse mapping. +type Map struct { + // Key for the list of literal IP addresses must be a FQDN lowercased host name. + name4 map[string][]net.IP + name6 map[string][]net.IP + + // Key for the list of host names must be a literal IP address + // including IPv6 address without zone identifier. + // We don't support old-classful IP address notation. + addr map[string][]string +} + +func newMap() *Map { + return &Map{ + name4: make(map[string][]net.IP), + name6: make(map[string][]net.IP), + addr: make(map[string][]string), + } +} + +// Len returns the total number of addresses in the hostmap, this includes V4/V6 and any reverse addresses. +func (h *Map) Len() int { + l := 0 + for _, v4 := range h.name4 { + l += len(v4) + } + for _, v6 := range h.name6 { + l += len(v6) + } + for _, a := range h.addr { + l += len(a) + } + return l +} + +// Hostsfile contains known host entries. +type Hostsfile struct { + sync.RWMutex + + // list of zones we are authoritative for + Origins []string + + // hosts maps for lookups + hmap *Map + + // inline saves the hosts file that is inlined in a Corefile. + inline *Map + + // path to the hosts file + path string + + // mtime and size are only read and modified by a single goroutine + mtime time.Time + size int64 + + options *options +} + +// readHosts determines if the cached data needs to be updated based on the size and modification time of the hostsfile. +func (h *Hostsfile) readHosts() { + file, err := os.Open(h.path) + if err != nil { + // We already log a warning if the file doesn't exist or can't be opened on setup. No need to return the error here. + return + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return + } + h.RLock() + size := h.size + h.RUnlock() + + if h.mtime.Equal(stat.ModTime()) && size == stat.Size() { + return + } + + newMap := h.parse(file) + log.Debugf("Parsed hosts file into %d entries", newMap.Len()) + + h.Lock() + + h.hmap = newMap + // Update the data cache. + h.mtime = stat.ModTime() + h.size = stat.Size() + + hostsEntries.WithLabelValues().Set(float64(h.inline.Len() + h.hmap.Len())) + hostsReloadTime.Set(float64(stat.ModTime().UnixNano()) / 1e9) + h.Unlock() +} + +func (h *Hostsfile) initInline(inline []string) { + if len(inline) == 0 { + return + } + + h.inline = h.parse(strings.NewReader(strings.Join(inline, "\n"))) +} + +// Parse reads the hostsfile and populates the byName and addr maps. +func (h *Hostsfile) parse(r io.Reader) *Map { + hmap := newMap() + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Bytes() + if i := bytes.Index(line, []byte{'#'}); i >= 0 { + // Discard comments. + line = line[0:i] + } + f := bytes.Fields(line) + if len(f) < 2 { + continue + } + addr := parseIP(string(f[0])) + if addr == nil { + continue + } + + family := 0 + if addr.To4() != nil { + family = 1 + } else { + family = 2 + } + + for i := 1; i < len(f); i++ { + name := plugin.Name(string(f[i])).Normalize() + if plugin.Zones(h.Origins).Matches(name) == "" { + // name is not in Origins + continue + } + switch family { + case 1: + hmap.name4[name] = append(hmap.name4[name], addr) + case 2: + hmap.name6[name] = append(hmap.name6[name], addr) + default: + continue + } + if !h.options.autoReverse { + continue + } + hmap.addr[addr.String()] = append(hmap.addr[addr.String()], name) + } + } + + return hmap +} + +// lookupStaticHost looks up the IP addresses for the given host from the hosts file. +func (h *Hostsfile) lookupStaticHost(m map[string][]net.IP, host string) []net.IP { + h.RLock() + defer h.RUnlock() + + if len(m) == 0 { + return nil + } + + ips, ok := m[host] + if !ok { + return nil + } + ipsCp := make([]net.IP, len(ips)) + copy(ipsCp, ips) + return ipsCp +} + +// LookupStaticHostV4 looks up the IPv4 addresses for the given host from the hosts file. +func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP { + host = strings.ToLower(host) + ip1 := h.lookupStaticHost(h.hmap.name4, host) + ip2 := h.lookupStaticHost(h.inline.name4, host) + return append(ip1, ip2...) +} + +// LookupStaticHostV6 looks up the IPv6 addresses for the given host from the hosts file. +func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP { + host = strings.ToLower(host) + ip1 := h.lookupStaticHost(h.hmap.name6, host) + ip2 := h.lookupStaticHost(h.inline.name6, host) + return append(ip1, ip2...) +} + +// LookupStaticAddr looks up the hosts for the given address from the hosts file. +func (h *Hostsfile) LookupStaticAddr(addr string) []string { + addr = parseIP(addr).String() + if addr == "" { + return nil + } + + h.RLock() + defer h.RUnlock() + hosts1 := h.hmap.addr[addr] + hosts2 := h.inline.addr[addr] + + if len(hosts1) == 0 && len(hosts2) == 0 { + return nil + } + + hostsCp := make([]string, len(hosts1)+len(hosts2)) + copy(hostsCp, hosts1) + copy(hostsCp[len(hosts1):], hosts2) + return hostsCp +} diff --git a/ag_201_coredns/plugin/hosts/hostsfile_test.go b/ag_201_coredns/plugin/hosts/hostsfile_test.go new file mode 100644 index 0000000..05b064e --- /dev/null +++ b/ag_201_coredns/plugin/hosts/hostsfile_test.go @@ -0,0 +1,241 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package hosts + +import ( + "net" + "reflect" + "strings" + "testing" + + "github.com/coredns/coredns/plugin" +) + +func testHostsfile(file string) *Hostsfile { + h := &Hostsfile{ + Origins: []string{"."}, + hmap: newMap(), + inline: newMap(), + options: newOptions(), + } + h.hmap = h.parse(strings.NewReader(file)) + return h +} + +type staticHostEntry struct { + in string + v4 []string + v6 []string +} + +var ( + hosts = `255.255.255.255 broadcasthost + 127.0.0.2 odin + 127.0.0.3 odin # inline comment + ::2 odin + 127.1.1.1 thor + # aliases + 127.1.1.2 ullr ullrhost + fe80::1%lo0 localhost + # Bogus entries that must be ignored. + 123.123.123 loki + 321.321.321.321` + singlelinehosts = `127.0.0.2 odin` + ipv4hosts = `# See https://tools.ietf.org/html/rfc1123. + # + + # internet address and host name + 127.0.0.1 localhost # inline comment separated by tab + 127.0.0.2 localhost # inline comment separated by space + + # internet address, host name and aliases + 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 + ::1 localhost # inline comment separated by tab + fe80:0000:0000:0000:0000:0000:0000:0001 localhost # inline comment separated by space + + # internet address with zone identifier and host name + fe80:0000:0000:0000:0000:0000:0000:0002%lo0 localhost + + # internet address, host name and aliases + fe80::3%lo0 localhost localhost.localdomain` + casehosts = `127.0.0.1 PreserveMe PreserveMe.local + ::1 PreserveMe PreserveMe.local` +) + +var lookupStaticHostTests = []struct { + file string + ents []staticHostEntry +}{ + { + hosts, + []staticHostEntry{ + {"odin.", []string{"127.0.0.2", "127.0.0.3"}, []string{"::2"}}, + {"thor.", []string{"127.1.1.1"}, []string{}}, + {"ullr.", []string{"127.1.1.2"}, []string{}}, + {"ullrhost.", []string{"127.1.1.2"}, []string{}}, + {"localhost.", []string{}, []string{"fe80::1"}}, + }, + }, + { + singlelinehosts, // see golang.org/issue/6646 + []staticHostEntry{ + {"odin.", []string{"127.0.0.2"}, []string{}}, + }, + }, + { + ipv4hosts, + []staticHostEntry{ + {"localhost.", []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, []string{}}, + {"localhost.localdomain.", []string{"127.0.0.3"}, []string{}}, + }, + }, + { + ipv6hosts, + []staticHostEntry{ + {"localhost.", []string{}, []string{"::1", "fe80::1", "fe80::2", "fe80::3"}}, + {"localhost.localdomain.", []string{}, []string{"fe80::3"}}, + }, + }, + { + casehosts, + []staticHostEntry{ + {"PreserveMe.", []string{"127.0.0.1"}, []string{"::1"}}, + {"PreserveMe.local.", []string{"127.0.0.1"}, []string{"::1"}}, + }, + }, +} + +func TestLookupStaticHost(t *testing.T) { + for _, tt := range lookupStaticHostTests { + h := testHostsfile(tt.file) + for _, ent := range tt.ents { + testStaticHost(t, ent, h) + } + } +} + +func testStaticHost(t *testing.T, ent staticHostEntry, h *Hostsfile) { + 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) + if len(addrsV4) != len(ent.v4) { + t.Fatalf("%d, lookupStaticHostV4(%s) = %v; want %v", k, in, addrsV4, ent.v4) + } + for i, v4 := range addrsV4 { + if v4.String() != ent.v4[i] { + t.Fatalf("%d, lookupStaticHostV4(%s) = %v; want %v", k, in, addrsV4, ent.v4) + } + } + addrsV6 := h.LookupStaticHostV6(in) + if len(addrsV6) != len(ent.v6) { + t.Fatalf("%d, lookupStaticHostV6(%s) = %v; want %v", k, in, addrsV6, ent.v6) + } + for i, v6 := range addrsV6 { + if v6.String() != ent.v6[i] { + t.Fatalf("%d, lookupStaticHostV6(%s) = %v; want %v", k, in, addrsV6, ent.v6) + } + } + } +} + +type staticIPEntry struct { + in string + out []string +} + +var lookupStaticAddrTests = []struct { + file string + ents []staticIPEntry +}{ + { + hosts, + []staticIPEntry{ + {"255.255.255.255", []string{"broadcasthost."}}, + {"127.0.0.2", []string{"odin."}}, + {"127.0.0.3", []string{"odin."}}, + {"::2", []string{"odin."}}, + {"127.1.1.1", []string{"thor."}}, + {"127.1.1.2", []string{"ullr.", "ullrhost."}}, + {"fe80::1", []string{"localhost."}}, + }, + }, + { + singlelinehosts, // see golang.org/issue/6646 + []staticIPEntry{ + {"127.0.0.2", []string{"odin."}}, + }, + }, + { + ipv4hosts, // see golang.org/issue/8996 + []staticIPEntry{ + {"127.0.0.1", []string{"localhost."}}, + {"127.0.0.2", []string{"localhost."}}, + {"127.0.0.3", []string{"localhost.", "localhost.localdomain."}}, + }, + }, + { + ipv6hosts, // see golang.org/issue/8996 + []staticIPEntry{ + {"::1", []string{"localhost."}}, + {"fe80::1", []string{"localhost."}}, + {"fe80::2", []string{"localhost."}}, + {"fe80::3", []string{"localhost.", "localhost.localdomain."}}, + }, + }, + { + casehosts, // see golang.org/issue/12806 + []staticIPEntry{ + {"127.0.0.1", []string{"PreserveMe.", "PreserveMe.local."}}, + {"::1", []string{"PreserveMe.", "PreserveMe.local."}}, + }, + }, +} + +func TestLookupStaticAddr(t *testing.T) { + for _, tt := range lookupStaticAddrTests { + h := testHostsfile(tt.file) + for _, ent := range tt.ents { + testStaticAddr(t, ent, h) + } + } +} + +func testStaticAddr(t *testing.T, ent staticIPEntry, h *Hostsfile) { + hosts := h.LookupStaticAddr(ent.in) + for i := range ent.out { + ent.out[i] = plugin.Name(ent.out[i]).Normalize() + } + if !reflect.DeepEqual(hosts, ent.out) { + t.Errorf("%s, lookupStaticAddr(%s) = %v; want %v", h.path, ent.in, hosts, h) + } +} + +func TestHostCacheModification(t *testing.T) { + // Ensure that programs can't modify the internals of the host cache. + // See https://github.com/golang/go/issues/14212. + + h := testHostsfile(ipv4hosts) + ent := staticHostEntry{"localhost.", []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, []string{}} + testStaticHost(t, ent, h) + // Modify the addresses return by lookupStaticHost. + addrs := h.LookupStaticHostV6(ent.in) + for i := range addrs { + addrs[i] = net.IPv4zero + } + testStaticHost(t, ent, h) + + h = testHostsfile(ipv6hosts) + entip := staticIPEntry{"::1", []string{"localhost."}} + testStaticAddr(t, entip, h) + // Modify the hosts return by lookupStaticAddr. + hosts := h.LookupStaticAddr(entip.in) + for i := range hosts { + hosts[i] += "junk" + } + testStaticAddr(t, entip, h) +} diff --git a/ag_201_coredns/plugin/hosts/log_test.go b/ag_201_coredns/plugin/hosts/log_test.go new file mode 100644 index 0000000..e784bd6 --- /dev/null +++ b/ag_201_coredns/plugin/hosts/log_test.go @@ -0,0 +1,5 @@ +package hosts + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/hosts/metrics.go b/ag_201_coredns/plugin/hosts/metrics.go new file mode 100644 index 0000000..f97497b --- /dev/null +++ b/ag_201_coredns/plugin/hosts/metrics.go @@ -0,0 +1,25 @@ +package hosts + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // hostsEntries is the combined number of entries in hosts and Corefile. + hostsEntries = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: plugin.Namespace, + Subsystem: "hosts", + Name: "entries", + Help: "The combined number of entries in hosts and Corefile.", + }, []string{}) + // hostsReloadTime is the timestamp of the last reload of hosts file. + hostsReloadTime = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: plugin.Namespace, + Subsystem: "hosts", + Name: "reload_timestamp_seconds", + Help: "The timestamp of the last reload of hosts file.", + }) +) diff --git a/ag_201_coredns/plugin/hosts/setup.go b/ag_201_coredns/plugin/hosts/setup.go new file mode 100644 index 0000000..c256dc1 --- /dev/null +++ b/ag_201_coredns/plugin/hosts/setup.go @@ -0,0 +1,157 @@ +package hosts + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" +) + +var log = clog.NewWithPlugin("hosts") + +func init() { plugin.Register("hosts", setup) } + +func periodicHostsUpdate(h *Hosts) chan bool { + parseChan := make(chan bool) + + if h.options.reload == 0 { + return parseChan + } + + go func() { + ticker := time.NewTicker(h.options.reload) + for { + select { + case <-parseChan: + return + case <-ticker.C: + h.readHosts() + } + } + }() + return parseChan +} + +func setup(c *caddy.Controller) error { + h, err := hostsParse(c) + if err != nil { + return plugin.Error("hosts", err) + } + + parseChan := periodicHostsUpdate(&h) + + c.OnStartup(func() error { + h.readHosts() + return nil + }) + + c.OnShutdown(func() error { + close(parseChan) + return nil + }) + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + h.Next = next + return h + }) + + return nil +} + +func hostsParse(c *caddy.Controller) (Hosts, error) { + config := dnsserver.GetConfig(c) + + h := Hosts{ + Hostsfile: &Hostsfile{ + path: "/etc/hosts", + hmap: newMap(), + inline: newMap(), + options: newOptions(), + }, + } + + inline := []string{} + i := 0 + for c.Next() { + if i > 0 { + return h, plugin.ErrOnce + } + i++ + + args := c.RemainingArgs() + + if len(args) >= 1 { + h.path = args[0] + args = args[1:] + + if !filepath.IsAbs(h.path) && config.Root != "" { + h.path = filepath.Join(config.Root, h.path) + } + s, err := os.Stat(h.path) + if err != nil { + if os.IsNotExist(err) { + log.Warningf("File does not exist: %s", h.path) + } else { + return h, c.Errf("unable to access hosts file '%s': %v", h.path, err) + } + } + if s != nil && s.IsDir() { + log.Warningf("Hosts file %q is a directory", h.path) + } + } + + h.Origins = plugin.OriginsFromArgsOrServerBlock(args, c.ServerBlockKeys) + + for c.NextBlock() { + switch c.Val() { + case "fallthrough": + h.Fall.SetZonesFromArgs(c.RemainingArgs()) + case "no_reverse": + h.options.autoReverse = false + case "ttl": + remaining := c.RemainingArgs() + if len(remaining) < 1 { + return h, c.Errf("ttl needs a time in second") + } + ttl, err := strconv.Atoi(remaining[0]) + if err != nil { + return h, c.Errf("ttl needs a number of second") + } + if ttl <= 0 || ttl > 65535 { + return h, c.Errf("ttl provided is invalid") + } + h.options.ttl = uint32(ttl) + case "reload": + remaining := c.RemainingArgs() + if len(remaining) != 1 { + return h, c.Errf("reload needs a duration (zero seconds to disable)") + } + reload, err := time.ParseDuration(remaining[0]) + if err != nil { + return h, c.Errf("invalid duration for reload '%s'", remaining[0]) + } + if reload < 0 { + return h, c.Errf("invalid negative duration for reload '%s'", remaining[0]) + } + h.options.reload = reload + default: + if len(h.Fall.Zones) == 0 { + line := strings.Join(append([]string{c.Val()}, c.RemainingArgs()...), " ") + inline = append(inline, line) + continue + } + return h, c.Errf("unknown property '%s'", c.Val()) + } + } + } + + h.initInline(inline) + + return h, nil +} diff --git a/ag_201_coredns/plugin/hosts/setup_test.go b/ag_201_coredns/plugin/hosts/setup_test.go new file mode 100644 index 0000000..38c7c31 --- /dev/null +++ b/ag_201_coredns/plugin/hosts/setup_test.go @@ -0,0 +1,169 @@ +package hosts + +import ( + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/pkg/fall" +) + +func TestHostsParse(t *testing.T) { + tests := []struct { + inputFileRules string + shouldErr bool + expectedPath string + expectedOrigins []string + expectedFallthrough fall.F + }{ + { + `hosts +`, + false, "/etc/hosts", nil, fall.Zero, + }, + { + `hosts /tmp`, + false, "/tmp", nil, fall.Zero, + }, + { + `hosts /etc/hosts miek.nl.`, + false, "/etc/hosts", []string{"miek.nl."}, fall.Zero, + }, + { + `hosts /etc/hosts miek.nl. pun.gent.`, + false, "/etc/hosts", []string{"miek.nl.", "pun.gent."}, fall.Zero, + }, + { + `hosts { + fallthrough + }`, + false, "/etc/hosts", nil, fall.Root, + }, + { + `hosts /tmp { + fallthrough + }`, + false, "/tmp", nil, fall.Root, + }, + { + `hosts /etc/hosts miek.nl. { + fallthrough + }`, + false, "/etc/hosts", []string{"miek.nl."}, fall.Root, + }, + { + `hosts /etc/hosts miek.nl 10.0.0.9/8 { + fallthrough + }`, + false, "/etc/hosts", []string{"miek.nl.", "10.in-addr.arpa."}, fall.Root, + }, + { + `hosts /etc/hosts { + fallthrough + } + hosts /etc/hosts { + fallthrough + }`, + true, "/etc/hosts", nil, fall.Root, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputFileRules) + h, err := hostsParse(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 h.path != test.expectedPath { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedPath, h.path) + } + } else { + if !h.Fall.Equal(test.expectedFallthrough) { + t.Fatalf("Test %d expected fallthrough of %v, got %v", i, test.expectedFallthrough, h.Fall) + } + if len(h.Origins) != len(test.expectedOrigins) { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedOrigins, h.Origins) + } + for j, name := range test.expectedOrigins { + if h.Origins[j] != name { + t.Fatalf("Test %d expected %v for %d th zone, got %v", i, name, j, h.Origins[j]) + } + } + } + } +} + +func TestHostsInlineParse(t *testing.T) { + tests := []struct { + inputFileRules string + shouldErr bool + expectedaddr map[string][]string + expectedFallthrough fall.F + }{ + { + `hosts highly_unlikely_to_exist_hosts_file example.org { + 10.0.0.1 example.org + fallthrough + }`, + false, + map[string][]string{ + `10.0.0.1`: { + `example.org.`, + }, + }, + fall.Root, + }, + { + `hosts highly_unlikely_to_exist_hosts_file example.org { + 10.0.0.1 example.org + }`, + false, + map[string][]string{ + `10.0.0.1`: { + `example.org.`, + }, + }, + fall.Zero, + }, + { + `hosts highly_unlikely_to_exist_hosts_file example.org { + fallthrough + 10.0.0.1 example.org + }`, + true, + map[string][]string{}, + fall.Root, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputFileRules) + h, err := hostsParse(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 !h.Fall.Equal(test.expectedFallthrough) { + t.Errorf("Test %d expected fallthrough of %v, got %v", i, test.expectedFallthrough, h.Fall) + } + for k, expectedVal := range test.expectedaddr { + val, ok := h.inline.addr[k] + if !ok { + t.Errorf("Test %d expected %v, got no entry", i, k) + continue + } + if len(expectedVal) != len(val) { + t.Errorf("Test %d expected %v records for %v, got %v", i, len(expectedVal), k, len(val)) + } + for j := range expectedVal { + if expectedVal[j] != val[j] { + t.Errorf("Test %d expected %v for %v, got %v", i, expectedVal[j], j, val[j]) + } + } + } + } + } +} diff --git a/ag_201_coredns/plugin/import/README.md b/ag_201_coredns/plugin/import/README.md new file mode 100644 index 0000000..aaaaa1b --- /dev/null +++ b/ag_201_coredns/plugin/import/README.md @@ -0,0 +1,73 @@ +# import + +## Name + +*import* - includes files or references snippets from a Corefile. + +## Description + +The *import* plugin can be used to include files into the main configuration. Another use is to +reference predefined snippets. Both can help to avoid some duplication. + +This is a unique plugin in that *import* can appear outside of a server block. In other words, it +can appear at the top of a Corefile where an address would normally be. + +## Syntax + +~~~ +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. + +## Files + +You can use *import* to include a file or files. This file's location is relative to the +Corefile's location. It is an error if a specific file cannot be found, but an empty glob pattern is +not an error. + +## Snippets + +You can define snippets to be reused later in your Corefile by defining a block with a single-token +label surrounded by parentheses: + +~~~ corefile +(mysnippet) { + ... +} +~~~ + +Then you can invoke the snippet with *import*: + +~~~ +import mysnippet +~~~ + +## Examples + +Import a shared configuration: + +~~~ +. { + import config/common.conf +} +~~~ + +Where `config/common.conf` contains: + +~~~ +prometheus +errors +log +~~~ + +This imports files found in the zones directory: + +~~~ +import ../zones/* +~~~ + +## See Also + +See corefile(5). diff --git a/ag_201_coredns/plugin/k8s_external/README.md b/ag_201_coredns/plugin/k8s_external/README.md new file mode 100644 index 0000000..1cf5eca --- /dev/null +++ b/ag_201_coredns/plugin/k8s_external/README.md @@ -0,0 +1,113 @@ +# k8s_external + +## Name + +*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 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, SRV, and PTR 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. + +By default the apex of the zone will look like the following (assuming the zone used is `example.org`): + +~~~ dns +example.org. 5 IN SOA ns1.dns.example.org. hostmaster.example.org. ( + 12345 ; serial + 14400 ; refresh (4 hours) + 3600 ; retry (1 hour) + 604800 ; expire (1 week) + 5 ; minimum (4 hours) + ) +example.org 5 IN NS ns1.dns.example.org. + +ns1.dns.example.org. 5 IN A .... +ns1.dns.example.org. 5 IN AAAA .... +~~~ + +Note that we use the `dns` subdomain for the records DNS needs (see the `apex` directive). Also +note the SOA's serial number is static. The IP addresses of the nameserver records are those of the +CoreDNS service. + +The *k8s_external* plugin handles the subdomain `dns` and the apex of the zone itself; all other +queries are resolved to addresses in the cluster. + +## Syntax + +~~~ +k8s_external [ZONE...] +~~~ + +* **ZONES** zones *k8s_external* should be authoritative for. + +If you want to change the apex domain or use a different TTL for the returned records you can use +this extended syntax. + +~~~ +k8s_external [ZONE...] { + apex APEX + ttl TTL +} +~~~ + +* **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 + +## Examples + +Enable names under `example.org` to be resolved to in-cluster DNS addresses. + +~~~ +. { + kubernetes cluster.local + k8s_external example.org +} +~~~ + +With the Corefile above, the following Service will get an `A` record for `test.default.example.org` with the IP address `192.168.200.123`. + +~~~ +apiVersion: v1 +kind: Service +metadata: + name: test + namespace: default +spec: + clusterIP: None + externalIPs: + - 192.168.200.123 + 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 + } + ~~~ + +# 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). + diff --git a/ag_201_coredns/plugin/k8s_external/apex.go b/ag_201_coredns/plugin/k8s_external/apex.go new file mode 100644 index 0000000..e575e5e --- /dev/null +++ b/ag_201_coredns/plugin/k8s_external/apex.go @@ -0,0 +1,112 @@ +package external + +import ( + "github.com/coredns/coredns/plugin/pkg/dnsutil" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// serveApex serves request that hit the zone' apex. A reply is written back to the client. +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, e.headless) + for _, rr := range addr { + rr.Header().Ttl = e.ttl + rr.Header().Name = dnsutil.Join("ns1", e.apex, state.QName()) + m.Extra = append(m.Extra, rr) + } + default: + m.Ns = []dns.RR{e.soa(state)} + } + + state.W.WriteMsg(m) + return 0, nil +} + +// serveSubApex serves requests that hit the zones fake 'dns' subdomain where our nameservers live. +func (e *External) serveSubApex(state request.Request) (int, error) { + base, _ := dnsutil.TrimZone(state.Name(), state.Zone) + + 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 { + default: + m.SetRcode(m, dns.RcodeNameError) + m.Ns = []dns.RR{e.soa(state)} + state.W.WriteMsg(m) + return 0, nil + case 2: + nl, _ := dns.NextLabel(base, 0) + ns := base[:nl] + if ns != "ns1." { + // nxdomain + m.SetRcode(m, dns.RcodeNameError) + m.Ns = []dns.RR{e.soa(state)} + state.W.WriteMsg(m) + return 0, nil + } + + addr := e.externalAddrFunc(state, e.headless) + for _, rr := range addr { + rr.Header().Ttl = e.ttl + rr.Header().Name = state.QName() + switch state.QType() { + case dns.TypeA: + if rr.Header().Rrtype == dns.TypeA { + m.Answer = append(m.Answer, rr) + } + case dns.TypeAAAA: + if rr.Header().Rrtype == dns.TypeAAAA { + m.Answer = append(m.Answer, rr) + } + } + } + + if len(m.Answer) == 0 { + m.Ns = []dns.RR{e.soa(state)} + } + + state.W.WriteMsg(m) + return 0, nil + + case 1: + // nodata for the dns empty non-terminal + m.Ns = []dns.RR{e.soa(state)} + state.W.WriteMsg(m) + return 0, nil + } +} + +func (e *External) soa(state request.Request) *dns.SOA { + header := dns.RR_Header{Name: state.Zone, Rrtype: dns.TypeSOA, Ttl: e.ttl, Class: dns.ClassINET} + + soa := &dns.SOA{Hdr: header, + Mbox: dnsutil.Join(e.hostmaster, e.apex, state.Zone), + Ns: dnsutil.Join("ns1", e.apex, state.Zone), + Serial: e.externalSerialFunc(state.Zone), + Refresh: 7200, + Retry: 1800, + Expire: 86400, + Minttl: e.ttl, + } + return soa +} + +func (e *External) ns(state request.Request) *dns.NS { + header := dns.RR_Header{Name: state.Zone, Rrtype: dns.TypeNS, Ttl: e.ttl, Class: dns.ClassINET} + ns := &dns.NS{Hdr: header, Ns: dnsutil.Join("ns1", e.apex, state.Zone)} + + return ns +} diff --git a/ag_201_coredns/plugin/k8s_external/apex_test.go b/ag_201_coredns/plugin/k8s_external/apex_test.go new file mode 100644 index 0000000..ab08187 --- /dev/null +++ b/ag_201_coredns/plugin/k8s_external/apex_test.go @@ -0,0 +1,122 @@ +package external + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/kubernetes" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestApex(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.Next = test.NextHandler(dns.RcodeSuccess, nil) + e.externalFunc = k.External + e.externalAddrFunc = externalAddress // internal test function + e.externalSerialFunc = externalSerial // internal test function + + ctx := context.TODO() + for i, tc := range testsApex { + r := tc.Msg() + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := e.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d expected no error, got %v", i, err) + return + } + if tc.Error != nil { + continue + } + + resp := w.Msg + 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) + } + } + } +} + +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.dns.example.com. 1499347823 7200 1800 86400 5"), + }, + }, + { + Qname: "example.com.", Qtype: dns.TypeNS, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.NS("example.com. 5 IN NS ns1.dns.example.com."), + }, + Extra: []dns.RR{ + test.A("ns1.dns.example.com. 5 IN A 127.0.0.1"), + }, + }, + { + Qname: "example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + 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.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.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.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.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.dns.example.com. 1499347823 7200 1800 86400 5"), + }, + }, + { + Qname: "ns1.dns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("ns1.dns.example.com. 5 IN A 127.0.0.1"), + }, + }, +} diff --git a/ag_201_coredns/plugin/k8s_external/external.go b/ag_201_coredns/plugin/k8s_external/external.go new file mode 100644 index 0000000..2cbf885 --- /dev/null +++ b/ag_201_coredns/plugin/k8s_external/external.go @@ -0,0 +1,125 @@ +/* +Package external implements external names for kubernetes clusters. + +This plugin only handles three qtypes (except the apex queries, because those are handled +differently). We support A, AAAA and SRV request, for all other types we return NODATA or +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 + +import ( + "context" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/pkg/upstream" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Externaler defines the interface that a plugin should implement in order to be used by External. +type Externaler interface { + // External returns a slice of msg.Services that are looked up in the backend and match + // the request. + External(request.Request, bool) ([]msg.Service, int) + // ExternalAddress should return a string slice of addresses for the nameserving endpoint. + 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 serves records for External IPs and Loadbalance IPs of Services in Kubernetes clusters. +type External struct { + Next plugin.Handler + Zones []string + + hostmaster string + apex string + ttl uint32 + headless bool + + upstream *upstream.Upstream + + 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. +func New() *External { + e := &External{hostmaster: "hostmaster", ttl: 5, apex: "dns"} + return e +} + +// ServeDNS implements the plugin.Handle interface. +func (e *External) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + + zone := plugin.Zones(e.Zones).Matches(state.Name()) + if zone == "" { + 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. + if state.Name() == z { // apex query + ret, err := e.serveApex(state) + return ret, err + } + if dns.IsSubDomain(e.apex+"."+z, state.Name()) { + // dns subdomain test for ns. and dns. queries + ret, err := e.serveSubApex(state) + return ret, err + } + } + + svc, rcode := e.externalFunc(state, e.headless) + + m := new(dns.Msg) + m.SetReply(state.Req) + m.Authoritative = true + + if len(svc) == 0 { + m.Rcode = rcode + m.Ns = []dns.RR{e.soa(state)} + w.WriteMsg(m) + return 0, nil + } + + switch state.QType() { + case dns.TypeA: + m.Answer, m.Truncated = e.a(ctx, svc, state) + case dns.TypeAAAA: + m.Answer, m.Truncated = e.aaaa(ctx, svc, state) + case dns.TypeSRV: + 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)} + } + + // If we did have records, but queried for the wrong qtype return a nodata response. + if len(m.Answer) == 0 { + m.Ns = []dns.RR{e.soa(state)} + } + + w.WriteMsg(m) + return 0, nil +} + +// Name implements the Handler interface. +func (e *External) Name() string { return "k8s_external" } diff --git a/ag_201_coredns/plugin/k8s_external/external_test.go b/ag_201_coredns/plugin/k8s_external/external_test.go new file mode 100644 index 0000000..9987c0e --- /dev/null +++ b/ag_201_coredns/plugin/k8s_external/external_test.go @@ -0,0 +1,426 @@ +package external + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/kubernetes" + "github.com/coredns/coredns/plugin/kubernetes/object" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + api "k8s.io/api/core/v1" +) + +func TestExternal(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.", "in-addr.arpa."} + e.headless = true + e.Next = test.NextHandler(dns.RcodeSuccess, nil) + e.externalFunc = k.External + e.externalAddrFunc = externalAddress // internal test function + e.externalSerialFunc = externalSerial // internal test function + + ctx := context.TODO() + for i, tc := range tests { + r := tc.Msg() + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := e.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d expected no error, got %v", i, err) + return + } + if tc.Error != nil { + continue + } + + resp := w.Msg + + 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.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, + Answer: []dns.RR{ + test.A("svc1.testns.example.com. 5 IN A 1.2.3.4"), + }, + }, + { + 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")}, + }, + // SRV Service Not udp/tcp + { + Qname: "*._not-udp-or-tcp.svc1.testns.example.com.", Qtype: dns.TypeSRV, 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"), + }, + }, + // SRV Service + { + Qname: "_http._tcp.svc1.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("_http._tcp.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"), + }, + }, + // AAAA Service (with an existing A record, but no AAAA record) + { + Qname: "svc1.testns.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"), + }, + }, + // AAAA Service (non-existing service) + { + Qname: "svc0.testns.example.com.", Qtype: dns.TypeAAAA, 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 (non-existing service) + { + Qname: "svc0.testns.example.com.", Qtype: dns.TypeA, 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 (non-existing namespace) + { + Qname: "svc0.svc-nons.example.com.", Qtype: dns.TypeA, 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"), + }, + }, + // AAAA Service + { + Qname: "svc6.testns.example.com.", Qtype: dns.TypeAAAA, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.AAAA("svc6.testns.example.com. 5 IN AAAA 1:2::5"), + }, + }, + // SRV + { + Qname: "_http._tcp.svc6.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("_http._tcp.svc6.testns.example.com. 5 IN SRV 0 100 80 svc6.testns.example.com."), + }, + Extra: []dns.RR{ + test.AAAA("svc6.testns.example.com. 5 IN AAAA 1:2::5"), + }, + }, + // SRV + { + Qname: "svc6.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("svc6.testns.example.com. 5 IN SRV 0 100 80 svc6.testns.example.com."), + }, + Extra: []dns.RR{ + test.AAAA("svc6.testns.example.com. 5 IN AAAA 1:2::5"), + }, + }, + { + Qname: "testns.example.com.", Qtype: dns.TypeA, 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"), + }, + }, + { + Qname: "testns.example.com.", Qtype: dns.TypeSOA, 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"), + }, + }, + // svc11 + { + Qname: "svc11.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + 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(bool) int64 { return 0 } +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) 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": { + { + Name: "svc1", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{"10.0.0.1"}, + ExternalIPs: []string{"1.2.3.4"}, + Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, + }, + }, + "svc6.testns": { + { + Name: "svc6", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{"10.0.0.3"}, + ExternalIPs: []string{"1:2::5"}, + Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, + }, + }, + "svc11.testns": { + { + Name: "svc11", + Namespace: "testns", + Type: api.ServiceTypeLoadBalancer, + ExternalIPs: []string{"2.3.4.5"}, + ClusterIPs: []string{"10.0.0.3"}, + Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, + }, + }, + "svc12.testns": { + { + 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 { + var svcs []*object.Service + for _, svc := range svcIndexExternal { + svcs = append(svcs, svc...) + } + return svcs +} + +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/ag_201_coredns/plugin/k8s_external/msg_to_dns.go b/ag_201_coredns/plugin/k8s_external/msg_to_dns.go new file mode 100644 index 0000000..6975718 --- /dev/null +++ b/ag_201_coredns/plugin/k8s_external/msg_to_dns.go @@ -0,0 +1,190 @@ +package external + +import ( + "context" + "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, truncated bool) { + dup := make(map[string]struct{}) + + for _, s := range services { + what, ip := s.HostType() + + switch what { + case dns.TypeCNAME: + 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 { + records = append(records, resp.Answer...) + if resp.Truncated { + truncated = true + } + } + + case dns.TypeA: + if _, ok := dup[s.Host]; !ok { + dup[s.Host] = struct{}{} + rr := s.NewA(state.QName(), ip) + rr.Hdr.Ttl = e.ttl + records = append(records, rr) + } + + case dns.TypeAAAA: + // nada + } + } + return records, truncated +} + +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 { + case dns.TypeCNAME: + 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 { + records = append(records, resp.Answer...) + if resp.Truncated { + truncated = true + } + } + + case dns.TypeA: + // nada + + case dns.TypeAAAA: + if _, ok := dup[s.Host]; !ok { + dup[s.Host] = struct{}{} + rr := s.NewAAAA(state.QName(), ip) + rr.Hdr.Ttl = e.ttl + records = append(records, rr) + } + } + } + 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(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. + w := make(map[int]int) + for _, s := range services { + weight := 100 + if s.Weight != 0 { + weight = s.Weight + } + if _, ok := w[s.Priority]; !ok { + w[s.Priority] = weight + continue + } + w[s.Priority] += weight + } + for _, s := range services { + // Don't add the entry if the port is -1 (invalid). The kubernetes plugin uses port -1 when a service/endpoint + // does not have any declared ports. + if s.Port == -1 { + continue + } + w1 := 100.0 / float64(w[s.Priority]) + if s.Weight == 0 { + w1 *= 100 + } else { + w1 *= float64(s.Weight) + } + weight := uint16(math.Floor(w1)) + // weight should be at least 1 + if weight == 0 { + weight = 1 + } + + what, ip := s.HostType() + + switch what { + case dns.TypeCNAME: + 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) + 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 { + hdr := dns.RR_Header{Name: srv.Target, Rrtype: what, Class: dns.ClassINET, Ttl: e.ttl} + + switch what { + case dns.TypeA: + extra = append(extra, &dns.A{Hdr: hdr, A: ip}) + case dns.TypeAAAA: + extra = append(extra, &dns.AAAA{Hdr: hdr, AAAA: ip}) + } + } + } + } + return records, extra +} + +// not sure if this is even needed. + +// item holds records. +type item struct { + name string // name of the record (either owner or something else unique). + port uint16 // port of the record (used for address records, A and AAAA). + addr string // address of the record (A and AAAA). +} + +// isDuplicate uses m to see if the combo (name, addr, port) already exists. If it does +// not exist already IsDuplicate will also add the record to the map. +func isDuplicate(m map[item]struct{}, name, addr string, port uint16) bool { + if addr != "" { + _, ok := m[item{name, 0, addr}] + if !ok { + m[item{name, 0, addr}] = struct{}{} + } + return ok + } + _, ok := m[item{name, port, ""}] + if !ok { + m[item{name, port, ""}] = struct{}{} + } + return ok +} diff --git a/ag_201_coredns/plugin/k8s_external/setup.go b/ag_201_coredns/plugin/k8s_external/setup.go new file mode 100644 index 0000000..dbb1372 --- /dev/null +++ b/ag_201_coredns/plugin/k8s_external/setup.go @@ -0,0 +1,79 @@ +package external + +import ( + "strconv" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/upstream" +) + +func init() { plugin.Register("k8s_external", setup) } + +func setup(c *caddy.Controller) error { + e, err := parse(c) + if err != nil { + return plugin.Error("k8s_external", err) + } + + // Do this in OnStartup, so all plugins have been initialized. + c.OnStartup(func() error { + m := dnsserver.GetConfig(c).Handler("kubernetes") + if m == nil { + return nil + } + if x, ok := m.(Externaler); ok { + e.externalFunc = x.External + e.externalAddrFunc = x.ExternalAddress + e.externalServicesFunc = x.ExternalServices + e.externalSerialFunc = x.ExternalSerial + } + return nil + }) + + e.upstream = upstream.New() + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + e.Next = next + return e + }) + + return nil +} + +func parse(c *caddy.Controller) (*External, error) { + e := New() + + for c.Next() { // external + e.Zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) + for c.NextBlock() { + switch c.Val() { + case "ttl": + args := c.RemainingArgs() + if len(args) == 0 { + return nil, c.ArgErr() + } + t, err := strconv.Atoi(args[0]) + if err != nil { + return nil, err + } + if t < 0 || t > 3600 { + return nil, c.Errf("ttl must be in range [0, 3600]: %d", t) + } + e.ttl = uint32(t) + case "apex": + args := c.RemainingArgs() + if len(args) == 0 { + return nil, c.ArgErr() + } + e.apex = args[0] + case "headless": + e.headless = true + default: + return nil, c.Errf("unknown property '%s'", c.Val()) + } + } + } + return e, nil +} diff --git a/ag_201_coredns/plugin/k8s_external/setup_test.go b/ag_201_coredns/plugin/k8s_external/setup_test.go new file mode 100644 index 0000000..351b35a --- /dev/null +++ b/ag_201_coredns/plugin/k8s_external/setup_test.go @@ -0,0 +1,57 @@ +package external + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedZone string + expectedApex string + expectedHeadless bool + }{ + {`k8s_external`, false, "", "dns", false}, + {`k8s_external example.org`, false, "example.org.", "dns", false}, + {`k8s_external example.org { + apex testdns +}`, false, "example.org.", "testdns", false}, + {`k8s_external example.org { + headless +}`, false, "example.org.", "dns", true}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + e, err := parse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, 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 !test.shouldErr && test.expectedZone != "" { + if test.expectedZone != e.Zones[0] { + t.Errorf("Test %d, expected zone %q for input %s, got: %q", i, test.expectedZone, test.input, e.Zones[0]) + } + } + if !test.shouldErr { + if test.expectedApex != e.apex { + 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) + } + } + } +} diff --git a/ag_201_coredns/plugin/k8s_external/transfer.go b/ag_201_coredns/plugin/k8s_external/transfer.go new file mode 100644 index 0000000..781f19f --- /dev/null +++ b/ag_201_coredns/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/ag_201_coredns/plugin/k8s_external/transfer_test.go b/ag_201_coredns/plugin/k8s_external/transfer_test.go new file mode 100644 index 0000000..4f525f9 --- /dev/null +++ b/ag_201_coredns/plugin/k8s_external/transfer_test.go @@ -0,0 +1,148 @@ +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 + e = &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/ag_201_coredns/plugin/kubernetes/README.md b/ag_201_coredns/plugin/kubernetes/README.md new file mode 100644 index 0000000..8d66af8 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/README.md @@ -0,0 +1,239 @@ +# kubernetes + +## Name + +*kubernetes* - enables reading zone data from a Kubernetes cluster. + +## Description + +This plugin implements the [Kubernetes DNS-Based Service Discovery +Specification](https://github.com/kubernetes/dns/blob/master/docs/specification.md). + +CoreDNS running the kubernetes plugin can be used as a replacement for kube-dns in a kubernetes +cluster. See the [deployment](https://github.com/coredns/deployment) repository for details on [how +to deploy CoreDNS in Kubernetes](https://github.com/coredns/deployment/tree/master/kubernetes). + +[stubDomains and upstreamNameservers](https://kubernetes.io/blog/2017/04/configuring-private-dns-zones-upstream-nameservers-kubernetes/) +are implemented via the *forward* plugin. See the examples below. + +This plugin can only be used once per Server Block. + +## Syntax + +~~~ +kubernetes [ZONES...] +~~~ + +With only the plugin specified, the *kubernetes* plugin will default to the zone specified in +the server's block. It will handle all queries in that zone and connect to Kubernetes in-cluster. It +will not provide PTR records for services or A records for pods. If **ZONES** is used it specifies +all the zones the plugin should be authoritative for. + +``` +kubernetes [ZONES...] { + endpoint URL + tls CERT KEY CACERT + kubeconfig KUBECONFIG [CONTEXT] + namespaces NAMESPACE... + labels EXPRESSION + pods POD-MODE + endpoint_pod_names + ttl TTL + noendpoints + fallthrough [ZONES...] + ignore empty_service +} +``` + +* `endpoint` specifies the **URL** for a remote k8s API endpoint. + If omitted, it will connect to k8s in-cluster using the cluster service account. +* `tls` **CERT** **KEY** **CACERT** are the TLS cert, key and the CA cert file names for remote k8s connection. + This option is ignored if connecting in-cluster (i.e. endpoint is not specified). +* `kubeconfig` **KUBECONFIG [CONTEXT]** authenticates the connection to a remote k8s cluster using a kubeconfig file. + **[CONTEXT]** is optional, if not set, then the current context specified in kubeconfig will be used. + It supports TLS, username and password, or token-based authentication. + This option is ignored if connecting in-cluster (i.e., the endpoint is not specified). +* `namespaces` **NAMESPACE [NAMESPACE...]** only exposes the k8s namespaces listed. + If this option is omitted all namespaces are exposed +* `namespace_labels` **EXPRESSION** only expose the records for Kubernetes namespaces that match this label selector. + The label selector syntax is described in the + [Kubernetes User Guide - Labels](https://kubernetes.io/docs/user-guide/labels/). An example that + only exposes namespaces labeled as "istio-injection=enabled", would use: + `labels istio-injection=enabled`. +* `labels` **EXPRESSION** only exposes the records for Kubernetes objects that match this label selector. + The label selector syntax is described in the + [Kubernetes User Guide - Labels](https://kubernetes.io/docs/user-guide/labels/). An example that + only exposes objects labeled as "application=nginx" in the "staging" or "qa" environments, would + use: `labels environment in (staging, qa),application=nginx`. +* `pods` **POD-MODE** sets the mode for handling IP-based pod A records, e.g. + `1-2-3-4.ns.pod.cluster.local. in A 1.2.3.4`. + This option is provided to facilitate use of SSL certs when connecting directly to pods. Valid + values for **POD-MODE**: + + * `disabled`: Default. Do not process pod requests, always returning `NXDOMAIN` + * `insecure`: Always return an A record with IP from request (without checking k8s). This option + is vulnerable to abuse if used maliciously in conjunction with wildcard SSL certs. This + option is provided for backward compatibility with kube-dns. + * `verified`: Return an A record if there exists a pod in same namespace with matching IP. This + option requires substantially more memory than in insecure mode, since it will maintain a watch + on all pods. + +* `endpoint_pod_names` uses the pod name of the pod targeted by the endpoint as + the endpoint name in A records, e.g., + `endpoint-name.my-service.namespace.svc.cluster.local. in A 1.2.3.4` + By default, the endpoint-name name selection is as follows: Use the hostname + of the endpoint, or if hostname is not set, use the dashed form of the endpoint + IP address (e.g., `1-2-3-4.my-service.namespace.svc.cluster.local.`) + 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 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. + All endpoint queries and headless service queries will result in an NXDOMAIN. +* `fallthrough` **[ZONES...]** If a query for a record in the zones for which the plugin is authoritative + results in NXDOMAIN, normally that is what the response will be. However, if you specify this option, + the query will instead be passed on down the plugin chain, which can include another plugin to handle + the query. 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. +* `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. + +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. + +## Monitoring Kubernetes Endpoints + +By default the *kubernetes* plugin watches Endpoints via the `discovery.EndpointSlices` API. However the +`api.Endpoints` API is used instead if the Kubernetes version does not support the `EndpointSliceProxying` +feature gate by default (i.e. Kubernetes version < 1.19). + +## Ready + +This plugin reports readiness to the ready plugin. This will happen after it has synced to the +Kubernetes API. + +## Examples + +Handle all queries in the `cluster.local` zone. Connect to Kubernetes in-cluster. Also handle all +`in-addr.arpa` `PTR` requests for `10.0.0.0/17` . Verify the existence of pods when answering pod +requests. + +~~~ txt +10.0.0.0/17 cluster.local { + kubernetes { + pods verified + } +} +~~~ + +Or you can selectively expose some namespaces: + +~~~ txt +kubernetes cluster.local { + namespaces test staging +} +~~~ + +Connect to Kubernetes with CoreDNS running outside the cluster: + +~~~ txt +kubernetes cluster.local { + endpoint https://k8s-endpoint:8443 + tls cert key cacert +} +~~~ + +## 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`. +Also configured is an upstreamNameserver `8.8.8.8:53` that will be used for resolving names that do not fall in `cluster.local` +or `example.local`. + +~~~ txt +cluster.local:53 { + kubernetes cluster.local +} +example.local { + forward . 10.100.0.10:53 +} + +. { + forward . 8.8.8.8:53 +} +~~~ + +The configuration above represents the following Kube-DNS stubDomains and upstreamNameservers configuration. + +~~~ txt +stubDomains: | + {“example.local”: [“10.100.0.10:53”]} +upstreamNameservers: | + [“8.8.8.8:53”] +~~~ + +## AutoPath + +The *kubernetes* plugin can be used in conjunction with the *autopath* plugin. Using this +feature enables server-side domain search path completion in Kubernetes clusters. Note: `pods` must +be set to `verified` for this to function properly. Furthermore, the remote IP address in the DNS +packet received by CoreDNS must be the IP address of the Pod that sent the request. + + cluster.local { + autopath @kubernetes + kubernetes { + pods verified + } + } + +## Metadata + +The kubernetes plugin will publish the following metadata, if the *metadata* +plugin is also enabled: + + * `kubernetes/endpoint`: the endpoint name in the query + * `kubernetes/kind`: the resource kind (pod or svc) in the query + * `kubernetes/namespace`: the namespace in the query + * `kubernetes/port-name`: the port name in an SRV query + * `kubernetes/protocol`: the protocol in an SRV query + * `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) + +The `kubernetes/client-namespace` and `kubernetes/client-pod-name` metadata work by reconciling the +client IP address in the DNS request packet to a known pod IP address. Therefore the following is required: + * `pods verified` mode must be enabled + * the remote IP address in the DNS packet received by CoreDNS must be the IP address + of the Pod that sent the request. + +## Metrics + +If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported: + +* `coredns_kubernetes_dns_programming_duration_seconds{service_kind}` - Exports the + [DNS programming latency SLI](https://github.com/kubernetes/community/blob/master/sig-scalability/slos/dns_programming_latency.md). + The metrics has the `service_kind` label that identifies the kind of the + [kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service). + It may take one of the three values: + * `cluster_ip` + * `headless_with_selector` + * `headless_without_selector` + +## Bugs + +The duration metric only supports the "headless\_with\_selector" service currently. + +## See Also + +See the *autopath* plugin to enable search path optimizations. And use the *transfer* plugin to +enable outgoing zone transfers. diff --git a/ag_201_coredns/plugin/kubernetes/autopath.go b/ag_201_coredns/plugin/kubernetes/autopath.go new file mode 100644 index 0000000..e873897 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/autopath.go @@ -0,0 +1,62 @@ +package kubernetes + +import ( + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/kubernetes/object" + "github.com/coredns/coredns/request" +) + +// AutoPath implements the AutoPathFunc call from the autopath plugin. +// It returns a per-query search path or nil indicating no searchpathing should happen. +func (k *Kubernetes) AutoPath(state request.Request) []string { + // Check if the query falls in a zone we are actually authoritative for and thus if we want autopath. + zone := plugin.Zones(k.Zones).Matches(state.Name()) + if zone == "" { + return nil + } + + // cluster.local { + // autopath @kubernetes + // kubernetes { + // pods verified # + // } + // } + // if pods != verified will cause panic and return SERVFAIL, expect worked as normal without autopath function + if !k.opts.initPodCache { + return nil + } + + ip := state.IP() + + pod := k.podWithIP(ip) + if pod == nil { + return nil + } + + search := make([]string, 3) + if zone == "." { + search[0] = pod.Namespace + ".svc." + search[1] = "svc." + search[2] = "." + } else { + search[0] = pod.Namespace + ".svc." + zone + search[1] = "svc." + zone + search[2] = zone + } + + search = append(search, k.autoPathSearch...) + search = append(search, "") // sentinel + return search +} + +// podWithIP returns the api.Pod for source IP. It returns nil if nothing can be found. +func (k *Kubernetes) podWithIP(ip string) *object.Pod { + if k.podMode != podModeVerified { + return nil + } + ps := k.APIConn.PodIndex(ip) + if len(ps) == 0 { + return nil + } + return ps[0] +} diff --git a/ag_201_coredns/plugin/kubernetes/controller.go b/ag_201_coredns/plugin/kubernetes/controller.go new file mode 100644 index 0000000..a34b641 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/controller.go @@ -0,0 +1,757 @@ +package kubernetes + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/coredns/coredns/plugin/kubernetes/object" + + api "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" + discoveryV1beta1 "k8s.io/api/discovery/v1beta1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" +) + +const ( + podIPIndex = "PodIP" + svcNameNamespaceIndex = "ServiceNameNamespace" + svcIPIndex = "ServiceIP" + svcExtIPIndex = "ServiceExternalIP" + epNameNamespaceIndex = "EndpointNameNamespace" + epIPIndex = "EndpointsIP" +) + +type dnsController interface { + ServiceList() []*object.Service + EndpointsList() []*object.Endpoints + SvcIndex(string) []*object.Service + SvcIndexReverse(string) []*object.Service + SvcExtIndexReverse(string) []*object.Service + PodIndex(string) []*object.Pod + EpIndex(string) []*object.Endpoints + EpIndexReverse(string) []*object.Endpoints + + GetNodeByName(context.Context, string) (*api.Node, error) + GetNamespaceByName(string) (*object.Namespace, error) + + Run() + HasSynced() bool + Stop() error + + // Modified returns the timestamp of the most recent changes to services. If the passed bool is true, it should + // return the timestamp of the most recent changes to services with external facing IP addresses + Modified(bool) int64 +} + +type dnsControl struct { + // modified tracks timestamp of the most recent changes + // It needs to be first because it is guaranteed to be 8-byte + // aligned ( we use sync.LoadAtomic with this ) + modified int64 + // extModified tracks timestamp of the most recent changes to + // services with external facing IP addresses + extModified int64 + + client kubernetes.Interface + + selector labels.Selector + namespaceSelector labels.Selector + + // epLock is used to lock reads of epLister and epController while they are being replaced + // with the api.Endpoints Lister/Controller on k8s systems that don't use discovery.EndpointSlices + epLock sync.RWMutex + + svcController cache.Controller + podController cache.Controller + epController cache.Controller + nsController cache.Controller + + svcLister cache.Indexer + podLister cache.Indexer + epLister cache.Indexer + nsLister cache.Store + + // stopLock is used to enforce only a single call to Stop is active. + // Needed because we allow stopping through an http endpoint and + // allowing concurrent stoppers leads to stack traces. + stopLock sync.Mutex + shutdown bool + stopCh chan struct{} + + zones []string + endpointNameMode bool +} + +type dnsControlOpts struct { + initPodCache bool + initEndpointsCache bool + ignoreEmptyService bool + + // Label handling. + labelSelector *meta.LabelSelector + selector labels.Selector + namespaceLabelSelector *meta.LabelSelector + namespaceSelector labels.Selector + + zones []string + endpointNameMode bool +} + +// newdnsController creates a controller for CoreDNS. +func newdnsController(ctx context.Context, kubeClient kubernetes.Interface, opts dnsControlOpts) *dnsControl { + dns := dnsControl{ + client: kubeClient, + selector: opts.selector, + namespaceSelector: opts.namespaceSelector, + stopCh: make(chan struct{}), + zones: opts.zones, + endpointNameMode: opts.endpointNameMode, + } + + dns.svcLister, dns.svcController = object.NewIndexerInformer( + &cache.ListWatch{ + ListFunc: serviceListFunc(ctx, dns.client, api.NamespaceAll, dns.selector), + WatchFunc: serviceWatchFunc(ctx, dns.client, api.NamespaceAll, dns.selector), + }, + &api.Service{}, + cache.ResourceEventHandlerFuncs{AddFunc: dns.Add, UpdateFunc: dns.Update, DeleteFunc: dns.Delete}, + cache.Indexers{svcNameNamespaceIndex: svcNameNamespaceIndexFunc, svcIPIndex: svcIPIndexFunc, svcExtIPIndex: svcExtIPIndexFunc}, + object.DefaultProcessor(object.ToService, nil), + ) + + if opts.initPodCache { + dns.podLister, dns.podController = object.NewIndexerInformer( + &cache.ListWatch{ + ListFunc: podListFunc(ctx, dns.client, api.NamespaceAll, dns.selector), + WatchFunc: podWatchFunc(ctx, dns.client, api.NamespaceAll, dns.selector), + }, + &api.Pod{}, + cache.ResourceEventHandlerFuncs{AddFunc: dns.Add, UpdateFunc: dns.Update, DeleteFunc: dns.Delete}, + cache.Indexers{podIPIndex: podIPIndexFunc}, + object.DefaultProcessor(object.ToPod, nil), + ) + } + + if opts.initEndpointsCache { + dns.epLock.Lock() + dns.epLister, dns.epController = object.NewIndexerInformer( + &cache.ListWatch{ + ListFunc: endpointSliceListFunc(ctx, dns.client, api.NamespaceAll, dns.selector), + WatchFunc: endpointSliceWatchFunc(ctx, dns.client, api.NamespaceAll, dns.selector), + }, + &discovery.EndpointSlice{}, + cache.ResourceEventHandlerFuncs{AddFunc: dns.Add, UpdateFunc: dns.Update, DeleteFunc: dns.Delete}, + cache.Indexers{epNameNamespaceIndex: epNameNamespaceIndexFunc, epIPIndex: epIPIndexFunc}, + object.DefaultProcessor(object.EndpointSliceToEndpoints, dns.EndpointSliceLatencyRecorder()), + ) + dns.epLock.Unlock() + } + + dns.nsLister, dns.nsController = object.NewIndexerInformer( + &cache.ListWatch{ + ListFunc: namespaceListFunc(ctx, dns.client, dns.namespaceSelector), + WatchFunc: namespaceWatchFunc(ctx, dns.client, dns.namespaceSelector), + }, + &api.Namespace{}, + cache.ResourceEventHandlerFuncs{}, + cache.Indexers{}, + object.DefaultProcessor(object.ToNamespace, nil), + ) + + return &dns +} + +// WatchEndpoints will set the endpoint Lister and Controller to watch object.Endpoints +// instead of the default discovery.EndpointSlice. This is used in older k8s clusters where +// discovery.EndpointSlice is not fully supported. +// This can be removed when all supported k8s versions fully support EndpointSlice. +func (dns *dnsControl) WatchEndpoints(ctx context.Context) { + dns.epLock.Lock() + dns.epLister, dns.epController = object.NewIndexerInformer( + &cache.ListWatch{ + ListFunc: endpointsListFunc(ctx, dns.client, api.NamespaceAll, dns.selector), + WatchFunc: endpointsWatchFunc(ctx, dns.client, api.NamespaceAll, dns.selector), + }, + &api.Endpoints{}, + cache.ResourceEventHandlerFuncs{AddFunc: dns.Add, UpdateFunc: dns.Update, DeleteFunc: dns.Delete}, + cache.Indexers{epNameNamespaceIndex: epNameNamespaceIndexFunc, epIPIndex: epIPIndexFunc}, + object.DefaultProcessor(object.ToEndpoints, dns.EndpointsLatencyRecorder()), + ) + dns.epLock.Unlock() +} + +// WatchEndpointSliceV1beta1 will set the endpoint Lister and Controller to watch v1beta1 +// instead of the default v1. +func (dns *dnsControl) WatchEndpointSliceV1beta1(ctx context.Context) { + dns.epLock.Lock() + dns.epLister, dns.epController = object.NewIndexerInformer( + &cache.ListWatch{ + ListFunc: endpointSliceListFuncV1beta1(ctx, dns.client, api.NamespaceAll, dns.selector), + WatchFunc: endpointSliceWatchFuncV1beta1(ctx, dns.client, api.NamespaceAll, dns.selector), + }, + &discoveryV1beta1.EndpointSlice{}, + cache.ResourceEventHandlerFuncs{AddFunc: dns.Add, UpdateFunc: dns.Update, DeleteFunc: dns.Delete}, + cache.Indexers{epNameNamespaceIndex: epNameNamespaceIndexFunc, epIPIndex: epIPIndexFunc}, + object.DefaultProcessor(object.EndpointSliceV1beta1ToEndpoints, dns.EndpointSliceLatencyRecorder()), + ) + dns.epLock.Unlock() +} + +func (dns *dnsControl) EndpointsLatencyRecorder() *object.EndpointLatencyRecorder { + return &object.EndpointLatencyRecorder{ + ServiceFunc: func(o meta.Object) []*object.Service { + return dns.SvcIndex(object.ServiceKey(o.GetName(), o.GetNamespace())) + }, + } +} +func (dns *dnsControl) EndpointSliceLatencyRecorder() *object.EndpointLatencyRecorder { + return &object.EndpointLatencyRecorder{ + ServiceFunc: func(o meta.Object) []*object.Service { + return dns.SvcIndex(object.ServiceKey(o.GetLabels()[discovery.LabelServiceName], o.GetNamespace())) + }, + } +} + +func podIPIndexFunc(obj interface{}) ([]string, error) { + p, ok := obj.(*object.Pod) + if !ok { + return nil, errObj + } + return []string{p.PodIP}, nil +} + +func svcIPIndexFunc(obj interface{}) ([]string, error) { + svc, ok := obj.(*object.Service) + if !ok { + return nil, errObj + } + idx := make([]string, len(svc.ClusterIPs)) + copy(idx, svc.ClusterIPs) + return idx, nil +} + +func svcExtIPIndexFunc(obj interface{}) ([]string, error) { + svc, ok := obj.(*object.Service) + if !ok { + return nil, errObj + } + idx := make([]string, len(svc.ExternalIPs)) + copy(idx, svc.ExternalIPs) + return idx, nil +} + +func svcNameNamespaceIndexFunc(obj interface{}) ([]string, error) { + s, ok := obj.(*object.Service) + if !ok { + return nil, errObj + } + return []string{s.Index}, nil +} + +func epNameNamespaceIndexFunc(obj interface{}) ([]string, error) { + s, ok := obj.(*object.Endpoints) + if !ok { + return nil, errObj + } + return []string{s.Index}, nil +} + +func epIPIndexFunc(obj interface{}) ([]string, error) { + ep, ok := obj.(*object.Endpoints) + if !ok { + return nil, errObj + } + return ep.IndexIP, nil +} + +func serviceListFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(meta.ListOptions) (runtime.Object, error) { + return func(opts meta.ListOptions) (runtime.Object, error) { + if s != nil { + opts.LabelSelector = s.String() + } + return c.CoreV1().Services(ns).List(ctx, opts) + } +} + +func podListFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(meta.ListOptions) (runtime.Object, error) { + return func(opts meta.ListOptions) (runtime.Object, error) { + if s != nil { + opts.LabelSelector = s.String() + } + if len(opts.FieldSelector) > 0 { + opts.FieldSelector = opts.FieldSelector + "," + } + opts.FieldSelector = opts.FieldSelector + "status.phase!=Succeeded,status.phase!=Failed,status.phase!=Unknown" + return c.CoreV1().Pods(ns).List(ctx, opts) + } +} +func endpointSliceListFuncV1beta1(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(meta.ListOptions) (runtime.Object, error) { + return func(opts meta.ListOptions) (runtime.Object, error) { + if s != nil { + opts.LabelSelector = s.String() + } + return c.DiscoveryV1beta1().EndpointSlices(ns).List(ctx, opts) + } +} + +func endpointSliceListFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(meta.ListOptions) (runtime.Object, error) { + return func(opts meta.ListOptions) (runtime.Object, error) { + if s != nil { + opts.LabelSelector = s.String() + } + return c.DiscoveryV1().EndpointSlices(ns).List(ctx, opts) + } +} + +func endpointsListFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(meta.ListOptions) (runtime.Object, error) { + return func(opts meta.ListOptions) (runtime.Object, error) { + if s != nil { + opts.LabelSelector = s.String() + } + return c.CoreV1().Endpoints(ns).List(ctx, opts) + } +} + +func namespaceListFunc(ctx context.Context, c kubernetes.Interface, s labels.Selector) func(meta.ListOptions) (runtime.Object, error) { + return func(opts meta.ListOptions) (runtime.Object, error) { + if s != nil { + opts.LabelSelector = s.String() + } + return c.CoreV1().Namespaces().List(ctx, opts) + } +} + +func serviceWatchFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(options meta.ListOptions) (watch.Interface, error) { + return func(options meta.ListOptions) (watch.Interface, error) { + if s != nil { + options.LabelSelector = s.String() + } + return c.CoreV1().Services(ns).Watch(ctx, options) + } +} + +func podWatchFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(options meta.ListOptions) (watch.Interface, error) { + return func(options meta.ListOptions) (watch.Interface, error) { + if s != nil { + options.LabelSelector = s.String() + } + if len(options.FieldSelector) > 0 { + options.FieldSelector = options.FieldSelector + "," + } + options.FieldSelector = options.FieldSelector + "status.phase!=Succeeded,status.phase!=Failed,status.phase!=Unknown" + return c.CoreV1().Pods(ns).Watch(ctx, options) + } +} + +func endpointSliceWatchFuncV1beta1(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(options meta.ListOptions) (watch.Interface, error) { + return func(options meta.ListOptions) (watch.Interface, error) { + if s != nil { + options.LabelSelector = s.String() + } + return c.DiscoveryV1beta1().EndpointSlices(ns).Watch(ctx, options) + } +} + +func endpointSliceWatchFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(options meta.ListOptions) (watch.Interface, error) { + return func(options meta.ListOptions) (watch.Interface, error) { + if s != nil { + options.LabelSelector = s.String() + } + return c.DiscoveryV1().EndpointSlices(ns).Watch(ctx, options) + } +} + +func endpointsWatchFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(options meta.ListOptions) (watch.Interface, error) { + return func(options meta.ListOptions) (watch.Interface, error) { + if s != nil { + options.LabelSelector = s.String() + } + return c.CoreV1().Endpoints(ns).Watch(ctx, options) + } +} + +func namespaceWatchFunc(ctx context.Context, c kubernetes.Interface, s labels.Selector) func(options meta.ListOptions) (watch.Interface, error) { + return func(options meta.ListOptions) (watch.Interface, error) { + if s != nil { + options.LabelSelector = s.String() + } + return c.CoreV1().Namespaces().Watch(ctx, options) + } +} + +// Stop stops the controller. +func (dns *dnsControl) Stop() error { + dns.stopLock.Lock() + defer dns.stopLock.Unlock() + + // Only try draining the workqueue if we haven't already. + if !dns.shutdown { + close(dns.stopCh) + dns.shutdown = true + + return nil + } + + return fmt.Errorf("shutdown already in progress") +} + +// Run starts the controller. +func (dns *dnsControl) Run() { + go dns.svcController.Run(dns.stopCh) + if dns.epController != nil { + go func() { + dns.epLock.RLock() + dns.epController.Run(dns.stopCh) + dns.epLock.RUnlock() + }() + } + if dns.podController != nil { + go dns.podController.Run(dns.stopCh) + } + go dns.nsController.Run(dns.stopCh) + <-dns.stopCh +} + +// HasSynced calls on all controllers. +func (dns *dnsControl) HasSynced() bool { + a := dns.svcController.HasSynced() + b := true + if dns.epController != nil { + dns.epLock.RLock() + b = dns.epController.HasSynced() + dns.epLock.RUnlock() + } + c := true + if dns.podController != nil { + c = dns.podController.HasSynced() + } + d := dns.nsController.HasSynced() + return a && b && c && d +} + +func (dns *dnsControl) ServiceList() (svcs []*object.Service) { + os := dns.svcLister.List() + for _, o := range os { + s, ok := o.(*object.Service) + if !ok { + continue + } + svcs = append(svcs, s) + } + return svcs +} + +func (dns *dnsControl) EndpointsList() (eps []*object.Endpoints) { + dns.epLock.RLock() + defer dns.epLock.RUnlock() + os := dns.epLister.List() + for _, o := range os { + ep, ok := o.(*object.Endpoints) + if !ok { + continue + } + eps = append(eps, ep) + } + return eps +} + +func (dns *dnsControl) PodIndex(ip string) (pods []*object.Pod) { + os, err := dns.podLister.ByIndex(podIPIndex, ip) + if err != nil { + return nil + } + for _, o := range os { + p, ok := o.(*object.Pod) + if !ok { + continue + } + pods = append(pods, p) + } + return pods +} + +func (dns *dnsControl) SvcIndex(idx string) (svcs []*object.Service) { + os, err := dns.svcLister.ByIndex(svcNameNamespaceIndex, idx) + if err != nil { + return nil + } + for _, o := range os { + s, ok := o.(*object.Service) + if !ok { + continue + } + svcs = append(svcs, s) + } + return svcs +} + +func (dns *dnsControl) SvcIndexReverse(ip string) (svcs []*object.Service) { + os, err := dns.svcLister.ByIndex(svcIPIndex, ip) + if err != nil { + return nil + } + + for _, o := range os { + s, ok := o.(*object.Service) + if !ok { + continue + } + svcs = append(svcs, s) + } + return svcs +} + +func (dns *dnsControl) SvcExtIndexReverse(ip string) (svcs []*object.Service) { + os, err := dns.svcLister.ByIndex(svcExtIPIndex, ip) + if err != nil { + return nil + } + + for _, o := range os { + s, ok := o.(*object.Service) + if !ok { + continue + } + svcs = append(svcs, s) + } + return svcs +} + +func (dns *dnsControl) EpIndex(idx string) (ep []*object.Endpoints) { + dns.epLock.RLock() + defer dns.epLock.RUnlock() + os, err := dns.epLister.ByIndex(epNameNamespaceIndex, idx) + if err != nil { + return nil + } + for _, o := range os { + e, ok := o.(*object.Endpoints) + if !ok { + continue + } + ep = append(ep, e) + } + return ep +} + +func (dns *dnsControl) EpIndexReverse(ip string) (ep []*object.Endpoints) { + dns.epLock.RLock() + defer dns.epLock.RUnlock() + os, err := dns.epLister.ByIndex(epIPIndex, ip) + if err != nil { + return nil + } + for _, o := range os { + e, ok := o.(*object.Endpoints) + if !ok { + continue + } + ep = append(ep, e) + } + return ep +} + +// GetNodeByName return the node by name. If nothing is found an error is +// returned. This query causes a roundtrip to the k8s API server, so use +// sparingly. Currently this is only used for Federation. +func (dns *dnsControl) GetNodeByName(ctx context.Context, name string) (*api.Node, error) { + v1node, err := dns.client.CoreV1().Nodes().Get(ctx, name, meta.GetOptions{}) + return v1node, err +} + +// GetNamespaceByName returns the namespace by name. If nothing is found an error is returned. +func (dns *dnsControl) GetNamespaceByName(name string) (*object.Namespace, error) { + o, exists, err := dns.nsLister.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, fmt.Errorf("namespace not found") + } + ns, ok := o.(*object.Namespace) + if !ok { + return nil, fmt.Errorf("found key but not namespace") + } + return ns, nil +} + +func (dns *dnsControl) Add(obj interface{}) { dns.updateModified() } +func (dns *dnsControl) Delete(obj interface{}) { dns.updateModified() } +func (dns *dnsControl) Update(oldObj, newObj interface{}) { dns.detectChanges(oldObj, newObj) } + +// detectChanges detects changes in objects, and updates the modified timestamp +func (dns *dnsControl) detectChanges(oldObj, newObj interface{}) { + // If both objects have the same resource version, they are identical. + if newObj != nil && oldObj != nil && (oldObj.(meta.Object).GetResourceVersion() == newObj.(meta.Object).GetResourceVersion()) { + return + } + obj := newObj + if obj == nil { + obj = oldObj + } + switch ob := obj.(type) { + case *object.Service: + imod, emod := serviceModified(oldObj, newObj) + if imod { + dns.updateModified() + } + if emod { + dns.updateExtModifed() + } + case *object.Pod: + dns.updateModified() + case *object.Endpoints: + if !endpointsEquivalent(oldObj.(*object.Endpoints), newObj.(*object.Endpoints)) { + dns.updateModified() + } + default: + log.Warningf("Updates for %T not supported.", ob) + } +} + +// subsetsEquivalent checks if two endpoint subsets are significantly equivalent +// I.e. that they have the same ready addresses, host names, ports (including protocol +// and service names for SRV) +func subsetsEquivalent(sa, sb object.EndpointSubset) bool { + if len(sa.Addresses) != len(sb.Addresses) { + return false + } + if len(sa.Ports) != len(sb.Ports) { + return false + } + + // in Addresses and Ports, we should be able to rely on + // these being sorted and able to be compared + // they are supposed to be in a canonical format + for addr, aaddr := range sa.Addresses { + baddr := sb.Addresses[addr] + if aaddr.IP != baddr.IP { + return false + } + if aaddr.Hostname != baddr.Hostname { + return false + } + } + + for port, aport := range sa.Ports { + bport := sb.Ports[port] + if aport.Name != bport.Name { + return false + } + if aport.Port != bport.Port { + return false + } + if aport.Protocol != bport.Protocol { + return false + } + } + return true +} + +// endpointsEquivalent checks if the update to an endpoint is something +// that matters to us or if they are effectively equivalent. +func endpointsEquivalent(a, b *object.Endpoints) bool { + if a == nil || b == nil { + return false + } + + if len(a.Subsets) != len(b.Subsets) { + return false + } + + // we should be able to rely on + // these being sorted and able to be compared + // they are supposed to be in a canonical format + for i, sa := range a.Subsets { + sb := b.Subsets[i] + if !subsetsEquivalent(sa, sb) { + return false + } + } + return true +} + +// serviceModified checks the services passed for changes that result in changes +// to internal and or external records. It returns two booleans, one for internal +// record changes, and a second for external record changes +func serviceModified(oldObj, newObj interface{}) (intSvc, extSvc bool) { + if oldObj != nil && newObj == nil { + // deleted service only modifies external zone records if it had external ips + return true, len(oldObj.(*object.Service).ExternalIPs) > 0 + } + + if oldObj == nil && newObj != nil { + // added service only modifies external zone records if it has external ips + return true, len(newObj.(*object.Service).ExternalIPs) > 0 + } + + newSvc := newObj.(*object.Service) + oldSvc := oldObj.(*object.Service) + + // External IPs are mutable, affecting external zone records + if len(oldSvc.ExternalIPs) != len(newSvc.ExternalIPs) { + extSvc = true + } else { + for i := range oldSvc.ExternalIPs { + if oldSvc.ExternalIPs[i] != newSvc.ExternalIPs[i] { + extSvc = true + break + } + } + } + + // ExternalName is mutable, affecting internal zone records + intSvc = oldSvc.ExternalName != newSvc.ExternalName + + if intSvc && extSvc { + return intSvc, extSvc + } + + // All Port fields are mutable, affecting both internal/external zone records + if len(oldSvc.Ports) != len(newSvc.Ports) { + return true, true + } + for i := range oldSvc.Ports { + if oldSvc.Ports[i].Name != newSvc.Ports[i].Name { + return true, true + } + if oldSvc.Ports[i].Port != newSvc.Ports[i].Port { + return true, true + } + if oldSvc.Ports[i].Protocol != newSvc.Ports[i].Protocol { + return true, true + } + } + + return intSvc, extSvc +} + +func (dns *dnsControl) Modified(external bool) int64 { + if external { + return atomic.LoadInt64(&dns.extModified) + } + return atomic.LoadInt64(&dns.modified) +} + +// updateModified set dns.modified to the current time. +func (dns *dnsControl) updateModified() { + unix := time.Now().Unix() + atomic.StoreInt64(&dns.modified, unix) +} + +// updateExtModified set dns.extModified to the current time. +func (dns *dnsControl) updateExtModifed() { + unix := time.Now().Unix() + atomic.StoreInt64(&dns.extModified, unix) +} + +var errObj = errors.New("obj was not of the correct type") diff --git a/ag_201_coredns/plugin/kubernetes/controller_test.go b/ag_201_coredns/plugin/kubernetes/controller_test.go new file mode 100644 index 0000000..469eb59 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/controller_test.go @@ -0,0 +1,238 @@ +package kubernetes + +import ( + "context" + "net" + "strconv" + "testing" + + "github.com/coredns/coredns/plugin/kubernetes/object" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" + api "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" +) + +func inc(ip net.IP) { + for j := len(ip) - 1; j >= 0; j-- { + ip[j]++ + if ip[j] > 0 { + break + } + } +} + +func BenchmarkController(b *testing.B) { + client := fake.NewSimpleClientset() + dco := dnsControlOpts{ + zones: []string{"cluster.local."}, + } + ctx := context.Background() + controller := newdnsController(ctx, client, dco) + cidr := "10.0.0.0/19" + + // Add resources + generateEndpoints(cidr, client) + generateSvcs(cidr, "all", client) + m := new(dns.Msg) + m.SetQuestion("svc1.testns.svc.cluster.local.", dns.TypeA) + k := New([]string{"cluster.local."}) + k.APIConn = controller + rw := &test.ResponseWriter{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + k.ServeDNS(ctx, rw, m) + } +} + +func generateEndpoints(cidr string, client kubernetes.Interface) { + // https://groups.google.com/d/msg/golang-nuts/zlcYA4qk-94/TWRFHeXJCcYJ + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + log.Fatal(err) + } + + count := 1 + ep := &api.Endpoints{ + Subsets: []api.EndpointSubset{{ + Ports: []api.EndpointPort{ + { + Port: 80, + Protocol: "tcp", + Name: "http", + }, + }, + }}, + ObjectMeta: meta.ObjectMeta{ + Namespace: "testns", + }, + } + ctx := context.TODO() + for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { + ep.Subsets[0].Addresses = []api.EndpointAddress{ + { + IP: ip.String(), + Hostname: "foo" + strconv.Itoa(count), + }, + } + ep.ObjectMeta.Name = "svc" + strconv.Itoa(count) + client.CoreV1().Endpoints("testns").Create(ctx, ep, meta.CreateOptions{}) + count++ + } +} + +func generateSvcs(cidr string, svcType string, client kubernetes.Interface) { + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + log.Fatal(err) + } + + count := 1 + switch svcType { + case "clusterip": + for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { + createClusterIPSvc(count, client, ip) + count++ + } + case "headless": + for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { + createHeadlessSvc(count, client, ip) + count++ + } + case "external": + for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { + createExternalSvc(count, client, ip) + count++ + } + default: + for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { + if count%3 == 0 { + createClusterIPSvc(count, client, ip) + } else if count%3 == 1 { + createHeadlessSvc(count, client, ip) + } else if count%3 == 2 { + createExternalSvc(count, client, ip) + } + count++ + } + } +} + +func createClusterIPSvc(suffix int, client kubernetes.Interface, ip net.IP) { + ctx := context.TODO() + client.CoreV1().Services("testns").Create(ctx, &api.Service{ + ObjectMeta: meta.ObjectMeta{ + Name: "svc" + strconv.Itoa(suffix), + Namespace: "testns", + }, + Spec: api.ServiceSpec{ + ClusterIP: ip.String(), + Ports: []api.ServicePort{{ + Name: "http", + Protocol: "tcp", + Port: 80, + }}, + }, + }, meta.CreateOptions{}) +} + +func createHeadlessSvc(suffix int, client kubernetes.Interface, ip net.IP) { + ctx := context.TODO() + client.CoreV1().Services("testns").Create(ctx, &api.Service{ + ObjectMeta: meta.ObjectMeta{ + Name: "hdls" + strconv.Itoa(suffix), + Namespace: "testns", + }, + Spec: api.ServiceSpec{ + ClusterIP: api.ClusterIPNone, + }, + }, meta.CreateOptions{}) +} + +func createExternalSvc(suffix int, client kubernetes.Interface, ip net.IP) { + ctx := context.TODO() + client.CoreV1().Services("testns").Create(ctx, &api.Service{ + ObjectMeta: meta.ObjectMeta{ + Name: "external" + strconv.Itoa(suffix), + Namespace: "testns", + }, + Spec: api.ServiceSpec{ + ExternalName: "coredns" + strconv.Itoa(suffix) + ".io", + Ports: []api.ServicePort{{ + Name: "http", + Protocol: "tcp", + Port: 80, + }}, + Type: api.ServiceTypeExternalName, + }, + }, meta.CreateOptions{}) +} + +func TestServiceModified(t *testing.T) { + var tests = []struct { + oldSvc interface{} + newSvc interface{} + ichanged bool + echanged bool + }{ + { + oldSvc: nil, + newSvc: &object.Service{}, + ichanged: true, + echanged: false, + }, + { + oldSvc: &object.Service{}, + newSvc: nil, + ichanged: true, + echanged: false, + }, + { + oldSvc: nil, + newSvc: &object.Service{ExternalIPs: []string{"10.0.0.1"}}, + ichanged: true, + echanged: true, + }, + { + oldSvc: &object.Service{ExternalIPs: []string{"10.0.0.1"}}, + newSvc: nil, + ichanged: true, + echanged: true, + }, + { + oldSvc: &object.Service{ExternalIPs: []string{"10.0.0.1"}}, + newSvc: &object.Service{ExternalIPs: []string{"10.0.0.2"}}, + ichanged: false, + echanged: true, + }, + { + oldSvc: &object.Service{ExternalName: "10.0.0.1"}, + newSvc: &object.Service{ExternalName: "10.0.0.2"}, + ichanged: true, + echanged: false, + }, + { + oldSvc: &object.Service{Ports: []api.ServicePort{{Name: "test1"}}}, + newSvc: &object.Service{Ports: []api.ServicePort{{Name: "test2"}}}, + ichanged: true, + echanged: true, + }, + { + oldSvc: &object.Service{Ports: []api.ServicePort{{Name: "test1"}}}, + newSvc: &object.Service{Ports: []api.ServicePort{{Name: "test2"}, {Name: "test3"}}}, + ichanged: true, + echanged: true, + }, + } + + for i, test := range tests { + ichanged, echanged := serviceModified(test.oldSvc, test.newSvc) + if test.ichanged != ichanged || test.echanged != echanged { + t.Errorf("Expected %v, %v for test %v. Got %v, %v", test.ichanged, test.echanged, i, ichanged, echanged) + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/external.go b/ag_201_coredns/plugin/kubernetes/external.go new file mode 100644 index 0000000..b6531ab --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/external.go @@ -0,0 +1,236 @@ +package kubernetes + +import ( + "strings" + + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/kubernetes/object" + "github.com/coredns/coredns/plugin/pkg/dnsutil" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Those constants are used to distinguish between records in ExternalServices headless +// return values. +// They are always appendedn to key in a map which is +// either base service key eg. /com/example/namespace/service/endpoint or +// /com/example/namespace/service/_http/_tcp/port.protocol +// this will allow us to distinguish services in implementation of Transfer protocol +// see plugin/k8s_external/transfer.go +const ( + Endpoint = "endpoint" + PortProtocol = "port.protocol" +) + +// External implements the ExternalFunc call from the external plugin. +// It returns any services matching in the services' ExternalIPs and if enabled, headless endpoints.. +func (k *Kubernetes) External(state request.Request, headless bool) ([]msg.Service, int) { + if state.QType() == dns.TypePTR { + ip := dnsutil.ExtractAddressFromReverse(state.Name()) + if ip != "" { + svcs, err := k.ExternalReverse(ip) + if err != nil { + return nil, dns.RcodeNameError + } + return svcs, dns.RcodeSuccess + } + // for invalid reverse names, fall through to determine proper nxdomain/nodata response + } + + base, _ := dnsutil.TrimZone(state.Name(), state.Zone) + + segs := dns.SplitDomainName(base) + last := len(segs) - 1 + if last < 0 { + return nil, dns.RcodeServerFailure + } + // We are dealing with a fairly normal domain name here, but we still need to have the service, + // namespace and if present, endpoint: + // service.namespace. or + // endpoint.service.namespace. + var port, protocol, endpoint string + namespace := segs[last] + if !k.namespaceExposed(namespace) { + return nil, dns.RcodeNameError + } + + last-- + if last < 0 { + return nil, dns.RcodeSuccess + } + + service := segs[last] + last-- + if last == 0 { + endpoint = stripUnderscore(segs[last]) + last-- + } else if last == 1 { + protocol = stripUnderscore(segs[last]) + port = stripUnderscore(segs[last-1]) + last -= 2 + } + + if last != -1 { + // too long + return nil, dns.RcodeNameError + } + + var ( + endpointsList []*object.Endpoints + serviceList []*object.Service + ) + + idx := object.ServiceKey(service, namespace) + serviceList = k.APIConn.SvcIndex(idx) + + services := []msg.Service{} + zonePath := msg.Path(state.Zone, coredns) + rcode := dns.RcodeNameError + + for _, svc := range serviceList { + if namespace != svc.Namespace { + continue + } + if service != svc.Name { + continue + } + + if headless && len(svc.ExternalIPs) == 0 && (svc.Headless() || endpoint != "") { + if endpointsList == nil { + endpointsList = k.APIConn.EpIndex(idx) + } + // Endpoint query or headless service + for _, ep := range endpointsList { + if object.EndpointsKey(svc.Name, svc.Namespace) != ep.Index { + continue + } + + for _, eps := range ep.Subsets { + for _, addr := range eps.Addresses { + if endpoint != "" && !match(endpoint, endpointHostname(addr, k.endpointNameMode)) { + continue + } + + for _, p := range eps.Ports { + if !(matchPortAndProtocol(port, p.Name, protocol, p.Protocol)) { + continue + } + s := msg.Service{Host: addr.IP, Port: int(p.Port), TTL: k.ttl} + s.Key = strings.Join([]string{zonePath, svc.Namespace, svc.Name, endpointHostname(addr, k.endpointNameMode)}, "/") + + services = append(services, s) + } + } + } + } + continue + } else { + for _, ip := range svc.ExternalIPs { + for _, p := range svc.Ports { + if !(matchPortAndProtocol(port, p.Name, protocol, string(p.Protocol))) { + continue + } + rcode = dns.RcodeSuccess + s := msg.Service{Host: ip, Port: int(p.Port), TTL: k.ttl} + s.Key = strings.Join([]string{zonePath, svc.Namespace, svc.Name}, "/") + + services = append(services, s) + } + } + } + } + if state.QType() == dns.TypePTR { + // if this was a PTR request, return empty service list, but retain rcode for proper nxdomain/nodata response + return nil, rcode + } + return services, rcode +} + +// ExternalAddress returns the external service address(es) for the CoreDNS service. +func (k *Kubernetes) ExternalAddress(state request.Request, headless bool) []dns.RR { + // If CoreDNS is running inside the Kubernetes cluster: k.nsAddrs() will return the external IPs of the services + // targeting the CoreDNS Pod. + // If CoreDNS is running outside of the Kubernetes cluster: k.nsAddrs() will return the first non-loopback IP + // address seen on the local system it is running on. This could be the wrong answer if coredns is using the *bind* + // plugin to bind to a different IP address. + return k.nsAddrs(true, headless, state.Zone) +} + +// ExternalServices returns all services with external IPs and if enabled headless services +func (k *Kubernetes) ExternalServices(zone string, headless bool) (services []msg.Service, headlessServices map[string][]msg.Service) { + zonePath := msg.Path(zone, coredns) + headlessServices = make(map[string][]msg.Service) + for _, svc := range k.APIConn.ServiceList() { + // Endpoints and headless services + if headless && len(svc.ExternalIPs) == 0 && svc.Headless() { + idx := object.ServiceKey(svc.Name, svc.Namespace) + endpointsList := k.APIConn.EpIndex(idx) + + for _, ep := range endpointsList { + for _, eps := range ep.Subsets { + for _, addr := range eps.Addresses { + // we need to have some answers grouped together + // 1. for endpoint requests eg. endpoint-0.service.example.com - will always have one endpoint + // 2. for service requests eg. service.example.com - can have multiple endpoints + // 3. for port.protocol requests eg. _http._tcp.service.example.com - can have multiple endpoints + for _, p := range eps.Ports { + s := msg.Service{Host: addr.IP, Port: int(p.Port), TTL: k.ttl} + baseSvc := strings.Join([]string{zonePath, svc.Namespace, svc.Name}, "/") + s.Key = strings.Join([]string{baseSvc, endpointHostname(addr, k.endpointNameMode)}, "/") + headlessServices[strings.Join([]string{baseSvc, Endpoint}, "/")] = append(headlessServices[strings.Join([]string{baseSvc, Endpoint}, "/")], s) + + // As per spec unnamed ports do not have a srv record + // https://github.com/kubernetes/dns/blob/master/docs/specification.md#232---srv-records + if p.Name == "" { + continue + } + s.Host = msg.Domain(s.Key) + s.Key = strings.Join(append([]string{zonePath, svc.Namespace, svc.Name}, strings.ToLower("_"+string(p.Protocol)), strings.ToLower("_"+string(p.Name))), "/") + headlessServices[strings.Join([]string{s.Key, PortProtocol}, "/")] = append(headlessServices[strings.Join([]string{s.Key, PortProtocol}, "/")], s) + } + } + } + } + continue + } else { + for _, ip := range svc.ExternalIPs { + for _, p := range svc.Ports { + s := msg.Service{Host: ip, Port: int(p.Port), TTL: k.ttl} + s.Key = strings.Join([]string{zonePath, svc.Namespace, svc.Name}, "/") + services = append(services, s) + s.Key = strings.Join(append([]string{zonePath, svc.Namespace, svc.Name}, strings.ToLower("_"+string(p.Protocol)), strings.ToLower("_"+string(p.Name))), "/") + s.TargetStrip = 2 + services = append(services, s) + } + } + } + } + return services, headlessServices +} + +//ExternalSerial returns the serial of the external zone +func (k *Kubernetes) ExternalSerial(string) uint32 { + return uint32(k.APIConn.Modified(true)) +} + +// ExternalReverse does a reverse lookup for the external IPs +func (k *Kubernetes) ExternalReverse(ip string) ([]msg.Service, error) { + records := k.serviceRecordForExternalIP(ip) + if len(records) == 0 { + return records, errNoItems + } + return records, nil +} + +func (k *Kubernetes) serviceRecordForExternalIP(ip string) []msg.Service { + var svcs []msg.Service + for _, service := range k.APIConn.SvcExtIndexReverse(ip) { + if len(k.Namespaces) > 0 && !k.namespaceExposed(service.Namespace) { + continue + } + domain := strings.Join([]string{service.Name, service.Namespace}, ".") + svcs = append(svcs, msg.Service{Host: domain, TTL: k.ttl}) + } + return svcs +} diff --git a/ag_201_coredns/plugin/kubernetes/external_test.go b/ag_201_coredns/plugin/kubernetes/external_test.go new file mode 100644 index 0000000..670d2b9 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/external_test.go @@ -0,0 +1,199 @@ +package kubernetes + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/kubernetes/object" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + api "k8s.io/api/core/v1" +) + +var extCases = []struct { + Qname string + Qtype uint16 + Msg []msg.Service + Rcode int +}{ + { + Qname: "svc1.testns.example.org.", Rcode: dns.RcodeSuccess, + Msg: []msg.Service{ + {Host: "1.2.3.4", Port: 80, TTL: 5, Key: "/c/org/example/testns/svc1"}, + }, + }, + { + Qname: "svc6.testns.example.org.", Rcode: dns.RcodeSuccess, + Msg: []msg.Service{ + {Host: "1:2::5", Port: 80, TTL: 5, Key: "/c/org/example/testns/svc1"}, + }, + }, + { + Qname: "_http._tcp.svc1.testns.example.com.", Rcode: dns.RcodeSuccess, + Msg: []msg.Service{ + {Host: "1.2.3.4", Port: 80, TTL: 5, Key: "/c/org/example/testns/svc1"}, + }, + }, + { + Qname: "svc0.testns.example.com.", Rcode: dns.RcodeNameError, + }, + { + Qname: "svc0.svc-nons.example.com.", Rcode: dns.RcodeNameError, + }, + { + Qname: "svc-headless.testns.example.com.", Rcode: dns.RcodeSuccess, + Msg: []msg.Service{ + {Host: "1.2.3.4", Port: 80, TTL: 5, Weight: 50, Key: "/c/org/example/testns/svc-headless"}, + {Host: "1.2.3.5", Port: 80, TTL: 5, Weight: 50, Key: "/c/org/example/testns/svc-headless"}, + }, + }, + { + Qname: "endpoint-0.svc-headless.testns.example.com.", Rcode: dns.RcodeSuccess, + Msg: []msg.Service{ + {Host: "1.2.3.4", Port: 80, TTL: 5, Weight: 100, Key: "/c/org/example/testns/svc-headless/endpoint-0"}, + }, + }, + { + Qname: "endpoint-1.svc-nons.testns.example.com.", Rcode: dns.RcodeNameError, + }, +} + +func TestExternal(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &external{} + k.Next = test.NextHandler(dns.RcodeSuccess, nil) + k.Namespaces = map[string]struct{}{"testns": {}} + + for i, tc := range extCases { + state := testRequest(tc.Qname) + + svc, rcode := k.External(state, true) + + if x := tc.Rcode; x != rcode { + t.Errorf("Test %d, expected rcode %d, got %d", i, x, rcode) + } + + if len(svc) != len(tc.Msg) { + t.Errorf("Test %d, expected %d for messages, got %d", i, len(tc.Msg), len(svc)) + } + + for j, s := range svc { + if x := tc.Msg[j].Key; x != s.Key { + t.Errorf("Test %d, expected key %s, got %s", i, x, s.Key) + } + return + } + } +} + +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) SvcExtIndexReverse(string) []*object.Service { return nil } +func (external) Modified(bool) int64 { return 0 } +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) (*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": { + { + Name: "svc1", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{"10.0.0.1"}, + ExternalIPs: []string{"1.2.3.4"}, + Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, + }, + }, + "svc6.testns": { + { + Name: "svc6", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{"10.0.0.3"}, + ExternalIPs: []string{"1:2::5"}, + Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, + }, + }, + "svc-headless.testns": { + { + Name: "svc-headless", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{api.ClusterIPNone}, + Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, + }, + }, +} + +func (external) ServiceList() []*object.Service { + var svcs []*object.Service + for _, svc := range svcIndexExternal { + svcs = append(svcs, svc...) + } + return svcs +} + +func testRequest(name string) request.Request { + m := new(dns.Msg).SetQuestion(name, dns.TypeA) + return request.Request{W: &test.ResponseWriter{}, Req: m, Zone: "example.org."} +} diff --git a/ag_201_coredns/plugin/kubernetes/handler.go b/ag_201_coredns/plugin/kubernetes/handler.go new file mode 100644 index 0000000..d673a7a --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/handler.go @@ -0,0 +1,94 @@ +package kubernetes + +import ( + "context" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// ServeDNS implements the plugin.Handler interface. +func (k Kubernetes) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + + qname := state.QName() + zone := plugin.Zones(k.Zones).Matches(qname) + if zone == "" { + return plugin.NextOrFailure(k.Name(), k.Next, ctx, w, r) + } + zone = qname[len(qname)-len(zone):] // maintain case of original query + state.Zone = zone + + var ( + records []dns.RR + extra []dns.RR + truncated bool + err error + ) + + switch state.QType() { + case dns.TypeA: + records, truncated, err = plugin.A(ctx, &k, zone, state, nil, plugin.Options{}) + case dns.TypeAAAA: + records, truncated, err = plugin.AAAA(ctx, &k, zone, state, nil, plugin.Options{}) + case dns.TypeTXT: + records, truncated, err = plugin.TXT(ctx, &k, zone, state, nil, plugin.Options{}) + case dns.TypeCNAME: + records, err = plugin.CNAME(ctx, &k, zone, state, plugin.Options{}) + case dns.TypePTR: + records, err = plugin.PTR(ctx, &k, zone, state, plugin.Options{}) + case dns.TypeMX: + records, extra, err = plugin.MX(ctx, &k, zone, state, plugin.Options{}) + case dns.TypeSRV: + records, extra, err = plugin.SRV(ctx, &k, zone, state, plugin.Options{}) + case dns.TypeSOA: + if qname == zone { + records, err = plugin.SOA(ctx, &k, zone, state, plugin.Options{}) + } + case dns.TypeAXFR, dns.TypeIXFR: + return dns.RcodeRefused, nil + case dns.TypeNS: + if state.Name() == zone { + records, extra, err = plugin.NS(ctx, &k, zone, state, plugin.Options{}) + break + } + fallthrough + default: + // Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN + fake := state.NewWithQuestion(state.QName(), dns.TypeA) + fake.Zone = state.Zone + _, _, err = plugin.A(ctx, &k, zone, fake, nil, plugin.Options{}) + } + + if k.IsNameError(err) { + if k.Fall.Through(state.Name()) { + return plugin.NextOrFailure(k.Name(), k.Next, ctx, w, r) + } + if !k.APIConn.HasSynced() { + // If we haven't synchronized with the kubernetes cluster, return server failure + return plugin.BackendError(ctx, &k, zone, dns.RcodeServerFailure, state, nil /* err */, plugin.Options{}) + } + return plugin.BackendError(ctx, &k, zone, dns.RcodeNameError, state, nil /* err */, plugin.Options{}) + } + if err != nil { + return dns.RcodeServerFailure, err + } + + if len(records) == 0 { + return plugin.BackendError(ctx, &k, zone, dns.RcodeSuccess, state, nil, plugin.Options{}) + } + + 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...) + w.WriteMsg(m) + return dns.RcodeSuccess, nil +} + +// Name implements the Handler interface. +func (k Kubernetes) Name() string { return "kubernetes" } diff --git a/ag_201_coredns/plugin/kubernetes/handler_case_test.go b/ag_201_coredns/plugin/kubernetes/handler_case_test.go new file mode 100644 index 0000000..c3f90f1 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/handler_case_test.go @@ -0,0 +1,80 @@ +package kubernetes + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var dnsPreserveCaseCases = []test.Case{ + // Negative response + { + Qname: "not-a-service.testns.svc.ClUsTeR.lOcAl.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("ClUsTeR.lOcAl. 5 IN SOA ns.dns.ClUsTeR.lOcAl. hostmaster.ClUsTeR.lOcAl. 1499347823 7200 1800 86400 5"), + }, + }, + // A Service + { + Qname: "SvC1.TeStNs.SvC.cLuStEr.LoCaL.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("SvC1.TeStNs.SvC.cLuStEr.LoCaL. 5 IN A 10.0.0.1"), + }, + }, + // SRV Service + { + Qname: "_HtTp._TcP.sVc1.TeStNs.SvC.cLuStEr.LoCaL.", Qtype: dns.TypeSRV, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("_HtTp._TcP.sVc1.TeStNs.SvC.cLuStEr.LoCaL. 5 IN SRV 0 100 80 svc1.testns.svc.cLuStEr.LoCaL."), + }, + Extra: []dns.RR{ + test.A("svc1.testns.svc.cLuStEr.LoCaL. 5 IN A 10.0.0.1"), + }, + }, + { + Qname: "Cluster.local.", Qtype: dns.TypeSOA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SOA("Cluster.local. 5 IN SOA ns.dns.Cluster.local. hostmaster.Cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, +} + +func TestPreserveCase(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.opts.ignoreEmptyService = true + k.Next = test.NextHandler(dns.RcodeSuccess, nil) + ctx := context.TODO() + + for i, tc := range dnsPreserveCaseCases { + r := tc.Msg() + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := k.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d expected no error, got %v", i, err) + return + } + if tc.Error != nil { + continue + } + + resp := w.Msg + if resp == nil { + t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name) + } + + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/handler_ignore_emptyservice_test.go b/ag_201_coredns/plugin/kubernetes/handler_ignore_emptyservice_test.go new file mode 100644 index 0000000..7af77fe --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/handler_ignore_emptyservice_test.go @@ -0,0 +1,67 @@ +package kubernetes + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var dnsEmptyServiceTestCases = []test.Case{ + // A Service + { + Qname: "svcempty.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, + // CNAME to external + { + Qname: "external.testns.svc.cluster.local.", Qtype: dns.TypeCNAME, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.CNAME("external.testns.svc.cluster.local. 5 IN CNAME ext.interwebs.test."), + }, + }, +} + +func TestServeDNSEmptyService(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.opts.ignoreEmptyService = true + k.Next = test.NextHandler(dns.RcodeSuccess, nil) + ctx := context.TODO() + + for i, tc := range dnsEmptyServiceTestCases { + r := tc.Msg() + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := k.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d expected no error, got %v", i, err) + return + } + if tc.Error != nil { + continue + } + + resp := w.Msg + if resp == nil { + t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name) + } + + // Before sorting, make sure that CNAMES do not appear after their target records + if err := test.CNAMEOrder(resp); err != nil { + t.Error(err) + } + + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/handler_pod_disabled_test.go b/ag_201_coredns/plugin/kubernetes/handler_pod_disabled_test.go new file mode 100644 index 0000000..be7e7a3 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/handler_pod_disabled_test.go @@ -0,0 +1,60 @@ +package kubernetes + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var podModeDisabledCases = []test.Case{ + { + Qname: "10-240-0-1.podns.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, + { + Qname: "172-0-0-2.podns.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, +} + +func TestServeDNSModeDisabled(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.Next = test.NextHandler(dns.RcodeSuccess, nil) + k.podMode = podModeDisabled + ctx := context.TODO() + + for i, tc := range podModeDisabledCases { + r := tc.Msg() + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := k.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d got unexpected error %v", i, err) + return + } + if tc.Error != nil { + continue + } + + resp := w.Msg + if resp == nil { + t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name) + } + + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/handler_pod_insecure_test.go b/ag_201_coredns/plugin/kubernetes/handler_pod_insecure_test.go new file mode 100644 index 0000000..b01d53f --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/handler_pod_insecure_test.go @@ -0,0 +1,95 @@ +package kubernetes + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var podModeInsecureCases = []test.Case{ + { + Qname: "10-240-0-1.podns.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("10-240-0-1.podns.pod.cluster.local. 5 IN A 10.240.0.1"), + }, + }, + { + Qname: "172-0-0-2.podns.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("172-0-0-2.podns.pod.cluster.local. 5 IN A 172.0.0.2"), + }, + }, + { + Qname: "blah.podns.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1515173576 7200 1800 86400 30"), + }, + }, + { + Qname: "blah.podns.pod.cluster.local.", Qtype: dns.TypeAAAA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1515173576 7200 1800 86400 30"), + }, + }, + { + Qname: "blah.podns.pod.cluster.local.", Qtype: dns.TypeHINFO, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1515173576 7200 1800 86400 30"), + }, + }, + { + Qname: "blah.pod-nons.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1515173576 7200 1800 86400 30"), + }, + }, + { + Qname: "podns.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1515173576 7200 1800 86400 30"), + }, + }, +} + +func TestServeDNSModeInsecure(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.Next = test.NextHandler(dns.RcodeSuccess, nil) + ctx := context.TODO() + k.podMode = podModeInsecure + + for i, tc := range podModeInsecureCases { + r := tc.Msg() + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := k.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d expected no error, got %v", i, err) + return + } + if tc.Error != nil { + continue + } + + resp := w.Msg + if resp == nil { + t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name) + } + + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/handler_pod_verified_test.go b/ag_201_coredns/plugin/kubernetes/handler_pod_verified_test.go new file mode 100644 index 0000000..c8b09c4 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/handler_pod_verified_test.go @@ -0,0 +1,81 @@ +package kubernetes + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var podModeVerifiedCases = []test.Case{ + { + Qname: "10-240-0-1.podns.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("10-240-0-1.podns.pod.cluster.local. 5 IN A 10.240.0.1"), + }, + }, + { + Qname: "podns.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, + { + Qname: "svcns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, + { + Qname: "pod-nons.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, + { + Qname: "172-0-0-2.podns.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, +} + +func TestServeDNSModeVerified(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.Next = test.NextHandler(dns.RcodeSuccess, nil) + ctx := context.TODO() + k.podMode = podModeVerified + + for i, tc := range podModeVerifiedCases { + r := tc.Msg() + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := k.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d expected no error, got %v", i, err) + return + } + if tc.Error != nil { + continue + } + + resp := w.Msg + if resp == nil { + t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name) + } + + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/handler_test.go b/ag_201_coredns/plugin/kubernetes/handler_test.go new file mode 100644 index 0000000..ecf4788 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/handler_test.go @@ -0,0 +1,816 @@ +package kubernetes + +import ( + "context" + "fmt" + "testing" + + "github.com/coredns/coredns/plugin/kubernetes/object" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + api "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type kubeTestCase struct { + Upstream Upstreamer + Truncated bool + test.Case +} + +var dnsTestCases = []kubeTestCase{ + // A Service + {Case: test.Case{ + Qname: "svc1.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("svc1.testns.svc.cluster.local. 5 IN A 10.0.0.1"), + }, + }}, + {Case: test.Case{ + Qname: "svcempty.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("svcempty.testns.svc.cluster.local. 5 IN A 10.0.0.1"), + }, + }}, + {Case: test.Case{ + Qname: "svc1.testns.svc.cluster.local.", Qtype: dns.TypeSRV, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{test.SRV("svc1.testns.svc.cluster.local. 5 IN SRV 0 100 80 svc1.testns.svc.cluster.local.")}, + Extra: []dns.RR{test.A("svc1.testns.svc.cluster.local. 5 IN A 10.0.0.1")}, + }}, + {Case: test.Case{ + Qname: "svcempty.testns.svc.cluster.local.", Qtype: dns.TypeSRV, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{test.SRV("svcempty.testns.svc.cluster.local. 5 IN SRV 0 100 80 svcempty.testns.svc.cluster.local.")}, + Extra: []dns.RR{test.A("svcempty.testns.svc.cluster.local. 5 IN A 10.0.0.1")}, + }}, + {Case: test.Case{ + Qname: "svc6.testns.svc.cluster.local.", Qtype: dns.TypeSRV, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{test.SRV("svc6.testns.svc.cluster.local. 5 IN SRV 0 100 80 svc6.testns.svc.cluster.local.")}, + Extra: []dns.RR{test.AAAA("svc6.testns.svc.cluster.local. 5 IN AAAA 1234:abcd::1")}, + }}, + // SRV Service + {Case: test.Case{ + + Qname: "_http._tcp.svc1.testns.svc.cluster.local.", Qtype: dns.TypeSRV, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("_http._tcp.svc1.testns.svc.cluster.local. 5 IN SRV 0 100 80 svc1.testns.svc.cluster.local."), + }, + Extra: []dns.RR{ + test.A("svc1.testns.svc.cluster.local. 5 IN A 10.0.0.1"), + }, + }}, + {Case: test.Case{ + + Qname: "_http._tcp.svcempty.testns.svc.cluster.local.", Qtype: dns.TypeSRV, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("_http._tcp.svcempty.testns.svc.cluster.local. 5 IN SRV 0 100 80 svcempty.testns.svc.cluster.local."), + }, + Extra: []dns.RR{ + test.A("svcempty.testns.svc.cluster.local. 5 IN A 10.0.0.1"), + }, + }}, + // A Service (Headless) + {Case: test.Case{ + Qname: "hdls1.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.2"), + test.A("hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.3"), + test.A("hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.4"), + test.A("hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.5"), + }, + }}, + // A Service (Headless and Portless) + {Case: test.Case{ + Qname: "hdlsprtls.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("hdlsprtls.testns.svc.cluster.local. 5 IN A 172.0.0.20"), + }, + }}, + // An Endpoint with no port + {Case: test.Case{ + Qname: "172-0-0-20.hdlsprtls.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("172-0-0-20.hdlsprtls.testns.svc.cluster.local. 5 IN A 172.0.0.20"), + }, + }}, + // An Endpoint ip + {Case: test.Case{ + Qname: "172-0-0-2.hdls1.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("172-0-0-2.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.2"), + }, + }}, + // A Endpoint ip + {Case: test.Case{ + Qname: "172-0-0-3.hdls1.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("172-0-0-3.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.3"), + }, + }}, + // An Endpoint by name + {Case: test.Case{ + Qname: "dup-name.hdls1.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("dup-name.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.4"), + test.A("dup-name.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.5"), + }, + }}, + // SRV Service (Headless) + {Case: test.Case{ + Qname: "_http._tcp.hdls1.testns.svc.cluster.local.", Qtype: dns.TypeSRV, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("_http._tcp.hdls1.testns.svc.cluster.local. 5 IN SRV 0 16 80 172-0-0-2.hdls1.testns.svc.cluster.local."), + test.SRV("_http._tcp.hdls1.testns.svc.cluster.local. 5 IN SRV 0 16 80 172-0-0-3.hdls1.testns.svc.cluster.local."), + test.SRV("_http._tcp.hdls1.testns.svc.cluster.local. 5 IN SRV 0 16 80 5678-abcd--1.hdls1.testns.svc.cluster.local."), + test.SRV("_http._tcp.hdls1.testns.svc.cluster.local. 5 IN SRV 0 16 80 5678-abcd--2.hdls1.testns.svc.cluster.local."), + test.SRV("_http._tcp.hdls1.testns.svc.cluster.local. 5 IN SRV 0 16 80 dup-name.hdls1.testns.svc.cluster.local."), + }, + Extra: []dns.RR{ + test.A("172-0-0-2.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.2"), + test.A("172-0-0-3.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.3"), + test.AAAA("5678-abcd--1.hdls1.testns.svc.cluster.local. 5 IN AAAA 5678:abcd::1"), + test.AAAA("5678-abcd--2.hdls1.testns.svc.cluster.local. 5 IN AAAA 5678:abcd::2"), + test.A("dup-name.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.4"), + test.A("dup-name.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.5"), + }, + }}, + {Case: test.Case{ // An A record query for an existing headless service should return a record for each of its ipv4 endpoints + Qname: "hdls1.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.2"), + test.A("hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.3"), + test.A("hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.4"), + test.A("hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.5"), + }, + }}, + // AAAA + {Case: test.Case{ + Qname: "5678-abcd--2.hdls1.testns.svc.cluster.local", Qtype: dns.TypeAAAA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{test.AAAA("5678-abcd--2.hdls1.testns.svc.cluster.local. 5 IN AAAA 5678:abcd::2")}, + }}, + // CNAME External + {Case: test.Case{ + Qname: "external.testns.svc.cluster.local.", Qtype: dns.TypeCNAME, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.CNAME("external.testns.svc.cluster.local. 5 IN CNAME ext.interwebs.test."), + }, + }}, + // CNAME External Truncated Lookup + { + Case: test.Case{ + Qname: "external.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("ext.interwebs.test. 5 IN A 1.2.3.4"), + test.CNAME("external.testns.svc.cluster.local. 5 IN CNAME ext.interwebs.test."), + }, + }, + Upstream: &Upstub{ + Truncated: true, + Qclass: dns.ClassINET, + Case: test.Case{ + Qname: "external.testns.svc.cluster.local.", + Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ext.interwebs.test. 5 IN A 1.2.3.4"), + test.CNAME("external.testns.svc.cluster.local. 5 IN CNAME ext.interwebs.test."), + }, + }, + }, + Truncated: true, + }, + // CNAME External To Internal Service + {Case: test.Case{ + Qname: "external-to-service.testns.svc.cluster.local", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.CNAME("external-to-service.testns.svc.cluster.local. 5 IN CNAME svc1.testns.svc.cluster.local."), + test.A("svc1.testns.svc.cluster.local. 5 IN A 10.0.0.1"), + }, + }}, + // AAAA Service (with an existing A record, but no AAAA record) + {Case: test.Case{ + Qname: "svc1.testns.svc.cluster.local.", Qtype: dns.TypeAAAA, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + // AAAA Service (non-existing service) + {Case: test.Case{ + Qname: "svc0.testns.svc.cluster.local.", Qtype: dns.TypeAAAA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + // A Service (non-existing service) + {Case: test.Case{ + Qname: "svc0.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + // A Service (non-existing namespace) + {Case: test.Case{ + Qname: "svc0.svc-nons.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + // TXT Schema + {Case: test.Case{ + Qname: "dns-version.cluster.local.", Qtype: dns.TypeTXT, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.TXT("dns-version.cluster.local 28800 IN TXT 1.1.0"), + }, + }}, + // A Service (Headless) does not exist + {Case: test.Case{ + Qname: "bogusendpoint.hdls1.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + // A Service does not exist + {Case: test.Case{ + Qname: "bogusendpoint.svc0.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + // AAAA Service + {Case: test.Case{ + Qname: "svc6.testns.svc.cluster.local.", Qtype: dns.TypeAAAA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.AAAA("svc6.testns.svc.cluster.local. 5 IN AAAA 1234:abcd::1"), + }, + }}, + // SRV + {Case: test.Case{ + Qname: "_http._tcp.svc6.testns.svc.cluster.local.", Qtype: dns.TypeSRV, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SRV("_http._tcp.svc6.testns.svc.cluster.local. 5 IN SRV 0 100 80 svc6.testns.svc.cluster.local."), + }, + Extra: []dns.RR{ + test.AAAA("svc6.testns.svc.cluster.local. 5 IN AAAA 1234:abcd::1"), + }, + }}, + // AAAA Service (Headless) + {Case: test.Case{ + Qname: "hdls1.testns.svc.cluster.local.", Qtype: dns.TypeAAAA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.AAAA("hdls1.testns.svc.cluster.local. 5 IN AAAA 5678:abcd::1"), + test.AAAA("hdls1.testns.svc.cluster.local. 5 IN AAAA 5678:abcd::2"), + }, + }}, + // AAAA Endpoint + {Case: test.Case{ + Qname: "5678-abcd--1.hdls1.testns.svc.cluster.local.", Qtype: dns.TypeAAAA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.AAAA("5678-abcd--1.hdls1.testns.svc.cluster.local. 5 IN AAAA 5678:abcd::1"), + }, + }}, + + {Case: test.Case{ + Qname: "svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + {Case: test.Case{ + Qname: "pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + {Case: test.Case{ + Qname: "testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + // NS query for qname != zone (existing domain) + {Case: test.Case{ + Qname: "svc.cluster.local.", Qtype: dns.TypeNS, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + // NS query for qname != zone (existing domain) + {Case: test.Case{ + Qname: "testns.svc.cluster.local.", Qtype: dns.TypeNS, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + // NS query for qname != zone (non existing domain) + {Case: test.Case{ + Qname: "foo.cluster.local.", Qtype: dns.TypeNS, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + // NS query for qname != zone (non existing domain) + {Case: test.Case{ + Qname: "foo.svc.cluster.local.", Qtype: dns.TypeNS, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, + // Dual Stack ClusterIP Services + {Case: test.Case{ + Qname: "svc-dual-stack.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("svc-dual-stack.testns.svc.cluster.local. 5 IN A 10.0.0.3"), + }, + }}, + {Case: test.Case{ + Qname: "svc-dual-stack.testns.svc.cluster.local.", Qtype: dns.TypeAAAA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.AAAA("svc-dual-stack.testns.svc.cluster.local. 5 IN AAAA 10::3"), + }, + }}, + {Case: test.Case{ + Qname: "svc-dual-stack.testns.svc.cluster.local.", Qtype: dns.TypeSRV, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{test.SRV("svc-dual-stack.testns.svc.cluster.local. 5 IN SRV 0 50 80 svc-dual-stack.testns.svc.cluster.local.")}, + Extra: []dns.RR{ + test.A("svc-dual-stack.testns.svc.cluster.local. 5 IN A 10.0.0.3"), + test.AAAA("svc-dual-stack.testns.svc.cluster.local. 5 IN AAAA 10::3"), + }, + }}, + {Case: test.Case{ + Qname: "svc1.testns.svc.cluster.local.", Qtype: dns.TypeSOA, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }}, +} + +func TestServeDNS(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.Next = test.NextHandler(dns.RcodeSuccess, nil) + k.Namespaces = map[string]struct{}{"testns": {}} + ctx := context.TODO() + + for i, tc := range dnsTestCases { + k.Upstream = tc.Upstream + + r := tc.Msg() + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := k.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d expected no error, got %v", i, err) + return + } + if tc.Error != nil { + continue + } + + resp := w.Msg + if resp == nil { + t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name) + } + + if tc.Truncated != resp.Truncated { + t.Errorf("Expected truncation %t, got truncation %t", tc.Truncated, resp.Truncated) + } + + // Before sorting, make sure that CNAMES do not appear after their target records + if err := test.CNAMEOrder(resp); err != nil { + t.Errorf("Test %d, %v", i, err) + } + + if err := test.SortAndCheck(resp, tc.Case); err != nil { + t.Errorf("Test %d, %v", i, err) + } + } +} + +var nsTestCases = []test.Case{ + // A Service for an "exposed" namespace that "does exist" + { + Qname: "svc1.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("svc1.testns.svc.cluster.local. 5 IN A 10.0.0.1"), + }, + }, + // A service for an "exposed" namespace that "doesn't exist" + { + Qname: "svc1.nsnoexist.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 300 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1551484803 7200 1800 86400 30"), + }, + }, +} + +func TestServeNamespaceDNS(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.Next = test.NextHandler(dns.RcodeSuccess, nil) + // if no namespaces are explicitly exposed, then they are all implicitly exposed + k.Namespaces = map[string]struct{}{} + ctx := context.TODO() + + for i, tc := range nsTestCases { + r := tc.Msg() + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := k.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d expected no error, got %v", i, err) + return + } + if tc.Error != nil { + continue + } + + resp := w.Msg + if resp == nil { + t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name) + } + + // Before sorting, make sure that CNAMES do not appear after their target records + test.CNAMEOrder(resp) + + test.SortAndCheck(resp, tc) + } +} + +var notSyncedTestCases = []test.Case{ + { + // We should get ServerFailure instead of NameError for missing records when we kubernetes hasn't synced + Qname: "svc0.testns.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, +} + +func TestNotSyncedServeDNS(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{ + notSynced: true, + } + k.Next = test.NextHandler(dns.RcodeSuccess, nil) + k.Namespaces = map[string]struct{}{"testns": {}} + ctx := context.TODO() + + for i, tc := range notSyncedTestCases { + r := tc.Msg() + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := k.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d expected no error, got %v", i, err) + return + } + if tc.Error != nil { + continue + } + + resp := w.Msg + if resp == nil { + t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name) + } + + if err := test.CNAMEOrder(resp); err != nil { + t.Error(err) + } + + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} + +type APIConnServeTest struct { + notSynced bool +} + +func (a APIConnServeTest) HasSynced() bool { return !a.notSynced } +func (APIConnServeTest) Run() {} +func (APIConnServeTest) Stop() error { return nil } +func (APIConnServeTest) EpIndexReverse(string) []*object.Endpoints { return nil } +func (APIConnServeTest) SvcIndexReverse(string) []*object.Service { return nil } +func (APIConnServeTest) SvcExtIndexReverse(string) []*object.Service { return nil } +func (APIConnServeTest) Modified(bool) int64 { return int64(3) } + +func (APIConnServeTest) PodIndex(ip string) []*object.Pod { + if ip != "10.240.0.1" { + return []*object.Pod{} + } + a := []*object.Pod{ + {Namespace: "podns", Name: "foo", PodIP: "10.240.0.1"}, // Remote IP set in test.ResponseWriter + } + return a +} + +var svcIndex = map[string][]*object.Service{ + "kubedns.kube-system": { + { + Name: "kubedns", + Namespace: "kube-system", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{"10.0.0.10"}, + Ports: []api.ServicePort{ + {Name: "dns", Protocol: "udp", Port: 53}, + }, + }, + }, + "svc1.testns": { + { + Name: "svc1", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{"10.0.0.1"}, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + }, + "svcempty.testns": { + { + Name: "svcempty", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{"10.0.0.1"}, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + }, + "svc6.testns": { + { + Name: "svc6", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{"1234:abcd::1"}, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + }, + "hdls1.testns": { + { + Name: "hdls1", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{api.ClusterIPNone}, + }, + }, + "external.testns": { + { + Name: "external", + Namespace: "testns", + ExternalName: "ext.interwebs.test", + Type: api.ServiceTypeExternalName, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + }, + "external-to-service.testns": { + { + Name: "external-to-service", + Namespace: "testns", + ExternalName: "svc1.testns.svc.cluster.local.", + Type: api.ServiceTypeExternalName, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + }, + "hdlsprtls.testns": { + { + Name: "hdlsprtls", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{api.ClusterIPNone}, + }, + }, + "svc1.unexposedns": { + { + Name: "svc1", + Namespace: "unexposedns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{"10.0.0.2"}, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + }, + "svc-dual-stack.testns": { + { + Name: "svc-dual-stack", + Namespace: "testns", + Type: api.ServiceTypeClusterIP, + ClusterIPs: []string{"10.0.0.3", "10::3"}, Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + }, +} + +func (APIConnServeTest) SvcIndex(s string) []*object.Service { return svcIndex[s] } + +func (APIConnServeTest) ServiceList() []*object.Service { + var svcs []*object.Service + for _, svc := range svcIndex { + svcs = append(svcs, svc...) + } + return svcs +} + +var epsIndex = map[string][]*object.Endpoints{ + "kubedns.kube-system": {{ + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "172.0.0.100"}, + }, + Ports: []object.EndpointPort{ + {Port: 53, Protocol: "udp", Name: "dns"}, + }, + }, + }, + Name: "kubedns", + Namespace: "kube-system", + Index: object.EndpointsKey("kubedns", "kube-system"), + }}, + "svc1.testns": {{ + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "172.0.0.1", Hostname: "ep1a"}, + }, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "svc1-slice1", + Namespace: "testns", + Index: object.EndpointsKey("svc1", "testns"), + }}, + "svcempty.testns": {{ + Subsets: []object.EndpointSubset{ + { + Addresses: nil, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "svcempty-slice1", + Namespace: "testns", + Index: object.EndpointsKey("svcempty", "testns"), + }}, + "hdls1.testns": {{ + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "172.0.0.2"}, + {IP: "172.0.0.3"}, + {IP: "172.0.0.4", Hostname: "dup-name"}, + {IP: "172.0.0.5", Hostname: "dup-name"}, + {IP: "5678:abcd::1"}, + {IP: "5678:abcd::2"}, + }, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "hdls1-slice1", + Namespace: "testns", + Index: object.EndpointsKey("hdls1", "testns"), + }}, + "hdlsprtls.testns": {{ + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "172.0.0.20"}, + }, + Ports: []object.EndpointPort{{Port: -1}}, + }, + }, + Name: "hdlsprtls-slice1", + Namespace: "testns", + Index: object.EndpointsKey("hdlsprtls", "testns"), + }}, +} + +func (APIConnServeTest) EpIndex(s string) []*object.Endpoints { + return epsIndex[s] +} + +func (APIConnServeTest) EndpointsList() []*object.Endpoints { + var eps []*object.Endpoints + for _, ep := range epsIndex { + eps = append(eps, ep...) + } + return eps +} + +func (APIConnServeTest) GetNodeByName(ctx context.Context, name string) (*api.Node, error) { + return &api.Node{ + ObjectMeta: meta.ObjectMeta{ + Name: "test.node.foo.bar", + }, + }, nil +} + +func (APIConnServeTest) GetNamespaceByName(name string) (*object.Namespace, error) { + if name == "pod-nons" { // handler_pod_verified_test.go uses this for non-existent namespace. + return nil, fmt.Errorf("namespace not found") + } + if name == "nsnoexist" { + return nil, fmt.Errorf("namespace not found") + } + return &object.Namespace{ + Name: name, + }, nil +} + +// Upstub implements an Upstreamer that returns a set response for test purposes +type Upstub struct { + test.Case + Truncated bool + Qclass uint16 +} + +// Lookup returns a set response +func (t *Upstub) Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) { + var answer []dns.RR + // if query type is not CNAME, remove any CNAME with same name as qname from the answer + if t.Qtype != dns.TypeCNAME { + for _, a := range t.Answer { + if c, ok := a.(*dns.CNAME); ok && c.Header().Name == t.Qname { + continue + } + answer = append(answer, a) + } + } else { + answer = t.Answer + } + + return &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Response: true, + Truncated: t.Truncated, + Rcode: t.Rcode, + }, + Question: []dns.Question{{Name: t.Qname, Qtype: t.Qtype, Qclass: t.Qclass}}, + Answer: answer, + Extra: t.Extra, + Ns: t.Ns, + }, nil +} diff --git a/ag_201_coredns/plugin/kubernetes/informer_test.go b/ag_201_coredns/plugin/kubernetes/informer_test.go new file mode 100644 index 0000000..ae68b5c --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/informer_test.go @@ -0,0 +1,120 @@ +package kubernetes + +import ( + "fmt" + "testing" + + "github.com/coredns/coredns/plugin/kubernetes/object" + + api "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" +) + +func TestDefaultProcessor(t *testing.T) { + pbuild := object.DefaultProcessor(object.ToService, nil) + reh := cache.ResourceEventHandlerFuncs{} + idx := cache.NewIndexer(cache.DeletionHandlingMetaNamespaceKeyFunc, cache.Indexers{}) + processor := pbuild(idx, reh) + testProcessor(t, processor, idx) +} + +func testProcessor(t *testing.T, processor cache.ProcessFunc, idx cache.Indexer) { + obj := &api.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "service1", Namespace: "test1"}, + Spec: api.ServiceSpec{ + ClusterIP: "1.2.3.4", + ClusterIPs: []string{"1.2.3.4"}, + Ports: []api.ServicePort{{Port: 80}}, + }, + } + obj2 := &api.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "service2", Namespace: "test1"}, + Spec: api.ServiceSpec{ + ClusterIP: "5.6.7.8", + ClusterIPs: []string{"5.6.7.8"}, + Ports: []api.ServicePort{{Port: 80}}, + }, + } + + // Add the objects + err := processor(cache.Deltas{ + {Type: cache.Added, Object: obj.DeepCopy()}, + {Type: cache.Added, Object: obj2.DeepCopy()}, + }) + if err != nil { + t.Fatalf("add failed: %v", err) + } + got, exists, err := idx.Get(obj) + if err != nil { + t.Fatalf("get added object failed: %v", err) + } + if !exists { + t.Fatal("added object not found in index") + } + svc, ok := got.(*object.Service) + if !ok { + t.Fatal("object in index was incorrect type") + } + if fmt.Sprintf("%v", svc.ClusterIPs) != fmt.Sprintf("%v", obj.Spec.ClusterIPs) { + t.Fatalf("expected '%v', got '%v'", obj.Spec.ClusterIPs, svc.ClusterIPs) + } + + // Update an object + obj.Spec.ClusterIP = "1.2.3.5" + err = processor(cache.Deltas{{ + Type: cache.Updated, + Object: obj.DeepCopy(), + }}) + if err != nil { + t.Fatalf("update failed: %v", err) + } + got, exists, err = idx.Get(obj) + if err != nil { + t.Fatalf("get updated object failed: %v", err) + } + if !exists { + t.Fatal("updated object not found in index") + } + svc, ok = got.(*object.Service) + if !ok { + t.Fatal("object in index was incorrect type") + } + if fmt.Sprintf("%v", svc.ClusterIPs) != fmt.Sprintf("%v", obj.Spec.ClusterIPs) { + t.Fatalf("expected '%v', got '%v'", obj.Spec.ClusterIPs, svc.ClusterIPs) + } + + // Delete an object + err = processor(cache.Deltas{{ + Type: cache.Deleted, + Object: obj2.DeepCopy(), + }}) + if err != nil { + t.Fatalf("delete test failed: %v", err) + } + _, exists, err = idx.Get(obj2) + if err != nil { + t.Fatalf("get deleted object failed: %v", err) + } + if exists { + t.Fatal("deleted object found in index") + } + + // Delete an object via tombstone + key, _ := cache.MetaNamespaceKeyFunc(obj) + tombstone := cache.DeletedFinalStateUnknown{Key: key, Obj: svc} + err = processor(cache.Deltas{{ + Type: cache.Deleted, + Object: tombstone, + }}) + if err != nil { + t.Fatalf("tombstone delete test failed: %v", err) + } + _, exists, err = idx.Get(svc) + if err != nil { + t.Fatalf("get tombstone deleted object failed: %v", err) + } + if exists { + t.Fatal("tombstone deleted object found in index") + } +} diff --git a/ag_201_coredns/plugin/kubernetes/kubernetes.go b/ag_201_coredns/plugin/kubernetes/kubernetes.go new file mode 100644 index 0000000..10d8b7e --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/kubernetes.go @@ -0,0 +1,601 @@ +// Package kubernetes provides the kubernetes backend. +package kubernetes + +import ( + "context" + "errors" + "fmt" + "net" + "strconv" + "strings" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/kubernetes/object" + "github.com/coredns/coredns/plugin/pkg/dnsutil" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + api "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" + discoveryV1beta1 "k8s.io/api/discovery/v1beta1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +// Kubernetes implements a plugin that connects to a Kubernetes cluster. +type Kubernetes struct { + Next plugin.Handler + Zones []string + Upstream Upstreamer + APIServerList []string + APICertAuth string + APIClientCert string + APIClientKey string + ClientConfig clientcmd.ClientConfig + APIConn dnsController + Namespaces map[string]struct{} + podMode string + endpointNameMode bool + Fall fall.F + ttl uint32 + opts dnsControlOpts + primaryZoneIndex int + localIPs []net.IP + autoPathSearch []string // Local search path from /etc/resolv.conf. Needed for autopath. +} + +// Upstreamer is used to resolve CNAME or other external targets +type Upstreamer interface { + Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) +} + +// New returns a initialized Kubernetes. It default interfaceAddrFunc to return 127.0.0.1. All other +// values default to their zero value, primaryZoneIndex will thus point to the first zone. +func New(zones []string) *Kubernetes { + k := new(Kubernetes) + k.Zones = zones + k.Namespaces = make(map[string]struct{}) + k.podMode = podModeDisabled + k.ttl = defaultTTL + + return k +} + +const ( + // podModeDisabled is the default value where pod requests are ignored + podModeDisabled = "disabled" + // podModeVerified is where Pod requests are answered only if they exist + podModeVerified = "verified" + // podModeInsecure is where pod requests are answered without verifying they exist + podModeInsecure = "insecure" + // DNSSchemaVersion is the schema version: https://github.com/kubernetes/dns/blob/master/docs/specification.md + DNSSchemaVersion = "1.1.0" + // Svc is the DNS schema for kubernetes services + Svc = "svc" + // Pod is the DNS schema for kubernetes pods + Pod = "pod" + // defaultTTL to apply to all answers. + defaultTTL = 5 +) + +var ( + errNoItems = errors.New("no items found") + errNsNotExposed = errors.New("namespace is not exposed") + errInvalidRequest = errors.New("invalid query name") +) + +// Services implements the ServiceBackend interface. +func (k *Kubernetes) Services(ctx context.Context, state request.Request, exact bool, opt plugin.Options) (svcs []msg.Service, err error) { + // We're looking again at types, which we've already done in ServeDNS, but there are some types k8s just can't answer. + switch state.QType() { + case dns.TypeTXT: + // 1 label + zone, label must be "dns-version". + t, _ := dnsutil.TrimZone(state.Name(), state.Zone) + + segs := dns.SplitDomainName(t) + if len(segs) != 1 { + return nil, nil + } + if segs[0] != "dns-version" { + return nil, nil + } + svc := msg.Service{Text: DNSSchemaVersion, TTL: 28800, Key: msg.Path(state.QName(), coredns)} + return []msg.Service{svc}, nil + + case dns.TypeNS: + // We can only get here if the qname equals the zone, see ServeDNS in handler.go. + nss := k.nsAddrs(false, false, state.Zone) + var svcs []msg.Service + for _, ns := range nss { + if ns.Header().Rrtype == dns.TypeA { + svcs = append(svcs, msg.Service{Host: ns.(*dns.A).A.String(), Key: msg.Path(ns.Header().Name, coredns), TTL: k.ttl}) + continue + } + if ns.Header().Rrtype == dns.TypeAAAA { + svcs = append(svcs, msg.Service{Host: ns.(*dns.AAAA).AAAA.String(), Key: msg.Path(ns.Header().Name, coredns), TTL: k.ttl}) + } + } + return svcs, nil + } + + if isDefaultNS(state.Name(), state.Zone) { + nss := k.nsAddrs(false, false, state.Zone) + var svcs []msg.Service + for _, ns := range nss { + if ns.Header().Rrtype == dns.TypeA && state.QType() == dns.TypeA { + svcs = append(svcs, msg.Service{Host: ns.(*dns.A).A.String(), Key: msg.Path(state.QName(), coredns), TTL: k.ttl}) + continue + } + if ns.Header().Rrtype == dns.TypeAAAA && state.QType() == dns.TypeAAAA { + svcs = append(svcs, msg.Service{Host: ns.(*dns.AAAA).AAAA.String(), Key: msg.Path(state.QName(), coredns), TTL: k.ttl}) + } + } + return svcs, nil + } + + s, e := k.Records(ctx, state, false) + + // SRV for external services is not yet implemented, so remove those records. + + if state.QType() != dns.TypeSRV { + return s, e + } + + internal := []msg.Service{} + for _, svc := range s { + if t, _ := svc.HostType(); t != dns.TypeCNAME { + internal = append(internal, svc) + } + } + + return internal, e +} + +// primaryZone will return the first non-reverse zone being handled by this plugin +func (k *Kubernetes) primaryZone() string { return k.Zones[k.primaryZoneIndex] } + +// Lookup implements the ServiceBackend interface. +func (k *Kubernetes) Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) { + return k.Upstream.Lookup(ctx, state, name, typ) +} + +// IsNameError implements the ServiceBackend interface. +func (k *Kubernetes) IsNameError(err error) bool { + return err == errNoItems || err == errNsNotExposed || err == errInvalidRequest +} + +func (k *Kubernetes) getClientConfig() (*rest.Config, error) { + if k.ClientConfig != nil { + return k.ClientConfig.ClientConfig() + } + loadingRules := &clientcmd.ClientConfigLoadingRules{} + overrides := &clientcmd.ConfigOverrides{} + clusterinfo := clientcmdapi.Cluster{} + authinfo := clientcmdapi.AuthInfo{} + + // Connect to API from in cluster + if len(k.APIServerList) == 0 { + cc, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + cc.ContentType = "application/vnd.kubernetes.protobuf" + return cc, err + } + + // Connect to API from out of cluster + // Only the first one is used. We will deprecate multiple endpoints later. + clusterinfo.Server = k.APIServerList[0] + + if len(k.APICertAuth) > 0 { + clusterinfo.CertificateAuthority = k.APICertAuth + } + if len(k.APIClientCert) > 0 { + authinfo.ClientCertificate = k.APIClientCert + } + if len(k.APIClientKey) > 0 { + authinfo.ClientKey = k.APIClientKey + } + + overrides.ClusterInfo = clusterinfo + overrides.AuthInfo = authinfo + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) + + cc, err := clientConfig.ClientConfig() + if err != nil { + return nil, err + } + cc.ContentType = "application/vnd.kubernetes.protobuf" + return cc, err +} + +// InitKubeCache initializes a new Kubernetes cache. +func (k *Kubernetes) InitKubeCache(ctx context.Context) (onStart func() error, onShut func() error, err error) { + config, err := k.getClientConfig() + if err != nil { + return nil, nil, err + } + + kubeClient, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, nil, fmt.Errorf("failed to create kubernetes notification controller: %q", err) + } + + if k.opts.labelSelector != nil { + var selector labels.Selector + selector, err = meta.LabelSelectorAsSelector(k.opts.labelSelector) + if err != nil { + return nil, nil, fmt.Errorf("unable to create Selector for LabelSelector '%s': %q", k.opts.labelSelector, err) + } + k.opts.selector = selector + } + + if k.opts.namespaceLabelSelector != nil { + var selector labels.Selector + selector, err = meta.LabelSelectorAsSelector(k.opts.namespaceLabelSelector) + if err != nil { + return nil, nil, fmt.Errorf("unable to create Selector for LabelSelector '%s': %q", k.opts.namespaceLabelSelector, err) + } + k.opts.namespaceSelector = selector + } + + k.opts.initPodCache = k.podMode == podModeVerified + + k.opts.zones = k.Zones + k.opts.endpointNameMode = k.endpointNameMode + + k.APIConn = newdnsController(ctx, kubeClient, k.opts) + + initEndpointWatch := k.opts.initEndpointsCache + + onStart = func() error { + go func() { + if initEndpointWatch { + // Revert to watching Endpoints for incompatible K8s. + // This can be removed when all supported k8s versions support endpointslices. + ok, v := k.endpointSliceSupported(kubeClient) + if !ok { + k.APIConn.(*dnsControl).WatchEndpoints(ctx) + } + // Revert to EndpointSlice v1beta1 if v1 is not supported + if ok && v == discoveryV1beta1.SchemeGroupVersion.String() { + k.APIConn.(*dnsControl).WatchEndpointSliceV1beta1(ctx) + } + } + k.APIConn.Run() + }() + + timeout := 5 * time.Second + timeoutTicker := time.NewTicker(timeout) + defer timeoutTicker.Stop() + logDelay := 500 * time.Millisecond + logTicker := time.NewTicker(logDelay) + defer logTicker.Stop() + checkSyncTicker := time.NewTicker(100 * time.Millisecond) + defer checkSyncTicker.Stop() + for { + select { + case <-checkSyncTicker.C: + if k.APIConn.HasSynced() { + return nil + } + case <-logTicker.C: + log.Info("waiting for Kubernetes API before starting server") + case <-timeoutTicker.C: + log.Warning("starting server with unsynced Kubernetes API") + return nil + } + } + } + + onShut = func() error { + return k.APIConn.Stop() + } + + return onStart, onShut, err +} + +// endpointSliceSupported will determine which endpoint object type to watch (endpointslices or endpoints) +// based on the supportability of endpointslices in the API and server version. It will return true when endpointslices +// should be watched, and false when endpoints should be watched. +// If the API supports discovery, and the server versions >= 1.19, true is returned. +// Also returned is the discovery version supported: "v1" if v1 is supported, and v1beta1 if v1beta1 is supported and +// v1 is not supported. +// This function should be removed, when all supported versions of k8s support v1. +func (k *Kubernetes) endpointSliceSupported(kubeClient *kubernetes.Clientset) (bool, string) { + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + logTicker := time.NewTicker(10 * time.Second) + defer logTicker.Stop() + var connErr error + for { + select { + case <-logTicker.C: + if connErr == nil { + continue + } + log.Warningf("Kubernetes API connection failure: %v", connErr) + case <-ticker.C: + sv, err := kubeClient.ServerVersion() + if err != nil { + connErr = err + continue + } + + // Disable use of endpoint slices for k8s versions 1.18 and earlier. The Endpointslices API was enabled + // by default in 1.17 but Service -> Pod proxy continued to use Endpoints by default until 1.19. + // DNS results should be built from the same source data that the proxy uses. This decision assumes + // k8s EndpointSliceProxying feature gate is at the default (i.e. only enabled for k8s >= 1.19). + major, _ := strconv.Atoi(sv.Major) + minor, _ := strconv.Atoi(strings.TrimRight(sv.Minor, "+")) + if major <= 1 && minor <= 18 { + log.Info("Watching Endpoints instead of EndpointSlices in k8s versions < 1.19") + return false, "" + } + + // Enable use of endpoint slices if the API supports the discovery api + _, err = kubeClient.Discovery().ServerResourcesForGroupVersion(discovery.SchemeGroupVersion.String()) + if err == nil { + return true, discovery.SchemeGroupVersion.String() + } else if !kerrors.IsNotFound(err) { + connErr = err + continue + } + + _, err = kubeClient.Discovery().ServerResourcesForGroupVersion(discoveryV1beta1.SchemeGroupVersion.String()) + if err == nil { + return true, discoveryV1beta1.SchemeGroupVersion.String() + } else if !kerrors.IsNotFound(err) { + connErr = err + continue + } + + // Disable use of endpoint slices in case that it is disabled in k8s versions 1.19 and newer. + log.Info("Endpointslices API disabled. Watching Endpoints instead.") + return false, "" + } + } +} + +// Records looks up services in kubernetes. +func (k *Kubernetes) Records(ctx context.Context, state request.Request, exact bool) ([]msg.Service, error) { + r, e := parseRequest(state.Name(), state.Zone) + if e != nil { + return nil, e + } + if r.podOrSvc == "" { + return nil, nil + } + + if dnsutil.IsReverse(state.Name()) > 0 { + return nil, errNoItems + } + + if !k.namespaceExposed(r.namespace) { + return nil, errNsNotExposed + } + + if r.podOrSvc == Pod { + pods, err := k.findPods(r, state.Zone) + return pods, err + } + + services, err := k.findServices(r, state.Zone) + return services, err +} + +func endpointHostname(addr object.EndpointAddress, endpointNameMode bool) string { + if addr.Hostname != "" { + return addr.Hostname + } + if endpointNameMode && addr.TargetRefName != "" { + return addr.TargetRefName + } + if strings.Contains(addr.IP, ".") { + return strings.Replace(addr.IP, ".", "-", -1) + } + if strings.Contains(addr.IP, ":") { + return strings.Replace(addr.IP, ":", "-", -1) + } + return "" +} + +func (k *Kubernetes) findPods(r recordRequest, zone string) (pods []msg.Service, err error) { + if k.podMode == podModeDisabled { + return nil, errNoItems + } + + namespace := r.namespace + if !k.namespaceExposed(namespace) { + return nil, errNoItems + } + + podname := r.service + + // handle empty pod name + if podname == "" { + if k.namespaceExposed(namespace) { + // NODATA + return nil, nil + } + // NXDOMAIN + return nil, errNoItems + } + + zonePath := msg.Path(zone, coredns) + ip := "" + if strings.Count(podname, "-") == 3 && !strings.Contains(podname, "--") { + ip = strings.ReplaceAll(podname, "-", ".") + } else { + ip = strings.ReplaceAll(podname, "-", ":") + } + + if k.podMode == podModeInsecure { + if !k.namespaceExposed(namespace) { // namespace does not exist + return nil, errNoItems + } + + // If ip does not parse as an IP address, we return an error, otherwise we assume a CNAME and will try to resolve it in backend_lookup.go + if net.ParseIP(ip) == nil { + return nil, errNoItems + } + + return []msg.Service{{Key: strings.Join([]string{zonePath, Pod, namespace, podname}, "/"), Host: ip, TTL: k.ttl}}, err + } + + // PodModeVerified + err = errNoItems + + for _, p := range k.APIConn.PodIndex(ip) { + // check for matching ip and namespace + if ip == p.PodIP && match(namespace, p.Namespace) { + s := msg.Service{Key: strings.Join([]string{zonePath, Pod, namespace, podname}, "/"), Host: ip, TTL: k.ttl} + pods = append(pods, s) + + err = nil + } + } + return pods, err +} + +// findServices returns the services matching r from the cache. +func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg.Service, err error) { + if !k.namespaceExposed(r.namespace) { + return nil, errNoItems + } + + // handle empty service name + if r.service == "" { + if k.namespaceExposed(r.namespace) { + // NODATA + return nil, nil + } + // NXDOMAIN + return nil, errNoItems + } + + err = errNoItems + + var ( + endpointsListFunc func() []*object.Endpoints + endpointsList []*object.Endpoints + serviceList []*object.Service + ) + + idx := object.ServiceKey(r.service, r.namespace) + serviceList = k.APIConn.SvcIndex(idx) + endpointsListFunc = func() []*object.Endpoints { return k.APIConn.EpIndex(idx) } + + zonePath := msg.Path(zone, coredns) + for _, svc := range serviceList { + if !(match(r.namespace, svc.Namespace) && match(r.service, svc.Name)) { + continue + } + + // If "ignore empty_service" option is set and no endpoints exist, return NXDOMAIN unless + // it's a headless or externalName service (covered below). + if k.opts.ignoreEmptyService && svc.Type != api.ServiceTypeExternalName && !svc.Headless() { // serve NXDOMAIN if no endpoint is able to answer + podsCount := 0 + for _, ep := range endpointsListFunc() { + for _, eps := range ep.Subsets { + podsCount += len(eps.Addresses) + } + } + + if podsCount == 0 { + continue + } + } + + // External service + if svc.Type == api.ServiceTypeExternalName { + s := msg.Service{Key: strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/"), Host: svc.ExternalName, TTL: k.ttl} + if t, _ := s.HostType(); t == dns.TypeCNAME { + s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/") + services = append(services, s) + + err = nil + } + continue + } + + // Endpoint query or headless service + if svc.Headless() || r.endpoint != "" { + if endpointsList == nil { + endpointsList = endpointsListFunc() + } + + for _, ep := range endpointsList { + if object.EndpointsKey(svc.Name, svc.Namespace) != ep.Index { + continue + } + + for _, eps := range ep.Subsets { + for _, addr := range eps.Addresses { + // See comments in parse.go parseRequest about the endpoint handling. + if r.endpoint != "" { + if !match(r.endpoint, endpointHostname(addr, k.endpointNameMode)) { + continue + } + } + + for _, p := range eps.Ports { + if !(matchPortAndProtocol(r.port, p.Name, r.protocol, p.Protocol)) { + continue + } + s := msg.Service{Host: addr.IP, Port: int(p.Port), TTL: k.ttl} + s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name, endpointHostname(addr, k.endpointNameMode)}, "/") + + err = nil + + services = append(services, s) + } + } + } + } + continue + } + + // ClusterIP service + for _, p := range svc.Ports { + if !(matchPortAndProtocol(r.port, p.Name, r.protocol, string(p.Protocol))) { + continue + } + + err = nil + + for _, ip := range svc.ClusterIPs { + s := msg.Service{Host: ip, Port: int(p.Port), TTL: k.ttl} + s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/") + services = append(services, s) + } + } + } + return services, err +} + +// Serial return the SOA serial. +func (k *Kubernetes) Serial(state request.Request) uint32 { return uint32(k.APIConn.Modified(false)) } + +// MinTTL returns the minimal TTL. +func (k *Kubernetes) MinTTL(state request.Request) uint32 { return k.ttl } + +// match checks if a and b are equal. +func match(a, b string) bool { + return strings.EqualFold(a, b) +} + +// matchPortAndProtocol matches port and protocol, permitting the 'a' inputs to be wild +func matchPortAndProtocol(aPort, bPort, aProtocol, bProtocol string) bool { + return (match(aPort, bPort) || aPort == "") && (match(aProtocol, bProtocol) || aProtocol == "") +} + +const coredns = "c" // used as a fake key prefix in msg.Service diff --git a/ag_201_coredns/plugin/kubernetes/kubernetes_apex_test.go b/ag_201_coredns/plugin/kubernetes/kubernetes_apex_test.go new file mode 100644 index 0000000..7531e21 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/kubernetes_apex_test.go @@ -0,0 +1,92 @@ +package kubernetes + +import ( + "context" + "net" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var kubeApexCases = []test.Case{ + { + Qname: "cluster.local.", Qtype: dns.TypeSOA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, + { + Qname: "cluster.local.", Qtype: dns.TypeHINFO, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, + { + Qname: "cluster.local.", Qtype: dns.TypeNS, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.NS("cluster.local. 5 IN NS ns.dns.cluster.local."), + }, + Extra: []dns.RR{ + test.A("ns.dns.cluster.local. 5 IN A 127.0.0.1"), + }, + }, + { + Qname: "cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, + { + Qname: "cluster.local.", Qtype: dns.TypeAAAA, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, + { + Qname: "cluster.local.", Qtype: dns.TypeSRV, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 5"), + }, + }, +} + +func TestServeDNSApex(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.Next = test.NextHandler(dns.RcodeSuccess, nil) + k.localIPs = []net.IP{net.ParseIP("127.0.0.1")} + ctx := context.TODO() + + for i, tc := range kubeApexCases { + r := tc.Msg() + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := k.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d, expected no error, got %v", i, err) + return + } + if tc.Error != nil { + continue + } + + resp := w.Msg + if resp == nil { + t.Fatalf("Test %d, got nil message and no error ford", i) + } + + if err := test.SortAndCheck(resp, tc); err != nil { + t.Errorf("Test %d: %v", i, err) + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/kubernetes_test.go b/ag_201_coredns/plugin/kubernetes/kubernetes_test.go new file mode 100644 index 0000000..acdfd4c --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/kubernetes_test.go @@ -0,0 +1,367 @@ +package kubernetes + +import ( + "context" + "net" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/kubernetes/object" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + api "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestEndpointHostname(t *testing.T) { + var tests = []struct { + ip string + hostname string + expected string + podName string + endpointNameMode bool + }{ + {"10.11.12.13", "", "10-11-12-13", "", false}, + {"10.11.12.13", "epname", "epname", "", false}, + {"10.11.12.13", "", "10-11-12-13", "hello-abcde", false}, + {"10.11.12.13", "epname", "epname", "hello-abcde", false}, + {"10.11.12.13", "epname", "epname", "hello-abcde", true}, + {"10.11.12.13", "", "hello-abcde", "hello-abcde", true}, + } + for _, test := range tests { + result := endpointHostname(object.EndpointAddress{IP: test.ip, Hostname: test.hostname, TargetRefName: test.podName}, test.endpointNameMode) + if result != test.expected { + t.Errorf("Expected endpoint name for (ip:%v hostname:%v) to be '%v', but got '%v'", test.ip, test.hostname, test.expected, result) + } + } +} + +type APIConnServiceTest struct{} + +func (APIConnServiceTest) HasSynced() bool { return true } +func (APIConnServiceTest) Run() {} +func (APIConnServiceTest) Stop() error { return nil } +func (APIConnServiceTest) PodIndex(string) []*object.Pod { return nil } +func (APIConnServiceTest) SvcIndexReverse(string) []*object.Service { return nil } +func (APIConnServiceTest) SvcExtIndexReverse(string) []*object.Service { return nil } +func (APIConnServiceTest) EpIndexReverse(string) []*object.Endpoints { return nil } +func (APIConnServiceTest) Modified(bool) int64 { return 0 } + +func (APIConnServiceTest) SvcIndex(string) []*object.Service { + svcs := []*object.Service{ + { + Name: "svc1", + Namespace: "testns", + ClusterIPs: []string{"10.0.0.1"}, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + { + Name: "svc-dual-stack", + Namespace: "testns", + ClusterIPs: []string{"10.0.0.2", "10::2"}, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + { + Name: "hdls1", + Namespace: "testns", + ClusterIPs: []string{api.ClusterIPNone}, + }, + { + Name: "external", + Namespace: "testns", + ExternalName: "coredns.io", + Type: api.ServiceTypeExternalName, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + } + return svcs +} + +func (APIConnServiceTest) ServiceList() []*object.Service { + svcs := []*object.Service{ + { + Name: "svc1", + Namespace: "testns", + ClusterIPs: []string{"10.0.0.1"}, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + { + Name: "svc-dual-stack", + Namespace: "testns", + ClusterIPs: []string{"10.0.0.2", "10::2"}, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + { + Name: "hdls1", + Namespace: "testns", + ClusterIPs: []string{api.ClusterIPNone}, + }, + { + Name: "external", + Namespace: "testns", + ExternalName: "coredns.io", + Type: api.ServiceTypeExternalName, + Ports: []api.ServicePort{ + {Name: "http", Protocol: "tcp", Port: 80}, + }, + }, + } + return svcs +} + +func (APIConnServiceTest) EpIndex(string) []*object.Endpoints { + eps := []*object.Endpoints{ + { + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "172.0.0.1", Hostname: "ep1a"}, + }, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "svc1-slice1", + Namespace: "testns", + Index: object.EndpointsKey("svc1", "testns"), + }, + { + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "172.0.0.2"}, + }, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "hdls1-slice1", + Namespace: "testns", + Index: object.EndpointsKey("hdls1", "testns"), + }, + { + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "10.9.8.7", NodeName: "test.node.foo.bar"}, + }, + }, + }, + }, + } + return eps +} + +func (APIConnServiceTest) EndpointsList() []*object.Endpoints { + eps := []*object.Endpoints{ + { + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "172.0.0.1", Hostname: "ep1a"}, + }, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "svc1-slice1", + Namespace: "testns", + Index: object.EndpointsKey("svc1", "testns"), + }, + { + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "172.0.0.2"}, + }, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "hdls1-slice1", + Namespace: "testns", + Index: object.EndpointsKey("hdls1", "testns"), + }, + { + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "172.0.0.2"}, + }, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "hdls1-slice2", + Namespace: "testns", + Index: object.EndpointsKey("hdls1", "testns"), + }, + { + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "10.9.8.7", NodeName: "test.node.foo.bar"}, + }, + }, + }, + }, + } + return eps +} + +func (APIConnServiceTest) GetNodeByName(ctx context.Context, name string) (*api.Node, error) { + return &api.Node{ + ObjectMeta: meta.ObjectMeta{ + Name: "test.node.foo.bar", + }, + }, nil +} + +func (APIConnServiceTest) GetNamespaceByName(name string) (*object.Namespace, error) { + return &object.Namespace{ + Name: name, + }, nil +} + +func TestServices(t *testing.T) { + k := New([]string{"interwebs.test."}) + k.APIConn = &APIConnServiceTest{} + + type svcAns struct { + host string + key string + } + type svcTest struct { + qname string + qtype uint16 + answer []svcAns + } + tests := []svcTest{ + // Cluster IP Services + {qname: "svc1.testns.svc.interwebs.test.", qtype: dns.TypeA, answer: []svcAns{{host: "10.0.0.1", key: "/" + coredns + "/test/interwebs/svc/testns/svc1"}}}, + {qname: "_http._tcp.svc1.testns.svc.interwebs.test.", qtype: dns.TypeSRV, answer: []svcAns{{host: "10.0.0.1", key: "/" + coredns + "/test/interwebs/svc/testns/svc1"}}}, + {qname: "ep1a.svc1.testns.svc.interwebs.test.", qtype: dns.TypeA, answer: []svcAns{{host: "172.0.0.1", key: "/" + coredns + "/test/interwebs/svc/testns/svc1/ep1a"}}}, + + // Dual-Stack Cluster IP Service + { + qname: "_http._tcp.svc-dual-stack.testns.svc.interwebs.test.", + qtype: dns.TypeSRV, + answer: []svcAns{ + {host: "10.0.0.2", key: "/" + coredns + "/test/interwebs/svc/testns/svc-dual-stack"}, + {host: "10::2", key: "/" + coredns + "/test/interwebs/svc/testns/svc-dual-stack"}, + }, + }, + + // External Services + {qname: "external.testns.svc.interwebs.test.", qtype: dns.TypeCNAME, answer: []svcAns{{host: "coredns.io", key: "/" + coredns + "/test/interwebs/svc/testns/external"}}}, + + // Headless Services + {qname: "hdls1.testns.svc.interwebs.test.", qtype: dns.TypeA, answer: []svcAns{{host: "172.0.0.2", key: "/" + coredns + "/test/interwebs/svc/testns/hdls1/172-0-0-2"}}}, + } + + for i, test := range tests { + state := request.Request{ + Req: &dns.Msg{Question: []dns.Question{{Name: test.qname, Qtype: test.qtype}}}, + Zone: "interwebs.test.", // must match from k.Zones[0] + } + svcs, e := k.Services(context.TODO(), state, false, plugin.Options{}) + if e != nil { + t.Errorf("Test %d: got error '%v'", i, e) + continue + } + if len(svcs) != len(test.answer) { + t.Errorf("Test %d, expected %v answer, got %v", i, len(test.answer), len(svcs)) + continue + } + + for j := range svcs { + if test.answer[j].host != svcs[j].Host { + t.Errorf("Test %d, expected host '%v', got '%v'", i, test.answer[j].host, svcs[j].Host) + } + if test.answer[j].key != svcs[j].Key { + t.Errorf("Test %d, expected key '%v', got '%v'", i, test.answer[j].key, svcs[j].Key) + } + } + } +} + +func TestServicesAuthority(t *testing.T) { + k := New([]string{"interwebs.test."}) + k.APIConn = &APIConnServiceTest{} + + type svcAns struct { + host string + key string + } + type svcTest struct { + localIPs []net.IP + qname string + qtype uint16 + answer []svcAns + } + tests := []svcTest{ + {localIPs: []net.IP{net.ParseIP("1.2.3.4")}, qname: "ns.dns.interwebs.test.", qtype: dns.TypeA, answer: []svcAns{{host: "1.2.3.4", key: "/" + coredns + "/test/interwebs/dns/ns"}}}, + {localIPs: []net.IP{net.ParseIP("1.2.3.4")}, qname: "ns.dns.interwebs.test.", qtype: dns.TypeAAAA}, + {localIPs: []net.IP{net.ParseIP("1:2::3:4")}, qname: "ns.dns.interwebs.test.", qtype: dns.TypeA}, + {localIPs: []net.IP{net.ParseIP("1:2::3:4")}, qname: "ns.dns.interwebs.test.", qtype: dns.TypeAAAA, answer: []svcAns{{host: "1:2::3:4", key: "/" + coredns + "/test/interwebs/dns/ns"}}}, + { + localIPs: []net.IP{net.ParseIP("1.2.3.4"), net.ParseIP("1:2::3:4")}, + qname: "ns.dns.interwebs.test.", + qtype: dns.TypeNS, answer: []svcAns{ + {host: "1.2.3.4", key: "/" + coredns + "/test/interwebs/dns/ns"}, + {host: "1:2::3:4", key: "/" + coredns + "/test/interwebs/dns/ns"}, + }, + }, + } + + for i, test := range tests { + k.localIPs = test.localIPs + + state := request.Request{ + Req: &dns.Msg{Question: []dns.Question{{Name: test.qname, Qtype: test.qtype}}}, + Zone: k.Zones[0], + } + svcs, e := k.Services(context.TODO(), state, false, plugin.Options{}) + if e != nil { + t.Errorf("Test %d: got error '%v'", i, e) + continue + } + if test.answer != nil && len(svcs) != len(test.answer) { + t.Errorf("Test %d, expected 1 answer, got %v", i, len(svcs)) + continue + } + if test.answer == nil && len(svcs) != 0 { + t.Errorf("Test %d, expected no answer, got %v", i, len(svcs)) + continue + } + + if test.answer == nil && len(svcs) == 0 { + continue + } + + for i, answer := range test.answer { + if answer.host != svcs[i].Host { + t.Errorf("Test %d, expected host '%v', got '%v'", i, answer.host, svcs[i].Host) + } + if answer.key != svcs[i].Key { + t.Errorf("Test %d, expected key '%v', got '%v'", i, answer.key, svcs[i].Key) + } + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/local.go b/ag_201_coredns/plugin/kubernetes/local.go new file mode 100644 index 0000000..a754f21 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/local.go @@ -0,0 +1,37 @@ +package kubernetes + +import ( + "net" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" +) + +// boundIPs returns the list of non-loopback IPs that CoreDNS is bound to +func boundIPs(c *caddy.Controller) (ips []net.IP) { + conf := dnsserver.GetConfig(c) + hosts := conf.ListenHosts + if hosts == nil || hosts[0] == "" { + hosts = nil + addrs, err := net.InterfaceAddrs() + if err != nil { + return nil + } + for _, addr := range addrs { + hosts = append(hosts, addr.String()) + } + } + for _, host := range hosts { + ip, _, _ := net.ParseCIDR(host) + ip4 := ip.To4() + if ip4 != nil && !ip4.IsLoopback() { + ips = append(ips, ip4) + continue + } + ip6 := ip.To16() + if ip6 != nil && !ip6.IsLoopback() { + ips = append(ips, ip6) + } + } + return ips +} diff --git a/ag_201_coredns/plugin/kubernetes/log_test.go b/ag_201_coredns/plugin/kubernetes/log_test.go new file mode 100644 index 0000000..b8b7b74 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/log_test.go @@ -0,0 +1,5 @@ +package kubernetes + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/kubernetes/logger.go b/ag_201_coredns/plugin/kubernetes/logger.go new file mode 100644 index 0000000..ac9fe80 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/logger.go @@ -0,0 +1,38 @@ +package kubernetes + +import ( + clog "github.com/coredns/coredns/plugin/pkg/log" + + "github.com/go-logr/logr" +) + +// loggerAdapter is a simple wrapper around CoreDNS plugin logger made to implement logr.LogSink interface, which is used +// as part of klog library for logging in Kubernetes client. By using this adapter CoreDNS is able to log messages/errors from +// kubernetes client in a CoreDNS logging format +type loggerAdapter struct { + clog.P +} + +func (l *loggerAdapter) Init(_ logr.RuntimeInfo) { +} + +func (l *loggerAdapter) Enabled(_ int) bool { + // verbosity is controlled inside klog library, we do not need to do anything here + return true +} + +func (l *loggerAdapter) Info(_ int, msg string, _ ...interface{}) { + l.P.Info(msg) +} + +func (l *loggerAdapter) Error(_ error, msg string, _ ...interface{}) { + l.P.Error(msg) +} + +func (l *loggerAdapter) WithValues(_ ...interface{}) logr.LogSink { + return l +} + +func (l *loggerAdapter) WithName(_ string) logr.LogSink { + return l +} diff --git a/ag_201_coredns/plugin/kubernetes/metadata.go b/ag_201_coredns/plugin/kubernetes/metadata.go new file mode 100644 index 0000000..36e2f9a --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/metadata.go @@ -0,0 +1,62 @@ +package kubernetes + +import ( + "context" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/request" +) + +// Metadata implements the metadata.Provider interface. +func (k *Kubernetes) Metadata(ctx context.Context, state request.Request) context.Context { + pod := k.podWithIP(state.IP()) + if pod != nil { + metadata.SetValueFunc(ctx, "kubernetes/client-namespace", func() string { + return pod.Namespace + }) + + metadata.SetValueFunc(ctx, "kubernetes/client-pod-name", func() string { + return pod.Name + }) + } + + zone := plugin.Zones(k.Zones).Matches(state.Name()) + if zone == "" { + return ctx + } + // possible optimization: cache r so it doesn't need to be calculated again in ServeDNS + r, err := parseRequest(state.Name(), zone) + if err != nil { + metadata.SetValueFunc(ctx, "kubernetes/parse-error", func() string { + return err.Error() + }) + return ctx + } + + metadata.SetValueFunc(ctx, "kubernetes/port-name", func() string { + return r.port + }) + + metadata.SetValueFunc(ctx, "kubernetes/protocol", func() string { + return r.protocol + }) + + metadata.SetValueFunc(ctx, "kubernetes/endpoint", func() string { + return r.endpoint + }) + + metadata.SetValueFunc(ctx, "kubernetes/service", func() string { + return r.service + }) + + metadata.SetValueFunc(ctx, "kubernetes/namespace", func() string { + return r.namespace + }) + + metadata.SetValueFunc(ctx, "kubernetes/kind", func() string { + return r.podOrSvc + }) + + return ctx +} diff --git a/ag_201_coredns/plugin/kubernetes/metadata_test.go b/ag_201_coredns/plugin/kubernetes/metadata_test.go new file mode 100644 index 0000000..009c533 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/metadata_test.go @@ -0,0 +1,155 @@ +package kubernetes + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +var metadataCases = []struct { + Qname string + Qtype uint16 + RemoteIP string + Md map[string]string +}{ + { + Qname: "foo.bar.notapod.cluster.local.", Qtype: dns.TypeA, + Md: map[string]string{ + "kubernetes/parse-error": "invalid query name", + }, + }, + { + Qname: "10-240-0-1.podns.pod.cluster.local.", Qtype: dns.TypeA, + Md: map[string]string{ + "kubernetes/endpoint": "", + "kubernetes/kind": "pod", + "kubernetes/namespace": "podns", + "kubernetes/port-name": "", + "kubernetes/protocol": "", + "kubernetes/service": "10-240-0-1", + }, + }, + { + Qname: "s.ns.svc.cluster.local.", Qtype: dns.TypeA, + Md: map[string]string{ + "kubernetes/endpoint": "", + "kubernetes/kind": "svc", + "kubernetes/namespace": "ns", + "kubernetes/port-name": "", + "kubernetes/protocol": "", + "kubernetes/service": "s", + }, + }, + { + Qname: "s.ns.svc.cluster.local.", Qtype: dns.TypeA, + RemoteIP: "10.10.10.10", + Md: map[string]string{ + "kubernetes/endpoint": "", + "kubernetes/kind": "svc", + "kubernetes/namespace": "ns", + "kubernetes/port-name": "", + "kubernetes/protocol": "", + "kubernetes/service": "s", + }, + }, + { + Qname: "_http._tcp.s.ns.svc.cluster.local.", Qtype: dns.TypeSRV, + RemoteIP: "10.10.10.10", + Md: map[string]string{ + "kubernetes/endpoint": "", + "kubernetes/kind": "svc", + "kubernetes/namespace": "ns", + "kubernetes/port-name": "http", + "kubernetes/protocol": "tcp", + "kubernetes/service": "s", + }, + }, + { + Qname: "ep.s.ns.svc.cluster.local.", Qtype: dns.TypeA, + RemoteIP: "10.10.10.10", + Md: map[string]string{ + "kubernetes/endpoint": "ep", + "kubernetes/kind": "svc", + "kubernetes/namespace": "ns", + "kubernetes/port-name": "", + "kubernetes/protocol": "", + "kubernetes/service": "s", + }, + }, + { + Qname: "example.com.", Qtype: dns.TypeA, + RemoteIP: "10.10.10.10", + Md: map[string]string{}, + }, +} + +func mapsDiffer(a, b map[string]string) bool { + if len(a) != len(b) { + return true + } + + for k, va := range a { + vb, ok := b[k] + if !ok || va != vb { + return true + } + } + return false +} + +func TestMetadata(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + + for i, tc := range metadataCases { + ctx := metadata.ContextWithMetadata(context.Background()) + state := request.Request{ + Req: &dns.Msg{Question: []dns.Question{{Name: tc.Qname, Qtype: tc.Qtype}}}, + Zone: ".", + W: &test.ResponseWriter{RemoteIP: tc.RemoteIP}, + } + + k.Metadata(ctx, state) + + md := make(map[string]string) + for _, l := range metadata.Labels(ctx) { + md[l] = metadata.ValueFunc(ctx, l)() + } + if mapsDiffer(tc.Md, md) { + t.Errorf("Case %d expected metadata %v and got %v", i, tc.Md, md) + } + } +} + +func TestMetadataPodsVerified(t *testing.T) { + k := New([]string{"cluster.local."}) + k.podMode = podModeVerified + k.APIConn = &APIConnServeTest{} + + ctx := metadata.ContextWithMetadata(context.Background()) + state := request.Request{ + Req: &dns.Msg{Question: []dns.Question{{Name: "example.com.", Qtype: dns.TypeA}}}, + Zone: ".", + W: &test.ResponseWriter{}, + } + + k.Metadata(ctx, state) + + expect := map[string]string{ + "kubernetes/client-namespace": "podns", + "kubernetes/client-pod-name": "foo", + } + + md := make(map[string]string) + for _, l := range metadata.Labels(ctx) { + md[l] = metadata.ValueFunc(ctx, l)() + } + if mapsDiffer(expect, md) { + t.Errorf("Expected metadata %v and got %v", expect, md) + } +} diff --git a/ag_201_coredns/plugin/kubernetes/metrics_test.backup b/ag_201_coredns/plugin/kubernetes/metrics_test.backup new file mode 100644 index 0000000..8274eef --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/metrics_test.backup @@ -0,0 +1,203 @@ +package kubernetes + +import ( + "strings" + "testing" + "time" + + "github.com/coredns/coredns/plugin/kubernetes/object" + "github.com/prometheus/client_golang/prometheus/testutil" + api "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1beta1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" +) + +const ( + namespace = "testns" +) + +var expected = ` + # HELP coredns_kubernetes_dns_programming_duration_seconds Histogram of the time (in seconds) it took to program a dns instance. + # TYPE coredns_kubernetes_dns_programming_duration_seconds histogram + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="0.001"} 0 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="0.002"} 0 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="0.004"} 0 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="0.008"} 0 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="0.016"} 0 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="0.032"} 0 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="0.064"} 0 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="0.128"} 0 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="0.256"} 0 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="0.512"} 0 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="1.024"} 1 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="2.048"} 2 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="4.096"} 2 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="8.192"} 2 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="16.384"} 2 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="32.768"} 2 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="65.536"} 2 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="131.072"} 2 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="262.144"} 2 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="524.288"} 2 + coredns_kubernetes_dns_programming_duration_seconds_bucket{service_kind="headless_with_selector",le="+Inf"} 2 + coredns_kubernetes_dns_programming_duration_seconds_sum{service_kind="headless_with_selector"} 3 + coredns_kubernetes_dns_programming_duration_seconds_count{service_kind="headless_with_selector"} 2 + ` + +func TestDNSProgrammingLatencyEndpointSlices(t *testing.T) { + now := time.Now() + + svcIdx := cache.NewIndexer(cache.DeletionHandlingMetaNamespaceKeyFunc, cache.Indexers{svcNameNamespaceIndex: svcNameNamespaceIndexFunc}) + epIdx := cache.NewIndexer(cache.DeletionHandlingMetaNamespaceKeyFunc, cache.Indexers{}) + + dns := dnsControl{svcLister: svcIdx} + svcProc := object.DefaultProcessor(object.ToService, nil)(svcIdx, cache.ResourceEventHandlerFuncs{}) + epProc := object.DefaultProcessor(object.EndpointSliceToEndpoints, dns.EndpointSliceLatencyRecorder())(epIdx, cache.ResourceEventHandlerFuncs{}) + + object.DurationSinceFunc = func(t time.Time) time.Duration { + return now.Sub(t) + } + object.DNSProgrammingLatency.Reset() + + endpoints1 := []discovery.Endpoint{{ + Addresses: []string{"1.2.3.4"}, + }} + + endpoints2 := []discovery.Endpoint{{ + Addresses: []string{"1.2.3.45"}, + }} + + createService(t, svcProc, "my-service", api.ClusterIPNone) + createEndpointSlice(t, epProc, "my-service", now.Add(-2*time.Second), endpoints1) + updateEndpointSlice(t, epProc, "my-service", now.Add(-1*time.Second), endpoints2) + + createEndpointSlice(t, epProc, "endpoints-no-service", now.Add(-4*time.Second), nil) + + createService(t, svcProc, "clusterIP-service", "10.40.0.12") + createEndpointSlice(t, epProc, "clusterIP-service", now.Add(-8*time.Second), nil) + + createService(t, svcProc, "headless-no-annotation", api.ClusterIPNone) + createEndpointSlice(t, epProc, "headless-no-annotation", nil, nil) + + createService(t, svcProc, "headless-wrong-annotation", api.ClusterIPNone) + createEndpointSlice(t, epProc, "headless-wrong-annotation", "wrong-value", nil) + + if err := testutil.CollectAndCompare(object.DNSProgrammingLatency, strings.NewReader(expected)); err != nil { + t.Error(err) + } +} + +func TestDnsProgrammingLatencyEndpoints(t *testing.T) { + now := time.Now() + + svcIdx := cache.NewIndexer(cache.DeletionHandlingMetaNamespaceKeyFunc, cache.Indexers{svcNameNamespaceIndex: svcNameNamespaceIndexFunc}) + epIdx := cache.NewIndexer(cache.DeletionHandlingMetaNamespaceKeyFunc, cache.Indexers{}) + + dns := dnsControl{svcLister: svcIdx} + svcProc := object.DefaultProcessor(object.ToService, nil)(svcIdx, cache.ResourceEventHandlerFuncs{}) + epProc := object.DefaultProcessor(object.ToEndpoints, dns.EndpointsLatencyRecorder())(epIdx, cache.ResourceEventHandlerFuncs{}) + + object.DurationSinceFunc = func(t time.Time) time.Duration { + return now.Sub(t) + } + object.DNSProgrammingLatency.Reset() + + subset1 := []api.EndpointSubset{{ + Addresses: []api.EndpointAddress{{IP: "1.2.3.4", Hostname: "foo"}}, + }} + + subset2 := []api.EndpointSubset{{ + Addresses: []api.EndpointAddress{{IP: "1.2.3.5", Hostname: "foo"}}, + }} + + createService(t, svcProc, "my-service", api.ClusterIPNone) + createEndpoints(t, epProc, "my-service", now.Add(-2*time.Second), subset1) + updateEndpoints(t, epProc, "my-service", now.Add(-1*time.Second), subset2) + + createEndpoints(t, epProc, "endpoints-no-service", now.Add(-4*time.Second), nil) + + createService(t, svcProc, "clusterIP-service", "10.40.0.12") + createEndpoints(t, epProc, "clusterIP-service", now.Add(-8*time.Second), nil) + + createService(t, svcProc, "headless-no-annotation", api.ClusterIPNone) + createEndpoints(t, epProc, "headless-no-annotation", nil, nil) + + createService(t, svcProc, "headless-wrong-annotation", api.ClusterIPNone) + createEndpoints(t, epProc, "headless-wrong-annotation", "wrong-value", nil) + + if err := testutil.CollectAndCompare(object.DNSProgrammingLatency, strings.NewReader(expected)); err != nil { + t.Error(err) + } +} + +func buildEndpoints(name string, lastChangeTriggerTime interface{}, subsets []api.EndpointSubset) *api.Endpoints { + annotations := make(map[string]string) + switch v := lastChangeTriggerTime.(type) { + case string: + annotations[api.EndpointsLastChangeTriggerTime] = v + case time.Time: + annotations[api.EndpointsLastChangeTriggerTime] = v.Format(time.RFC3339Nano) + } + return &api.Endpoints{ + ObjectMeta: meta.ObjectMeta{Namespace: namespace, Name: name, Annotations: annotations}, + Subsets: subsets, + } +} + +func buildEndpointSlice(name string, lastChangeTriggerTime interface{}, endpoints []discovery.Endpoint) *discovery.EndpointSlice { + annotations := make(map[string]string) + switch v := lastChangeTriggerTime.(type) { + case string: + annotations[api.EndpointsLastChangeTriggerTime] = v + case time.Time: + annotations[api.EndpointsLastChangeTriggerTime] = v.Format(time.RFC3339Nano) + } + return &discovery.EndpointSlice{ + ObjectMeta: meta.ObjectMeta{ + Namespace: namespace, Name: name + "-12345", + Labels: map[string]string{discovery.LabelServiceName: name}, + Annotations: annotations, + }, + Endpoints: endpoints, + } +} + +func createEndpoints(t *testing.T, processor cache.ProcessFunc, name string, triggerTime interface{}, subsets []api.EndpointSubset) { + err := processor(cache.Deltas{{Type: cache.Added, Object: buildEndpoints(name, triggerTime, subsets)}}) + if err != nil { + t.Fatal(err) + } +} + +func updateEndpoints(t *testing.T, processor cache.ProcessFunc, name string, triggerTime interface{}, subsets []api.EndpointSubset) { + err := processor(cache.Deltas{{Type: cache.Updated, Object: buildEndpoints(name, triggerTime, subsets)}}) + if err != nil { + t.Fatal(err) + } +} + +func createEndpointSlice(t *testing.T, processor cache.ProcessFunc, name string, triggerTime interface{}, endpoints []discovery.Endpoint) { + err := processor(cache.Deltas{{Type: cache.Added, Object: buildEndpointSlice(name, triggerTime, endpoints)}}) + if err != nil { + t.Fatal(err) + } +} + +func updateEndpointSlice(t *testing.T, processor cache.ProcessFunc, name string, triggerTime interface{}, endpoints []discovery.Endpoint) { + err := processor(cache.Deltas{{Type: cache.Updated, Object: buildEndpointSlice(name, triggerTime, endpoints)}}) + if err != nil { + t.Fatal(err) + } +} + +func createService(t *testing.T, processor cache.ProcessFunc, name string, clusterIp string) { + obj := &api.Service{ + ObjectMeta: meta.ObjectMeta{Namespace: namespace, Name: name}, + Spec: api.ServiceSpec{ClusterIP: clusterIp}, + } + err := processor(cache.Deltas{{Type: cache.Added, Object: obj}}) + if err != nil { + t.Fatal(err) + } +} diff --git a/ag_201_coredns/plugin/kubernetes/namespace.go b/ag_201_coredns/plugin/kubernetes/namespace.go new file mode 100644 index 0000000..3e90bab --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/namespace.go @@ -0,0 +1,24 @@ +package kubernetes + +// filteredNamespaceExists checks if namespace exists in this cluster +// according to any `namespace_labels` plugin configuration specified. +// Returns true even for namespaces not exposed by plugin configuration, +// see namespaceExposed. +func (k *Kubernetes) filteredNamespaceExists(namespace string) bool { + _, err := k.APIConn.GetNamespaceByName(namespace) + return err == nil +} + +// configuredNamespace returns true when the namespace is exposed through the plugin +// `namespaces` configuration. +func (k *Kubernetes) configuredNamespace(namespace string) bool { + _, ok := k.Namespaces[namespace] + if len(k.Namespaces) > 0 && !ok { + return false + } + return true +} + +func (k *Kubernetes) namespaceExposed(namespace string) bool { + return k.configuredNamespace(namespace) && k.filteredNamespaceExists(namespace) +} diff --git a/ag_201_coredns/plugin/kubernetes/namespace_test.go b/ag_201_coredns/plugin/kubernetes/namespace_test.go new file mode 100644 index 0000000..c302b42 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/namespace_test.go @@ -0,0 +1,72 @@ +package kubernetes + +import ( + "testing" +) + +func TestFilteredNamespaceExists(t *testing.T) { + tests := []struct { + expected bool + kubernetesNamespaces map[string]struct{} + testNamespace string + }{ + {true, map[string]struct{}{}, "foobar"}, + {false, map[string]struct{}{}, "nsnoexist"}, + } + + k := Kubernetes{} + k.APIConn = &APIConnServeTest{} + for i, test := range tests { + k.Namespaces = test.kubernetesNamespaces + actual := k.filteredNamespaceExists(test.testNamespace) + if actual != test.expected { + t.Errorf("Test %d failed. Filtered namespace %s was expected to exist", i, test.testNamespace) + } + } +} + +func TestNamespaceExposed(t *testing.T) { + tests := []struct { + expected bool + kubernetesNamespaces map[string]struct{} + testNamespace string + }{ + {true, map[string]struct{}{"foobar": {}}, "foobar"}, + {false, map[string]struct{}{"foobar": {}}, "nsnoexist"}, + {true, map[string]struct{}{}, "foobar"}, + {true, map[string]struct{}{}, "nsnoexist"}, + } + + k := Kubernetes{} + k.APIConn = &APIConnServeTest{} + for i, test := range tests { + k.Namespaces = test.kubernetesNamespaces + actual := k.configuredNamespace(test.testNamespace) + if actual != test.expected { + t.Errorf("Test %d failed. Namespace %s was expected to be exposed", i, test.testNamespace) + } + } +} + +func TestNamespaceValid(t *testing.T) { + tests := []struct { + expected bool + kubernetesNamespaces map[string]struct{} + testNamespace string + }{ + {true, map[string]struct{}{"foobar": {}}, "foobar"}, + {false, map[string]struct{}{"foobar": {}}, "nsnoexist"}, + {true, map[string]struct{}{}, "foobar"}, + {false, map[string]struct{}{}, "nsnoexist"}, + } + + k := Kubernetes{} + k.APIConn = &APIConnServeTest{} + for i, test := range tests { + k.Namespaces = test.kubernetesNamespaces + actual := k.namespaceExposed(test.testNamespace) + if actual != test.expected { + t.Errorf("Test %d failed. Namespace %s was expected to be valid", i, test.testNamespace) + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/ns.go b/ag_201_coredns/plugin/kubernetes/ns.go new file mode 100644 index 0000000..eb40c34 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/ns.go @@ -0,0 +1,103 @@ +package kubernetes + +import ( + "net" + "strings" + + "github.com/miekg/dns" +) + +func isDefaultNS(name, zone string) bool { + return strings.Index(name, defaultNSName) == 0 && strings.Index(name, zone) == len(defaultNSName) +} + +// nsAddrs returns the A or AAAA records for the CoreDNS service in the cluster. If the service cannot be found, +// it returns a record for the local address of the machine we're running on. +func (k *Kubernetes) nsAddrs(external, headless bool, zone string) []dns.RR { + var ( + svcNames []string + svcIPs []net.IP + foundEndpoint bool + ) + + // Find the CoreDNS Endpoints + for _, localIP := range k.localIPs { + endpoints := k.APIConn.EpIndexReverse(localIP.String()) + + // Collect IPs for all Services of the Endpoints + for _, endpoint := range endpoints { + foundEndpoint = true + svcs := k.APIConn.SvcIndex(endpoint.Index) + for _, svc := range svcs { + if external { + svcName := strings.Join([]string{svc.Name, svc.Namespace, zone}, ".") + + if headless && svc.Headless() { + for _, s := range endpoint.Subsets { + for _, a := range s.Addresses { + svcNames = append(svcNames, endpointHostname(a, k.endpointNameMode)+"."+svcName) + svcIPs = append(svcIPs, net.ParseIP(a.IP)) + } + } + } else { + for _, exIP := range svc.ExternalIPs { + svcNames = append(svcNames, svcName) + svcIPs = append(svcIPs, net.ParseIP(exIP)) + } + } + + continue + } + svcName := strings.Join([]string{svc.Name, svc.Namespace, Svc, zone}, ".") + if svc.Headless() { + // For a headless service, use the endpoints IPs + for _, s := range endpoint.Subsets { + for _, a := range s.Addresses { + svcNames = append(svcNames, endpointHostname(a, k.endpointNameMode)+"."+svcName) + svcIPs = append(svcIPs, net.ParseIP(a.IP)) + } + } + } else { + for _, clusterIP := range svc.ClusterIPs { + svcNames = append(svcNames, svcName) + svcIPs = append(svcIPs, net.ParseIP(clusterIP)) + } + } + } + } + } + + // If no CoreDNS endpoints were found, use the localIPs directly + if !foundEndpoint { + svcIPs = make([]net.IP, len(k.localIPs)) + svcNames = make([]string, len(k.localIPs)) + for i, localIP := range k.localIPs { + svcNames[i] = defaultNSName + zone + svcIPs[i] = localIP + } + } + + // Create an RR slice of collected IPs + rrs := make([]dns.RR, len(svcIPs)) + for i, ip := range svcIPs { + if ip.To4() == nil { + rr := new(dns.AAAA) + rr.Hdr.Class = dns.ClassINET + rr.Hdr.Rrtype = dns.TypeAAAA + rr.Hdr.Name = svcNames[i] + rr.AAAA = ip + rrs[i] = rr + continue + } + rr := new(dns.A) + rr.Hdr.Class = dns.ClassINET + rr.Hdr.Rrtype = dns.TypeA + rr.Hdr.Name = svcNames[i] + rr.A = ip + rrs[i] = rr + } + + return rrs +} + +const defaultNSName = "ns.dns." diff --git a/ag_201_coredns/plugin/kubernetes/ns_test.go b/ag_201_coredns/plugin/kubernetes/ns_test.go new file mode 100644 index 0000000..e3c67d4 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/ns_test.go @@ -0,0 +1,219 @@ +package kubernetes + +import ( + "context" + "fmt" + "net" + "testing" + + "github.com/coredns/coredns/plugin/kubernetes/object" + + "github.com/miekg/dns" + api "k8s.io/api/core/v1" +) + +type APIConnTest struct{} + +func (APIConnTest) HasSynced() bool { return true } +func (APIConnTest) Run() {} +func (APIConnTest) Stop() error { return nil } +func (APIConnTest) PodIndex(string) []*object.Pod { return nil } +func (APIConnTest) SvcIndexReverse(string) []*object.Service { return nil } +func (APIConnTest) SvcExtIndexReverse(string) []*object.Service { return nil } +func (APIConnTest) EpIndex(string) []*object.Endpoints { return nil } +func (APIConnTest) EndpointsList() []*object.Endpoints { return nil } +func (APIConnTest) Modified(bool) int64 { return 0 } + +func (a APIConnTest) SvcIndex(s string) []*object.Service { + switch s { + case "dns-service.kube-system": + return []*object.Service{a.ServiceList()[0]} + case "hdls-dns-service.kube-system": + return []*object.Service{a.ServiceList()[1]} + case "dns6-service.kube-system": + return []*object.Service{a.ServiceList()[2]} + } + return nil +} + +var svcs = []*object.Service{ + { + Name: "dns-service", + Namespace: "kube-system", + ClusterIPs: []string{"10.0.0.111"}, + }, + { + Name: "hdls-dns-service", + Namespace: "kube-system", + ClusterIPs: []string{api.ClusterIPNone}, + }, + { + Name: "dns6-service", + Namespace: "kube-system", + ClusterIPs: []string{"10::111"}, + }, +} + +func (APIConnTest) ServiceList() []*object.Service { + return svcs +} + +func (APIConnTest) EpIndexReverse(ip string) []*object.Endpoints { + if ip != "10.244.0.20" { + return nil + } + eps := []*object.Endpoints{ + { + Name: "dns-service-slice1", + Namespace: "kube-system", + Index: object.EndpointsKey("dns-service", "kube-system"), + Subsets: []object.EndpointSubset{ + {Addresses: []object.EndpointAddress{{IP: "10.244.0.20"}}}, + }, + }, + { + Name: "hdls-dns-service-slice1", + Namespace: "kube-system", + Index: object.EndpointsKey("hdls-dns-service", "kube-system"), + Subsets: []object.EndpointSubset{ + {Addresses: []object.EndpointAddress{{IP: "10.244.0.20"}}}, + }, + }, + { + Name: "dns6-service-slice1", + Namespace: "kube-system", + Index: object.EndpointsKey("dns6-service", "kube-system"), + Subsets: []object.EndpointSubset{ + {Addresses: []object.EndpointAddress{{IP: "10.244.0.20"}}}, + }, + }, + } + return eps +} + +func (APIConnTest) GetNodeByName(ctx context.Context, name string) (*api.Node, error) { + return &api.Node{}, nil +} +func (APIConnTest) GetNamespaceByName(name string) (*object.Namespace, error) { + return nil, fmt.Errorf("namespace not found") +} + +func TestNsAddrs(t *testing.T) { + k := New([]string{"inter.webs.test."}) + k.APIConn = &APIConnTest{} + k.localIPs = []net.IP{net.ParseIP("10.244.0.20")} + + cdrs := k.nsAddrs(false, false, k.Zones[0]) + + if len(cdrs) != 3 { + t.Fatalf("Expected 3 results, got %v", len(cdrs)) + } + cdr := cdrs[0] + expected := "10.0.0.111" + if cdr.(*dns.A).A.String() != expected { + t.Errorf("Expected 1st A to be %q, got %q", expected, cdr.(*dns.A).A.String()) + } + expected = "dns-service.kube-system.svc.inter.webs.test." + if cdr.Header().Name != expected { + t.Errorf("Expected 1st Header Name to be %q, got %q", expected, cdr.Header().Name) + } + cdr = cdrs[1] + expected = "10.244.0.20" + if cdr.(*dns.A).A.String() != expected { + t.Errorf("Expected 2nd A to be %q, got %q", expected, cdr.(*dns.A).A.String()) + } + expected = "10-244-0-20.hdls-dns-service.kube-system.svc.inter.webs.test." + if cdr.Header().Name != expected { + t.Errorf("Expected 2nd Header Name to be %q, got %q", expected, cdr.Header().Name) + } + cdr = cdrs[2] + expected = "10::111" + if cdr.(*dns.AAAA).AAAA.String() != expected { + t.Errorf("Expected AAAA to be %q, got %q", expected, cdr.(*dns.A).A.String()) + } + expected = "dns6-service.kube-system.svc.inter.webs.test." + if cdr.Header().Name != expected { + t.Errorf("Expected AAAA Header Name to be %q, got %q", expected, cdr.Header().Name) + } +} + +func TestNsAddrsExternalHeadless(t *testing.T) { + k := New([]string{"example.com."}) + k.APIConn = &APIConnTest{} + k.localIPs = []net.IP{net.ParseIP("10.244.0.20")} + + // there are only headless sevices + cdrs := k.nsAddrs(true, true, k.Zones[0]) + + if len(cdrs) != 1 { + t.Fatalf("Expected 0 results, got %v", cdrs) + } + + cdr := cdrs[0] + expected := "10.244.0.20" + if cdr.(*dns.A).A.String() != expected { + t.Errorf("Expected A address to be %q, got %q", expected, cdr.(*dns.A).A.String()) + } + expected = "10-244-0-20.hdls-dns-service.kube-system.example.com." + if cdr.Header().Name != expected { + t.Errorf("Expected record name to be %q, got %q", expected, cdr.Header().Name) + } +} + +func TestNsAddrsExternal(t *testing.T) { + k := New([]string{"example.com."}) + k.APIConn = &APIConnTest{} + k.localIPs = []net.IP{net.ParseIP("10.244.0.20")} + + // initially no services have an external IP ... + cdrs := k.nsAddrs(true, false, k.Zones[0]) + + if len(cdrs) != 0 { + t.Fatalf("Expected 0 results, got %v", len(cdrs)) + } + + // Add an external IP to one of the services ... + svcs[0].ExternalIPs = []string{"1.2.3.4"} + cdrs = k.nsAddrs(true, false, k.Zones[0]) + + if len(cdrs) != 1 { + t.Fatalf("Expected 1 results, got %v", len(cdrs)) + } + cdr := cdrs[0] + expected := "1.2.3.4" + if cdr.(*dns.A).A.String() != expected { + t.Errorf("Expected A address to be %q, got %q", expected, cdr.(*dns.A).A.String()) + } + expected = "dns-service.kube-system.example.com." + if cdr.Header().Name != expected { + t.Errorf("Expected record name to be %q, got %q", expected, cdr.Header().Name) + } +} + +func TestNsAddrsExternalWithPreexistingExternalIP(t *testing.T) { + k := New([]string{"example.com."}) + k.APIConn = &APIConnTest{} + k.localIPs = []net.IP{net.ParseIP("10.244.0.20")} + + svcs[0].ExternalIPs = []string{"1.2.3.4"} + + // initially no services have an external IP ... + cdrs := k.nsAddrs(true, false, k.Zones[0]) + + if len(cdrs) != 1 { + t.Fatalf("Expected 1 results, got %v", len(cdrs)) + } + + if len(cdrs) != 1 { + t.Fatalf("Expected 1 results, got %v", len(cdrs)) + } + cdr := cdrs[0] + expected := "1.2.3.4" + if cdr.(*dns.A).A.String() != expected { + t.Errorf("Expected A address to be %q, got %q", expected, cdr.(*dns.A).A.String()) + } + expected = "dns-service.kube-system.example.com." + if cdr.Header().Name != expected { + t.Errorf("Expected record name to be %q, got %q", expected, cdr.Header().Name) + } +} diff --git a/ag_201_coredns/plugin/kubernetes/object/endpoint.go b/ag_201_coredns/plugin/kubernetes/object/endpoint.go new file mode 100644 index 0000000..4af64f3 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/object/endpoint.go @@ -0,0 +1,276 @@ +package object + +import ( + "fmt" + + api "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" + discoveryV1beta1 "k8s.io/api/discovery/v1beta1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// Endpoints is a stripped down api.Endpoints with only the items we need for CoreDNS. +type Endpoints struct { + // Don't add new fields to this struct without talking to the CoreDNS maintainers. + Version string + Name string + Namespace string + Index string + IndexIP []string + Subsets []EndpointSubset + + *Empty +} + +// EndpointSubset is a group of addresses with a common set of ports. The +// expanded set of endpoints is the Cartesian product of Addresses x Ports. +type EndpointSubset struct { + Addresses []EndpointAddress + Ports []EndpointPort +} + +// EndpointAddress is a tuple that describes single IP address. +type EndpointAddress struct { + IP string + Hostname string + NodeName string + TargetRefName string +} + +// EndpointPort is a tuple that describes a single port. +type EndpointPort struct { + Port int32 + Name string + Protocol string +} + +// EndpointsKey returns a string using for the index. +func EndpointsKey(name, namespace string) string { return name + "." + namespace } + +// ToEndpoints converts an *api.Endpoints to a *Endpoints. +func ToEndpoints(obj meta.Object) (meta.Object, error) { + end, ok := obj.(*api.Endpoints) + if !ok { + return nil, fmt.Errorf("unexpected object %v", obj) + } + e := &Endpoints{ + Version: end.GetResourceVersion(), + Name: end.GetName(), + Namespace: end.GetNamespace(), + Index: EndpointsKey(end.GetName(), end.GetNamespace()), + Subsets: make([]EndpointSubset, len(end.Subsets)), + } + for i, eps := range end.Subsets { + sub := EndpointSubset{ + Addresses: make([]EndpointAddress, len(eps.Addresses)), + } + if len(eps.Ports) == 0 { + // Add sentinel if there are no ports. + sub.Ports = []EndpointPort{{Port: -1}} + } else { + sub.Ports = make([]EndpointPort, len(eps.Ports)) + } + + for j, a := range eps.Addresses { + ea := EndpointAddress{IP: a.IP, Hostname: a.Hostname} + if a.NodeName != nil { + ea.NodeName = *a.NodeName + } + if a.TargetRef != nil { + ea.TargetRefName = a.TargetRef.Name + } + sub.Addresses[j] = ea + } + + for k, p := range eps.Ports { + ep := EndpointPort{Port: p.Port, Name: p.Name, Protocol: string(p.Protocol)} + sub.Ports[k] = ep + } + + e.Subsets[i] = sub + } + + for _, eps := range end.Subsets { + for _, a := range eps.Addresses { + e.IndexIP = append(e.IndexIP, a.IP) + } + } + + *end = api.Endpoints{} + + return e, nil +} + +// EndpointSliceToEndpoints converts a *discovery.EndpointSlice to a *Endpoints. +func EndpointSliceToEndpoints(obj meta.Object) (meta.Object, error) { + ends, ok := obj.(*discovery.EndpointSlice) + if !ok { + return nil, fmt.Errorf("unexpected object %v", obj) + } + e := &Endpoints{ + Version: ends.GetResourceVersion(), + Name: ends.GetName(), + Namespace: ends.GetNamespace(), + Index: EndpointsKey(ends.Labels[discovery.LabelServiceName], ends.GetNamespace()), + Subsets: make([]EndpointSubset, 1), + } + + if len(ends.Ports) == 0 { + // Add sentinel if there are no ports. + e.Subsets[0].Ports = []EndpointPort{{Port: -1}} + } else { + e.Subsets[0].Ports = make([]EndpointPort, len(ends.Ports)) + for k, p := range ends.Ports { + ep := EndpointPort{Port: *p.Port, Name: *p.Name, Protocol: string(*p.Protocol)} + e.Subsets[0].Ports[k] = ep + } + } + + for _, end := range ends.Endpoints { + if !endpointsliceReady(end.Conditions.Ready) { + continue + } + for _, a := range end.Addresses { + ea := EndpointAddress{IP: a} + if end.Hostname != nil { + ea.Hostname = *end.Hostname + } + // ignore pod names that are too long to be a valid label + if end.TargetRef != nil && len(end.TargetRef.Name) < 64 { + ea.TargetRefName = end.TargetRef.Name + } + if end.NodeName != nil { + ea.NodeName = *end.NodeName + } + e.Subsets[0].Addresses = append(e.Subsets[0].Addresses, ea) + e.IndexIP = append(e.IndexIP, a) + } + } + + *ends = discovery.EndpointSlice{} + + return e, nil +} + +// EndpointSliceV1beta1ToEndpoints converts a v1beta1 *discovery.EndpointSlice to a *Endpoints. +func EndpointSliceV1beta1ToEndpoints(obj meta.Object) (meta.Object, error) { + ends, ok := obj.(*discoveryV1beta1.EndpointSlice) + if !ok { + return nil, fmt.Errorf("unexpected object %v", obj) + } + e := &Endpoints{ + Version: ends.GetResourceVersion(), + Name: ends.GetName(), + Namespace: ends.GetNamespace(), + Index: EndpointsKey(ends.Labels[discovery.LabelServiceName], ends.GetNamespace()), + Subsets: make([]EndpointSubset, 1), + } + + if len(ends.Ports) == 0 { + // Add sentinel if there are no ports. + e.Subsets[0].Ports = []EndpointPort{{Port: -1}} + } else { + e.Subsets[0].Ports = make([]EndpointPort, len(ends.Ports)) + for k, p := range ends.Ports { + ep := EndpointPort{Port: *p.Port, Name: *p.Name, Protocol: string(*p.Protocol)} + e.Subsets[0].Ports[k] = ep + } + } + + for _, end := range ends.Endpoints { + if !endpointsliceReady(end.Conditions.Ready) { + continue + } + for _, a := range end.Addresses { + ea := EndpointAddress{IP: a} + if end.Hostname != nil { + ea.Hostname = *end.Hostname + } + // ignore pod names that are too long to be a valid label + if end.TargetRef != nil && len(end.TargetRef.Name) < 64 { + ea.TargetRefName = end.TargetRef.Name + } + // EndpointSlice does not contain NodeName, leave blank + e.Subsets[0].Addresses = append(e.Subsets[0].Addresses, ea) + e.IndexIP = append(e.IndexIP, a) + } + } + + *ends = discoveryV1beta1.EndpointSlice{} + + return e, nil +} + +func endpointsliceReady(ready *bool) bool { + // Per API docs: a nil value indicates an unknown state. In most cases consumers + // should interpret this unknown state as ready. + if ready == nil { + return true + } + return *ready +} + +// CopyWithoutSubsets copies e, without the subsets. +func (e *Endpoints) CopyWithoutSubsets() *Endpoints { + e1 := &Endpoints{ + Version: e.Version, + Name: e.Name, + Namespace: e.Namespace, + Index: e.Index, + IndexIP: make([]string, len(e.IndexIP)), + } + copy(e1.IndexIP, e.IndexIP) + return e1 +} + +var _ runtime.Object = &Endpoints{} + +// DeepCopyObject implements the ObjectKind interface. +func (e *Endpoints) DeepCopyObject() runtime.Object { + e1 := &Endpoints{ + Version: e.Version, + Name: e.Name, + Namespace: e.Namespace, + Index: e.Index, + IndexIP: make([]string, len(e.IndexIP)), + Subsets: make([]EndpointSubset, len(e.Subsets)), + } + copy(e1.IndexIP, e.IndexIP) + + for i, eps := range e.Subsets { + sub := EndpointSubset{ + Addresses: make([]EndpointAddress, len(eps.Addresses)), + Ports: make([]EndpointPort, len(eps.Ports)), + } + for j, a := range eps.Addresses { + ea := EndpointAddress{IP: a.IP, Hostname: a.Hostname, NodeName: a.NodeName, TargetRefName: a.TargetRefName} + sub.Addresses[j] = ea + } + for k, p := range eps.Ports { + ep := EndpointPort{Port: p.Port, Name: p.Name, Protocol: p.Protocol} + sub.Ports[k] = ep + } + + e1.Subsets[i] = sub + } + return e1 +} + +// GetNamespace implements the metav1.Object interface. +func (e *Endpoints) GetNamespace() string { return e.Namespace } + +// SetNamespace implements the metav1.Object interface. +func (e *Endpoints) SetNamespace(namespace string) {} + +// GetName implements the metav1.Object interface. +func (e *Endpoints) GetName() string { return e.Name } + +// SetName implements the metav1.Object interface. +func (e *Endpoints) SetName(name string) {} + +// GetResourceVersion implements the metav1.Object interface. +func (e *Endpoints) GetResourceVersion() string { return e.Version } + +// SetResourceVersion implements the metav1.Object interface. +func (e *Endpoints) SetResourceVersion(version string) {} diff --git a/ag_201_coredns/plugin/kubernetes/object/informer.go b/ag_201_coredns/plugin/kubernetes/object/informer.go new file mode 100644 index 0000000..aac95bd --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/object/informer.go @@ -0,0 +1,88 @@ +package object + +import ( + "fmt" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/cache" +) + +// NewIndexerInformer is a copy of the cache.NewIndexerInformer function, but allows custom process function +func NewIndexerInformer(lw cache.ListerWatcher, objType runtime.Object, h cache.ResourceEventHandler, indexers cache.Indexers, builder ProcessorBuilder) (cache.Indexer, cache.Controller) { + clientState := cache.NewIndexer(cache.DeletionHandlingMetaNamespaceKeyFunc, indexers) + + cfg := &cache.Config{ + Queue: cache.NewDeltaFIFOWithOptions(cache.DeltaFIFOOptions{KeyFunction: cache.MetaNamespaceKeyFunc, KnownObjects: clientState}), + ListerWatcher: lw, + ObjectType: objType, + FullResyncPeriod: defaultResyncPeriod, + RetryOnError: false, + Process: builder(clientState, h), + } + return clientState, cache.New(cfg) +} + +// RecordLatencyFunc is a function for recording api object delta latency +type RecordLatencyFunc func(meta.Object) + +// DefaultProcessor is based on the Process function from cache.NewIndexerInformer except it does a conversion. +func DefaultProcessor(convert ToFunc, recordLatency *EndpointLatencyRecorder) ProcessorBuilder { + return func(clientState cache.Indexer, h cache.ResourceEventHandler) cache.ProcessFunc { + return func(obj interface{}) error { + for _, d := range obj.(cache.Deltas) { + if recordLatency != nil { + if o, ok := d.Object.(meta.Object); ok { + recordLatency.init(o) + } + } + switch d.Type { + case cache.Sync, cache.Added, cache.Updated: + obj, err := convert(d.Object.(meta.Object)) + if err != nil { + return err + } + if old, exists, err := clientState.Get(obj); err == nil && exists { + if err := clientState.Update(obj); err != nil { + return err + } + h.OnUpdate(old, obj) + } else { + if err := clientState.Add(obj); err != nil { + return err + } + h.OnAdd(obj) + } + if recordLatency != nil { + recordLatency.record() + } + case cache.Deleted: + var obj interface{} + obj, ok := d.Object.(cache.DeletedFinalStateUnknown) + if !ok { + var err error + metaObj, ok := d.Object.(meta.Object) + if !ok { + return fmt.Errorf("unexpected object %v", d.Object) + } + obj, err = convert(metaObj) + if err != nil && err != errPodTerminating { + return err + } + } + + if err := clientState.Delete(obj); err != nil { + return err + } + h.OnDelete(obj) + if !ok && recordLatency != nil { + recordLatency.record() + } + } + } + return nil + } + } +} + +const defaultResyncPeriod = 0 diff --git a/ag_201_coredns/plugin/kubernetes/object/metrics.go b/ag_201_coredns/plugin/kubernetes/object/metrics.go new file mode 100644 index 0000000..f39744b --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/object/metrics.go @@ -0,0 +1,82 @@ +package object + +import ( + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/log" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + api "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + // DNSProgrammingLatency is defined as the time it took to program a DNS instance - from the time + // a service or pod has changed to the time the change was propagated and was available to be + // served by a DNS server. + // The definition of this SLI can be found at https://github.com/kubernetes/community/blob/master/sig-scalability/slos/dns_programming_latency.md + // Note that the metrics is partially based on the time exported by the endpoints controller on + // the master machine. The measurement may be inaccurate if there is a clock drift between the + // node and master machine. + // The service_kind label can be one of: + // * cluster_ip + // * headless_with_selector + // * headless_without_selector + DNSProgrammingLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: plugin.Namespace, + Subsystem: "kubernetes", + Name: "dns_programming_duration_seconds", + // From 1 millisecond to ~17 minutes. + Buckets: prometheus.ExponentialBuckets(0.001, 2, 20), + Help: "Histogram of the time (in seconds) it took to program a dns instance.", + }, []string{"service_kind"}) + + // DurationSinceFunc returns the duration elapsed since the given time. + // Added as a global variable to allow injection for testing. + DurationSinceFunc = time.Since +) + +// EndpointLatencyRecorder records latency metric for endpoint objects +type EndpointLatencyRecorder struct { + TT time.Time + ServiceFunc func(meta.Object) []*Service + Services []*Service +} + +func (l *EndpointLatencyRecorder) init(o meta.Object) { + l.Services = l.ServiceFunc(o) + l.TT = time.Time{} + stringVal, ok := o.GetAnnotations()[api.EndpointsLastChangeTriggerTime] + if ok { + tt, err := time.Parse(time.RFC3339Nano, stringVal) + if err != nil { + log.Warningf("DnsProgrammingLatency cannot be calculated for Endpoints '%s/%s'; invalid %q annotation RFC3339 value of %q", + o.GetNamespace(), o.GetName(), api.EndpointsLastChangeTriggerTime, stringVal) + // In case of error val = time.Zero, which is ignored downstream. + } + l.TT = tt + } +} + +func (l *EndpointLatencyRecorder) record() { + // isHeadless indicates whether the endpoints object belongs to a headless + // service (i.e. clusterIp = None). Note that this can be a false negatives if the service + // informer is lagging, i.e. we may not see a recently created service. Given that the services + // don't change very often (comparing to much more frequent endpoints changes), cases when this method + // will return wrong answer should be relatively rare. Because of that we intentionally accept this + // flaw to keep the solution simple. + isHeadless := len(l.Services) == 1 && l.Services[0].Headless() + + if !isHeadless || l.TT.IsZero() { + return + } + + // If we're here it means that the Endpoints object is for a headless service and that + // the Endpoints object was created by the endpoints-controller (because the + // LastChangeTriggerTime annotation is set). It means that the corresponding service is a + // "headless service with selector". + DNSProgrammingLatency.WithLabelValues("headless_with_selector"). + Observe(DurationSinceFunc(l.TT).Seconds()) +} diff --git a/ag_201_coredns/plugin/kubernetes/object/namespace.go b/ag_201_coredns/plugin/kubernetes/object/namespace.go new file mode 100644 index 0000000..ec1b466 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/object/namespace.go @@ -0,0 +1,61 @@ +package object + +import ( + "fmt" + + api "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// Namespace is a stripped down api.Namespace with only the items we need for CoreDNS. +type Namespace struct { + // Don't add new fields to this struct without talking to the CoreDNS maintainers. + Version string + Name string + + *Empty +} + +// ToNamespace returns a function that converts an api.Namespace to a *Namespace. +func ToNamespace(obj meta.Object) (meta.Object, error) { + ns, ok := obj.(*api.Namespace) + if !ok { + return nil, fmt.Errorf("unexpected object %v", obj) + } + n := &Namespace{ + Version: ns.GetResourceVersion(), + Name: ns.GetName(), + } + *ns = api.Namespace{} + return n, nil +} + +var _ runtime.Object = &Namespace{} + +// DeepCopyObject implements the ObjectKind interface. +func (n *Namespace) DeepCopyObject() runtime.Object { + n1 := &Namespace{ + Version: n.Version, + Name: n.Name, + } + return n1 +} + +// GetNamespace implements the metav1.Object interface. +func (n *Namespace) GetNamespace() string { return "" } + +// SetNamespace implements the metav1.Object interface. +func (n *Namespace) SetNamespace(namespace string) {} + +// GetName implements the metav1.Object interface. +func (n *Namespace) GetName() string { return n.Name } + +// SetName implements the metav1.Object interface. +func (n *Namespace) SetName(name string) {} + +// GetResourceVersion implements the metav1.Object interface. +func (n *Namespace) GetResourceVersion() string { return n.Version } + +// SetResourceVersion implements the metav1.Object interface. +func (n *Namespace) SetResourceVersion(version string) {} diff --git a/ag_201_coredns/plugin/kubernetes/object/object.go b/ag_201_coredns/plugin/kubernetes/object/object.go new file mode 100644 index 0000000..3421779 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/object/object.go @@ -0,0 +1,113 @@ +// Package object holds functions that convert the objects from the k8s API in +// to a more memory efficient structures. +// +// Adding new fields to any of the structures defined in pod.go, endpoint.go +// and service.go should not be done lightly as this increases the memory use +// and will leads to OOMs in the k8s scale test. +// +// We can do some optimizations here as well. We store IP addresses as strings, +// this might be moved to uint32 (for v4) for instance, but then we need to +// convert those again. +// +// Also the msg.Service use in this plugin may be deprecated at some point, as +// we don't use most of those features anyway and would free us from the *etcd* +// dependency, where msg.Service is defined. And should save some mem/cpu as we +// convert to and from msg.Services. +package object + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/cache" +) + +// ToFunc converts one v1.Object to another v1.Object. +type ToFunc func(v1.Object) (v1.Object, error) + +// ProcessorBuilder returns function to process cache events. +type ProcessorBuilder func(cache.Indexer, cache.ResourceEventHandler) cache.ProcessFunc + +// Empty is an empty struct. +type Empty struct{} + +// GetObjectKind implements the ObjectKind interface as a noop. +func (e *Empty) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } + +// GetGenerateName implements the metav1.Object interface. +func (e *Empty) GetGenerateName() string { return "" } + +// SetGenerateName implements the metav1.Object interface. +func (e *Empty) SetGenerateName(name string) {} + +// GetUID implements the metav1.Object interface. +func (e *Empty) GetUID() types.UID { return "" } + +// SetUID implements the metav1.Object interface. +func (e *Empty) SetUID(uid types.UID) {} + +// GetGeneration implements the metav1.Object interface. +func (e *Empty) GetGeneration() int64 { return 0 } + +// SetGeneration implements the metav1.Object interface. +func (e *Empty) SetGeneration(generation int64) {} + +// GetSelfLink implements the metav1.Object interface. +func (e *Empty) GetSelfLink() string { return "" } + +// SetSelfLink implements the metav1.Object interface. +func (e *Empty) SetSelfLink(selfLink string) {} + +// GetCreationTimestamp implements the metav1.Object interface. +func (e *Empty) GetCreationTimestamp() v1.Time { return v1.Time{} } + +// SetCreationTimestamp implements the metav1.Object interface. +func (e *Empty) SetCreationTimestamp(timestamp v1.Time) {} + +// GetDeletionTimestamp implements the metav1.Object interface. +func (e *Empty) GetDeletionTimestamp() *v1.Time { return &v1.Time{} } + +// SetDeletionTimestamp implements the metav1.Object interface. +func (e *Empty) SetDeletionTimestamp(timestamp *v1.Time) {} + +// GetDeletionGracePeriodSeconds implements the metav1.Object interface. +func (e *Empty) GetDeletionGracePeriodSeconds() *int64 { return nil } + +// SetDeletionGracePeriodSeconds implements the metav1.Object interface. +func (e *Empty) SetDeletionGracePeriodSeconds(*int64) {} + +// GetLabels implements the metav1.Object interface. +func (e *Empty) GetLabels() map[string]string { return nil } + +// SetLabels implements the metav1.Object interface. +func (e *Empty) SetLabels(labels map[string]string) {} + +// GetAnnotations implements the metav1.Object interface. +func (e *Empty) GetAnnotations() map[string]string { return nil } + +// SetAnnotations implements the metav1.Object interface. +func (e *Empty) SetAnnotations(annotations map[string]string) {} + +// GetFinalizers implements the metav1.Object interface. +func (e *Empty) GetFinalizers() []string { return nil } + +// SetFinalizers implements the metav1.Object interface. +func (e *Empty) SetFinalizers(finalizers []string) {} + +// GetOwnerReferences implements the metav1.Object interface. +func (e *Empty) GetOwnerReferences() []v1.OwnerReference { return nil } + +// SetOwnerReferences implements the metav1.Object interface. +func (e *Empty) SetOwnerReferences([]v1.OwnerReference) {} + +// GetZZZ_DeprecatedClusterName implements the metav1.Object interface. +func (e *Empty) GetZZZ_DeprecatedClusterName() string { return "" } + +// SetZZZ_DeprecatedClusterName implements the metav1.Object interface. +func (e *Empty) SetZZZ_DeprecatedClusterName(clusterName string) {} + +// GetManagedFields implements the metav1.Object interface. +func (e *Empty) GetManagedFields() []v1.ManagedFieldsEntry { return nil } + +// SetManagedFields implements the metav1.Object interface. +func (e *Empty) SetManagedFields(managedFields []v1.ManagedFieldsEntry) {} diff --git a/ag_201_coredns/plugin/kubernetes/object/pod.go b/ag_201_coredns/plugin/kubernetes/object/pod.go new file mode 100644 index 0000000..9b9d564 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/object/pod.go @@ -0,0 +1,78 @@ +package object + +import ( + "errors" + "fmt" + + api "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// Pod is a stripped down api.Pod with only the items we need for CoreDNS. +type Pod struct { + // Don't add new fields to this struct without talking to the CoreDNS maintainers. + Version string + PodIP string + Name string + Namespace string + + *Empty +} + +var errPodTerminating = errors.New("pod terminating") + +// ToPod converts an api.Pod to a *Pod. +func ToPod(obj meta.Object) (meta.Object, error) { + apiPod, ok := obj.(*api.Pod) + if !ok { + return nil, fmt.Errorf("unexpected object %v", obj) + } + pod := &Pod{ + Version: apiPod.GetResourceVersion(), + PodIP: apiPod.Status.PodIP, + Namespace: apiPod.GetNamespace(), + Name: apiPod.GetName(), + } + t := apiPod.ObjectMeta.DeletionTimestamp + if t != nil && !(*t).Time.IsZero() { + // if the pod is in the process of termination, return an error so it can be ignored + // during add/update event processing + return pod, errPodTerminating + } + + *apiPod = api.Pod{} + + return pod, nil +} + +var _ runtime.Object = &Pod{} + +// DeepCopyObject implements the ObjectKind interface. +func (p *Pod) DeepCopyObject() runtime.Object { + p1 := &Pod{ + Version: p.Version, + PodIP: p.PodIP, + Namespace: p.Namespace, + Name: p.Name, + } + return p1 +} + +// GetNamespace implements the metav1.Object interface. +func (p *Pod) GetNamespace() string { return p.Namespace } + +// SetNamespace implements the metav1.Object interface. +func (p *Pod) SetNamespace(namespace string) {} + +// GetName implements the metav1.Object interface. +func (p *Pod) GetName() string { return p.Name } + +// SetName implements the metav1.Object interface. +func (p *Pod) SetName(name string) {} + +// GetResourceVersion implements the metav1.Object interface. +func (p *Pod) GetResourceVersion() string { return p.Version } + +// SetResourceVersion implements the metav1.Object interface. +func (p *Pod) SetResourceVersion(version string) {} diff --git a/ag_201_coredns/plugin/kubernetes/object/service.go b/ag_201_coredns/plugin/kubernetes/object/service.go new file mode 100644 index 0000000..bd3e3d3 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/object/service.go @@ -0,0 +1,120 @@ +package object + +import ( + "fmt" + + api "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// Service is a stripped down api.Service with only the items we need for CoreDNS. +type Service struct { + // Don't add new fields to this struct without talking to the CoreDNS maintainers. + Version string + Name string + Namespace string + Index string + ClusterIPs []string + Type api.ServiceType + ExternalName string + Ports []api.ServicePort + + // ExternalIPs we may want to export. + ExternalIPs []string + + *Empty +} + +// ServiceKey returns a string using for the index. +func ServiceKey(name, namespace string) string { return name + "." + namespace } + +// ToService converts an api.Service to a *Service. +func ToService(obj meta.Object) (meta.Object, error) { + svc, ok := obj.(*api.Service) + if !ok { + return nil, fmt.Errorf("unexpected object %v", obj) + } + s := &Service{ + Version: svc.GetResourceVersion(), + Name: svc.GetName(), + Namespace: svc.GetNamespace(), + Index: ServiceKey(svc.GetName(), svc.GetNamespace()), + Type: svc.Spec.Type, + ExternalName: svc.Spec.ExternalName, + + ExternalIPs: make([]string, len(svc.Status.LoadBalancer.Ingress)+len(svc.Spec.ExternalIPs)), + } + + if len(svc.Spec.ClusterIPs) > 0 { + s.ClusterIPs = make([]string, len(svc.Spec.ClusterIPs)) + copy(s.ClusterIPs, svc.Spec.ClusterIPs) + } else { + s.ClusterIPs = []string{svc.Spec.ClusterIP} + } + + if len(svc.Spec.Ports) == 0 { + // Add sentinel if there are no ports. + s.Ports = []api.ServicePort{{Port: -1}} + } else { + s.Ports = make([]api.ServicePort, len(svc.Spec.Ports)) + copy(s.Ports, svc.Spec.Ports) + } + + li := copy(s.ExternalIPs, svc.Spec.ExternalIPs) + for i, lb := range svc.Status.LoadBalancer.Ingress { + if lb.IP != "" { + s.ExternalIPs[li+i] = lb.IP + continue + } + s.ExternalIPs[li+i] = lb.Hostname + } + + *svc = api.Service{} + + return s, nil +} + +// Headless returns true if the service is headless +func (s *Service) Headless() bool { + return s.ClusterIPs[0] == api.ClusterIPNone +} + +var _ runtime.Object = &Service{} + +// DeepCopyObject implements the ObjectKind interface. +func (s *Service) DeepCopyObject() runtime.Object { + s1 := &Service{ + Version: s.Version, + Name: s.Name, + Namespace: s.Namespace, + Index: s.Index, + Type: s.Type, + ExternalName: s.ExternalName, + ClusterIPs: make([]string, len(s.ClusterIPs)), + Ports: make([]api.ServicePort, len(s.Ports)), + ExternalIPs: make([]string, len(s.ExternalIPs)), + } + copy(s1.ClusterIPs, s.ClusterIPs) + copy(s1.Ports, s.Ports) + copy(s1.ExternalIPs, s.ExternalIPs) + return s1 +} + +// GetNamespace implements the metav1.Object interface. +func (s *Service) GetNamespace() string { return s.Namespace } + +// SetNamespace implements the metav1.Object interface. +func (s *Service) SetNamespace(namespace string) {} + +// GetName implements the metav1.Object interface. +func (s *Service) GetName() string { return s.Name } + +// SetName implements the metav1.Object interface. +func (s *Service) SetName(name string) {} + +// GetResourceVersion implements the metav1.Object interface. +func (s *Service) GetResourceVersion() string { return s.Version } + +// SetResourceVersion implements the metav1.Object interface. +func (s *Service) SetResourceVersion(version string) {} diff --git a/ag_201_coredns/plugin/kubernetes/parse.go b/ag_201_coredns/plugin/kubernetes/parse.go new file mode 100644 index 0000000..4690c81 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/parse.go @@ -0,0 +1,103 @@ +package kubernetes + +import ( + "github.com/coredns/coredns/plugin/pkg/dnsutil" + + "github.com/miekg/dns" +) + +type recordRequest struct { + // The named port from the kubernetes DNS spec, this is the service part (think _https) from a well formed + // SRV record. + port string + // The protocol is usually _udp or _tcp (if set), and comes from the protocol part of a well formed + // SRV record. + protocol string + endpoint string + // The servicename used in Kubernetes. + service string + // The namespace used in Kubernetes. + namespace string + // A each name can be for a pod or a service, here we track what we've seen, either "pod" or "service". + podOrSvc string +} + +// parseRequest parses the qname to find all the elements we need for querying k8s. Anything +// that is not parsed will have the wildcard "*" value (except r.endpoint). +// Potential underscores are stripped from _port and _protocol. +func parseRequest(name, zone string) (r recordRequest, err error) { + // 3 Possible cases: + // 1. _port._protocol.service.namespace.pod|svc.zone + // 2. (endpoint): endpoint.service.namespace.pod|svc.zone + // 3. (service): service.namespace.pod|svc.zone + + base, _ := dnsutil.TrimZone(name, zone) + // return NODATA for apex queries + if base == "" || base == Svc || base == Pod { + return r, nil + } + segs := dns.SplitDomainName(base) + + last := len(segs) - 1 + if last < 0 { + return r, nil + } + r.podOrSvc = segs[last] + if r.podOrSvc != Pod && r.podOrSvc != Svc { + return r, errInvalidRequest + } + last-- + if last < 0 { + return r, nil + } + + r.namespace = segs[last] + last-- + if last < 0 { + return r, nil + } + + r.service = segs[last] + last-- + if last < 0 { + return r, nil + } + + // Because of ambiguity we check the labels left: 1: an endpoint. 2: port and protocol. + // Anything else is a query that is too long to answer and can safely be delegated to return an nxdomain. + switch last { + case 0: // endpoint only + r.endpoint = segs[last] + case 1: // service and port + r.protocol = stripUnderscore(segs[last]) + r.port = stripUnderscore(segs[last-1]) + + default: // too long + return r, errInvalidRequest + } + + return r, nil +} + +// stripUnderscore removes a prefixed underscore from s. +func stripUnderscore(s string) string { + if len(s) == 0 { + return s + } + if s[0] != '_' { + return s + } + return s[1:] +} + +// String returns a string representation of r, it just returns all fields concatenated with dots. +// This is mostly used in tests. +func (r recordRequest) String() string { + s := r.port + s += "." + r.protocol + s += "." + r.endpoint + s += "." + r.service + s += "." + r.namespace + s += "." + r.podOrSvc + return s +} diff --git a/ag_201_coredns/plugin/kubernetes/parse_test.go b/ag_201_coredns/plugin/kubernetes/parse_test.go new file mode 100644 index 0000000..739a405 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/parse_test.go @@ -0,0 +1,62 @@ +package kubernetes + +import ( + "testing" + + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +func TestParseRequest(t *testing.T) { + tests := []struct { + query string + expected string // output from r.String() + }{ + // valid SRV request + {"_http._tcp.webs.mynamespace.svc.inter.webs.tests.", "http.tcp..webs.mynamespace.svc"}, + // A request of endpoint + {"1-2-3-4.webs.mynamespace.svc.inter.webs.tests.", "..1-2-3-4.webs.mynamespace.svc"}, + // bare zone + {"inter.webs.tests.", "....."}, + // bare svc type + {"svc.inter.webs.tests.", "....."}, + // bare pod type + {"pod.inter.webs.tests.", "....."}, + // SRV request with empty segments + {"..webs.mynamespace.svc.inter.webs.tests.", "...webs.mynamespace.svc"}, + } + for i, tc := range tests { + m := new(dns.Msg) + m.SetQuestion(tc.query, dns.TypeA) + state := request.Request{Zone: zone, Req: m} + + r, e := parseRequest(state.Name(), state.Zone) + if e != nil { + t.Errorf("Test %d, expected no error, got '%v'.", i, e) + } + rs := r.String() + if rs != tc.expected { + t.Errorf("Test %d, expected (stringified) recordRequest: %s, got %s", i, tc.expected, rs) + } + } +} + +func TestParseInvalidRequest(t *testing.T) { + invalid := []string{ + "webs.mynamespace.pood.inter.webs.test.", // Request must be for pod or svc subdomain. + "too.long.for.what.I.am.trying.to.pod.inter.webs.tests.", // Too long. + } + + for i, query := range invalid { + m := new(dns.Msg) + m.SetQuestion(query, dns.TypeA) + state := request.Request{Zone: zone, Req: m} + + if _, e := parseRequest(state.Name(), state.Zone); e == nil { + t.Errorf("Test %d: expected error from %s, got none", i, query) + } + } +} + +const zone = "inter.webs.tests." diff --git a/ag_201_coredns/plugin/kubernetes/ready.go b/ag_201_coredns/plugin/kubernetes/ready.go new file mode 100644 index 0000000..2625f3b --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/ready.go @@ -0,0 +1,4 @@ +package kubernetes + +// Ready implements the ready.Readiness interface. +func (k *Kubernetes) Ready() bool { return k.APIConn.HasSynced() } diff --git a/ag_201_coredns/plugin/kubernetes/reverse.go b/ag_201_coredns/plugin/kubernetes/reverse.go new file mode 100644 index 0000000..26fc3b4 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/reverse.go @@ -0,0 +1,55 @@ +package kubernetes + +import ( + "context" + "strings" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/pkg/dnsutil" + "github.com/coredns/coredns/request" +) + +// Reverse implements the ServiceBackend interface. +func (k *Kubernetes) Reverse(ctx context.Context, state request.Request, exact bool, opt plugin.Options) ([]msg.Service, error) { + ip := dnsutil.ExtractAddressFromReverse(state.Name()) + if ip == "" { + _, e := k.Records(ctx, state, exact) + return nil, e + } + + records := k.serviceRecordForIP(ip, state.Name()) + if len(records) == 0 { + return records, errNoItems + } + return records, nil +} + +// serviceRecordForIP gets a service record with a cluster ip matching the ip argument +// If a service cluster ip does not match, it checks all endpoints +func (k *Kubernetes) serviceRecordForIP(ip, name string) []msg.Service { + // First check services with cluster ips + for _, service := range k.APIConn.SvcIndexReverse(ip) { + if len(k.Namespaces) > 0 && !k.namespaceExposed(service.Namespace) { + continue + } + domain := strings.Join([]string{service.Name, service.Namespace, Svc, k.primaryZone()}, ".") + return []msg.Service{{Host: domain, TTL: k.ttl}} + } + // If no cluster ips match, search endpoints + var svcs []msg.Service + for _, ep := range k.APIConn.EpIndexReverse(ip) { + if len(k.Namespaces) > 0 && !k.namespaceExposed(ep.Namespace) { + continue + } + for _, eps := range ep.Subsets { + for _, addr := range eps.Addresses { + if addr.IP == ip { + domain := strings.Join([]string{endpointHostname(addr, k.endpointNameMode), ep.Index, Svc, k.primaryZone()}, ".") + svcs = append(svcs, msg.Service{Host: domain, TTL: k.ttl}) + } + } + } + } + return svcs +} diff --git a/ag_201_coredns/plugin/kubernetes/reverse_test.go b/ag_201_coredns/plugin/kubernetes/reverse_test.go new file mode 100644 index 0000000..370b9f9 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/reverse_test.go @@ -0,0 +1,256 @@ +package kubernetes + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/kubernetes/object" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" + api "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type APIConnReverseTest struct{} + +func (APIConnReverseTest) HasSynced() bool { return true } +func (APIConnReverseTest) Run() {} +func (APIConnReverseTest) Stop() error { return nil } +func (APIConnReverseTest) PodIndex(string) []*object.Pod { return nil } +func (APIConnReverseTest) EpIndex(string) []*object.Endpoints { return nil } +func (APIConnReverseTest) EndpointsList() []*object.Endpoints { return nil } +func (APIConnReverseTest) ServiceList() []*object.Service { return nil } +func (APIConnReverseTest) SvcExtIndexReverse(string) []*object.Service { return nil } +func (APIConnReverseTest) Modified(bool) int64 { return 0 } + +func (APIConnReverseTest) SvcIndex(svc string) []*object.Service { + if svc != "svc1.testns" { + return nil + } + svcs := []*object.Service{ + { + Name: "svc1", + Namespace: "testns", + ClusterIPs: []string{"192.168.1.100"}, + Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, + }, + } + return svcs +} + +func (APIConnReverseTest) SvcIndexReverse(ip string) []*object.Service { + if ip != "192.168.1.100" { + return nil + } + svcs := []*object.Service{ + { + Name: "svc1", + Namespace: "testns", + ClusterIPs: []string{"192.168.1.100"}, + Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, + }, + } + return svcs +} + +func (APIConnReverseTest) EpIndexReverse(ip string) []*object.Endpoints { + ep1s1 := object.Endpoints{ + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "10.0.0.100", Hostname: "ep1a"}, + {IP: "10.0.0.99", Hostname: "double-ep"}, // this endpoint is used by two services + }, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "svc1-slice1", + Namespace: "testns", + Index: object.EndpointsKey("svc1", "testns"), + } + ep1s2 := object.Endpoints{ + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "1234:abcd::1", Hostname: "ep1b"}, + {IP: "fd00:77:30::a", Hostname: "ip6svc1ex"}, + {IP: "fd00:77:30::2:9ba6", Hostname: "ip6svc1in"}, + }, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "svc1-slice2", + Namespace: "testns", + Index: object.EndpointsKey("svc1", "testns"), + } + ep1s3 := object.Endpoints{ + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "10.0.0.100", Hostname: "ep1a"}, // duplicate endpointslice address + }, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "svc1-ccccc", + Namespace: "testns", + Index: object.EndpointsKey("svc1", "testns"), + } + ep2 := object.Endpoints{ + Subsets: []object.EndpointSubset{ + { + Addresses: []object.EndpointAddress{ + {IP: "10.0.0.99", Hostname: "double-ep"}, // this endpoint is used by two services + }, + Ports: []object.EndpointPort{ + {Port: 80, Protocol: "tcp", Name: "http"}, + }, + }, + }, + Name: "svc2-slice1", + Namespace: "testns", + Index: object.EndpointsKey("svc2", "testns"), + } + switch ip { + case "1234:abcd::1": + fallthrough + case "fd00:77:30::a": + fallthrough + case "fd00:77:30::2:9ba6": + return []*object.Endpoints{&ep1s2} + case "10.0.0.100": // two EndpointSlices for a Service contain this IP (EndpointSlice skew) + return []*object.Endpoints{&ep1s1, &ep1s3} + case "10.0.0.99": // two different Services select this IP + return []*object.Endpoints{&ep1s1, &ep2} + } + return nil +} + +func (APIConnReverseTest) GetNodeByName(ctx context.Context, name string) (*api.Node, error) { + return &api.Node{ + ObjectMeta: meta.ObjectMeta{ + Name: "test.node.foo.bar", + }, + }, nil +} + +func (APIConnReverseTest) GetNamespaceByName(name string) (*object.Namespace, error) { + return &object.Namespace{ + Name: name, + }, nil +} + +func TestReverse(t *testing.T) { + k := New([]string{"cluster.local.", "0.10.in-addr.arpa.", "168.192.in-addr.arpa.", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.d.c.b.a.4.3.2.1.ip6.arpa.", "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."}) + k.APIConn = &APIConnReverseTest{} + + tests := []test.Case{ + { + Qname: "100.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.PTR("100.0.0.10.in-addr.arpa. 5 IN PTR ep1a.svc1.testns.svc.cluster.local."), + }, + }, + { + Qname: "100.1.168.192.in-addr.arpa.", Qtype: dns.TypePTR, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.PTR("100.1.168.192.in-addr.arpa. 5 IN PTR svc1.testns.svc.cluster.local."), + }, + }, + { // A PTR record query for an existing ipv6 endpoint should return a record + Qname: "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.d.c.b.a.4.3.2.1.ip6.arpa.", Qtype: dns.TypePTR, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.PTR("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.d.c.b.a.4.3.2.1.ip6.arpa. 5 IN PTR ep1b.svc1.testns.svc.cluster.local."), + }, + }, + { // A PTR record query for an existing ipv6 endpoint should return a record + Qname: "a.0.0.0.0.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.", Qtype: dns.TypePTR, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.PTR("a.0.0.0.0.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. 5 IN PTR ip6svc1ex.svc1.testns.svc.cluster.local."), + }, + }, + { // A PTR record query for an existing ipv6 endpoint should return a record + Qname: "6.a.b.9.2.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.", Qtype: dns.TypePTR, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.PTR("6.a.b.9.2.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. 5 IN PTR ip6svc1in.svc1.testns.svc.cluster.local."), + }, + }, + { + Qname: "101.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("0.10.in-addr.arpa. 5 IN SOA ns.dns.0.10.in-addr.arpa. hostmaster.0.10.in-addr.arpa. 1502782828 7200 1800 86400 5"), + }, + }, + { + Qname: "example.org.cluster.local.", Qtype: dns.TypePTR, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1502989566 7200 1800 86400 5"), + }, + }, + { + Qname: "svc1.testns.svc.cluster.local.", Qtype: dns.TypePTR, + Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1502989566 7200 1800 86400 5"), + }, + }, + { + Qname: "svc1.testns.svc.0.10.in-addr.arpa.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("0.10.in-addr.arpa. 5 IN SOA ns.dns.0.10.in-addr.arpa. hostmaster.0.10.in-addr.arpa. 1502989566 7200 1800 86400 5"), + }, + }, + { + Qname: "100.0.0.10.cluster.local.", Qtype: dns.TypePTR, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1502989566 7200 1800 86400 5"), + }, + }, + { + Qname: "99.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.PTR("99.0.0.10.in-addr.arpa. 5 IN PTR double-ep.svc1.testns.svc.cluster.local."), + test.PTR("99.0.0.10.in-addr.arpa. 5 IN PTR double-ep.svc2.testns.svc.cluster.local."), + }, + }, + } + + ctx := context.TODO() + for i, tc := range tests { + r := tc.Msg() + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := k.ServeDNS(ctx, w, r) + if err != tc.Error { + t.Errorf("Test %d: expected no error, got %v", i, err) + return + } + + resp := w.Msg + if resp == nil { + t.Fatalf("Test %d: got nil message and no error for: %s %d", i, r.Question[0].Name, r.Question[0].Qtype) + } + if err := test.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/setup.go b/ag_201_coredns/plugin/kubernetes/setup.go new file mode 100644 index 0000000..d7f11e1 --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/setup.go @@ -0,0 +1,253 @@ +package kubernetes + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnsutil" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/upstream" + + "github.com/go-logr/logr" + "github.com/miekg/dns" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" // pull this in here, because we want it excluded if plugin.cfg doesn't have k8s + _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" // pull this in here, because we want it excluded if plugin.cfg doesn't have k8s + _ "k8s.io/client-go/plugin/pkg/client/auth/openstack" // pull this in here, because we want it excluded if plugin.cfg doesn't have k8s + "k8s.io/client-go/tools/clientcmd" + "k8s.io/klog/v2" +) + +const pluginName = "kubernetes" + +var log = clog.NewWithPlugin(pluginName) + +func init() { plugin.Register(pluginName, setup) } + +func setup(c *caddy.Controller) error { + // Do not call klog.InitFlags(nil) here. It will cause reload to panic. + klog.SetLogger(logr.New(&loggerAdapter{log})) + + k, err := kubernetesParse(c) + if err != nil { + return plugin.Error(pluginName, err) + } + + onStart, onShut, err := k.InitKubeCache(context.Background()) + if err != nil { + return plugin.Error(pluginName, err) + } + if onStart != nil { + c.OnStartup(onStart) + } + if onShut != nil { + c.OnShutdown(onShut) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + k.Next = next + return k + }) + + // get locally bound addresses + c.OnStartup(func() error { + k.localIPs = boundIPs(c) + return nil + }) + + return nil +} + +func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) { + var ( + k8s *Kubernetes + err error + ) + + i := 0 + for c.Next() { + if i > 0 { + return nil, plugin.ErrOnce + } + i++ + + k8s, err = ParseStanza(c) + if err != nil { + return k8s, err + } + } + return k8s, nil +} + +// ParseStanza parses a kubernetes stanza +func ParseStanza(c *caddy.Controller) (*Kubernetes, error) { + k8s := New([]string{""}) + k8s.autoPathSearch = searchFromResolvConf() + + opts := dnsControlOpts{ + initEndpointsCache: true, + ignoreEmptyService: false, + } + k8s.opts = opts + + k8s.Zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) + + k8s.primaryZoneIndex = -1 + for i, z := range k8s.Zones { + if dnsutil.IsReverse(z) > 0 { + continue + } + k8s.primaryZoneIndex = i + break + } + + if k8s.primaryZoneIndex == -1 { + return nil, errors.New("non-reverse zone name must be used") + } + + k8s.Upstream = upstream.New() + + for c.NextBlock() { + switch c.Val() { + case "endpoint_pod_names": + args := c.RemainingArgs() + if len(args) > 0 { + return nil, c.ArgErr() + } + k8s.endpointNameMode = true + continue + case "pods": + args := c.RemainingArgs() + if len(args) == 1 { + switch args[0] { + case podModeDisabled, podModeInsecure, podModeVerified: + k8s.podMode = args[0] + default: + return nil, fmt.Errorf("wrong value for pods: %s, must be one of: disabled, verified, insecure", args[0]) + } + continue + } + return nil, c.ArgErr() + case "namespaces": + args := c.RemainingArgs() + if len(args) > 0 { + for _, a := range args { + k8s.Namespaces[a] = struct{}{} + } + continue + } + return nil, c.ArgErr() + case "endpoint": + args := c.RemainingArgs() + if len(args) > 0 { + // Multiple endpoints are deprecated but still could be specified, + // only the first one be used, though + k8s.APIServerList = args + if len(args) > 1 { + log.Warningf("Multiple endpoints have been deprecated, only the first specified endpoint '%s' is used", args[0]) + } + continue + } + return nil, c.ArgErr() + case "tls": // cert key cacertfile + args := c.RemainingArgs() + if len(args) == 3 { + k8s.APIClientCert, k8s.APIClientKey, k8s.APICertAuth = args[0], args[1], args[2] + continue + } + return nil, c.ArgErr() + case "labels": + args := c.RemainingArgs() + if len(args) > 0 { + labelSelectorString := strings.Join(args, " ") + ls, err := meta.ParseToLabelSelector(labelSelectorString) + if err != nil { + return nil, fmt.Errorf("unable to parse label selector value: '%v': %v", labelSelectorString, err) + } + k8s.opts.labelSelector = ls + continue + } + return nil, c.ArgErr() + case "namespace_labels": + args := c.RemainingArgs() + if len(args) > 0 { + namespaceLabelSelectorString := strings.Join(args, " ") + nls, err := meta.ParseToLabelSelector(namespaceLabelSelectorString) + if err != nil { + return nil, fmt.Errorf("unable to parse namespace_label selector value: '%v': %v", namespaceLabelSelectorString, err) + } + k8s.opts.namespaceLabelSelector = nls + continue + } + return nil, c.ArgErr() + case "fallthrough": + k8s.Fall.SetZonesFromArgs(c.RemainingArgs()) + case "ttl": + args := c.RemainingArgs() + if len(args) == 0 { + return nil, c.ArgErr() + } + t, err := strconv.Atoi(args[0]) + if err != nil { + return nil, err + } + if t < 0 || t > 3600 { + return nil, c.Errf("ttl must be in range [0, 3600]: %d", t) + } + k8s.ttl = uint32(t) + case "noendpoints": + if len(c.RemainingArgs()) != 0 { + return nil, c.ArgErr() + } + k8s.opts.initEndpointsCache = false + case "ignore": + args := c.RemainingArgs() + if len(args) > 0 { + ignore := args[0] + if ignore == "empty_service" { + k8s.opts.ignoreEmptyService = true + continue + } else { + return nil, fmt.Errorf("unable to parse ignore value: '%v'", ignore) + } + } + case "kubeconfig": + args := c.RemainingArgs() + if len(args) != 1 && len(args) != 2 { + return nil, c.ArgErr() + } + overrides := &clientcmd.ConfigOverrides{} + if len(args) == 2 { + overrides.CurrentContext = args[1] + } + config := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: args[0]}, + overrides, + ) + k8s.ClientConfig = config + default: + return nil, c.Errf("unknown property '%s'", c.Val()) + } + } + + if len(k8s.Namespaces) != 0 && k8s.opts.namespaceLabelSelector != nil { + return nil, c.Errf("namespaces and namespace_labels cannot both be set") + } + + return k8s, nil +} + +func searchFromResolvConf() []string { + rc, err := dns.ClientConfigFromFile("/etc/resolv.conf") + if err != nil { + return nil + } + plugin.Zones(rc.Search).Normalize() + return rc.Search +} diff --git a/ag_201_coredns/plugin/kubernetes/setup_test.go b/ag_201_coredns/plugin/kubernetes/setup_test.go new file mode 100644 index 0000000..52b0d3f --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/setup_test.go @@ -0,0 +1,612 @@ +package kubernetes + +import ( + "strings" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/pkg/fall" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestKubernetesParse(t *testing.T) { + tests := []struct { + input string // Corefile data as string + shouldErr bool // true if test case is expected to produce an error. + expectedErrContent string // substring from the expected error. Empty for positive cases. + expectedZoneCount int // expected count of defined zones. + expectedNSCount int // expected count of namespaces. + expectedLabelSelector string // expected label selector value + expectedNamespaceLabelSelector string // expected namespace label selector value + expectedPodMode string + expectedFallthrough fall.F + }{ + // positive + { + `kubernetes coredns.local`, + false, + "", + 1, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local test.local`, + false, + "", + 2, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { +}`, + false, + "", + 1, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + endpoint http://localhost:9090 http://localhost:9091 +}`, + false, + "", + 1, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + namespaces demo +}`, + false, + "", + 1, + 1, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + namespaces demo test +}`, + false, + "", + 1, + 2, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + labels environment=prod +}`, + false, + "", + 1, + 0, + "environment=prod", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + labels environment in (production, staging, qa),application=nginx +}`, + false, + "", + 1, + 0, + "application=nginx,environment in (production,qa,staging)", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + namespace_labels istio-injection=enabled +}`, + false, + "", + 1, + 0, + "", + "istio-injection=enabled", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + namespaces foo bar + namespace_labels istio-injection=enabled +}`, + true, + "Error during parsing: namespaces and namespace_labels cannot both be set", + -1, + 0, + "", + "istio-injection=enabled", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local test.local { + endpoint http://localhost:8080 + namespaces demo test + labels environment in (production, staging, qa),application=nginx + fallthrough +}`, + false, + "", + 2, + 2, + "application=nginx,environment in (production,qa,staging)", + "", + podModeDisabled, + fall.Root, + }, + // negative + { + `kubernetes coredns.local { + endpoint +}`, + true, + "rong argument count or unexpected line ending", + -1, + -1, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + namespaces +}`, + true, + "rong argument count or unexpected line ending", + -1, + -1, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + labels +}`, + true, + "rong argument count or unexpected line ending", + -1, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + labels environment in (production, qa +}`, + true, + "unable to parse label selector", + -1, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + // pods disabled + { + `kubernetes coredns.local { + pods disabled +}`, + false, + "", + 1, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + // pods insecure + { + `kubernetes coredns.local { + pods insecure +}`, + false, + "", + 1, + 0, + "", + "", + podModeInsecure, + fall.Zero, + }, + // pods verified + { + `kubernetes coredns.local { + pods verified +}`, + false, + "", + 1, + 0, + "", + "", + podModeVerified, + fall.Zero, + }, + // pods invalid + { + `kubernetes coredns.local { + pods giant_seed +}`, + true, + "rong value for pods", + -1, + 0, + "", + "", + podModeVerified, + fall.Zero, + }, + // fallthrough with zones + { + `kubernetes coredns.local { + fallthrough ip6.arpa inaddr.arpa foo.com +}`, + false, + "rong argument count", + 1, + 0, + "", + "", + podModeDisabled, + fall.F{Zones: []string{"ip6.arpa.", "inaddr.arpa.", "foo.com."}}, + }, + // More than one Kubernetes not allowed + { + `kubernetes coredns.local +kubernetes cluster.local`, + true, + "this plugin", + -1, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + kubeconfig +}`, + true, + "Wrong argument count or unexpected line ending after", + -1, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + kubeconfig file context extraarg +}`, + true, + "Wrong argument count or unexpected line ending after", + -1, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + kubeconfig file +}`, + false, + "", + 1, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + { + `kubernetes coredns.local { + kubeconfig file context +}`, + false, + "", + 1, + 0, + "", + "", + podModeDisabled, + fall.Zero, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + k8sController, err := kubernetesParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error, but did not find error for input '%s'. Error was: '%v'", i, test.input, err) + } + + 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) + continue + } + + if test.shouldErr && (len(test.expectedErrContent) < 1) { + t.Fatalf("Test %d: Test marked as expecting an error, but no expectedErrContent provided for input '%s'. Error was: '%v'", i, test.input, err) + } + + if test.shouldErr && (test.expectedZoneCount >= 0) { + t.Errorf("Test %d: Test marked as expecting an error, but provides value for expectedZoneCount!=-1 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) + } + continue + } + + // No error was raised, so validate initialization of k8sController + // Zones + foundZoneCount := len(k8sController.Zones) + if foundZoneCount != test.expectedZoneCount { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with %d zones, instead found %d zones: '%v' for input '%s'", i, test.expectedZoneCount, foundZoneCount, k8sController.Zones, test.input) + } + + // Namespaces + foundNSCount := len(k8sController.Namespaces) + if foundNSCount != test.expectedNSCount { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with %d namespaces. Instead found %d namespaces: '%v' for input '%s'", i, test.expectedNSCount, foundNSCount, k8sController.Namespaces, test.input) + } + + // Labels + if k8sController.opts.labelSelector != nil { + foundLabelSelectorString := meta.FormatLabelSelector(k8sController.opts.labelSelector) + if foundLabelSelectorString != test.expectedLabelSelector { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with label selector '%s'. Instead found selector '%s' for input '%s'", i, test.expectedLabelSelector, foundLabelSelectorString, test.input) + } + } + // Pods + foundPodMode := k8sController.podMode + if foundPodMode != test.expectedPodMode { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with pod mode '%s'. Instead found pod mode '%s' for input '%s'", i, test.expectedPodMode, foundPodMode, test.input) + } + + // fallthrough + if !k8sController.Fall.Equal(test.expectedFallthrough) { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with fallthrough '%v'. Instead found fallthrough '%v' for input '%s'", i, test.expectedFallthrough, k8sController.Fall, test.input) + } + } +} + +func TestKubernetesParseEndpointPodNames(t *testing.T) { + tests := []struct { + input string // Corefile data as string + shouldErr bool // true if test case is expected to produce an error. + expectedErrContent string // substring from the expected error. Empty for positive cases. + expectedEndpointMode bool + }{ + // valid endpoints mode + { + `kubernetes coredns.local { + endpoint_pod_names +}`, + false, + "", + true, + }, + // endpoints invalid + { + `kubernetes coredns.local { + endpoint_pod_names giant_seed +}`, + true, + "rong argument count or unexpected", + false, + }, + // endpoint not set + { + `kubernetes coredns.local { +}`, + false, + "", + false, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + k8sController, err := kubernetesParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error, but did not find error for input '%s'. Error was: '%v'", i, test.input, err) + } + + 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) + continue + } + + 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) + } + continue + } + + // Endpoints + foundEndpointNameMode := k8sController.endpointNameMode + if foundEndpointNameMode != test.expectedEndpointMode { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with endpoints mode '%v'. Instead found endpoints mode '%v' for input '%s'", i, test.expectedEndpointMode, foundEndpointNameMode, test.input) + } + } +} + +func TestKubernetesParseNoEndpoints(t *testing.T) { + tests := []struct { + input string // Corefile data as string + shouldErr bool // true if test case is expected to produce an error. + expectedErrContent string // substring from the expected error. Empty for positive cases. + expectedEndpointsInit bool + }{ + // valid + { + `kubernetes coredns.local { + noendpoints +}`, + false, + "", + false, + }, + // invalid + { + `kubernetes coredns.local { + noendpoints ixnay on the endpointsay +}`, + true, + "rong argument count or unexpected", + true, + }, + // not set + { + `kubernetes coredns.local { +}`, + false, + "", + true, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + k8sController, err := kubernetesParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error, but did not find error for input '%s'. Error was: '%v'", i, test.input, err) + } + + 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) + continue + } + + 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) + } + continue + } + + foundEndpointsInit := k8sController.opts.initEndpointsCache + if foundEndpointsInit != test.expectedEndpointsInit { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with endpoints watch '%v'. Instead found endpoints watch '%v' for input '%s'", i, test.expectedEndpointsInit, foundEndpointsInit, test.input) + } + } +} + +func TestKubernetesParseIgnoreEmptyService(t *testing.T) { + tests := []struct { + input string // Corefile data as string + shouldErr bool // true if test case is expected to produce an error. + expectedErrContent string // substring from the expected error. Empty for positive cases. + expectedEndpointsInit bool + }{ + // valid + { + `kubernetes coredns.local { + ignore empty_service +}`, + false, + "", + true, + }, + // invalid + { + `kubernetes coredns.local { + ignore ixnay on the endpointsay +}`, + true, + "unable to parse ignore value", + false, + }, + { + `kubernetes coredns.local { + ignore empty_service ixnay on the endpointsay +}`, + false, + "", + true, + }, + // not set + { + `kubernetes coredns.local { +}`, + false, + "", + false, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + k8sController, err := kubernetesParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error, but did not find error for input '%s'. Error was: '%v'", i, test.input, err) + } + + 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) + continue + } + + 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) + } + continue + } + + foundIgnoreEmptyService := k8sController.opts.ignoreEmptyService + if foundIgnoreEmptyService != test.expectedEndpointsInit { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with ignore empty_service '%v'. Instead found ignore empty_service watch '%v' for input '%s'", i, test.expectedEndpointsInit, foundIgnoreEmptyService, test.input) + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/setup_ttl_test.go b/ag_201_coredns/plugin/kubernetes/setup_ttl_test.go new file mode 100644 index 0000000..16b9b4a --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/setup_ttl_test.go @@ -0,0 +1,45 @@ +package kubernetes + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestKubernetesParseTTL(t *testing.T) { + tests := []struct { + input string // Corefile data as string + expectedTTL uint32 // expected count of defined zones. + shouldErr bool + }{ + {`kubernetes cluster.local { + ttl 56 + }`, 56, false}, + {`kubernetes cluster.local`, defaultTTL, false}, + {`kubernetes cluster.local { + ttl -1 + }`, 0, true}, + {`kubernetes cluster.local { + ttl 3601 + }`, 0, true}, + } + + for i, tc := range tests { + c := caddy.NewTestController("dns", tc.input) + k, err := kubernetesParse(c) + if err != nil && !tc.shouldErr { + t.Fatalf("Test %d: Expected no error, got %q", i, err) + } + if err == nil && tc.shouldErr { + t.Fatalf("Test %d: Expected error, got none", i) + } + if err != nil && tc.shouldErr { + // input should error + continue + } + + if k.ttl != tc.expectedTTL { + t.Errorf("Test %d: Expected TTl to be %d, got %d", i, tc.expectedTTL, k.ttl) + } + } +} diff --git a/ag_201_coredns/plugin/kubernetes/xfr.go b/ag_201_coredns/plugin/kubernetes/xfr.go new file mode 100644 index 0000000..1990bea --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/xfr.go @@ -0,0 +1,195 @@ +package kubernetes + +import ( + "context" + "math" + "net" + "sort" + "strings" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/transfer" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + api "k8s.io/api/core/v1" +) + +// Transfer implements the transfer.Transfer interface. +func (k *Kubernetes) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) { + match := plugin.Zones(k.Zones).Matches(zone) + if match == "" { + return nil, transfer.ErrNotAuthoritative + } + // state is not used here, hence the empty request.Request{] + soa, err := plugin.SOA(context.TODO(), k, zone, request.Request{}, plugin.Options{}) + if err != nil { + return nil, transfer.ErrNotAuthoritative + } + + ch := make(chan []dns.RR) + + zonePath := msg.Path(zone, "coredns") + serviceList := k.APIConn.ServiceList() + + go func() { + // ixfr fallback + if serial != 0 && soa[0].(*dns.SOA).Serial == serial { + ch <- soa + close(ch) + return + } + ch <- soa + + nsAddrs := k.nsAddrs(false, false, zone) + nsHosts := make(map[string]struct{}) + for _, nsAddr := range nsAddrs { + nsHost := nsAddr.Header().Name + if _, ok := nsHosts[nsHost]; !ok { + nsHosts[nsHost] = struct{}{} + ch <- []dns.RR{&dns.NS{Hdr: dns.RR_Header{Name: zone, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: k.ttl}, Ns: nsHost}} + } + ch <- nsAddrs + } + + sort.Slice(serviceList, func(i, j int) bool { + return serviceList[i].Name < serviceList[j].Name + }) + + for _, svc := range serviceList { + if !k.namespaceExposed(svc.Namespace) { + continue + } + svcBase := []string{zonePath, Svc, svc.Namespace, svc.Name} + switch svc.Type { + case api.ServiceTypeClusterIP, api.ServiceTypeNodePort, api.ServiceTypeLoadBalancer: + clusterIP := net.ParseIP(svc.ClusterIPs[0]) + if clusterIP != nil { + var host string + for _, ip := range svc.ClusterIPs { + s := msg.Service{Host: ip, TTL: k.ttl} + s.Key = strings.Join(svcBase, "/") + + // Change host from IP to Name for SRV records + host = emitAddressRecord(ch, s) + } + + for _, p := range svc.Ports { + s := msg.Service{Host: host, Port: int(p.Port), TTL: k.ttl} + s.Key = strings.Join(svcBase, "/") + + // Need to generate this to handle use cases for peer-finder + // ref: https://github.com/coredns/coredns/pull/823 + ch <- []dns.RR{s.NewSRV(msg.Domain(s.Key), 100)} + + // As per spec unnamed ports do not have a srv record + // https://github.com/kubernetes/dns/blob/master/docs/specification.md#232---srv-records + if p.Name == "" { + continue + } + + s.Key = strings.Join(append(svcBase, strings.ToLower("_"+string(p.Protocol)), strings.ToLower("_"+string(p.Name))), "/") + + ch <- []dns.RR{s.NewSRV(msg.Domain(s.Key), 100)} + } + + // Skip endpoint discovery if clusterIP is defined + continue + } + + endpointsList := k.APIConn.EpIndex(svc.Name + "." + svc.Namespace) + + for _, ep := range endpointsList { + for _, eps := range ep.Subsets { + srvWeight := calcSRVWeight(len(eps.Addresses)) + for _, addr := range eps.Addresses { + s := msg.Service{Host: addr.IP, TTL: k.ttl} + s.Key = strings.Join(svcBase, "/") + // We don't need to change the msg.Service host from IP to Name yet + // so disregard the return value here + emitAddressRecord(ch, s) + + s.Key = strings.Join(append(svcBase, endpointHostname(addr, k.endpointNameMode)), "/") + // Change host from IP to Name for SRV records + host := emitAddressRecord(ch, s) + s.Host = host + + for _, p := range eps.Ports { + // As per spec unnamed ports do not have a srv record + // https://github.com/kubernetes/dns/blob/master/docs/specification.md#232---srv-records + if p.Name == "" { + continue + } + + s.Port = int(p.Port) + + s.Key = strings.Join(append(svcBase, strings.ToLower("_"+string(p.Protocol)), strings.ToLower("_"+string(p.Name))), "/") + ch <- []dns.RR{s.NewSRV(msg.Domain(s.Key), srvWeight)} + } + } + } + } + + case api.ServiceTypeExternalName: + + s := msg.Service{Key: strings.Join(svcBase, "/"), Host: svc.ExternalName, TTL: k.ttl} + if t, _ := s.HostType(); t == dns.TypeCNAME { + ch <- []dns.RR{s.NewCNAME(msg.Domain(s.Key), s.Host)} + } + } + } + ch <- soa + close(ch) + }() + return ch, nil +} + +// emitAddressRecord generates a new A or AAAA record based on the msg.Service and writes it to a channel. +// emitAddressRecord returns the host name from the generated record. +func emitAddressRecord(c chan<- []dns.RR, s msg.Service) string { + ip := net.ParseIP(s.Host) + dnsType, _ := s.HostType() + switch dnsType { + case dns.TypeA: + r := s.NewA(msg.Domain(s.Key), ip) + c <- []dns.RR{r} + return r.Hdr.Name + case dns.TypeAAAA: + r := s.NewAAAA(msg.Domain(s.Key), ip) + c <- []dns.RR{r} + return r.Hdr.Name + } + + return "" +} + +// calcSrvWeight borrows the logic implemented in plugin.SRV for dynamically +// calculating the srv weight and priority +func calcSRVWeight(numservices int) uint16 { + var services []msg.Service + + for i := 0; i < numservices; i++ { + services = append(services, msg.Service{}) + } + + w := make(map[int]int) + for _, serv := range services { + weight := 100 + if serv.Weight != 0 { + weight = serv.Weight + } + if _, ok := w[serv.Priority]; !ok { + w[serv.Priority] = weight + continue + } + w[serv.Priority] += weight + } + weight := uint16(math.Floor((100.0 / float64(w[0])) * 100)) + // weight should be at least 1 + if weight == 0 { + weight = 1 + } + + return weight +} diff --git a/ag_201_coredns/plugin/kubernetes/xfr_test.go b/ag_201_coredns/plugin/kubernetes/xfr_test.go new file mode 100644 index 0000000..61e5d0a --- /dev/null +++ b/ag_201_coredns/plugin/kubernetes/xfr_test.go @@ -0,0 +1,156 @@ +package kubernetes + +import ( + "net" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/transfer" + + "github.com/miekg/dns" +) + +func TestKubernetesTransferNonAuthZone(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.Namespaces = map[string]struct{}{"testns": {}, "kube-system": {}} + k.localIPs = []net.IP{net.ParseIP("10.0.0.10")} + + dnsmsg := &dns.Msg{} + dnsmsg.SetAxfr("example.com") + + _, err := k.Transfer("example.com", 0) + if err != transfer.ErrNotAuthoritative { + t.Error(err) + } +} + +func TestKubernetesAXFR(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.Namespaces = map[string]struct{}{"testns": {}, "kube-system": {}} + k.localIPs = []net.IP{net.ParseIP("10.0.0.10")} + + dnsmsg := &dns.Msg{} + dnsmsg.SetAxfr(k.Zones[0]) + + ch, err := k.Transfer(k.Zones[0], 0) + if err != nil { + t.Error(err) + } + validateAXFR(t, ch) +} + +func TestKubernetesIXFRFallback(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.Namespaces = map[string]struct{}{"testns": {}, "kube-system": {}} + k.localIPs = []net.IP{net.ParseIP("10.0.0.10")} + + dnsmsg := &dns.Msg{} + dnsmsg.SetAxfr(k.Zones[0]) + + ch, err := k.Transfer(k.Zones[0], 1) + if err != nil { + t.Error(err) + } + validateAXFR(t, ch) +} + +func TestKubernetesIXFRCurrent(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + k.Namespaces = map[string]struct{}{"testns": {}, "kube-system": {}} + k.localIPs = []net.IP{net.ParseIP("10.0.0.10")} + + dnsmsg := &dns.Msg{} + dnsmsg.SetAxfr(k.Zones[0]) + + ch, err := k.Transfer(k.Zones[0], 3) + if err != nil { + t.Error(err) + } + + var gotRRs []dns.RR + for rrs := range ch { + gotRRs = append(gotRRs, rrs...) + } + + // ensure only one record is returned + if len(gotRRs) > 1 { + t.Errorf("Expected only one answer, got %d", len(gotRRs)) + } + + // Ensure first record is a SOA + if gotRRs[0].Header().Rrtype != dns.TypeSOA { + t.Error("Invalid transfer response, does not start with SOA record") + } +} + +func validateAXFR(t *testing.T, ch <-chan []dns.RR) { + xfr := []dns.RR{} + for rrs := range ch { + xfr = append(xfr, rrs...) + } + if xfr[0].Header().Rrtype != dns.TypeSOA { + t.Error("Invalid transfer response, does not start with SOA record") + } + + zp := dns.NewZoneParser(strings.NewReader(expectedZone), "", "") + i := 0 + for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { + if !dns.IsDuplicate(rr, xfr[i]) { + t.Fatalf("Record %d, expected\n%v\n, got\n%v", i, rr, xfr[i]) + } + i++ + } + + if err := zp.Err(); err != nil { + t.Fatal(err) + } +} + +const expectedZone = ` +cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 3 7200 1800 86400 5 +cluster.local. 5 IN NS ns.dns.cluster.local. +ns.dns.cluster.local. 5 IN A 10.0.0.10 +external.testns.svc.cluster.local. 5 IN CNAME ext.interwebs.test. +external-to-service.testns.svc.cluster.local. 5 IN CNAME svc1.testns.svc.cluster.local. +hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.2 +172-0-0-2.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.2 +_http._tcp.hdls1.testns.svc.cluster.local. 5 IN SRV 0 16 80 172-0-0-2.hdls1.testns.svc.cluster.local. +hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.3 +172-0-0-3.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.3 +_http._tcp.hdls1.testns.svc.cluster.local. 5 IN SRV 0 16 80 172-0-0-3.hdls1.testns.svc.cluster.local. +hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.4 +dup-name.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.4 +_http._tcp.hdls1.testns.svc.cluster.local. 5 IN SRV 0 16 80 dup-name.hdls1.testns.svc.cluster.local. +hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.5 +dup-name.hdls1.testns.svc.cluster.local. 5 IN A 172.0.0.5 +_http._tcp.hdls1.testns.svc.cluster.local. 5 IN SRV 0 16 80 dup-name.hdls1.testns.svc.cluster.local. +hdls1.testns.svc.cluster.local. 5 IN AAAA 5678:abcd::1 +5678-abcd--1.hdls1.testns.svc.cluster.local. 5 IN AAAA 5678:abcd::1 +_http._tcp.hdls1.testns.svc.cluster.local. 5 IN SRV 0 16 80 5678-abcd--1.hdls1.testns.svc.cluster.local. +hdls1.testns.svc.cluster.local. 5 IN AAAA 5678:abcd::2 +5678-abcd--2.hdls1.testns.svc.cluster.local. 5 IN AAAA 5678:abcd::2 +_http._tcp.hdls1.testns.svc.cluster.local. 5 IN SRV 0 16 80 5678-abcd--2.hdls1.testns.svc.cluster.local. +hdlsprtls.testns.svc.cluster.local. 5 IN A 172.0.0.20 +172-0-0-20.hdlsprtls.testns.svc.cluster.local. 5 IN A 172.0.0.20 +kubedns.kube-system.svc.cluster.local. 5 IN A 10.0.0.10 +kubedns.kube-system.svc.cluster.local. 5 IN SRV 0 100 53 kubedns.kube-system.svc.cluster.local. +_dns._udp.kubedns.kube-system.svc.cluster.local. 5 IN SRV 0 100 53 kubedns.kube-system.svc.cluster.local. +svc-dual-stack.testns.svc.cluster.local. 5 IN A 10.0.0.3 +svc-dual-stack.testns.svc.cluster.local. 5 IN AAAA 10::3 +svc-dual-stack.testns.svc.cluster.local. 5 IN SRV 0 100 80 svc-dual-stack.testns.svc.cluster.local. +_http._tcp.svc-dual-stack.testns.svc.cluster.local. 5 IN SRV 0 100 80 svc-dual-stack.testns.svc.cluster.local. +svc1.testns.svc.cluster.local. 5 IN A 10.0.0.1 +svc1.testns.svc.cluster.local. 5 IN SRV 0 100 80 svc1.testns.svc.cluster.local. +_http._tcp.svc1.testns.svc.cluster.local. 5 IN SRV 0 100 80 svc1.testns.svc.cluster.local. +svc6.testns.svc.cluster.local. 5 IN AAAA 1234:abcd::1 +svc6.testns.svc.cluster.local. 5 IN SRV 0 100 80 svc6.testns.svc.cluster.local. +_http._tcp.svc6.testns.svc.cluster.local. 5 IN SRV 0 100 80 svc6.testns.svc.cluster.local. +svcempty.testns.svc.cluster.local. 5 IN A 10.0.0.1 +svcempty.testns.svc.cluster.local. 5 IN SRV 0 100 80 svcempty.testns.svc.cluster.local. +_http._tcp.svcempty.testns.svc.cluster.local. 5 IN SRV 0 100 80 svcempty.testns.svc.cluster.local. +cluster.local. 5 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 3 7200 1800 86400 5 +` diff --git a/ag_201_coredns/plugin/loadbalance/README.md b/ag_201_coredns/plugin/loadbalance/README.md new file mode 100644 index 0000000..81a6580 --- /dev/null +++ b/ag_201_coredns/plugin/loadbalance/README.md @@ -0,0 +1,33 @@ +# loadbalance + +## Name + +*loadbalance* - randomizes the order of A, AAAA and MX records. + +## Description + +The *loadbalance* will act as a round-robin DNS load balancer by randomizing the order of A, AAAA, +and MX records in the answer. + +See [Wikipedia](https://en.wikipedia.org/wiki/Round-robin_DNS) about the pros and cons of this +setup. It will take care to sort any CNAMEs before any address records, because some stub resolver +implementations (like glibc) are particular about that. + +## Syntax + +~~~ +loadbalance [POLICY] +~~~ + +* **POLICY** is how to balance. The default, and only option, is "round_robin". + +## Examples + +Load balance replies coming back from Google Public DNS: + +~~~ corefile +. { + loadbalance round_robin + forward . 8.8.8.8 8.8.4.4 +} +~~~ diff --git a/ag_201_coredns/plugin/loadbalance/handler.go b/ag_201_coredns/plugin/loadbalance/handler.go new file mode 100644 index 0000000..ac046c8 --- /dev/null +++ b/ag_201_coredns/plugin/loadbalance/handler.go @@ -0,0 +1,24 @@ +// Package loadbalance is a plugin for rewriting responses to do "load balancing" +package loadbalance + +import ( + "context" + + "github.com/coredns/coredns/plugin" + + "github.com/miekg/dns" +) + +// RoundRobin is a plugin to rewrite responses for "load balancing". +type RoundRobin struct { + Next plugin.Handler +} + +// ServeDNS implements the plugin.Handler interface. +func (rr RoundRobin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + wrr := &RoundRobinResponseWriter{w} + return plugin.NextOrFailure(rr.Name(), rr.Next, ctx, wrr, r) +} + +// Name implements the Handler interface. +func (rr RoundRobin) Name() string { return "loadbalance" } diff --git a/ag_201_coredns/plugin/loadbalance/loadbalance.go b/ag_201_coredns/plugin/loadbalance/loadbalance.go new file mode 100644 index 0000000..966121d --- /dev/null +++ b/ag_201_coredns/plugin/loadbalance/loadbalance.go @@ -0,0 +1,80 @@ +// Package loadbalance shuffles A, AAAA and MX records. +package loadbalance + +import ( + "github.com/miekg/dns" +) + +// RoundRobinResponseWriter is a response writer that shuffles A, AAAA and MX records. +type RoundRobinResponseWriter struct{ dns.ResponseWriter } + +// WriteMsg implements the dns.ResponseWriter interface. +func (r *RoundRobinResponseWriter) WriteMsg(res *dns.Msg) error { + if res.Rcode != dns.RcodeSuccess { + return r.ResponseWriter.WriteMsg(res) + } + + if res.Question[0].Qtype == dns.TypeAXFR || res.Question[0].Qtype == dns.TypeIXFR { + return r.ResponseWriter.WriteMsg(res) + } + + res.Answer = roundRobin(res.Answer) + res.Ns = roundRobin(res.Ns) + res.Extra = roundRobin(res.Extra) + + return r.ResponseWriter.WriteMsg(res) +} + +func roundRobin(in []dns.RR) []dns.RR { + cname := []dns.RR{} + address := []dns.RR{} + mx := []dns.RR{} + rest := []dns.RR{} + for _, r := range in { + switch r.Header().Rrtype { + case dns.TypeCNAME: + cname = append(cname, r) + case dns.TypeA, dns.TypeAAAA: + address = append(address, r) + case dns.TypeMX: + mx = append(mx, r) + default: + rest = append(rest, r) + } + } + + roundRobinShuffle(address) + roundRobinShuffle(mx) + + out := append(cname, rest...) + out = append(out, address...) + out = append(out, mx...) + return out +} + +func roundRobinShuffle(records []dns.RR) { + switch l := len(records); l { + case 0, 1: + break + case 2: + if dns.Id()%2 == 0 { + records[0], records[1] = records[1], records[0] + } + default: + for j := 0; j < l; j++ { + p := j + (int(dns.Id()) % (l - j)) + if j == p { + continue + } + records[j], records[p] = records[p], records[j] + } + } +} + +// Write implements the dns.ResponseWriter interface. +func (r *RoundRobinResponseWriter) Write(buf []byte) (int, error) { + // Should we pack and unpack here to fiddle with the packet... Not likely. + log.Warning("RoundRobin called with Write: not shuffling records") + n, err := r.ResponseWriter.Write(buf) + return n, err +} diff --git a/ag_201_coredns/plugin/loadbalance/loadbalance_test.go b/ag_201_coredns/plugin/loadbalance/loadbalance_test.go new file mode 100644 index 0000000..6f50b6e --- /dev/null +++ b/ag_201_coredns/plugin/loadbalance/loadbalance_test.go @@ -0,0 +1,203 @@ +package loadbalance + +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 TestLoadBalance(t *testing.T) { + rm := RoundRobin{Next: handler()} + + // the first X records must be cnames after this test + tests := []struct { + answer []dns.RR + extra []dns.RR + cnameAnswer int + cnameExtra int + addressAnswer int + addressExtra int + mxAnswer int + mxExtra int + }{ + { + answer: []dns.RR{ + test.CNAME("cname1.region2.skydns.test. 300 IN CNAME cname2.region2.skydns.test."), + test.CNAME("cname2.region2.skydns.test. 300 IN CNAME cname3.region2.skydns.test."), + test.CNAME("cname5.region2.skydns.test. 300 IN CNAME cname6.region2.skydns.test."), + test.CNAME("cname6.region2.skydns.test. 300 IN CNAME endpoint.region2.skydns.test."), + test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.1"), + test.MX("mx.region2.skydns.test. 300 IN MX 1 mx1.region2.skydns.test."), + test.MX("mx.region2.skydns.test. 300 IN MX 2 mx2.region2.skydns.test."), + test.MX("mx.region2.skydns.test. 300 IN MX 3 mx3.region2.skydns.test."), + }, + cnameAnswer: 4, + addressAnswer: 1, + mxAnswer: 3, + }, + { + answer: []dns.RR{ + test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.1"), + test.MX("mx.region2.skydns.test. 300 IN MX 1 mx1.region2.skydns.test."), + test.CNAME("cname.region2.skydns.test. 300 IN CNAME endpoint.region2.skydns.test."), + }, + cnameAnswer: 1, + addressAnswer: 1, + mxAnswer: 1, + }, + { + answer: []dns.RR{ + test.MX("mx.region2.skydns.test. 300 IN MX 1 mx1.region2.skydns.test."), + test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.1"), + test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.2"), + test.MX("mx.region2.skydns.test. 300 IN MX 1 mx2.region2.skydns.test."), + test.CNAME("cname2.region2.skydns.test. 300 IN CNAME cname3.region2.skydns.test."), + test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.3"), + test.MX("mx.region2.skydns.test. 300 IN MX 1 mx3.region2.skydns.test."), + }, + extra: []dns.RR{ + test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.1"), + test.AAAA("endpoint.region2.skydns.test. 300 IN AAAA ::1"), + test.MX("mx.region2.skydns.test. 300 IN MX 1 mx1.region2.skydns.test."), + test.CNAME("cname2.region2.skydns.test. 300 IN CNAME cname3.region2.skydns.test."), + test.MX("mx.region2.skydns.test. 300 IN MX 1 mx2.region2.skydns.test."), + test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.3"), + test.AAAA("endpoint.region2.skydns.test. 300 IN AAAA ::2"), + test.MX("mx.region2.skydns.test. 300 IN MX 1 mx3.region2.skydns.test."), + }, + cnameAnswer: 1, + cnameExtra: 1, + addressAnswer: 3, + addressExtra: 4, + mxAnswer: 3, + mxExtra: 3, + }, + } + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + for i, test := range tests { + req := new(dns.Msg) + req.SetQuestion("region2.skydns.test.", dns.TypeSRV) + req.Answer = test.answer + req.Extra = test.extra + + _, err := rm.ServeDNS(context.TODO(), rec, req) + if err != nil { + t.Errorf("Test %d: Expected no error, but got %s", i, err) + continue + } + + cname, address, mx, sorted := countRecords(rec.Msg.Answer) + if !sorted { + t.Errorf("Test %d: Expected CNAMEs, then AAAAs, then MX in Answer, but got mixed", i) + } + if cname != test.cnameAnswer { + t.Errorf("Test %d: Expected %d CNAMEs in Answer, but got %d", i, test.cnameAnswer, cname) + } + if address != test.addressAnswer { + t.Errorf("Test %d: Expected %d A/AAAAs in Answer, but got %d", i, test.addressAnswer, address) + } + if mx != test.mxAnswer { + t.Errorf("Test %d: Expected %d MXs in Answer, but got %d", i, test.mxAnswer, mx) + } + + cname, address, mx, sorted = countRecords(rec.Msg.Extra) + if !sorted { + t.Errorf("Test %d: Expected CNAMEs, then AAAAs, then MX in Extra, but got mixed", i) + } + if cname != test.cnameExtra { + t.Errorf("Test %d: Expected %d CNAMEs in Extra, but got %d", i, test.cnameAnswer, cname) + } + if address != test.addressExtra { + t.Errorf("Test %d: Expected %d A/AAAAs in Extra, but got %d", i, test.addressAnswer, address) + } + if mx != test.mxExtra { + t.Errorf("Test %d: Expected %d MXs in Extra, but got %d", i, test.mxAnswer, mx) + } + } +} + +func TestLoadBalanceXFR(t *testing.T) { + rm := RoundRobin{Next: handler()} + + answer := []dns.RR{ + test.SOA("skydns.test. 30 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1542756695 7200 1800 86400 30"), + test.MX("mx.region2.skydns.test. 300 IN MX 1 mx1.region2.skydns.test."), + test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.1"), + test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.2"), + test.MX("mx.region2.skydns.test. 300 IN MX 1 mx2.region2.skydns.test."), + test.CNAME("cname2.region2.skydns.test. 300 IN CNAME cname3.region2.skydns.test."), + test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.3"), + test.MX("mx.region2.skydns.test. 300 IN MX 1 mx3.region2.skydns.test."), + test.SOA("skydns.test. 30 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1542756695 7200 1800 86400 30"), + } + + for _, xfrtype := range []uint16{dns.TypeIXFR, dns.TypeAXFR} { + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + req := new(dns.Msg) + req.SetQuestion("skydns.test.", xfrtype) + req.Answer = answer + _, err := rm.ServeDNS(context.TODO(), rec, req) + if err != nil { + t.Errorf("Expected no error, but got %s for %s", err, dns.TypeToString[xfrtype]) + continue + } + + if rec.Msg.Answer[0].Header().Rrtype != dns.TypeSOA { + t.Errorf("Expected SOA record for first answer for %s", dns.TypeToString[xfrtype]) + } + + if rec.Msg.Answer[len(rec.Msg.Answer)-1].Header().Rrtype != dns.TypeSOA { + t.Errorf("Expected SOA record for last answer for %s", dns.TypeToString[xfrtype]) + } + } +} + +func countRecords(result []dns.RR) (cname int, address int, mx int, sorted bool) { + const ( + Start = iota + CNAMERecords + ARecords + MXRecords + Any + ) + + // The order of the records is used to determine if the round-robin actually did anything. + sorted = true + cname = 0 + address = 0 + mx = 0 + state := Start + for _, r := range result { + switch r.Header().Rrtype { + case dns.TypeCNAME: + sorted = sorted && state <= CNAMERecords + state = CNAMERecords + cname++ + case dns.TypeA, dns.TypeAAAA: + sorted = sorted && state <= ARecords + state = ARecords + address++ + case dns.TypeMX: + sorted = sorted && state <= MXRecords + state = MXRecords + mx++ + default: + state = Any + } + } + return +} + +func handler() plugin.Handler { + return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + w.WriteMsg(r) + return dns.RcodeSuccess, nil + }) +} diff --git a/ag_201_coredns/plugin/loadbalance/log_test.go b/ag_201_coredns/plugin/loadbalance/log_test.go new file mode 100644 index 0000000..e4dbd6d --- /dev/null +++ b/ag_201_coredns/plugin/loadbalance/log_test.go @@ -0,0 +1,5 @@ +package loadbalance + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/loadbalance/setup.go b/ag_201_coredns/plugin/loadbalance/setup.go new file mode 100644 index 0000000..d8f273a --- /dev/null +++ b/ag_201_coredns/plugin/loadbalance/setup.go @@ -0,0 +1,43 @@ +package loadbalance + +import ( + "fmt" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" +) + +var log = clog.NewWithPlugin("loadbalance") + +func init() { plugin.Register("loadbalance", setup) } + +func setup(c *caddy.Controller) error { + err := parse(c) + if err != nil { + return plugin.Error("loadbalance", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return RoundRobin{Next: next} + }) + + return nil +} + +func parse(c *caddy.Controller) error { + for c.Next() { + args := c.RemainingArgs() + switch len(args) { + case 0: + return nil + case 1: + if args[0] != "round_robin" { + return fmt.Errorf("unknown policy: %s", args[0]) + } + return nil + } + } + return c.ArgErr() +} diff --git a/ag_201_coredns/plugin/loadbalance/setup_test.go b/ag_201_coredns/plugin/loadbalance/setup_test.go new file mode 100644 index 0000000..38cea14 --- /dev/null +++ b/ag_201_coredns/plugin/loadbalance/setup_test.go @@ -0,0 +1,43 @@ +package loadbalance + +import ( + "strings" + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedPolicy string + expectedErrContent string // substring from the expected error. Empty for positive cases. + }{ + // positive + {`loadbalance`, false, "round_robin", ""}, + {`loadbalance round_robin`, false, "round_robin", ""}, + // negative + {`loadbalance fleeb`, true, "", "unknown policy"}, + {`loadbalance a b`, true, "", "argument count or unexpected line"}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + err := parse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, 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/ag_201_coredns/plugin/local/README.md b/ag_201_coredns/plugin/local/README.md new file mode 100644 index 0000000..08fff01 --- /dev/null +++ b/ag_201_coredns/plugin/local/README.md @@ -0,0 +1,52 @@ +# local + +## Name + +*local* - respond to local names. + +## Description + +*local* will respond with a basic reply to a "local request". Local request are defined to be +names in the following zones: localhost, 0.in-addr.arpa, 127.in-addr.arpa and 255.in-addr.arpa *and* +any query asking for `localhost.`. When seeing the latter a metric counter is increased and +if *debug* is enabled a debug log is emitted. + +With *local* enabled any query falling under these zones will get a reply. The prevents the query +from "escaping" to the internet and putting strain on external infrastructure. + +The zones are mostly empty, only `localhost.` address records (A and AAAA) are defined and a +`1.0.0.127.in-addr.arpa.` reverse (PTR) record. + +## Syntax + +~~~ txt +local +~~~ + +## Metrics + +If monitoring is enabled (via the *prometheus* plugin) then the following metric is exported: + +* `coredns_local_localhost_requests_total{}` - a counter of the number of `localhost.` + requests CoreDNS has seen. Note this does *not* count `localhost.` queries. + +Note that this metric *does not* have a `server` label, because it's more interesting to find the +client(s) performing these queries than to see which server handled it. You'll need to inspect the +debug log to get the client IP address. + +## Examples + +~~~ corefile +. { + local +} +~~~ + +## Bugs + +Only the `in-addr.arpa.` reverse zone is implemented, `ip6.arpa.` queries are not intercepted. + +## See Also + +BIND9's configuration in Debian comes with these zones preconfigured. See the *debug* plugin for +enabling debug logging. diff --git a/ag_201_coredns/plugin/local/local.go b/ag_201_coredns/plugin/local/local.go new file mode 100644 index 0000000..570f113 --- /dev/null +++ b/ag_201_coredns/plugin/local/local.go @@ -0,0 +1,127 @@ +package local + +import ( + "context" + "net" + "strings" + + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +var log = clog.NewWithPlugin("local") + +// Local is a plugin that returns standard replies for local queries. +type Local struct { + Next plugin.Handler +} + +var zones = []string{"localhost.", "0.in-addr.arpa.", "127.in-addr.arpa.", "255.in-addr.arpa."} + +func soaFromOrigin(origin string) []dns.RR { + hdr := dns.RR_Header{Name: origin, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeSOA} + return []dns.RR{&dns.SOA{Hdr: hdr, Ns: "localhost.", Mbox: "root.localhost.", Serial: 1, Refresh: 0, Retry: 0, Expire: 0, Minttl: ttl}} +} + +func nsFromOrigin(origin string) []dns.RR { + hdr := dns.RR_Header{Name: origin, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeNS} + return []dns.RR{&dns.NS{Hdr: hdr, Ns: "localhost."}} +} + +// ServeDNS implements the plugin.Handler interface. +func (l Local) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.QName() + + lc := len("localhost.") + if len(state.Name()) > lc && strings.HasPrefix(state.Name(), "localhost.") { + // we have multiple labels, but the first one is localhost, intercept this and return 127.0.0.1 or ::1 + log.Debugf("Intercepting localhost query for %q %s, from %s", state.Name(), state.Type(), state.IP()) + LocalhostCount.Inc() + reply := doLocalhost(state) + w.WriteMsg(reply) + return 0, nil + } + + zone := plugin.Zones(zones).Matches(qname) + if zone == "" { + return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r) + } + + m := new(dns.Msg) + m.SetReply(r) + zone = qname[len(qname)-len(zone):] + + switch q := state.Name(); q { + case "localhost.", "0.in-addr.arpa.", "127.in-addr.arpa.", "255.in-addr.arpa.": + switch state.QType() { + case dns.TypeA: + if q != "localhost." { + // nodata + m.Ns = soaFromOrigin(qname) + break + } + + hdr := dns.RR_Header{Name: qname, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeA} + m.Answer = []dns.RR{&dns.A{Hdr: hdr, A: net.ParseIP("127.0.0.1").To4()}} + case dns.TypeAAAA: + if q != "localhost." { + // nodata + m.Ns = soaFromOrigin(qname) + break + } + + hdr := dns.RR_Header{Name: qname, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeAAAA} + m.Answer = []dns.RR{&dns.AAAA{Hdr: hdr, AAAA: net.ParseIP("::1")}} + case dns.TypeSOA: + m.Answer = soaFromOrigin(qname) + case dns.TypeNS: + m.Answer = nsFromOrigin(qname) + default: + // nodata + m.Ns = soaFromOrigin(qname) + } + case "1.0.0.127.in-addr.arpa.": + switch state.QType() { + case dns.TypePTR: + hdr := dns.RR_Header{Name: qname, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypePTR} + m.Answer = []dns.RR{&dns.PTR{Hdr: hdr, Ptr: "localhost."}} + default: + // nodata + m.Ns = soaFromOrigin(zone) + } + } + + if len(m.Answer) == 0 && len(m.Ns) == 0 { + m.Ns = soaFromOrigin(zone) + m.Rcode = dns.RcodeNameError + } + + w.WriteMsg(m) + return 0, nil +} + +// Name implements the plugin.Handler interface. +func (l Local) Name() string { return "local" } + +func doLocalhost(state request.Request) *dns.Msg { + m := new(dns.Msg) + m.SetReply(state.Req) + switch state.QType() { + case dns.TypeA: + hdr := dns.RR_Header{Name: state.QName(), Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeA} + m.Answer = []dns.RR{&dns.A{Hdr: hdr, A: net.ParseIP("127.0.0.1").To4()}} + case dns.TypeAAAA: + hdr := dns.RR_Header{Name: state.QName(), Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeAAAA} + m.Answer = []dns.RR{&dns.AAAA{Hdr: hdr, AAAA: net.ParseIP("::1")}} + default: + // nodata + m.Ns = soaFromOrigin(state.QName()) + } + return m +} + +const ttl = 604800 diff --git a/ag_201_coredns/plugin/local/local_test.go b/ag_201_coredns/plugin/local/local_test.go new file mode 100644 index 0000000..8e1561a --- /dev/null +++ b/ag_201_coredns/plugin/local/local_test.go @@ -0,0 +1,77 @@ +package local + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var testcases = []struct { + question string + qtype uint16 + rcode int + answer dns.RR + ns dns.RR +}{ + {"localhost.", dns.TypeA, dns.RcodeSuccess, test.A("localhost. IN A 127.0.0.1"), nil}, + {"localHOst.", dns.TypeA, dns.RcodeSuccess, test.A("localHOst. IN A 127.0.0.1"), nil}, + {"localhost.", dns.TypeAAAA, dns.RcodeSuccess, test.AAAA("localhost. IN AAAA ::1"), nil}, + {"localhost.", dns.TypeNS, dns.RcodeSuccess, test.NS("localhost. IN NS localhost."), nil}, + {"localhost.", dns.TypeSOA, dns.RcodeSuccess, test.SOA("localhost. IN SOA root.localhost. localhost. 1 0 0 0 0"), nil}, + {"127.in-addr.arpa.", dns.TypeA, dns.RcodeSuccess, nil, test.SOA("127.in-addr.arpa. IN SOA root.localhost. localhost. 1 0 0 0 0")}, + {"localhost.", dns.TypeMX, dns.RcodeSuccess, nil, test.SOA("localhost. IN SOA root.localhost. localhost. 1 0 0 0 0")}, + {"a.localhost.", dns.TypeA, dns.RcodeNameError, nil, test.SOA("localhost. IN SOA root.localhost. localhost. 1 0 0 0 0")}, + {"1.0.0.127.in-addr.arpa.", dns.TypePTR, dns.RcodeSuccess, test.PTR("1.0.0.127.in-addr.arpa. IN PTR localhost."), nil}, + {"1.0.0.127.in-addr.arpa.", dns.TypeMX, dns.RcodeSuccess, nil, test.SOA("127.in-addr.arpa. IN SOA root.localhost. localhost. 1 0 0 0 0")}, + {"2.0.0.127.in-addr.arpa.", dns.TypePTR, dns.RcodeNameError, nil, test.SOA("127.in-addr.arpa. IN SOA root.localhost. localhost. 1 0 0 0 0")}, + {"localhost.example.net.", dns.TypeA, dns.RcodeSuccess, test.A("localhost.example.net. IN A 127.0.0.1"), nil}, + {"localhost.example.net.", dns.TypeAAAA, dns.RcodeSuccess, test.AAAA("localhost.example.net IN AAAA ::1"), nil}, + {"localhost.example.net.", dns.TypeSOA, dns.RcodeSuccess, nil, test.SOA("localhost.example.net. IN SOA root.localhost.example.net. localhost.example.net. 1 0 0 0 0")}, +} + +func TestLocal(t *testing.T) { + req := new(dns.Msg) + l := &Local{} + + for i, tc := range testcases { + req.SetQuestion(tc.question, tc.qtype) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := l.ServeDNS(context.TODO(), rec, req) + + if err != nil { + t.Errorf("Test %d, expected no error, but got %q", i, err) + continue + } + if rec.Msg.Rcode != tc.rcode { + t.Errorf("Test %d, expected rcode %d, got %d", i, tc.rcode, rec.Msg.Rcode) + } + if tc.answer == nil && len(rec.Msg.Answer) > 0 { + t.Errorf("Test %d, expected no answer RR, got %s", i, rec.Msg.Answer[0]) + continue + } + if tc.ns == nil && len(rec.Msg.Ns) > 0 { + t.Errorf("Test %d, expected no authority RR, got %s", i, rec.Msg.Ns[0]) + continue + } + if tc.answer != nil { + if x := tc.answer.Header().Rrtype; x != rec.Msg.Answer[0].Header().Rrtype { + t.Errorf("Test %d, expected RR type %d in answer, got %d", i, x, rec.Msg.Answer[0].Header().Rrtype) + } + if x := tc.answer.Header().Name; x != rec.Msg.Answer[0].Header().Name { + t.Errorf("Test %d, expected RR name %q in answer, got %q", i, x, rec.Msg.Answer[0].Header().Name) + } + } + if tc.ns != nil { + if x := tc.ns.Header().Rrtype; x != rec.Msg.Ns[0].Header().Rrtype { + t.Errorf("Test %d, expected RR type %d in authority, got %d", i, x, rec.Msg.Ns[0].Header().Rrtype) + } + if x := tc.ns.Header().Name; x != rec.Msg.Ns[0].Header().Name { + t.Errorf("Test %d, expected RR name %q in authority, got %q", i, x, rec.Msg.Ns[0].Header().Name) + } + } + } +} diff --git a/ag_201_coredns/plugin/local/metrics.go b/ag_201_coredns/plugin/local/metrics.go new file mode 100644 index 0000000..361f9ab --- /dev/null +++ b/ag_201_coredns/plugin/local/metrics.go @@ -0,0 +1,18 @@ +package local + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // LocalhostCount report the number of times we've seen a localhost. query. + LocalhostCount = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "local", + Name: "localhost_requests_total", + Help: "Counter of localhost. requests.", + }) +) diff --git a/ag_201_coredns/plugin/local/setup.go b/ag_201_coredns/plugin/local/setup.go new file mode 100644 index 0000000..9bd0dd6 --- /dev/null +++ b/ag_201_coredns/plugin/local/setup.go @@ -0,0 +1,20 @@ +package local + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("local", setup) } + +func setup(c *caddy.Controller) error { + l := Local{} + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + l.Next = next + return l + }) + + return nil +} diff --git a/ag_201_coredns/plugin/log/README.md b/ag_201_coredns/plugin/log/README.md new file mode 100644 index 0000000..52dd9d7 --- /dev/null +++ b/ag_201_coredns/plugin/log/README.md @@ -0,0 +1,155 @@ +# log + +## Name + +*log* - enables query logging to standard output. + +## Description + +By just using *log* you dump all queries (and parts for the reply) on standard output. Options exist +to tweak the output a little. Note that for busy servers logging will incur a performance hit. + +Enabling or disabling the *log* plugin only affects the query logging, any other logging from +CoreDNS will show up regardless. + +## Syntax + +~~~ txt +log +~~~ + +With no arguments, a query log entry is written to *stdout* in the common log format for all requests. +Or if you want/need slightly more control: + +~~~ txt +log [NAMES...] [FORMAT] +~~~ + +* `NAMES` is the name list to match in order to be logged +* `FORMAT` is the log format to use (default is Common Log Format), `{common}` is used as a shortcut + for the Common Log Format. You can also use `{combined}` for a format that adds the query opcode + `{>opcode}` to the Common Log Format. + +You can further specify the classes of responses that get logged: + +~~~ txt +log [NAMES...] [FORMAT] { + class CLASSES... +} +~~~ + +* `CLASSES` is a space-separated list of classes of responses that should be logged + +The classes of responses have the following meaning: + +* `success`: successful response +* `denial`: either NXDOMAIN or nodata responses (Name exists, type does not). A nodata response + sets the return code to NOERROR. +* `error`: SERVFAIL, NOTIMP, REFUSED, etc. Anything that indicates the remote server is not willing to + resolve the request. +* `all`: the default - nothing is specified. Using of this class means that all messages will be + logged whatever we mix together with "all". + +If no class is specified, it defaults to `all`. + +## Log Format + +You can specify a custom log format with any placeholder values. Log supports both request and +response placeholders. + +The following place holders are supported: + +* `{type}`: qtype of the request +* `{name}`: qname of the request +* `{class}`: qclass of the request +* `{proto}`: protocol used (tcp or udp) +* `{remote}`: client's IP address, for IPv6 addresses these are enclosed in brackets: `[::1]` +* `{local}`: server's IP address, for IPv6 addresses these are enclosed in brackets: `[::1]` +* `{size}`: request size in bytes +* `{port}`: client's port +* `{duration}`: response duration +* `{rcode}`: response RCODE +* `{rsize}`: raw (uncompressed), response size (a client may receive a smaller response) +* `{>rflags}`: response flags, each set flag will be displayed, e.g. "aa, tc". This includes the qr + bit as well +* `{>bufsize}`: the EDNS0 buffer size advertised in the query +* `{>do}`: is the EDNS0 DO (DNSSEC OK) bit set in the query +* `{>id}`: query ID +* `{>opcode}`: query OPCODE +* `{common}`: the default Common Log Format. +* `{combined}`: the Common Log Format with the query opcode. +* `{/LABEL}`: any metadata label is accepted as a place holder if it is enclosed between `{/` and + `}`, the place holder will be replaced by the corresponding metadata value or the default value + `-` if label is not defined. See the *metadata* plugin for more information. + +The default Common Log Format is: + +~~~ txt +`{remote}:{port} - {>id} "{type} {class} {name} {proto} {size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {duration}` +~~~ + +Each of these logs will be outputted with `log.Infof`, so a typical example looks like this: + +~~~ txt +[INFO] [::1]:50759 - 29008 "A IN example.org. udp 41 false 4096" NOERROR qr,rd,ra,ad 68 0.037990251s +~~~ + +## Examples + +Log all requests to stdout + +~~~ corefile +. { + log + whoami +} +~~~ + +Custom log format, for all zones (`.`) + +~~~ corefile +. { + log . "{proto} Request: {name} {type} {>id}" +} +~~~ + +Only log denials (NXDOMAIN and nodata) for example.org (and below) + +~~~ corefile +. { + log example.org { + class denial + } +} +~~~ + +Log all queries which were not resolved successfully in the Combined Log Format. + +~~~ corefile +. { + log . {combined} { + class denial error + } +} +~~~ + +Log all queries on which we did not get errors + +~~~ corefile +. { + log . { + class denial success + } +} +~~~ + +Also the multiple statements can be OR-ed, for example, we can rewrite the above case as following: + +~~~ corefile +. { + log . { + class denial + class success + } +} +~~~ diff --git a/ag_201_coredns/plugin/log/log.go b/ag_201_coredns/plugin/log/log.go new file mode 100644 index 0000000..8a3575f --- /dev/null +++ b/ag_201_coredns/plugin/log/log.go @@ -0,0 +1,74 @@ +// Package log implements basic but useful request (access) logging plugin. +package log + +import ( + "context" + "time" + + "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/pkg/replacer" + "github.com/coredns/coredns/plugin/pkg/response" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Logger is a basic request logging plugin. +type Logger struct { + Next plugin.Handler + Rules []Rule + + repl replacer.Replacer +} + +// ServeDNS implements the plugin.Handler interface. +func (l Logger) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + name := state.Name() + for _, rule := range l.Rules { + if !plugin.Name(rule.NameScope).Matches(name) { + continue + } + + rrw := dnstest.NewRecorder(w) + rc, err := plugin.NextOrFailure(l.Name(), l.Next, ctx, rrw, r) + + // If we don't set up a class in config, the default "all" will be added + // and we shouldn't have an empty rule.Class. + _, ok := rule.Class[response.All] + var ok1 bool + if !ok { + tpe, _ := response.Typify(rrw.Msg, time.Now().UTC()) + class := response.Classify(tpe) + _, ok1 = rule.Class[class] + } + if ok || ok1 { + logstr := l.repl.Replace(ctx, state, rrw, rule.Format) + clog.Info(logstr) + } + + return rc, err + } + return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r) +} + +// Name implements the Handler interface. +func (l Logger) Name() string { return "log" } + +// Rule configures the logging plugin. +type Rule struct { + NameScope string + Class map[response.Class]struct{} + Format string +} + +const ( + // CommonLogFormat is the common log format. + CommonLogFormat = `{remote}:{port} ` + replacer.EmptyValue + ` {>id} "{type} {class} {name} {proto} {size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {duration}` + // CombinedLogFormat is the combined log format. + CombinedLogFormat = CommonLogFormat + ` "{>opcode}"` + // DefaultLogFormat is the default log format. + DefaultLogFormat = CommonLogFormat +) diff --git a/ag_201_coredns/plugin/log/log_test.go b/ag_201_coredns/plugin/log/log_test.go new file mode 100644 index 0000000..e2f3acf --- /dev/null +++ b/ag_201_coredns/plugin/log/log_test.go @@ -0,0 +1,280 @@ +package log + +import ( + "bytes" + "context" + "io" + "log" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/replacer" + "github.com/coredns/coredns/plugin/pkg/response" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func init() { clog.Discard() } + +func TestLoggedStatus(t *testing.T) { + rule := Rule{ + NameScope: ".", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + } + + var f bytes.Buffer + log.SetOutput(&f) + + logger := Logger{ + Rules: []Rule{rule}, + Next: test.ErrorHandler(), + repl: replacer.New(), + } + + ctx := context.TODO() + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + rcode, _ := logger.ServeDNS(ctx, rec, r) + if rcode != 2 { + t.Errorf("Expected rcode to be 2 - was: %d", rcode) + } + + logged := f.String() + if !strings.Contains(logged, "A IN example.org. udp 29 false 512") { + t.Errorf("Expected it to be logged. Logged string: %s", logged) + } +} + +func TestLoggedClassDenial(t *testing.T) { + rule := Rule{ + NameScope: ".", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.Denial: {}}, + } + + var f bytes.Buffer + log.SetOutput(&f) + + logger := Logger{ + Rules: []Rule{rule}, + Next: test.ErrorHandler(), + repl: replacer.New(), + } + + ctx := context.TODO() + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + logger.ServeDNS(ctx, rec, r) + + logged := f.String() + if len(logged) != 0 { + t.Errorf("Expected it not to be logged, but got string: %s", logged) + } +} + +func TestLoggedClassError(t *testing.T) { + rule := Rule{ + NameScope: ".", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.Error: {}}, + } + + var f bytes.Buffer + log.SetOutput(&f) + + logger := Logger{ + Rules: []Rule{rule}, + Next: test.ErrorHandler(), + repl: replacer.New(), + } + + ctx := context.TODO() + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + logger.ServeDNS(ctx, rec, r) + + logged := f.String() + if !strings.Contains(logged, "SERVFAIL") { + t.Errorf("Expected it to be logged. Logged string: %s", logged) + } +} + +func TestLogged(t *testing.T) { + tests := []struct { + Rules []Rule + Domain string + ShouldLog bool + ShouldString string + ShouldNOTString string // for test format + }{ + // case for NameScope + { + Rules: []Rule{ + { + NameScope: "example.org.", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }, + }, + Domain: "example.org.", + ShouldLog: true, + ShouldString: "A IN example.org.", + }, + { + Rules: []Rule{ + { + NameScope: "example.org.", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }, + }, + Domain: "example.net.", + ShouldLog: false, + ShouldString: "", + }, + { + Rules: []Rule{ + { + NameScope: "example.org.", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }, + { + NameScope: "example.net.", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }, + }, + Domain: "example.net.", + ShouldLog: true, + ShouldString: "A IN example.net.", + }, + + // case for format + { + Rules: []Rule{ + { + NameScope: ".", + Format: "{type} {class}", + Class: map[response.Class]struct{}{response.All: {}}, + }, + }, + Domain: "example.org.", + ShouldLog: true, + ShouldString: "A IN", + ShouldNOTString: "example.org", + }, + { + Rules: []Rule{ + { + NameScope: ".", + Format: "{remote}:{port}", + Class: map[response.Class]struct{}{response.All: {}}, + }, + }, + Domain: "example.org.", + ShouldLog: true, + ShouldString: "10.240.0.1:40212", + ShouldNOTString: "A IN example.org", + }, + { + Rules: []Rule{ + { + NameScope: ".", + Format: CombinedLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }, + }, + Domain: "example.org.", + ShouldLog: true, + ShouldString: "\"0\"", + }, + { + Rules: []Rule{ + { + NameScope: ".", + Format: CombinedLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }, + }, + Domain: "foo.%s.example.org.", + ShouldLog: true, + ShouldString: "foo.%s.example.org.", + ShouldNOTString: "%!s(MISSING)", + }, + } + + for _, tc := range tests { + var f bytes.Buffer + log.SetOutput(&f) + + logger := Logger{ + Rules: tc.Rules, + Next: test.ErrorHandler(), + repl: replacer.New(), + } + + ctx := context.TODO() + r := new(dns.Msg) + r.SetQuestion(tc.Domain, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := logger.ServeDNS(ctx, rec, r) + if err != nil { + t.Fatal(err) + } + + logged := f.String() + + if !tc.ShouldLog && len(logged) != 0 { + t.Errorf("Expected it not to be logged, but got string: %s", logged) + } + if tc.ShouldLog && !strings.Contains(logged, tc.ShouldString) { + t.Errorf("Expected it to contains: %s. Logged string: %s", tc.ShouldString, logged) + } + if tc.ShouldLog && tc.ShouldNOTString != "" && strings.Contains(logged, tc.ShouldNOTString) { + t.Errorf("Expected it to NOT contains: %s. Logged string: %s", tc.ShouldNOTString, logged) + } + } +} + +func BenchmarkLogged(b *testing.B) { + log.SetOutput(io.Discard) + + rule := Rule{ + NameScope: ".", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + } + + logger := Logger{ + Rules: []Rule{rule}, + Next: test.ErrorHandler(), + repl: replacer.New(), + } + + ctx := context.TODO() + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + b.StartTimer() + for i := 0; i < b.N; i++ { + logger.ServeDNS(ctx, rec, r) + } +} diff --git a/ag_201_coredns/plugin/log/setup.go b/ag_201_coredns/plugin/log/setup.go new file mode 100644 index 0000000..e1d9913 --- /dev/null +++ b/ag_201_coredns/plugin/log/setup.go @@ -0,0 +1,102 @@ +package log + +import ( + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/replacer" + "github.com/coredns/coredns/plugin/pkg/response" + + "github.com/miekg/dns" +) + +func init() { plugin.Register("log", setup) } + +func setup(c *caddy.Controller) error { + rules, err := logParse(c) + if err != nil { + return plugin.Error("log", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return Logger{Next: next, Rules: rules, repl: replacer.New()} + }) + + return nil +} + +func logParse(c *caddy.Controller) ([]Rule, error) { + var rules []Rule + + for c.Next() { + args := c.RemainingArgs() + length := len(rules) + + switch len(args) { + case 0: + // Nothing specified; use defaults + rules = append(rules, Rule{ + NameScope: ".", + Format: DefaultLogFormat, + Class: make(map[response.Class]struct{}), + }) + case 1: + rules = append(rules, Rule{ + NameScope: dns.Fqdn(args[0]), + Format: DefaultLogFormat, + Class: make(map[response.Class]struct{}), + }) + default: + // Name scopes, and maybe a format specified + format := DefaultLogFormat + + if strings.Contains(args[len(args)-1], "{") { + format = args[len(args)-1] + format = strings.Replace(format, "{common}", CommonLogFormat, -1) + format = strings.Replace(format, "{combined}", CombinedLogFormat, -1) + args = args[:len(args)-1] + } + + for _, str := range args { + rules = append(rules, Rule{ + NameScope: dns.Fqdn(str), + Format: format, + Class: make(map[response.Class]struct{}), + }) + } + } + + // Class refinements in an extra block. + classes := make(map[response.Class]struct{}) + for c.NextBlock() { + switch c.Val() { + // class followed by combinations of all, denial, error and success. + case "class": + classesArgs := c.RemainingArgs() + if len(classesArgs) == 0 { + return nil, c.ArgErr() + } + for _, c := range classesArgs { + cls, err := response.ClassFromString(c) + if err != nil { + return nil, err + } + classes[cls] = struct{}{} + } + default: + return nil, c.ArgErr() + } + } + if len(classes) == 0 { + classes[response.All] = struct{}{} + } + + for i := len(rules) - 1; i >= length; i-- { + rules[i].Class = classes + } + } + + return rules, nil +} diff --git a/ag_201_coredns/plugin/log/setup_test.go b/ag_201_coredns/plugin/log/setup_test.go new file mode 100644 index 0000000..2586ade --- /dev/null +++ b/ag_201_coredns/plugin/log/setup_test.go @@ -0,0 +1,184 @@ +package log + +import ( + "reflect" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/pkg/response" +) + +func TestLogParse(t *testing.T) { + tests := []struct { + inputLogRules string + shouldErr bool + expectedLogRules []Rule + }{ + {`log`, false, []Rule{{ + NameScope: ".", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org`, false, []Rule{{ + NameScope: "example.org.", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org. {common}`, false, []Rule{{ + NameScope: "example.org.", + Format: CommonLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org {combined}`, false, []Rule{{ + NameScope: "example.org.", + Format: CombinedLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org. + log example.net {combined}`, false, []Rule{{ + NameScope: "example.org.", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }, { + NameScope: "example.net.", + Format: CombinedLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org {host} + log example.org {when}`, false, []Rule{{ + NameScope: "example.org.", + Format: "{host}", + Class: map[response.Class]struct{}{response.All: {}}, + }, { + NameScope: "example.org.", + Format: "{when}", + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org example.net`, false, []Rule{{ + NameScope: "example.org.", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }, { + NameScope: "example.net.", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org example.net {host}`, false, []Rule{{ + NameScope: "example.org.", + Format: "{host}", + Class: map[response.Class]struct{}{response.All: {}}, + }, { + NameScope: "example.net.", + Format: "{host}", + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org example.net {when} { + class denial + }`, false, []Rule{{ + NameScope: "example.org.", + Format: "{when}", + Class: map[response.Class]struct{}{response.Denial: {}}, + }, { + NameScope: "example.net.", + Format: "{when}", + Class: map[response.Class]struct{}{response.Denial: {}}, + }}}, + + {`log example.org { + class all + }`, false, []Rule{{ + NameScope: "example.org.", + Format: CommonLogFormat, + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org { + class denial + }`, false, []Rule{{ + NameScope: "example.org.", + Format: CommonLogFormat, + Class: map[response.Class]struct{}{response.Denial: {}}, + }}}, + {`log { + class denial + }`, false, []Rule{{ + NameScope: ".", + Format: CommonLogFormat, + Class: map[response.Class]struct{}{response.Denial: {}}, + }}}, + {`log { + class denial error + }`, false, []Rule{{ + NameScope: ".", + Format: CommonLogFormat, + Class: map[response.Class]struct{}{response.Denial: {}, response.Error: {}}, + }}}, + {`log { + class denial + class error + }`, false, []Rule{{ + NameScope: ".", + Format: CommonLogFormat, + Class: map[response.Class]struct{}{response.Denial: {}, response.Error: {}}, + }}}, + {`log { + class abracadabra + }`, true, []Rule{}}, + {`log { + class + }`, true, []Rule{}}, + {`log { + unknown + }`, true, []Rule{}}, + {`log example.org "{combined} {/forward/upstream}"`, false, []Rule{{ + NameScope: "example.org.", + Format: CombinedLogFormat + " {/forward/upstream}", + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org "{common} {/forward/upstream}"`, false, []Rule{{ + NameScope: "example.org.", + Format: CommonLogFormat + " {/forward/upstream}", + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org "{when} {combined} {/forward/upstream}"`, false, []Rule{{ + NameScope: "example.org.", + Format: "{when} " + CombinedLogFormat + " {/forward/upstream}", + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + {`log example.org "{when} {common} {/forward/upstream}"`, false, []Rule{{ + NameScope: "example.org.", + Format: "{when} " + CommonLogFormat + " {/forward/upstream}", + Class: map[response.Class]struct{}{response.All: {}}, + }}}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputLogRules) + actualLogRules, err := logParse(c) + + if err == nil && test.shouldErr { + t.Errorf("Test %d with input '%s' didn't error, but it should have", i, test.inputLogRules) + } else if err != nil && !test.shouldErr { + t.Errorf("Test %d with input '%s' errored, but it shouldn't have; got '%v'", + i, test.inputLogRules, err) + } + if len(actualLogRules) != len(test.expectedLogRules) { + t.Fatalf("Test %d expected %d no of Log rules, but got %d", + i, len(test.expectedLogRules), len(actualLogRules)) + } + for j, actualLogRule := range actualLogRules { + if actualLogRule.NameScope != test.expectedLogRules[j].NameScope { + t.Errorf("Test %d expected %dth LogRule NameScope for '%s' to be %s , but got %s", + i, j, test.inputLogRules, test.expectedLogRules[j].NameScope, actualLogRule.NameScope) + } + + if actualLogRule.Format != test.expectedLogRules[j].Format { + t.Errorf("Test %d expected %dth LogRule Format for '%s' to be %s , but got %s", + i, j, test.inputLogRules, test.expectedLogRules[j].Format, actualLogRule.Format) + } + + if !reflect.DeepEqual(actualLogRule.Class, test.expectedLogRules[j].Class) { + t.Errorf("Test %d expected %dth LogRule Class to be %v , but got %v", + i, j, test.expectedLogRules[j].Class, actualLogRule.Class) + } + } + } +} diff --git a/ag_201_coredns/plugin/log_test.go b/ag_201_coredns/plugin/log_test.go new file mode 100644 index 0000000..0ee4b7c --- /dev/null +++ b/ag_201_coredns/plugin/log_test.go @@ -0,0 +1,5 @@ +package plugin + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/loop/README.md b/ag_201_coredns/plugin/loop/README.md new file mode 100644 index 0000000..826f5c5 --- /dev/null +++ b/ag_201_coredns/plugin/loop/README.md @@ -0,0 +1,93 @@ +# loop + +## Name + +*loop* - detects simple forwarding loops and halts the server. + +## Description + +The *loop* plugin will send a random probe query to ourselves and will then keep track of how many times +we see it. If we see it more than twice, we assume CoreDNS has seen a forwarding loop and we halt the process. + +The plugin will try to send the query for up to 30 seconds. This is done to give CoreDNS enough time +to start up. Once a query has been successfully sent, *loop* disables itself to prevent a query of +death. + +Note that *loop* will _only_ send "looping queries" for the first zone given in the Server Block. + +The query sent is `..zone` with type set to HINFO. + +## Syntax + +~~~ txt +loop +~~~ + +## Examples + +Start a server on the default port and load the *loop* and *forward* plugins. The *forward* plugin +forwards to it self. + +~~~ txt +. { + loop + forward . 127.0.0.1 +} +~~~ + +After CoreDNS has started it stops the process while logging: + +~~~ txt +plugin/loop: Loop (127.0.0.1:55953 -> :1053) detected for zone ".", see https://coredns.io/plugins/loop#troubleshooting. Query: "HINFO 4547991504243258144.3688648895315093531." +~~~ + +## Limitations + +This plugin only attempts to find simple static forwarding loops at start up time. To detect a loop, +the following must be true: + +* the loop must be present at start up time. + +* the loop must occur for the `HINFO` query type. + +## Troubleshooting + +When CoreDNS logs contain the message `Loop ... detected ...`, this means that the `loop` detection +plugin has detected an infinite forwarding loop in one of the upstream DNS servers. This is a fatal +error because operating with an infinite loop will consume memory and CPU until eventual out of +memory death by the host. + +A forwarding loop is usually caused by: + +* Most commonly, CoreDNS forwarding requests directly to itself. e.g. via a loopback address such as `127.0.0.1`, `::1` or `127.0.0.53` +* Less commonly, CoreDNS forwarding to an upstream server that in turn, forwards requests back to CoreDNS. + +To troubleshoot this problem, look in your Corefile for any `forward`s to the zone +in which the loop was detected. Make sure that they are not forwarding to a local address or +to another DNS server that is forwarding requests back to CoreDNS. If `forward` is +using a file (e.g. `/etc/resolv.conf`), make sure that file does not contain local addresses. + +### Troubleshooting Loops In Kubernetes Clusters + +When a CoreDNS Pod deployed in Kubernetes detects a loop, the CoreDNS Pod will start to "CrashLoopBackOff". +This is because Kubernetes will try to restart the Pod every time CoreDNS detects the loop and exits. + +A common cause of forwarding loops in Kubernetes clusters is an interaction with a local DNS cache +on the host node (e.g. `systemd-resolved`). For example, in certain configurations `systemd-resolved` will +put the loopback address `127.0.0.53` as a nameserver into `/etc/resolv.conf`. Kubernetes (via `kubelet`) by default +will pass this `/etc/resolv.conf` file to all Pods using the `default` dnsPolicy rendering them +unable to make DNS lookups (this includes CoreDNS Pods). CoreDNS uses this `/etc/resolv.conf` +as a list of upstreams to forward requests to. Since it contains a loopback address, CoreDNS ends up forwarding +requests to itself. + +There are many ways to work around this issue, some are listed here: + +* Add the following to your `kubelet` config yaml: `resolvConf: ` (or via command line flag `--resolv-conf` deprecated in 1.10). Your "real" + `resolv.conf` is the one that contains the actual IPs of your upstream servers, and no local/loopback address. + This flag tells `kubelet` to pass an alternate `resolv.conf` to Pods. For systems using `systemd-resolved`, +`/run/systemd/resolve/resolv.conf` is typically the location of the "real" `resolv.conf`, +although this can be different depending on your distribution. +* Disable the local DNS cache on host nodes, and restore `/etc/resolv.conf` to the original. +* A quick and dirty fix is to edit your Corefile, replacing `forward . /etc/resolv.conf` with +the IP address of your upstream DNS, for example `forward . 8.8.8.8`. But this only fixes the issue for CoreDNS, +kubelet will continue to forward the invalid `resolv.conf` to all `default` dnsPolicy Pods, leaving them unable to resolve DNS. diff --git a/ag_201_coredns/plugin/loop/log_test.go b/ag_201_coredns/plugin/loop/log_test.go new file mode 100644 index 0000000..882b5c8 --- /dev/null +++ b/ag_201_coredns/plugin/loop/log_test.go @@ -0,0 +1,5 @@ +package loop + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/loop/loop.go b/ag_201_coredns/plugin/loop/loop.go new file mode 100644 index 0000000..8d29798 --- /dev/null +++ b/ag_201_coredns/plugin/loop/loop.go @@ -0,0 +1,109 @@ +package loop + +import ( + "context" + "sync" + + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +var log = clog.NewWithPlugin("loop") + +// Loop is a plugin that implements loop detection by sending a "random" query. +type Loop struct { + Next plugin.Handler + + zone string + qname string + addr string + + sync.RWMutex + i int + off bool +} + +// New returns a new initialized Loop. +func New(zone string) *Loop { return &Loop{zone: zone, qname: qname(zone)} } + +// ServeDNS implements the plugin.Handler interface. +func (l *Loop) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if r.Question[0].Qtype != dns.TypeHINFO { + return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r) + } + if l.disabled() { + return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r) + } + + state := request.Request{W: w, Req: r} + + zone := plugin.Zones([]string{l.zone}).Matches(state.Name()) + if zone == "" { + return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r) + } + + if state.Name() == l.qname { + l.inc() + } + + if l.seen() > 2 { + log.Fatalf(`Loop (%s -> %s) detected for zone %q, see https://coredns.io/plugins/loop#troubleshooting. Query: "HINFO %s"`, state.RemoteAddr(), l.address(), l.zone, l.qname) + } + + return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r) +} + +// Name implements the plugin.Handler interface. +func (l *Loop) Name() string { return "loop" } + +func (l *Loop) exchange(addr string) (*dns.Msg, error) { + m := new(dns.Msg) + m.SetQuestion(l.qname, dns.TypeHINFO) + + return dns.Exchange(m, addr) +} + +func (l *Loop) seen() int { + l.RLock() + defer l.RUnlock() + return l.i +} + +func (l *Loop) inc() { + l.Lock() + defer l.Unlock() + l.i++ +} + +func (l *Loop) reset() { + l.Lock() + defer l.Unlock() + l.i = 0 +} + +func (l *Loop) setDisabled() { + l.Lock() + defer l.Unlock() + l.off = true +} + +func (l *Loop) disabled() bool { + l.RLock() + defer l.RUnlock() + return l.off +} + +func (l *Loop) setAddress(addr string) { + l.Lock() + defer l.Unlock() + l.addr = addr +} + +func (l *Loop) address() string { + l.RLock() + defer l.RUnlock() + return l.addr +} diff --git a/ag_201_coredns/plugin/loop/loop_test.go b/ag_201_coredns/plugin/loop/loop_test.go new file mode 100644 index 0000000..e7a4b06 --- /dev/null +++ b/ag_201_coredns/plugin/loop/loop_test.go @@ -0,0 +1,11 @@ +package loop + +import "testing" + +func TestLoop(t *testing.T) { + l := New(".") + l.inc() + if l.seen() != 1 { + t.Errorf("Failed to inc loop, expected %d, got %d", 1, l.seen()) + } +} diff --git a/ag_201_coredns/plugin/loop/setup.go b/ag_201_coredns/plugin/loop/setup.go new file mode 100644 index 0000000..4e076c6 --- /dev/null +++ b/ag_201_coredns/plugin/loop/setup.go @@ -0,0 +1,87 @@ +package loop + +import ( + "net" + "strconv" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnsutil" + "github.com/coredns/coredns/plugin/pkg/rand" +) + +func init() { plugin.Register("loop", setup) } + +func setup(c *caddy.Controller) error { + l, err := parse(c) + if err != nil { + return plugin.Error("loop", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + l.Next = next + return l + }) + + // Send query to ourselves and see if it end up with us again. + c.OnStartup(func() error { + // Another Go function, otherwise we block startup and can't send the packet. + go func() { + deadline := time.Now().Add(30 * time.Second) + conf := dnsserver.GetConfig(c) + lh := conf.ListenHosts[0] + addr := net.JoinHostPort(lh, conf.Port) + + for time.Now().Before(deadline) { + l.setAddress(addr) + if _, err := l.exchange(addr); err != nil { + l.reset() + time.Sleep(1 * time.Second) + continue + } + + go func() { + time.Sleep(2 * time.Second) + l.setDisabled() + }() + + break + } + l.setDisabled() + }() + return nil + }) + + return nil +} + +func parse(c *caddy.Controller) (*Loop, error) { + i := 0 + zones := []string{"."} + for c.Next() { + if i > 0 { + return nil, plugin.ErrOnce + } + i++ + if c.NextArg() { + return nil, c.ArgErr() + } + + if len(c.ServerBlockKeys) > 0 { + zones = plugin.Host(c.ServerBlockKeys[0]).NormalizeExact() + } + } + return New(zones[0]), nil +} + +// qname returns a random name. .. +func qname(zone string) string { + l1 := strconv.Itoa(r.Int()) + l2 := strconv.Itoa(r.Int()) + + return dnsutil.Join(l1, l2, zone) +} + +var r = rand.New(time.Now().UnixNano()) diff --git a/ag_201_coredns/plugin/loop/setup_test.go b/ag_201_coredns/plugin/loop/setup_test.go new file mode 100644 index 0000000..6b80b9b --- /dev/null +++ b/ag_201_coredns/plugin/loop/setup_test.go @@ -0,0 +1,19 @@ +package loop + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", `loop`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `loop argument`) + if err := setup(c); err == nil { + t.Fatal("Expected errors, but got none") + } +} diff --git a/ag_201_coredns/plugin/metadata/README.md b/ag_201_coredns/plugin/metadata/README.md new file mode 100644 index 0000000..6eb2c39 --- /dev/null +++ b/ag_201_coredns/plugin/metadata/README.md @@ -0,0 +1,49 @@ +# metadata + +## Name + +*metadata* - enables a metadata collector. + +## Description + +By enabling *metadata* any plugin that implements [metadata.Provider +interface](https://godoc.org/github.com/coredns/coredns/plugin/metadata#Provider) will be called for +each DNS query, at the beginning of the process for that query, in order to add its own metadata to +context. + +The metadata collected will be available for all plugins, via the Context parameter provided in the +ServeDNS function. The package (code) documentation has examples on how to inspect and retrieve +metadata a plugin might be interested in. + +The metadata is added by setting a label with a value in the context. These labels should be named +`plugin/NAME`, where **NAME** is something descriptive. The only hard requirement the *metadata* +plugin enforces is that the labels contain a slash. See the documentation for +`metadata.SetValueFunc`. + +The value stored is a string. The empty string signals "no metadata". See the documentation for +`metadata.ValueFunc` on how to retrieve this. + +## Syntax + +~~~ +metadata [ZONES... ] +~~~ + +* **ZONES** zones metadata should be invoked for. + +## Plugins + +`metadata.Provider` interface needs to be implemented by each plugin willing to provide metadata +information for other plugins. It will be called by metadata and gather the information from all +plugins in context. + +Note: this method should work quickly, because it is called for every request. + +## Examples + +The *rewrite* plugin uses meta data to rewrite requests. + +## See Also + +The [Provider interface](https://godoc.org/github.com/coredns/coredns/plugin/metadata#Provider) and +the [package level](https://godoc.org/github.com/coredns/coredns/plugin/metadata) documentation. diff --git a/ag_201_coredns/plugin/metadata/log_test.go b/ag_201_coredns/plugin/metadata/log_test.go new file mode 100644 index 0000000..8d1e924 --- /dev/null +++ b/ag_201_coredns/plugin/metadata/log_test.go @@ -0,0 +1,5 @@ +package metadata + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/metadata/metadata.go b/ag_201_coredns/plugin/metadata/metadata.go new file mode 100644 index 0000000..58e5ce2 --- /dev/null +++ b/ag_201_coredns/plugin/metadata/metadata.go @@ -0,0 +1,44 @@ +package metadata + +import ( + "context" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Metadata implements collecting metadata information from all plugins that +// implement the Provider interface. +type Metadata struct { + Zones []string + Providers []Provider + Next plugin.Handler +} + +// Name implements the Handler interface. +func (m *Metadata) Name() string { return "metadata" } + +// ContextWithMetadata is exported for use by provider tests +func ContextWithMetadata(ctx context.Context) context.Context { + return context.WithValue(ctx, key{}, md{}) +} + +// ServeDNS implements the plugin.Handler interface. +func (m *Metadata) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + rcode, err := plugin.NextOrFailure(m.Name(), m.Next, ctx, w, r) + return rcode, err +} + +// Collect will retrieve metadata functions from each metadata provider and update the context +func (m *Metadata) Collect(ctx context.Context, state request.Request) context.Context { + ctx = ContextWithMetadata(ctx) + if plugin.Zones(m.Zones).Matches(state.Name()) != "" { + // Go through all Providers and collect metadata. + for _, p := range m.Providers { + ctx = p.Metadata(ctx, state) + } + } + return ctx +} diff --git a/ag_201_coredns/plugin/metadata/metadata_test.go b/ag_201_coredns/plugin/metadata/metadata_test.go new file mode 100644 index 0000000..6b8da6d --- /dev/null +++ b/ag_201_coredns/plugin/metadata/metadata_test.go @@ -0,0 +1,93 @@ +package metadata + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +type testProvider map[string]Func + +func (tp testProvider) Metadata(ctx context.Context, state request.Request) context.Context { + for k, v := range tp { + SetValueFunc(ctx, k, v) + } + return ctx +} + +type testHandler struct{ ctx context.Context } + +func (m *testHandler) Name() string { return "test" } + +func (m *testHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + m.ctx = ctx + return 0, nil +} + +func TestMetadataServeDNS(t *testing.T) { + expectedMetadata := []testProvider{ + {"test/key1": func() string { return "testvalue1" }}, + {"test/key2": func() string { return "two" }, "test/key3": func() string { return "testvalue3" }}, + } + // Create fake Providers based on expectedMetadata + providers := []Provider{} + for _, e := range expectedMetadata { + providers = append(providers, e) + } + + next := &testHandler{} // fake handler which stores the resulting context + m := Metadata{ + Zones: []string{"."}, + Providers: providers, + Next: next, + } + + ctx := context.TODO() + w := &test.ResponseWriter{} + r := new(dns.Msg) + ctx = m.Collect(ctx, request.Request{W: w, Req: r}) + m.ServeDNS(ctx, w, r) + nctx := next.ctx + + for _, expected := range expectedMetadata { + for label, expVal := range expected { + if !IsLabel(label) { + t.Errorf("Expected label %s is not considered a valid label", label) + } + val := ValueFunc(nctx, label) + if val() != expVal() { + t.Errorf("Expected value %s for %s, but got %s", expVal(), label, val()) + } + } + } +} + +func TestLabelFormat(t *testing.T) { + labels := []struct { + label string + isValid bool + }{ + // ok + {"plugin/LABEL", true}, + {"p/LABEL", true}, + {"plugin/L", true}, + {"PLUGIN/LABEL/SUB-LABEL", true}, + // fails + {"LABEL", false}, + {"plugin.LABEL", false}, + {"/NO-PLUGIN-NOT-ACCEPTED", false}, + {"ONLY-PLUGIN-NOT-ACCEPTED/", false}, + {"/", false}, + {"//", false}, + } + + for _, test := range labels { + if x := IsLabel(test.label); x != test.isValid { + t.Errorf("Label %v expected %v, got: %v", test.label, test.isValid, x) + } + } +} diff --git a/ag_201_coredns/plugin/metadata/provider.go b/ag_201_coredns/plugin/metadata/provider.go new file mode 100644 index 0000000..e1bd705 --- /dev/null +++ b/ag_201_coredns/plugin/metadata/provider.go @@ -0,0 +1,127 @@ +// Package metadata provides an API that allows plugins to add metadata to the context. +// Each metadata is stored under a label that has the form /. Each metadata +// is returned as a Func. When Func is called the metadata is returned. If Func is expensive to +// execute it is its responsibility to provide some form of caching. During the handling of a +// query it is expected the metadata stays constant. +// +// Basic example: +// +// Implement the Provider interface for a plugin p: +// +// func (p P) Metadata(ctx context.Context, state request.Request) context.Context { +// metadata.SetValueFunc(ctx, "test/something", func() string { return "myvalue" }) +// return ctx +// } +// +// Basic example with caching: +// +// func (p P) Metadata(ctx context.Context, state request.Request) context.Context { +// cached := "" +// f := func() string { +// if cached != "" { +// return cached +// } +// cached = expensiveFunc() +// return cached +// } +// metadata.SetValueFunc(ctx, "test/something", f) +// return ctx +// } +// +// If you need access to this metadata from another plugin: +// +// // ... +// valueFunc := metadata.ValueFunc(ctx, "test/something") +// value := valueFunc() +// // use 'value' +// +package metadata + +import ( + "context" + "strings" + + "github.com/coredns/coredns/request" +) + +// Provider interface needs to be implemented by each plugin willing to provide +// metadata information for other plugins. +type Provider interface { + // Metadata adds metadata to the context and returns a (potentially) new context. + // Note: this method should work quickly, because it is called for every request + // from the metadata plugin. + Metadata(ctx context.Context, state request.Request) context.Context +} + +// Func is the type of function in the metadata, when called they return the value of the label. +type Func func() string + +// IsLabel checks that the provided name is a valid label name, i.e. two or more words separated by a slash. +func IsLabel(label string) bool { + p := strings.Index(label, "/") + if p <= 0 || p >= len(label)-1 { + // cannot accept namespace empty nor label empty + return false + } + return true +} + +// Labels returns all metadata keys stored in the context. These label names should be named +// as: plugin/NAME, where NAME is something descriptive. +func Labels(ctx context.Context) []string { + if metadata := ctx.Value(key{}); metadata != nil { + if m, ok := metadata.(md); ok { + return keys(m) + } + } + return nil +} + +// ValueFuncs returns the map[string]Func from the context, or nil if it does not exist. +func ValueFuncs(ctx context.Context) map[string]Func { + if metadata := ctx.Value(key{}); metadata != nil { + if m, ok := metadata.(md); ok { + return m + } + } + return nil +} + +// ValueFunc returns the value function of label. If none can be found nil is returned. Calling the +// function returns the value of the label. +func ValueFunc(ctx context.Context, label string) Func { + if metadata := ctx.Value(key{}); metadata != nil { + if m, ok := metadata.(md); ok { + return m[label] + } + } + return nil +} + +// SetValueFunc set the metadata label to the value function. If no metadata can be found this is a noop and +// false is returned. Any existing value is overwritten. +func SetValueFunc(ctx context.Context, label string, f Func) bool { + if metadata := ctx.Value(key{}); metadata != nil { + if m, ok := metadata.(md); ok { + m[label] = f + return true + } + } + return false +} + +// md is metadata information storage. +type md map[string]Func + +// key defines the type of key that is used to save metadata into the context. +type key struct{} + +func keys(m map[string]Func) []string { + s := make([]string, len(m)) + i := 0 + for k := range m { + s[i] = k + i++ + } + return s +} diff --git a/ag_201_coredns/plugin/metadata/setup.go b/ag_201_coredns/plugin/metadata/setup.go new file mode 100644 index 0000000..90b1cf9 --- /dev/null +++ b/ag_201_coredns/plugin/metadata/setup.go @@ -0,0 +1,44 @@ +package metadata + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("metadata", setup) } + +func setup(c *caddy.Controller) error { + m, err := metadataParse(c) + if err != nil { + return err + } + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + m.Next = next + return m + }) + + c.OnStartup(func() error { + plugins := dnsserver.GetConfig(c).Handlers() + for _, p := range plugins { + if met, ok := p.(Provider); ok { + m.Providers = append(m.Providers, met) + } + } + return nil + }) + + return nil +} + +func metadataParse(c *caddy.Controller) (*Metadata, error) { + m := &Metadata{} + c.Next() + + m.Zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) + + if c.NextBlock() || c.Next() { + return nil, plugin.Error("metadata", c.ArgErr()) + } + return m, nil +} diff --git a/ag_201_coredns/plugin/metadata/setup_test.go b/ag_201_coredns/plugin/metadata/setup_test.go new file mode 100644 index 0000000..ed552f7 --- /dev/null +++ b/ag_201_coredns/plugin/metadata/setup_test.go @@ -0,0 +1,70 @@ +package metadata + +import ( + "reflect" + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + tests := []struct { + input string + zones []string + shouldErr bool + }{ + {"metadata", []string{}, false}, + {"metadata example.com.", []string{"example.com."}, false}, + {"metadata example.com. net.", []string{"example.com.", "net."}, false}, + + {"metadata example.com. { some_param }", []string{}, true}, + {"metadata\nmetadata", []string{}, true}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + err := setup(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Setup call expected error but found none for input %s", i, test.input) + } + + if !test.shouldErr && err != nil { + t.Errorf("Test %d: Setup call expected no error but found one for input %s. Error was: %v", i, test.input, err) + } + } +} + +func TestSetupHealth(t *testing.T) { + tests := []struct { + input string + zones []string + shouldErr bool + }{ + {"metadata", []string{}, false}, + {"metadata example.com.", []string{"example.com."}, false}, + {"metadata example.com. net.", []string{"example.com.", "net."}, false}, + + {"metadata example.com. { some_param }", []string{}, true}, + {"metadata\nmetadata", []string{}, true}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + m, err := metadataParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found none for input %s", i, test.input) + } + + if !test.shouldErr && err != nil { + t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) + } + + if !test.shouldErr && err == nil { + if !reflect.DeepEqual(test.zones, m.Zones) { + t.Errorf("Test %d: Expected zones %s. Zones were: %v", i, test.zones, m.Zones) + } + } + } +} diff --git a/ag_201_coredns/plugin/metrics/README.md b/ag_201_coredns/plugin/metrics/README.md new file mode 100644 index 0000000..ec5da10 --- /dev/null +++ b/ag_201_coredns/plugin/metrics/README.md @@ -0,0 +1,90 @@ +# prometheus + +## Name + +*prometheus* - enables [Prometheus](https://prometheus.io/) metrics. + +## Description + +With *prometheus* you export metrics from CoreDNS and any plugin that has them. +The default location for the metrics is `localhost:9153`. The metrics path is fixed to `/metrics`. + +In addition to the default Go metrics exported by the [Prometheus Go client](https://prometheus.io/docs/guides/go-application/), +the following metrics are exported: + +* `coredns_build_info{version, revision, goversion}` - info about CoreDNS itself. +* `coredns_panics_total{}` - total number of panics. +* `coredns_dns_requests_total{server, zone, view, proto, family, type}` - total query count. +* `coredns_dns_request_duration_seconds{server, zone, view, type}` - duration to process each query. +* `coredns_dns_request_size_bytes{server, zone, view, proto}` - size of the request in bytes. +* `coredns_dns_do_requests_total{server, view, zone}` - queries that have the DO bit set +* `coredns_dns_response_size_bytes{server, zone, view, proto}` - response size in bytes. +* `coredns_dns_responses_total{server, zone, view, rcode, plugin}` - response per zone, rcode and plugin. +* `coredns_dns_https_responses_total{server, status}` - responses per server and http status code. +* `coredns_plugin_enabled{server, zone, view, name}` - indicates whether a plugin is enabled on per server, zone and view basis. + +Almost each counter has a label `zone` which is the zonename used for the request/response. + +Extra labels used are: + +* `server` is identifying the server responsible for the request. This is a string formatted + as the server's listening address: `://[]:`. I.e. for a "normal" DNS server + this is `dns://:53`. If you are using the *bind* plugin an IP address is included, e.g.: `dns://127.0.0.53:53`. +* `proto` which holds the transport of the response ("udp" or "tcp") +* The address family (`family`) of the transport (1 = IP (IP version 4), 2 = IP6 (IP version 6)). +* `type` which holds the query type. It holds most common types (A, AAAA, MX, SOA, CNAME, PTR, TXT, + NS, SRV, DS, DNSKEY, RRSIG, NSEC, NSEC3, HTTPS, IXFR, AXFR and ANY) and "other" which lumps together all + other types. +* `status` which holds the https status code. Possible values are: + * 200 - request is processed, + * 404 - request has been rejected on validation, + * 400 - request to dns message conversion failed, + * 500 - processing ended up with no response. +* the `plugin` 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. + +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). + +Other plugins may export additional stats when the _prometheus_ plugin is enabled. Those stats are documented in each +plugin's README. + +This plugin can only be used once per Server Block. + +## Syntax + +~~~ +prometheus [ADDRESS] +~~~ + +For each zone that you want to see metrics for. + +It optionally takes a bind address to which the metrics are exported; the default +listens on `localhost:9153`. The metrics path is fixed to `/metrics`. + +## Examples + +Use an alternative listening address: + +~~~ corefile +. { + prometheus localhost:9253 +} +~~~ + +Or via an environment variable (this is supported throughout the Corefile): `export PORT=9253`, and +then: + +~~~ corefile +. { + prometheus localhost:{$PORT} +} +~~~ + +## Bugs + +When reloading, the Prometheus handler is stopped before the new server instance is started. +If that new server fails to start, then the initial server instance is still available and DNS queries still served, +but Prometheus handler stays down. +Prometheus will not reply HTTP request until a successful reload or a complete restart of CoreDNS. +Only the plugins that register as Handler are visible in `coredns_plugin_enabled{server, zone, name}`. As of today the plugins reload and bind will not be reported. diff --git a/ag_201_coredns/plugin/metrics/context.go b/ag_201_coredns/plugin/metrics/context.go new file mode 100644 index 0000000..ae2856d --- /dev/null +++ b/ag_201_coredns/plugin/metrics/context.go @@ -0,0 +1,37 @@ +package metrics + +import ( + "context" + + "github.com/coredns/coredns/core/dnsserver" +) + +// WithServer returns the current server handling the request. It returns the +// server listening address: ://[]: Normally this is +// something like "dns://:53", but if the bind plugin is used, i.e. "bind +// 127.0.0.53", it will be "dns://127.0.0.53:53", etc. If not address is found +// the empty string is returned. +// +// Basic usage with a metric: +// +// .WithLabelValues(metrics.WithServer(ctx), labels..).Add(1) +func WithServer(ctx context.Context) string { + srv := ctx.Value(dnsserver.Key{}) + if srv == nil { + return "" + } + return srv.(*dnsserver.Server).Addr +} + +// WithView returns the name of the view currently handling the request, if a view is defined. +// +// Basic usage with a metric: +// +// .WithLabelValues(metrics.WithView(ctx), labels..).Add(1) +func WithView(ctx context.Context) string { + v := ctx.Value(dnsserver.ViewKey{}) + if v == nil { + return "" + } + return v.(string) +} diff --git a/ag_201_coredns/plugin/metrics/handler.go b/ag_201_coredns/plugin/metrics/handler.go new file mode 100644 index 0000000..41da690 --- /dev/null +++ b/ag_201_coredns/plugin/metrics/handler.go @@ -0,0 +1,57 @@ +package metrics + +import ( + "context" + "path/filepath" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metrics/vars" + "github.com/coredns/coredns/plugin/pkg/rcode" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// ServeDNS implements the Handler interface. +func (m *Metrics) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + + qname := state.QName() + zone := plugin.Zones(m.ZoneNames()).Matches(qname) + if zone == "" { + zone = "." + } + + // Record response to get status code and size of the reply. + rw := NewRecorder(w) + status, err := plugin.NextOrFailure(m.Name(), m.Next, ctx, rw, r) + + rc := rw.Rcode + if !plugin.ClientWrite(status) { + // when no response was written, fallback to status returned from next plugin as this status + // is actually used as rcode of DNS response + // see https://github.com/coredns/coredns/blob/master/core/dnsserver/server.go#L318 + rc = status + } + plugin := m.authoritativePlugin(rw.Caller) + vars.Report(WithServer(ctx), state, zone, WithView(ctx), rcode.ToString(rc), plugin, rw.Len, rw.Start) + + return status, err +} + +// Name implements the Handler interface. +func (m *Metrics) Name() string { return "prometheus" } + +// authoritativePlugin returns which of made the write, if none is found the empty string is returned. +func (m *Metrics) authoritativePlugin(caller [3]string) string { + // a b and c contain the full path of the caller, the plugin name 2nd last elements + // .../coredns/plugin/whoami/whoami.go --> whoami + // this is likely FS specific, so use filepath. + for _, c := range caller { + plug := filepath.Base(filepath.Dir(c)) + if _, ok := m.plugins[plug]; ok { + return plug + } + } + return "" +} diff --git a/ag_201_coredns/plugin/metrics/log_test.go b/ag_201_coredns/plugin/metrics/log_test.go new file mode 100644 index 0000000..101098a --- /dev/null +++ b/ag_201_coredns/plugin/metrics/log_test.go @@ -0,0 +1,5 @@ +package metrics + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/metrics/metrics.go b/ag_201_coredns/plugin/metrics/metrics.go new file mode 100644 index 0000000..6a9e652 --- /dev/null +++ b/ag_201_coredns/plugin/metrics/metrics.go @@ -0,0 +1,172 @@ +// Package metrics implement a handler and plugin that provides Prometheus metrics. +package metrics + +import ( + "context" + "net" + "net/http" + "sync" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/reuseport" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// Metrics holds the prometheus configuration. The metrics' path is fixed to be /metrics . +type Metrics struct { + Next plugin.Handler + Addr string + Reg *prometheus.Registry + + ln net.Listener + lnSetup bool + + mux *http.ServeMux + srv *http.Server + + zoneNames []string + zoneMap map[string]struct{} + zoneMu sync.RWMutex + + plugins map[string]struct{} // all available plugins, used to determine which plugin made the client write +} + +// New returns a new instance of Metrics with the given address. +func New(addr string) *Metrics { + met := &Metrics{ + Addr: addr, + Reg: prometheus.DefaultRegisterer.(*prometheus.Registry), + zoneMap: make(map[string]struct{}), + plugins: pluginList(caddy.ListPlugins()), + } + + return met +} + +// MustRegister wraps m.Reg.MustRegister. +func (m *Metrics) MustRegister(c prometheus.Collector) { + err := m.Reg.Register(c) + if err != nil { + // ignore any duplicate error, but fatal on any other kind of error + if _, ok := err.(prometheus.AlreadyRegisteredError); !ok { + log.Fatalf("Cannot register metrics collector: %s", err) + } + } +} + +// AddZone adds zone z to m. +func (m *Metrics) AddZone(z string) { + m.zoneMu.Lock() + m.zoneMap[z] = struct{}{} + m.zoneNames = keys(m.zoneMap) + m.zoneMu.Unlock() +} + +// RemoveZone remove zone z from m. +func (m *Metrics) RemoveZone(z string) { + m.zoneMu.Lock() + delete(m.zoneMap, z) + m.zoneNames = keys(m.zoneMap) + m.zoneMu.Unlock() +} + +// ZoneNames returns the zones of m. +func (m *Metrics) ZoneNames() []string { + m.zoneMu.RLock() + s := m.zoneNames + m.zoneMu.RUnlock() + return s +} + +// OnStartup sets up the metrics on startup. +func (m *Metrics) OnStartup() error { + ln, err := reuseport.Listen("tcp", m.Addr) + if err != nil { + log.Errorf("Failed to start metrics handler: %s", err) + return err + } + + m.ln = ln + m.lnSetup = true + + m.mux = http.NewServeMux() + m.mux.Handle("/metrics", promhttp.HandlerFor(m.Reg, promhttp.HandlerOpts{})) + + // creating some helper variables to avoid data races on m.srv and m.ln + server := &http.Server{Handler: m.mux} + m.srv = server + + go func() { + server.Serve(ln) + }() + + ListenAddr = ln.Addr().String() // For tests. + return nil +} + +// OnRestart stops the listener on reload. +func (m *Metrics) OnRestart() error { + if !m.lnSetup { + return nil + } + u.Unset(m.Addr) + return m.stopServer() +} + +func (m *Metrics) stopServer() error { + if !m.lnSetup { + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + defer cancel() + if err := m.srv.Shutdown(ctx); err != nil { + log.Infof("Failed to stop prometheus http server: %s", err) + return err + } + m.lnSetup = false + m.ln.Close() + return nil +} + +// OnFinalShutdown tears down the metrics listener on shutdown and restart. +func (m *Metrics) OnFinalShutdown() error { return m.stopServer() } + +func keys(m map[string]struct{}) []string { + sx := []string{} + for k := range m { + sx = append(sx, k) + } + return sx +} + +// pluginList iterates over the returned plugin map from caddy and removes the "dns." prefix from them. +func pluginList(m map[string][]string) map[string]struct{} { + pm := map[string]struct{}{} + for _, p := range m["others"] { + // only add 'dns.' plugins + if len(p) > 3 { + pm[p[4:]] = struct{}{} + continue + } + } + return pm +} + +// ListenAddr is assigned the address of the prometheus listener. Its use is mainly in tests where +// we listen on "localhost:0" and need to retrieve the actual address. +var ListenAddr string + +// shutdownTimeout is the maximum amount of time the metrics plugin will wait +// before erroring when it tries to close the metrics server +const shutdownTimeout time.Duration = time.Second * 5 + +var buildInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: plugin.Namespace, + Name: "build_info", + Help: "A metric with a constant '1' value labeled by version, revision, and goversion from which CoreDNS was built.", +}, []string{"version", "revision", "goversion"}) diff --git a/ag_201_coredns/plugin/metrics/metrics_test.go b/ag_201_coredns/plugin/metrics/metrics_test.go new file mode 100644 index 0000000..bd72bf1 --- /dev/null +++ b/ag_201_coredns/plugin/metrics/metrics_test.go @@ -0,0 +1,82 @@ +package metrics + +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 TestMetrics(t *testing.T) { + met := New("localhost:0") + if err := met.OnStartup(); err != nil { + t.Fatalf("Failed to start metrics handler: %s", err) + } + defer met.OnFinalShutdown() + + met.AddZone("example.org.") + + tests := []struct { + next plugin.Handler + qname string + qtype uint16 + metric string + expectedValue string + }{ + // This all works because 1 bucket (1 zone, 1 type) + { + next: test.NextHandler(dns.RcodeSuccess, nil), + qname: "example.org.", + metric: "coredns_dns_requests_total", + expectedValue: "1", + }, + { + next: test.NextHandler(dns.RcodeSuccess, nil), + qname: "example.org.", + metric: "coredns_dns_requests_total", + expectedValue: "2", + }, + { + next: test.NextHandler(dns.RcodeSuccess, nil), + qname: "example.org.", + metric: "coredns_dns_requests_total", + expectedValue: "3", + }, + { + next: test.NextHandler(dns.RcodeSuccess, nil), + qname: "example.org.", + metric: "coredns_dns_responses_total", + expectedValue: "4", + }, + } + + ctx := context.TODO() + + for i, tc := range tests { + req := new(dns.Msg) + if tc.qtype == 0 { + tc.qtype = dns.TypeA + } + req.SetQuestion(tc.qname, tc.qtype) + met.Next = tc.next + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := met.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("Test %d: Expected no error, but got %s", i, err) + } + + result := test.Scrape("http://" + ListenAddr + "/metrics") + + if tc.expectedValue != "" { + got, _ := test.MetricValue(tc.metric, result) + if got != tc.expectedValue { + t.Errorf("Test %d: Expected value %s for metrics %s, but got %s", i, tc.expectedValue, tc.metric, got) + } + } + } +} diff --git a/ag_201_coredns/plugin/metrics/recorder.go b/ag_201_coredns/plugin/metrics/recorder.go new file mode 100644 index 0000000..d4d42ba --- /dev/null +++ b/ag_201_coredns/plugin/metrics/recorder.go @@ -0,0 +1,28 @@ +package metrics + +import ( + "runtime" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + + "github.com/miekg/dns" +) + +// Recorder is a dnstest.Recorder specific to the metrics plugin. +type Recorder struct { + *dnstest.Recorder + // CallerN holds the string return value of the call to runtime.Caller(N+1) + Caller [3]string +} + +// NewRecorder makes and returns a new Recorder. +func NewRecorder(w dns.ResponseWriter) *Recorder { return &Recorder{Recorder: dnstest.NewRecorder(w)} } + +// WriteMsg records the status code and calls the +// underlying ResponseWriter's WriteMsg method. +func (r *Recorder) WriteMsg(res *dns.Msg) error { + _, r.Caller[0], _, _ = runtime.Caller(1) + _, r.Caller[1], _, _ = runtime.Caller(2) + _, r.Caller[2], _, _ = runtime.Caller(3) + return r.Recorder.WriteMsg(res) +} diff --git a/ag_201_coredns/plugin/metrics/recorder_test.go b/ag_201_coredns/plugin/metrics/recorder_test.go new file mode 100644 index 0000000..fd8c5fc --- /dev/null +++ b/ag_201_coredns/plugin/metrics/recorder_test.go @@ -0,0 +1,68 @@ +package metrics + +import ( + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +type inmemoryWriter struct { + test.ResponseWriter + written []byte +} + +func (r *inmemoryWriter) WriteMsg(m *dns.Msg) error { + r.written, _ = m.Pack() + return r.ResponseWriter.WriteMsg(m) +} + +func (r *inmemoryWriter) Write(buf []byte) (int, error) { + r.written = buf + return r.ResponseWriter.Write(buf) +} + +func TestRecorder_WriteMsg(t *testing.T) { + successResp := dns.Msg{} + successResp.Answer = []dns.RR{ + test.A("a.example.org. 1800 IN A 127.0.0.53"), + } + + nxdomainResp := dns.Msg{} + nxdomainResp.Rcode = dns.RcodeNameError + + tests := []struct { + name string + msg *dns.Msg + }{ + { + name: "should record successful response", + msg: &successResp, + }, + { + name: "should record nxdomain response", + msg: &nxdomainResp, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tw := inmemoryWriter{ResponseWriter: test.ResponseWriter{}} + rec := NewRecorder(&tw) + + if err := rec.WriteMsg(tt.msg); err != nil { + t.Errorf("Test %d: WriteMsg() unexpected error %v", i, err) + } + + if rec.Msg != tt.msg { + t.Errorf("Test %d: Expected value %v for msg, but got %v", i, tt.msg, rec.Msg) + } + if rec.Len != tt.msg.Len() { + t.Errorf("Test %d: Expected value %d for len, but got %d", i, tt.msg.Len(), rec.Len) + } + if rec.Rcode != tt.msg.Rcode { + t.Errorf("Test %d: Expected value %d for rcode, but got %d", i, tt.msg.Rcode, rec.Rcode) + } + }) + } +} diff --git a/ag_201_coredns/plugin/metrics/registry.go b/ag_201_coredns/plugin/metrics/registry.go new file mode 100644 index 0000000..2d6a92e --- /dev/null +++ b/ag_201_coredns/plugin/metrics/registry.go @@ -0,0 +1,28 @@ +package metrics + +import ( + "sync" + + "github.com/prometheus/client_golang/prometheus" +) + +type reg struct { + sync.RWMutex + r map[string]*prometheus.Registry +} + +func newReg() *reg { return ®{r: make(map[string]*prometheus.Registry)} } + +// update sets the registry if not already there and returns the input. Or it returns +// a previous set value. +func (r *reg) getOrSet(addr string, pr *prometheus.Registry) *prometheus.Registry { + r.Lock() + defer r.Unlock() + + if v, ok := r.r[addr]; ok { + return v + } + + r.r[addr] = pr + return pr +} diff --git a/ag_201_coredns/plugin/metrics/setup.go b/ag_201_coredns/plugin/metrics/setup.go new file mode 100644 index 0000000..bee7d1f --- /dev/null +++ b/ag_201_coredns/plugin/metrics/setup.go @@ -0,0 +1,105 @@ +package metrics + +import ( + "net" + "runtime" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/coremain" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metrics/vars" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/uniq" +) + +var ( + log = clog.NewWithPlugin("prometheus") + u = uniq.New() + registry = newReg() +) + +func init() { plugin.Register("prometheus", setup) } + +func setup(c *caddy.Controller) error { + m, err := parse(c) + if err != nil { + return plugin.Error("prometheus", err) + } + m.Reg = registry.getOrSet(m.Addr, m.Reg) + + c.OnStartup(func() error { m.Reg = registry.getOrSet(m.Addr, m.Reg); u.Set(m.Addr, m.OnStartup); return nil }) + c.OnRestartFailed(func() error { m.Reg = registry.getOrSet(m.Addr, m.Reg); u.Set(m.Addr, m.OnStartup); return nil }) + + c.OnStartup(func() error { return u.ForEach() }) + c.OnRestartFailed(func() error { return u.ForEach() }) + + c.OnStartup(func() error { + conf := dnsserver.GetConfig(c) + for _, h := range conf.ListenHosts { + addrstr := conf.Transport + "://" + net.JoinHostPort(h, conf.Port) + for _, p := range conf.Handlers() { + vars.PluginEnabled.WithLabelValues(addrstr, conf.Zone, conf.ViewName, p.Name()).Set(1) + } + } + return nil + }) + c.OnRestartFailed(func() error { + conf := dnsserver.GetConfig(c) + for _, h := range conf.ListenHosts { + addrstr := conf.Transport + "://" + net.JoinHostPort(h, conf.Port) + for _, p := range conf.Handlers() { + vars.PluginEnabled.WithLabelValues(addrstr, conf.Zone, conf.ViewName, p.Name()).Set(1) + } + } + return nil + }) + + c.OnRestart(m.OnRestart) + c.OnRestart(func() error { vars.PluginEnabled.Reset(); return nil }) + c.OnFinalShutdown(m.OnFinalShutdown) + + // Initialize metrics. + buildInfo.WithLabelValues(coremain.CoreVersion, coremain.GitCommit, runtime.Version()).Set(1) + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + m.Next = next + return m + }) + + return nil +} + +func parse(c *caddy.Controller) (*Metrics, error) { + met := New(defaultAddr) + + i := 0 + for c.Next() { + if i > 0 { + return nil, plugin.ErrOnce + } + i++ + + zones := plugin.OriginsFromArgsOrServerBlock(nil /* args */, c.ServerBlockKeys) + for _, z := range zones { + met.AddZone(z) + } + args := c.RemainingArgs() + + switch len(args) { + case 0: + case 1: + met.Addr = args[0] + _, _, e := net.SplitHostPort(met.Addr) + if e != nil { + return met, e + } + default: + return met, c.ArgErr() + } + } + return met, nil +} + +// defaultAddr is the address the where the metrics are exported by default. +const defaultAddr = "localhost:9153" diff --git a/ag_201_coredns/plugin/metrics/setup_test.go b/ag_201_coredns/plugin/metrics/setup_test.go new file mode 100644 index 0000000..3a584a6 --- /dev/null +++ b/ag_201_coredns/plugin/metrics/setup_test.go @@ -0,0 +1,42 @@ +package metrics + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestPrometheusParse(t *testing.T) { + tests := []struct { + input string + shouldErr bool + addr string + }{ + // oks + {`prometheus`, false, "localhost:9153"}, + {`prometheus localhost:53`, false, "localhost:53"}, + // fails + {`prometheus {}`, true, ""}, + {`prometheus /foo`, true, ""}, + {`prometheus a b c`, true, ""}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + m, err := parse(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 test.addr != m.Addr { + t.Errorf("Test %v: Expected address %s but found: %s", i, test.addr, m.Addr) + } + } +} diff --git a/ag_201_coredns/plugin/metrics/vars/monitor.go b/ag_201_coredns/plugin/metrics/vars/monitor.go new file mode 100644 index 0000000..191324e --- /dev/null +++ b/ag_201_coredns/plugin/metrics/vars/monitor.go @@ -0,0 +1,36 @@ +package vars + +import ( + "github.com/miekg/dns" +) + +var monitorType = map[uint16]struct{}{ + dns.TypeAAAA: {}, + dns.TypeA: {}, + dns.TypeCNAME: {}, + dns.TypeDNSKEY: {}, + dns.TypeDS: {}, + dns.TypeMX: {}, + dns.TypeNSEC3: {}, + dns.TypeNSEC: {}, + dns.TypeNS: {}, + dns.TypePTR: {}, + dns.TypeRRSIG: {}, + dns.TypeSOA: {}, + dns.TypeSRV: {}, + dns.TypeTXT: {}, + dns.TypeHTTPS: {}, + // Meta Qtypes + dns.TypeIXFR: {}, + dns.TypeAXFR: {}, + dns.TypeANY: {}, +} + +// qTypeString returns the RR type based on monitorType. It returns the text representation +// of those types. RR types not in that list will have "other" returned. +func qTypeString(qtype uint16) string { + if _, known := monitorType[qtype]; known { + return dns.Type(qtype).String() + } + return "other" +} diff --git a/ag_201_coredns/plugin/metrics/vars/report.go b/ag_201_coredns/plugin/metrics/vars/report.go new file mode 100644 index 0000000..92f6bc1 --- /dev/null +++ b/ag_201_coredns/plugin/metrics/vars/report.go @@ -0,0 +1,33 @@ +package vars + +import ( + "time" + + "github.com/coredns/coredns/request" +) + +// Report reports the metrics data associated with request. This function is exported because it is also +// called from core/dnsserver to report requests hitting the server that should not be handled and are thus +// not sent down the plugin chain. +func Report(server string, req request.Request, zone, view, rcode, plugin string, size int, start time.Time) { + // Proto and Family. + net := req.Proto() + fam := "1" + if req.Family() == 2 { + fam = "2" + } + + if req.Do() { + RequestDo.WithLabelValues(server, zone, view).Inc() + } + + qType := qTypeString(req.QType()) + RequestCount.WithLabelValues(server, zone, view, net, fam, qType).Inc() + + RequestDuration.WithLabelValues(server, zone, view).Observe(time.Since(start).Seconds()) + + ResponseSize.WithLabelValues(server, zone, view, net).Observe(float64(size)) + RequestSize.WithLabelValues(server, zone, view, net).Observe(float64(req.Len())) + + ResponseRcode.WithLabelValues(server, zone, view, rcode, plugin).Inc() +} diff --git a/ag_201_coredns/plugin/metrics/vars/vars.go b/ag_201_coredns/plugin/metrics/vars/vars.go new file mode 100644 index 0000000..f0cf829 --- /dev/null +++ b/ag_201_coredns/plugin/metrics/vars/vars.go @@ -0,0 +1,82 @@ +package vars + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// Request* and Response* are the prometheus counters and gauges we are using for exporting metrics. +var ( + RequestCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: subsystem, + Name: "requests_total", + Help: "Counter of DNS requests made per zone, protocol and family.", + }, []string{"server", "zone", "view", "proto", "family", "type"}) + + RequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: plugin.Namespace, + Subsystem: subsystem, + Name: "request_duration_seconds", + Buckets: plugin.TimeBuckets, + Help: "Histogram of the time (in seconds) each request took per zone.", + }, []string{"server", "zone", "view"}) + + RequestSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: plugin.Namespace, + Subsystem: subsystem, + Name: "request_size_bytes", + Help: "Size of the EDNS0 UDP buffer in bytes (64K for TCP) per zone and protocol.", + Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3}, + }, []string{"server", "zone", "view", "proto"}) + + RequestDo = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: subsystem, + Name: "do_requests_total", + Help: "Counter of DNS requests with DO bit set per zone.", + }, []string{"server", "zone", "view"}) + + ResponseSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: plugin.Namespace, + Subsystem: subsystem, + Name: "response_size_bytes", + Help: "Size of the returned response in bytes.", + Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3}, + }, []string{"server", "zone", "view", "proto"}) + + ResponseRcode = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: subsystem, + Name: "responses_total", + Help: "Counter of response status codes.", + }, []string{"server", "zone", "view", "rcode", "plugin"}) + + Panic = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Name: "panics_total", + Help: "A metrics that counts the number of panics.", + }) + + PluginEnabled = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: plugin.Namespace, + Name: "plugin_enabled", + Help: "A metric that indicates whether a plugin is enabled on per server and zone basis.", + }, []string{"server", "zone", "view", "name"}) + + HTTPSResponsesCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: subsystem, + Name: "https_responses_total", + Help: "Counter of DoH responses per server and http status code.", + }, []string{"server", "status"}) +) + +const ( + subsystem = "dns" + + // Dropped indicates we dropped the query before any handling. It has no closing dot, so it can not be a valid zone. + Dropped = "dropped" +) diff --git a/ag_201_coredns/plugin/minimal/README.md b/ag_201_coredns/plugin/minimal/README.md new file mode 100644 index 0000000..a225743 --- /dev/null +++ b/ag_201_coredns/plugin/minimal/README.md @@ -0,0 +1,36 @@ +# minimal + +## Name + +*minimal* - minimizes size of the DNS response message whenever possible. + +## Description + +The *minimal* plugin tries to minimize the size of the response. Depending on the response type it +removes resource records from the AUTHORITY and ADDITIONAL sections. + +Specifically this plugin looks at successful responses (this excludes negative responses, i.e. +nodata or name error). If the successful response isn't a delegation only the RRs in the answer +section are written to the client. + +## Syntax + +~~~ txt +minimal +~~~ + +## Examples + +Enable minimal responses: + +~~~ corefile +example.org { + whoami + forward . 8.8.8.8 + minimal +} +~~~ + +## See Also + +[BIND 9 Configuration Reference](https://bind9.readthedocs.io/en/latest/reference.html#boolean-options) diff --git a/ag_201_coredns/plugin/minimal/minimal.go b/ag_201_coredns/plugin/minimal/minimal.go new file mode 100644 index 0000000..0bac6a3 --- /dev/null +++ b/ag_201_coredns/plugin/minimal/minimal.go @@ -0,0 +1,55 @@ +package minimal + +import ( + "context" + "fmt" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/nonwriter" + "github.com/coredns/coredns/plugin/pkg/response" + + "github.com/miekg/dns" +) + +// minimalHandler implements the plugin.Handler interface. +type minimalHandler struct { + Next plugin.Handler +} + +func (m *minimalHandler) Name() string { return "minimal" } + +func (m *minimalHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + nw := nonwriter.New(w) + + rcode, err := plugin.NextOrFailure(m.Name(), m.Next, ctx, nw, r) + if err != nil { + return rcode, err + } + + ty, _ := response.Typify(nw.Msg, time.Now().UTC()) + cl := response.Classify(ty) + + // if response is Denial or Error pass through also if the type is Delegation pass through + if cl == response.Denial || cl == response.Error || ty == response.Delegation { + w.WriteMsg(nw.Msg) + return 0, nil + } + if ty != response.NoError { + w.WriteMsg(nw.Msg) + return 0, plugin.Error("minimal", fmt.Errorf("unhandled response type %q for %q", ty, nw.Msg.Question[0].Name)) + } + + // copy over the original Msg params, deep copy not required as RRs are not modified + d := &dns.Msg{ + MsgHdr: nw.Msg.MsgHdr, + Compress: nw.Msg.Compress, + Question: nw.Msg.Question, + Answer: nw.Msg.Answer, + Ns: nil, + Extra: nil, + } + + w.WriteMsg(d) + return 0, nil +} diff --git a/ag_201_coredns/plugin/minimal/minimal_test.go b/ag_201_coredns/plugin/minimal/minimal_test.go new file mode 100644 index 0000000..406d787 --- /dev/null +++ b/ag_201_coredns/plugin/minimal/minimal_test.go @@ -0,0 +1,153 @@ +package minimal + +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" +) + +// testHandler implements plugin.Handler and will be used to create a stub handler for the test +type testHandler struct { + Response *test.Case + Next plugin.Handler +} + +func (t *testHandler) Name() string { return "test-handler" } + +func (t *testHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + d := new(dns.Msg) + d.SetReply(r) + if t.Response != nil { + d.Answer = t.Response.Answer + d.Ns = t.Response.Ns + d.Extra = t.Response.Extra + d.Rcode = t.Response.Rcode + } + w.WriteMsg(d) + return 0, nil +} + +func TestMinimizeResponse(t *testing.T) { + baseAnswer := []dns.RR{ + test.A("example.com. 293 IN A 142.250.76.46"), + } + baseNs := []dns.RR{ + test.NS("example.com. 157127 IN NS ns2.example.com."), + test.NS("example.com. 157127 IN NS ns1.example.com."), + test.NS("example.com. 157127 IN NS ns3.example.com."), + test.NS("example.com. 157127 IN NS ns4.example.com."), + } + + baseExtra := []dns.RR{ + test.A("ns2.example.com. 316273 IN A 216.239.34.10"), + test.AAAA("ns2.example.com. 157127 IN AAAA 2001:4860:4802:34::a"), + test.A("ns3.example.com. 316274 IN A 216.239.36.10"), + test.AAAA("ns3.example.com. 157127 IN AAAA 2001:4860:4802:36::a"), + test.A("ns1.example.com. 165555 IN A 216.239.32.10"), + test.AAAA("ns1.example.com. 165555 IN AAAA 2001:4860:4802:32::a"), + test.A("ns4.example.com. 190188 IN A 216.239.38.10"), + test.AAAA("ns4.example.com. 157127 IN AAAA 2001:4860:4802:38::a"), + } + + tests := []struct { + active bool + original test.Case + minimal test.Case + }{ + { // minimization possible NoError case + original: test.Case{ + Answer: baseAnswer, + Ns: nil, + Extra: baseExtra, + Rcode: 0, + }, + minimal: test.Case{ + Answer: baseAnswer, + Ns: nil, + Extra: nil, + Rcode: 0, + }, + }, + { // delegate response case + original: test.Case{ + Answer: nil, + Ns: baseNs, + Extra: baseExtra, + Rcode: 0, + }, + minimal: test.Case{ + Answer: nil, + Ns: baseNs, + Extra: baseExtra, + Rcode: 0, + }, + }, { // negative response case + original: test.Case{ + Answer: baseAnswer, + Ns: baseNs, + Extra: baseExtra, + Rcode: 2, + }, + minimal: test.Case{ + Answer: baseAnswer, + Ns: baseNs, + Extra: baseExtra, + Rcode: 2, + }, + }, + } + + for i, tc := range tests { + req := new(dns.Msg) + req.SetQuestion("example.com", dns.TypeA) + + tHandler := &testHandler{ + Response: &tc.original, + Next: nil, + } + o := &minimalHandler{Next: tHandler} + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := o.ServeDNS(context.TODO(), rec, req) + + if err != nil { + t.Errorf("Expected no error, but got %q", err) + } + + if len(tc.minimal.Answer) != len(rec.Msg.Answer) { + t.Errorf("Test %d: Expected %d Answer, but got %d", i, len(tc.minimal.Answer), len(req.Answer)) + continue + } + if len(tc.minimal.Ns) != len(rec.Msg.Ns) { + t.Errorf("Test %d: Expected %d Ns, but got %d", i, len(tc.minimal.Ns), len(req.Ns)) + continue + } + + if len(tc.minimal.Extra) != len(rec.Msg.Extra) { + t.Errorf("Test %d: Expected %d Extras, but got %d", i, len(tc.minimal.Extra), len(req.Extra)) + continue + } + + for j, a := range rec.Msg.Answer { + if tc.minimal.Answer[j].String() != a.String() { + t.Errorf("Test %d: Expected Answer %d to be %v, but got %v", i, j, tc.minimal.Answer[j], a) + } + } + + for j, a := range rec.Msg.Ns { + if tc.minimal.Ns[j].String() != a.String() { + t.Errorf("Test %d: Expected NS %d to be %v, but got %v", i, j, tc.minimal.Ns[j], a) + } + } + + for j, a := range rec.Msg.Extra { + if tc.minimal.Extra[j].String() != a.String() { + t.Errorf("Test %d: Expected Extra %d to be %v, but got %v", i, j, tc.minimal.Extra[j], a) + } + } + } +} diff --git a/ag_201_coredns/plugin/minimal/setup.go b/ag_201_coredns/plugin/minimal/setup.go new file mode 100644 index 0000000..1bf37a6 --- /dev/null +++ b/ag_201_coredns/plugin/minimal/setup.go @@ -0,0 +1,24 @@ +package minimal + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { + plugin.Register("minimal", setup) +} + +func setup(c *caddy.Controller) error { + c.Next() + if c.NextArg() { + return plugin.Error("minimal", c.ArgErr()) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return &minimalHandler{Next: next} + }) + + return nil +} diff --git a/ag_201_coredns/plugin/minimal/setup_test.go b/ag_201_coredns/plugin/minimal/setup_test.go new file mode 100644 index 0000000..49341c4 --- /dev/null +++ b/ag_201_coredns/plugin/minimal/setup_test.go @@ -0,0 +1,19 @@ +package minimal + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", `minimal-response`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `minimal-response example.org`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } +} diff --git a/ag_201_coredns/plugin/normalize.go b/ag_201_coredns/plugin/normalize.go new file mode 100644 index 0000000..4b92bb4 --- /dev/null +++ b/ag_201_coredns/plugin/normalize.go @@ -0,0 +1,196 @@ +package plugin + +import ( + "fmt" + "net" + "runtime" + "strconv" + "strings" + + "github.com/coredns/coredns/plugin/pkg/cidr" + "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/parse" + + "github.com/miekg/dns" +) + +// See core/dnsserver/address.go - we should unify these two impls. + +// Zones represents a lists of zone names. +type Zones []string + +// Matches checks if qname is a subdomain of any of the zones in z. The match +// will return the most specific zones that matches. The empty string +// signals a not found condition. +func (z Zones) Matches(qname string) string { + zone := "" + for _, zname := range z { + if dns.IsSubDomain(zname, qname) { + // We want the *longest* matching zone, otherwise we may end up in a parent + if len(zname) > len(zone) { + zone = zname + } + } + } + return zone +} + +// Normalize fully qualifies all zones in z. The zones in Z must be domain names, without +// a port or protocol prefix. +func (z Zones) Normalize() { + for i := range z { + z[i] = Name(z[i]).Normalize() + } +} + +// Name represents a domain name. +type Name string + +// Matches checks to see if other is a subdomain (or the same domain) of n. +// This method assures that names can be easily and consistently matched. +func (n Name) Matches(child string) bool { + if dns.Name(n) == dns.Name(child) { + return true + } + return dns.IsSubDomain(string(n), child) +} + +// Normalize lowercases and makes n fully qualified. +func (n Name) Normalize() string { return strings.ToLower(dns.Fqdn(string(n))) } + +type ( + // Host represents a host from the Corefile, may contain port. + Host string +) + +// Normalize will return the host portion of host, stripping +// of any port or transport. The host will also be fully qualified and lowercased. +// An empty string is returned on failure +// Deprecated: use OriginsFromArgsOrServerBlock or NormalizeExact +func (h Host) Normalize() string { + var caller string + if _, file, line, ok := runtime.Caller(1); ok { + caller = fmt.Sprintf("(%v line %d) ", file, line) + } + log.Warning("An external plugin " + caller + "is using the deprecated function Normalize. " + + "This will be removed in a future versions of CoreDNS. The plugin should be updated to use " + + "OriginsFromArgsOrServerBlock or NormalizeExact instead.") + + s := string(h) + _, s = parse.Transport(s) + + // The error can be ignored here, because this function is called after the corefile has already been vetted. + hosts, _, err := SplitHostPort(s) + if err != nil { + return "" + } + return Name(hosts[0]).Normalize() +} + +// MustNormalize will return the host portion of host, stripping +// of any port or transport. The host will also be fully qualified and lowercased. +// An error is returned on error +// Deprecated: use OriginsFromArgsOrServerBlock or NormalizeExact +func (h Host) MustNormalize() (string, error) { + var caller string + if _, file, line, ok := runtime.Caller(1); ok { + caller = fmt.Sprintf("(%v line %d) ", file, line) + } + log.Warning("An external plugin " + caller + "is using the deprecated function MustNormalize. " + + "This will be removed in a future versions of CoreDNS. The plugin should be updated to use " + + "OriginsFromArgsOrServerBlock or NormalizeExact instead.") + + s := string(h) + _, s = parse.Transport(s) + + // The error can be ignored here, because this function is called after the corefile has already been vetted. + hosts, _, err := SplitHostPort(s) + if err != nil { + return "", err + } + return Name(hosts[0]).Normalize(), nil +} + +// NormalizeExact will return the host portion of host, stripping +// of any port or transport. The host will also be fully qualified and lowercased. +// An empty slice is returned on failure +func (h Host) NormalizeExact() []string { + // The error can be ignored here, because this function should only be called after the corefile has already been vetted. + s := string(h) + _, s = parse.Transport(s) + + hosts, _, err := SplitHostPort(s) + if err != nil { + return nil + } + for i := range hosts { + hosts[i] = Name(hosts[i]).Normalize() + } + return hosts +} + +// SplitHostPort splits s up in a host(s) and port portion, taking reverse address notation into account. +// String the string s should *not* be prefixed with any protocols, i.e. dns://. SplitHostPort can return +// multiple hosts when a reverse notation on a non-octet boundary is given. +func SplitHostPort(s string) (hosts []string, port string, err error) { + // If there is: :[0-9]+ on the end we assume this is the port. This works for (ascii) domain + // names and our reverse syntax, which always needs a /mask *before* the port. + // So from the back, find first colon, and then check if it's a number. + colon := strings.LastIndex(s, ":") + if colon == len(s)-1 { + return nil, "", fmt.Errorf("expecting data after last colon: %q", s) + } + if colon != -1 { + if p, err := strconv.Atoi(s[colon+1:]); err == nil { + port = strconv.Itoa(p) + s = s[:colon] + } + } + + // TODO(miek): this should take escaping into account. + if len(s) > 255 { + return nil, "", fmt.Errorf("specified zone is too long: %d > 255", len(s)) + } + + if _, ok := dns.IsDomainName(s); !ok { + return nil, "", fmt.Errorf("zone is not a valid domain name: %s", s) + } + + // Check if it parses as a reverse zone, if so we use that. Must be fully specified IP and mask. + _, n, err := net.ParseCIDR(s) + if err != nil { + return []string{s}, port, nil + } + + if s[0] == ':' || (s[0] == '0' && strings.Contains(s, ":")) { + return nil, "", fmt.Errorf("invalid CIDR %s", s) + } + + // now check if multiple hosts must be returned. + nets := cidr.Split(n) + hosts = cidr.Reverse(nets) + return hosts, port, nil +} + +// OriginsFromArgsOrServerBlock returns the normalized args if that slice +// is not empty, otherwise the serverblock slice is returned (in a newly copied slice). +func OriginsFromArgsOrServerBlock(args, serverblock []string) []string { + if len(args) == 0 { + s := make([]string, len(serverblock)) + copy(s, serverblock) + for i := range s { + s[i] = Host(s[i]).NormalizeExact()[0] // expansion of these already happened in dnsserver/register.go + } + return s + } + s := []string{} + for i := range args { + sx := Host(args[i]).NormalizeExact() + if len(sx) == 0 { + continue // silently ignores errors. + } + s = append(s, sx...) + } + + return s +} diff --git a/ag_201_coredns/plugin/normalize_test.go b/ag_201_coredns/plugin/normalize_test.go new file mode 100644 index 0000000..cc32eae --- /dev/null +++ b/ag_201_coredns/plugin/normalize_test.go @@ -0,0 +1,140 @@ +package plugin + +import ( + "sort" + "testing" +) + +func TestZoneMatches(t *testing.T) { + child := "example.org." + zones := Zones([]string{"org.", "."}) + actual := zones.Matches(child) + if actual != "org." { + t.Errorf("Expected %v, got %v", "org.", actual) + } + + child = "bla.example.org." + zones = Zones([]string{"bla.example.org.", "org.", "."}) + actual = zones.Matches(child) + + if actual != "bla.example.org." { + t.Errorf("Expected %v, got %v", "org.", actual) + } +} + +func TestZoneNormalize(t *testing.T) { + zones := Zones([]string{"example.org", "Example.ORG.", "example.org."}) + expected := "example.org." + zones.Normalize() + + for _, actual := range zones { + if actual != expected { + t.Errorf("Expected %v, got %v", expected, actual) + } + } +} + +func TestNameMatches(t *testing.T) { + matches := []struct { + child string + parent string + expected bool + }{ + {".", ".", true}, + {"example.org.", ".", true}, + {"example.org.", "example.org.", true}, + {"example.org.", "org.", true}, + {"org.", "example.org.", false}, + } + + for _, m := range matches { + actual := Name(m.parent).Matches(m.child) + if actual != m.expected { + t.Errorf("Expected %v for %s/%s, got %v", m.expected, m.parent, m.child, actual) + } + } +} + +func TestNameNormalize(t *testing.T) { + names := []string{ + "example.org", "example.org.", + "Example.ORG.", "example.org."} + + for i := 0; i < len(names); i += 2 { + ts := names[i] + expected := names[i+1] + actual := Name(ts).Normalize() + if expected != actual { + t.Errorf("Expected %v, got %v", expected, actual) + } + } +} + +func TestHostNormalizeExact(t *testing.T) { + tests := []struct { + in string + out []string + }{ + {".:53", []string{"."}}, + {"example.org:53", []string{"example.org."}}, + {"example.org.:53", []string{"example.org."}}, + {"10.0.0.0/8:53", []string{"10.in-addr.arpa."}}, + {"10.0.0.0/15", []string{"0.10.in-addr.arpa.", "1.10.in-addr.arpa."}}, + {"10.9.3.0/18", []string{"0.9.10.in-addr.arpa.", "1.9.10.in-addr.arpa.", "2.9.10.in-addr.arpa."}}, + {"2001:db8::/29", []string{ + "8.b.d.0.1.0.0.2.ip6.arpa.", + "9.b.d.0.1.0.0.2.ip6.arpa.", + "a.b.d.0.1.0.0.2.ip6.arpa.", + "b.b.d.0.1.0.0.2.ip6.arpa.", + "c.b.d.0.1.0.0.2.ip6.arpa.", + "d.b.d.0.1.0.0.2.ip6.arpa.", + "e.b.d.0.1.0.0.2.ip6.arpa.", + "f.b.d.0.1.0.0.2.ip6.arpa.", + }}, + {"2001:db8::/30", []string{ + "8.b.d.0.1.0.0.2.ip6.arpa.", + "9.b.d.0.1.0.0.2.ip6.arpa.", + "a.b.d.0.1.0.0.2.ip6.arpa.", + "b.b.d.0.1.0.0.2.ip6.arpa.", + }}, + {"2001:db8::/115", []string{ + "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + }}, + {"2001:db8::/114", []string{ + "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "3.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + }}, + {"2001:db8::/113", []string{ + "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "3.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "6.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "7.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + }}, + {"2001:db8::/112", []string{ + "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + }}, + {"2001:db8::/108", []string{ + "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + }}, + {"::fFFF:B:F/115", nil}, + {"dns://example.org", []string{"example.org."}}, + } + + for i := range tests { + actual := Host(tests[i].in).NormalizeExact() + expected := tests[i].out + sort.Strings(expected) + for j := range expected { + if expected[j] != actual[j] { + t.Errorf("Test %d, expected %v, got %v", i, expected[j], actual[j]) + } + } + } +} diff --git a/ag_201_coredns/plugin/nsid/README.md b/ag_201_coredns/plugin/nsid/README.md new file mode 100644 index 0000000..7bb15ca --- /dev/null +++ b/ag_201_coredns/plugin/nsid/README.md @@ -0,0 +1,57 @@ +# nsid + +## Name + +*nsid* - adds an identifier of this server to each reply. + +## Description + +This plugin implements [RFC 5001](https://tools.ietf.org/html/rfc5001) and adds an EDNS0 OPT +resource record to replies that uniquely identify the server. This is useful in anycast setups to +see which server was responsible for generating the reply and for debugging. + +This plugin can only be used once per Server Block. + + +## Syntax + +~~~ txt +nsid [DATA] +~~~ + +**DATA** is the string to use in the nsid record. + +If **DATA** is not given, the host's name is used. + +## Examples + +Enable nsid: + +~~~ corefile +example.org { + whoami + nsid Use The Force +} +~~~ + +And now a client with NSID support will see an OPT record with the NSID option: + +~~~ sh +% dig +nsid @localhost a whoami.example.org + +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46880 +;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 3 + +.... + +; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 4096 +; NSID: 55 73 65 20 54 68 65 20 46 6f 72 63 65 ("Use The Force") +;; QUESTION SECTION: +;whoami.example.org. IN A +~~~ + +## See Also + +[RFC 5001](https://tools.ietf.org/html/rfc5001) diff --git a/ag_201_coredns/plugin/nsid/log_test.go b/ag_201_coredns/plugin/nsid/log_test.go new file mode 100644 index 0000000..1aea379 --- /dev/null +++ b/ag_201_coredns/plugin/nsid/log_test.go @@ -0,0 +1,5 @@ +package nsid + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/nsid/nsid.go b/ag_201_coredns/plugin/nsid/nsid.go new file mode 100644 index 0000000..e2506b4 --- /dev/null +++ b/ag_201_coredns/plugin/nsid/nsid.go @@ -0,0 +1,69 @@ +// Package nsid implements NSID protocol +package nsid + +import ( + "context" + "encoding/hex" + + "github.com/coredns/coredns/plugin" + + "github.com/miekg/dns" +) + +// Nsid plugin +type Nsid struct { + Next plugin.Handler + Data string +} + +// ResponseWriter is a response writer that adds NSID response +type ResponseWriter struct { + dns.ResponseWriter + Data string + request *dns.Msg +} + +// ServeDNS implements the plugin.Handler interface. +func (n Nsid) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if option := r.IsEdns0(); option != nil { + for _, o := range option.Option { + if _, ok := o.(*dns.EDNS0_NSID); ok { + nw := &ResponseWriter{ResponseWriter: w, Data: n.Data, request: r} + return plugin.NextOrFailure(n.Name(), n.Next, ctx, nw, r) + } + } + } + return plugin.NextOrFailure(n.Name(), n.Next, ctx, w, r) +} + +// WriteMsg implements the dns.ResponseWriter interface. +func (w *ResponseWriter) WriteMsg(res *dns.Msg) error { + if w.request.IsEdns0() != nil && res.IsEdns0() == nil { + res.SetEdns0(w.request.IsEdns0().UDPSize(), true) + } + + if option := res.IsEdns0(); option != nil { + var exists bool + + for _, o := range option.Option { + if e, ok := o.(*dns.EDNS0_NSID); ok { + e.Code = dns.EDNS0NSID + e.Nsid = hex.EncodeToString([]byte(w.Data)) + exists = true + } + } + + // Append the NSID if it doesn't exist in EDNS0 options + if !exists { + option.Option = append(option.Option, &dns.EDNS0_NSID{ + Code: dns.EDNS0NSID, + Nsid: hex.EncodeToString([]byte(w.Data)), + }) + } + } + + return w.ResponseWriter.WriteMsg(res) +} + +// Name implements the Handler interface. +func (n Nsid) Name() string { return "nsid" } diff --git a/ag_201_coredns/plugin/nsid/nsid_test.go b/ag_201_coredns/plugin/nsid/nsid_test.go new file mode 100644 index 0000000..eda4530 --- /dev/null +++ b/ag_201_coredns/plugin/nsid/nsid_test.go @@ -0,0 +1,136 @@ +package nsid + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/cache" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/plugin/whoami" + + "github.com/miekg/dns" +) + +func TestNsid(t *testing.T) { + em := Nsid{ + Data: "NSID", + } + + tests := []struct { + next plugin.Handler + qname string + qtype uint16 + expectedCode int + expectedReply string + expectedErr error + }{ + { + next: whoami.Whoami{}, + qname: ".", + expectedCode: dns.RcodeSuccess, + expectedReply: hex.EncodeToString([]byte("NSID")), + expectedErr: nil, + }, + } + + ctx := context.TODO() + + for i, tc := range tests { + req := new(dns.Msg) + if tc.qtype == 0 { + tc.qtype = dns.TypeA + } + req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype) + req.Question[0].Qclass = dns.ClassINET + + req.SetEdns0(4096, false) + option := req.Extra[0].(*dns.OPT) + option.Option = append(option.Option, &dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""}) + em.Next = tc.next + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + code, err := em.ServeDNS(ctx, rec, req) + + if err != tc.expectedErr { + t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expectedErr, err) + } + if code != tc.expectedCode { + t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code) + } + if tc.expectedReply != "" { + for _, extra := range rec.Msg.Extra { + if option, ok := extra.(*dns.OPT); ok { + e := option.Option[0].(*dns.EDNS0_NSID) + if e.Nsid != tc.expectedReply { + t.Errorf("Test %d: Expected answer %s, but got %s", i, tc.expectedReply, e.Nsid) + } + } + } + } + } +} + +func TestNsidCache(t *testing.T) { + em := Nsid{ + Data: "NSID", + } + c := cache.New() + + tests := []struct { + next plugin.Handler + qname string + qtype uint16 + expectedCode int + expectedReply string + expectedErr error + }{ + { + next: whoami.Whoami{}, + qname: ".", + expectedCode: dns.RcodeSuccess, + expectedReply: hex.EncodeToString([]byte("NSID")), + expectedErr: nil, + }, + } + + ctx := context.TODO() + + for i, tc := range tests { + req := new(dns.Msg) + if tc.qtype == 0 { + tc.qtype = dns.TypeA + } + req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype) + req.Question[0].Qclass = dns.ClassINET + + req.SetEdns0(4096, false) + option := req.Extra[0].(*dns.OPT) + option.Option = append(option.Option, &dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""}) + em.Next = tc.next + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + c.Next = em + code, err := c.ServeDNS(ctx, rec, req) + + if err != tc.expectedErr { + t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expectedErr, err) + } + if code != int(tc.expectedCode) { + t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code) + } + if tc.expectedReply != "" { + for _, extra := range rec.Msg.Extra { + if option, ok := extra.(*dns.OPT); ok { + e := option.Option[0].(*dns.EDNS0_NSID) + if e.Nsid != tc.expectedReply { + t.Errorf("Test %d: Expected answer %s, but got %s", i, tc.expectedReply, e.Nsid) + } + } + } + } + } +} diff --git a/ag_201_coredns/plugin/nsid/setup.go b/ag_201_coredns/plugin/nsid/setup.go new file mode 100644 index 0000000..07c6493 --- /dev/null +++ b/ag_201_coredns/plugin/nsid/setup.go @@ -0,0 +1,45 @@ +package nsid + +import ( + "os" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("nsid", setup) } + +func setup(c *caddy.Controller) error { + nsid, err := nsidParse(c) + if err != nil { + return plugin.Error("nsid", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return Nsid{Next: next, Data: nsid} + }) + + return nil +} + +func nsidParse(c *caddy.Controller) (string, error) { + // Use hostname as the default + nsid, err := os.Hostname() + if err != nil { + nsid = "localhost" + } + i := 0 + for c.Next() { + if i > 0 { + return nsid, plugin.ErrOnce + } + i++ + args := c.RemainingArgs() + if len(args) > 0 { + nsid = strings.Join(args, " ") + } + } + return nsid, nil +} diff --git a/ag_201_coredns/plugin/nsid/setup_test.go b/ag_201_coredns/plugin/nsid/setup_test.go new file mode 100644 index 0000000..15d4042 --- /dev/null +++ b/ag_201_coredns/plugin/nsid/setup_test.go @@ -0,0 +1,68 @@ +package nsid + +import ( + "os" + "strings" + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupNsid(t *testing.T) { + defaultNsid, err := os.Hostname() + if err != nil { + defaultNsid = "localhost" + } + tests := []struct { + input string + shouldErr bool + expectedData string + expectedErrContent string // substring from the expected error. Empty for positive cases. + }{ + {`nsid`, false, defaultNsid, ""}, + {`nsid "ps0"`, false, "ps0", ""}, + {`nsid "worker1"`, false, "worker1", ""}, + {`nsid "tf 2"`, false, "tf 2", ""}, + {`nsid + nsid`, true, "", "plugin"}, + } + 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 %s for input %s", i, err, 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) + } + } + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + nsid, err := nsidParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, 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) + } + } + + if !test.shouldErr && nsid != test.expectedData { + t.Errorf("Nsid not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedData, nsid) + } + } +} diff --git a/ag_201_coredns/plugin/pkg/cache/cache.go b/ag_201_coredns/plugin/pkg/cache/cache.go new file mode 100644 index 0000000..6c4105e --- /dev/null +++ b/ag_201_coredns/plugin/pkg/cache/cache.go @@ -0,0 +1,157 @@ +// Package cache implements a cache. The cache hold 256 shards, each shard +// holds a cache: a map with a mutex. There is no fancy expunge algorithm, it +// just randomly evicts elements when it gets full. +package cache + +import ( + "hash/fnv" + "sync" +) + +// Hash returns the FNV hash of what. +func Hash(what []byte) uint64 { + h := fnv.New64() + h.Write(what) + return h.Sum64() +} + +// Cache is cache. +type Cache struct { + shards [shardSize]*shard +} + +// shard is a cache with random eviction. +type shard struct { + items map[uint64]interface{} + size int + + sync.RWMutex +} + +// New returns a new cache. +func New(size int) *Cache { + ssize := size / shardSize + if ssize < 4 { + ssize = 4 + } + + c := &Cache{} + + // Initialize all the shards + for i := 0; i < shardSize; i++ { + c.shards[i] = newShard(ssize) + } + return c +} + +// Add adds a new element to the cache. If the element already exists it is overwritten. +// Returns true if an existing element was evicted to make room for this element. +func (c *Cache) Add(key uint64, el interface{}) bool { + shard := key & (shardSize - 1) + return c.shards[shard].Add(key, el) +} + +// Get looks up element index under key. +func (c *Cache) Get(key uint64) (interface{}, bool) { + shard := key & (shardSize - 1) + return c.shards[shard].Get(key) +} + +// Remove removes the element indexed with key. +func (c *Cache) Remove(key uint64) { + shard := key & (shardSize - 1) + c.shards[shard].Remove(key) +} + +// Len returns the number of elements in the cache. +func (c *Cache) Len() int { + l := 0 + for _, s := range &c.shards { + l += s.Len() + } + return l +} + +// Walk walks each shard in the cache. +func (c *Cache) Walk(f func(map[uint64]interface{}, uint64) bool) { + for _, s := range &c.shards { + s.Walk(f) + } +} + +// newShard returns a new shard with size. +func newShard(size int) *shard { return &shard{items: make(map[uint64]interface{}), size: size} } + +// Add adds element indexed by key into the cache. Any existing element is overwritten +// Returns true if an existing element was evicted to make room for this element. +func (s *shard) Add(key uint64, el interface{}) bool { + eviction := false + s.Lock() + if len(s.items) >= s.size { + if _, ok := s.items[key]; !ok { + for k := range s.items { + delete(s.items, k) + eviction = true + break + } + } + } + s.items[key] = el + s.Unlock() + return eviction +} + +// Remove removes the element indexed by key from the cache. +func (s *shard) Remove(key uint64) { + s.Lock() + delete(s.items, key) + s.Unlock() +} + +// Evict removes a random element from the cache. +func (s *shard) Evict() { + s.Lock() + for k := range s.items { + delete(s.items, k) + break + } + s.Unlock() +} + +// Get looks up the element indexed under key. +func (s *shard) Get(key uint64) (interface{}, bool) { + s.RLock() + el, found := s.items[key] + s.RUnlock() + return el, found +} + +// Len returns the current length of the cache. +func (s *shard) Len() int { + s.RLock() + l := len(s.items) + s.RUnlock() + return l +} + +// Walk walks the shard for each element the function f is executed while holding a write lock. +func (s *shard) Walk(f func(map[uint64]interface{}, uint64) bool) { + s.RLock() + items := make([]uint64, len(s.items)) + i := 0 + for k := range s.items { + items[i] = k + i++ + } + s.RUnlock() + for _, k := range items { + s.Lock() + ok := f(s.items, k) + s.Unlock() + if !ok { + return + } + } +} + +const shardSize = 256 diff --git a/ag_201_coredns/plugin/pkg/cache/cache_test.go b/ag_201_coredns/plugin/pkg/cache/cache_test.go new file mode 100644 index 0000000..e9e0a30 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/cache/cache_test.go @@ -0,0 +1,85 @@ +package cache + +import ( + "testing" +) + +func TestCacheAddAndGet(t *testing.T) { + const N = shardSize * 4 + c := New(N) + c.Add(1, 1) + + if _, found := c.Get(1); !found { + t.Fatal("Failed to find inserted record") + } + + for i := 0; i < N; i++ { + c.Add(uint64(i), 1) + } + for i := 0; i < N; i++ { + c.Add(uint64(i), 1) + if c.Len() != N { + t.Fatal("A item was unnecessarily evicted from the cache") + } + } +} + +func TestCacheLen(t *testing.T) { + c := New(4) + + c.Add(1, 1) + if l := c.Len(); l != 1 { + t.Fatalf("Cache size should %d, got %d", 1, l) + } + + c.Add(1, 1) + if l := c.Len(); l != 1 { + t.Fatalf("Cache size should %d, got %d", 1, l) + } + + c.Add(2, 2) + if l := c.Len(); l != 2 { + t.Fatalf("Cache size should %d, got %d", 2, l) + } +} + +func TestCacheSharding(t *testing.T) { + c := New(shardSize) + for i := 0; i < shardSize*2; i++ { + c.Add(uint64(i), 1) + } + for i, s := range c.shards { + if s.Len() == 0 { + t.Errorf("Failed to populate shard: %d", i) + } + } +} + +func TestCacheWalk(t *testing.T) { + c := New(10) + exp := make([]int, 10*2) + for i := 0; i < 10*2; i++ { + c.Add(uint64(i), 1) + exp[i] = 1 + } + got := make([]int, 10*2) + c.Walk(func(items map[uint64]interface{}, key uint64) bool { + got[key] = items[key].(int) + return true + }) + for i := range exp { + if exp[i] != got[i] { + t.Errorf("Expected %d, got %d", exp[i], got[i]) + } + } +} + +func BenchmarkCache(b *testing.B) { + b.ReportAllocs() + + c := New(4) + for n := 0; n < b.N; n++ { + c.Add(1, 1) + c.Get(1) + } +} diff --git a/ag_201_coredns/plugin/pkg/cache/shard_test.go b/ag_201_coredns/plugin/pkg/cache/shard_test.go new file mode 100644 index 0000000..a383130 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/cache/shard_test.go @@ -0,0 +1,139 @@ +package cache + +import ( + "sync" + "testing" +) + +func TestShardAddAndGet(t *testing.T) { + s := newShard(1) + s.Add(1, 1) + + if _, found := s.Get(1); !found { + t.Fatal("Failed to find inserted record") + } + + s.Add(2, 1) + if _, found := s.Get(1); found { + t.Fatal("Failed to evict record") + } + if _, found := s.Get(2); !found { + t.Fatal("Failed to find inserted record") + } +} + +func TestAddEvict(t *testing.T) { + const size = 1024 + s := newShard(size) + + for i := uint64(0); i < size; i++ { + s.Add(i, 1) + } + for i := uint64(0); i < size; i++ { + s.Add(i, 1) + if s.Len() != size { + t.Fatal("A item was unnecessarily evicted from the cache") + } + } +} + +func TestShardLen(t *testing.T) { + s := newShard(4) + + s.Add(1, 1) + if l := s.Len(); l != 1 { + t.Fatalf("Shard size should %d, got %d", 1, l) + } + + s.Add(1, 1) + if l := s.Len(); l != 1 { + t.Fatalf("Shard size should %d, got %d", 1, l) + } + + s.Add(2, 2) + if l := s.Len(); l != 2 { + t.Fatalf("Shard size should %d, got %d", 2, l) + } +} + +func TestShardEvict(t *testing.T) { + s := newShard(1) + s.Add(1, 1) + s.Add(2, 2) + // 1 should be gone + + if _, found := s.Get(1); found { + t.Fatal("Found item that should have been evicted") + } +} + +func TestShardLenEvict(t *testing.T) { + s := newShard(4) + s.Add(1, 1) + s.Add(2, 1) + s.Add(3, 1) + s.Add(4, 1) + + if l := s.Len(); l != 4 { + t.Fatalf("Shard size should %d, got %d", 4, l) + } + + // This should evict one element + s.Add(5, 1) + if l := s.Len(); l != 4 { + t.Fatalf("Shard size should %d, got %d", 4, l) + } + + // Make sure we don't accidentally evict an element when + // we the key is already stored. + for i := 0; i < 4; i++ { + s.Add(5, 1) + if l := s.Len(); l != 4 { + t.Fatalf("Shard size should %d, got %d", 4, l) + } + } +} + +func TestShardEvictParallel(t *testing.T) { + s := newShard(shardSize) + for i := uint64(0); i < shardSize; i++ { + s.Add(i, struct{}{}) + } + start := make(chan struct{}) + var wg sync.WaitGroup + for i := 0; i < shardSize; i++ { + wg.Add(1) + go func() { + <-start + s.Evict() + wg.Done() + }() + } + close(start) // start evicting in parallel + wg.Wait() + if s.Len() != 0 { + t.Fatalf("Failed to evict all keys in parallel: %d", s.Len()) + } +} + +func BenchmarkShard(b *testing.B) { + s := newShard(shardSize) + b.ResetTimer() + for i := 0; i < b.N; i++ { + k := uint64(i) % shardSize * 2 + s.Add(k, 1) + s.Get(k) + } +} + +func BenchmarkShardParallel(b *testing.B) { + s := newShard(shardSize) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for i := uint64(0); pb.Next(); i++ { + k := i % shardSize * 2 + s.Add(k, 1) + s.Get(k) + } + }) +} diff --git a/ag_201_coredns/plugin/pkg/cidr/cidr.go b/ag_201_coredns/plugin/pkg/cidr/cidr.go new file mode 100644 index 0000000..91aead9 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/cidr/cidr.go @@ -0,0 +1,83 @@ +// Package cidr contains functions that deal with classless reverse zones in the DNS. +package cidr + +import ( + "math" + "net" + "strings" + + "github.com/apparentlymart/go-cidr/cidr" + "github.com/miekg/dns" +) + +// Split returns a slice of non-overlapping subnets that in union equal the subnet n, +// and where each subnet falls on a reverse name segment boundary. +// for ipv4 this is any multiple of 8 bits (/8, /16, /24 or /32) +// for ipv6 this is any multiple of 4 bits +func Split(n *net.IPNet) []string { + boundary := 8 + nstr := n.String() + if strings.Contains(nstr, ":") { + boundary = 4 + } + ones, _ := n.Mask.Size() + if ones%boundary == 0 { + return []string{n.String()} + } + + mask := int(math.Ceil(float64(ones)/float64(boundary))) * boundary + networks := nets(n, mask) + cidrs := make([]string, len(networks)) + for i := range networks { + cidrs[i] = networks[i].String() + } + return cidrs +} + +// nets return a slice of prefixes with the desired mask subnetted from original network. +func nets(network *net.IPNet, newPrefixLen int) []*net.IPNet { + prefixLen, _ := network.Mask.Size() + maxSubnets := int(math.Exp2(float64(newPrefixLen)) / math.Exp2(float64(prefixLen))) + nets := []*net.IPNet{{IP: network.IP, Mask: net.CIDRMask(newPrefixLen, 8*len(network.IP))}} + + for i := 1; i < maxSubnets; i++ { + next, exceeds := cidr.NextSubnet(nets[len(nets)-1], newPrefixLen) + nets = append(nets, next) + if exceeds { + break + } + } + + return nets +} + +// Reverse return the reverse zones that are authoritative for each net in ns. +func Reverse(nets []string) []string { + rev := make([]string, len(nets)) + for i := range nets { + ip, n, _ := net.ParseCIDR(nets[i]) + r, err1 := dns.ReverseAddr(ip.String()) + if err1 != nil { + continue + } + ones, bits := n.Mask.Size() + // get the size, in bits, of each portion of hostname defined in the reverse address. (8 for IPv4, 4 for IPv6) + sizeDigit := 8 + if len(n.IP) == net.IPv6len { + sizeDigit = 4 + } + // Get the first lower octet boundary to see what encompassing zone we should be authoritative for. + mod := (bits - ones) % sizeDigit + nearest := (bits - ones) + mod + offset := 0 + var end bool + for i := 0; i < nearest/sizeDigit; i++ { + offset, end = dns.NextLabel(r, offset) + if end { + break + } + } + rev[i] = r[offset:] + } + return rev +} diff --git a/ag_201_coredns/plugin/pkg/cidr/cidr_test.go b/ag_201_coredns/plugin/pkg/cidr/cidr_test.go new file mode 100644 index 0000000..055a82e --- /dev/null +++ b/ag_201_coredns/plugin/pkg/cidr/cidr_test.go @@ -0,0 +1,47 @@ +package cidr + +import ( + "net" + "testing" +) + +var tests = []struct { + in string + expected []string + zones []string +}{ + {"10.0.0.0/15", []string{"10.0.0.0/16", "10.1.0.0/16"}, []string{"0.10.in-addr.arpa.", "1.10.in-addr.arpa."}}, + {"10.0.0.0/16", []string{"10.0.0.0/16"}, []string{"0.10.in-addr.arpa."}}, + {"192.168.1.1/23", []string{"192.168.0.0/24", "192.168.1.0/24"}, []string{"0.168.192.in-addr.arpa.", "1.168.192.in-addr.arpa."}}, + {"10.129.60.0/22", []string{"10.129.60.0/24", "10.129.61.0/24", "10.129.62.0/24", "10.129.63.0/24"}, []string{"60.129.10.in-addr.arpa.", "61.129.10.in-addr.arpa.", "62.129.10.in-addr.arpa.", "63.129.10.in-addr.arpa."}}, + {"2001:db8::/31", []string{"2001:db8::/32", "2001:db9::/32"}, []string{"8.b.d.0.1.0.0.2.ip6.arpa.", "9.b.d.0.1.0.0.2.ip6.arpa."}}, +} + +func TestSplit(t *testing.T) { + for i, tc := range tests { + _, n, _ := net.ParseCIDR(tc.in) + nets := Split(n) + if len(nets) != len(tc.expected) { + t.Errorf("Test %d, expected %d subnets, got %d", i, len(tc.expected), len(nets)) + continue + } + for j := range nets { + if nets[j] != tc.expected[j] { + t.Errorf("Test %d, expected %s, got %s", i, tc.expected[j], nets[j]) + } + } + } +} + +func TestReverse(t *testing.T) { + for i, tc := range tests { + _, n, _ := net.ParseCIDR(tc.in) + nets := Split(n) + reverse := Reverse(nets) + for j := range reverse { + if reverse[j] != tc.zones[j] { + t.Errorf("Test %d, expected %s, got %s", i, tc.zones[j], reverse[j]) + } + } + } +} diff --git a/ag_201_coredns/plugin/pkg/dnstest/multirecorder.go b/ag_201_coredns/plugin/pkg/dnstest/multirecorder.go new file mode 100644 index 0000000..fe8ee03 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnstest/multirecorder.go @@ -0,0 +1,41 @@ +package dnstest + +import ( + "time" + + "github.com/miekg/dns" +) + +// MultiRecorder is a type of ResponseWriter that captures all messages written to it. +type MultiRecorder struct { + Len int + Msgs []*dns.Msg + Start time.Time + dns.ResponseWriter +} + +// NewMultiRecorder makes and returns a new MultiRecorder. +func NewMultiRecorder(w dns.ResponseWriter) *MultiRecorder { + return &MultiRecorder{ + ResponseWriter: w, + Msgs: make([]*dns.Msg, 0), + Start: time.Now(), + } +} + +// WriteMsg records the message and its length written to it and call the +// underlying ResponseWriter's WriteMsg method. +func (r *MultiRecorder) WriteMsg(res *dns.Msg) error { + r.Len += res.Len() + r.Msgs = append(r.Msgs, res) + return r.ResponseWriter.WriteMsg(res) +} + +// Write is a wrapper that records the length of the messages that get written to it. +func (r *MultiRecorder) Write(buf []byte) (int, error) { + n, err := r.ResponseWriter.Write(buf) + if err == nil { + r.Len += n + } + return n, err +} diff --git a/ag_201_coredns/plugin/pkg/dnstest/multirecorder_test.go b/ag_201_coredns/plugin/pkg/dnstest/multirecorder_test.go new file mode 100644 index 0000000..1299db5 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnstest/multirecorder_test.go @@ -0,0 +1,38 @@ +package dnstest + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestMultiWriteMsg(t *testing.T) { + w := &responseWriter{} + record := NewMultiRecorder(w) + + responseTestName := "testmsg.example.org." + responseTestMsg := new(dns.Msg) + responseTestMsg.SetQuestion(responseTestName, dns.TypeA) + + record.WriteMsg(responseTestMsg) + record.WriteMsg(responseTestMsg) + + if len(record.Msgs) != 2 { + t.Fatalf("Expected 2 messages to be written, but instead found %d\n", len(record.Msgs)) + } + if record.Len != responseTestMsg.Len()*2 { + t.Fatalf("Expected the bytes written counter to be %d, but instead found %d\n", responseTestMsg.Len()*2, record.Len) + } +} + +func TestMultiWrite(t *testing.T) { + w := &responseWriter{} + record := NewRecorder(w) + responseTest := []byte("testmsg.example.org.") + + record.Write(responseTest) + record.Write(responseTest) + if record.Len != len(responseTest)*2 { + t.Fatalf("Expected the bytes written counter to be %d, but instead found %d\n", len(responseTest)*2, record.Len) + } +} diff --git a/ag_201_coredns/plugin/pkg/dnstest/recorder.go b/ag_201_coredns/plugin/pkg/dnstest/recorder.go new file mode 100644 index 0000000..1da063e --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnstest/recorder.go @@ -0,0 +1,54 @@ +// Package dnstest allows for easy testing of DNS client against a test server. +package dnstest + +import ( + "time" + + "github.com/miekg/dns" +) + +// Recorder is a type of ResponseWriter that captures +// the rcode code written to it and also the size of the message +// written in the response. A rcode code does not have +// to be written, however, in which case 0 must be assumed. +// It is best to have the constructor initialize this type +// with that default status code. +type Recorder struct { + dns.ResponseWriter + Rcode int + Len int + Msg *dns.Msg + Start time.Time +} + +// NewRecorder makes and returns a new Recorder, +// which captures the DNS rcode from the ResponseWriter +// and also the length of the response message written through it. +func NewRecorder(w dns.ResponseWriter) *Recorder { + return &Recorder{ + ResponseWriter: w, + Rcode: 0, + Msg: nil, + Start: time.Now(), + } +} + +// WriteMsg records the status code and calls the +// underlying ResponseWriter's WriteMsg method. +func (r *Recorder) WriteMsg(res *dns.Msg) error { + r.Rcode = res.Rcode + // We may get called multiple times (axfr for instance). + // Save the last message, but add the sizes. + r.Len += res.Len() + r.Msg = res + return r.ResponseWriter.WriteMsg(res) +} + +// Write is a wrapper that records the length of the message that gets written. +func (r *Recorder) Write(buf []byte) (int, error) { + n, err := r.ResponseWriter.Write(buf) + if err == nil { + r.Len += n + } + return n, err +} diff --git a/ag_201_coredns/plugin/pkg/dnstest/recorder_test.go b/ag_201_coredns/plugin/pkg/dnstest/recorder_test.go new file mode 100644 index 0000000..96af7b0 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnstest/recorder_test.go @@ -0,0 +1,50 @@ +package dnstest + +import ( + "testing" + + "github.com/miekg/dns" +) + +type responseWriter struct{ dns.ResponseWriter } + +func (r *responseWriter) WriteMsg(m *dns.Msg) error { return nil } +func (r *responseWriter) Write(buf []byte) (int, error) { return len(buf), nil } + +func TestNewRecorder(t *testing.T) { + w := &responseWriter{} + record := NewRecorder(w) + if record.ResponseWriter != w { + t.Fatalf("Expected Response writer in the Recording to be same as the one sent\n") + } + if record.Rcode != dns.RcodeSuccess { + t.Fatalf("Expected recorded status to be dns.RcodeSuccess (%d) , but found %d\n ", dns.RcodeSuccess, record.Rcode) + } +} + +func TestWriteMsg(t *testing.T) { + w := &responseWriter{} + record := NewRecorder(w) + responseTestName := "testmsg.example.org." + responseTestMsg := new(dns.Msg) + responseTestMsg.SetQuestion(responseTestName, dns.TypeA) + + record.WriteMsg(responseTestMsg) + if record.Len != responseTestMsg.Len() { + t.Fatalf("Expected the bytes written counter to be %d, but instead found %d\n", responseTestMsg.Len(), record.Len) + } + if x := record.Msg.Question[0].Name; x != responseTestName { + t.Fatalf("Expected Msg Qname to be %s , but found %s\n", responseTestName, x) + } +} + +func TestWrite(t *testing.T) { + w := &responseWriter{} + record := NewRecorder(w) + responseTest := []byte("testmsg.example.org.") + + record.Write(responseTest) + if record.Len != len(responseTest) { + t.Fatalf("Expected the bytes written counter to be %d, but instead found %d\n", len(responseTest), record.Len) + } +} diff --git a/ag_201_coredns/plugin/pkg/dnstest/server.go b/ag_201_coredns/plugin/pkg/dnstest/server.go new file mode 100644 index 0000000..94c3906 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnstest/server.go @@ -0,0 +1,65 @@ +package dnstest + +import ( + "net" + + "github.com/coredns/coredns/plugin/pkg/reuseport" + + "github.com/miekg/dns" +) + +// A Server is an DNS server listening on a system-chosen port on the local +// loopback interface, for use in end-to-end DNS tests. +type Server struct { + Addr string // Address where the server listening. + + s1 *dns.Server // udp + s2 *dns.Server // tcp +} + +// NewServer starts and returns a new Server. The caller should call Close when +// finished, to shut it down. +func NewServer(f dns.HandlerFunc) *Server { + dns.HandleFunc(".", f) + + ch1 := make(chan bool) + ch2 := make(chan bool) + + s1 := &dns.Server{} // udp + s2 := &dns.Server{} // tcp + + for i := 0; i < 5; i++ { // 5 attempts + s2.Listener, _ = reuseport.Listen("tcp", ":0") + if s2.Listener == nil { + continue + } + + s1.PacketConn, _ = net.ListenPacket("udp", s2.Listener.Addr().String()) + if s1.PacketConn != nil { + break + } + + // perhaps UPD port is in use, try again + s2.Listener.Close() + s2.Listener = nil + } + if s2.Listener == nil { + panic("dnstest.NewServer(): failed to create new server") + } + + s1.NotifyStartedFunc = func() { close(ch1) } + s2.NotifyStartedFunc = func() { close(ch2) } + go s1.ActivateAndServe() + go s2.ActivateAndServe() + + <-ch1 + <-ch2 + + return &Server{s1: s1, s2: s2, Addr: s2.Listener.Addr().String()} +} + +// Close shuts down the server. +func (s *Server) Close() { + s.s1.Shutdown() + s.s2.Shutdown() +} diff --git a/ag_201_coredns/plugin/pkg/dnstest/server_test.go b/ag_201_coredns/plugin/pkg/dnstest/server_test.go new file mode 100644 index 0000000..41450e4 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnstest/server_test.go @@ -0,0 +1,37 @@ +package dnstest + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestNewServer(t *testing.T) { + s := NewServer(func(w dns.ResponseWriter, r *dns.Msg) { + ret := new(dns.Msg) + ret.SetReply(r) + w.WriteMsg(ret) + }) + defer s.Close() + + c := new(dns.Client) + c.Net = "tcp" + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeSOA) + ret, _, err := c.Exchange(m, s.Addr) + if err != nil { + t.Fatalf("Could not send message to dnstest.Server: %s", err) + } + if ret.Id != m.Id { + t.Fatalf("Msg ID's should match, expected %d, got %d", m.Id, ret.Id) + } + + c.Net = "udp" + ret, _, err = c.Exchange(m, s.Addr) + if err != nil { + t.Fatalf("Could not send message to dnstest.Server: %s", err) + } + if ret.Id != m.Id { + t.Fatalf("Msg ID's should match, expected %d, got %d", m.Id, ret.Id) + } +} diff --git a/ag_201_coredns/plugin/pkg/dnsutil/cname.go b/ag_201_coredns/plugin/pkg/dnsutil/cname.go new file mode 100644 index 0000000..281e032 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnsutil/cname.go @@ -0,0 +1,15 @@ +package dnsutil + +import "github.com/miekg/dns" + +// DuplicateCNAME returns true if r already exists in records. +func DuplicateCNAME(r *dns.CNAME, records []dns.RR) bool { + for _, rec := range records { + if v, ok := rec.(*dns.CNAME); ok { + if v.Target == r.Target { + return true + } + } + } + return false +} diff --git a/ag_201_coredns/plugin/pkg/dnsutil/cname_test.go b/ag_201_coredns/plugin/pkg/dnsutil/cname_test.go new file mode 100644 index 0000000..5fb8d30 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnsutil/cname_test.go @@ -0,0 +1,55 @@ +package dnsutil + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestDuplicateCNAME(t *testing.T) { + tests := []struct { + cname string + records []string + expected bool + }{ + { + "1.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 1.0.0.0.192.IN-ADDR.ARPA.", + []string{ + "US. 86400 IN NSEC 0-.us. NS SOA RRSIG NSEC DNSKEY TYPE65534", + "1.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 1.0.0.0.192.IN-ADDR.ARPA.", + }, + true, + }, + { + "1.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 1.0.0.0.192.IN-ADDR.ARPA.", + []string{ + "US. 86400 IN NSEC 0-.us. NS SOA RRSIG NSEC DNSKEY TYPE65534", + }, + false, + }, + { + "1.0.0.192.IN-ADDR.ARPA. 3600 IN CNAME 1.0.0.0.192.IN-ADDR.ARPA.", + []string{}, + false, + }, + } + for i, test := range tests { + cnameRR, err := dns.NewRR(test.cname) + if err != nil { + t.Fatalf("Test %d, cname ('%s') error (%s)!", i, test.cname, err) + } + cname := cnameRR.(*dns.CNAME) + records := []dns.RR{} + for j, r := range test.records { + rr, err := dns.NewRR(r) + if err != nil { + t.Fatalf("Test %d, record %d ('%s') error (%s)!", i, j, r, err) + } + records = append(records, rr) + } + got := DuplicateCNAME(cname, records) + if got != test.expected { + t.Errorf("Test %d, expected '%v', got '%v' for CNAME ('%s') and RECORDS (%v)", i, test.expected, got, test.cname, test.records) + } + } +} diff --git a/ag_201_coredns/plugin/pkg/dnsutil/doc.go b/ag_201_coredns/plugin/pkg/dnsutil/doc.go new file mode 100644 index 0000000..75d1e8c --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnsutil/doc.go @@ -0,0 +1,2 @@ +// Package dnsutil contains DNS related helper functions. +package dnsutil diff --git a/ag_201_coredns/plugin/pkg/dnsutil/join.go b/ag_201_coredns/plugin/pkg/dnsutil/join.go new file mode 100644 index 0000000..b3a40db --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnsutil/join.go @@ -0,0 +1,17 @@ +package dnsutil + +import ( + "strings" + + "github.com/miekg/dns" +) + +// Join joins labels to form a fully qualified domain name. If the last label is +// the root label it is ignored. Not other syntax checks are performed. +func Join(labels ...string) string { + ll := len(labels) + if labels[ll-1] == "." { + return strings.Join(labels[:ll-1], ".") + "." + } + return dns.Fqdn(strings.Join(labels, ".")) +} diff --git a/ag_201_coredns/plugin/pkg/dnsutil/join_test.go b/ag_201_coredns/plugin/pkg/dnsutil/join_test.go new file mode 100644 index 0000000..1a50a3c --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnsutil/join_test.go @@ -0,0 +1,21 @@ +package dnsutil + +import "testing" + +func TestJoin(t *testing.T) { + tests := []struct { + in []string + out string + }{ + {[]string{"bla", "bliep", "example", "org"}, "bla.bliep.example.org."}, + {[]string{"example", "."}, "example."}, + {[]string{"example", "org."}, "example.org."}, // technically we should not be called like this. + {[]string{"."}, "."}, + } + + for i, tc := range tests { + if x := Join(tc.in...); x != tc.out { + t.Errorf("Test %d, expected %s, got %s", i, tc.out, x) + } + } +} diff --git a/ag_201_coredns/plugin/pkg/dnsutil/reverse.go b/ag_201_coredns/plugin/pkg/dnsutil/reverse.go new file mode 100644 index 0000000..7bfd235 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnsutil/reverse.go @@ -0,0 +1,81 @@ +package dnsutil + +import ( + "net" + "strings" +) + +// ExtractAddressFromReverse turns a standard PTR reverse record name +// into an IP address. This works for ipv4 or ipv6. +// +// 54.119.58.176.in-addr.arpa. becomes 176.58.119.54. If the conversion +// fails the empty string is returned. +func ExtractAddressFromReverse(reverseName string) string { + search := "" + + f := reverse + + switch { + case strings.HasSuffix(reverseName, IP4arpa): + search = strings.TrimSuffix(reverseName, IP4arpa) + case strings.HasSuffix(reverseName, IP6arpa): + search = strings.TrimSuffix(reverseName, IP6arpa) + f = reverse6 + default: + return "" + } + + // Reverse the segments and then combine them. + return f(strings.Split(search, ".")) +} + +// IsReverse returns 0 is name is not in a reverse zone. Anything > 0 indicates +// name is in a reverse zone. The returned integer will be 1 for in-addr.arpa. (IPv4) +// and 2 for ip6.arpa. (IPv6). +func IsReverse(name string) int { + if strings.HasSuffix(name, IP4arpa) { + return 1 + } + if strings.HasSuffix(name, IP6arpa) { + return 2 + } + return 0 +} + +func reverse(slice []string) string { + for i := 0; i < len(slice)/2; i++ { + j := len(slice) - i - 1 + slice[i], slice[j] = slice[j], slice[i] + } + ip := net.ParseIP(strings.Join(slice, ".")).To4() + if ip == nil { + return "" + } + return ip.String() +} + +// reverse6 reverse the segments and combine them according to RFC3596: +// b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2 +// is reversed to 2001:db8::567:89ab +func reverse6(slice []string) string { + for i := 0; i < len(slice)/2; i++ { + j := len(slice) - i - 1 + slice[i], slice[j] = slice[j], slice[i] + } + slice6 := []string{} + for i := 0; i < len(slice)/4; i++ { + slice6 = append(slice6, strings.Join(slice[i*4:i*4+4], "")) + } + ip := net.ParseIP(strings.Join(slice6, ":")).To16() + if ip == nil { + return "" + } + return ip.String() +} + +const ( + // IP4arpa is the reverse tree suffix for v4 IP addresses. + IP4arpa = ".in-addr.arpa." + // IP6arpa is the reverse tree suffix for v6 IP addresses. + IP6arpa = ".ip6.arpa." +) diff --git a/ag_201_coredns/plugin/pkg/dnsutil/reverse_test.go b/ag_201_coredns/plugin/pkg/dnsutil/reverse_test.go new file mode 100644 index 0000000..6fb8279 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnsutil/reverse_test.go @@ -0,0 +1,70 @@ +package dnsutil + +import ( + "testing" +) + +func TestExtractAddressFromReverse(t *testing.T) { + tests := []struct { + reverseName string + expectedAddress string + }{ + { + "54.119.58.176.in-addr.arpa.", + "176.58.119.54", + }, + { + ".58.176.in-addr.arpa.", + "", + }, + { + "b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.in-addr.arpa.", + "", + }, + { + "b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", + "2001:db8::567:89ab", + }, + { + "d.0.1.0.0.2.ip6.arpa.", + "", + }, + { + "54.119.58.176.ip6.arpa.", + "", + }, + { + "NONAME", + "", + }, + { + "", + "", + }, + } + for i, test := range tests { + got := ExtractAddressFromReverse(test.reverseName) + if got != test.expectedAddress { + t.Errorf("Test %d, expected '%s', got '%s'", i, test.expectedAddress, got) + } + } +} + +func TestIsReverse(t *testing.T) { + tests := []struct { + name string + expected int + }{ + {"b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", 2}, + {"d.0.1.0.0.2.in-addr.arpa.", 1}, + {"example.com.", 0}, + {"", 0}, + {"in-addr.arpa.example.com.", 0}, + } + for i, tc := range tests { + got := IsReverse(tc.name) + if got != tc.expected { + t.Errorf("Test %d, got %d, expected %d for %s", i, got, tc.expected, tc.name) + } + } +} diff --git a/ag_201_coredns/plugin/pkg/dnsutil/ttl.go b/ag_201_coredns/plugin/pkg/dnsutil/ttl.go new file mode 100644 index 0000000..e2b2652 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnsutil/ttl.go @@ -0,0 +1,52 @@ +package dnsutil + +import ( + "time" + + "github.com/coredns/coredns/plugin/pkg/response" + + "github.com/miekg/dns" +) + +// MinimalTTL scans the message returns the lowest TTL found taking into the response.Type of the message. +func MinimalTTL(m *dns.Msg, mt response.Type) time.Duration { + if mt != response.NoError && mt != response.NameError && mt != response.NoData { + return MinimalDefaultTTL + } + + // No records or OPT is the only record, return a short ttl as a fail safe. + if len(m.Answer)+len(m.Ns) == 0 && + (len(m.Extra) == 0 || (len(m.Extra) == 1 && m.Extra[0].Header().Rrtype == dns.TypeOPT)) { + return MinimalDefaultTTL + } + + minTTL := MaximumDefaulTTL + for _, r := range m.Answer { + if r.Header().Ttl < uint32(minTTL.Seconds()) { + minTTL = time.Duration(r.Header().Ttl) * time.Second + } + } + for _, r := range m.Ns { + if r.Header().Ttl < uint32(minTTL.Seconds()) { + minTTL = time.Duration(r.Header().Ttl) * time.Second + } + } + + for _, r := range m.Extra { + if r.Header().Rrtype == dns.TypeOPT { + // OPT records use TTL field for extended rcode and flags + continue + } + if r.Header().Ttl < uint32(minTTL.Seconds()) { + minTTL = time.Duration(r.Header().Ttl) * time.Second + } + } + return minTTL +} + +const ( + // MinimalDefaultTTL is the absolute lowest TTL we use in CoreDNS. + MinimalDefaultTTL = 5 * time.Second + // MaximumDefaulTTL is the maximum TTL was use on RRsets in CoreDNS. + MaximumDefaulTTL = 1 * time.Hour +) diff --git a/ag_201_coredns/plugin/pkg/dnsutil/ttl_test.go b/ag_201_coredns/plugin/pkg/dnsutil/ttl_test.go new file mode 100644 index 0000000..7dab65c --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnsutil/ttl_test.go @@ -0,0 +1,72 @@ +package dnsutil + +import ( + "testing" + "time" + + "github.com/coredns/coredns/plugin/pkg/response" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +// See https://github.com/kubernetes/dns/issues/121, add some specific tests for those use cases. + +func TestMinimalTTL(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion("z.alm.im.", dns.TypeA) + m.Ns = []dns.RR{ + test.SOA("alm.im. 1800 IN SOA ivan.ns.cloudflare.com. dns.cloudflare.com. 2025042470 10000 2400 604800 3600"), + } + + utc := time.Now().UTC() + + mt, _ := response.Typify(m, utc) + if mt != response.NoData { + t.Fatalf("Expected type to be response.NoData, got %s", mt) + } + dur := MinimalTTL(m, mt) // minTTL on msg is 3600 (neg. ttl on SOA) + if dur != time.Duration(1800*time.Second) { + t.Fatalf("Expected minttl duration to be %d, got %d", 1800, dur) + } + + m.Rcode = dns.RcodeNameError + mt, _ = response.Typify(m, utc) + if mt != response.NameError { + t.Fatalf("Expected type to be response.NameError, got %s", mt) + } + dur = MinimalTTL(m, mt) // minTTL on msg is 3600 (neg. ttl on SOA) + if dur != time.Duration(1800*time.Second) { + t.Fatalf("Expected minttl duration to be %d, got %d", 1800, dur) + } +} + +func BenchmarkMinimalTTL(b *testing.B) { + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + m.Ns = []dns.RR{ + test.A("a.example.org. 1800 IN A 127.0.0.53"), + test.A("b.example.org. 1900 IN A 127.0.0.53"), + test.A("c.example.org. 1600 IN A 127.0.0.53"), + test.A("d.example.org. 1100 IN A 127.0.0.53"), + test.A("e.example.org. 1000 IN A 127.0.0.53"), + } + m.Extra = []dns.RR{ + test.A("a.example.org. 1800 IN A 127.0.0.53"), + test.A("b.example.org. 1600 IN A 127.0.0.53"), + test.A("c.example.org. 1400 IN A 127.0.0.53"), + test.A("d.example.org. 1200 IN A 127.0.0.53"), + test.A("e.example.org. 1100 IN A 127.0.0.53"), + } + + utc := time.Now().UTC() + mt, _ := response.Typify(m, utc) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + dur := MinimalTTL(m, mt) + if dur != 1000*time.Second { + b.Fatalf("Wrong MinimalTTL %d, expected %d", dur, 1000*time.Second) + } + } +} diff --git a/ag_201_coredns/plugin/pkg/dnsutil/zone.go b/ag_201_coredns/plugin/pkg/dnsutil/zone.go new file mode 100644 index 0000000..579fef1 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnsutil/zone.go @@ -0,0 +1,20 @@ +package dnsutil + +import ( + "errors" + + "github.com/miekg/dns" +) + +// TrimZone removes the zone component from q. It returns the trimmed +// name or an error is zone is longer then qname. The trimmed name will be returned +// without a trailing dot. +func TrimZone(q string, z string) (string, error) { + zl := dns.CountLabel(z) + i, ok := dns.PrevLabel(q, zl) + if ok || i-1 < 0 { + return "", errors.New("trimzone: overshot qname: " + q + "for zone " + z) + } + // This includes the '.', remove on return + return q[:i-1], nil +} diff --git a/ag_201_coredns/plugin/pkg/dnsutil/zone_test.go b/ag_201_coredns/plugin/pkg/dnsutil/zone_test.go new file mode 100644 index 0000000..81cd1ad --- /dev/null +++ b/ag_201_coredns/plugin/pkg/dnsutil/zone_test.go @@ -0,0 +1,39 @@ +package dnsutil + +import ( + "errors" + "testing" + + "github.com/miekg/dns" +) + +func TestTrimZone(t *testing.T) { + tests := []struct { + qname string + zone string + expected string + err error + }{ + {"a.example.org", "example.org", "a", nil}, + {"a.b.example.org", "example.org", "a.b", nil}, + {"b.", ".", "b", nil}, + {"example.org", "example.org", "", errors.New("should err")}, + {"org", "example.org", "", errors.New("should err")}, + } + + for i, tc := range tests { + got, err := TrimZone(dns.Fqdn(tc.qname), dns.Fqdn(tc.zone)) + if tc.err != nil && err == nil { + t.Errorf("Test %d, expected error got nil", i) + continue + } + if tc.err == nil && err != nil { + t.Errorf("Test %d, expected no error got %v", i, err) + continue + } + if got != tc.expected { + t.Errorf("Test %d, expected %s, got %s", i, tc.expected, got) + continue + } + } +} diff --git a/ag_201_coredns/plugin/pkg/doh/doh.go b/ag_201_coredns/plugin/pkg/doh/doh.go new file mode 100644 index 0000000..9d5305b --- /dev/null +++ b/ag_201_coredns/plugin/pkg/doh/doh.go @@ -0,0 +1,116 @@ +package doh + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" + "net/http" + + "github.com/miekg/dns" +) + +// MimeType is the DoH mimetype that should be used. +const MimeType = "application/dns-message" + +// Path is the URL path that should be used. +const Path = "/dns-query" + +// NewRequest returns a new DoH request given a method, URL (without any paths, so exclude /dns-query) and dns.Msg. +func NewRequest(method, url string, m *dns.Msg) (*http.Request, error) { + buf, err := m.Pack() + if err != nil { + return nil, err + } + + switch method { + case http.MethodGet: + b64 := base64.RawURLEncoding.EncodeToString(buf) + + req, err := http.NewRequest(http.MethodGet, "https://"+url+Path+"?dns="+b64, nil) + if err != nil { + return req, err + } + + req.Header.Set("content-type", MimeType) + req.Header.Set("accept", MimeType) + return req, nil + + case http.MethodPost: + req, err := http.NewRequest(http.MethodPost, "https://"+url+Path+"?bla=foo:443", bytes.NewReader(buf)) + if err != nil { + return req, err + } + + req.Header.Set("content-type", MimeType) + req.Header.Set("accept", MimeType) + return req, nil + + default: + return nil, fmt.Errorf("method not allowed: %s", method) + } +} + +// ResponseToMsg converts a http.Response to a dns message. +func ResponseToMsg(resp *http.Response) (*dns.Msg, error) { + defer resp.Body.Close() + + return toMsg(resp.Body) +} + +// RequestToMsg converts a http.Request to a dns message. +func RequestToMsg(req *http.Request) (*dns.Msg, error) { + switch req.Method { + case http.MethodGet: + return requestToMsgGet(req) + + case http.MethodPost: + return requestToMsgPost(req) + + default: + return nil, fmt.Errorf("method not allowed: %s", req.Method) + } +} + +// requestToMsgPost extracts the dns message from the request body. +func requestToMsgPost(req *http.Request) (*dns.Msg, error) { + defer req.Body.Close() + return toMsg(req.Body) +} + +// requestToMsgGet extract the dns message from the GET request. +func requestToMsgGet(req *http.Request) (*dns.Msg, error) { + values := req.URL.Query() + b64, ok := values["dns"] + if !ok { + return nil, fmt.Errorf("no 'dns' query parameter found") + } + if len(b64) != 1 { + return nil, fmt.Errorf("multiple 'dns' query values found") + } + return base64ToMsg(b64[0]) +} + +func toMsg(r io.ReadCloser) (*dns.Msg, error) { + buf, err := io.ReadAll(http.MaxBytesReader(nil, r, 65536)) + if err != nil { + return nil, err + } + m := new(dns.Msg) + err = m.Unpack(buf) + return m, err +} + +func base64ToMsg(b64 string) (*dns.Msg, error) { + buf, err := b64Enc.DecodeString(b64) + if err != nil { + return nil, err + } + + m := new(dns.Msg) + err = m.Unpack(buf) + + return m, err +} + +var b64Enc = base64.RawURLEncoding diff --git a/ag_201_coredns/plugin/pkg/doh/doh_test.go b/ag_201_coredns/plugin/pkg/doh/doh_test.go new file mode 100644 index 0000000..4491661 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/doh/doh_test.go @@ -0,0 +1,52 @@ +package doh + +import ( + "net/http" + "testing" + + "github.com/miekg/dns" +) + +func TestPostRequest(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeDNSKEY) + + req, err := NewRequest(http.MethodPost, "https://example.org:443", m) + if err != nil { + t.Errorf("Failure to make request: %s", err) + } + + m, err = RequestToMsg(req) + if err != nil { + t.Fatalf("Failure to get message from request: %s", err) + } + + if x := m.Question[0].Name; x != "example.org." { + t.Errorf("Qname expected %s, got %s", "example.org.", x) + } + if x := m.Question[0].Qtype; x != dns.TypeDNSKEY { + t.Errorf("Qname expected %d, got %d", x, dns.TypeDNSKEY) + } +} + +func TestGetRequest(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeDNSKEY) + + req, err := NewRequest(http.MethodGet, "https://example.org:443", m) + if err != nil { + t.Errorf("Failure to make request: %s", err) + } + + m, err = RequestToMsg(req) + if err != nil { + t.Fatalf("Failure to get message from request: %s", err) + } + + if x := m.Question[0].Name; x != "example.org." { + t.Errorf("Qname expected %s, got %s", "example.org.", x) + } + if x := m.Question[0].Qtype; x != dns.TypeDNSKEY { + t.Errorf("Qname expected %d, got %d", x, dns.TypeDNSKEY) + } +} diff --git a/ag_201_coredns/plugin/pkg/edns/edns.go b/ag_201_coredns/plugin/pkg/edns/edns.go new file mode 100644 index 0000000..31f57ea --- /dev/null +++ b/ag_201_coredns/plugin/pkg/edns/edns.go @@ -0,0 +1,74 @@ +// Package edns provides function useful for adding/inspecting OPT records to/in messages. +package edns + +import ( + "errors" + "sync" + + "github.com/miekg/dns" +) + +var sup = &supported{m: make(map[uint16]struct{})} + +type supported struct { + m map[uint16]struct{} + sync.RWMutex +} + +// SetSupportedOption adds a new supported option the set of EDNS0 options that we support. Plugins typically call +// this in their setup code to signal support for a new option. +// By default we support: +// dns.EDNS0NSID, dns.EDNS0EXPIRE, dns.EDNS0COOKIE, dns.EDNS0TCPKEEPALIVE, dns.EDNS0PADDING. These +// values are not in this map and checked directly in the server. +func SetSupportedOption(option uint16) { + sup.Lock() + sup.m[option] = struct{}{} + sup.Unlock() +} + +// SupportedOption returns true if the option code is supported as an extra EDNS0 option. +func SupportedOption(option uint16) bool { + sup.RLock() + _, ok := sup.m[option] + sup.RUnlock() + return ok +} + +// Version checks the EDNS version in the request. If error +// is nil everything is OK and we can invoke the plugin. If non-nil, the +// returned Msg is valid to be returned to the client (and should). For some +// reason this response should not contain a question RR in the question section. +func Version(req *dns.Msg) (*dns.Msg, error) { + opt := req.IsEdns0() + if opt == nil { + return nil, nil + } + if opt.Version() == 0 { + return nil, nil + } + m := new(dns.Msg) + m.SetReply(req) + // zero out question section, wtf. + m.Question = nil + + o := new(dns.OPT) + o.Hdr.Name = "." + o.Hdr.Rrtype = dns.TypeOPT + o.SetVersion(0) + m.Rcode = dns.RcodeBadVers + o.SetExtendedRcode(dns.RcodeBadVers) + m.Extra = []dns.RR{o} + + return m, errors.New("EDNS0 BADVERS") +} + +// Size returns a normalized size based on proto. +func Size(proto string, size uint16) uint16 { + if proto == "tcp" { + return dns.MaxMsgSize + } + if size < dns.MinMsgSize { + return dns.MinMsgSize + } + return size +} diff --git a/ag_201_coredns/plugin/pkg/edns/edns_test.go b/ag_201_coredns/plugin/pkg/edns/edns_test.go new file mode 100644 index 0000000..a775b50 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/edns/edns_test.go @@ -0,0 +1,37 @@ +package edns + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestVersion(t *testing.T) { + m := ednsMsg() + m.Extra[0].(*dns.OPT).SetVersion(2) + + _, err := Version(m) + if err == nil { + t.Errorf("Expected wrong version, but got OK") + } +} + +func TestVersionNoEdns(t *testing.T) { + m := ednsMsg() + m.Extra = nil + + _, err := Version(m) + if err != nil { + t.Errorf("Expected no error, but got one: %s", err) + } +} + +func ednsMsg() *dns.Msg { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + o := new(dns.OPT) + o.Hdr.Name = "." + o.Hdr.Rrtype = dns.TypeOPT + m.Extra = append(m.Extra, o) + return m +} diff --git a/ag_201_coredns/plugin/pkg/expression/expression.go b/ag_201_coredns/plugin/pkg/expression/expression.go new file mode 100644 index 0000000..dad38fe --- /dev/null +++ b/ag_201_coredns/plugin/pkg/expression/expression.go @@ -0,0 +1,47 @@ +package expression + +import ( + "context" + "errors" + "net" + + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/request" +) + +// DefaultEnv returns the default set of custom state variables and functions available to for use in expression evaluation. +func DefaultEnv(ctx context.Context, state *request.Request) map[string]interface{} { + return map[string]interface{}{ + "incidr": func(ipStr, cidrStr string) (bool, error) { + ip := net.ParseIP(ipStr) + if ip == nil { + return false, errors.New("first argument is not an IP address") + } + _, cidr, err := net.ParseCIDR(cidrStr) + if err != nil { + return false, err + } + return cidr.Contains(ip), nil + }, + "metadata": func(label string) string { + f := metadata.ValueFunc(ctx, label) + if f == nil { + return "" + } + return f() + }, + "type": state.Type, + "name": state.Name, + "class": state.Class, + "proto": state.Proto, + "size": state.Len, + "client_ip": state.IP, + "port": state.Port, + "id": func() int { return int(state.Req.Id) }, + "opcode": func() int { return state.Req.Opcode }, + "do": state.Do, + "bufsize": state.Size, + "server_ip": state.LocalIP, + "server_port": state.LocalPort, + } +} diff --git a/ag_201_coredns/plugin/pkg/expression/expression_test.go b/ag_201_coredns/plugin/pkg/expression/expression_test.go new file mode 100644 index 0000000..b39c679 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/expression/expression_test.go @@ -0,0 +1,73 @@ +package expression + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/request" +) + +func TestInCidr(t *testing.T) { + incidr := DefaultEnv(context.Background(), &request.Request{})["incidr"] + + cases := []struct { + ip string + cidr string + expected bool + shouldErr bool + }{ + // positive + {ip: "1.2.3.4", cidr: "1.2.0.0/16", expected: true, shouldErr: false}, + {ip: "10.2.3.4", cidr: "1.2.0.0/16", expected: false, shouldErr: false}, + {ip: "1:2::3:4", cidr: "1:2::/64", expected: true, shouldErr: false}, + {ip: "A:2::3:4", cidr: "1:2::/64", expected: false, shouldErr: false}, + // negative + {ip: "1.2.3.4", cidr: "invalid", shouldErr: true}, + {ip: "invalid", cidr: "1.2.0.0/16", shouldErr: true}, + } + + for i, c := range cases { + r, err := incidr.(func(string, string) (bool, error))(c.ip, c.cidr) + if err != nil && !c.shouldErr { + t.Errorf("Test %d: unexpected error %v", i, err) + continue + } + if err == nil && c.shouldErr { + t.Errorf("Test %d: expected error", i) + continue + } + if c.shouldErr { + continue + } + if r != c.expected { + t.Errorf("Test %d: expected %v", i, c.expected) + continue + } + } +} + +func TestMetadata(t *testing.T) { + ctx := metadata.ContextWithMetadata(context.Background()) + metadata.SetValueFunc(ctx, "test/metadata", func() string { + return "success" + }) + f := DefaultEnv(ctx, &request.Request{})["metadata"] + + cases := []struct { + label string + expected string + shouldErr bool + }{ + {label: "test/metadata", expected: "success"}, + {label: "test/nonexistent", expected: ""}, + } + + for i, c := range cases { + r := f.(func(string) string)(c.label) + if r != c.expected { + t.Errorf("Test %d: expected %v", i, c.expected) + continue + } + } +} diff --git a/ag_201_coredns/plugin/pkg/fall/fall.go b/ag_201_coredns/plugin/pkg/fall/fall.go new file mode 100644 index 0000000..f819f99 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/fall/fall.go @@ -0,0 +1,71 @@ +// Package fall handles the fallthrough logic used in plugins that support it. Be careful when including this +// functionality in your plugin. Why? In the DNS only 1 source is authoritative for a set of names. Fallthrough +// breaks this convention by allowing a plugin to query multiple sources, depending on the replies it got sofar. +// +// This may cause issues in downstream caches, where different answers for the same query can potentially confuse clients. +// On the other hand this is a powerful feature that can aid in migration or other edge cases. +// +// The take away: be mindful of this and don't blindly assume it's a good feature to have in your plugin. +// +// See https://github.com/coredns/coredns/issues/2723 for some discussion on this, which includes this quote: +// +// TL;DR: `fallthrough` is indeed risky and hackish, but still a good feature of CoreDNS as it allows to quickly answer boring edge cases. +// +package fall + +import ( + "github.com/coredns/coredns/plugin" +) + +// F can be nil to allow for no fallthrough, empty allow all zones to fallthrough or +// contain a zone list that is checked. +type F struct { + Zones []string +} + +// Through will check if we should fallthrough for qname. Note that we've named the +// variable in each plugin "Fall", so this then reads Fall.Through(). +func (f F) Through(qname string) bool { + return plugin.Zones(f.Zones).Matches(qname) != "" +} + +// setZones will set zones in f. +func (f *F) setZones(zones []string) { + z := []string{} + for i := range zones { + z = append(z, plugin.Host(zones[i]).NormalizeExact()...) + } + f.Zones = z +} + +// SetZonesFromArgs sets zones in f to the passed value or to "." if the slice is empty. +func (f *F) SetZonesFromArgs(zones []string) { + if len(zones) == 0 { + f.setZones(Root.Zones) + return + } + f.setZones(zones) +} + +// Equal returns true if f and g are equal. +func (f *F) Equal(g F) bool { + if len(f.Zones) != len(g.Zones) { + return false + } + for i := range f.Zones { + if f.Zones[i] != g.Zones[i] { + return false + } + } + return true +} + +// Zero returns a zero valued F. +var Zero = func() F { + return F{[]string{}} +}() + +// Root returns F set to only ".". +var Root = func() F { + return F{[]string{"."}} +}() diff --git a/ag_201_coredns/plugin/pkg/fall/fall_test.go b/ag_201_coredns/plugin/pkg/fall/fall_test.go new file mode 100644 index 0000000..26cfbc2 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/fall/fall_test.go @@ -0,0 +1,65 @@ +package fall + +import "testing" + +func TestEqual(t *testing.T) { + var z F + f := F{Zones: []string{"example.com."}} + g := F{Zones: []string{"example.net."}} + h := F{Zones: []string{"example.com."}} + + if !f.Equal(h) { + t.Errorf("%v should equal %v", f, h) + } + + if z.Equal(f) { + t.Errorf("%v should not be equal to %v", z, f) + } + + if f.Equal(g) { + t.Errorf("%v should not be equal to %v", f, g) + } +} + +func TestZero(t *testing.T) { + var f F + if !f.Equal(Zero) { + t.Errorf("F should be zero") + } +} + +func TestSetZonesFromArgs(t *testing.T) { + var f F + f.SetZonesFromArgs([]string{}) + if !f.Equal(Root) { + t.Errorf("F should have the root zone") + } + + f.SetZonesFromArgs([]string{"example.com", "example.net."}) + expected := F{Zones: []string{"example.com.", "example.net."}} + if !f.Equal(expected) { + t.Errorf("F should be %v but is %v", expected, f) + } +} + +func TestFallthrough(t *testing.T) { + var fall F + if fall.Through("foo.com.") { + t.Errorf("Expected false, got true for zero fallthrough") + } + + fall.SetZonesFromArgs([]string{}) + if !fall.Through("foo.net.") { + t.Errorf("Expected true, got false for all zone fallthrough") + } + + fall.SetZonesFromArgs([]string{"foo.com", "bar.com"}) + + if fall.Through("foo.net.") { + t.Errorf("Expected false, got true for non-matching fallthrough zone") + } + + if !fall.Through("bar.com.") { + t.Errorf("Expected true, got false for matching fallthrough zone") + } +} diff --git a/ag_201_coredns/plugin/pkg/fuzz/do.go b/ag_201_coredns/plugin/pkg/fuzz/do.go new file mode 100644 index 0000000..054c429 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/fuzz/do.go @@ -0,0 +1,31 @@ +// Package fuzz contains functions that enable fuzzing of plugins. +package fuzz + +import ( + "context" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +// Do will fuzz p - used by gofuzz. See Makefile.fuzz for comments and context. +func Do(p plugin.Handler, data []byte) int { + ctx := context.TODO() + r := new(dns.Msg) + if err := r.Unpack(data); err != nil { + return 0 // plugin will never be called when this happens. + } + // If the data unpack into a dns msg, but does not have a proper question section discard it. + // The server parts make sure this is true before calling the plugins; mimic this behavior. + if len(r.Question) == 0 { + return 0 + } + + if _, err := p.ServeDNS(ctx, &test.ResponseWriter{}, r); err != nil { + return 1 + } + + return 0 +} diff --git a/ag_201_coredns/plugin/pkg/log/listener.go b/ag_201_coredns/plugin/pkg/log/listener.go new file mode 100644 index 0000000..2dfe815 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/log/listener.go @@ -0,0 +1,141 @@ +package log + +import ( + "sync" +) + +// Listener listens for all log prints of plugin loggers aka loggers with plugin name. +// When a plugin logger gets called, it should first call the same method in the Listener object. +// A usage example is, the external plugin k8s_event will replicate log prints to Kubernetes events. +type Listener interface { + Name() string + Debug(plugin string, v ...interface{}) + Debugf(plugin string, format string, v ...interface{}) + Info(plugin string, v ...interface{}) + Infof(plugin string, format string, v ...interface{}) + Warning(plugin string, v ...interface{}) + Warningf(plugin string, format string, v ...interface{}) + Error(plugin string, v ...interface{}) + Errorf(plugin string, format string, v ...interface{}) + Fatal(plugin string, v ...interface{}) + Fatalf(plugin string, format string, v ...interface{}) +} + +type listeners struct { + listeners []Listener + sync.RWMutex +} + +var ls *listeners + +func init() { + ls = &listeners{} + ls.listeners = make([]Listener, 0) +} + +// RegisterListener register a listener object. +func RegisterListener(new Listener) error { + ls.Lock() + defer ls.Unlock() + for k, l := range ls.listeners { + if l.Name() == new.Name() { + ls.listeners[k] = new + return nil + } + } + ls.listeners = append(ls.listeners, new) + return nil +} + +// DeregisterListener deregister a listener object. +func DeregisterListener(old Listener) error { + ls.Lock() + defer ls.Unlock() + for k, l := range ls.listeners { + if l.Name() == old.Name() { + ls.listeners = append(ls.listeners[:k], ls.listeners[k+1:]...) + return nil + } + } + return nil +} + +func (ls *listeners) debug(plugin string, v ...interface{}) { + ls.RLock() + for _, l := range ls.listeners { + l.Debug(plugin, v...) + } + ls.RUnlock() +} + +func (ls *listeners) debugf(plugin string, format string, v ...interface{}) { + ls.RLock() + for _, l := range ls.listeners { + l.Debugf(plugin, format, v...) + } + ls.RUnlock() +} + +func (ls *listeners) info(plugin string, v ...interface{}) { + ls.RLock() + for _, l := range ls.listeners { + l.Info(plugin, v...) + } + ls.RUnlock() +} + +func (ls *listeners) infof(plugin string, format string, v ...interface{}) { + ls.RLock() + for _, l := range ls.listeners { + l.Infof(plugin, format, v...) + } + ls.RUnlock() +} + +func (ls *listeners) warning(plugin string, v ...interface{}) { + ls.RLock() + for _, l := range ls.listeners { + l.Warning(plugin, v...) + } + ls.RUnlock() +} + +func (ls *listeners) warningf(plugin string, format string, v ...interface{}) { + ls.RLock() + for _, l := range ls.listeners { + l.Warningf(plugin, format, v...) + } + ls.RUnlock() +} + +func (ls *listeners) error(plugin string, v ...interface{}) { + ls.RLock() + for _, l := range ls.listeners { + l.Error(plugin, v...) + } + ls.RUnlock() +} + +func (ls *listeners) errorf(plugin string, format string, v ...interface{}) { + ls.RLock() + for _, l := range ls.listeners { + l.Errorf(plugin, format, v...) + } + ls.RUnlock() +} + +func (ls *listeners) fatal(plugin string, v ...interface{}) { + ls.RLock() + for _, l := range ls.listeners { + l.Fatal(plugin, v...) + } + ls.RUnlock() +} + +func (ls *listeners) fatalf(plugin string, format string, v ...interface{}) { + ls.RLock() + for _, l := range ls.listeners { + l.Fatalf(plugin, format, v...) + } + ls.RUnlock() +} diff --git a/ag_201_coredns/plugin/pkg/log/listener_test.go b/ag_201_coredns/plugin/pkg/log/listener_test.go new file mode 100644 index 0000000..0df03b4 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/log/listener_test.go @@ -0,0 +1,120 @@ +package log + +import ( + "bytes" + golog "log" + "strings" + "testing" +) + +func TestRegisterAndDeregisterListener(t *testing.T) { + for _, name := range []string{"listener1", "listener2", "listener1"} { + err := RegisterListener(NewMockListener(name)) + if err != nil { + t.Errorf("RegsiterListener Error %s", err) + } + } + if len(ls.listeners) != 2 { + t.Errorf("Expected number of listeners to be %d, got %d", 2, len(ls.listeners)) + } + for _, name := range []string{"listener1", "listener2"} { + err := DeregisterListener(NewMockListener(name)) + if err != nil { + t.Errorf("DeregsiterListener Error %s", err) + } + } + if len(ls.listeners) != 0 { + t.Errorf("Expected number of listeners to be %d, got %d", 0, len(ls.listeners)) + } +} + +func TestSingleListenerMock(t *testing.T) { + listener1Name := "listener1" + listener1Output := info + listener1Name + " mocked info" + testListenersCalled(t, []string{listener1Name}, []string{listener1Output}) +} + +func TestMultipleListenerMock(t *testing.T) { + listener1Name := "listener1" + listener1Output := info + listener1Name + " mocked info" + listener2Name := "listener2" + listener2Output := info + listener2Name + " mocked info" + testListenersCalled(t, []string{listener1Name, listener2Name}, []string{listener1Output, listener2Output}) +} + +func testListenersCalled(t *testing.T, listenerNames []string, outputs []string) { + for _, name := range listenerNames { + err := RegisterListener(NewMockListener(name)) + if err != nil { + t.Errorf("RegsiterListener Error %s", err) + } + } + var f bytes.Buffer + const ts = "test" + golog.SetOutput(&f) + lg := NewWithPlugin("testplugin") + lg.Info(ts) + for _, str := range outputs { + if x := f.String(); !strings.Contains(x, str) { + t.Errorf("Expected log to contain %s, got %s", str, x) + } + } + for _, name := range listenerNames { + err := DeregisterListener(NewMockListener(name)) + if err != nil { + t.Errorf("DeregsiterListener Error %s", err) + } + } +} + +type mockListener struct { + name string +} + +func NewMockListener(name string) *mockListener { + return &mockListener{name: name} +} + +func (l *mockListener) Name() string { + return l.name +} + +func (l *mockListener) Debug(plugin string, v ...interface{}) { + log(debug, l.name+" mocked debug") +} + +func (l *mockListener) Debugf(plugin string, format string, v ...interface{}) { + log(debug, l.name+" mocked debug") +} + +func (l *mockListener) Info(plugin string, v ...interface{}) { + log(info, l.name+" mocked info") +} + +func (l *mockListener) Infof(plugin string, format string, v ...interface{}) { + log(info, l.name+" mocked info") +} + +func (l *mockListener) Warning(plugin string, v ...interface{}) { + log(warning, l.name+" mocked warning") +} + +func (l *mockListener) Warningf(plugin string, format string, v ...interface{}) { + log(warning, l.name+" mocked warning") +} + +func (l *mockListener) Error(plugin string, v ...interface{}) { + log(err, l.name+" mocked error") +} + +func (l *mockListener) Errorf(plugin string, format string, v ...interface{}) { + log(err, l.name+" mocked error") +} + +func (l *mockListener) Fatal(plugin string, v ...interface{}) { + log(fatal, l.name+" mocked fatal") +} + +func (l *mockListener) Fatalf(plugin string, format string, v ...interface{}) { + log(fatal, l.name+" mocked fatal") +} diff --git a/ag_201_coredns/plugin/pkg/log/log.go b/ag_201_coredns/plugin/pkg/log/log.go new file mode 100644 index 0000000..0589a34 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/log/log.go @@ -0,0 +1,113 @@ +// Package log implements a small wrapper around the std lib log package. It +// implements log levels by prefixing the logs with [INFO], [DEBUG], [WARNING] +// or [ERROR]. Debug logging is available and enabled if the *debug* plugin is +// used. +// +// log.Info("this is some logging"), will log on the Info level. +// +// log.Debug("this is debug output"), will log in the Debug level, etc. +package log + +import ( + "fmt" + "io" + golog "log" + "os" + "sync" +) + +// D controls whether we should output debug logs. If true, we do, once set +// it can not be unset. +var D = &d{} + +type d struct { + on bool + sync.RWMutex +} + +// Set enables debug logging. +func (d *d) Set() { + d.Lock() + d.on = true + d.Unlock() +} + +// Clear disables debug logging. +func (d *d) Clear() { + d.Lock() + d.on = false + d.Unlock() +} + +// Value returns if debug logging is enabled. +func (d *d) Value() bool { + d.RLock() + b := d.on + d.RUnlock() + return b +} + +// logf calls log.Printf prefixed with level. +func logf(level, format string, v ...interface{}) { + golog.Print(level, fmt.Sprintf(format, v...)) +} + +// log calls log.Print prefixed with level. +func log(level string, v ...interface{}) { + golog.Print(level, fmt.Sprint(v...)) +} + +// Debug is equivalent to log.Print(), but prefixed with "[DEBUG] ". It only outputs something +// if D is true. +func Debug(v ...interface{}) { + if !D.Value() { + return + } + log(debug, v...) +} + +// Debugf is equivalent to log.Printf(), but prefixed with "[DEBUG] ". It only outputs something +// if D is true. +func Debugf(format string, v ...interface{}) { + if !D.Value() { + return + } + logf(debug, format, v...) +} + +// Info is equivalent to log.Print, but prefixed with "[INFO] ". +func Info(v ...interface{}) { log(info, v...) } + +// Infof is equivalent to log.Printf, but prefixed with "[INFO] ". +func Infof(format string, v ...interface{}) { logf(info, format, v...) } + +// Warning is equivalent to log.Print, but prefixed with "[WARNING] ". +func Warning(v ...interface{}) { log(warning, v...) } + +// Warningf is equivalent to log.Printf, but prefixed with "[WARNING] ". +func Warningf(format string, v ...interface{}) { logf(warning, format, v...) } + +// Error is equivalent to log.Print, but prefixed with "[ERROR] ". +func Error(v ...interface{}) { log(err, v...) } + +// Errorf is equivalent to log.Printf, but prefixed with "[ERROR] ". +func Errorf(format string, v ...interface{}) { logf(err, format, v...) } + +// Fatal is equivalent to log.Print, but prefixed with "[FATAL] ", and calling +// os.Exit(1). +func Fatal(v ...interface{}) { log(fatal, v...); os.Exit(1) } + +// Fatalf is equivalent to log.Printf, but prefixed with "[FATAL] ", and calling +// os.Exit(1) +func Fatalf(format string, v ...interface{}) { logf(fatal, format, v...); os.Exit(1) } + +// Discard sets the log output to /dev/null. +func Discard() { golog.SetOutput(io.Discard) } + +const ( + debug = "[DEBUG] " + err = "[ERROR] " + fatal = "[FATAL] " + info = "[INFO] " + warning = "[WARNING] " +) diff --git a/ag_201_coredns/plugin/pkg/log/log_test.go b/ag_201_coredns/plugin/pkg/log/log_test.go new file mode 100644 index 0000000..32c1d39 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/log/log_test.go @@ -0,0 +1,72 @@ +package log + +import ( + "bytes" + golog "log" + "strings" + "testing" +) + +func TestDebug(t *testing.T) { + var f bytes.Buffer + golog.SetOutput(&f) + + // D == false + Debug("debug") + if x := f.String(); x != "" { + t.Errorf("Expected no debug logs, got %s", x) + } + f.Reset() + + D.Set() + Debug("debug") + if x := f.String(); !strings.Contains(x, debug+"debug") { + t.Errorf("Expected debug log to be %s, got %s", debug+"debug", x) + } + f.Reset() + + D.Clear() + Debug("debug") + if x := f.String(); x != "" { + t.Errorf("Expected no debug logs, got %s", x) + } +} + +func TestDebugx(t *testing.T) { + var f bytes.Buffer + golog.SetOutput(&f) + + D.Set() + + Debugf("%s", "debug") + if x := f.String(); !strings.Contains(x, debug+"debug") { + t.Errorf("Expected debug log to be %s, got %s", debug+"debug", x) + } + f.Reset() + + Debug("debug") + if x := f.String(); !strings.Contains(x, debug+"debug") { + t.Errorf("Expected debug log to be %s, got %s", debug+"debug", x) + } +} + +func TestLevels(t *testing.T) { + var f bytes.Buffer + const ts = "test" + golog.SetOutput(&f) + + Info(ts) + if x := f.String(); !strings.Contains(x, info+ts) { + t.Errorf("Expected log to be %s, got %s", info+ts, x) + } + f.Reset() + Warning(ts) + if x := f.String(); !strings.Contains(x, warning+ts) { + t.Errorf("Expected log to be %s, got %s", warning+ts, x) + } + f.Reset() + Error(ts) + if x := f.String(); !strings.Contains(x, err+ts) { + t.Errorf("Expected log to be %s, got %s", err+ts, x) + } +} diff --git a/ag_201_coredns/plugin/pkg/log/plugin.go b/ag_201_coredns/plugin/pkg/log/plugin.go new file mode 100644 index 0000000..1be79f1 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/log/plugin.go @@ -0,0 +1,91 @@ +package log + +import ( + "fmt" + "os" +) + +// P is a logger that includes the plugin doing the logging. +type P struct { + plugin string +} + +// NewWithPlugin returns a logger that includes "plugin/name: " in the log message. +// I.e [INFO] plugin/: message. +func NewWithPlugin(name string) P { return P{"plugin/" + name + ": "} } + +func (p P) logf(level, format string, v ...interface{}) { + log(level, p.plugin, fmt.Sprintf(format, v...)) +} + +func (p P) log(level string, v ...interface{}) { + log(level+p.plugin, v...) +} + +// Debug logs as log.Debug. +func (p P) Debug(v ...interface{}) { + if !D.Value() { + return + } + ls.debug(p.plugin, v...) + p.log(debug, v...) +} + +// Debugf logs as log.Debugf. +func (p P) Debugf(format string, v ...interface{}) { + if !D.Value() { + return + } + ls.debugf(p.plugin, format, v...) + p.logf(debug, format, v...) +} + +// Info logs as log.Info. +func (p P) Info(v ...interface{}) { + ls.info(p.plugin, v...) + p.log(info, v...) +} + +// Infof logs as log.Infof. +func (p P) Infof(format string, v ...interface{}) { + ls.infof(p.plugin, format, v...) + p.logf(info, format, v...) +} + +// Warning logs as log.Warning. +func (p P) Warning(v ...interface{}) { + ls.warning(p.plugin, v...) + p.log(warning, v...) +} + +// Warningf logs as log.Warningf. +func (p P) Warningf(format string, v ...interface{}) { + ls.warningf(p.plugin, format, v...) + p.logf(warning, format, v...) +} + +// Error logs as log.Error. +func (p P) Error(v ...interface{}) { + ls.error(p.plugin, v...) + p.log(err, v...) +} + +// Errorf logs as log.Errorf. +func (p P) Errorf(format string, v ...interface{}) { + ls.errorf(p.plugin, format, v...) + p.logf(err, format, v...) +} + +// Fatal logs as log.Fatal and calls os.Exit(1). +func (p P) Fatal(v ...interface{}) { + ls.fatal(p.plugin, v...) + p.log(fatal, v...) + os.Exit(1) +} + +// Fatalf logs as log.Fatalf and calls os.Exit(1). +func (p P) Fatalf(format string, v ...interface{}) { + ls.fatalf(p.plugin, format, v...) + p.logf(fatal, format, v...) + os.Exit(1) +} diff --git a/ag_201_coredns/plugin/pkg/log/plugin_test.go b/ag_201_coredns/plugin/pkg/log/plugin_test.go new file mode 100644 index 0000000..b24caa4 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/log/plugin_test.go @@ -0,0 +1,21 @@ +package log + +import ( + "bytes" + golog "log" + "strings" + "testing" +) + +func TestPlugins(t *testing.T) { + var f bytes.Buffer + const ts = "test" + golog.SetOutput(&f) + + lg := NewWithPlugin("testplugin") + + lg.Info(ts) + if x := f.String(); !strings.Contains(x, "plugin/testplugin") { + t.Errorf("Expected log to be %s, got %s", info+ts, x) + } +} diff --git a/ag_201_coredns/plugin/pkg/nonwriter/nonwriter.go b/ag_201_coredns/plugin/pkg/nonwriter/nonwriter.go new file mode 100644 index 0000000..411e98a --- /dev/null +++ b/ag_201_coredns/plugin/pkg/nonwriter/nonwriter.go @@ -0,0 +1,21 @@ +// Package nonwriter implements a dns.ResponseWriter that never writes, but captures the dns.Msg being written. +package nonwriter + +import ( + "github.com/miekg/dns" +) + +// Writer is a type of ResponseWriter that captures the message, but never writes to the client. +type Writer struct { + dns.ResponseWriter + Msg *dns.Msg +} + +// New makes and returns a new NonWriter. +func New(w dns.ResponseWriter) *Writer { return &Writer{ResponseWriter: w} } + +// WriteMsg records the message, but doesn't write it itself. +func (w *Writer) WriteMsg(res *dns.Msg) error { + w.Msg = res + return nil +} diff --git a/ag_201_coredns/plugin/pkg/nonwriter/nonwriter_test.go b/ag_201_coredns/plugin/pkg/nonwriter/nonwriter_test.go new file mode 100644 index 0000000..d8433af --- /dev/null +++ b/ag_201_coredns/plugin/pkg/nonwriter/nonwriter_test.go @@ -0,0 +1,19 @@ +package nonwriter + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestNonWriter(t *testing.T) { + nw := New(nil) + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + if err := nw.WriteMsg(m); err != nil { + t.Errorf("Got error when writing to nonwriter: %s", err) + } + if x := nw.Msg.Question[0].Name; x != "example.org." { + t.Errorf("Expacted 'example.org.' got %q:", x) + } +} diff --git a/ag_201_coredns/plugin/pkg/parse/host.go b/ag_201_coredns/plugin/pkg/parse/host.go new file mode 100644 index 0000000..9206a03 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/parse/host.go @@ -0,0 +1,115 @@ +package parse + +import ( + "errors" + "fmt" + "net" + "os" + "strings" + + "github.com/coredns/coredns/plugin/pkg/transport" + + "github.com/miekg/dns" +) + +// ErrNoNameservers is returned by HostPortOrFile if no servers can be parsed. +var ErrNoNameservers = errors.New("no nameservers found") + +// Strips the zone, but preserves any port that comes after the zone +func stripZone(host string) string { + if strings.Contains(host, "%") { + lastPercent := strings.LastIndex(host, "%") + newHost := host[:lastPercent] + return newHost + } + return host +} + +// HostPortOrFile parses the strings in s, each string can either be a +// address, [scheme://]address:port or a filename. The address part is checked +// and in case of filename a resolv.conf like file is (assumed) and parsed and +// the nameservers found are returned. +func HostPortOrFile(s ...string) ([]string, error) { + var servers []string + for _, h := range s { + trans, host := Transport(h) + + addr, _, err := net.SplitHostPort(host) + + if err != nil { + // Parse didn't work, it is not a addr:port combo + hostNoZone := stripZone(host) + if net.ParseIP(hostNoZone) == nil { + ss, err := tryFile(host) + if err == nil { + servers = append(servers, ss...) + continue + } + return servers, fmt.Errorf("not an IP address or file: %q", host) + } + var ss string + switch trans { + case transport.DNS: + ss = net.JoinHostPort(host, transport.Port) + case transport.TLS: + ss = transport.TLS + "://" + net.JoinHostPort(host, transport.TLSPort) + case transport.GRPC: + ss = transport.GRPC + "://" + net.JoinHostPort(host, transport.GRPCPort) + case transport.HTTPS: + ss = transport.HTTPS + "://" + net.JoinHostPort(host, transport.HTTPSPort) + } + servers = append(servers, ss) + continue + } + + if net.ParseIP(stripZone(addr)) == nil { + ss, err := tryFile(host) + if err == nil { + servers = append(servers, ss...) + continue + } + return servers, fmt.Errorf("not an IP address or file: %q", host) + } + servers = append(servers, h) + } + if len(servers) == 0 { + return servers, ErrNoNameservers + } + return servers, nil +} + +// Try to open this is a file first. +func tryFile(s string) ([]string, error) { + c, err := dns.ClientConfigFromFile(s) + if err == os.ErrNotExist { + return nil, fmt.Errorf("failed to open file %q: %q", s, err) + } else if err != nil { + return nil, err + } + + servers := []string{} + for _, s := range c.Servers { + servers = append(servers, net.JoinHostPort(s, c.Port)) + } + return servers, nil +} + +// HostPort will check if the host part is a valid IP address, if the +// IP address is valid, but no port is found, defaultPort is added. +func HostPort(s, defaultPort string) (string, error) { + addr, port, err := net.SplitHostPort(s) + if port == "" { + port = defaultPort + } + if err != nil { + if net.ParseIP(s) == nil { + return "", fmt.Errorf("must specify an IP address: `%s'", s) + } + return net.JoinHostPort(s, port), nil + } + + if net.ParseIP(addr) == nil { + return "", fmt.Errorf("must specify an IP address: `%s'", addr) + } + return net.JoinHostPort(addr, port), nil +} diff --git a/ag_201_coredns/plugin/pkg/parse/host_test.go b/ag_201_coredns/plugin/pkg/parse/host_test.go new file mode 100644 index 0000000..611f828 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/parse/host_test.go @@ -0,0 +1,111 @@ +package parse + +import ( + "os" + "testing" + + "github.com/coredns/coredns/plugin/pkg/transport" +) + +func TestHostPortOrFile(t *testing.T) { + tests := []struct { + in string + expected string + shouldErr bool + }{ + { + "8.8.8.8", + "8.8.8.8:53", + false, + }, + { + "8.8.8.8:153", + "8.8.8.8:153", + false, + }, + { + "/etc/resolv.conf:53", + "", + true, + }, + { + "resolv.conf", + "127.0.0.1:53", + false, + }, + { + "fe80::1", + "[fe80::1]:53", + false, + }, + { + "fe80::1%ens3", + "[fe80::1%ens3]:53", + false, + }, + { + "[fd01::1]:153", + "[fd01::1]:153", + false, + }, + { + "[fd01::1%ens3]:153", + "[fd01::1%ens3]:153", + false, + }, + { + "8.9.1043", + "", + true, + }, + } + + err := os.WriteFile("resolv.conf", []byte("nameserver 127.0.0.1\n"), 0600) + if err != nil { + t.Fatalf("Failed to write test resolv.conf") + } + defer os.Remove("resolv.conf") + + for i, tc := range tests { + got, err := HostPortOrFile(tc.in) + if err == nil && tc.shouldErr { + t.Errorf("Test %d, expected error, got nil", i) + continue + } + if err != nil && tc.shouldErr { + continue + } + if got[0] != tc.expected { + t.Errorf("Test %d, expected %q, got %q", i, tc.expected, got[0]) + } + } +} + +func TestParseHostPort(t *testing.T) { + tests := []struct { + in string + expected string + shouldErr bool + }{ + {"8.8.8.8:53", "8.8.8.8:53", false}, + {"a.a.a.a:153", "", true}, + {"8.8.8.8", "8.8.8.8:53", false}, + {"8.8.8.8:", "8.8.8.8:53", false}, + {"8.8.8.8::53", "", true}, + {"resolv.conf", "", true}, + } + + for i, tc := range tests { + got, err := HostPort(tc.in, transport.Port) + if err == nil && tc.shouldErr { + t.Errorf("Test %d, expected error, got nil", i) + continue + } + if err != nil && !tc.shouldErr { + t.Errorf("Test %d, expected no error, got %q", i, err) + } + if got != tc.expected { + t.Errorf("Test %d, expected %q, got %q", i, tc.expected, got) + } + } +} diff --git a/ag_201_coredns/plugin/pkg/parse/parse.go b/ag_201_coredns/plugin/pkg/parse/parse.go new file mode 100644 index 0000000..300a57a --- /dev/null +++ b/ag_201_coredns/plugin/pkg/parse/parse.go @@ -0,0 +1,38 @@ +// Package parse contains functions that can be used in the setup code for plugins. +package parse + +import ( + "fmt" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/pkg/transport" +) + +// TransferIn parses transfer statements: 'transfer from [address...]'. +func TransferIn(c *caddy.Controller) (froms []string, err error) { + if !c.NextArg() { + return nil, c.ArgErr() + } + value := c.Val() + switch value { + default: + return nil, c.Errf("unknown property %s", value) + case "from": + froms = c.RemainingArgs() + if len(froms) == 0 { + return nil, c.ArgErr() + } + for i := range froms { + if froms[i] != "*" { + normalized, err := HostPort(froms[i], transport.Port) + if err != nil { + return nil, err + } + froms[i] = normalized + } else { + return nil, fmt.Errorf("can't use '*' in transfer from") + } + } + } + return froms, nil +} diff --git a/ag_201_coredns/plugin/pkg/parse/parse_test.go b/ag_201_coredns/plugin/pkg/parse/parse_test.go new file mode 100644 index 0000000..4f253a9 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/parse/parse_test.go @@ -0,0 +1,59 @@ +package parse + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestTransferIn(t *testing.T) { + tests := []struct { + inputFileRules string + shouldErr bool + expectedFrom []string + }{ + { + `from 127.0.0.1`, + false, []string{"127.0.0.1:53"}, + }, + // OK transfer froms + { + `from 127.0.0.1 127.0.0.2`, + false, []string{"127.0.0.1:53", "127.0.0.2:53"}, + }, + // Bad transfer from garbage + { + `from !@#$%^&*()`, + true, []string{}, + }, + // Bad transfer from no args + { + `from`, + true, []string{}, + }, + // Bad transfer from * + { + `from *`, + true, []string{}, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputFileRules) + froms, err := TransferIn(c) + + if err == nil && test.shouldErr { + t.Fatalf("Test %d expected errors, but got no error %+v %+v", i, err, test) + } else if err != nil && !test.shouldErr { + t.Fatalf("Test %d expected no errors, but got '%v'", i, err) + } + + if test.expectedFrom != nil { + for j, got := range froms { + if got != test.expectedFrom[j] { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedFrom[j], got) + } + } + } + } +} diff --git a/ag_201_coredns/plugin/pkg/parse/transport.go b/ag_201_coredns/plugin/pkg/parse/transport.go new file mode 100644 index 0000000..d632120 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/parse/transport.go @@ -0,0 +1,33 @@ +package parse + +import ( + "strings" + + "github.com/coredns/coredns/plugin/pkg/transport" +) + +// Transport returns the transport defined in s and a string where the +// transport prefix is removed (if there was any). If no transport is defined +// we default to TransportDNS +func Transport(s string) (trans string, addr string) { + switch { + case strings.HasPrefix(s, transport.TLS+"://"): + s = s[len(transport.TLS+"://"):] + return transport.TLS, s + + case strings.HasPrefix(s, transport.DNS+"://"): + s = s[len(transport.DNS+"://"):] + return transport.DNS, s + + case strings.HasPrefix(s, transport.GRPC+"://"): + s = s[len(transport.GRPC+"://"):] + return transport.GRPC, s + + case strings.HasPrefix(s, transport.HTTPS+"://"): + s = s[len(transport.HTTPS+"://"):] + + return transport.HTTPS, s + } + + return transport.DNS, s +} diff --git a/ag_201_coredns/plugin/pkg/parse/transport_test.go b/ag_201_coredns/plugin/pkg/parse/transport_test.go new file mode 100644 index 0000000..d0e0fcd --- /dev/null +++ b/ag_201_coredns/plugin/pkg/parse/transport_test.go @@ -0,0 +1,25 @@ +package parse + +import ( + "testing" + + "github.com/coredns/coredns/plugin/pkg/transport" +) + +func TestTransport(t *testing.T) { + for i, test := range []struct { + input string + expected string + }{ + {"dns://.:53", transport.DNS}, + {"2003::1/64.:53", transport.DNS}, + {"grpc://example.org:1443 ", transport.GRPC}, + {"tls://example.org ", transport.TLS}, + {"https://example.org ", transport.HTTPS}, + } { + actual, _ := Transport(test.input) + if actual != test.expected { + t.Errorf("Test %d: Expected %s but got %s", i, test.expected, actual) + } + } +} diff --git a/ag_201_coredns/plugin/pkg/rand/rand.go b/ag_201_coredns/plugin/pkg/rand/rand.go new file mode 100644 index 0000000..490f59b --- /dev/null +++ b/ag_201_coredns/plugin/pkg/rand/rand.go @@ -0,0 +1,35 @@ +// Package rand is used for concurrency safe random number generator. +package rand + +import ( + "math/rand" + "sync" +) + +// Rand is used for concurrency safe random number generator. +type Rand struct { + m sync.Mutex + r *rand.Rand +} + +// New returns a new Rand from seed. +func New(seed int64) *Rand { + return &Rand{r: rand.New(rand.NewSource(seed))} +} + +// Int returns a non-negative pseudo-random int from the Source in Rand.r. +func (r *Rand) Int() int { + r.m.Lock() + v := r.r.Int() + r.m.Unlock() + return v +} + +// Perm returns, as a slice of n ints, a pseudo-random permutation of the +// integers in the half-open interval [0,n) from the Source in Rand.r. +func (r *Rand) Perm(n int) []int { + r.m.Lock() + v := r.r.Perm(n) + r.m.Unlock() + return v +} diff --git a/ag_201_coredns/plugin/pkg/rcode/rcode.go b/ag_201_coredns/plugin/pkg/rcode/rcode.go new file mode 100644 index 0000000..d221bcb --- /dev/null +++ b/ag_201_coredns/plugin/pkg/rcode/rcode.go @@ -0,0 +1,15 @@ +package rcode + +import ( + "strconv" + + "github.com/miekg/dns" +) + +// ToString convert the rcode to the official DNS string, or to "RCODE"+value if the RCODE value is unknown. +func ToString(rcode int) string { + if str, ok := dns.RcodeToString[rcode]; ok { + return str + } + return "RCODE" + strconv.Itoa(rcode) +} diff --git a/ag_201_coredns/plugin/pkg/rcode/rcode_test.go b/ag_201_coredns/plugin/pkg/rcode/rcode_test.go new file mode 100644 index 0000000..bfca32f --- /dev/null +++ b/ag_201_coredns/plugin/pkg/rcode/rcode_test.go @@ -0,0 +1,29 @@ +package rcode + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestToString(t *testing.T) { + tests := []struct { + in int + expected string + }{ + { + dns.RcodeSuccess, + "NOERROR", + }, + { + 28, + "RCODE28", + }, + } + for i, test := range tests { + got := ToString(test.in) + if got != test.expected { + t.Errorf("Test %d, expected %s, got %s", i, test.expected, got) + } + } +} diff --git a/ag_201_coredns/plugin/pkg/replacer/replacer.go b/ag_201_coredns/plugin/pkg/replacer/replacer.go new file mode 100644 index 0000000..f927305 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/replacer/replacer.go @@ -0,0 +1,277 @@ +package replacer + +import ( + "context" + "strconv" + "strings" + "sync" + "time" + + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Replacer replaces labels for values in strings. +type Replacer struct{} + +// New makes a new replacer. This only needs to be called once in the setup and +// then call Replace for each incoming message. A replacer is safe for concurrent use. +func New() Replacer { + return Replacer{} +} + +// Replace performs a replacement of values on s and returns the string with the replaced values. +func (r Replacer) Replace(ctx context.Context, state request.Request, rr *dnstest.Recorder, s string) string { + return loadFormat(s).Replace(ctx, state, rr) +} + +const ( + headerReplacer = "{>" + // EmptyValue is the default empty value. + EmptyValue = "-" +) + +// labels are all supported labels that can be used in the default Replacer. +var labels = map[string]struct{}{ + "{type}": {}, + "{name}": {}, + "{class}": {}, + "{proto}": {}, + "{size}": {}, + "{remote}": {}, + "{port}": {}, + "{local}": {}, + // Header values. + headerReplacer + "id}": {}, + headerReplacer + "opcode}": {}, + headerReplacer + "do}": {}, + headerReplacer + "bufsize}": {}, + // Recorded replacements. + "{rcode}": {}, + "{rsize}": {}, + "{duration}": {}, + headerReplacer + "rflags}": {}, +} + +// appendValue appends the current value of label. +func appendValue(b []byte, state request.Request, rr *dnstest.Recorder, label string) []byte { + switch label { + case "{type}": + return append(b, state.Type()...) + case "{name}": + return append(b, state.Name()...) + case "{class}": + return append(b, state.Class()...) + case "{proto}": + return append(b, state.Proto()...) + case "{size}": + return strconv.AppendInt(b, int64(state.Req.Len()), 10) + case "{remote}": + return appendAddrToRFC3986(b, state.IP()) + case "{port}": + return append(b, state.Port()...) + case "{local}": + return appendAddrToRFC3986(b, state.LocalIP()) + // Header placeholders (case-insensitive). + case headerReplacer + "id}": + return strconv.AppendInt(b, int64(state.Req.Id), 10) + case headerReplacer + "opcode}": + return strconv.AppendInt(b, int64(state.Req.Opcode), 10) + case headerReplacer + "do}": + return strconv.AppendBool(b, state.Do()) + case headerReplacer + "bufsize}": + return strconv.AppendInt(b, int64(state.Size()), 10) + // Recorded replacements. + case "{rcode}": + if rr == nil || rr.Msg == nil { + return append(b, EmptyValue...) + } + if rcode := dns.RcodeToString[rr.Rcode]; rcode != "" { + return append(b, rcode...) + } + return strconv.AppendInt(b, int64(rr.Rcode), 10) + case "{rsize}": + if rr == nil { + return append(b, EmptyValue...) + } + return strconv.AppendInt(b, int64(rr.Len), 10) + case "{duration}": + if rr == nil { + return append(b, EmptyValue...) + } + secs := time.Since(rr.Start).Seconds() + return append(strconv.AppendFloat(b, secs, 'f', -1, 64), 's') + case headerReplacer + "rflags}": + if rr != nil && rr.Msg != nil { + return appendFlags(b, rr.Msg.MsgHdr) + } + return append(b, EmptyValue...) + default: + return append(b, EmptyValue...) + } +} + +// appendFlags checks all header flags and appends those +// that are set as a string separated with commas +func appendFlags(b []byte, h dns.MsgHdr) []byte { + origLen := len(b) + if h.Response { + b = append(b, "qr,"...) + } + if h.Authoritative { + b = append(b, "aa,"...) + } + if h.Truncated { + b = append(b, "tc,"...) + } + if h.RecursionDesired { + b = append(b, "rd,"...) + } + if h.RecursionAvailable { + b = append(b, "ra,"...) + } + if h.Zero { + b = append(b, "z,"...) + } + if h.AuthenticatedData { + b = append(b, "ad,"...) + } + if h.CheckingDisabled { + b = append(b, "cd,"...) + } + if n := len(b); n > origLen { + return b[:n-1] // trim trailing ',' + } + return b +} + +// appendAddrToRFC3986 will add brackets to the address if it is an IPv6 address. +func appendAddrToRFC3986(b []byte, addr string) []byte { + if strings.IndexByte(addr, ':') != -1 { + b = append(b, '[') + b = append(b, addr...) + b = append(b, ']') + } else { + b = append(b, addr...) + } + return b +} + +type nodeType int + +const ( + typeLabel nodeType = iota // "{type}" + typeLiteral // "foo" + typeMetadata // "{/metadata}" +) + +// A node represents a segment of a parsed format. For example: "A {type}" +// contains two nodes: "A " (literal); and "{type}" (label). +type node struct { + value string // Literal value, label or metadata label + typ nodeType +} + +// A replacer is an ordered list of all the nodes in a format. +type replacer []node + +func parseFormat(s string) replacer { + // Assume there is a literal between each label - its cheaper to over + // allocate once than allocate twice. + rep := make(replacer, 0, strings.Count(s, "{")*2) + for { + // We find the right bracket then backtrack to find the left bracket. + // This allows us to handle formats like: "{ {foo} }". + j := strings.IndexByte(s, '}') + if j < 0 { + break + } + i := strings.LastIndexByte(s[:j], '{') + if i < 0 { + // Handle: "A } {foo}" by treating "A }" as a literal + rep = append(rep, node{ + value: s[:j+1], + typ: typeLiteral, + }) + s = s[j+1:] + continue + } + + val := s[i : j+1] + var typ nodeType + switch _, ok := labels[val]; { + case ok: + typ = typeLabel + case strings.HasPrefix(val, "{/"): + // Strip "{/}" from metadata labels + val = val[2 : len(val)-1] + typ = typeMetadata + default: + // Given: "A {X}" val is "{X}" expand it to the whole literal. + val = s[:j+1] + typ = typeLiteral + } + + // Append any leading literal. Given "A {type}" the literal is "A " + if i != 0 && typ != typeLiteral { + rep = append(rep, node{ + value: s[:i], + typ: typeLiteral, + }) + } + rep = append(rep, node{ + value: val, + typ: typ, + }) + s = s[j+1:] + } + if len(s) != 0 { + rep = append(rep, node{ + value: s, + typ: typeLiteral, + }) + } + return rep +} + +var replacerCache sync.Map // map[string]replacer + +func loadFormat(s string) replacer { + if v, ok := replacerCache.Load(s); ok { + return v.(replacer) + } + v, _ := replacerCache.LoadOrStore(s, parseFormat(s)) + return v.(replacer) +} + +// bufPool stores pointers to scratch buffers. +var bufPool = sync.Pool{ + New: func() interface{} { + return make([]byte, 0, 256) + }, +} + +func (r replacer) Replace(ctx context.Context, state request.Request, rr *dnstest.Recorder) string { + b := bufPool.Get().([]byte) + for _, s := range r { + switch s.typ { + case typeLabel: + b = appendValue(b, state, rr, s.value) + case typeLiteral: + b = append(b, s.value...) + case typeMetadata: + if fm := metadata.ValueFunc(ctx, s.value); fm != nil { + b = append(b, fm()...) + } else { + b = append(b, EmptyValue...) + } + } + } + s := string(b) + //nolint:staticcheck + bufPool.Put(b[:0]) + return s +} diff --git a/ag_201_coredns/plugin/pkg/replacer/replacer_test.go b/ag_201_coredns/plugin/pkg/replacer/replacer_test.go new file mode 100644 index 0000000..28bb08d --- /dev/null +++ b/ag_201_coredns/plugin/pkg/replacer/replacer_test.go @@ -0,0 +1,442 @@ +package replacer + +import ( + "context" + "reflect" + "strings" + "testing" + + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// This is the default format used by the log package +const CommonLogFormat = `{remote}:{port} - {>id} "{type} {class} {name} {proto} {size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {duration}` + +func TestReplacer(t *testing.T) { + w := dnstest.NewRecorder(&test.ResponseWriter{}) + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeHINFO) + r.MsgHdr.AuthenticatedData = true + state := request.Request{W: w, Req: r} + + replacer := New() + + if x := replacer.Replace(context.TODO(), state, nil, "{type}"); x != "HINFO" { + t.Errorf("Expected type to be HINFO, got %q", x) + } + if x := replacer.Replace(context.TODO(), state, nil, "{name}"); x != "example.org." { + t.Errorf("Expected request name to be example.org., got %q", x) + } + if x := replacer.Replace(context.TODO(), state, nil, "{size}"); x != "29" { + t.Errorf("Expected size to be 29, got %q", x) + } +} + +func TestParseFormat(t *testing.T) { + type formatTest struct { + Format string + Expected replacer + } + tests := []formatTest{ + { + Format: "", + Expected: replacer{}, + }, + { + Format: "A", + Expected: replacer{ + {"A", typeLiteral}, + }, + }, + { + Format: "A {A}", + Expected: replacer{ + {"A {A}", typeLiteral}, + }, + }, + { + Format: "{{remote}}", + Expected: replacer{ + {"{", typeLiteral}, + {"{remote}", typeLabel}, + {"}", typeLiteral}, + }, + }, + { + Format: "{ A {remote} A }", + Expected: replacer{ + {"{ A ", typeLiteral}, + {"{remote}", typeLabel}, + {" A }", typeLiteral}, + }, + }, + { + Format: "{remote}}", + Expected: replacer{ + {"{remote}", typeLabel}, + {"}", typeLiteral}, + }, + }, + { + Format: "{{remote}", + Expected: replacer{ + {"{", typeLiteral}, + {"{remote}", typeLabel}, + }, + }, + { + Format: `Foo } {remote}`, + Expected: replacer{ + // we don't do any optimizations to join adjacent literals + {"Foo }", typeLiteral}, + {" ", typeLiteral}, + {"{remote}", typeLabel}, + }, + }, + { + Format: `{ Foo`, + Expected: replacer{ + {"{ Foo", typeLiteral}, + }, + }, + { + Format: `} Foo`, + Expected: replacer{ + {"}", typeLiteral}, + {" Foo", typeLiteral}, + }, + }, + { + Format: "A { {remote} {type} {/meta1} } B", + Expected: replacer{ + {"A { ", typeLiteral}, + {"{remote}", typeLabel}, + {" ", typeLiteral}, + {"{type}", typeLabel}, + {" ", typeLiteral}, + {"meta1", typeMetadata}, + {" }", typeLiteral}, + {" B", typeLiteral}, + }, + }, + { + Format: `LOG {remote}:{port} - {>id} "{type} {class} {name} {proto} ` + + `{size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {/meta1}-{/meta2} ` + + `{duration} END OF LINE`, + Expected: replacer{ + {"LOG ", typeLiteral}, + {"{remote}", typeLabel}, + {":", typeLiteral}, + {"{port}", typeLabel}, + {" - ", typeLiteral}, + {"{>id}", typeLabel}, + {` "`, typeLiteral}, + {"{type}", typeLabel}, + {" ", typeLiteral}, + {"{class}", typeLabel}, + {" ", typeLiteral}, + {"{name}", typeLabel}, + {" ", typeLiteral}, + {"{proto}", typeLabel}, + {" ", typeLiteral}, + {"{size}", typeLabel}, + {" ", typeLiteral}, + {"{>do}", typeLabel}, + {" ", typeLiteral}, + {"{>bufsize}", typeLabel}, + {`" `, typeLiteral}, + {"{rcode}", typeLabel}, + {" ", typeLiteral}, + {"{>rflags}", typeLabel}, + {" ", typeLiteral}, + {"{rsize}", typeLabel}, + {" ", typeLiteral}, + {"meta1", typeMetadata}, + {"-", typeLiteral}, + {"meta2", typeMetadata}, + {" ", typeLiteral}, + {"{duration}", typeLabel}, + {" END OF LINE", typeLiteral}, + }, + }, + } + for i, x := range tests { + r := parseFormat(x.Format) + if !reflect.DeepEqual(r, x.Expected) { + t.Errorf("%d: Expected:\n\t%+v\nGot:\n\t%+v", i, x.Expected, r) + } + } +} + +func TestParseFormatNodes(t *testing.T) { + // If we parse the format successfully the result of joining all the + // segments should match the original format. + formats := []string{ + "", + "msg", + "{remote}", + "{remote}", + "{{remote}", + "{{remote}}", + "{{remote}} A", + CommonLogFormat, + CommonLogFormat + " FOO} {BAR}", + "A " + CommonLogFormat + " FOO} {BAR}", + "A " + CommonLogFormat + " {/meta}", + } + join := func(r replacer) string { + a := make([]string, len(r)) + for i, n := range r { + if n.typ == typeMetadata { + a[i] = "{/" + n.value + "}" + } else { + a[i] = n.value + } + } + return strings.Join(a, "") + } + for _, format := range formats { + r := parseFormat(format) + s := join(r) + if s != format { + t.Errorf("Expected format to be: '%s' got: '%s'", format, s) + } + } +} + +func TestLabels(t *testing.T) { + w := dnstest.NewRecorder(&test.ResponseWriter{}) + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeHINFO) + r.Id = 1053 + r.AuthenticatedData = true + r.CheckingDisabled = true + w.WriteMsg(r) + state := request.Request{W: w, Req: r} + + replacer := New() + ctx := context.TODO() + + // This couples the test very tightly to the code, but so be it. + expect := map[string]string{ + "{type}": "HINFO", + "{name}": "example.org.", + "{class}": "IN", + "{proto}": "udp", + "{size}": "29", + "{remote}": "10.240.0.1", + "{port}": "40212", + "{local}": "127.0.0.1", + headerReplacer + "id}": "1053", + headerReplacer + "opcode}": "0", + headerReplacer + "do}": "false", + headerReplacer + "bufsize}": "512", + "{rcode}": "NOERROR", + "{rsize}": "29", + "{duration}": "0", + headerReplacer + "rflags}": "rd,ad,cd", + } + if len(expect) != len(labels) { + t.Fatalf("Expect %d labels, got %d", len(expect), len(labels)) + } + + for lbl := range labels { + repl := replacer.Replace(ctx, state, w, lbl) + if lbl == "{duration}" { + if repl[len(repl)-1] != 's' { + t.Errorf("Expected seconds, got %q", repl) + } + continue + } + if repl != expect[lbl] { + t.Errorf("Expected value %q, got %q", expect[lbl], repl) + } + } +} + +func BenchmarkReplacer(b *testing.B) { + w := dnstest.NewRecorder(&test.ResponseWriter{}) + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeHINFO) + r.MsgHdr.AuthenticatedData = true + state := request.Request{W: w, Req: r} + + b.ResetTimer() + b.ReportAllocs() + + replacer := New() + for i := 0; i < b.N; i++ { + replacer.Replace(context.TODO(), state, nil, "{type} {name} {size}") + } +} + +func BenchmarkReplacer_CommonLogFormat(b *testing.B) { + w := dnstest.NewRecorder(&test.ResponseWriter{}) + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeHINFO) + r.Id = 1053 + r.AuthenticatedData = true + r.CheckingDisabled = true + r.MsgHdr.AuthenticatedData = true + w.WriteMsg(r) + state := request.Request{W: w, Req: r} + + replacer := New() + ctxt := context.TODO() + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + replacer.Replace(ctxt, state, w, CommonLogFormat) + } +} + +func BenchmarkParseFormat(b *testing.B) { + for i := 0; i < b.N; i++ { + parseFormat(CommonLogFormat) + } +} + +type testProvider map[string]metadata.Func + +func (tp testProvider) Metadata(ctx context.Context, state request.Request) context.Context { + for k, v := range tp { + metadata.SetValueFunc(ctx, k, v) + } + return ctx +} + +type testHandler struct{ ctx context.Context } + +func (m *testHandler) Name() string { return "test" } + +func (m *testHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + m.ctx = ctx + return 0, nil +} + +func TestMetadataReplacement(t *testing.T) { + tests := []struct { + expr string + result string + }{ + {"{/test/meta2}", "two"}, + {"{/test/meta2} {/test/key4}", "two -"}, + {"{/test/meta2} {/test/meta3}", "two three"}, + } + + next := &testHandler{} + m := metadata.Metadata{ + Zones: []string{"."}, + Providers: []metadata.Provider{ + testProvider{"test/meta2": func() string { return "two" }}, + testProvider{"test/meta3": func() string { return "three" }}, + }, + Next: next, + } + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeHINFO) + + ctx := m.Collect(context.TODO(), request.Request{W: w, Req: r}) + + repl := New() + state := request.Request{W: w, Req: r} + + for i, ts := range tests { + r := repl.Replace(ctx, state, nil, ts.expr) + if r != ts.result { + t.Errorf("Test %d - expr : %s, expected %q, got %q", i, ts.expr, ts.result, r) + } + } +} + +func TestMetadataMalformed(t *testing.T) { + tests := []struct { + expr string + result string + }{ + {"{/test/meta2", "{/test/meta2"}, + {"{test/meta2} {/test/meta4}", "{test/meta2} -"}, + {"{test}", "{test}"}, + } + + next := &testHandler{} + m := metadata.Metadata{ + Zones: []string{"."}, + Providers: []metadata.Provider{testProvider{"test/meta2": func() string { return "two" }}}, + Next: next, + } + + m.ServeDNS(context.TODO(), &test.ResponseWriter{}, new(dns.Msg)) + ctx := next.ctx // important because the m.ServeDNS has only now populated the context + + w := dnstest.NewRecorder(&test.ResponseWriter{}) + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeHINFO) + + repl := New() + state := request.Request{W: w, Req: r} + + for i, ts := range tests { + r := repl.Replace(ctx, state, nil, ts.expr) + if r != ts.result { + t.Errorf("Test %d - expr : %s, expected %q, got %q", i, ts.expr, ts.result, r) + } + } +} + +func TestNoResponseWasWritten(t *testing.T) { + w := dnstest.NewRecorder(&test.ResponseWriter{}) + r := new(dns.Msg) + r.SetQuestion("example.org.", dns.TypeHINFO) + r.Id = 1053 + r.AuthenticatedData = true + r.CheckingDisabled = true + state := request.Request{W: w, Req: r} + + replacer := New() + ctx := context.TODO() + + // This couples the test very tightly to the code, but so be it. + expect := map[string]string{ + "{type}": "HINFO", + "{name}": "example.org.", + "{class}": "IN", + "{proto}": "udp", + "{size}": "29", + "{remote}": "10.240.0.1", + "{port}": "40212", + "{local}": "127.0.0.1", + headerReplacer + "id}": "1053", + headerReplacer + "opcode}": "0", + headerReplacer + "do}": "false", + headerReplacer + "bufsize}": "512", + "{rcode}": "-", + "{rsize}": "0", + "{duration}": "0", + headerReplacer + "rflags}": "-", + } + if len(expect) != len(labels) { + t.Fatalf("Expect %d labels, got %d", len(expect), len(labels)) + } + + for lbl := range labels { + repl := replacer.Replace(ctx, state, w, lbl) + if lbl == "{duration}" { + if repl[len(repl)-1] != 's' { + t.Errorf("Expected seconds, got %q", repl) + } + continue + } + if repl != expect[lbl] { + t.Errorf("Expected value %q, got %q", expect[lbl], repl) + } + } +} diff --git a/ag_201_coredns/plugin/pkg/response/classify.go b/ag_201_coredns/plugin/pkg/response/classify.go new file mode 100644 index 0000000..2e705cb --- /dev/null +++ b/ag_201_coredns/plugin/pkg/response/classify.go @@ -0,0 +1,61 @@ +package response + +import "fmt" + +// Class holds sets of Types +type Class int + +const ( + // All is a meta class encompassing all the classes. + All Class = iota + // Success is a class for a successful response. + Success + // Denial is a class for denying existence (NXDOMAIN, or a nodata: type does not exist) + Denial + // Error is a class for errors, right now defined as not Success and not Denial + Error +) + +func (c Class) String() string { + switch c { + case All: + return "all" + case Success: + return "success" + case Denial: + return "denial" + case Error: + return "error" + } + return "" +} + +// ClassFromString returns the class from the string s. If not class matches +// the All class and an error are returned +func ClassFromString(s string) (Class, error) { + switch s { + case "all": + return All, nil + case "success": + return Success, nil + case "denial": + return Denial, nil + case "error": + return Error, nil + } + return All, fmt.Errorf("invalid Class: %s", s) +} + +// Classify classifies the Type t, it returns its Class. +func Classify(t Type) Class { + switch t { + case NoError, Delegation: + return Success + case NameError, NoData: + return Denial + case OtherError: + fallthrough + default: + return Error + } +} diff --git a/ag_201_coredns/plugin/pkg/response/typify.go b/ag_201_coredns/plugin/pkg/response/typify.go new file mode 100644 index 0000000..df314d4 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/response/typify.go @@ -0,0 +1,151 @@ +package response + +import ( + "fmt" + "time" + + "github.com/miekg/dns" +) + +// Type is the type of the message. +type Type int + +const ( + // NoError indicates a positive reply + NoError Type = iota + // NameError is a NXDOMAIN in header, SOA in auth. + NameError + // ServerError is a set of errors we want to cache, for now it contains SERVFAIL and NOTIMPL. + ServerError + // NoData indicates name found, but not the type: NOERROR in header, SOA in auth. + NoData + // Delegation is a msg with a pointer to another nameserver: NOERROR in header, NS in auth, optionally fluff in additional (not checked). + Delegation + // Meta indicates a meta message, NOTIFY, or a transfer: qType is IXFR or AXFR. + Meta + // Update is an dynamic update message. + Update + // OtherError indicates any other error: don't cache these. + OtherError +) + +var toString = map[Type]string{ + NoError: "NOERROR", + NameError: "NXDOMAIN", + ServerError: "SERVERERROR", + NoData: "NODATA", + Delegation: "DELEGATION", + Meta: "META", + Update: "UPDATE", + OtherError: "OTHERERROR", +} + +func (t Type) String() string { return toString[t] } + +// TypeFromString returns the type from the string s. If not type matches +// the OtherError type and an error are returned. +func TypeFromString(s string) (Type, error) { + for t, str := range toString { + if s == str { + return t, nil + } + } + return NoError, fmt.Errorf("invalid Type: %s", s) +} + +// Typify classifies a message, it returns the Type. +func Typify(m *dns.Msg, t time.Time) (Type, *dns.OPT) { + if m == nil { + return OtherError, nil + } + opt := m.IsEdns0() + do := false + if opt != nil { + do = opt.Do() + } + + if m.Opcode == dns.OpcodeUpdate { + return Update, opt + } + + // Check transfer and update first + if m.Opcode == dns.OpcodeNotify { + return Meta, opt + } + + if len(m.Question) > 0 { + if m.Question[0].Qtype == dns.TypeAXFR || m.Question[0].Qtype == dns.TypeIXFR { + return Meta, opt + } + } + + // If our message contains any expired sigs and we care about that, we should return expired + if do { + if expired := typifyExpired(m, t); expired { + return OtherError, opt + } + } + + if len(m.Answer) > 0 && m.Rcode == dns.RcodeSuccess { + return NoError, opt + } + + soa := false + ns := 0 + for _, r := range m.Ns { + if r.Header().Rrtype == dns.TypeSOA { + soa = true + continue + } + if r.Header().Rrtype == dns.TypeNS { + ns++ + } + } + + if soa && m.Rcode == dns.RcodeSuccess { + return NoData, opt + } + if soa && m.Rcode == dns.RcodeNameError { + return NameError, opt + } + + if m.Rcode == dns.RcodeServerFailure || m.Rcode == dns.RcodeNotImplemented { + return ServerError, opt + } + + if ns > 0 && m.Rcode == dns.RcodeSuccess { + return Delegation, opt + } + + if m.Rcode == dns.RcodeSuccess { + return NoError, opt + } + + return OtherError, opt +} + +func typifyExpired(m *dns.Msg, t time.Time) bool { + if expired := typifyExpiredRRSIG(m.Answer, t); expired { + return true + } + if expired := typifyExpiredRRSIG(m.Ns, t); expired { + return true + } + if expired := typifyExpiredRRSIG(m.Extra, t); expired { + return true + } + return false +} + +func typifyExpiredRRSIG(rrs []dns.RR, t time.Time) bool { + for _, r := range rrs { + if r.Header().Rrtype != dns.TypeRRSIG { + continue + } + ok := r.(*dns.RRSIG).ValidityPeriod(t) + if !ok { + return true + } + } + return false +} diff --git a/ag_201_coredns/plugin/pkg/response/typify_test.go b/ag_201_coredns/plugin/pkg/response/typify_test.go new file mode 100644 index 0000000..3d9abdf --- /dev/null +++ b/ag_201_coredns/plugin/pkg/response/typify_test.go @@ -0,0 +1,101 @@ +package response + +import ( + "testing" + "time" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestTypifyNilMsg(t *testing.T) { + var m *dns.Msg + + ty, _ := Typify(m, time.Now().UTC()) + if ty != OtherError { + t.Errorf("Message wrongly typified, expected OtherError, got %s", ty) + } +} + +func TestTypifyDelegation(t *testing.T) { + m := delegationMsg() + mt, _ := Typify(m, time.Now().UTC()) + if mt != Delegation { + t.Errorf("Message is wrongly typified, expected Delegation, got %s", mt) + } +} + +func TestTypifyRRSIG(t *testing.T) { + now, _ := time.Parse(time.UnixDate, "Fri Apr 21 10:51:21 BST 2017") + utc := now.UTC() + + m := delegationMsgRRSIGOK() + if mt, _ := Typify(m, utc); mt != Delegation { + t.Errorf("Message is wrongly typified, expected Delegation, got %s", mt) + } + + // Still a Delegation because EDNS0 OPT DO bool is not set, so we won't check the sigs. + m = delegationMsgRRSIGFail() + if mt, _ := Typify(m, utc); mt != Delegation { + t.Errorf("Message is wrongly typified, expected Delegation, got %s", mt) + } + + m = delegationMsgRRSIGFail() + m.Extra = append(m.Extra, test.OPT(4096, true)) + if mt, _ := Typify(m, utc); mt != OtherError { + t.Errorf("Message is wrongly typified, expected OtherError, got %s", mt) + } +} + +func TestTypifyImpossible(t *testing.T) { + // create impossible message that denies its own existence + m := new(dns.Msg) + m.SetQuestion("bar.www.example.org.", dns.TypeAAAA) + m.Rcode = dns.RcodeNameError // name does not exist + m.Answer = []dns.RR{test.CNAME("bar.www.example.org. IN CNAME foo.example.org.")} // but we add a cname with the name! + mt, _ := Typify(m, time.Now().UTC()) + if mt != OtherError { + t.Errorf("Impossible message not typified as OtherError, got %s", mt) + } +} + +func TestTypifyRefused(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion("foo.example.org.", dns.TypeA) + m.Rcode = dns.RcodeRefused + mt, _ := Typify(m, time.Now().UTC()) + if mt != OtherError { + t.Errorf("Refused message not typified as OtherError, got %s", mt) + } +} + +func delegationMsg() *dns.Msg { + return &dns.Msg{ + Ns: []dns.RR{ + test.NS("miek.nl. 3600 IN NS linode.atoom.net."), + test.NS("miek.nl. 3600 IN NS ns-ext.nlnetlabs.nl."), + test.NS("miek.nl. 3600 IN NS omval.tednet.nl."), + }, + Extra: []dns.RR{ + test.A("omval.tednet.nl. 3600 IN A 185.49.141.42"), + test.AAAA("omval.tednet.nl. 3600 IN AAAA 2a04:b900:0:100::42"), + }, + } +} + +func delegationMsgRRSIGOK() *dns.Msg { + del := delegationMsg() + del.Ns = append(del.Ns, + test.RRSIG("miek.nl. 1800 IN RRSIG NS 8 2 1800 20170521031301 20170421031301 12051 miek.nl. PIUu3TKX/sB/N1n1E1yWxHHIcPnc2q6Wq9InShk+5ptRqChqKdZNMLDm gCq+1bQAZ7jGvn2PbwTwE65JzES7T+hEiqR5PU23DsidvZyClbZ9l0xG JtKwgzGXLtUHxp4xv/Plq+rq/7pOG61bNCxRyS7WS7i7QcCCWT1BCcv+ wZ0="), + ) + return del +} + +func delegationMsgRRSIGFail() *dns.Msg { + del := delegationMsg() + del.Ns = append(del.Ns, + test.RRSIG("miek.nl. 1800 IN RRSIG NS 8 2 1800 20160521031301 20160421031301 12051 miek.nl. PIUu3TKX/sB/N1n1E1yWxHHIcPnc2q6Wq9InShk+5ptRqChqKdZNMLDm gCq+1bQAZ7jGvn2PbwTwE65JzES7T+hEiqR5PU23DsidvZyClbZ9l0xG JtKwgzGXLtUHxp4xv/Plq+rq/7pOG61bNCxRyS7WS7i7QcCCWT1BCcv+ wZ0="), + ) + return del +} diff --git a/ag_201_coredns/plugin/pkg/reuseport/listen_no_reuseport.go b/ag_201_coredns/plugin/pkg/reuseport/listen_no_reuseport.go new file mode 100644 index 0000000..1018a9b --- /dev/null +++ b/ag_201_coredns/plugin/pkg/reuseport/listen_no_reuseport.go @@ -0,0 +1,13 @@ +//go:build !go1.11 || (!aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd) + +package reuseport + +import "net" + +// Listen is a wrapper around net.Listen. +func Listen(network, addr string) (net.Listener, error) { return net.Listen(network, addr) } + +// ListenPacket is a wrapper around net.ListenPacket. +func ListenPacket(network, addr string) (net.PacketConn, error) { + return net.ListenPacket(network, addr) +} diff --git a/ag_201_coredns/plugin/pkg/reuseport/listen_reuseport.go b/ag_201_coredns/plugin/pkg/reuseport/listen_reuseport.go new file mode 100644 index 0000000..71fac3e --- /dev/null +++ b/ag_201_coredns/plugin/pkg/reuseport/listen_reuseport.go @@ -0,0 +1,36 @@ +//go:build go1.11 && (aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd) + +package reuseport + +import ( + "context" + "net" + "syscall" + + "github.com/coredns/coredns/plugin/pkg/log" + + "golang.org/x/sys/unix" +) + +func control(network, address string, c syscall.RawConn) error { + c.Control(func(fd uintptr) { + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { + log.Warningf("Failed to set SO_REUSEPORT on socket: %s", err) + } + }) + return nil +} + +// Listen announces on the local network address. See net.Listen for more information. +// If SO_REUSEPORT is available it will be set on the socket. +func Listen(network, addr string) (net.Listener, error) { + lc := net.ListenConfig{Control: control} + return lc.Listen(context.Background(), network, addr) +} + +// ListenPacket announces on the local network address. See net.ListenPacket for more information. +// If SO_REUSEPORT is available it will be set on the socket. +func ListenPacket(network, addr string) (net.PacketConn, error) { + lc := net.ListenConfig{Control: control} + return lc.ListenPacket(context.Background(), network, addr) +} diff --git a/ag_201_coredns/plugin/pkg/singleflight/singleflight.go b/ag_201_coredns/plugin/pkg/singleflight/singleflight.go new file mode 100644 index 0000000..e70646c --- /dev/null +++ b/ag_201_coredns/plugin/pkg/singleflight/singleflight.go @@ -0,0 +1,64 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package singleflight provides a duplicate function call suppression +// mechanism. +package singleflight + +import "sync" + +// call is an in-flight or completed Do call +type call struct { + wg sync.WaitGroup + val interface{} + err error +} + +// Group represents a class of work and forms a namespace in which +// units of work can be executed with duplicate suppression. +type Group struct { + mu sync.Mutex // protects m + m map[uint64]*call // lazily initialized +} + +// Do executes and returns the results of the given function, making +// sure that only one execution is in-flight for a given key at a +// time. If a duplicate comes in, the duplicate caller waits for the +// original to complete and receives the same results. +func (g *Group) Do(key uint64, fn func() (interface{}, error)) (interface{}, error) { + g.mu.Lock() + if g.m == nil { + g.m = make(map[uint64]*call) + } + if c, ok := g.m[key]; ok { + g.mu.Unlock() + c.wg.Wait() + return c.val, c.err + } + c := new(call) + c.wg.Add(1) + g.m[key] = c + g.mu.Unlock() + + c.val, c.err = fn() + c.wg.Done() + + g.mu.Lock() + delete(g.m, key) + g.mu.Unlock() + + return c.val, c.err +} diff --git a/ag_201_coredns/plugin/pkg/singleflight/singleflight_test.go b/ag_201_coredns/plugin/pkg/singleflight/singleflight_test.go new file mode 100644 index 0000000..0e75d41 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/singleflight/singleflight_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package singleflight + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" +) + +func TestDo(t *testing.T) { + var g Group + v, err := g.Do(1, func() (interface{}, error) { + return "bar", nil + }) + if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want { + t.Errorf("Do = %v; want %v", got, want) + } + if err != nil { + t.Errorf("Do error = %v", err) + } +} + +func TestDoErr(t *testing.T) { + var g Group + someErr := errors.New("some error") + v, err := g.Do(1, func() (interface{}, error) { + return nil, someErr + }) + if err != someErr { + t.Errorf("Do error = %v; want someErr", err) + } + if v != nil { + t.Errorf("Unexpected non-nil value %#v", v) + } +} + +func TestDoDupSuppress(t *testing.T) { + var g Group + c := make(chan string) + var calls int32 + fn := func() (interface{}, error) { + atomic.AddInt32(&calls, 1) + return <-c, nil + } + + const n = 10 + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + v, err := g.Do(1, fn) + if err != nil { + t.Errorf("Do error: %v", err) + } + if v.(string) != "bar" { + t.Errorf("Got %q; want %q", v, "bar") + } + wg.Done() + }() + } + time.Sleep(100 * time.Millisecond) // let goroutines above block + c <- "bar" + wg.Wait() + if got := atomic.LoadInt32(&calls); got != 1 { + t.Errorf("Number of calls = %d; want 1", got) + } +} diff --git a/ag_201_coredns/plugin/pkg/tls/tls.go b/ag_201_coredns/plugin/pkg/tls/tls.go new file mode 100644 index 0000000..cba2550 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/tls/tls.go @@ -0,0 +1,146 @@ +package tls + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "time" +) + +func setTLSDefaults(ctls *tls.Config) { + ctls.MinVersion = tls.VersionTLS12 + ctls.MaxVersion = tls.VersionTLS13 + ctls.CipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + } +} + +// NewTLSConfigFromArgs returns a TLS config based upon the passed +// in list of arguments. Typically these come straight from the +// Corefile. +// no args +// - creates a Config with no cert and using system CAs +// - use for a client that talks to a server with a public signed cert (CA installed in system) +// - the client will not be authenticated by the server since there is no cert +// one arg: the path to CA PEM file +// - creates a Config with no cert using a specific CA +// - use for a client that talks to a server with a private signed cert (CA not installed in system) +// - the client will not be authenticated by the server since there is no cert +// two args: path to cert PEM file, the path to private key PEM file +// - creates a Config with a cert, using system CAs to validate the other end +// - use for: +// - a server; or, +// - a client that talks to a server with a public cert and needs certificate-based authentication +// - the other end will authenticate this end via the provided cert +// - the cert of the other end will be verified via system CAs +// three args: path to cert PEM file, path to client private key PEM file, path to CA PEM file +// - creates a Config with the cert, using specified CA to validate the other end +// - use for: +// - a server; or, +// - a client that talks to a server with a privately signed cert and needs certificate-based +// authentication +// - the other end will authenticate this end via the provided cert +// - this end will verify the other end's cert using the specified CA +func NewTLSConfigFromArgs(args ...string) (*tls.Config, error) { + var err error + var c *tls.Config + switch len(args) { + case 0: + // No client cert, use system CA + c, err = NewTLSClientConfig("") + case 1: + // No client cert, use specified CA + c, err = NewTLSClientConfig(args[0]) + case 2: + // Client cert, use system CA + c, err = NewTLSConfig(args[0], args[1], "") + case 3: + // Client cert, use specified CA + c, err = NewTLSConfig(args[0], args[1], args[2]) + default: + err = fmt.Errorf("maximum of three arguments allowed for TLS config, found %d", len(args)) + } + if err != nil { + return nil, err + } + return c, nil +} + +// NewTLSConfig returns a TLS config that includes a certificate +// Use for server TLS config or when using a client certificate +// If caPath is empty, system CAs will be used +func NewTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) { + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, fmt.Errorf("could not load TLS cert: %s", err) + } + + roots, err := loadRoots(caPath) + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}, RootCAs: roots} + setTLSDefaults(tlsConfig) + + return tlsConfig, nil +} + +// NewTLSClientConfig returns a TLS config for a client connection +// If caPath is empty, system CAs will be used +func NewTLSClientConfig(caPath string) (*tls.Config, error) { + roots, err := loadRoots(caPath) + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{RootCAs: roots} + setTLSDefaults(tlsConfig) + + return tlsConfig, nil +} + +func loadRoots(caPath string) (*x509.CertPool, error) { + if caPath == "" { + return nil, nil + } + + roots := x509.NewCertPool() + pem, err := os.ReadFile(filepath.Clean(caPath)) + if err != nil { + return nil, fmt.Errorf("error reading %s: %s", caPath, err) + } + ok := roots.AppendCertsFromPEM(pem) + if !ok { + return nil, fmt.Errorf("could not read root certs: %s", err) + } + return roots, nil +} + +// NewHTTPSTransport returns an HTTP transport configured using tls.Config +func NewHTTPSTransport(cc *tls.Config) *http.Transport { + tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: cc, + MaxIdleConnsPerHost: 25, + } + + return tr +} diff --git a/ag_201_coredns/plugin/pkg/tls/tls_test.go b/ag_201_coredns/plugin/pkg/tls/tls_test.go new file mode 100644 index 0000000..b94efc2 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/tls/tls_test.go @@ -0,0 +1,101 @@ +package tls + +import ( + "path/filepath" + "testing" + + "github.com/coredns/coredns/plugin/test" +) + +func getPEMFiles(t *testing.T) (rmFunc func(), cert, key, ca string) { + tempDir, rmFunc, err := test.WritePEMFiles("") + if err != nil { + t.Fatalf("Could not write PEM files: %s", err) + } + + cert = filepath.Join(tempDir, "cert.pem") + key = filepath.Join(tempDir, "key.pem") + ca = filepath.Join(tempDir, "ca.pem") + + return +} + +func TestNewTLSConfig(t *testing.T) { + rmFunc, cert, key, ca := getPEMFiles(t) + defer rmFunc() + + _, err := NewTLSConfig(cert, key, ca) + if err != nil { + t.Errorf("Failed to create TLSConfig: %s", err) + } +} + +func TestNewTLSClientConfig(t *testing.T) { + rmFunc, _, _, ca := getPEMFiles(t) + defer rmFunc() + + _, err := NewTLSClientConfig(ca) + if err != nil { + t.Errorf("Failed to create TLSConfig: %s", err) + } +} + +func TestNewTLSConfigFromArgs(t *testing.T) { + rmFunc, cert, key, ca := getPEMFiles(t) + defer rmFunc() + + _, err := NewTLSConfigFromArgs() + if err != nil { + t.Errorf("Failed to create TLSConfig: %s", err) + } + + c, err := NewTLSConfigFromArgs(ca) + if err != nil { + t.Errorf("Failed to create TLSConfig: %s", err) + } + if c.RootCAs == nil { + t.Error("RootCAs should not be nil when one arg passed") + } + + c, err = NewTLSConfigFromArgs(cert, key) + if err != nil { + t.Errorf("Failed to create TLSConfig: %s", err) + } + if c.RootCAs != nil { + t.Error("RootCAs should be nil when two args passed") + } + if len(c.Certificates) != 1 { + t.Error("Certificates should have a single entry when two args passed") + } + args := []string{cert, key, ca} + c, err = NewTLSConfigFromArgs(args...) + if err != nil { + t.Errorf("Failed to create TLSConfig: %s", err) + } + if c.RootCAs == nil { + t.Error("RootCAs should not be nil when three args passed") + } + if len(c.Certificates) != 1 { + t.Error("Certificates should have a single entry when three args passed") + } +} + +func TestNewHTTPSTransport(t *testing.T) { + rmFunc, _, _, ca := getPEMFiles(t) + defer rmFunc() + + cc, err := NewTLSClientConfig(ca) + if err != nil { + t.Errorf("Failed to create TLSConfig: %s", err) + } + + tr := NewHTTPSTransport(cc) + if tr == nil { + t.Errorf("Failed to create https transport with cc") + } + + tr = NewHTTPSTransport(nil) + if tr == nil { + t.Errorf("Failed to create https transport without cc") + } +} diff --git a/ag_201_coredns/plugin/pkg/trace/trace.go b/ag_201_coredns/plugin/pkg/trace/trace.go new file mode 100644 index 0000000..6585d80 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/trace/trace.go @@ -0,0 +1,13 @@ +package trace + +import ( + "github.com/coredns/coredns/plugin" + + ot "github.com/opentracing/opentracing-go" +) + +// Trace holds the tracer and endpoint info +type Trace interface { + plugin.Handler + Tracer() ot.Tracer +} diff --git a/ag_201_coredns/plugin/pkg/transport/transport.go b/ag_201_coredns/plugin/pkg/transport/transport.go new file mode 100644 index 0000000..85b3bee --- /dev/null +++ b/ag_201_coredns/plugin/pkg/transport/transport.go @@ -0,0 +1,21 @@ +package transport + +// These transports are supported by CoreDNS. +const ( + DNS = "dns" + TLS = "tls" + GRPC = "grpc" + HTTPS = "https" +) + +// Port numbers for the various transports. +const ( + // Port is the default port for DNS + Port = "53" + // TLSPort is the default port for DNS-over-TLS. + TLSPort = "853" + // GRPCPort is the default port for DNS-over-gRPC. + GRPCPort = "443" + // HTTPSPort is the default port for DNS-over-HTTPS. + HTTPSPort = "443" +) diff --git a/ag_201_coredns/plugin/pkg/uniq/uniq.go b/ag_201_coredns/plugin/pkg/uniq/uniq.go new file mode 100644 index 0000000..5f95e41 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/uniq/uniq.go @@ -0,0 +1,46 @@ +// Package uniq keeps track of "thing" that are either "todo" or "done". Multiple +// identical events will only be processed once. +package uniq + +// U keeps track of item to be done. +type U struct { + u map[string]item +} + +type item struct { + state int // either todo or done + f func() error // function to be executed. +} + +// New returns a new initialized U. +func New() U { return U{u: make(map[string]item)} } + +// Set sets function f in U under key. If the key already exists it is not overwritten. +func (u U) Set(key string, f func() error) { + if _, ok := u.u[key]; ok { + return + } + u.u[key] = item{todo, f} +} + +// Unset removes the key. +func (u U) Unset(key string) { + delete(u.u, key) +} + +// ForEach iterates over u and executes f for each element that is 'todo' and sets it to 'done'. +func (u U) ForEach() error { + for k, v := range u.u { + if v.state == todo { + v.f() + } + v.state = done + u.u[k] = v + } + return nil +} + +const ( + todo = 1 + done = 2 +) diff --git a/ag_201_coredns/plugin/pkg/uniq/uniq_test.go b/ag_201_coredns/plugin/pkg/uniq/uniq_test.go new file mode 100644 index 0000000..5d58c92 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/uniq/uniq_test.go @@ -0,0 +1,17 @@ +package uniq + +import "testing" + +func TestForEach(t *testing.T) { + u, i := New(), 0 + u.Set("test", func() error { i++; return nil }) + + u.ForEach() + if i != 1 { + t.Errorf("Failed to executed f for %s", "test") + } + u.ForEach() + if i != 1 { + t.Errorf("Executed f twice instead of once") + } +} diff --git a/ag_201_coredns/plugin/pkg/up/up.go b/ag_201_coredns/plugin/pkg/up/up.go new file mode 100644 index 0000000..6f18ffb --- /dev/null +++ b/ag_201_coredns/plugin/pkg/up/up.go @@ -0,0 +1,83 @@ +// Package up is used to run a function for some duration. If a new function is added while a previous run is +// still ongoing, nothing new will be executed. +package up + +import ( + "sync" + "time" +) + +// Probe is used to run a single Func until it returns true (indicating a target is healthy). If an Func +// is already in progress no new one will be added, i.e. there is always a maximum of 1 checks in flight. +// +// There is a tradeoff to be made in figuring out quickly that an upstream is healthy and not doing to much work +// (sending queries) to find that out. Having some kind of exp. backoff here won't help much, because you don't won't +// to backoff too much. You then also need random queries to be perfomed every so often to quickly detect a working +// upstream. In the end we just send a query every 0.5 second to check the upstream. This hopefully strikes a balance +// between getting information about the upstream state quickly and not doing too much work. Note that 0.5s is still an +// eternity in DNS, so we may actually want to shorten it. +type Probe struct { + sync.Mutex + inprogress int + interval time.Duration +} + +// Func is used to determine if a target is alive. If so this function must return nil. +type Func func() error + +// New returns a pointer to an initialized Probe. +func New() *Probe { return &Probe{} } + +// Do will probe target, if a probe is already in progress this is a noop. +func (p *Probe) Do(f Func) { + p.Lock() + if p.inprogress != idle { + p.Unlock() + return + } + p.inprogress = active + interval := p.interval + p.Unlock() + // Passed the lock. Now run f for as long it returns false. If a true is returned + // we return from the goroutine and we can accept another Func to run. + go func() { + i := 1 + for { + if err := f(); err == nil { + break + } + time.Sleep(interval) + p.Lock() + if p.inprogress == stop { + p.Unlock() + return + } + p.Unlock() + i++ + } + + p.Lock() + p.inprogress = idle + p.Unlock() + }() +} + +// Stop stops the probing. +func (p *Probe) Stop() { + p.Lock() + p.inprogress = stop + p.Unlock() +} + +// Start will initialize the probe manager, after which probes can be initiated with Do. +func (p *Probe) Start(interval time.Duration) { + p.Lock() + p.interval = interval + p.Unlock() +} + +const ( + idle = iota + active + stop +) diff --git a/ag_201_coredns/plugin/pkg/up/up_test.go b/ag_201_coredns/plugin/pkg/up/up_test.go new file mode 100644 index 0000000..eeaecea --- /dev/null +++ b/ag_201_coredns/plugin/pkg/up/up_test.go @@ -0,0 +1,40 @@ +package up + +import ( + "sync" + "sync/atomic" + "testing" + "time" +) + +func TestUp(t *testing.T) { + pr := New() + wg := sync.WaitGroup{} + hits := int32(0) + + upfunc := func() error { + atomic.AddInt32(&hits, 1) + // Sleep tiny amount so that our other pr.Do() calls hit the lock. + time.Sleep(3 * time.Millisecond) + wg.Done() + return nil + } + + pr.Start(5 * time.Millisecond) + defer pr.Stop() + + // These functions AddInt32 to the same hits variable, but we only want to wait when + // upfunc finishes, as that only calls Done() on the waitgroup. + upfuncNoWg := func() error { atomic.AddInt32(&hits, 1); return nil } + wg.Add(1) + pr.Do(upfunc) + pr.Do(upfuncNoWg) + pr.Do(upfuncNoWg) + + wg.Wait() + + h := atomic.LoadInt32(&hits) + if h != 1 { + t.Errorf("Expected hits to be %d, got %d", 1, h) + } +} diff --git a/ag_201_coredns/plugin/pkg/upstream/upstream.go b/ag_201_coredns/plugin/pkg/upstream/upstream.go new file mode 100644 index 0000000..b531b70 --- /dev/null +++ b/ag_201_coredns/plugin/pkg/upstream/upstream.go @@ -0,0 +1,35 @@ +// Package upstream abstracts a upstream lookups so that plugins can handle them in an unified way. +package upstream + +import ( + "context" + "fmt" + + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin/pkg/nonwriter" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Upstream is used to resolve CNAME or other external targets via CoreDNS itself. +type Upstream struct{} + +// New creates a new Upstream to resolve names using the coredns process. +func New() *Upstream { return &Upstream{} } + +// Lookup routes lookups to our selves to make it follow the plugin chain *again*, but with a (possibly) new query. As +// we are doing the query against ourselves again, there is no actual new hop, as such RFC 6891 does not apply and we +// need the EDNS0 option present in the *original* query to be present here too. +func (u *Upstream) Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) { + server, ok := ctx.Value(dnsserver.Key{}).(*dnsserver.Server) + if !ok { + return nil, fmt.Errorf("no full server is running") + } + req := state.NewWithQuestion(name, typ) + + nw := nonwriter.New(state.W) + server.ServeDNS(ctx, nw, req.Req) + + return nw.Msg, nil +} diff --git a/ag_201_coredns/plugin/plugin.go b/ag_201_coredns/plugin/plugin.go new file mode 100644 index 0000000..51f5ba7 --- /dev/null +++ b/ag_201_coredns/plugin/plugin.go @@ -0,0 +1,112 @@ +// Package plugin provides some types and functions common among plugin. +package plugin + +import ( + "context" + "errors" + "fmt" + + "github.com/miekg/dns" + ot "github.com/opentracing/opentracing-go" + "github.com/prometheus/client_golang/prometheus" +) + +type ( + // Plugin is a middle layer which represents the traditional + // idea of plugin: it chains one Handler to the next by being + // passed the next Handler in the chain. + Plugin func(Handler) Handler + + // Handler is like dns.Handler except ServeDNS may return an rcode + // and/or error. + // + // If ServeDNS writes to the response body, it should return a status + // code. CoreDNS assumes *no* reply has yet been written if the status + // code is one of the following: + // + // * SERVFAIL (dns.RcodeServerFailure) + // + // * REFUSED (dns.RecodeRefused) + // + // * FORMERR (dns.RcodeFormatError) + // + // * NOTIMP (dns.RcodeNotImplemented) + // + // All other response codes signal other handlers above it that the + // response message is already written, and that they should not write + // to it also. + // + // If ServeDNS encounters an error, it should return the error value + // so it can be logged by designated error-handling plugin. + // + // If writing a response after calling another ServeDNS method, the + // returned rcode SHOULD be used when writing the response. + // + // If handling errors after calling another ServeDNS method, the + // returned error value SHOULD be logged or handled accordingly. + // + // Otherwise, return values should be propagated down the plugin + // chain by returning them unchanged. + Handler interface { + ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) + Name() string + } + + // HandlerFunc is a convenience type like dns.HandlerFunc, except + // ServeDNS returns an rcode and an error. See Handler + // documentation for more information. + HandlerFunc func(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) +) + +// ServeDNS implements the Handler interface. +func (f HandlerFunc) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + return f(ctx, w, r) +} + +// Name implements the Handler interface. +func (f HandlerFunc) Name() string { return "handlerfunc" } + +// Error returns err with 'plugin/name: ' prefixed to it. +func Error(name string, err error) error { return fmt.Errorf("%s/%s: %s", "plugin", name, err) } + +// NextOrFailure calls next.ServeDNS when next is not nil, otherwise it will return, a ServerFailure and a `no next plugin found` error. +func NextOrFailure(name string, next Handler, ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { // nolint: golint + if next != nil { + if span := ot.SpanFromContext(ctx); span != nil { + child := span.Tracer().StartSpan(next.Name(), ot.ChildOf(span.Context())) + defer child.Finish() + ctx = ot.ContextWithSpan(ctx, child) + } + return next.ServeDNS(ctx, w, r) + } + + return dns.RcodeServerFailure, Error(name, errors.New("no next plugin found")) +} + +// ClientWrite returns true if the response has been written to the client. +// Each plugin to adhere to this protocol. +func ClientWrite(rcode int) bool { + switch rcode { + case dns.RcodeServerFailure: + fallthrough + case dns.RcodeRefused: + fallthrough + case dns.RcodeFormatError: + fallthrough + case dns.RcodeNotImplemented: + return false + } + return true +} + +// Namespace is the namespace used for the metrics. +const Namespace = "coredns" + +// TimeBuckets is based on Prometheus client_golang prometheus.DefBuckets +var TimeBuckets = prometheus.ExponentialBuckets(0.00025, 2, 16) // from 0.25ms to 8 seconds + +// SlimTimeBuckets is low cardinality set of duration buckets. +var SlimTimeBuckets = prometheus.ExponentialBuckets(0.00025, 10, 5) // from 0.25ms to 2.5 seconds + +// ErrOnce is returned when a plugin doesn't support multiple setups per server. +var ErrOnce = errors.New("this plugin can only be used once per Server Block") diff --git a/ag_201_coredns/plugin/pprof/README.md b/ag_201_coredns/plugin/pprof/README.md new file mode 100644 index 0000000..c63d152 --- /dev/null +++ b/ag_201_coredns/plugin/pprof/README.md @@ -0,0 +1,74 @@ +# pprof + +## Name + +*pprof* - publishes runtime profiling data at endpoints under `/debug/pprof`. + +## Description + +You can visit `/debug/pprof` on your site for an index of the available endpoints. By default it +will listen on localhost:6053. + +This is a debugging tool. Certain requests (such as collecting execution traces) can be slow. If +you use pprof on a live server, consider restricting access or enabling it only temporarily. + +This plugin can only be used once per Server Block. + +## Syntax + +~~~ txt +pprof [ADDRESS] +~~~ + +Optionally pprof takes an address; the default is `localhost:6053`. + +An extra option can be set with this extended syntax: + +~~~ txt +pprof [ADDRESS] { + block [RATE] +} +~~~ + +* `block` option enables block profiling, **RATE** defaults to 1. **RATE** must be a positive value. + See [Diagnostics, chapter profiling](https://golang.org/doc/diagnostics.html) and + [runtime.SetBlockProfileRate](https://golang.org/pkg/runtime/#SetBlockProfileRate) for what block + profiling entails. + +## Examples + +Enable a pprof endpoint: + +~~~ +. { + pprof +} +~~~ + +And use the pprof tool to get statistics: `go tool pprof http://localhost:6053`. + +Listen on an alternate address: + +~~~ txt +. { + pprof 10.9.8.7:6060 +} +~~~ + +Listen on an all addresses on port 6060, and enable block profiling + +~~~ txt +. { + pprof :6060 { + block + } +} +~~~ + +## See Also + +See [Go's pprof documentation](https://golang.org/pkg/net/http/pprof/) and [Profiling Go +Programs](https://blog.golang.org/profiling-go-programs). + +See [runtime.SetBlockProfileRate](https://golang.org/pkg/runtime/#SetBlockProfileRate) for +background on block profiling. diff --git a/ag_201_coredns/plugin/pprof/log_test.go b/ag_201_coredns/plugin/pprof/log_test.go new file mode 100644 index 0000000..7e2c252 --- /dev/null +++ b/ag_201_coredns/plugin/pprof/log_test.go @@ -0,0 +1,5 @@ +package pprof + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/pprof/pprof.go b/ag_201_coredns/plugin/pprof/pprof.go new file mode 100644 index 0000000..822e6e2 --- /dev/null +++ b/ag_201_coredns/plugin/pprof/pprof.go @@ -0,0 +1,60 @@ +// Package pprof implements a debug endpoint for getting profiles using the +// go pprof tooling. +package pprof + +import ( + "net" + "net/http" + pp "net/http/pprof" + "runtime" + + "github.com/coredns/coredns/plugin/pkg/reuseport" +) + +type handler struct { + addr string + rateBloc int + ln net.Listener + mux *http.ServeMux +} + +func (h *handler) Startup() error { + // Reloading the plugin without changing the listening address results + // in an error unless we reuse the port because Startup is called for + // new handlers before Shutdown is called for the old ones. + ln, err := reuseport.Listen("tcp", h.addr) + if err != nil { + log.Errorf("Failed to start pprof handler: %s", err) + return err + } + + h.ln = ln + + h.mux = http.NewServeMux() + h.mux.HandleFunc(path, func(rw http.ResponseWriter, req *http.Request) { + http.Redirect(rw, req, path+"/", http.StatusFound) + }) + h.mux.HandleFunc(path+"/", pp.Index) + h.mux.HandleFunc(path+"/cmdline", pp.Cmdline) + h.mux.HandleFunc(path+"/profile", pp.Profile) + h.mux.HandleFunc(path+"/symbol", pp.Symbol) + h.mux.HandleFunc(path+"/trace", pp.Trace) + + runtime.SetBlockProfileRate(h.rateBloc) + + go func() { + http.Serve(h.ln, h.mux) + }() + return nil +} + +func (h *handler) Shutdown() error { + if h.ln != nil { + return h.ln.Close() + } + return nil +} + +const ( + path = "/debug/pprof" +) diff --git a/ag_201_coredns/plugin/pprof/setup.go b/ag_201_coredns/plugin/pprof/setup.go new file mode 100644 index 0000000..3505b5d --- /dev/null +++ b/ag_201_coredns/plugin/pprof/setup.go @@ -0,0 +1,65 @@ +package pprof + +import ( + "net" + "strconv" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" +) + +var log = clog.NewWithPlugin("pprof") + +const defaultAddr = "localhost:6053" + +func init() { plugin.Register("pprof", setup) } + +func setup(c *caddy.Controller) error { + h := &handler{addr: defaultAddr} + + i := 0 + for c.Next() { + if i > 0 { + return plugin.Error("pprof", plugin.ErrOnce) + } + i++ + + args := c.RemainingArgs() + if len(args) == 1 { + h.addr = args[0] + _, _, e := net.SplitHostPort(h.addr) + if e != nil { + return plugin.Error("pprof", c.Errf("%v", e)) + } + } + + if len(args) > 1 { + return plugin.Error("pprof", c.ArgErr()) + } + + for c.NextBlock() { + switch c.Val() { + case "block": + args := c.RemainingArgs() + if len(args) > 1 { + return plugin.Error("pprof", c.ArgErr()) + } + h.rateBloc = 1 + if len(args) > 0 { + t, err := strconv.Atoi(args[0]) + if err != nil { + return plugin.Error("pprof", c.Errf("property '%s' invalid integer value '%v'", "block", args[0])) + } + h.rateBloc = t + } + default: + return plugin.Error("pprof", c.Errf("unknown property '%s'", c.Val())) + } + } + } + + c.OnStartup(h.Startup) + c.OnShutdown(h.Shutdown) + return nil +} diff --git a/ag_201_coredns/plugin/pprof/setup_test.go b/ag_201_coredns/plugin/pprof/setup_test.go new file mode 100644 index 0000000..500a400 --- /dev/null +++ b/ag_201_coredns/plugin/pprof/setup_test.go @@ -0,0 +1,44 @@ +package pprof + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestPProf(t *testing.T) { + tests := []struct { + input string + shouldErr bool + }{ + {`pprof`, false}, + {`pprof 1.2.3.4:1234`, false}, + {`pprof :1234`, false}, + {`pprof :1234 -1`, true}, + {`pprof { + }`, false}, + {`pprof /foo`, true}, + {`pprof { + a b + }`, true}, + {`pprof { block + }`, false}, + {`pprof :1234 { + block 20 + }`, false}, + {`pprof { + block 20 30 + }`, true}, + {`pprof + pprof`, true}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + err := setup(c) + if test.shouldErr && err == nil { + t.Errorf("Test %v: Expected error but found nil", i) + } else if !test.shouldErr && err != nil { + t.Errorf("Test %v: Expected no error but found error: %v", i, err) + } + } +} diff --git a/ag_201_coredns/plugin/ready/README.md b/ag_201_coredns/plugin/ready/README.md new file mode 100644 index 0000000..d2e430d --- /dev/null +++ b/ag_201_coredns/plugin/ready/README.md @@ -0,0 +1,58 @@ +# ready + +## Name + +*ready* - enables a readiness check HTTP endpoint. + +## Description + +By enabling *ready* an HTTP endpoint on port 8181 will return 200 OK, when all plugins that are able +to signal readiness have done so. If some are not ready yet the endpoint will return a 503 with the +body containing the list of plugins that are not ready. Once a plugin has signaled it is ready it +will not be queried again. + +Each Server Block that enables the *ready* plugin will have the plugins *in that server block* +report readiness into the /ready endpoint that runs on the same port. This also means that the +*same* plugin with different configurations (in potentially *different* Server Blocks) will have +their readiness reported as the union of their respective readinesses. + +## Syntax + +~~~ +ready [ADDRESS] +~~~ + +*ready* optionally takes an address; the default is `:8181`. The path is fixed to `/ready`. The +readiness endpoint returns a 200 response code and the word "OK" when this server is ready. It +returns a 503 otherwise *and* the list of plugins that are not ready. + +## Plugins + +Any plugin wanting to signal readiness will need to implement the `ready.Readiness` interface by +implementing a method `Ready() bool` that returns true when the plugin is ready and false otherwise. + +## Examples + +Let *ready* report readiness for both the `.` and `example.org` servers (assuming the *whois* +plugin also exports readiness): + +~~~ txt +. { + ready + erratic +} + +example.org { + ready + whoami +} + +~~~ + +Run *ready* on a different port. + +~~~ txt +. { + ready localhost:8091 +} +~~~ diff --git a/ag_201_coredns/plugin/ready/list.go b/ag_201_coredns/plugin/ready/list.go new file mode 100644 index 0000000..c246287 --- /dev/null +++ b/ag_201_coredns/plugin/ready/list.go @@ -0,0 +1,56 @@ +package ready + +import ( + "sort" + "strings" + "sync" +) + +// list is a structure that holds the plugins that signals readiness for this server block. +type list struct { + sync.RWMutex + rs []Readiness + names []string +} + +// Reset resets l +func (l *list) Reset() { + l.Lock() + defer l.Unlock() + l.rs = nil + l.names = nil +} + +// Append adds a new readiness to l. +func (l *list) Append(r Readiness, name string) { + l.Lock() + defer l.Unlock() + l.rs = append(l.rs, r) + l.names = append(l.names, name) +} + +// Ready return true when all plugins ready, if the returned value is false the string +// contains a comma separated list of plugins that are not ready. +func (l *list) Ready() (bool, string) { + l.RLock() + defer l.RUnlock() + ok := true + s := []string{} + for i, r := range l.rs { + if r == nil { + continue + } + if !r.Ready() { + ok = false + s = append(s, l.names[i]) + } else { + // if ok, this plugin is ready and will not be queried anymore. + l.rs[i] = nil + } + } + if ok { + return true, "" + } + sort.Strings(s) + return false, strings.Join(s, ",") +} diff --git a/ag_201_coredns/plugin/ready/readiness.go b/ag_201_coredns/plugin/ready/readiness.go new file mode 100644 index 0000000..7aca5df --- /dev/null +++ b/ag_201_coredns/plugin/ready/readiness.go @@ -0,0 +1,7 @@ +package ready + +// The Readiness interface needs to be implemented by each plugin willing to provide a readiness check. +type Readiness interface { + // Ready is called by ready to see whether the plugin is ready. + Ready() bool +} diff --git a/ag_201_coredns/plugin/ready/ready.go b/ag_201_coredns/plugin/ready/ready.go new file mode 100644 index 0000000..2002e4a --- /dev/null +++ b/ag_201_coredns/plugin/ready/ready.go @@ -0,0 +1,81 @@ +// Package ready is used to signal readiness of the CoreDNS process. Once all +// plugins have called in the plugin will signal readiness by returning a 200 +// OK on the HTTP handler (on port 8181). If not ready yet, the handler will +// return a 503. +package ready + +import ( + "io" + "net" + "net/http" + "sync" + + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/reuseport" + "github.com/coredns/coredns/plugin/pkg/uniq" +) + +var ( + log = clog.NewWithPlugin("ready") + plugins = &list{} + uniqAddr = uniq.New() +) + +type ready struct { + Addr string + + sync.RWMutex + ln net.Listener + done bool + mux *http.ServeMux +} + +func (rd *ready) onStartup() error { + ln, err := reuseport.Listen("tcp", rd.Addr) + if err != nil { + return err + } + + rd.Lock() + rd.ln = ln + rd.mux = http.NewServeMux() + rd.done = true + rd.Unlock() + + rd.mux.HandleFunc("/ready", func(w http.ResponseWriter, _ *http.Request) { + rd.Lock() + defer rd.Unlock() + if !rd.done { + w.WriteHeader(http.StatusServiceUnavailable) + io.WriteString(w, "Shutting down") + return + } + ok, todo := plugins.Ready() + if ok { + w.WriteHeader(http.StatusOK) + io.WriteString(w, http.StatusText(http.StatusOK)) + return + } + log.Infof("Still waiting on: %q", todo) + w.WriteHeader(http.StatusServiceUnavailable) + io.WriteString(w, todo) + }) + + go func() { http.Serve(rd.ln, rd.mux) }() + + return nil +} + +func (rd *ready) onFinalShutdown() error { + rd.Lock() + defer rd.Unlock() + if !rd.done { + return nil + } + + uniqAddr.Unset(rd.Addr) + + rd.ln.Close() + rd.done = false + return nil +} diff --git a/ag_201_coredns/plugin/ready/ready_test.go b/ag_201_coredns/plugin/ready/ready_test.go new file mode 100644 index 0000000..fff19cc --- /dev/null +++ b/ag_201_coredns/plugin/ready/ready_test.go @@ -0,0 +1,69 @@ +package ready + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/coredns/coredns/plugin/erratic" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func init() { clog.Discard() } + +func TestReady(t *testing.T) { + rd := &ready{Addr: ":0"} + e := &erratic.Erratic{} + plugins.Append(e, "erratic") + + if err := rd.onStartup(); err != nil { + t.Fatalf("Unable to startup the readiness server: %v", err) + } + + defer rd.onFinalShutdown() + + address := fmt.Sprintf("http://%s/ready", rd.ln.Addr().String()) + + response, err := http.Get(address) + if err != nil { + t.Fatalf("Unable to query %s: %v", address, err) + } + if response.StatusCode != 503 { + t.Errorf("Invalid status code: expecting %d, got %d", 503, response.StatusCode) + } + response.Body.Close() + + // make it ready by giving erratic 3 queries. + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m) + e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m) + e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m) + + response, err = http.Get(address) + if err != nil { + t.Fatalf("Unable to query %s: %v", address, err) + } + if response.StatusCode != 200 { + t.Errorf("Invalid status code: expecting %d, got %d", 200, response.StatusCode) + } + response.Body.Close() + + // make erratic not-ready by giving it more queries, this should not change the process readiness + e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m) + e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m) + e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m) + + response, err = http.Get(address) + if err != nil { + t.Fatalf("Unable to query %s: %v", address, err) + } + if response.StatusCode != 200 { + t.Errorf("Invalid status code: expecting %d, got %d", 200, response.StatusCode) + } + response.Body.Close() +} diff --git a/ag_201_coredns/plugin/ready/setup.go b/ag_201_coredns/plugin/ready/setup.go new file mode 100644 index 0000000..e5657f6 --- /dev/null +++ b/ag_201_coredns/plugin/ready/setup.go @@ -0,0 +1,73 @@ +package ready + +import ( + "net" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("ready", setup) } + +func setup(c *caddy.Controller) error { + addr, err := parse(c) + if err != nil { + return plugin.Error("ready", err) + } + rd := &ready{Addr: addr} + + uniqAddr.Set(addr, rd.onStartup) + c.OnStartup(func() error { uniqAddr.Set(addr, rd.onStartup); return nil }) + c.OnRestartFailed(func() error { uniqAddr.Set(addr, rd.onStartup); return nil }) + + c.OnStartup(func() error { return uniqAddr.ForEach() }) + c.OnRestartFailed(func() error { return uniqAddr.ForEach() }) + + c.OnStartup(func() error { + plugins.Reset() + for _, p := range dnsserver.GetConfig(c).Handlers() { + if r, ok := p.(Readiness); ok { + plugins.Append(r, p.Name()) + } + } + return nil + }) + c.OnRestartFailed(func() error { + for _, p := range dnsserver.GetConfig(c).Handlers() { + if r, ok := p.(Readiness); ok { + plugins.Append(r, p.Name()) + } + } + return nil + }) + + c.OnRestart(rd.onFinalShutdown) + c.OnFinalShutdown(rd.onFinalShutdown) + + return nil +} + +func parse(c *caddy.Controller) (string, error) { + addr := ":8181" + i := 0 + for c.Next() { + if i > 0 { + return "", plugin.ErrOnce + } + i++ + args := c.RemainingArgs() + + switch len(args) { + case 0: + case 1: + addr = args[0] + if _, _, e := net.SplitHostPort(addr); e != nil { + return "", e + } + default: + return "", c.ArgErr() + } + } + return addr, nil +} diff --git a/ag_201_coredns/plugin/ready/setup_test.go b/ag_201_coredns/plugin/ready/setup_test.go new file mode 100644 index 0000000..1dd0d4a --- /dev/null +++ b/ag_201_coredns/plugin/ready/setup_test.go @@ -0,0 +1,34 @@ +package ready + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupReady(t *testing.T) { + tests := []struct { + input string + shouldErr bool + }{ + {`ready`, false}, + {`ready localhost:1234`, false}, + {`ready localhost:1234 b`, true}, + {`ready bla`, true}, + {`ready bla bla`, true}, + } + + for i, test := range tests { + _, err := parse(caddy.NewTestController("dns", test.input)) + + 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) + } + } + } +} diff --git a/ag_201_coredns/plugin/register.go b/ag_201_coredns/plugin/register.go new file mode 100644 index 0000000..16090ff --- /dev/null +++ b/ag_201_coredns/plugin/register.go @@ -0,0 +1,11 @@ +package plugin + +import "github.com/coredns/caddy" + +// Register registers your plugin with CoreDNS and allows it to be called when the server is running. +func Register(name string, action caddy.SetupFunc) { + caddy.RegisterPlugin(name, caddy.Plugin{ + ServerType: "dns", + Action: action, + }) +} diff --git a/ag_201_coredns/plugin/reload/README.md b/ag_201_coredns/plugin/reload/README.md new file mode 100644 index 0000000..1288a23 --- /dev/null +++ b/ag_201_coredns/plugin/reload/README.md @@ -0,0 +1,108 @@ +# reload + +## Name + +*reload* - allows automatic reload of a changed Corefile. + +## Description + +This plugin allows automatic reload of a changed _Corefile_. +To enable automatic reloading of _zone file_ changes, use the `auto` plugin. + +This plugin periodically checks if the Corefile has changed by reading +it and calculating its SHA512 checksum. If the file has changed, it reloads +CoreDNS with the new Corefile. This eliminates the need to send a SIGHUP +or SIGUSR1 after changing the Corefile. + +The reloads are graceful - you should not see any loss of service when the +reload happens. Even if the new Corefile has an error, CoreDNS will continue +to run the old config and an error message will be printed to the log. But see +the Bugs section for failure modes. + +In some environments (for example, Kubernetes), there may be many CoreDNS +instances that started very near the same time and all share a common +Corefile. To prevent these all from reloading at the same time, some +jitter is added to the reload check interval. This is jitter from the +perspective of multiple CoreDNS instances; each instance still checks on a +regular interval, but all of these instances will have their reloads spread +out across the jitter duration. This isn't strictly necessary given that the +reloads are graceful, and can be disabled by setting the jitter to `0s`. + +Jitter is re-calculated whenever the Corefile is reloaded. + +This plugin can only be used once per Server Block. + +## Syntax + +~~~ txt +reload [INTERVAL] [JITTER] +~~~ + +The plugin will check for changes every **INTERVAL**, subject to +/- the **JITTER** duration. + +* **INTERVAL** and **JITTER** are Golang [durations](https://golang.org/pkg/time/#ParseDuration). + The default **INTERVAL** is 30s, default **JITTER** is 15s, the minimal value for **INTERVAL** + is 2s, and for **JITTER** it is 1s. If **JITTER** is more than half of **INTERVAL**, it will be + set to half of **INTERVAL** + +## Examples + +Check with the default intervals: + +~~~ corefile +. { + reload + erratic +} +~~~ + +Check every 10 seconds (jitter is automatically set to 10 / 2 = 5 in this case): + +~~~ corefile +. { + reload 10s + erratic +} +~~~ + +## Bugs + +The reload happens without data loss (i.e. DNS queries keep flowing), but there is a corner case +where the reload fails, and you loose functionality. Consider the following Corefile: + +~~~ txt +. { + health :8080 + whoami +} +~~~ + +CoreDNS starts and serves health from :8080. Now you change `:8080` to `:443` not knowing a process +is already listening on that port. The process reloads and performs the following steps: + +1. close the listener on 8080 +2. reload and parse the config again +3. fail to start a new listener on 443 +4. fail loading the new Corefile, abort and keep using the old process + +After the aborted attempt to reload we are left with the old processes running, but the listener is +closed in step 1; so the health endpoint is broken. The same can happen in the prometheus plugin. + +In general be careful with assigning new port and expecting reload to work fully. + +In CoreDNS v1.6.0 and earlier any `import` statements are not discovered by this plugin. +This means if any of these imported files changes the *reload* plugin is ignorant of that fact. +CoreDNS v1.7.0 and later does parse the Corefile and supports detecting changes in imported files. + +## Metrics + + If monitoring is enabled (via the *prometheus* plugin) then the following metric is exported: + +* `coredns_reload_failed_total{}` - counts the number of failed reload attempts. +* `coredns_reload_version_info{hash, value}` - record the hash value during reload. + +Currently the type of `hash` is "sha512", the `value` is the returned hash value. + +## See Also + +See coredns-import(7) and corefile(5). diff --git a/ag_201_coredns/plugin/reload/log_test.go b/ag_201_coredns/plugin/reload/log_test.go new file mode 100644 index 0000000..2f6598d --- /dev/null +++ b/ag_201_coredns/plugin/reload/log_test.go @@ -0,0 +1,5 @@ +package reload + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/reload/metrics.go b/ag_201_coredns/plugin/reload/metrics.go new file mode 100644 index 0000000..7224791 --- /dev/null +++ b/ag_201_coredns/plugin/reload/metrics.go @@ -0,0 +1,26 @@ +package reload + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// Metrics for the reload plugin +var ( + // failedCount is the counter of the number of failed reload attempts. + failedCount = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "reload", + Name: "failed_total", + Help: "Counter of the number of failed reload attempts.", + }) + // reloadInfo is record the hash value during reload. + reloadInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: plugin.Namespace, + Subsystem: "reload", + Name: "version_info", + Help: "A metric with a constant '1' value labeled by hash, and value which type of hash generated.", + }, []string{"hash", "value"}) +) diff --git a/ag_201_coredns/plugin/reload/reload.go b/ag_201_coredns/plugin/reload/reload.go new file mode 100644 index 0000000..632c036 --- /dev/null +++ b/ag_201_coredns/plugin/reload/reload.go @@ -0,0 +1,127 @@ +// Package reload periodically checks if the Corefile has changed, and reloads if so. +package reload + +import ( + "bytes" + "crypto/sha512" + "encoding/hex" + "encoding/json" + "sync" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/caddy/caddyfile" + + "github.com/prometheus/client_golang/prometheus" +) + +const ( + unused = 0 + maybeUsed = 1 + used = 2 +) + +type reload struct { + dur time.Duration + u int + mtx sync.RWMutex + quit chan bool +} + +func (r *reload) setUsage(u int) { + r.mtx.Lock() + defer r.mtx.Unlock() + r.u = u +} + +func (r *reload) usage() int { + r.mtx.RLock() + defer r.mtx.RUnlock() + return r.u +} + +func (r *reload) setInterval(i time.Duration) { + r.mtx.Lock() + defer r.mtx.Unlock() + r.dur = i +} + +func (r *reload) interval() time.Duration { + r.mtx.RLock() + defer r.mtx.RUnlock() + return r.dur +} + +func parse(corefile caddy.Input) ([]byte, error) { + serverBlocks, err := caddyfile.Parse(corefile.Path(), bytes.NewReader(corefile.Body()), nil) + if err != nil { + return nil, err + } + return json.Marshal(serverBlocks) +} + +func hook(event caddy.EventName, info interface{}) error { + if event != caddy.InstanceStartupEvent { + return nil + } + // if reload is removed from the Corefile, then the hook + // is still registered but setup is never called again + // so we need a flag to tell us not to reload + if r.usage() == unused { + return nil + } + + // this should be an instance. ok to panic if not + instance := info.(*caddy.Instance) + parsedCorefile, err := parse(instance.Caddyfile()) + if err != nil { + return err + } + + sha512sum := sha512.Sum512(parsedCorefile) + log.Infof("Running configuration SHA512 = %x\n", sha512sum) + + go func() { + tick := time.NewTicker(r.interval()) + + for { + select { + case <-tick.C: + corefile, err := caddy.LoadCaddyfile(instance.Caddyfile().ServerType()) + if err != nil { + continue + } + parsedCorefile, err := parse(corefile) + if err != nil { + log.Warningf("Corefile parse failed: %s", err) + continue + } + s := sha512.Sum512(parsedCorefile) + if s != sha512sum { + reloadInfo.Delete(prometheus.Labels{"hash": "sha512", "value": hex.EncodeToString(sha512sum[:])}) + // Let not try to restart with the same file, even though it is wrong. + sha512sum = s + // now lets consider that plugin will not be reload, unless appear in next config file + // change status of usage will be reset in setup if the plugin appears in config file + r.setUsage(maybeUsed) + _, err := instance.Restart(corefile) + reloadInfo.WithLabelValues("sha512", hex.EncodeToString(sha512sum[:])).Set(1) + if err != nil { + log.Errorf("Corefile changed but reload failed: %s", err) + failedCount.Add(1) + continue + } + // we are done, if the plugin was not set used, then it is not. + if r.usage() == maybeUsed { + r.setUsage(unused) + } + return + } + case <-r.quit: + return + } + } + }() + + return nil +} diff --git a/ag_201_coredns/plugin/reload/setup.go b/ag_201_coredns/plugin/reload/setup.go new file mode 100644 index 0000000..6df3234 --- /dev/null +++ b/ag_201_coredns/plugin/reload/setup.go @@ -0,0 +1,87 @@ +package reload + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" +) + +var log = clog.NewWithPlugin("reload") + +func init() { plugin.Register("reload", setup) } + +// the info reload is global to all application, whatever number of reloads. +// it is used to transmit data between Setup and start of the hook called 'onInstanceStartup' +// channel for QUIT is never changed in purpose. +// WARNING: this data may be unsync after an invalid attempt of reload Corefile. +var ( + r = reload{dur: defaultInterval, u: unused, quit: make(chan bool)} + once, shutOnce sync.Once +) + +func setup(c *caddy.Controller) error { + c.Next() // 'reload' + args := c.RemainingArgs() + + if len(args) > 2 { + return plugin.Error("reload", c.ArgErr()) + } + + i := defaultInterval + if len(args) > 0 { + d, err := time.ParseDuration(args[0]) + if err != nil { + return plugin.Error("reload", err) + } + i = d + } + if i < minInterval { + return plugin.Error("reload", fmt.Errorf("interval value must be greater or equal to %v", minInterval)) + } + + j := defaultJitter + if len(args) > 1 { + d, err := time.ParseDuration(args[1]) + if err != nil { + return plugin.Error("reload", err) + } + j = d + } + if j < minJitter { + return plugin.Error("reload", fmt.Errorf("jitter value must be greater or equal to %v", minJitter)) + } + + if j > i/2 { + j = i / 2 + } + + jitter := time.Duration(rand.Int63n(j.Nanoseconds()) - (j.Nanoseconds() / 2)) + i = i + jitter + + // prepare info for next onInstanceStartup event + r.setInterval(i) + r.setUsage(used) + once.Do(func() { + caddy.RegisterEventHook("reload", hook) + }) + // re-register on finalShutDown as the instance most-likely will be changed + shutOnce.Do(func() { + c.OnFinalShutdown(func() error { + r.quit <- true + return nil + }) + }) + return nil +} + +const ( + minJitter = 1 * time.Second + minInterval = 2 * time.Second + defaultInterval = 30 * time.Second + defaultJitter = 15 * time.Second +) diff --git a/ag_201_coredns/plugin/reload/setup_test.go b/ag_201_coredns/plugin/reload/setup_test.go new file mode 100644 index 0000000..5450fae --- /dev/null +++ b/ag_201_coredns/plugin/reload/setup_test.go @@ -0,0 +1,51 @@ +package reload + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetupReload(t *testing.T) { + c := caddy.NewTestController("dns", `reload`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `reload 10s`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `reload 10s 2s`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `reload foo`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `reload 10s foo`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `reload 10s 5s foo`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } + c = caddy.NewTestController("dns", `reload 1s`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } + c = caddy.NewTestController("dns", `reload 0s`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } + c = caddy.NewTestController("dns", `reload 3s 0.5s`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } +} diff --git a/ag_201_coredns/plugin/rewrite/README.md b/ag_201_coredns/plugin/rewrite/README.md new file mode 100644 index 0000000..b460989 --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/README.md @@ -0,0 +1,406 @@ +# rewrite + +## Name + +*rewrite* - performs internal message rewriting. + +## Description + +Rewrites are invisible to the client. There are simple rewrites (fast) and complex rewrites +(slower), but they're powerful enough to accommodate most dynamic back-end applications. + +## Syntax + +A simplified/easy-to-digest syntax for *rewrite* is... +~~~ +rewrite [continue|stop] FIELD [TYPE] [(FROM TO)|TTL] [OPTIONS] +~~~ + +* **FIELD** indicates what part of the request/response is being re-written. + + * `type` - the type field of the request will be rewritten. FROM/TO must be a DNS record type (`A`, `MX`, etc.); +e.g., to rewrite ANY queries to HINFO, use `rewrite type ANY HINFO`. + * `name` - the query name in the _request_ is rewritten; by default this is a full match of the + name, e.g., `rewrite name example.net example.org`. Other match types are supported, see the **Name Field Rewrites** section below. + * `class` - the class of the message will be rewritten. FROM/TO must be a DNS class type (`IN`, `CH`, or `HS`); e.g., to rewrite CH queries to IN use `rewrite class CH IN`. + * `edns0` - an EDNS0 option can be appended to the request as described below in the **EDNS0 Options** section. + * `ttl` - the TTL value in the _response_ is rewritten. + +* **TYPE** this optional element can be specified for a `name` or `ttl` field. + If not given type `exact` will be assumed. If options should be specified the + type must be given. +* **FROM** is the name (exact, suffix, prefix, substring, or regex) or type to match +* **TO** is the destination name or type to rewrite to +* **TTL** is the number of seconds to set the TTL value to (only for field `ttl`) + +* **OPTIONS** + + for field `name` further options are possible controlling the response rewrites. + All name matching types support the following options + + * `answer auto` - the names in the _response_ is rewritten in a best effort manner. + * `answer name FROM TO` - the query name in the _response_ is rewritten matching the from regex pattern. + * `answer value FROM TO` - the names in the _response_ is rewritten matching the from regex pattern. + + See below in the **Response Rewrites** section for further details. + +If you specify multiple rules and an incoming query matches multiple rules, the rewrite +will behave as follows: + + * `continue` will continue applying the next rule in the rule list. + * `stop` will consider the current rule the last rule and will not continue. The default behaviour is `stop` + +## Examples + +### Name Field Rewrites + +The `rewrite` plugin offers the ability to match the name in the question section of +a DNS request. The match could be exact, a substring match, or based on a prefix, suffix, or regular +expression. If the newly used name is not a legal domain name, the plugin returns an error to the +client. + +The syntax for name rewriting is as follows: + +``` +rewrite [continue|stop] name [exact|prefix|suffix|substring|regex] STRING STRING [OPTIONS] +``` + +The match type, e.g., `exact`, `substring`, etc., triggers rewrite: + +* **exact** (default): on an exact match of the name in the question section of a request +* **substring**: on a partial match of the name in the question section of a request +* **prefix**: when the name begins with the matching string +* **suffix**: when the name ends with the matching string +* **regex**: when the name in the question section of a request matches a regular expression + +If the match type is omitted, the `exact` match type is assumed. If OPTIONS are +given, the type must be specified. + +The following instruction allows rewriting names in the query that +contain the substring `service.us-west-1.example.org`: + +``` +rewrite name substring service.us-west-1.example.org service.us-west-1.consul +``` + +Thus: + +* Incoming Request Name: `ftp.service.us-west-1.example.org` +* Rewritten Request Name: `ftp.service.us-west-1.consul` + +The following instruction uses regular expressions. Names in requests +matching the regular expression `(.*)-(us-west-1)\.example\.org` are replaced with +`{1}.service.{2}.consul`, where `{1}` and `{2}` are regular expression match groups. + +``` +rewrite name regex (.*)-(us-west-1)\.example\.org {1}.service.{2}.consul +``` + +Thus: + +* Incoming Request Name: `ftp-us-west-1.example.org` +* Rewritten Request Name: `ftp.service.us-west-1.consul` + +The following example rewrites the `schmoogle.com` suffix to `google.com`. + +~~~ +rewrite name suffix .schmoogle.com. .google.com. +~~~ + +### Response Rewrites + +When rewriting incoming DNS requests' names (field `name`), CoreDNS re-writes +the `QUESTION SECTION` +section of the requests. It may be necessary to rewrite the `ANSWER SECTION` of the +requests, because some DNS resolvers treat mismatches between the `QUESTION SECTION` +and `ANSWER SECTION` as a man-in-the-middle attack (MITM). + +For example, a user tries to resolve `ftp-us-west-1.coredns.rocks`. The +CoreDNS configuration file has the following rule: + +``` +rewrite name regex (.*)-(us-west-1)\.coredns\.rocks {1}.service.{2}.consul +``` + +CoreDNS rewrote the request from `ftp-us-west-1.coredns.rocks` to +`ftp.service.us-west-1.consul` and ultimately resolved it to 3 records. +The resolved records, in the `ANSWER SECTION` below, were not from `coredns.rocks`, but +rather from `service.us-west-1.consul`. + + +``` +$ dig @10.1.1.1 ftp-us-west-1.coredns.rocks + +;; QUESTION SECTION: +;ftp-us-west-1.coredns.rocks. IN A + +;; ANSWER SECTION: +ftp.service.us-west-1.consul. 0 IN A 10.10.10.10 +ftp.service.us-west-1.consul. 0 IN A 10.20.20.20 +ftp.service.us-west-1.consul. 0 IN A 10.30.30.30 +``` + +The above is a mismatch between the question asked and the answer provided. + +There are three possibilities to specify an answer rewrite: +- A rewrite can request a best effort answer rewrite by adding the option `answer auto`. +- A rewrite may specify a dedicated regex based response name rewrite with the + `answer name FROM TO` option. +- A regex based rewrite of record values like `CNAME`, `SRV`, etc, can be requested by + an `answer value FROM TO` option. + +Hereby FROM/TO follow the rules for the `regex` name rewrite syntax. + +#### Auto Response Name Rewrite + +The following configuration snippet allows for rewriting of the +`ANSWER SECTION` according to the rewrite of the `QUESTION SECTION`: + +``` + rewrite stop { + name suffix .coredns.rocks .service.consul answer auto + } +``` + +Any occurrence of the rewritten question in the answer is mapped +back to the original value before the rewrite. + +Please note that answers for rewrites of type `exact` are always rewritten. +For a `suffix` name rule `auto` leads to a reverse suffix response rewrite, +exchanging FROM and TO from the rewrite request. + +#### Explicit Response Name Rewrite + +The following configuration snippet allows for rewriting of the +`ANSWER SECTION`, provided that the `QUESTION SECTION` was rewritten: + +``` + rewrite stop { + name regex (.*)-(us-west-1)\.coredns\.rocks {1}.service.{2}.consul + answer name (.*)\.service\.(us-west-1)\.consul {1}-{2}.coredns.rocks + } +``` + +Now, the `ANSWER SECTION` matches the `QUESTION SECTION`: + +``` +$ dig @10.1.1.1 ftp-us-west-1.coredns.rocks + +;; QUESTION SECTION: +;ftp-us-west-1.coredns.rocks. IN A + +;; ANSWER SECTION: +ftp-us-west-1.coredns.rocks. 0 IN A 10.10.10.10 +ftp-us-west-1.coredns.rocks. 0 IN A 10.20.20.20 +ftp-us-west-1.coredns.rocks. 0 IN A 10.30.30.30 +``` + +#### Rewriting other Response Values + +It is also possible to rewrite other values returned in the DNS response records +(e.g. the server names returned in `SRV` and `MX` records). This can be enabled by adding +the `answer value FROM TO` option to a name rule as specified below. `answer value` takes a +regular expression and a rewrite name as parameters and works in the same way as the +`answer name` rule. + +Note that names in the `AUTHORITY SECTION` and `ADDITIONAL SECTION` will also be +rewritten following the specified rules. The names returned by the following +record types: `CNAME`, `DNAME`, `SOA`, `SRV`, `MX`, `NAPTR`, `NS`, `PTR` will be rewritten +if the `answer value` rule is specified. + +The syntax for the rewrite of DNS request and response is as follows: + +``` +rewrite [continue|stop] { + name regex STRING STRING + answer name STRING STRING + [answer value STRING STRING] +} +``` + +Note that the above syntax is strict. For response rewrites, only `name` +rules are allowed to match the question section. The answer rewrite must be +after the name, as in the syntax example. + +##### Example: PTR Response Value Rewrite + +The original response contains the domain `service.consul.` in the `VALUE` part +of the `ANSWER SECTION` + +``` +$ dig @10.1.1.1 30.30.30.10.in-addr.arpa PTR + +;; QUESTION SECTION: +;30.30.30.10.in-addr.arpa. IN PTR + +;; ANSWER SECTION: +30.30.30.10.in-addr.arpa. 60 IN PTR ftp-us-west-1.service.consul. +``` + +The following configuration snippet allows for rewriting of the value +in the `ANSWER SECTION`: + +``` + rewrite stop { + name suffix .arpa .arpa + answer name auto + answer value (.*)\.service\.consul\. {1}.coredns.rocks. + } +``` + +Now, the `VALUE` in the `ANSWER SECTION` has been overwritten in the domain part: + +``` +$ dig @10.1.1.1 30.30.30.10.in-addr.arpa PTR + +;; QUESTION SECTION: +;30.30.30.10.in-addr.arpa. IN PTR + +;; ANSWER SECTION: +30.30.30.10.in-addr.arpa. 60 IN PTR ftp-us-west-1.coredns.rocks. +``` + +#### Multiple Response Rewrites + +`name` and `value` rewrites can be chained by appending multiple answer rewrite +options. For all occurrences but the first one the keyword `answer` might be +omitted. + +```options +answer (auto | (name|value FROM TO)) { [answer] (auto | (name|value FROM TO)) } +``` + +For example: +``` +rewrite [continue|stop] name regex FROM TO answer name FROM TO [answer] value FROM TO +``` + +When using `exact` name rewrite rules, the answer gets rewritten automatically, +and there is no need to define `answer name auto`. But it is still possible to define +additional `answer value` and `answer value` options. + +The rule below rewrites the name in a request from `RED` to `BLUE`, and subsequently +rewrites the name in a corresponding response from `BLUE` to `RED`. The +client in the request would see only `RED` and no `BLUE`. + +``` +rewrite [continue|stop] name exact RED BLUE +``` + +### TTL Field Rewrites + +At times, the need to rewrite a TTL value could arise. For example, a DNS server +may not cache records with a TTL of zero (`0`). An administrator +may want to increase the TTL to ensure it is cached, e.g., by increasing it to 15 seconds. + +In the below example, the TTL in the answers for `coredns.rocks` domain are +being set to `15`: + +``` + rewrite continue { + ttl regex (.*)\.coredns\.rocks 15 + } +``` + +By the same token, an administrator may use this feature to prevent or limit caching by +setting the TTL value really low. + + +The syntax for the TTL rewrite rule is as follows. The meaning of +`exact|prefix|suffix|substring|regex` is the same as with the name rewrite rules. +An omitted type is defaulted to `exact`. + +``` +rewrite [continue|stop] ttl [exact|prefix|suffix|substring|regex] STRING [SECONDS|MIN-MAX] +``` + +It is possible to supply a range of TTL values in the `SECONDS` parameters instead of a single value. +If a range is supplied, the TTL value is set to `MIN` if it is below, or set to `MAX` if it is above. +The TTL value is left unchanged if it is already inside the provided range. +The ranges can be unbounded on either side. + +TTL examples with ranges: +``` +# rewrite TTL to be between 30s and 300s +rewrite ttl example.com. 30-300 + +# cap TTL at 30s +rewrite ttl example.com. -30 # equivalent to rewrite ttl example.com. 0-30 + +# increase TTL to a minimum of 30s +rewrite ttl example.com. 30- + +# set TTL to 30s +rewrite ttl example.com. 30 # equivalent to rewrite ttl example.com. 30-30 +``` + +## EDNS0 Options + +Using the FIELD edns0, you can set, append, or replace specific EDNS0 options in the request. + +* `replace` will modify any "matching" option with the specified option. The criteria for "matching" varies based on EDNS0 type. +* `append` will add the option only if no matching option exists +* `set` will modify a matching option or add one if none is found + +Currently supported are `EDNS0_LOCAL`, `EDNS0_NSID` and `EDNS0_SUBNET`. + +### EDNS0_LOCAL + +This has two fields, code and data. A match is defined as having the same code. Data may be a string or a variable. + +* A string data is treated as hex if it starts with `0x`. Example: + +~~~ corefile +. { + rewrite edns0 local set 0xffee 0x61626364 + whoami +} +~~~ + +rewrites the first local option with code 0xffee, setting the data to "abcd". This is equivalent to: + +~~~ corefile +. { + rewrite edns0 local set 0xffee abcd +} +~~~ + +* A variable data is specified with a pair of curly brackets `{}`. Following are the supported variables: + {qname}, {qtype}, {client_ip}, {client_port}, {protocol}, {server_ip}, {server_port}. + +* If the metadata plugin is enabled, then labels are supported as variables if they are presented within curly brackets. +The variable data will be replaced with the value associated with that label. If that label is not provided, +the variable will be silently substituted with an empty string. + +Examples: + +~~~ +rewrite edns0 local set 0xffee {client_ip} +~~~ + +The following example uses metadata and an imaginary "some-plugin" that would provide "some-label" as metadata information. + +~~~ +metadata +some-plugin +rewrite edns0 local set 0xffee {some-plugin/some-label} +~~~ + +### EDNS0_NSID + +This has no fields; it will add an NSID option with an empty string for the NSID. If the option already exists +and the action is `replace` or `set`, then the NSID in the option will be set to the empty string. + +### EDNS0_SUBNET + +This has two fields, IPv4 bitmask length and IPv6 bitmask length. The bitmask +length is used to extract the client subnet from the source IP address in the query. + +Example: + +~~~ +rewrite edns0 subnet set 24 56 +~~~ + +* If the query's source IP address is an IPv4 address, the first 24 bits in the IP will be the network subnet. +* If the query's source IP address is an IPv6 address, the first 56 bits in the IP will be the network subnet. diff --git a/ag_201_coredns/plugin/rewrite/class.go b/ag_201_coredns/plugin/rewrite/class.go new file mode 100644 index 0000000..243a864 --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/class.go @@ -0,0 +1,44 @@ +package rewrite + +import ( + "context" + "fmt" + "strings" + + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +type classRule struct { + fromClass uint16 + toClass uint16 + NextAction string +} + +// newClassRule creates a class matching rule +func newClassRule(nextAction string, args ...string) (Rule, error) { + var from, to uint16 + var ok bool + if from, ok = dns.StringToClass[strings.ToUpper(args[0])]; !ok { + return nil, fmt.Errorf("invalid class %q", strings.ToUpper(args[0])) + } + if to, ok = dns.StringToClass[strings.ToUpper(args[1])]; !ok { + return nil, fmt.Errorf("invalid class %q", strings.ToUpper(args[1])) + } + return &classRule{from, to, nextAction}, nil +} + +// Rewrite rewrites the current request. +func (rule *classRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + if rule.fromClass > 0 && rule.toClass > 0 { + if state.Req.Question[0].Qclass == rule.fromClass { + state.Req.Question[0].Qclass = rule.toClass + return nil, RewriteDone + } + } + return nil, RewriteIgnored +} + +// Mode returns the processing mode. +func (rule *classRule) Mode() string { return rule.NextAction } diff --git a/ag_201_coredns/plugin/rewrite/edns0.go b/ag_201_coredns/plugin/rewrite/edns0.go new file mode 100644 index 0000000..85146c7 --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/edns0.go @@ -0,0 +1,371 @@ +// Package rewrite is a plugin for rewriting requests internally to something different. +package rewrite + +import ( + "context" + "encoding/hex" + "fmt" + "net" + "strconv" + "strings" + + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/pkg/edns" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// edns0LocalRule is a rewrite rule for EDNS0_LOCAL options. +type edns0LocalRule struct { + mode string + action string + code uint16 + data []byte +} + +// edns0VariableRule is a rewrite rule for EDNS0_LOCAL options with variable. +type edns0VariableRule struct { + mode string + action string + code uint16 + variable string +} + +// ends0NsidRule is a rewrite rule for EDNS0_NSID options. +type edns0NsidRule struct { + mode string + action string +} + +// setupEdns0Opt will retrieve the EDNS0 OPT or create it if it does not exist. +func setupEdns0Opt(r *dns.Msg) *dns.OPT { + o := r.IsEdns0() + if o == nil { + r.SetEdns0(4096, false) + o = r.IsEdns0() + } + return o +} + +// Rewrite will alter the request EDNS0 NSID option +func (rule *edns0NsidRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + o := setupEdns0Opt(state.Req) + + for _, s := range o.Option { + if e, ok := s.(*dns.EDNS0_NSID); ok { + if rule.action == Replace || rule.action == Set { + e.Nsid = "" // make sure it is empty for request + return nil, RewriteDone + } + } + } + + // add option if not found + if rule.action == Append || rule.action == Set { + o.Option = append(o.Option, &dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""}) + return nil, RewriteDone + } + + return nil, RewriteIgnored +} + +// Mode returns the processing mode. +func (rule *edns0NsidRule) Mode() string { return rule.mode } + +// Rewrite will alter the request EDNS0 local options. +func (rule *edns0LocalRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + o := setupEdns0Opt(state.Req) + + for _, s := range o.Option { + if e, ok := s.(*dns.EDNS0_LOCAL); ok { + if rule.code == e.Code { + if rule.action == Replace || rule.action == Set { + e.Data = rule.data + return nil, RewriteDone + } + } + } + } + + // add option if not found + if rule.action == Append || rule.action == Set { + o.Option = append(o.Option, &dns.EDNS0_LOCAL{Code: rule.code, Data: rule.data}) + return nil, RewriteDone + } + + return nil, RewriteIgnored +} + +// Mode returns the processing mode. +func (rule *edns0LocalRule) Mode() string { return rule.mode } + +// newEdns0Rule creates an EDNS0 rule of the appropriate type based on the args +func newEdns0Rule(mode string, args ...string) (Rule, error) { + if len(args) < 2 { + return nil, fmt.Errorf("too few arguments for an EDNS0 rule") + } + + ruleType := strings.ToLower(args[0]) + action := strings.ToLower(args[1]) + switch action { + case Append: + case Replace: + case Set: + default: + return nil, fmt.Errorf("invalid action: %q", action) + } + + switch ruleType { + case "local": + if len(args) != 4 { + return nil, fmt.Errorf("EDNS0 local rules require exactly three args") + } + // Check for variable option. + if strings.HasPrefix(args[3], "{") && strings.HasSuffix(args[3], "}") { + return newEdns0VariableRule(mode, action, args[2], args[3]) + } + return newEdns0LocalRule(mode, action, args[2], args[3]) + case "nsid": + if len(args) != 2 { + return nil, fmt.Errorf("EDNS0 NSID rules do not accept args") + } + return &edns0NsidRule{mode: mode, action: action}, nil + case "subnet": + if len(args) != 4 { + return nil, fmt.Errorf("EDNS0 subnet rules require exactly three args") + } + return newEdns0SubnetRule(mode, action, args[2], args[3]) + default: + return nil, fmt.Errorf("invalid rule type %q", ruleType) + } +} + +func newEdns0LocalRule(mode, action, code, data string) (*edns0LocalRule, error) { + c, err := strconv.ParseUint(code, 0, 16) + if err != nil { + return nil, err + } + + decoded := []byte(data) + if strings.HasPrefix(data, "0x") { + decoded, err = hex.DecodeString(data[2:]) + if err != nil { + return nil, err + } + } + + // Add this code to the ones the server supports. + edns.SetSupportedOption(uint16(c)) + + return &edns0LocalRule{mode: mode, action: action, code: uint16(c), data: decoded}, nil +} + +// newEdns0VariableRule creates an EDNS0 rule that handles variable substitution +func newEdns0VariableRule(mode, action, code, variable string) (*edns0VariableRule, error) { + c, err := strconv.ParseUint(code, 0, 16) + if err != nil { + return nil, err + } + //Validate + if !isValidVariable(variable) { + return nil, fmt.Errorf("unsupported variable name %q", variable) + } + + // Add this code to the ones the server supports. + edns.SetSupportedOption(uint16(c)) + + return &edns0VariableRule{mode: mode, action: action, code: uint16(c), variable: variable}, nil +} + +// ruleData returns the data specified by the variable. +func (rule *edns0VariableRule) ruleData(ctx context.Context, state request.Request) ([]byte, error) { + switch rule.variable { + case queryName: + return []byte(state.QName()), nil + + case queryType: + return uint16ToWire(state.QType()), nil + + case clientIP: + return ipToWire(state.Family(), state.IP()) + + case serverIP: + return ipToWire(state.Family(), state.LocalIP()) + + case clientPort: + return portToWire(state.Port()) + + case serverPort: + return portToWire(state.LocalPort()) + + case protocol: + return []byte(state.Proto()), nil + } + + fetcher := metadata.ValueFunc(ctx, rule.variable[1:len(rule.variable)-1]) + if fetcher != nil { + value := fetcher() + if len(value) > 0 { + return []byte(value), nil + } + } + + return nil, fmt.Errorf("unable to extract data for variable %s", rule.variable) +} + +// Rewrite will alter the request EDNS0 local options with specified variables. +func (rule *edns0VariableRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + data, err := rule.ruleData(ctx, state) + if err != nil || data == nil { + return nil, RewriteIgnored + } + + o := setupEdns0Opt(state.Req) + for _, s := range o.Option { + if e, ok := s.(*dns.EDNS0_LOCAL); ok { + if rule.code == e.Code { + if rule.action == Replace || rule.action == Set { + e.Data = data + return nil, RewriteDone + } + return nil, RewriteIgnored + } + } + } + + // add option if not found + if rule.action == Append || rule.action == Set { + o.Option = append(o.Option, &dns.EDNS0_LOCAL{Code: rule.code, Data: data}) + return nil, RewriteDone + } + + return nil, RewriteIgnored +} + +// Mode returns the processing mode. +func (rule *edns0VariableRule) Mode() string { return rule.mode } + +func isValidVariable(variable string) bool { + switch variable { + case + queryName, + queryType, + clientIP, + clientPort, + protocol, + serverIP, + serverPort: + return true + } + // we cannot validate the labels of metadata - but we can verify it has the syntax of a label + if strings.HasPrefix(variable, "{") && strings.HasSuffix(variable, "}") && metadata.IsLabel(variable[1:len(variable)-1]) { + return true + } + return false +} + +// ends0SubnetRule is a rewrite rule for EDNS0 subnet options +type edns0SubnetRule struct { + mode string + v4BitMaskLen uint8 + v6BitMaskLen uint8 + action string +} + +func newEdns0SubnetRule(mode, action, v4BitMaskLen, v6BitMaskLen string) (*edns0SubnetRule, error) { + v4Len, err := strconv.ParseUint(v4BitMaskLen, 0, 16) + if err != nil { + return nil, err + } + // validate V4 length + if v4Len > net.IPv4len*8 { + return nil, fmt.Errorf("invalid IPv4 bit mask length %d", v4Len) + } + + v6Len, err := strconv.ParseUint(v6BitMaskLen, 0, 16) + if err != nil { + return nil, err + } + // validate V6 length + if v6Len > net.IPv6len*8 { + return nil, fmt.Errorf("invalid IPv6 bit mask length %d", v6Len) + } + + return &edns0SubnetRule{mode: mode, action: action, + v4BitMaskLen: uint8(v4Len), v6BitMaskLen: uint8(v6Len)}, nil +} + +// fillEcsData sets the subnet data into the ecs option +func (rule *edns0SubnetRule) fillEcsData(state request.Request, ecs *dns.EDNS0_SUBNET) error { + family := state.Family() + if (family != 1) && (family != 2) { + return fmt.Errorf("unable to fill data for EDNS0 subnet due to invalid IP family") + } + + ecs.Family = uint16(family) + ecs.SourceScope = 0 + + ipAddr := state.IP() + switch family { + case 1: + ipv4Mask := net.CIDRMask(int(rule.v4BitMaskLen), 32) + ipv4Addr := net.ParseIP(ipAddr) + ecs.SourceNetmask = rule.v4BitMaskLen + ecs.Address = ipv4Addr.Mask(ipv4Mask).To4() + case 2: + ipv6Mask := net.CIDRMask(int(rule.v6BitMaskLen), 128) + ipv6Addr := net.ParseIP(ipAddr) + ecs.SourceNetmask = rule.v6BitMaskLen + ecs.Address = ipv6Addr.Mask(ipv6Mask).To16() + } + return nil +} + +// Rewrite will alter the request EDNS0 subnet option. +func (rule *edns0SubnetRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + o := setupEdns0Opt(state.Req) + + for _, s := range o.Option { + if e, ok := s.(*dns.EDNS0_SUBNET); ok { + if rule.action == Replace || rule.action == Set { + if rule.fillEcsData(state, e) == nil { + return nil, RewriteDone + } + } + return nil, RewriteIgnored + } + } + + // add option if not found + if rule.action == Append || rule.action == Set { + opt := &dns.EDNS0_SUBNET{Code: dns.EDNS0SUBNET} + if rule.fillEcsData(state, opt) == nil { + o.Option = append(o.Option, opt) + return nil, RewriteDone + } + } + + return nil, RewriteIgnored +} + +// Mode returns the processing mode +func (rule *edns0SubnetRule) Mode() string { return rule.mode } + +// These are all defined actions. +const ( + Replace = "replace" + Set = "set" + Append = "append" +) + +// Supported local EDNS0 variables +const ( + queryName = "{qname}" + queryType = "{qtype}" + clientIP = "{client_ip}" + clientPort = "{client_port}" + protocol = "{protocol}" + serverIP = "{server_ip}" + serverPort = "{server_port}" +) diff --git a/ag_201_coredns/plugin/rewrite/fuzz.go b/ag_201_coredns/plugin/rewrite/fuzz.go new file mode 100644 index 0000000..8e44ebb --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/fuzz.go @@ -0,0 +1,20 @@ +//go:build gofuzz + +package rewrite + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/pkg/fuzz" +) + +// Fuzz fuzzes rewrite. +func Fuzz(data []byte) int { + c := caddy.NewTestController("dns", "rewrite edns0 subnet set 24 56") + rules, err := rewriteParse(c) + if err != nil { + return 0 + } + r := Rewrite{Rules: rules} + + return fuzz.Do(r, data) +} diff --git a/ag_201_coredns/plugin/rewrite/log_test.go b/ag_201_coredns/plugin/rewrite/log_test.go new file mode 100644 index 0000000..6ce3627 --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/log_test.go @@ -0,0 +1,5 @@ +package rewrite + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/rewrite/name.go b/ag_201_coredns/plugin/rewrite/name.go new file mode 100644 index 0000000..95d2b7a --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/name.go @@ -0,0 +1,449 @@ +package rewrite + +import ( + "context" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// stringRewriter rewrites a string +type stringRewriter interface { + rewriteString(src string) string +} + +// regexStringRewriter can be used to rewrite strings by regex pattern. +// it contains all the information required to detect and execute a rewrite +// on a string. +type regexStringRewriter struct { + pattern *regexp.Regexp + replacement string +} + +var _ stringRewriter = ®exStringRewriter{} + +func newStringRewriter(pattern *regexp.Regexp, replacement string) stringRewriter { + return ®exStringRewriter{pattern, replacement} +} + +func (r *regexStringRewriter) rewriteString(src string) string { + regexGroups := r.pattern.FindStringSubmatch(src) + if len(regexGroups) == 0 { + return src + } + s := r.replacement + for groupIndex, groupValue := range regexGroups { + groupIndexStr := "{" + strconv.Itoa(groupIndex) + "}" + s = strings.Replace(s, groupIndexStr, groupValue, -1) + } + return s +} + +// remapStringRewriter maps a dedicated string to another string +// it also maps a the domain of a sub domain. +type remapStringRewriter struct { + orig string + replacement string +} + +var _ stringRewriter = &remapStringRewriter{} + +func newRemapStringRewriter(orig, replacement string) stringRewriter { + return &remapStringRewriter{orig, replacement} +} + +func (r *remapStringRewriter) rewriteString(src string) string { + if src == r.orig { + return r.replacement + } + if strings.HasSuffix(src, "."+r.orig) { + return src[0:len(src)-len(r.orig)] + r.replacement + } + return src +} + +// suffixStringRewriter maps a dedicated suffix string to another string +type suffixStringRewriter struct { + suffix string + replacement string +} + +var _ stringRewriter = &suffixStringRewriter{} + +func newSuffixStringRewriter(orig, replacement string) stringRewriter { + return &suffixStringRewriter{orig, replacement} +} + +func (r *suffixStringRewriter) rewriteString(src string) string { + if strings.HasSuffix(src, r.suffix) { + return strings.TrimSuffix(src, r.suffix) + r.replacement + } + return src +} + +// nameRewriterResponseRule maps a record name according to a stringRewriter. +type nameRewriterResponseRule struct { + stringRewriter +} + +func (r *nameRewriterResponseRule) RewriteResponse(rr dns.RR) { + rr.Header().Name = r.rewriteString(rr.Header().Name) +} + +// valueRewriterResponseRule maps a record value according to a stringRewriter. +type valueRewriterResponseRule struct { + stringRewriter +} + +func (r *valueRewriterResponseRule) RewriteResponse(rr dns.RR) { + value := getRecordValueForRewrite(rr) + if value != "" { + new := r.rewriteString(value) + if new != value { + setRewrittenRecordValue(rr, new) + } + } +} + +const ( + // ExactMatch matches only on exact match of the name in the question section of a request + ExactMatch = "exact" + // PrefixMatch matches when the name begins with the matching string + PrefixMatch = "prefix" + // SuffixMatch matches when the name ends with the matching string + SuffixMatch = "suffix" + // SubstringMatch matches on partial match of the name in the question section of a request + SubstringMatch = "substring" + // RegexMatch matches when the name in the question section of a request matches a regular expression + RegexMatch = "regex" + + // AnswerMatch matches an answer rewrite + AnswerMatch = "answer" + // AutoMatch matches the auto name answer rewrite + AutoMatch = "auto" + // NameMatch matches the name answer rewrite + NameMatch = "name" + // ValueMatch matches the value answer rewrite + ValueMatch = "value" +) + +type nameRuleBase struct { + nextAction string + auto bool + replacement string + static ResponseRules +} + +func newNameRuleBase(nextAction string, auto bool, replacement string, staticResponses ResponseRules) nameRuleBase { + return nameRuleBase{ + nextAction: nextAction, + auto: auto, + replacement: replacement, + static: staticResponses, + } +} + +// responseRuleFor create for auto mode dynamically response rewriters for name and value +// reverting the mapping done by the name rewrite rule, which can be found in the state. +func (rule *nameRuleBase) responseRuleFor(state request.Request) (ResponseRules, Result) { + if !rule.auto { + return rule.static, RewriteDone + } + + rewriter := newRemapStringRewriter(state.Req.Question[0].Name, state.Name()) + rules := ResponseRules{ + &nameRewriterResponseRule{rewriter}, + &valueRewriterResponseRule{rewriter}, + } + return append(rules, rule.static...), RewriteDone +} + +// Mode returns the processing nextAction +func (rule *nameRuleBase) Mode() string { return rule.nextAction } + +// exactNameRule rewrites the current request based upon exact match of the name +// in the question section of the request. +type exactNameRule struct { + nameRuleBase + from string +} + +func newExactNameRule(nextAction string, orig, replacement string, answers ResponseRules) Rule { + return &exactNameRule{ + newNameRuleBase(nextAction, true, replacement, answers), + orig, + } +} + +func (rule *exactNameRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + if rule.from == state.Name() { + state.Req.Question[0].Name = rule.replacement + return rule.responseRuleFor(state) + } + return nil, RewriteIgnored +} + +// prefixNameRule rewrites the current request when the name begins with the matching string. +type prefixNameRule struct { + nameRuleBase + prefix string +} + +func newPrefixNameRule(nextAction string, auto bool, prefix, replacement string, answers ResponseRules) Rule { + return &prefixNameRule{ + newNameRuleBase(nextAction, auto, replacement, answers), + prefix, + } +} + +func (rule *prefixNameRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + if strings.HasPrefix(state.Name(), rule.prefix) { + state.Req.Question[0].Name = rule.replacement + strings.TrimPrefix(state.Name(), rule.prefix) + return rule.responseRuleFor(state) + } + return nil, RewriteIgnored +} + +// suffixNameRule rewrites the current request when the name ends with the matching string. +type suffixNameRule struct { + nameRuleBase + suffix string +} + +func newSuffixNameRule(nextAction string, auto bool, suffix, replacement string, answers ResponseRules) Rule { + var rules ResponseRules + if auto { + // for a suffix rewriter better standard response rewrites can be done + // just by using the original suffix/replacement in the opposite order + rewriter := newSuffixStringRewriter(replacement, suffix) + rules = ResponseRules{ + &nameRewriterResponseRule{rewriter}, + &valueRewriterResponseRule{rewriter}, + } + } + return &suffixNameRule{ + newNameRuleBase(nextAction, false, replacement, append(rules, answers...)), + suffix, + } +} + +func (rule *suffixNameRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + if strings.HasSuffix(state.Name(), rule.suffix) { + state.Req.Question[0].Name = strings.TrimSuffix(state.Name(), rule.suffix) + rule.replacement + return rule.responseRuleFor(state) + } + return nil, RewriteIgnored +} + +// substringNameRule rewrites the current request based upon partial match of the +// name in the question section of the request. +type substringNameRule struct { + nameRuleBase + substring string +} + +func newSubstringNameRule(nextAction string, auto bool, substring, replacement string, answers ResponseRules) Rule { + return &substringNameRule{ + newNameRuleBase(nextAction, auto, replacement, answers), + substring, + } +} + +func (rule *substringNameRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + if strings.Contains(state.Name(), rule.substring) { + state.Req.Question[0].Name = strings.Replace(state.Name(), rule.substring, rule.replacement, -1) + return rule.responseRuleFor(state) + } + return nil, RewriteIgnored +} + +// regexNameRule rewrites the current request when the name in the question +// section of the request matches a regular expression. +type regexNameRule struct { + nameRuleBase + pattern *regexp.Regexp +} + +func newRegexNameRule(nextAction string, auto bool, pattern *regexp.Regexp, replacement string, answers ResponseRules) Rule { + return ®exNameRule{ + newNameRuleBase(nextAction, auto, replacement, answers), + pattern, + } +} + +func (rule *regexNameRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + regexGroups := rule.pattern.FindStringSubmatch(state.Name()) + if len(regexGroups) == 0 { + return nil, RewriteIgnored + } + s := rule.replacement + for groupIndex, groupValue := range regexGroups { + groupIndexStr := "{" + strconv.Itoa(groupIndex) + "}" + s = strings.Replace(s, groupIndexStr, groupValue, -1) + } + state.Req.Question[0].Name = s + return rule.responseRuleFor(state) +} + +// newNameRule creates a name matching rule based on exact, partial, or regex match +func newNameRule(nextAction string, args ...string) (Rule, error) { + var matchType, rewriteQuestionFrom, rewriteQuestionTo string + if len(args) < 2 { + return nil, fmt.Errorf("too few arguments for a name rule") + } + if len(args) == 2 { + matchType = ExactMatch + rewriteQuestionFrom = plugin.Name(args[0]).Normalize() + rewriteQuestionTo = plugin.Name(args[1]).Normalize() + } + if len(args) >= 3 { + matchType = strings.ToLower(args[0]) + if matchType == RegexMatch { + rewriteQuestionFrom = args[1] + rewriteQuestionTo = args[2] + } else { + rewriteQuestionFrom = plugin.Name(args[1]).Normalize() + rewriteQuestionTo = plugin.Name(args[2]).Normalize() + } + } + if matchType == ExactMatch || matchType == SuffixMatch { + if !hasClosingDot(rewriteQuestionFrom) { + rewriteQuestionFrom = rewriteQuestionFrom + "." + } + if !hasClosingDot(rewriteQuestionTo) { + rewriteQuestionTo = rewriteQuestionTo + "." + } + } + + var err error + var answers ResponseRules + auto := false + if len(args) > 3 { + auto, answers, err = parseAnswerRules(matchType, args[3:]) + if err != nil { + return nil, err + } + } + + switch matchType { + case ExactMatch: + if _, err := isValidRegexPattern(rewriteQuestionTo, rewriteQuestionFrom); err != nil { + return nil, err + } + return newExactNameRule(nextAction, rewriteQuestionFrom, rewriteQuestionTo, answers), nil + case PrefixMatch: + return newPrefixNameRule(nextAction, auto, rewriteQuestionFrom, rewriteQuestionTo, answers), nil + case SuffixMatch: + return newSuffixNameRule(nextAction, auto, rewriteQuestionFrom, rewriteQuestionTo, answers), nil + case SubstringMatch: + return newSubstringNameRule(nextAction, auto, rewriteQuestionFrom, rewriteQuestionTo, answers), nil + case RegexMatch: + rewriteQuestionFromPattern, err := isValidRegexPattern(rewriteQuestionFrom, rewriteQuestionTo) + if err != nil { + return nil, err + } + rewriteQuestionTo := plugin.Name(args[2]).Normalize() + return newRegexNameRule(nextAction, auto, rewriteQuestionFromPattern, rewriteQuestionTo, answers), nil + default: + return nil, fmt.Errorf("name rule supports only exact, prefix, suffix, substring, and regex name matching, received: %s", matchType) + } +} + +func parseAnswerRules(name string, args []string) (auto bool, rules ResponseRules, err error) { + auto = false + arg := 0 + nameRules := 0 + last := "" + if len(args) < 2 { + return false, nil, fmt.Errorf("invalid arguments for %s rule", name) + } + for arg < len(args) { + if last == "" && args[arg] != AnswerMatch { + if last == "" { + return false, nil, fmt.Errorf("exceeded the number of arguments for a non-answer rule argument for %s rule", name) + } + return false, nil, fmt.Errorf("exceeded the number of arguments for %s answer rule for %s rule", last, name) + } + if args[arg] == AnswerMatch { + arg++ + } + if len(args)-arg == 0 { + return false, nil, fmt.Errorf("type missing for answer rule for %s rule", name) + } + last = args[arg] + arg++ + switch last { + case AutoMatch: + auto = true + continue + case NameMatch: + if len(args)-arg < 2 { + return false, nil, fmt.Errorf("%s answer rule for %s rule: 2 arguments required", last, name) + } + rewriteAnswerFrom := args[arg] + rewriteAnswerTo := args[arg+1] + rewriteAnswerFromPattern, err := isValidRegexPattern(rewriteAnswerFrom, rewriteAnswerTo) + rewriteAnswerTo = plugin.Name(rewriteAnswerTo).Normalize() + if err != nil { + return false, nil, fmt.Errorf("%s answer rule for %s rule: %s", last, name, err) + } + rules = append(rules, &nameRewriterResponseRule{newStringRewriter(rewriteAnswerFromPattern, rewriteAnswerTo)}) + arg += 2 + nameRules++ + case ValueMatch: + if len(args)-arg < 2 { + return false, nil, fmt.Errorf("%s answer rule for %s rule: 2 arguments required", last, name) + } + rewriteAnswerFrom := args[arg] + rewriteAnswerTo := args[arg+1] + rewriteAnswerFromPattern, err := isValidRegexPattern(rewriteAnswerFrom, rewriteAnswerTo) + rewriteAnswerTo = plugin.Name(rewriteAnswerTo).Normalize() + if err != nil { + return false, nil, fmt.Errorf("%s answer rule for %s rule: %s", last, name, err) + } + rules = append(rules, &valueRewriterResponseRule{newStringRewriter(rewriteAnswerFromPattern, rewriteAnswerTo)}) + arg += 2 + default: + return false, nil, fmt.Errorf("invalid type %q for answer rule for %s rule", last, name) + } + } + + if auto && nameRules > 0 { + return false, nil, fmt.Errorf("auto name answer rule cannot be combined with explicit name anwer rules") + } + return +} + +// hasClosingDot returns true if s has a closing dot at the end. +func hasClosingDot(s string) bool { + return strings.HasSuffix(s, ".") +} + +// getSubExprUsage returns the number of subexpressions used in s. +func getSubExprUsage(s string) int { + subExprUsage := 0 + for i := 0; i <= 100; i++ { + if strings.Contains(s, "{"+strconv.Itoa(i)+"}") { + subExprUsage++ + } + } + return subExprUsage +} + +// isValidRegexPattern returns a regular expression for pattern matching or errors, if any. +func isValidRegexPattern(rewriteFrom, rewriteTo string) (*regexp.Regexp, error) { + rewriteFromPattern, err := regexp.Compile(rewriteFrom) + if err != nil { + return nil, fmt.Errorf("invalid regex matching pattern: %s", rewriteFrom) + } + if getSubExprUsage(rewriteTo) > rewriteFromPattern.NumSubexp() { + return nil, fmt.Errorf("the rewrite regex pattern (%s) uses more subexpressions than its corresponding matching regex pattern (%s)", rewriteTo, rewriteFrom) + } + return rewriteFromPattern, nil +} diff --git a/ag_201_coredns/plugin/rewrite/name_test.go b/ag_201_coredns/plugin/rewrite/name_test.go new file mode 100644 index 0000000..2dbf1d1 --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/name_test.go @@ -0,0 +1,376 @@ +package rewrite + +import ( + "context" + "strings" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestRewriteIllegalName(t *testing.T) { + r, _ := newNameRule("stop", "example.org.", "example..org.") + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + RevertPolicy: NoRevertPolicy(), + } + + ctx := context.TODO() + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := rw.ServeDNS(ctx, rec, m) + if !strings.Contains(err.Error(), "invalid name") { + t.Errorf("Expected invalid name, got %s", err.Error()) + } +} + +func TestRewriteNamePrefixSuffix(t *testing.T) { + ctx, close := context.WithCancel(context.TODO()) + defer close() + + tests := []struct { + next string + args []string + question string + expected string + }{ + {"stop", []string{"prefix", "foo", "bar"}, "foo.example.com.", "bar.example.com."}, + {"stop", []string{"prefix", "foo.", "bar."}, "foo.example.com.", "bar.example.com."}, + {"stop", []string{"suffix", "com", "org"}, "foo.example.com.", "foo.example.org."}, + {"stop", []string{"suffix", ".com", ".org"}, "foo.example.com.", "foo.example.org."}, + } + for _, tc := range tests { + r, err := newNameRule(tc.next, tc.args...) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + RevertPolicy: NoRevertPolicy(), + } + + m := new(dns.Msg) + m.SetQuestion(tc.question, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err = rw.ServeDNS(ctx, rec, m) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + actual := rec.Msg.Question[0].Name + if actual != tc.expected { + t.Fatalf("Expected rewrite to %v, got %v", tc.expected, actual) + } + } +} + +func TestRewriteNameNoRewrite(t *testing.T) { + ctx, close := context.WithCancel(context.TODO()) + defer close() + + tests := []struct { + next string + args []string + question string + expected string + }{ + {"stop", []string{"prefix", "foo", "bar"}, "coredns.foo.", "coredns.foo."}, + {"stop", []string{"prefix", "foo", "bar."}, "coredns.foo.", "coredns.foo."}, + {"stop", []string{"suffix", "com", "org"}, "com.coredns.", "com.coredns."}, + {"stop", []string{"suffix", "com", "org."}, "com.coredns.", "com.coredns."}, + {"stop", []string{"substring", "service", "svc"}, "com.coredns.", "com.coredns."}, + } + for i, tc := range tests { + r, err := newNameRule(tc.next, tc.args...) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + } + + m := new(dns.Msg) + m.SetQuestion(tc.question, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err = rw.ServeDNS(ctx, rec, m) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + actual := rec.Msg.Answer[0].Header().Name + if actual != tc.expected { + t.Fatalf("Test %d: Expected answer rewrite to %v, got %v", i, tc.expected, actual) + } + } +} + +func TestRewriteNamePrefixSuffixNoAutoAnswer(t *testing.T) { + ctx, close := context.WithCancel(context.TODO()) + defer close() + + tests := []struct { + next string + args []string + question string + expected string + }{ + {"stop", []string{"prefix", "foo", "bar"}, "foo.example.com.", "bar.example.com."}, + {"stop", []string{"prefix", "foo.", "bar."}, "foo.example.com.", "bar.example.com."}, + {"stop", []string{"suffix", "com", "org"}, "foo.example.com.", "foo.example.org."}, + {"stop", []string{"suffix", ".com", ".org"}, "foo.example.com.", "foo.example.org."}, + {"stop", []string{"suffix", ".ingress.coredns.rocks", "nginx.coredns.rocks"}, "coredns.ingress.coredns.rocks.", "corednsnginx.coredns.rocks."}, + } + for i, tc := range tests { + r, err := newNameRule(tc.next, tc.args...) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + } + + m := new(dns.Msg) + m.SetQuestion(tc.question, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err = rw.ServeDNS(ctx, rec, m) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + actual := rec.Msg.Answer[0].Header().Name + if actual != tc.expected { + t.Fatalf("Test %d: Expected answer rewrite to %v, got %v", i, tc.expected, actual) + } + } +} + +func TestRewriteNamePrefixSuffixAutoAnswer(t *testing.T) { + ctx, close := context.WithCancel(context.TODO()) + defer close() + + tests := []struct { + next string + args []string + question string + rewrite string + expected string + }{ + {"stop", []string{"prefix", "foo", "bar", "answer", "auto"}, "foo.example.com.", "bar.example.com.", "foo.example.com."}, + {"stop", []string{"prefix", "foo.", "bar.", "answer", "auto"}, "foo.example.com.", "bar.example.com.", "foo.example.com."}, + {"stop", []string{"suffix", "com", "org", "answer", "auto"}, "foo.example.com.", "foo.example.org.", "foo.example.com."}, + {"stop", []string{"suffix", ".com", ".org", "answer", "auto"}, "foo.example.com.", "foo.example.org.", "foo.example.com."}, + {"stop", []string{"suffix", ".ingress.coredns.rocks", "nginx.coredns.rocks", "answer", "auto"}, "coredns.ingress.coredns.rocks.", "corednsnginx.coredns.rocks.", "coredns.ingress.coredns.rocks."}, + } + for i, tc := range tests { + r, err := newNameRule(tc.next, tc.args...) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + RevertPolicy: NoRestorePolicy(), + } + + m := new(dns.Msg) + m.SetQuestion(tc.question, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err = rw.ServeDNS(ctx, rec, m) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + rewrite := rec.Msg.Question[0].Name + if rewrite != tc.rewrite { + t.Fatalf("Test %d: Expected question rewrite to %v, got %v", i, tc.rewrite, rewrite) + } + actual := rec.Msg.Answer[0].Header().Name + if actual != tc.expected { + t.Fatalf("Test %d: Expected answer rewrite to %v, got %v", i, tc.expected, actual) + } + } +} + +func TestRewriteNameExactAnswer(t *testing.T) { + ctx, close := context.WithCancel(context.TODO()) + defer close() + + tests := []struct { + next string + args []string + question string + rewrite string + expected string + }{ + {"stop", []string{"exact", "coredns.rocks", "service.consul", "answer", "auto"}, "coredns.rocks.", "service.consul.", "coredns.rocks."}, + {"stop", []string{"exact", "coredns.rocks.", "service.consul.", "answer", "auto"}, "coredns.rocks.", "service.consul.", "coredns.rocks."}, + {"stop", []string{"exact", "coredns.rocks", "service.consul"}, "coredns.rocks.", "service.consul.", "coredns.rocks."}, + {"stop", []string{"exact", "coredns.rocks.", "service.consul."}, "coredns.rocks.", "service.consul.", "coredns.rocks."}, + {"stop", []string{"exact", "coredns.org.", "service.consul."}, "coredns.rocks.", "coredns.rocks.", "coredns.rocks."}, + } + for i, tc := range tests { + r, err := newNameRule(tc.next, tc.args...) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + RevertPolicy: NoRestorePolicy(), + } + + m := new(dns.Msg) + m.SetQuestion(tc.question, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err = rw.ServeDNS(ctx, rec, m) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + rewrite := rec.Msg.Question[0].Name + if rewrite != tc.rewrite { + t.Fatalf("Test %d: Expected question rewrite to %v, got %v", i, tc.rewrite, rewrite) + } + actual := rec.Msg.Answer[0].Header().Name + if actual != tc.expected { + t.Fatalf("Test %d: Expected answer rewrite to %v, got %v", i, tc.expected, actual) + } + } +} + +func TestRewriteNameRegexAnswer(t *testing.T) { + ctx, close := context.WithCancel(context.TODO()) + defer close() + + tests := []struct { + next string + args []string + question string + rewrite string + expected string + }{ + {"stop", []string{"regex", "(.*).coredns.rocks", "{1}.coredns.maps", "answer", "auto"}, "foo.coredns.rocks.", "foo.coredns.maps.", "foo.coredns.rocks."}, + {"stop", []string{"regex", "(.*).coredns.rocks", "{1}.coredns.maps", "answer", "name", "(.*).coredns.maps", "{1}.coredns.works"}, "foo.coredns.rocks.", "foo.coredns.maps.", "foo.coredns.works."}, + {"stop", []string{"regex", "(.*).coredns.rocks", "{1}.coredns.maps"}, "foo.coredns.rocks.", "foo.coredns.maps.", "foo.coredns.maps."}, + } + for i, tc := range tests { + r, err := newNameRule(tc.next, tc.args...) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: []Rule{r}, + RevertPolicy: NoRestorePolicy(), + } + + m := new(dns.Msg) + m.SetQuestion(tc.question, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err = rw.ServeDNS(ctx, rec, m) + if err != nil { + t.Fatalf("Test %d: Expected no error, got %s", i, err) + } + rewrite := rec.Msg.Question[0].Name + if rewrite != tc.rewrite { + t.Fatalf("Test %d: Expected question rewrite to %v, got %v", i, tc.rewrite, rewrite) + } + actual := rec.Msg.Answer[0].Header().Name + if actual != tc.expected { + t.Fatalf("Test %d: Expected answer rewrite to %v, got %v", i, tc.expected, actual) + } + } +} + +func TestNewNameRule(t *testing.T) { + tests := []struct { + next string + args []string + expectedFail bool + }{ + {"stop", []string{"exact", "srv3.coredns.rocks", "srv4.coredns.rocks"}, false}, + {"stop", []string{"srv1.coredns.rocks", "srv2.coredns.rocks"}, false}, + {"stop", []string{"suffix", "coredns.rocks", "coredns.rocks."}, false}, + {"stop", []string{"suffix", "coredns.rocks.", "coredns.rocks"}, false}, + {"stop", []string{"suffix", "coredns.rocks.", "coredns.rocks."}, false}, + {"stop", []string{"regex", "srv1.coredns.rocks", "10"}, false}, + {"stop", []string{"regex", "(.*).coredns.rocks", "10"}, false}, + {"stop", []string{"regex", "(.*).coredns.rocks", "{1}.coredns.rocks"}, false}, + {"stop", []string{"regex", "(.*).coredns.rocks", "{1}.{2}.coredns.rocks"}, true}, + {"stop", []string{"regex", "staging.mydomain.com", "aws-loadbalancer-id.us-east-1.elb.amazonaws.com"}, false}, + {"stop", []string{"suffix", "staging.mydomain.com", "coredns.rock", "answer"}, true}, + {"stop", []string{"suffix", "staging.mydomain.com", "coredns.rock", "answer", "name"}, true}, + {"stop", []string{"suffix", "staging.mydomain.com", "coredns.rock", "answer", "other"}, true}, + {"stop", []string{"suffix", "staging.mydomain.com", "coredns.rock", "answer", "auto"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "auto"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name"}, true}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "coredns.rock", "staging.mydomain.com"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.{2}.staging.mydomain.com"}, true}, + + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "name", "(.*).coredns.rock"}, true}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"regex", "staging.mydomain.com", "coredns.rock", "answer", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com", "value", "(.*).coredns.rock"}, true}, + + {"stop", []string{"suffix", "staging.mydomain.com.", "coredns.rock.", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"suffix", "staging.mydomain.com.", "coredns.rock.", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"suffix", "staging.mydomain.com.", "coredns.rock.", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com", "name", "(.*).coredns.rock", "{1}.staging.mydomain.com"}, false}, + {"stop", []string{"suffix", "staging.mydomain.com.", "coredns.rock.", "answer", "value", "(.*).coredns.rock", "{1}.staging.mydomain.com", "value", "(.*).coredns.rock"}, true}, + } + for i, tc := range tests { + failed := false + rule, err := newNameRule(tc.next, tc.args...) + if err != nil { + failed = true + } + if !failed && !tc.expectedFail { + t.Logf("Test %d: PASS, passed as expected: (%s) %s", i, tc.next, tc.args) + continue + } + if failed && tc.expectedFail { + t.Logf("Test %d: PASS, failed as expected: (%s) %s: %s", i, tc.next, tc.args, err) + continue + } + if failed && !tc.expectedFail { + t.Fatalf("Test %d: FAIL, expected fail=%t, but received fail=%t: (%s) %s, rule=%v, error=%s", i, tc.expectedFail, failed, tc.next, tc.args, rule, err) + } + t.Fatalf("Test %d: FAIL, expected fail=%t, but received fail=%t: (%s) %s, rule=%v", i, tc.expectedFail, failed, tc.next, tc.args, rule) + } + for i, tc := range tests { + failed := false + tc.args = append([]string{tc.next, "name"}, tc.args...) + rule, err := newRule(tc.args...) + if err != nil { + failed = true + } + if !failed && !tc.expectedFail { + t.Logf("Test %d: PASS, passed as expected: (%s) %s", i, tc.next, tc.args) + continue + } + if failed && tc.expectedFail { + t.Logf("Test %d: PASS, failed as expected: (%s) %s: %s", i, tc.next, tc.args, err) + continue + } + t.Fatalf("Test %d: FAIL, expected fail=%t, but received fail=%t: (%s) %s, rule=%v", i, tc.expectedFail, failed, tc.next, tc.args, rule) + } +} diff --git a/ag_201_coredns/plugin/rewrite/reverter.go b/ag_201_coredns/plugin/rewrite/reverter.go new file mode 100644 index 0000000..7abbfb8 --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/reverter.go @@ -0,0 +1,146 @@ +package rewrite + +import ( + "github.com/miekg/dns" +) + +// RevertPolicy controls the overall reverting process +type RevertPolicy interface { + DoRevert() bool + DoQuestionRestore() bool +} + +type revertPolicy struct { + noRevert bool + noRestore bool +} + +func (p revertPolicy) DoRevert() bool { + return !p.noRevert +} + +func (p revertPolicy) DoQuestionRestore() bool { + return !p.noRestore +} + +// NoRevertPolicy disables all response rewrite rules +func NoRevertPolicy() RevertPolicy { + return revertPolicy{true, false} +} + +// NoRestorePolicy disables the question restoration during the response rewrite +func NoRestorePolicy() RevertPolicy { + return revertPolicy{false, true} +} + +// NewRevertPolicy creates a new reverter policy by dynamically specifying all +// options. +func NewRevertPolicy(noRevert, noRestore bool) RevertPolicy { + return revertPolicy{noRestore: noRestore, noRevert: noRevert} +} + +// ResponseRule contains a rule to rewrite a response with. +type ResponseRule interface { + RewriteResponse(rr dns.RR) +} + +// ResponseRules describes an ordered list of response rules to apply +// after a name rewrite +type ResponseRules = []ResponseRule + +// ResponseReverter reverses the operations done on the question section of a packet. +// This is need because the client will otherwise disregards the response, i.e. +// dig will complain with ';; Question section mismatch: got example.org/HINFO/IN' +type ResponseReverter struct { + dns.ResponseWriter + originalQuestion dns.Question + ResponseRules ResponseRules + revertPolicy RevertPolicy +} + +// NewResponseReverter returns a pointer to a new ResponseReverter. +func NewResponseReverter(w dns.ResponseWriter, r *dns.Msg, policy RevertPolicy) *ResponseReverter { + return &ResponseReverter{ + ResponseWriter: w, + originalQuestion: r.Question[0], + revertPolicy: policy, + } +} + +// WriteMsg records the status code and calls the underlying ResponseWriter's WriteMsg method. +func (r *ResponseReverter) WriteMsg(res1 *dns.Msg) error { + // Deep copy 'res' as to not (e.g). rewrite a message that's also stored in the cache. + res := res1.Copy() + + if r.revertPolicy.DoQuestionRestore() { + res.Question[0] = r.originalQuestion + } + if len(r.ResponseRules) > 0 { + for _, rr := range res.Ns { + r.rewriteResourceRecord(res, rr) + } + for _, rr := range res.Answer { + r.rewriteResourceRecord(res, rr) + } + for _, rr := range res.Extra { + r.rewriteResourceRecord(res, rr) + } + } + return r.ResponseWriter.WriteMsg(res) +} + +func (r *ResponseReverter) rewriteResourceRecord(res *dns.Msg, rr dns.RR) { + for _, rule := range r.ResponseRules { + rule.RewriteResponse(rr) + } +} + +// Write is a wrapper that records the size of the message that gets written. +func (r *ResponseReverter) Write(buf []byte) (int, error) { + n, err := r.ResponseWriter.Write(buf) + return n, err +} + +func getRecordValueForRewrite(rr dns.RR) (name string) { + switch rr.Header().Rrtype { + case dns.TypeSRV: + return rr.(*dns.SRV).Target + case dns.TypeMX: + return rr.(*dns.MX).Mx + case dns.TypeCNAME: + return rr.(*dns.CNAME).Target + case dns.TypeNS: + return rr.(*dns.NS).Ns + case dns.TypeDNAME: + return rr.(*dns.DNAME).Target + case dns.TypeNAPTR: + return rr.(*dns.NAPTR).Replacement + case dns.TypeSOA: + return rr.(*dns.SOA).Ns + case dns.TypePTR: + return rr.(*dns.PTR).Ptr + default: + return "" + } +} + +func setRewrittenRecordValue(rr dns.RR, value string) { + switch rr.Header().Rrtype { + case dns.TypeSRV: + rr.(*dns.SRV).Target = value + case dns.TypeMX: + rr.(*dns.MX).Mx = value + case dns.TypeCNAME: + rr.(*dns.CNAME).Target = value + case dns.TypeNS: + rr.(*dns.NS).Ns = value + case dns.TypeDNAME: + rr.(*dns.DNAME).Target = value + case dns.TypeNAPTR: + rr.(*dns.NAPTR).Replacement = value + case dns.TypeSOA: + rr.(*dns.SOA).Ns = value + case dns.TypePTR: + rr.(*dns.PTR).Ptr = value + } +} diff --git a/ag_201_coredns/plugin/rewrite/reverter_test.go b/ag_201_coredns/plugin/rewrite/reverter_test.go new file mode 100644 index 0000000..9156728 --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/reverter_test.go @@ -0,0 +1,177 @@ +package rewrite + +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" +) + +var tests = []struct { + from string + fromType uint16 + answer []dns.RR + to string + toType uint16 + noRevert bool +}{ + {"core.dns.rocks", dns.TypeA, []dns.RR{test.A("dns.core.rocks. 5 IN A 10.0.0.1")}, "core.dns.rocks", dns.TypeA, false}, + {"core.dns.rocks", dns.TypeSRV, []dns.RR{test.SRV("dns.core.rocks. 5 IN SRV 0 100 100 srv1.dns.core.rocks.")}, "core.dns.rocks", dns.TypeSRV, false}, + {"core.dns.rocks", dns.TypeA, []dns.RR{test.A("core.dns.rocks. 5 IN A 10.0.0.1")}, "dns.core.rocks.", dns.TypeA, true}, + {"core.dns.rocks", dns.TypeSRV, []dns.RR{test.SRV("core.dns.rocks. 5 IN SRV 0 100 100 srv1.dns.core.rocks.")}, "dns.core.rocks.", dns.TypeSRV, true}, + {"core.dns.rocks", dns.TypeHINFO, []dns.RR{test.HINFO("core.dns.rocks. 5 HINFO INTEL-64 \"RHEL 7.4\"")}, "core.dns.rocks", dns.TypeHINFO, false}, + {"core.dns.rocks", dns.TypeA, []dns.RR{ + test.A("dns.core.rocks. 5 IN A 10.0.0.1"), + test.A("dns.core.rocks. 5 IN A 10.0.0.2"), + }, "core.dns.rocks", dns.TypeA, false}, +} + +func TestResponseReverter(t *testing.T) { + rules := []Rule{} + r, _ := newNameRule("stop", "regex", `(core)\.(dns)\.(rocks)`, "{2}.{1}.{3}", "answer", "name", `(dns)\.(core)\.(rocks)`, "{2}.{1}.{3}") + rules = append(rules, r) + + doReverterTests(rules, t) + + rules = []Rule{} + r, _ = newNameRule("continue", "regex", `(core)\.(dns)\.(rocks)`, "{2}.{1}.{3}", "answer", "name", `(dns)\.(core)\.(rocks)`, "{2}.{1}.{3}") + rules = append(rules, r) + + doReverterTests(rules, t) +} + +func doReverterTests(rules []Rule, t *testing.T) { + ctx := context.TODO() + for i, tc := range tests { + m := new(dns.Msg) + m.SetQuestion(tc.from, tc.fromType) + m.Question[0].Qclass = dns.ClassINET + m.Answer = tc.answer + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, + RevertPolicy: NewRevertPolicy(tc.noRevert, false), + } + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + rw.ServeDNS(ctx, rec, m) + resp := rec.Msg + if resp.Question[0].Name != tc.to { + t.Errorf("Test %d: Expected Name to be %q but was %q", i, tc.to, resp.Question[0].Name) + } + if resp.Question[0].Qtype != tc.toType { + t.Errorf("Test %d: Expected Type to be '%d' but was '%d'", i, tc.toType, resp.Question[0].Qtype) + } + } +} + +var valueTests = []struct { + from string + fromType uint16 + answer []dns.RR + extra []dns.RR + to string + toType uint16 + noRevert bool + expectValue string + expectAnswerType uint16 + expectAddlName string +}{ + {"my.domain.uk.", dns.TypeSRV, []dns.RR{test.SRV("my.cluster.local. 5 IN SRV 0 100 100 srv1.my.cluster.local.")}, []dns.RR{test.A("srv1.my.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeSRV, false, "srv1.my.domain.uk.", dns.TypeSRV, "srv1.my.domain.uk."}, + {"my.domain.uk.", dns.TypeSRV, []dns.RR{test.SRV("my.cluster.local. 5 IN SRV 0 100 100 srv1.my.cluster.local.")}, []dns.RR{test.A("srv1.my.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeSRV, true, "srv1.my.cluster.local.", dns.TypeSRV, "srv1.my.cluster.local."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.CNAME("my.cluster.local. 3600 IN CNAME cname.cluster.local.")}, []dns.RR{test.A("cname.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeANY, false, "cname.domain.uk.", dns.TypeCNAME, "cname.domain.uk."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.CNAME("my.cluster.local. 3600 IN CNAME cname.cluster.local.")}, []dns.RR{test.A("cname.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeANY, true, "cname.cluster.local.", dns.TypeCNAME, "cname.cluster.local."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.DNAME("my.cluster.local. 3600 IN DNAME dname.cluster.local.")}, []dns.RR{test.A("dname.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeANY, false, "dname.domain.uk.", dns.TypeDNAME, "dname.domain.uk."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.DNAME("my.cluster.local. 3600 IN DNAME dname.cluster.local.")}, []dns.RR{test.A("dname.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeANY, true, "dname.cluster.local.", dns.TypeDNAME, "dname.cluster.local."}, + {"my.domain.uk.", dns.TypeMX, []dns.RR{test.MX("my.cluster.local. 3600 IN MX 1 mx1.cluster.local.")}, []dns.RR{test.A("mx1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeMX, false, "mx1.domain.uk.", dns.TypeMX, "mx1.domain.uk."}, + {"my.domain.uk.", dns.TypeMX, []dns.RR{test.MX("my.cluster.local. 3600 IN MX 1 mx1.cluster.local.")}, []dns.RR{test.A("mx1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeMX, true, "mx1.cluster.local.", dns.TypeMX, "mx1.cluster.local."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.NS("my.cluster.local. 3600 IN NS ns1.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeANY, false, "ns1.domain.uk.", dns.TypeNS, "ns1.domain.uk."}, + {"my.domain.uk.", dns.TypeANY, []dns.RR{test.NS("my.cluster.local. 3600 IN NS ns1.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeANY, true, "ns1.cluster.local.", dns.TypeNS, "ns1.cluster.local."}, + {"my.domain.uk.", dns.TypeSOA, []dns.RR{test.SOA("my.cluster.local. 1800 IN SOA ns1.cluster.local. admin.cluster.local. 1502165581 14400 3600 604800 14400")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeSOA, false, "ns1.domain.uk.", dns.TypeSOA, "ns1.domain.uk."}, + {"my.domain.uk.", dns.TypeSOA, []dns.RR{test.SOA("my.cluster.local. 1800 IN SOA ns1.cluster.local. admin.cluster.local. 1502165581 14400 3600 604800 14400")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeSOA, true, "ns1.cluster.local.", dns.TypeSOA, "ns1.cluster.local."}, + {"my.domain.uk.", dns.TypeNAPTR, []dns.RR{test.NAPTR("my.cluster.local. 100 IN NAPTR 100 10 \"S\" \"SIP+D2U\" \"!^.*$!sip:customer-service@example.com!\" _sip._udp.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.domain.uk.", dns.TypeNAPTR, false, "_sip._udp.domain.uk.", dns.TypeNAPTR, "ns1.domain.uk."}, + {"my.domain.uk.", dns.TypeNAPTR, []dns.RR{test.NAPTR("my.cluster.local. 100 IN NAPTR 100 10 \"S\" \"SIP+D2U\" \"!^.*$!sip:customer-service@example.com!\" _sip._udp.cluster.local.")}, []dns.RR{test.A("ns1.cluster.local. 5 IN A 10.0.0.1")}, "my.cluster.local.", dns.TypeNAPTR, true, "_sip._udp.cluster.local.", dns.TypeNAPTR, "ns1.cluster.local."}, +} + +func TestValueResponseReverter(t *testing.T) { + rules := []Rule{} + r, err := newNameRule("stop", "regex", `(.*)\.domain\.uk`, "{1}.cluster.local", "answer", "name", `(.*)\.cluster\.local`, "{1}.domain.uk", "answer", "value", `(.*)\.cluster\.local`, "{1}.domain.uk") + if err != nil { + t.Errorf("cannot parse rule: %s", err) + return + } + rules = append(rules, r) + + doValueReverterTests("stop", rules, t) + + rules = []Rule{} + r, err = newNameRule("continue", "regex", `(.*)\.domain\.uk`, "{1}.cluster.local", "answer", "name", `(.*)\.cluster\.local`, "{1}.domain.uk", "answer", "value", `(.*)\.cluster\.local`, "{1}.domain.uk") + if err != nil { + t.Errorf("cannot parse rule: %s", err) + return + } + rules = append(rules, r) + + doValueReverterTests("continue", rules, t) + + rules = []Rule{} + r, err = newNameRule("stop", "suffix", `.domain.uk`, ".cluster.local", "answer", "auto", "answer", "value", `(.*)\.cluster\.local`, "{1}.domain.uk") + if err != nil { + t.Errorf("cannot parse rule: %s", err) + return + } + rules = append(rules, r) + + doValueReverterTests("suffix", rules, t) +} + +func doValueReverterTests(name string, rules []Rule, t *testing.T) { + ctx := context.TODO() + for i, tc := range valueTests { + m := new(dns.Msg) + m.SetQuestion(tc.from, tc.fromType) + m.Question[0].Qclass = dns.ClassINET + m.Answer = tc.answer + m.Extra = tc.extra + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, + RevertPolicy: NewRevertPolicy(tc.noRevert, false), + } + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + rw.ServeDNS(ctx, rec, m) + resp := rec.Msg + if resp.Question[0].Name != tc.to { + t.Errorf("Test %s.%d: Expected Name to be %q but was %q", name, i, tc.to, resp.Question[0].Name) + } + if resp.Question[0].Qtype != tc.toType { + t.Errorf("Test %s.%d: Expected Type to be '%d' but was '%d'", name, i, tc.toType, resp.Question[0].Qtype) + } + + if len(resp.Answer) <= 0 { + t.Errorf("Test %s.%d: No Answers", name, i) + return + } + if len(resp.Answer) > 0 && resp.Answer[0].Header().Rrtype != tc.expectAnswerType { + t.Errorf("Test %s.%d: Unexpected Answer Record Type %d", name, i, resp.Answer[0].Header().Rrtype) + return + } + + value := getRecordValueForRewrite(resp.Answer[0]) + if value != tc.expectValue { + t.Errorf("Test %s.%d: Expected Target to be '%s' but was '%s'", name, i, tc.expectValue, value) + } + + if len(resp.Extra) <= 0 || resp.Extra[0].Header().Rrtype != dns.TypeA { + t.Errorf("Test %s.%d: Unexpected Additional Record Type / No Additional Records", name, i) + return + } + + if resp.Extra[0].Header().Name != tc.expectAddlName { + t.Errorf("Test %s.%d: Expected Extra Name to be %q but was %q", name, i, tc.expectAddlName, resp.Extra[0].Header().Name) + } + } +} diff --git a/ag_201_coredns/plugin/rewrite/rewrite.go b/ag_201_coredns/plugin/rewrite/rewrite.go new file mode 100644 index 0000000..b28352c --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/rewrite.go @@ -0,0 +1,145 @@ +package rewrite + +import ( + "context" + "fmt" + "strings" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Result is the result of a rewrite +type Result int + +const ( + // RewriteIgnored is returned when rewrite is not done on request. + RewriteIgnored Result = iota + // RewriteDone is returned when rewrite is done on request. + RewriteDone +) + +// These are defined processing mode. +const ( + // Processing should stop after completing this rule + Stop = "stop" + // Processing should continue to next rule + Continue = "continue" +) + +// Rewrite is a plugin to rewrite requests internally before being handled. +type Rewrite struct { + Next plugin.Handler + Rules []Rule + RevertPolicy +} + +// ServeDNS implements the plugin.Handler interface. +func (rw Rewrite) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if rw.RevertPolicy == nil { + rw.RevertPolicy = NewRevertPolicy(false, false) + } + wr := NewResponseReverter(w, r, rw.RevertPolicy) + state := request.Request{W: w, Req: r} + + for _, rule := range rw.Rules { + respRules, result := rule.Rewrite(ctx, state) + if result == RewriteDone { + if _, ok := dns.IsDomainName(state.Req.Question[0].Name); !ok { + err := fmt.Errorf("invalid name after rewrite: %s", state.Req.Question[0].Name) + state.Req.Question[0] = wr.originalQuestion + return dns.RcodeServerFailure, err + } + wr.ResponseRules = append(wr.ResponseRules, respRules...) + if rule.Mode() == Stop { + if !rw.RevertPolicy.DoRevert() { + return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, w, r) + } + rcode, err := plugin.NextOrFailure(rw.Name(), rw.Next, ctx, wr, r) + if plugin.ClientWrite(rcode) { + return rcode, err + } + // The next plugins didn't write a response, so write one now with the ResponseReverter. + // If server.ServeDNS does this then it will create an answer mismatch. + res := new(dns.Msg).SetRcode(r, rcode) + state.SizeAndDo(res) + wr.WriteMsg(res) + // return success, so server does not write a second error response to client + return dns.RcodeSuccess, err + } + } + } + if !rw.RevertPolicy.DoRevert() || len(wr.ResponseRules) == 0 { + return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, w, r) + } + return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, wr, r) +} + +// Name implements the Handler interface. +func (rw Rewrite) Name() string { return "rewrite" } + +// Rule describes a rewrite rule. +type Rule interface { + // Rewrite rewrites the current request. + Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) + // Mode returns the processing mode stop or continue. + Mode() string +} + +func newRule(args ...string) (Rule, error) { + if len(args) == 0 { + return nil, fmt.Errorf("no rule type specified for rewrite") + } + + arg0 := strings.ToLower(args[0]) + var ruleType string + var expectNumArgs, startArg int + mode := Stop + switch arg0 { + case Continue: + mode = Continue + if len(args) < 2 { + return nil, fmt.Errorf("continue rule must begin with a rule type") + } + ruleType = strings.ToLower(args[1]) + expectNumArgs = len(args) - 1 + startArg = 2 + case Stop: + if len(args) < 2 { + return nil, fmt.Errorf("stop rule must begin with a rule type") + } + ruleType = strings.ToLower(args[1]) + expectNumArgs = len(args) - 1 + startArg = 2 + default: + // for backward compatibility + ruleType = arg0 + expectNumArgs = len(args) + startArg = 1 + } + + switch ruleType { + case "answer": + return nil, fmt.Errorf("response rewrites must begin with a name rule") + case "name": + return newNameRule(mode, args[startArg:]...) + case "class": + if expectNumArgs != 3 { + return nil, fmt.Errorf("%s rules must have exactly two arguments", ruleType) + } + return newClassRule(mode, args[startArg:]...) + case "type": + if expectNumArgs != 3 { + return nil, fmt.Errorf("%s rules must have exactly two arguments", ruleType) + } + return newTypeRule(mode, args[startArg:]...) + case "edns0": + return newEdns0Rule(mode, args[startArg:]...) + case "ttl": + return newTTLRule(mode, args[startArg:]...) + default: + return nil, fmt.Errorf("invalid rule type %q", args[0]) + } +} diff --git a/ag_201_coredns/plugin/rewrite/rewrite_test.go b/ag_201_coredns/plugin/rewrite/rewrite_test.go new file mode 100644 index 0000000..03d4fff --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/rewrite_test.go @@ -0,0 +1,747 @@ +package rewrite + +import ( + "bytes" + "context" + "fmt" + "reflect" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +func msgPrinter(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if len(r.Answer) == 0 { + r.Answer = []dns.RR{ + test.A(fmt.Sprintf("%s 5 IN A 10.0.0.1", r.Question[0].Name)), + } + } + w.WriteMsg(r) + return 0, nil +} + +func TestNewRule(t *testing.T) { + tests := []struct { + args []string + shouldError bool + expType reflect.Type + }{ + {[]string{}, true, nil}, + {[]string{"foo"}, true, nil}, + {[]string{"name"}, true, nil}, + {[]string{"name", "a.com"}, true, nil}, + {[]string{"name", "a.com", "b.com", "c.com"}, true, nil}, + {[]string{"name", "a.com", "b.com"}, false, reflect.TypeOf(&exactNameRule{})}, + {[]string{"name", "exact", "a.com", "b.com"}, false, reflect.TypeOf(&exactNameRule{})}, + {[]string{"name", "prefix", "a.com", "b.com"}, false, reflect.TypeOf(&prefixNameRule{})}, + {[]string{"name", "suffix", "a.com", "b.com"}, false, reflect.TypeOf(&suffixNameRule{})}, + {[]string{"name", "substring", "a.com", "b.com"}, false, reflect.TypeOf(&substringNameRule{})}, + {[]string{"name", "regex", "([a])\\.com", "new-{1}.com"}, false, reflect.TypeOf(®exNameRule{})}, + {[]string{"name", "regex", "([a]\\.com", "new-{1}.com"}, true, nil}, + {[]string{"name", "regex", "(dns)\\.(core)\\.(rocks)", "{2}.{1}.{3}", "answer", "name", "(core)\\.(dns)\\.(rocks)", "{2}.{1}.{3}"}, false, reflect.TypeOf(®exNameRule{})}, + {[]string{"name", "regex", "(adns)\\.(core)\\.(rocks)", "{2}.{1}.{3}", "answer", "name", "(core)\\.(adns)\\.(rocks)", "{2}.{1}.{3}", "too.long", "way.too.long"}, true, nil}, + {[]string{"name", "regex", "(bdns)\\.(core)\\.(rocks)", "{2}.{1}.{3}", "NoAnswer", "name", "(core)\\.(bdns)\\.(rocks)", "{2}.{1}.{3}"}, true, nil}, + {[]string{"name", "regex", "(cdns)\\.(core)\\.(rocks)", "{2}.{1}.{3}", "answer", "ttl", "(core)\\.(cdns)\\.(rocks)", "{2}.{1}.{3}"}, true, nil}, + {[]string{"name", "regex", "(ddns)\\.(core)\\.(rocks)", "{2}.{1}.{3}", "answer", "name", "\xecore\\.(ddns)\\.(rocks)", "{2}.{1}.{3}"}, true, nil}, + {[]string{"name", "regex", "\xedns\\.(core)\\.(rocks)", "{2}.{1}.{3}", "answer", "name", "(core)\\.(edns)\\.(rocks)", "{2}.{1}.{3}"}, true, nil}, + {[]string{"name", "substring", "fcore.dns.rocks", "dns.fcore.rocks", "answer", "name", "(fcore)\\.(dns)\\.(rocks)", "{2}.{1}.{3}"}, false, reflect.TypeOf(&substringNameRule{})}, + {[]string{"name", "substring", "a.com", "b.com", "c.com"}, true, nil}, + {[]string{"type"}, true, nil}, + {[]string{"type", "a"}, true, nil}, + {[]string{"type", "any", "a", "a"}, true, nil}, + {[]string{"type", "any", "a"}, false, reflect.TypeOf(&typeRule{})}, + {[]string{"type", "XY", "WV"}, true, nil}, + {[]string{"type", "ANY", "WV"}, true, nil}, + {[]string{"class"}, true, nil}, + {[]string{"class", "IN"}, true, nil}, + {[]string{"class", "ch", "in", "in"}, true, nil}, + {[]string{"class", "ch", "in"}, false, reflect.TypeOf(&classRule{})}, + {[]string{"class", "XY", "WV"}, true, nil}, + {[]string{"class", "IN", "WV"}, true, nil}, + {[]string{"edns0"}, true, nil}, + {[]string{"edns0", "local"}, true, nil}, + {[]string{"edns0", "local", "set"}, true, nil}, + {[]string{"edns0", "local", "set", "0xffee"}, true, nil}, + {[]string{"edns0", "local", "set", "65518", "abcdefg"}, false, reflect.TypeOf(&edns0LocalRule{})}, + {[]string{"edns0", "local", "set", "0xffee", "abcdefg"}, false, reflect.TypeOf(&edns0LocalRule{})}, + {[]string{"edns0", "local", "append", "0xffee", "abcdefg"}, false, reflect.TypeOf(&edns0LocalRule{})}, + {[]string{"edns0", "local", "replace", "0xffee", "abcdefg"}, false, reflect.TypeOf(&edns0LocalRule{})}, + {[]string{"edns0", "local", "foo", "0xffee", "abcdefg"}, true, nil}, + {[]string{"edns0", "local", "set", "0xffee", "0xabcdefg"}, true, nil}, + {[]string{"edns0", "nsid", "set", "junk"}, true, nil}, + {[]string{"edns0", "nsid", "set"}, false, reflect.TypeOf(&edns0NsidRule{})}, + {[]string{"edns0", "nsid", "append"}, false, reflect.TypeOf(&edns0NsidRule{})}, + {[]string{"edns0", "nsid", "replace"}, false, reflect.TypeOf(&edns0NsidRule{})}, + {[]string{"edns0", "nsid", "foo"}, true, nil}, + {[]string{"edns0", "local", "set", "0xffee", "{dummy}"}, true, nil}, + {[]string{"edns0", "local", "set", "0xffee", "{qname}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "set", "0xffee", "{qtype}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "set", "0xffee", "{client_ip}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "set", "0xffee", "{client_port}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "set", "0xffee", "{protocol}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "set", "0xffee", "{server_ip}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "set", "0xffee", "{server_port}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "append", "0xffee", "{dummy}"}, true, nil}, + {[]string{"edns0", "local", "append", "0xffee", "{qname}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "append", "0xffee", "{qtype}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "append", "0xffee", "{client_ip}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "append", "0xffee", "{client_port}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "append", "0xffee", "{protocol}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "append", "0xffee", "{server_ip}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "append", "0xffee", "{server_port}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "replace", "0xffee", "{dummy}"}, true, nil}, + {[]string{"edns0", "local", "replace", "0xffee", "{qname}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "replace", "0xffee", "{qtype}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "replace", "0xffee", "{client_ip}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "replace", "0xffee", "{client_port}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "replace", "0xffee", "{protocol}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "replace", "0xffee", "{server_ip}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "local", "replace", "0xffee", "{server_port}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"edns0", "subnet", "set", "-1", "56"}, true, nil}, + {[]string{"edns0", "subnet", "set", "24", "-56"}, true, nil}, + {[]string{"edns0", "subnet", "set", "33", "56"}, true, nil}, + {[]string{"edns0", "subnet", "set", "24", "129"}, true, nil}, + {[]string{"edns0", "subnet", "set", "24", "56"}, false, reflect.TypeOf(&edns0SubnetRule{})}, + {[]string{"edns0", "subnet", "append", "24", "56"}, false, reflect.TypeOf(&edns0SubnetRule{})}, + {[]string{"edns0", "subnet", "replace", "24", "56"}, false, reflect.TypeOf(&edns0SubnetRule{})}, + {[]string{"unknown-action", "name", "a.com", "b.com"}, true, nil}, + {[]string{"stop", "name", "a.com", "b.com"}, false, reflect.TypeOf(&exactNameRule{})}, + {[]string{"continue", "name", "a.com", "b.com"}, false, reflect.TypeOf(&exactNameRule{})}, + {[]string{"unknown-action", "type", "any", "a"}, true, nil}, + {[]string{"stop", "type", "any", "a"}, false, reflect.TypeOf(&typeRule{})}, + {[]string{"continue", "type", "any", "a"}, false, reflect.TypeOf(&typeRule{})}, + {[]string{"unknown-action", "class", "ch", "in"}, true, nil}, + {[]string{"stop", "class", "ch", "in"}, false, reflect.TypeOf(&classRule{})}, + {[]string{"continue", "class", "ch", "in"}, false, reflect.TypeOf(&classRule{})}, + {[]string{"unknown-action", "edns0", "local", "set", "0xffee", "abcedef"}, true, nil}, + {[]string{"stop", "edns0", "local", "set", "0xffee", "abcdefg"}, false, reflect.TypeOf(&edns0LocalRule{})}, + {[]string{"continue", "edns0", "local", "set", "0xffee", "abcdefg"}, false, reflect.TypeOf(&edns0LocalRule{})}, + {[]string{"unknown-action", "edns0", "nsid", "set"}, true, nil}, + {[]string{"stop", "edns0", "nsid", "set"}, false, reflect.TypeOf(&edns0NsidRule{})}, + {[]string{"continue", "edns0", "nsid", "set"}, false, reflect.TypeOf(&edns0NsidRule{})}, + {[]string{"unknown-action", "edns0", "local", "set", "0xffee", "{qname}"}, true, nil}, + {[]string{"stop", "edns0", "local", "set", "0xffee", "{qname}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"stop", "edns0", "local", "set", "0xffee", "{qtype}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"stop", "edns0", "local", "set", "0xffee", "{client_ip}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"stop", "edns0", "local", "set", "0xffee", "{client_port}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"stop", "edns0", "local", "set", "0xffee", "{protocol}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"stop", "edns0", "local", "set", "0xffee", "{server_ip}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"stop", "edns0", "local", "set", "0xffee", "{server_port}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"continue", "edns0", "local", "set", "0xffee", "{qname}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"continue", "edns0", "local", "set", "0xffee", "{qtype}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"continue", "edns0", "local", "set", "0xffee", "{client_ip}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"continue", "edns0", "local", "set", "0xffee", "{client_port}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"continue", "edns0", "local", "set", "0xffee", "{protocol}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"continue", "edns0", "local", "set", "0xffee", "{server_ip}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"continue", "edns0", "local", "set", "0xffee", "{server_port}"}, false, reflect.TypeOf(&edns0VariableRule{})}, + {[]string{"unknown-action", "edns0", "subnet", "set", "24", "64"}, true, nil}, + {[]string{"stop", "edns0", "subnet", "set", "24", "56"}, false, reflect.TypeOf(&edns0SubnetRule{})}, + {[]string{"stop", "edns0", "subnet", "append", "24", "56"}, false, reflect.TypeOf(&edns0SubnetRule{})}, + {[]string{"stop", "edns0", "subnet", "replace", "24", "56"}, false, reflect.TypeOf(&edns0SubnetRule{})}, + {[]string{"continue", "edns0", "subnet", "set", "24", "56"}, false, reflect.TypeOf(&edns0SubnetRule{})}, + {[]string{"continue", "edns0", "subnet", "append", "24", "56"}, false, reflect.TypeOf(&edns0SubnetRule{})}, + {[]string{"continue", "edns0", "subnet", "replace", "24", "56"}, false, reflect.TypeOf(&edns0SubnetRule{})}, + } + + for i, tc := range tests { + r, err := newRule(tc.args...) + if err == nil && tc.shouldError { + t.Errorf("Test %d: expected error but got success", i) + } else if err != nil && !tc.shouldError { + t.Errorf("Test %d: expected success but got error: %s", i, err) + } + + if !tc.shouldError && reflect.TypeOf(r) != tc.expType { + t.Errorf("Test %d: expected %q but got %q", i, tc.expType, r) + } + } +} + +func TestRewriteDefaultRevertPolicy(t *testing.T) { + rules := []Rule{} + + r, _ := newNameRule("stop", "prefix", "prefix", "to") + rules = append(rules, r) + r, _ = newNameRule("stop", "suffix", ".suffix.", ".nl.") + rules = append(rules, r) + r, _ = newNameRule("stop", "substring", "from.substring", "to") + rules = append(rules, r) + r, _ = newNameRule("stop", "regex", "(f.*m)\\.regex\\.(nl)", "to.{2}") + rules = append(rules, r) + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, + // use production (default) RevertPolicy + } + + tests := []struct { + from string + fromT uint16 + fromC uint16 + to string + toT uint16 + toC uint16 + }{ + {"prefix.nl.", dns.TypeA, dns.ClassINET, "to.nl.", dns.TypeA, dns.ClassINET}, + {"to.suffix.", dns.TypeA, dns.ClassINET, "to.nl.", dns.TypeA, dns.ClassINET}, + {"from.substring.nl.", dns.TypeA, dns.ClassINET, "to.nl.", dns.TypeA, dns.ClassINET}, + {"from.regex.nl.", dns.TypeA, dns.ClassINET, "to.nl.", dns.TypeA, dns.ClassINET}, + } + + ctx := context.TODO() + for i, tc := range tests { + m := new(dns.Msg) + m.SetQuestion(tc.from, tc.fromT) + m.Question[0].Qclass = tc.fromC + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + rw.ServeDNS(ctx, rec, m) + + resp := rec.Msg + + if resp.Question[0].Name != tc.from { + t.Errorf("Test %d: Expected Name in Question to be %q but was %q", i, tc.from, resp.Question[0].Name) + } + + if resp.Answer[0].Header().Name != tc.to { + t.Errorf("Test %d: Expected Name in Answer to be %q but was %q", i, tc.to, resp.Answer[0].Header().Name) + } + } +} + +func TestRewrite(t *testing.T) { + rules := []Rule{} + r, _ := newNameRule("stop", "from.nl.", "to.nl.") + rules = append(rules, r) + r, _ = newNameRule("stop", "regex", "(core)\\.(dns)\\.(rocks)\\.(nl)", "{2}.{1}.{3}.{4}", "answer", "name", "(dns)\\.(core)\\.(rocks)\\.(nl)", "{2}.{1}.{3}.{4}") + rules = append(rules, r) + r, _ = newNameRule("stop", "exact", "from.exact.nl.", "to.nl.") + rules = append(rules, r) + r, _ = newNameRule("stop", "prefix", "prefix", "to") + rules = append(rules, r) + r, _ = newNameRule("stop", "suffix", ".suffix.", ".nl.") + rules = append(rules, r) + r, _ = newNameRule("stop", "substring", "from.substring", "to") + rules = append(rules, r) + r, _ = newNameRule("stop", "regex", "(f.*m)\\.regex\\.(nl)", "to.{2}") + rules = append(rules, r) + r, _ = newNameRule("continue", "regex", "consul\\.(rocks)", "core.dns.{1}") + rules = append(rules, r) + r, _ = newNameRule("stop", "core.dns.rocks", "to.nl.") + rules = append(rules, r) + r, _ = newClassRule("continue", "HS", "CH") + rules = append(rules, r) + r, _ = newClassRule("stop", "CH", "IN") + rules = append(rules, r) + r, _ = newTypeRule("stop", "ANY", "HINFO") + rules = append(rules, r) + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, + RevertPolicy: NoRevertPolicy(), + } + + tests := []struct { + from string + fromT uint16 + fromC uint16 + to string + toT uint16 + toC uint16 + }{ + {"from.nl.", dns.TypeA, dns.ClassINET, "to.nl.", dns.TypeA, dns.ClassINET}, + {"a.nl.", dns.TypeA, dns.ClassINET, "a.nl.", dns.TypeA, dns.ClassINET}, + {"a.nl.", dns.TypeA, dns.ClassCHAOS, "a.nl.", dns.TypeA, dns.ClassINET}, + {"a.nl.", dns.TypeANY, dns.ClassINET, "a.nl.", dns.TypeHINFO, dns.ClassINET}, + // name is rewritten, type is not. + {"from.nl.", dns.TypeANY, dns.ClassINET, "to.nl.", dns.TypeANY, dns.ClassINET}, + {"from.exact.nl.", dns.TypeA, dns.ClassINET, "to.nl.", dns.TypeA, dns.ClassINET}, + {"prefix.nl.", dns.TypeA, dns.ClassINET, "to.nl.", dns.TypeA, dns.ClassINET}, + {"to.suffix.", dns.TypeA, dns.ClassINET, "to.nl.", dns.TypeA, dns.ClassINET}, + {"from.substring.nl.", dns.TypeA, dns.ClassINET, "to.nl.", dns.TypeA, dns.ClassINET}, + {"from.regex.nl.", dns.TypeA, dns.ClassINET, "to.nl.", dns.TypeA, dns.ClassINET}, + {"consul.rocks.", dns.TypeA, dns.ClassINET, "to.nl.", dns.TypeA, dns.ClassINET}, + // name is not, type is, but class is, because class is the 2nd rule. + {"a.nl.", dns.TypeANY, dns.ClassCHAOS, "a.nl.", dns.TypeANY, dns.ClassINET}, + // class gets rewritten twice because of continue/stop logic: HS to CH, CH to IN + {"a.nl.", dns.TypeANY, 4, "a.nl.", dns.TypeANY, dns.ClassINET}, + {"core.dns.rocks.nl.", dns.TypeA, dns.ClassINET, "dns.core.rocks.nl.", dns.TypeA, dns.ClassINET}, + } + + ctx := context.TODO() + for i, tc := range tests { + m := new(dns.Msg) + m.SetQuestion(tc.from, tc.fromT) + m.Question[0].Qclass = tc.fromC + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + rw.ServeDNS(ctx, rec, m) + + resp := rec.Msg + if resp.Question[0].Name != tc.to { + t.Errorf("Test %d: Expected Name to be %q but was %q", i, tc.to, resp.Question[0].Name) + } + if resp.Question[0].Qtype != tc.toT { + t.Errorf("Test %d: Expected Type to be '%d' but was '%d'", i, tc.toT, resp.Question[0].Qtype) + } + if resp.Question[0].Qclass != tc.toC { + t.Errorf("Test %d: Expected Class to be '%d' but was '%d'", i, tc.toC, resp.Question[0].Qclass) + } + if tc.fromT == dns.TypeA && tc.toT == dns.TypeA { + if len(resp.Answer) > 0 { + if resp.Answer[0].(*dns.A).Hdr.Name != tc.to { + t.Errorf("Test %d: Expected Answer Name to be %q but was %q", i, tc.to, resp.Answer[0].(*dns.A).Hdr.Name) + } + } + } + } +} + +func TestRewriteEDNS0Local(t *testing.T) { + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + RevertPolicy: NoRevertPolicy(), + } + + tests := []struct { + fromOpts []dns.EDNS0 + args []string + toOpts []dns.EDNS0 + doBool bool + }{ + { + []dns.EDNS0{}, + []string{"local", "set", "0xffee", "0xabcdef"}, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0xab, 0xcd, 0xef}}}, + false, + }, + { + []dns.EDNS0{}, + []string{"local", "append", "0xffee", "abcdefghijklmnop"}, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("abcdefghijklmnop")}}, + false, + }, + { + []dns.EDNS0{}, + []string{"local", "replace", "0xffee", "abcdefghijklmnop"}, + []dns.EDNS0{}, + true, + }, + { + []dns.EDNS0{}, + []string{"nsid", "set"}, + []dns.EDNS0{&dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""}}, + false, + }, + { + []dns.EDNS0{}, + []string{"nsid", "append"}, + []dns.EDNS0{&dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""}}, + true, + }, + { + []dns.EDNS0{}, + []string{"nsid", "replace"}, + []dns.EDNS0{}, + true, + }, + } + + ctx := context.TODO() + for i, tc := range tests { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + m.Question[0].Qclass = dns.ClassINET + + r, err := newEdns0Rule("stop", tc.args...) + if err != nil { + t.Errorf("Error creating test rule: %s", err) + continue + } + rw.Rules = []Rule{r} + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + rw.ServeDNS(ctx, rec, m) + + resp := rec.Msg + o := resp.IsEdns0() + o.SetDo(tc.doBool) + if o == nil { + t.Errorf("Test %d: EDNS0 options not set", i) + continue + } + if o.Do() != tc.doBool { + t.Errorf("Test %d: Expected %v but got %v", i, tc.doBool, o.Do()) + } + if !optsEqual(o.Option, tc.toOpts) { + t.Errorf("Test %d: Expected %v but got %v", i, tc.toOpts, o) + } + } +} + +func TestEdns0LocalMultiRule(t *testing.T) { + rules := []Rule{} + r, _ := newEdns0Rule("stop", "local", "replace", "0xffee", "abcdef") + rules = append(rules, r) + r, _ = newEdns0Rule("stop", "local", "set", "0xffee", "fedcba") + rules = append(rules, r) + + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, + RevertPolicy: NoRevertPolicy(), + } + + tests := []struct { + fromOpts []dns.EDNS0 + toOpts []dns.EDNS0 + }{ + { + nil, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("fedcba")}}, + }, + { + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("foobar")}}, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("abcdef")}}, + }, + } + + ctx := context.TODO() + for i, tc := range tests { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + m.Question[0].Qclass = dns.ClassINET + if tc.fromOpts != nil { + o := m.IsEdns0() + if o == nil { + m.SetEdns0(4096, true) + o = m.IsEdns0() + } + o.Option = append(o.Option, tc.fromOpts...) + } + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + rw.ServeDNS(ctx, rec, m) + + resp := rec.Msg + o := resp.IsEdns0() + if o == nil { + t.Errorf("Test %d: EDNS0 options not set", i) + continue + } + if !optsEqual(o.Option, tc.toOpts) { + t.Errorf("Test %d: Expected %v but got %v", i, tc.toOpts, o) + } + } +} + +func optsEqual(a, b []dns.EDNS0) bool { + if len(a) != len(b) { + return false + } + for i := range a { + switch aa := a[i].(type) { + case *dns.EDNS0_LOCAL: + if bb, ok := b[i].(*dns.EDNS0_LOCAL); ok { + if aa.Code != bb.Code { + return false + } + if !bytes.Equal(aa.Data, bb.Data) { + return false + } + } else { + return false + } + case *dns.EDNS0_NSID: + if bb, ok := b[i].(*dns.EDNS0_NSID); ok { + if aa.Nsid != bb.Nsid { + return false + } + } else { + return false + } + case *dns.EDNS0_SUBNET: + if bb, ok := b[i].(*dns.EDNS0_SUBNET); ok { + if aa.Code != bb.Code { + return false + } + if aa.Family != bb.Family { + return false + } + if aa.SourceNetmask != bb.SourceNetmask { + return false + } + if aa.SourceScope != bb.SourceScope { + return false + } + if !aa.Address.Equal(bb.Address) { + return false + } + } else { + return false + } + + default: + return false + } + } + return true +} + +type testProvider map[string]metadata.Func + +func (tp testProvider) Metadata(ctx context.Context, state request.Request) context.Context { + for k, v := range tp { + metadata.SetValueFunc(ctx, k, v) + } + return ctx +} + +func TestRewriteEDNS0LocalVariable(t *testing.T) { + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + RevertPolicy: NoRevertPolicy(), + } + + expectedMetadata := []metadata.Provider{ + testProvider{"test/label": func() string { return "my-value" }}, + testProvider{"test/empty": func() string { return "" }}, + } + + meta := metadata.Metadata{ + Zones: []string{"."}, + Providers: expectedMetadata, + Next: &rw, + } + + // test.ResponseWriter has the following values: + // The remote will always be 10.240.0.1 and port 40212. + // The local address is always 127.0.0.1 and port 53. + + tests := []struct { + fromOpts []dns.EDNS0 + args []string + toOpts []dns.EDNS0 + doBool bool + }{ + { + []dns.EDNS0{}, + []string{"local", "set", "0xffee", "{qname}"}, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("example.com.")}}, + true, + }, + { + []dns.EDNS0{}, + []string{"local", "set", "0xffee", "{qtype}"}, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x00, 0x01}}}, + false, + }, + { + []dns.EDNS0{}, + []string{"local", "set", "0xffee", "{client_ip}"}, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x0A, 0xF0, 0x00, 0x01}}}, + false, + }, + { + []dns.EDNS0{}, + []string{"local", "set", "0xffee", "{client_port}"}, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x9D, 0x14}}}, + true, + }, + { + []dns.EDNS0{}, + []string{"local", "set", "0xffee", "{protocol}"}, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("udp")}}, + false, + }, + { + []dns.EDNS0{}, + []string{"local", "set", "0xffee", "{server_port}"}, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x00, 0x35}}}, + true, + }, + { + []dns.EDNS0{}, + []string{"local", "set", "0xffee", "{server_ip}"}, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x7F, 0x00, 0x00, 0x01}}}, + true, + }, + { + []dns.EDNS0{}, + []string{"local", "set", "0xffee", "{test/label}"}, + []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("my-value")}}, + true, + }, + { + []dns.EDNS0{}, + []string{"local", "set", "0xffee", "{test/empty}"}, + nil, + false, + }, + { + []dns.EDNS0{}, + []string{"local", "set", "0xffee", "{test/does-not-exist}"}, + nil, + false, + }, + } + + for i, tc := range tests { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + + r, err := newEdns0Rule("stop", tc.args...) + if err != nil { + t.Errorf("Error creating test rule: %s", err) + continue + } + rw.Rules = []Rule{r} + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + ctx := meta.Collect(context.TODO(), request.Request{W: rec, Req: m}) + meta.ServeDNS(ctx, rec, m) + + resp := rec.Msg + o := resp.IsEdns0() + if o == nil { + if tc.toOpts != nil { + t.Errorf("Test %d: EDNS0 options not set", i) + } + continue + } + o.SetDo(tc.doBool) + if o.Do() != tc.doBool { + t.Errorf("Test %d: Expected %v but got %v", i, tc.doBool, o.Do()) + } + if !optsEqual(o.Option, tc.toOpts) { + t.Errorf("Test %d: Expected %v but got %v", i, tc.toOpts, o) + } + } +} + +func TestRewriteEDNS0Subnet(t *testing.T) { + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + RevertPolicy: NoRevertPolicy(), + } + + tests := []struct { + writer dns.ResponseWriter + fromOpts []dns.EDNS0 + args []string + toOpts []dns.EDNS0 + doBool bool + }{ + { + &test.ResponseWriter{}, + []dns.EDNS0{}, + []string{"subnet", "set", "24", "56"}, + []dns.EDNS0{&dns.EDNS0_SUBNET{Code: 0x8, + Family: 0x1, + SourceNetmask: 0x18, + SourceScope: 0x0, + Address: []byte{0x0A, 0xF0, 0x00, 0x00}, + }}, + true, + }, + { + &test.ResponseWriter{}, + []dns.EDNS0{}, + []string{"subnet", "set", "32", "56"}, + []dns.EDNS0{&dns.EDNS0_SUBNET{Code: 0x8, + Family: 0x1, + SourceNetmask: 0x20, + SourceScope: 0x0, + Address: []byte{0x0A, 0xF0, 0x00, 0x01}, + }}, + false, + }, + { + &test.ResponseWriter{}, + []dns.EDNS0{}, + []string{"subnet", "set", "0", "56"}, + []dns.EDNS0{&dns.EDNS0_SUBNET{Code: 0x8, + Family: 0x1, + SourceNetmask: 0x0, + SourceScope: 0x0, + Address: []byte{0x00, 0x00, 0x00, 0x00}, + }}, + false, + }, + { + &test.ResponseWriter6{}, + []dns.EDNS0{}, + []string{"subnet", "set", "24", "56"}, + []dns.EDNS0{&dns.EDNS0_SUBNET{Code: 0x8, + Family: 0x2, + SourceNetmask: 0x38, + SourceScope: 0x0, + Address: []byte{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + }}, + true, + }, + { + &test.ResponseWriter6{}, + []dns.EDNS0{}, + []string{"subnet", "set", "24", "128"}, + []dns.EDNS0{&dns.EDNS0_SUBNET{Code: 0x8, + Family: 0x2, + SourceNetmask: 0x80, + SourceScope: 0x0, + Address: []byte{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x42, 0x00, 0xff, 0xfe, 0xca, 0x4c, 0x65}, + }}, + false, + }, + { + &test.ResponseWriter6{}, + []dns.EDNS0{}, + []string{"subnet", "set", "24", "0"}, + []dns.EDNS0{&dns.EDNS0_SUBNET{Code: 0x8, + Family: 0x2, + SourceNetmask: 0x0, + SourceScope: 0x0, + Address: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + }}, + true, + }, + } + + ctx := context.TODO() + for i, tc := range tests { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + + r, err := newEdns0Rule("stop", tc.args...) + if err != nil { + t.Errorf("Error creating test rule: %s", err) + continue + } + rw.Rules = []Rule{r} + rec := dnstest.NewRecorder(tc.writer) + rw.ServeDNS(ctx, rec, m) + + resp := rec.Msg + o := resp.IsEdns0() + o.SetDo(tc.doBool) + if o == nil { + t.Errorf("Test %d: EDNS0 options not set", i) + continue + } + if o.Do() != tc.doBool { + t.Errorf("Test %d: Expected %v but got %v", i, tc.doBool, o.Do()) + } + if !optsEqual(o.Option, tc.toOpts) { + t.Errorf("Test %d: Expected %v but got %v", i, tc.toOpts, o) + } + } +} diff --git a/ag_201_coredns/plugin/rewrite/setup.go b/ag_201_coredns/plugin/rewrite/setup.go new file mode 100644 index 0000000..36f31dc --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/setup.go @@ -0,0 +1,42 @@ +package rewrite + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("rewrite", setup) } + +func setup(c *caddy.Controller) error { + rewrites, err := rewriteParse(c) + if err != nil { + return plugin.Error("rewrite", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return Rewrite{Next: next, Rules: rewrites} + }) + + return nil +} + +func rewriteParse(c *caddy.Controller) ([]Rule, error) { + var rules []Rule + + for c.Next() { + args := c.RemainingArgs() + if len(args) < 2 { + // Handles rules out of nested instructions, i.e. the ones enclosed in curly brackets + for c.NextBlock() { + args = append(args, c.Val()) + } + } + rule, err := newRule(args...) + if err != nil { + return nil, err + } + rules = append(rules, rule) + } + return rules, nil +} diff --git a/ag_201_coredns/plugin/rewrite/setup_test.go b/ag_201_coredns/plugin/rewrite/setup_test.go new file mode 100644 index 0000000..88d332f --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/setup_test.go @@ -0,0 +1,51 @@ +package rewrite + +import ( + "strings" + "testing" + + "github.com/coredns/caddy" +) + +func TestParse(t *testing.T) { + tests := []struct { + inputFileRules string + shouldErr bool + errContains string + }{ + // parse errors + {`rewrite`, true, ""}, + {`rewrite name`, true, ""}, + {`rewrite name a.com b.com`, false, ""}, + {`rewrite stop { + name regex foo bar + answer name bar foo +}`, false, ""}, + {`rewrite stop name regex foo bar answer name bar foo`, false, ""}, + {`rewrite stop { + name regex foo bar + answer name bar foo + name baz +}`, true, "2 arguments required"}, + {`rewrite stop { + answer name bar foo + name regex foo bar +}`, true, "must begin with a name rule"}, + {`rewrite stop`, true, ""}, + {`rewrite continue`, true, ""}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputFileRules) + _, err := rewriteParse(c) + if err == nil && test.shouldErr { + t.Fatalf("Test %d expected errors, but got no error\n---\n%s", i, test.inputFileRules) + } else if err != nil && !test.shouldErr { + t.Fatalf("Test %d expected no errors, but got '%v'\n---\n%s", i, err, test.inputFileRules) + } + + if err != nil && test.errContains != "" && !strings.Contains(err.Error(), test.errContains) { + t.Errorf("Test %d got wrong error for invalid response rewrite: '%v'\n---\n%s", i, err.Error(), test.inputFileRules) + } + } +} diff --git a/ag_201_coredns/plugin/rewrite/ttl.go b/ag_201_coredns/plugin/rewrite/ttl.go new file mode 100644 index 0000000..1791301 --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/ttl.go @@ -0,0 +1,205 @@ +package rewrite + +import ( + "context" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +type ttlResponseRule struct { + minTTL uint32 + maxTTL uint32 +} + +func (r *ttlResponseRule) RewriteResponse(rr dns.RR) { + if rr.Header().Ttl < r.minTTL { + rr.Header().Ttl = r.minTTL + } else if rr.Header().Ttl > r.maxTTL { + rr.Header().Ttl = r.maxTTL + } +} + +type ttlRuleBase struct { + nextAction string + response ttlResponseRule +} + +func newTTLRuleBase(nextAction string, minTtl, maxTtl uint32) ttlRuleBase { + return ttlRuleBase{ + nextAction: nextAction, + response: ttlResponseRule{minTTL: minTtl, maxTTL: maxTtl}, + } +} + +func (rule *ttlRuleBase) responseRule(match bool) (ResponseRules, Result) { + if match { + return ResponseRules{&rule.response}, RewriteDone + } + return nil, RewriteIgnored +} + +// Mode returns the processing nextAction +func (rule *ttlRuleBase) Mode() string { return rule.nextAction } + +type exactTTLRule struct { + ttlRuleBase + From string +} + +type prefixTTLRule struct { + ttlRuleBase + Prefix string +} + +type suffixTTLRule struct { + ttlRuleBase + Suffix string +} + +type substringTTLRule struct { + ttlRuleBase + Substring string +} + +type regexTTLRule struct { + ttlRuleBase + Pattern *regexp.Regexp +} + +// Rewrite rewrites the current request based upon exact match of the name +// in the question section of the request. +func (rule *exactTTLRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + return rule.responseRule(rule.From == state.Name()) +} + +// Rewrite rewrites the current request when the name begins with the matching string. +func (rule *prefixTTLRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + return rule.responseRule(strings.HasPrefix(state.Name(), rule.Prefix)) +} + +// Rewrite rewrites the current request when the name ends with the matching string. +func (rule *suffixTTLRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + return rule.responseRule(strings.HasSuffix(state.Name(), rule.Suffix)) +} + +// Rewrite rewrites the current request based upon partial match of the +// name in the question section of the request. +func (rule *substringTTLRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + return rule.responseRule(strings.Contains(state.Name(), rule.Substring)) +} + +// Rewrite rewrites the current request when the name in the question +// section of the request matches a regular expression. +func (rule *regexTTLRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + return rule.responseRule(len(rule.Pattern.FindStringSubmatch(state.Name())) != 0) +} + +// newTTLRule creates a name matching rule based on exact, partial, or regex match +func newTTLRule(nextAction string, args ...string) (Rule, error) { + if len(args) < 2 { + return nil, fmt.Errorf("too few (%d) arguments for a ttl rule", len(args)) + } + var s string + if len(args) == 2 { + s = args[1] + } + if len(args) == 3 { + s = args[2] + } + minTtl, maxTtl, valid := isValidTTL(s) + if !valid { + return nil, fmt.Errorf("invalid TTL '%s' for a ttl rule", s) + } + if len(args) == 3 { + switch strings.ToLower(args[0]) { + case ExactMatch: + return &exactTTLRule{ + newTTLRuleBase(nextAction, minTtl, maxTtl), + plugin.Name(args[1]).Normalize(), + }, nil + case PrefixMatch: + return &prefixTTLRule{ + newTTLRuleBase(nextAction, minTtl, maxTtl), + plugin.Name(args[1]).Normalize(), + }, nil + case SuffixMatch: + return &suffixTTLRule{ + newTTLRuleBase(nextAction, minTtl, maxTtl), + plugin.Name(args[1]).Normalize(), + }, nil + case SubstringMatch: + return &substringTTLRule{ + newTTLRuleBase(nextAction, minTtl, maxTtl), + plugin.Name(args[1]).Normalize(), + }, nil + case RegexMatch: + regexPattern, err := regexp.Compile(args[1]) + if err != nil { + return nil, fmt.Errorf("invalid regex pattern in a ttl rule: %s", args[1]) + } + return ®exTTLRule{ + newTTLRuleBase(nextAction, minTtl, maxTtl), + regexPattern, + }, nil + default: + return nil, fmt.Errorf("ttl rule supports only exact, prefix, suffix, substring, and regex name matching") + } + } + if len(args) > 3 { + return nil, fmt.Errorf("many few arguments for a ttl rule") + } + return &exactTTLRule{ + newTTLRuleBase(nextAction, minTtl, maxTtl), + plugin.Name(args[0]).Normalize(), + }, nil +} + +// validTTL returns true if v is valid TTL value. +func isValidTTL(v string) (uint32, uint32, bool) { + s := strings.Split(v, "-") + if len(s) == 1 { + i, err := strconv.ParseUint(s[0], 10, 32) + if err != nil { + return 0, 0, false + } + return uint32(i), uint32(i), true + } + if len(s) == 2 { + var min, max uint64 + var err error + if s[0] == "" { + min = 0 + } else { + min, err = strconv.ParseUint(s[0], 10, 32) + if err != nil { + return 0, 0, false + } + } + if s[1] == "" { + if s[0] == "" { + // explicitly reject ttl directive "-" that would otherwise be interpreted + // as 0-2147483647 which is pretty useless + return 0, 0, false + } + max = 2147483647 + } else { + max, err = strconv.ParseUint(s[1], 10, 32) + if err != nil { + return 0, 0, false + } + } + if min > max { + // reject invalid range + return 0, 0, false + } + return uint32(min), uint32(max), true + } + return 0, 0, false +} diff --git a/ag_201_coredns/plugin/rewrite/ttl_test.go b/ag_201_coredns/plugin/rewrite/ttl_test.go new file mode 100644 index 0000000..40fa097 --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/ttl_test.go @@ -0,0 +1,157 @@ +package rewrite + +import ( + "context" + "reflect" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestNewTTLRule(t *testing.T) { + tests := []struct { + next string + args []string + expectedFail bool + }{ + {"stop", []string{"srv1.coredns.rocks", "10"}, false}, + {"stop", []string{"exact", "srv1.coredns.rocks", "15"}, false}, + {"stop", []string{"prefix", "coredns.rocks", "20"}, false}, + {"stop", []string{"suffix", "srv1", "25"}, false}, + {"stop", []string{"substring", "coredns", "30"}, false}, + {"stop", []string{"regex", `(srv1)\.(coredns)\.(rocks)`, "35"}, false}, + {"continue", []string{"srv1.coredns.rocks", "10"}, false}, + {"continue", []string{"exact", "srv1.coredns.rocks", "15"}, false}, + {"continue", []string{"prefix", "coredns.rocks", "20"}, false}, + {"continue", []string{"suffix", "srv1", "25"}, false}, + {"continue", []string{"substring", "coredns", "30"}, false}, + {"continue", []string{"regex", `(srv1)\.(coredns)\.(rocks)`, "35"}, false}, + {"stop", []string{"srv1.coredns.rocks", "12345678901234567890"}, true}, + {"stop", []string{"srv1.coredns.rocks", "coredns.rocks"}, true}, + {"stop", []string{"srv1.coredns.rocks", "#1"}, true}, + {"stop", []string{"range.coredns.rocks", "1-2"}, false}, + {"stop", []string{"ceil.coredns.rocks", "-2"}, false}, + {"stop", []string{"floor.coredns.rocks", "1-"}, false}, + {"stop", []string{"range.coredns.rocks", "2-2"}, false}, + {"stop", []string{"invalid.coredns.rocks", "-"}, true}, + {"stop", []string{"invalid.coredns.rocks", "2-1"}, true}, + {"stop", []string{"invalid.coredns.rocks", "5-10-20"}, true}, + } + for i, tc := range tests { + failed := false + rule, err := newTTLRule(tc.next, tc.args...) + if err != nil { + failed = true + } + if !failed && !tc.expectedFail { + continue + } + if failed && tc.expectedFail { + continue + } + t.Fatalf("Test %d: FAIL, expected fail=%t, but received fail=%t: (%s) %s, rule=%v", i, tc.expectedFail, failed, tc.next, tc.args, rule) + } + for i, tc := range tests { + failed := false + tc.args = append([]string{tc.next, "ttl"}, tc.args...) + rule, err := newRule(tc.args...) + if err != nil { + failed = true + } + if !failed && !tc.expectedFail { + continue + } + if failed && tc.expectedFail { + continue + } + t.Fatalf("Test %d: FAIL, expected fail=%t, but received fail=%t: (%s) %s, rule=%v", i, tc.expectedFail, failed, tc.next, tc.args, rule) + } +} + +func TestTtlRewrite(t *testing.T) { + rules := []Rule{} + ruleset := []struct { + args []string + expectedType reflect.Type + }{ + {[]string{"stop", "ttl", "srv1.coredns.rocks", "1"}, reflect.TypeOf(&exactTTLRule{})}, + {[]string{"stop", "ttl", "exact", "srv15.coredns.rocks", "15"}, reflect.TypeOf(&exactTTLRule{})}, + {[]string{"stop", "ttl", "prefix", "srv30", "30"}, reflect.TypeOf(&prefixTTLRule{})}, + {[]string{"stop", "ttl", "suffix", "45.coredns.rocks", "45"}, reflect.TypeOf(&suffixTTLRule{})}, + {[]string{"stop", "ttl", "substring", "rv50", "50"}, reflect.TypeOf(&substringTTLRule{})}, + {[]string{"stop", "ttl", "regex", `(srv10)\.(coredns)\.(rocks)`, "10"}, reflect.TypeOf(®exTTLRule{})}, + {[]string{"stop", "ttl", "regex", `(srv20)\.(coredns)\.(rocks)`, "20"}, reflect.TypeOf(®exTTLRule{})}, + {[]string{"stop", "ttl", "range.example.com.", "30-300"}, reflect.TypeOf(&exactTTLRule{})}, + {[]string{"stop", "ttl", "ceil.example.com.", "-11"}, reflect.TypeOf(&exactTTLRule{})}, + {[]string{"stop", "ttl", "floor.example.com.", "5-"}, reflect.TypeOf(&exactTTLRule{})}, + } + for i, r := range ruleset { + rule, err := newRule(r.args...) + if err != nil { + t.Fatalf("Rule %d: FAIL, %s: %s", i, r.args, err) + } + if reflect.TypeOf(rule) != r.expectedType { + t.Fatalf("Rule %d: FAIL, %s: rule type mismatch, expected %q, but got %q", i, r.args, r.expectedType, rule) + } + rules = append(rules, rule) + } + doTTLTests(rules, t) +} + +func doTTLTests(rules []Rule, t *testing.T) { + tests := []struct { + from string + fromType uint16 + answer []dns.RR + ttl uint32 + noRewrite bool + }{ + {"srv1.coredns.rocks.", dns.TypeA, []dns.RR{test.A("srv1.coredns.rocks. 5 IN A 10.0.0.1")}, 1, false}, + {"srv15.coredns.rocks.", dns.TypeA, []dns.RR{test.A("srv15.coredns.rocks. 5 IN A 10.0.0.15")}, 15, false}, + {"srv30.coredns.rocks.", dns.TypeA, []dns.RR{test.A("srv30.coredns.rocks. 5 IN A 10.0.0.30")}, 30, false}, + {"srv45.coredns.rocks.", dns.TypeA, []dns.RR{test.A("srv45.coredns.rocks. 5 IN A 10.0.0.45")}, 45, false}, + {"srv50.coredns.rocks.", dns.TypeA, []dns.RR{test.A("srv50.coredns.rocks. 5 IN A 10.0.0.50")}, 50, false}, + {"srv10.coredns.rocks.", dns.TypeA, []dns.RR{test.A("srv10.coredns.rocks. 5 IN A 10.0.0.10")}, 10, false}, + {"xmpp.coredns.rocks.", dns.TypeSRV, []dns.RR{test.SRV("xmpp.coredns.rocks. 5 IN SRV 0 100 100 srvxmpp.coredns.rocks.")}, 5, true}, + {"srv15.coredns.rocks.", dns.TypeHINFO, []dns.RR{test.HINFO("srv15.coredns.rocks. 5 HINFO INTEL-64 \"RHEL 7.5\"")}, 15, false}, + {"srv20.coredns.rocks.", dns.TypeA, []dns.RR{ + test.A("srv20.coredns.rocks. 5 IN A 10.0.0.22"), + test.A("srv20.coredns.rocks. 5 IN A 10.0.0.23"), + }, 20, false}, + {"range.example.com.", dns.TypeA, []dns.RR{test.A("range.example.com. 5 IN A 10.0.0.1")}, 30, false}, + {"range.example.com.", dns.TypeA, []dns.RR{test.A("range.example.com. 55 IN A 10.0.0.1")}, 55, false}, + {"range.example.com.", dns.TypeA, []dns.RR{test.A("range.example.com. 500 IN A 10.0.0.1")}, 300, false}, + {"ceil.example.com.", dns.TypeA, []dns.RR{test.A("ceil.example.com. 5 IN A 10.0.0.1")}, 5, false}, + {"ceil.example.com.", dns.TypeA, []dns.RR{test.A("ceil.example.com. 15 IN A 10.0.0.1")}, 11, false}, + {"floor.example.com.", dns.TypeA, []dns.RR{test.A("floor.example.com. 0 IN A 10.0.0.1")}, 5, false}, + {"floor.example.com.", dns.TypeA, []dns.RR{test.A("floor.example.com. 30 IN A 10.0.0.1")}, 30, false}, + } + ctx := context.TODO() + for i, tc := range tests { + m := new(dns.Msg) + m.SetQuestion(tc.from, tc.fromType) + m.Question[0].Qclass = dns.ClassINET + m.Answer = tc.answer + rw := Rewrite{ + Next: plugin.HandlerFunc(msgPrinter), + Rules: rules, + } + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + rw.ServeDNS(ctx, rec, m) + resp := rec.Msg + if len(resp.Answer) == 0 { + t.Errorf("Test %d: FAIL %s (%d) Expected valid response but received %q", i, tc.from, tc.fromType, resp) + continue + } + for _, a := range resp.Answer { + if a.Header().Ttl != tc.ttl { + t.Errorf("Test %d: FAIL %s (%d) Expected TTL to be %d but was %d", i, tc.from, tc.fromType, tc.ttl, a.Header().Ttl) + break + } + } + } +} diff --git a/ag_201_coredns/plugin/rewrite/type.go b/ag_201_coredns/plugin/rewrite/type.go new file mode 100644 index 0000000..63796e9 --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/type.go @@ -0,0 +1,45 @@ +// Package rewrite is a plugin for rewriting requests internally to something different. +package rewrite + +import ( + "context" + "fmt" + "strings" + + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// typeRule is a type rewrite rule. +type typeRule struct { + fromType uint16 + toType uint16 + nextAction string +} + +func newTypeRule(nextAction string, args ...string) (Rule, error) { + var from, to uint16 + var ok bool + if from, ok = dns.StringToType[strings.ToUpper(args[0])]; !ok { + return nil, fmt.Errorf("invalid type %q", strings.ToUpper(args[0])) + } + if to, ok = dns.StringToType[strings.ToUpper(args[1])]; !ok { + return nil, fmt.Errorf("invalid type %q", strings.ToUpper(args[1])) + } + return &typeRule{from, to, nextAction}, nil +} + +// Rewrite rewrites the current request. +func (rule *typeRule) Rewrite(ctx context.Context, state request.Request) (ResponseRules, Result) { + if rule.fromType > 0 && rule.toType > 0 { + if state.QType() == rule.fromType { + state.Req.Question[0].Qtype = rule.toType + return nil, RewriteDone + } + } + return nil, RewriteIgnored +} + +// Mode returns the processing mode. +func (rule *typeRule) Mode() string { return rule.nextAction } diff --git a/ag_201_coredns/plugin/rewrite/wire.go b/ag_201_coredns/plugin/rewrite/wire.go new file mode 100644 index 0000000..11b4dac --- /dev/null +++ b/ag_201_coredns/plugin/rewrite/wire.go @@ -0,0 +1,35 @@ +package rewrite + +import ( + "encoding/binary" + "fmt" + "net" + "strconv" +) + +// ipToWire writes IP address to wire/binary format, 4 or 16 bytes depends on IPV4 or IPV6. +func ipToWire(family int, ipAddr string) ([]byte, error) { + switch family { + case 1: + return net.ParseIP(ipAddr).To4(), nil + case 2: + return net.ParseIP(ipAddr).To16(), nil + } + return nil, fmt.Errorf("invalid IP address family (i.e. version) %d", family) +} + +// uint16ToWire writes unit16 to wire/binary format +func uint16ToWire(data uint16) []byte { + buf := make([]byte, 2) + binary.BigEndian.PutUint16(buf, uint16(data)) + return buf +} + +// portToWire writes port to wire/binary format, 2 bytes +func portToWire(portStr string) ([]byte, error) { + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return nil, err + } + return uint16ToWire(uint16(port)), nil +} diff --git a/ag_201_coredns/plugin/root/README.md b/ag_201_coredns/plugin/root/README.md new file mode 100644 index 0000000..1d21bc0 --- /dev/null +++ b/ag_201_coredns/plugin/root/README.md @@ -0,0 +1,30 @@ +# root + +## Name + +*root* - simply specifies the root of where to find (zone) files. + +## Description + +The default root is the current working directory of CoreDNS. The *root* plugin allows you to change +this. A relative root path is relative to the current working directory. + +This plugin can only be used once per Server Block. + +## Syntax + +~~~ txt +root PATH +~~~ + +**PATH** is the directory to set as CoreDNS' root. + +## Examples + +Serve zone data (when the *file* plugin is used) from `/etc/coredns/zones`: + +~~~ corefile +. { + root /etc/coredns/zones +} +~~~ diff --git a/ag_201_coredns/plugin/root/log_test.go b/ag_201_coredns/plugin/root/log_test.go new file mode 100644 index 0000000..f63caac --- /dev/null +++ b/ag_201_coredns/plugin/root/log_test.go @@ -0,0 +1,5 @@ +package root + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/root/root.go b/ag_201_coredns/plugin/root/root.go new file mode 100644 index 0000000..b8bdf94 --- /dev/null +++ b/ag_201_coredns/plugin/root/root.go @@ -0,0 +1,39 @@ +package root + +import ( + "os" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" +) + +var log = clog.NewWithPlugin("root") + +func init() { plugin.Register("root", setup) } + +func setup(c *caddy.Controller) error { + config := dnsserver.GetConfig(c) + + for c.Next() { + if !c.NextArg() { + return plugin.Error("root", c.ArgErr()) + } + config.Root = c.Val() + } + + // Check if root path exists + _, err := os.Stat(config.Root) + if err != nil { + if os.IsNotExist(err) { + // Allow this, because the folder might appear later. + // But make sure the user knows! + log.Warningf("Root path does not exist: %s", config.Root) + } else { + return plugin.Error("root", c.Errf("unable to access root path '%s': %v", config.Root, err)) + } + } + + return nil +} diff --git a/ag_201_coredns/plugin/root/root_test.go b/ag_201_coredns/plugin/root/root_test.go new file mode 100644 index 0000000..27bdf84 --- /dev/null +++ b/ag_201_coredns/plugin/root/root_test.go @@ -0,0 +1,102 @@ +package root + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" +) + +func TestRoot(t *testing.T) { + // Predefined error substrings + parseErrContent := "Error during parsing:" + unableToAccessErrContent := "unable to access root path" + + existingDirPath, err := getTempDirPath() + if err != nil { + t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err) + } + + nonExistingDir := filepath.Join(existingDirPath, "highly_unlikely_to_exist_dir") + + existingFile, err := os.CreateTemp("", "root_test") + if err != nil { + t.Fatalf("BeforeTest: Failed to create temp file for testing! Error was: %v", err) + } + defer func() { + existingFile.Close() + os.Remove(existingFile.Name()) + }() + + inaccessiblePath := getInaccessiblePath(existingFile.Name()) + + tests := []struct { + input string + shouldErr bool + expectedRoot string // expected root, set to the controller. Empty for negative cases. + expectedErrContent string // substring from the expected error. Empty for positive cases. + }{ + // positive + { + fmt.Sprintf(`root %s`, nonExistingDir), false, nonExistingDir, "", + }, + { + fmt.Sprintf(`root %s`, existingDirPath), false, existingDirPath, "", + }, + // negative + { + `root `, true, "", parseErrContent, + }, + { + fmt.Sprintf(`root %s`, inaccessiblePath), true, "", unableToAccessErrContent, + }, + { + fmt.Sprintf(`root { + %s + }`, existingDirPath), true, "", parseErrContent, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + err := setup(c) + cfg := dnsserver.GetConfig(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, 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) + } + } + + // check root only if we are in a positive test. + if !test.shouldErr && test.expectedRoot != cfg.Root { + t.Errorf("Root not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedRoot, cfg.Root) + } + } +} + +// getTempDirPath returns the path to the system temp directory. If it does not exist - an error is returned. +func getTempDirPath() (string, error) { + tempDir := os.TempDir() + _, err := os.Stat(tempDir) + if err != nil { + return "", err + } + return tempDir, nil +} + +func getInaccessiblePath(file string) string { + return filepath.Join("C:", "file\x00name") // null byte in filename is not allowed on Windows AND unix +} diff --git a/ag_201_coredns/plugin/route53/README.md b/ag_201_coredns/plugin/route53/README.md new file mode 100644 index 0000000..d3f982d --- /dev/null +++ b/ag_201_coredns/plugin/route53/README.md @@ -0,0 +1,131 @@ +# route53 + +## Name + +*route53* - enables serving zone data from AWS route53. + +## Description + +The route53 plugin is useful for serving zones from resource record +sets in AWS route53. This plugin supports all Amazon Route 53 records +([https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html)). +The route53 plugin can be used when CoreDNS is deployed on AWS or elsewhere. + +## Syntax + +~~~ txt +route53 [ZONE:HOSTED_ZONE_ID...] { + aws_access_key [AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY] # Deprecated, uses other authentication methods instead. + aws_endpoint ENDPOINT + credentials PROFILE [FILENAME] + fallthrough [ZONES...] + refresh DURATION +} +~~~ + +* **ZONE** the name of the domain to be accessed. When there are multiple zones with overlapping + domains (private vs. public hosted zone), CoreDNS does the lookup in the given order here. + Therefore, for a non-existing resource record, SOA response will be from the rightmost zone. + +* **HOSTED\_ZONE\_ID** the ID of the hosted zone that contains the resource record sets to be + accessed. + +* **AWS\_ACCESS\_KEY\_ID** and **AWS\_SECRET\_ACCESS\_KEY** the AWS access key ID and secret access key + to be used when querying AWS (optional). If they are not provided, CoreDNS tries to access + AWS credentials the same way as AWS CLI - environment variables, shared credential file (and optionally + shared config file if `AWS_SDK_LOAD_CONFIG` env is set), and lastly EC2 Instance Roles. + Note the usage of `aws_access_key` has been deprecated and may be removed in future versions. Instead, + user can use other methods to pass crentials, e.g., with environmental variable `AWS_ACCESS_KEY_ID` and + `AWS_SECRET_ACCESS_KEY`, respectively. + +* `aws_endpoint` can be used to control the endpoint to use when querying AWS (optional). **ENDPOINT** is the + URL of the endpoint to use. If this is not provided the default AWS endpoint resolution will occur. + +* `credentials` is used for overriding the shared credentials **FILENAME** and the **PROFILE** name for a + given zone. **PROFILE** is the AWS account profile name. Defaults to `default`. **FILENAME** is the + AWS shared credentials filename, defaults to `~/.aws/credentials`. CoreDNS will only load shared credentials + file and not shared config file (`~/.aws/config`) by default. Set `AWS_SDK_LOAD_CONFIG` env variable to + a truthy value to enable also loading of `~/.aws/config` (e.g. if you want to provide assumed IAM role + configuration). Will be ignored if static keys are set via `aws_access_key`. + +* `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. + +* `refresh` can be used to control how long between record retrievals from Route 53. It requires + a duration string as a parameter to specify the duration between update cycles. Each update + cycle may result in many AWS API calls depending on how many domains use this plugin and how + many records are in each. Adjusting the update frequency may help reduce the potential of API + rate-limiting imposed by AWS. + +* **DURATION** A duration string. Defaults to `1m`. If units are unspecified, seconds are assumed. + +## Examples + +Enable route53 with implicit AWS credentials and resolve CNAMEs via 10.0.0.1: + +~~~ txt +example.org { + route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 +} + +. { + forward . 10.0.0.1 +} +~~~ + +Enable route53 with explicit AWS credentials: + +~~~ txt +example.org { + route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 { + aws_access_key AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY # Deprecated, uses other authentication methods instead. + } +} +~~~ + +Enable route53 with an explicit AWS endpoint: + +~~~ txt +example.org { + route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 { + aws_endpoint https://test.us-west-2.amazonaws.com + } +} +~~~ + +Enable route53 with fallthrough: + +~~~ txt +. { + route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 example.gov.:Z654321543245 { + fallthrough example.gov. + } +} +~~~ + +Enable route53 with multiple hosted zones with the same domain: + +~~~ txt +example.org { + route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 example.org.:Z93A52145678156 +} +~~~ + +Enable route53 and refresh records every 3 minutes +~~~ txt +example.org { + route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 { + refresh 3m + } +} +~~~ + +## Authentication + +Route53 plugin uses [AWS Go SDK](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html) +for authentication, where there is a list of accepted configuration methods. +Note the usage of `aws_access_key` in Corefile has been deprecated and may be removed in future versions. Instead, +user can use other methods to pass crentials, e.g., with environmental variable `AWS_ACCESS_KEY_ID` and +`AWS_SECRET_ACCESS_KEY`, respectively. diff --git a/ag_201_coredns/plugin/route53/log_test.go b/ag_201_coredns/plugin/route53/log_test.go new file mode 100644 index 0000000..20d1f87 --- /dev/null +++ b/ag_201_coredns/plugin/route53/log_test.go @@ -0,0 +1,5 @@ +package route53 + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/route53/route53.go b/ag_201_coredns/plugin/route53/route53.go new file mode 100644 index 0000000..abe5a7d --- /dev/null +++ b/ag_201_coredns/plugin/route53/route53.go @@ -0,0 +1,293 @@ +// Package route53 implements a plugin that returns resource records +// from AWS route53. +package route53 + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/plugin/pkg/upstream" + "github.com/coredns/coredns/request" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/service/route53/route53iface" + "github.com/miekg/dns" +) + +// Route53 is a plugin that returns RR from AWS route53. +type Route53 struct { + Next plugin.Handler + Fall fall.F + + zoneNames []string + client route53iface.Route53API + upstream *upstream.Upstream + refresh time.Duration + + zMu sync.RWMutex + zones zones +} + +type zone struct { + id string + z *file.Zone + dns string +} + +type zones map[string][]*zone + +// New reads from the keys map which uses domain names as its key and hosted +// zone id lists as its values, validates that each domain name/zone id pair +// does exist, and returns a new *Route53. In addition to this, upstream is use +// for doing recursive queries against CNAMEs. Returns error if it cannot +// verify any given domain name/zone id pair. +func New(ctx context.Context, c route53iface.Route53API, keys map[string][]string, refresh time.Duration) (*Route53, error) { + zones := make(map[string][]*zone, len(keys)) + zoneNames := make([]string, 0, len(keys)) + for dns, hostedZoneIDs := range keys { + for _, hostedZoneID := range hostedZoneIDs { + _, err := c.ListHostedZonesByNameWithContext(ctx, &route53.ListHostedZonesByNameInput{ + DNSName: aws.String(dns), + HostedZoneId: aws.String(hostedZoneID), + }) + if err != nil { + return nil, err + } + if _, ok := zones[dns]; !ok { + zoneNames = append(zoneNames, dns) + } + zones[dns] = append(zones[dns], &zone{id: hostedZoneID, dns: dns, z: file.NewZone(dns, "")}) + } + } + return &Route53{ + client: c, + zoneNames: zoneNames, + zones: zones, + upstream: upstream.New(), + refresh: refresh, + }, nil +} + +// Run executes first update, spins up an update forever-loop. +// Returns error if first update fails. +func (h *Route53) Run(ctx context.Context) error { + if err := h.updateZones(ctx); err != nil { + return err + } + go func() { + timer := time.NewTimer(h.refresh) + defer timer.Stop() + for { + timer.Reset(h.refresh) + select { + case <-ctx.Done(): + log.Debugf("Breaking out of Route53 update loop for %v: %v", h.zoneNames, ctx.Err()) + return + 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) + } + } + } + }() + return nil +} + +// ServeDNS implements the plugin.Handler.ServeDNS. +func (h *Route53) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.Name() + + zName := plugin.Zones(h.zoneNames).Matches(qname) + if zName == "" { + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + z, ok := h.zones[zName] + if !ok || z == nil { + return dns.RcodeServerFailure, nil + } + + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative = true + var result file.Result + for _, hostedZone := range z { + h.zMu.RLock() + m.Answer, m.Ns, m.Extra, result = hostedZone.z.Lookup(ctx, state, qname) + h.zMu.RUnlock() + + // Take the answer if it's non-empty OR if there is another + // record type exists for this name (NODATA). + if len(m.Answer) != 0 || result == file.NoData { + break + } + } + + if len(m.Answer) == 0 && result != file.NoData && h.Fall.Through(qname) { + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + + switch result { + case file.Success: + case file.NoData: + case file.NameError: + m.Rcode = dns.RcodeNameError + case file.Delegation: + m.Authoritative = false + case file.ServerFailure: + return dns.RcodeServerFailure, nil + } + + w.WriteMsg(m) + return dns.RcodeSuccess, nil +} + +const escapeSeq = "\\" + +// maybeUnescape parses s and converts escaped ASCII codepoints (in octal) back +// to its ASCII representation. +// +// From AWS docs: +// +// "If the domain name includes any characters other than a to z, 0 to 9, - +// (hyphen), or _ (underscore), Route 53 API actions return the characters as +// escape codes." +// +// For our purposes (and with respect to RFC 1035), we'll fish for a-z, 0-9, +// '-', '.' and '*' as the leftmost character (for wildcards) and throw error +// for everything else. +// +// Example: +// `\\052.example.com.` -> `*.example.com` +// `\\137.example.com.` -> error ('_' is not valid) +func maybeUnescape(s string) (string, error) { + var out string + for { + i := strings.Index(s, escapeSeq) + if i < 0 { + return out + s, nil + } + + out += s[:i] + + li, ri := i+len(escapeSeq), i+len(escapeSeq)+3 + if ri > len(s) { + return "", fmt.Errorf("invalid escape sequence: '%s%s'", escapeSeq, s[li:]) + } + // Parse `\\xxx` in base 8 (2nd arg) and attempt to fit into + // 8-bit result (3rd arg). + n, err := strconv.ParseInt(s[li:ri], 8, 8) + if err != nil { + return "", fmt.Errorf("invalid escape sequence: '%s%s'", escapeSeq, s[li:ri]) + } + + r := rune(n) + switch { + case r >= rune('a') && r <= rune('z'): // Route53 converts everything to lowercase. + case r >= rune('0') && r <= rune('9'): + case r == rune('*'): + if out != "" { + return "", errors.New("`*' only supported as wildcard (leftmost label)") + } + case r == rune('-'): + case r == rune('.'): + default: + return "", fmt.Errorf("invalid character: %s%#03o", escapeSeq, r) + } + + out += string(r) + + s = s[i+len(escapeSeq)+3:] + } +} + +func updateZoneFromRRS(rrs *route53.ResourceRecordSet, z *file.Zone) error { + for _, rr := range rrs.ResourceRecords { + n, err := maybeUnescape(aws.StringValue(rrs.Name)) + if err != nil { + return fmt.Errorf("failed to unescape `%s' name: %v", aws.StringValue(rrs.Name), err) + } + v, err := maybeUnescape(aws.StringValue(rr.Value)) + if err != nil { + return fmt.Errorf("failed to unescape `%s' value: %v", aws.StringValue(rr.Value), err) + } + + // Assemble RFC 1035 conforming record to pass into dns scanner. + rfc1035 := fmt.Sprintf("%s %d IN %s %s", n, aws.Int64Value(rrs.TTL), aws.StringValue(rrs.Type), v) + r, err := dns.NewRR(rfc1035) + if err != nil { + return fmt.Errorf("failed to parse resource record: %v", err) + } + + z.Insert(r) + } + return nil +} + +// updateZones re-queries resource record sets for each zone and updates the +// zone object. +// Returns error if any zones error'ed out, but waits for other zones to +// complete first. +func (h *Route53) updateZones(ctx context.Context) error { + errc := make(chan error) + defer close(errc) + for zName, z := range h.zones { + go func(zName string, z []*zone) { + var err error + defer func() { + errc <- err + }() + + for i, hostedZone := range z { + newZ := file.NewZone(zName, "") + newZ.Upstream = h.upstream + in := &route53.ListResourceRecordSetsInput{ + HostedZoneId: aws.String(hostedZone.id), + MaxItems: aws.String("1000"), + } + err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in, + func(out *route53.ListResourceRecordSetsOutput, last bool) bool { + for _, rrs := range out.ResourceRecordSets { + if err := updateZoneFromRRS(rrs, newZ); err != nil { + // Maybe unsupported record type. Log and carry on. + log.Warningf("Failed to process resource record set: %v", err) + } + } + return true + }) + if err != nil { + err = fmt.Errorf("failed to list resource records for %v:%v from route53: %v", zName, hostedZone.id, err) + return + } + h.zMu.Lock() + (*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++ { + err := <-errc + if err != nil { + errs = append(errs, err.Error()) + } + } + if len(errs) != 0 { + return fmt.Errorf("errors updating zones: %v", errs) + } + return nil +} + +// Name implements plugin.Handler.Name. +func (h *Route53) Name() string { return "route53" } diff --git a/ag_201_coredns/plugin/route53/route53_test.go b/ag_201_coredns/plugin/route53/route53_test.go new file mode 100644 index 0000000..1e74036 --- /dev/null +++ b/ag_201_coredns/plugin/route53/route53_test.go @@ -0,0 +1,298 @@ +package route53 + +import ( + "context" + "errors" + "reflect" + "testing" + "time" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/plugin/test" + crequest "github.com/coredns/coredns/request" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/service/route53/route53iface" + "github.com/miekg/dns" +) + +type fakeRoute53 struct { + route53iface.Route53API +} + +func (fakeRoute53) ListHostedZonesByNameWithContext(_ aws.Context, input *route53.ListHostedZonesByNameInput, _ ...request.Option) (*route53.ListHostedZonesByNameOutput, error) { + return nil, nil +} + +func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *route53.ListResourceRecordSetsInput, fn func(*route53.ListResourceRecordSetsOutput, bool) bool, _ ...request.Option) error { + if aws.StringValue(in.HostedZoneId) == "0987654321" { + return errors.New("bad. zone is bad") + } + rrsResponse := map[string][]*route53.ResourceRecordSet{} + for _, r := range []struct { + rType, name, value, hostedZoneID string + }{ + {"A", "example.org.", "1.2.3.4", "1234567890"}, + {"A", "www.example.org", "1.2.3.4", "1234567890"}, + {"CNAME", `\052.www.example.org`, "www.example.org", "1234567890"}, + {"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334", "1234567890"}, + {"CNAME", "sample.example.org.", "example.org", "1234567890"}, + {"PTR", "example.org.", "ptr.example.org.", "1234567890"}, + {"SOA", "org.", "ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400", "1234567890"}, + {"NS", "com.", "ns-1536.awsdns-00.co.uk.", "1234567890"}, + {"A", "split-example.gov.", "1.2.3.4", "1234567890"}, + // Unsupported type should be ignored. + {"YOLO", "swag.", "foobar", "1234567890"}, + // Hosted zone with the same name, but a different id. + {"A", "other-example.org.", "3.5.7.9", "1357986420"}, + {"A", "split-example.org.", "1.2.3.4", "1357986420"}, + {"SOA", "org.", "ns-15.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400", "1357986420"}, + // Hosted zone without SOA. + } { + rrs, ok := rrsResponse[r.hostedZoneID] + if !ok { + rrs = make([]*route53.ResourceRecordSet, 0) + } + rrs = append(rrs, &route53.ResourceRecordSet{Type: aws.String(r.rType), + Name: aws.String(r.name), + ResourceRecords: []*route53.ResourceRecord{ + { + Value: aws.String(r.value), + }, + }, + TTL: aws.Int64(300), + }) + rrsResponse[r.hostedZoneID] = rrs + } + + if ok := fn(&route53.ListResourceRecordSetsOutput{ + ResourceRecordSets: rrsResponse[aws.StringValue(in.HostedZoneId)], + }, true); !ok { + return errors.New("paging function return false") + } + return nil +} + +func TestRoute53(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + r, err := New(ctx, fakeRoute53{}, map[string][]string{"bad.": {"0987654321"}}, time.Minute) + if err != nil { + t.Fatalf("Failed to create route53: %v", err) + } + if err = r.Run(ctx); err == nil { + t.Fatalf("Expected errors for zone bad.") + } + + r, err = New(ctx, fakeRoute53{}, map[string][]string{"org.": {"1357986420", "1234567890"}, "gov.": {"Z098765432", "1234567890"}}, 90*time.Second) + if err != nil { + t.Fatalf("Failed to create route53: %v", err) + } + r.Fall = fall.Zero + r.Fall.SetZonesFromArgs([]string{"gov."}) + r.Next = test.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := crequest.Request{W: w, Req: r} + qname := state.Name() + m := new(dns.Msg) + rcode := dns.RcodeServerFailure + if qname == "example.gov." { + m.SetReply(r) + rr, err := dns.NewRR("example.gov. 300 IN A 2.4.6.8") + if err != nil { + t.Fatalf("Failed to create Resource Record: %v", err) + } + m.Answer = []dns.RR{rr} + + m.Authoritative = true + rcode = dns.RcodeSuccess + } + + m.SetRcode(r, rcode) + w.WriteMsg(m) + return rcode, nil + }) + err = r.Run(ctx) + if err != nil { + t.Fatalf("Failed to initialize route53: %v", err) + } + + tests := []struct { + qname string + qtype uint16 + wantRetCode int + wantAnswer []string // ownernames for the records in the additional section. + wantMsgRCode int + wantNS []string + expectedErr error + }{ + // 0. example.org A found - success. + { + 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, + 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, + wantAnswer: []string{"example.org. 300 IN PTR ptr.example.org."}, + }, + // 3. sample.example.org points to example.org CNAME. + // Query must return both CNAME and A recs. + { + qname: "sample.example.org", + qtype: dns.TypeA, + wantAnswer: []string{ + "sample.example.org. 300 IN CNAME example.org.", + "example.org. 300 IN A 1.2.3.4", + }, + }, + // 4. Explicit CNAME query for sample.example.org. + // Query must return just CNAME. + { + 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, + wantNS: []string{"org. 300 IN SOA ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"}, + }, + // 6. AAAA query for split-example.org must return NODATA. + { + qname: "split-example.gov", + qtype: dns.TypeAAAA, + wantRetCode: dns.RcodeSuccess, + wantNS: []string{"org. 300 IN SOA ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"}, + }, + // 7. Zone not configured. + { + qname: "badexample.com", + qtype: dns.TypeA, + wantRetCode: dns.RcodeServerFailure, + wantMsgRCode: dns.RcodeServerFailure, + }, + // 8. No record found. Return SOA record. + { + qname: "bad.org", + qtype: dns.TypeA, + wantRetCode: dns.RcodeSuccess, + wantMsgRCode: dns.RcodeNameError, + wantNS: []string{"org. 300 IN SOA ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"}, + }, + // 9. No record found. Fallthrough. + { + 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, + 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, + wantNS: []string{"org. 300 IN SOA ns-15.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"}, + }, + // 12. *.www.example.org is a wildcard CNAME to www.example.org. + { + qname: "a.www.example.org", + qtype: dns.TypeA, + wantAnswer: []string{ + "a.www.example.org. 300 IN CNAME www.example.org.", + "www.example.org. 300 IN A 1.2.3.4", + }, + }, + } + + for ti, tc := range tests { + 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 != tc.expectedErr { + t.Fatalf("Test %d: Expected error %v, but got %v", ti, tc.expectedErr, err) + } + if code != int(tc.wantRetCode) { + t.Fatalf("Test %d: Expected returned status code %s, but got %s", ti, dns.RcodeToString[tc.wantRetCode], dns.RcodeToString[code]) + } + + if tc.wantMsgRCode != rec.Msg.Rcode { + t.Errorf("Test %d: Unexpected msg status code. Want: %s, got: %s", ti, dns.RcodeToString[tc.wantMsgRCode], dns.RcodeToString[rec.Msg.Rcode]) + } + + if len(tc.wantAnswer) != len(rec.Msg.Answer) { + t.Errorf("Test %d: Unexpected number of Answers. Want: %d, got: %d", ti, len(tc.wantAnswer), len(rec.Msg.Answer)) + } else { + for i, gotAnswer := range rec.Msg.Answer { + if gotAnswer.String() != tc.wantAnswer[i] { + t.Errorf("Test %d: Unexpected answer.\nWant:\n\t%s\nGot:\n\t%s", ti, tc.wantAnswer[i], gotAnswer) + } + } + } + + if len(tc.wantNS) != len(rec.Msg.Ns) { + t.Errorf("Test %d: Unexpected NS number. Want: %d, got: %d", ti, len(tc.wantNS), len(rec.Msg.Ns)) + } else { + for i, ns := range rec.Msg.Ns { + got, ok := ns.(*dns.SOA) + if !ok { + t.Errorf("Test %d: Unexpected NS type. Want: SOA, got: %v", ti, reflect.TypeOf(got)) + } + if got.String() != tc.wantNS[i] { + t.Errorf("Test %d: Unexpected NS.\nWant: %v\nGot: %v", ti, tc.wantNS[i], got) + } + } + } + } +} + +func TestMaybeUnescape(t *testing.T) { + for ti, tc := range []struct { + escaped, want string + wantErr error + }{ + // 0. empty string is fine. + {escaped: "", want: ""}, + // 1. non-escaped sequence. + {escaped: "example.com.", want: "example.com."}, + // 2. escaped `*` as first label - OK. + {escaped: `\052.example.com`, want: "*.example.com"}, + // 3. Escaped dot, 'a' and a hyphen. No idea why but we'll allow it. + {escaped: `weird\055ex\141mple\056com\056\056`, want: "weird-example.com.."}, + // 4. escaped `*` in the middle - NOT OK. + {escaped: `e\052ample.com`, wantErr: errors.New("`*' only supported as wildcard (leftmost label)")}, + // 5. Invalid character. + {escaped: `\000.example.com`, wantErr: errors.New(`invalid character: \000`)}, + // 6. Invalid escape sequence in the middle. + {escaped: `example\0com`, wantErr: errors.New(`invalid escape sequence: '\0co'`)}, + // 7. Invalid escape sequence at the end. + {escaped: `example.com\0`, wantErr: errors.New(`invalid escape sequence: '\0'`)}, + } { + got, gotErr := maybeUnescape(tc.escaped) + if tc.wantErr != gotErr && !reflect.DeepEqual(tc.wantErr, gotErr) { + t.Fatalf("Test %d: Expected error: `%v', but got: `%v'", ti, tc.wantErr, gotErr) + } + if tc.want != got { + t.Errorf("Test %d: Expected unescaped: `%s', but got: `%s'", ti, tc.want, got) + } + } +} diff --git a/ag_201_coredns/plugin/route53/setup.go b/ag_201_coredns/plugin/route53/setup.go new file mode 100644 index 0000000..3df6527 --- /dev/null +++ b/ag_201_coredns/plugin/route53/setup.go @@ -0,0 +1,144 @@ +package route53 + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/fall" + clog "github.com/coredns/coredns/plugin/pkg/log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/defaults" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/service/route53/route53iface" +) + +var log = clog.NewWithPlugin("route53") + +func init() { plugin.Register("route53", setup) } + +// exposed for testing +var f = func(opts session.Options) route53iface.Route53API { + return route53.New(session.Must(session.NewSessionWithOptions(opts))) +} + +func setup(c *caddy.Controller) error { + for c.Next() { + keyPairs := map[string]struct{}{} + keys := map[string][]string{} + + // Route53 plugin attempts to load AWS credentials following default SDK chaining. + // The order configuration is loaded in is: + // * Static AWS keys set in Corefile (deprecated) + // * Environment Variables + // * Shared Credentials file + // * Shared Configuration file (if AWS_SDK_LOAD_CONFIG is set to truthy value) + // * EC2 Instance Metadata (credentials only) + opts := session.Options{} + var fall fall.F + + refresh := time.Duration(1) * time.Minute // default update frequency to 1 minute + + args := c.RemainingArgs() + + for i := 0; i < len(args); i++ { + parts := strings.SplitN(args[i], ":", 2) + if len(parts) != 2 { + return plugin.Error("route53", c.Errf("invalid zone %q", args[i])) + } + dns, hostedZoneID := parts[0], parts[1] + if dns == "" || hostedZoneID == "" { + return plugin.Error("route53", c.Errf("invalid zone %q", args[i])) + } + if _, ok := keyPairs[args[i]]; ok { + return plugin.Error("route53", c.Errf("conflict zone %q", args[i])) + } + + keyPairs[args[i]] = struct{}{} + keys[dns] = append(keys[dns], hostedZoneID) + } + + for c.NextBlock() { + switch c.Val() { + case "aws_access_key": + v := c.RemainingArgs() + if len(v) < 2 { + return plugin.Error("route53", c.Errf("invalid access key: '%v'", v)) + } + opts.Config.Credentials = credentials.NewStaticCredentials(v[0], v[1], "") + log.Warningf("Save aws_access_key in Corefile has been deprecated, please use other authentication methods instead") + case "aws_endpoint": + if c.NextArg() { + opts.Config.Endpoint = aws.String(c.Val()) + } else { + return plugin.Error("route53", c.ArgErr()) + } + case "upstream": + c.RemainingArgs() // eats args + case "credentials": + if c.NextArg() { + opts.Profile = c.Val() + } else { + return c.ArgErr() + } + if c.NextArg() { + opts.SharedConfigFiles = []string{c.Val()} + // If AWS_SDK_LOAD_CONFIG is set also load ~/.aws/config to stay consistent + // with default SDK behavior. + if ok, _ := strconv.ParseBool(os.Getenv("AWS_SDK_LOAD_CONFIG")); ok { + opts.SharedConfigFiles = append(opts.SharedConfigFiles, defaults.SharedConfigFilename()) + } + } + case "fallthrough": + fall.SetZonesFromArgs(c.RemainingArgs()) + case "refresh": + if c.NextArg() { + refreshStr := c.Val() + _, err := strconv.Atoi(refreshStr) + if err == nil { + refreshStr = fmt.Sprintf("%ss", c.Val()) + } + refresh, err = time.ParseDuration(refreshStr) + if err != nil { + return plugin.Error("route53", c.Errf("Unable to parse duration: %v", err)) + } + if refresh <= 0 { + return plugin.Error("route53", c.Errf("refresh interval must be greater than 0: %q", refreshStr)) + } + } else { + return plugin.Error("route53", c.ArgErr()) + } + default: + return plugin.Error("route53", c.Errf("unknown property %q", c.Val())) + } + } + + client := f(opts) + ctx, cancel := context.WithCancel(context.Background()) + h, err := New(ctx, client, keys, refresh) + if err != nil { + cancel() + return plugin.Error("route53", c.Errf("failed to create route53 plugin: %v", err)) + } + h.Fall = fall + if err := h.Run(ctx); err != nil { + cancel() + return plugin.Error("route53", c.Errf("failed to initialize route53 plugin: %v", err)) + } + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + h.Next = next + return h + }) + c.OnShutdown(func() error { cancel(); return nil }) + } + return nil +} diff --git a/ag_201_coredns/plugin/route53/setup_test.go b/ag_201_coredns/plugin/route53/setup_test.go new file mode 100644 index 0000000..5d2792f --- /dev/null +++ b/ag_201_coredns/plugin/route53/setup_test.go @@ -0,0 +1,87 @@ +package route53 + +import ( + "testing" + + "github.com/coredns/caddy" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/route53/route53iface" +) + +func TestSetupRoute53(t *testing.T) { + f = func(opts session.Options) route53iface.Route53API { + return fakeRoute53{} + } + + tests := []struct { + body string + expectedError bool + }{ + {`route53`, false}, + {`route53 :`, true}, + {`route53 example.org:12345678`, false}, + {`route53 example.org:12345678 { + aws_access_key +}`, true}, + {`route53 example.org:12345678 { }`, false}, + + {`route53 example.org:12345678 { }`, false}, + {`route53 example.org:12345678 { wat +}`, true}, + {`route53 example.org:12345678 { + aws_access_key ACCESS_KEY_ID SEKRIT_ACCESS_KEY +}`, false}, + + {`route53 example.org:12345678 { + fallthrough +}`, false}, + {`route53 example.org:12345678 { + credentials + }`, true}, + + {`route53 example.org:12345678 { + credentials default + }`, false}, + {`route53 example.org:12345678 { + credentials default credentials + }`, false}, + {`route53 example.org:12345678 { + credentials default credentials extra-arg + }`, true}, + {`route53 example.org:12345678 example.org:12345678 { + }`, true}, + + {`route53 example.org:12345678 { + refresh 90 +}`, false}, + {`route53 example.org:12345678 { + refresh 5m +}`, false}, + {`route53 example.org:12345678 { + refresh +}`, true}, + {`route53 example.org:12345678 { + refresh foo +}`, true}, + {`route53 example.org:12345678 { + refresh -1m +}`, true}, + + {`route53 example.org { + }`, true}, + {`route53 example.org:12345678 { + aws_endpoint +}`, true}, + {`route53 example.org:12345678 { + aws_endpoint https://localhost +}`, false}, + } + + for _, test := range tests { + c := caddy.NewTestController("dns", test.body) + if err := setup(c); (err == nil) == test.expectedError { + t.Errorf("Unexpected errors: %v", err) + } + } +} diff --git a/ag_201_coredns/plugin/secondary/README.md b/ag_201_coredns/plugin/secondary/README.md new file mode 100644 index 0000000..b22965e --- /dev/null +++ b/ag_201_coredns/plugin/secondary/README.md @@ -0,0 +1,73 @@ +# secondary + +## Name + +*secondary* - enables serving a zone retrieved from a primary server. + +## Description + +With *secondary* you can transfer (via AXFR) a zone from another server. The retrieved zone is +*not committed* to disk (a violation of the RFC). This means restarting CoreDNS will cause it to +retrieve all secondary zones. + +If the primary server(s) don't respond when CoreDNS is starting up, the AXFR will be retried +indefinitely every 10s. + +## Syntax + +~~~ +secondary [ZONES...] +~~~ + +* **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block + are used. Note that without a remote address to *get* the zone from, the above is not that useful. + +A working syntax would be: + +~~~ +secondary [zones...] { + transfer from ADDRESS [ADDRESS...] +} +~~~ + +* `transfer from` specifies from which **ADDRESS** to fetch the zone. It can be specified multiple + times; if one does not work, another will be tried. Transferring this zone outwards again can be + done by enabling the *transfer* plugin. + +When a zone is due to be refreshed (refresh timer fires) a random jitter of 5 seconds is applied, +before fetching. In the case of retry this will be 2 seconds. If there are any errors during the +transfer in, the transfer fails; this will be logged. + +## Examples + +Transfer `example.org` from 10.0.1.1, and if that fails try 10.1.2.1. + +~~~ corefile +example.org { + secondary { + transfer from 10.0.1.1 10.1.2.1 + } +} +~~~ + +Or re-export the retrieved zone to other secondaries. + +~~~ corefile +example.net { + secondary { + transfer from 10.1.2.1 + } + transfer { + to * + } +} +~~~ + +## Bugs + +Only AXFR is supported and the retrieved zone is not committed to disk. + +## See Also + +See the *transfer* plugin to enable zone transfers _to_ other servers. +And RFC 5936 detailing the AXFR protocol. diff --git a/ag_201_coredns/plugin/secondary/log_test.go b/ag_201_coredns/plugin/secondary/log_test.go new file mode 100644 index 0000000..15cab00 --- /dev/null +++ b/ag_201_coredns/plugin/secondary/log_test.go @@ -0,0 +1,5 @@ +package secondary + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/secondary/secondary.go b/ag_201_coredns/plugin/secondary/secondary.go new file mode 100644 index 0000000..43934e8 --- /dev/null +++ b/ag_201_coredns/plugin/secondary/secondary.go @@ -0,0 +1,10 @@ +// Package secondary implements a secondary plugin. +package secondary + +import "github.com/coredns/coredns/plugin/file" + +// Secondary implements a secondary plugin that allows CoreDNS to retrieve (via AXFR) +// zone information from a primary server. +type Secondary struct { + file.File +} diff --git a/ag_201_coredns/plugin/secondary/setup.go b/ag_201_coredns/plugin/secondary/setup.go new file mode 100644 index 0000000..22f0d32 --- /dev/null +++ b/ag_201_coredns/plugin/secondary/setup.go @@ -0,0 +1,99 @@ +package secondary + +import ( + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/file" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/parse" + "github.com/coredns/coredns/plugin/pkg/upstream" +) + +var log = clog.NewWithPlugin("secondary") + +func init() { plugin.Register("secondary", setup) } + +func setup(c *caddy.Controller) error { + zones, err := secondaryParse(c) + if err != nil { + return plugin.Error("secondary", err) + } + + // Add startup functions to retrieve the zone and keep it up to date. + for i := range zones.Names { + n := zones.Names[i] + z := zones.Z[n] + if len(z.TransferFrom) > 0 { + c.OnStartup(func() error { + z.StartupOnce.Do(func() { + go func() { + dur := time.Millisecond * 250 + step := time.Duration(2) + max := time.Second * 10 + for { + err := z.TransferIn() + if err == nil { + break + } + log.Warningf("All '%s' masters failed to transfer, retrying in %s: %s", n, dur.String(), err) + time.Sleep(dur) + dur = step * dur + if dur > max { + dur = max + } + } + z.Update() + }() + }) + return nil + }) + } + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return Secondary{file.File{Next: next, Zones: zones}} + }) + + return nil +} + +func secondaryParse(c *caddy.Controller) (file.Zones, error) { + z := make(map[string]*file.Zone) + names := []string{} + for c.Next() { + if c.Val() == "secondary" { + // secondary [origin] + origins := plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) + for i := range origins { + z[origins[i]] = file.NewZone(origins[i], "stdin") + names = append(names, origins[i]) + } + + for c.NextBlock() { + var f []string + + switch c.Val() { + case "transfer": + var err error + f, err = parse.TransferIn(c) + if err != nil { + return file.Zones{}, err + } + default: + return file.Zones{}, c.Errf("unknown property '%s'", c.Val()) + } + + for _, origin := range origins { + if f != nil { + z[origin].TransferFrom = append(z[origin].TransferFrom, f...) + } + z[origin].Upstream = upstream.New() + } + } + } + } + return file.Zones{Z: z, Names: names}, nil +} diff --git a/ag_201_coredns/plugin/secondary/setup_test.go b/ag_201_coredns/plugin/secondary/setup_test.go new file mode 100644 index 0000000..4985ec5 --- /dev/null +++ b/ag_201_coredns/plugin/secondary/setup_test.go @@ -0,0 +1,63 @@ +package secondary + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSecondaryParse(t *testing.T) { + tests := []struct { + inputFileRules string + shouldErr bool + transferFrom string + zones []string + }{ + { + `secondary`, + false, // TODO(miek): should actually be true, because without transfer lines this does not make sense + "", + nil, + }, + { + `secondary { + transfer from 127.0.0.1 + }`, + false, + "127.0.0.1:53", + nil, + }, + { + `secondary example.org { + transfer from 127.0.0.1 + }`, + false, + "127.0.0.1:53", + []string{"example.org."}, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputFileRules) + s, err := secondaryParse(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) + } + + for i, name := range test.zones { + if x := s.Names[i]; x != name { + t.Fatalf("Test %d zone names don't match expected %q, but got %q", i, name, x) + } + } + + // This is only set *if* we have a zone (i.e. not in all tests above) + for _, v := range s.Z { + if x := v.TransferFrom[0]; x != test.transferFrom { + t.Fatalf("Test %d transform from names don't match expected %q, but got %q", i, test.transferFrom, x) + } + } + } +} diff --git a/ag_201_coredns/plugin/sign/README.md b/ag_201_coredns/plugin/sign/README.md new file mode 100644 index 0000000..6eb4ba8 --- /dev/null +++ b/ag_201_coredns/plugin/sign/README.md @@ -0,0 +1,168 @@ +# sign + +## Name + +*sign* - adds DNSSEC records to zone files. + +## Description + +The *sign* plugin is used to sign (see RFC 6781) zones. In this process DNSSEC resource records are +added. The signatures that sign the resource records sets have an expiration date, this means the +signing process must be repeated before this expiration data is reached. Otherwise the zone's data +will go BAD (RFC 4035, Section 5.5). The *sign* plugin takes care of this. + +Only NSEC is supported, *sign* does *not* support NSEC3. + +*Sign* works in conjunction with the *file* and *auto* plugins; this plugin **signs** the zones +files, *auto* and *file* **serve** the zones *data*. + +For this plugin to work at least one Common Signing Key, (see coredns-keygen(1)) is needed. This key +(or keys) will be used to sign the entire zone. *Sign* does *not* support the ZSK/KSK split, nor will +it do key or algorithm rollovers - it just signs. + +*Sign* will: + + * (Re)-sign the zone with the CSK(s) when: + + - the last time it was signed is more than a 6 days ago. Each zone will have some jitter + applied to the inception date. + + - the signature only has 14 days left before expiring. + + Both these dates are only checked on the SOA's signature(s). + + * Create RRSIGs that have an inception of -3 hours (minus a jitter between 0 and 18 hours) + and a expiration of +32 (plus a jitter between 0 and 5 days) days for every given DNSKEY. + + * Add NSEC records for all names in the zone. The TTL for these is the negative cache TTL from the + SOA record. + + * Add or replace *all* apex CDS/CDNSKEY records with the ones derived from the given keys. For + each key two CDS are created one with SHA1 and another with SHA256. + + * Update the SOA's serial number to the *Unix epoch* of when the signing happens. This will + overwrite *any* previous serial number. + + +There are two ways that dictate when a zone is signed. Normally every 6 days (plus jitter) it will +be resigned. If for some reason we fail this check, the 14 days before expiring kicks in. + +Keys are named (following BIND9): `K++.key` and `K++.private`. +The keys **must not** be included in your zone; they will be added by *sign*. These keys can be +generated with `coredns-keygen` or BIND9's `dnssec-keygen`. You don't have to adhere to this naming +scheme, but then you need to name your keys explicitly, see the `keys file` directive. + +A generated zone is written out in a file named `db..signed` in the directory named by the +`directory` directive (which defaults to `/var/lib/coredns`). + +## Syntax + +~~~ +sign DBFILE [ZONES...] { + key file|directory KEY...|DIR... + directory DIR +} +~~~ + +* **DBFILE** the zone database file to read and parse. If the path is relative, the path from the + *root* plugin will be prepended to it. +* **ZONES** zones it should be sign for. If empty, the zones from the configuration block are + used. +* `key` specifies the key(s) (there can be multiple) to sign the zone. If `file` is + used the **KEY**'s filenames are used as is. If `directory` is used, *sign* will look in **DIR** + for `K++` files. Any metadata in these files (Activate, Publish, etc.) is + *ignored*. These keys must also be Key Signing Keys (KSK). +* `directory` specifies the **DIR** where CoreDNS should save zones that have been signed. + If not given this defaults to `/var/lib/coredns`. The zones are saved under the name + `db..signed`. If the path is relative the path from the *root* plugin will be prepended + to it. + +Keys can be generated with `coredns-keygen`, to create one for use in the *sign* plugin, use: +`coredns-keygen example.org` or `dnssec-keygen -a ECDSAP256SHA256 -f KSK example.org`. + +## Examples + +Sign the `example.org` zone contained in the file `db.example.org` and write the result to +`./db.example.org.signed` to let the *file* plugin pick it up and serve it. The keys used +are read from `/etc/coredns/keys/Kexample.org.key` and `/etc/coredns/keys/Kexample.org.private`. + +~~~ txt +example.org { + file db.example.org.signed + + sign db.example.org { + key file /etc/coredns/keys/Kexample.org + directory . + } +} +~~~ + +Running this leads to the following log output (note the timers in this example have been set to +shorter intervals). + +~~~ txt +[WARNING] plugin/file: Failed to open "open /tmp/db.example.org.signed: no such file or directory": trying again in 1m0s +[INFO] plugin/sign: Signing "example.org." because open /tmp/db.example.org.signed: no such file or directory +[INFO] plugin/sign: Successfully signed zone "example.org." in "/tmp/db.example.org.signed" with key tags "59725" and 1564766865 SOA serial, elapsed 9.357933ms, next: 2019-08-02T22:27:45.270Z +[INFO] plugin/file: Successfully reloaded zone "example.org." in "/tmp/db.example.org.signed" with serial 1564766865 +~~~ + +Or use a single zone file for *multiple* zones, note that the **ZONES** are repeated for both plugins. +Also note this outputs *multiple* signed output files. Here we use the default output directory +`/var/lib/coredns`. + +~~~ txt +. { + file /var/lib/coredns/db.example.org.signed example.org + file /var/lib/coredns/db.example.net.signed example.net + sign db.example.org example.org example.net { + key directory /etc/coredns/keys + } +} +~~~ + +This is the same configuration, but the zones are put in the server block, but note that you still +need to specify what file is served for what zone in the *file* plugin: + +~~~ txt +example.org example.net { + file var/lib/coredns/db.example.org.signed example.org + file var/lib/coredns/db.example.net.signed example.net + sign db.example.org { + key directory /etc/coredns/keys + } +} +~~~ + +Be careful to fully list the origins you want to sign, if you don't: + +~~~ txt +example.org example.net { + sign plugin/sign/testdata/db.example.org miek.org { + key file /etc/coredns/keys/Kexample.org + } +} +~~~ + +This will lead to `db.example.org` be signed *twice*, as this entire section is parsed twice because +you have specified the origins `example.org` and `example.net` in the server block. + +Forcibly resigning a zone can be accomplished by removing the signed zone file (CoreDNS will keep +on serving it from memory), and sending SIGUSR1 to the process to make it reload and resign the zone +file. + +## See Also + +The DNSSEC RFCs: RFC 4033, RFC 4034 and RFC 4035. And the BCP on DNSSEC, RFC 6781. Further more the +manual pages coredns-keygen(1) and dnssec-keygen(8). And the *file* plugin's documentation. + +Coredns-keygen can be found at +[https://github.com/coredns/coredns-utils](https://github.com/coredns/coredns-utils) in the +coredns-keygen directory. + +Other useful DNSSEC tools can be found in [ldns](https://nlnetlabs.nl/projects/ldns/about/), e.g. +`ldns-key2ds` to create DS records from DNSKEYs. + +## Bugs + +`keys directory` is not implemented. diff --git a/ag_201_coredns/plugin/sign/dnssec.go b/ag_201_coredns/plugin/sign/dnssec.go new file mode 100644 index 0000000..a95e086 --- /dev/null +++ b/ag_201_coredns/plugin/sign/dnssec.go @@ -0,0 +1,20 @@ +package sign + +import ( + "github.com/miekg/dns" +) + +func (p Pair) signRRs(rrs []dns.RR, signerName string, ttl, incep, expir uint32) (*dns.RRSIG, error) { + rrsig := &dns.RRSIG{ + Hdr: dns.RR_Header{Rrtype: dns.TypeRRSIG, Ttl: ttl}, + Algorithm: p.Public.Algorithm, + SignerName: signerName, + KeyTag: p.KeyTag, + OrigTtl: ttl, + Inception: incep, + Expiration: expir, + } + + e := rrsig.Sign(p.Private, rrs) + return rrsig, e +} diff --git a/ag_201_coredns/plugin/sign/file.go b/ag_201_coredns/plugin/sign/file.go new file mode 100644 index 0000000..194ab69 --- /dev/null +++ b/ag_201_coredns/plugin/sign/file.go @@ -0,0 +1,92 @@ +package sign + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/file/tree" + + "github.com/miekg/dns" +) + +// write writes out the zone file to a temporary file which is then moved into the correct place. +func (s *Signer) write(z *file.Zone) error { + f, err := os.CreateTemp(s.directory, "signed-") + if err != nil { + return err + } + + if err := write(f, z); err != nil { + f.Close() + return err + } + + f.Close() + return os.Rename(f.Name(), filepath.Join(s.directory, s.signedfile)) +} + +func write(w io.Writer, z *file.Zone) error { + if _, err := io.WriteString(w, z.Apex.SOA.String()); err != nil { + return err + } + w.Write([]byte("\n")) // RR Stringer() method doesn't include newline, which ends the RR in a zone file, write that here. + for _, rr := range z.Apex.SIGSOA { + io.WriteString(w, rr.String()) + w.Write([]byte("\n")) + } + for _, rr := range z.Apex.NS { + io.WriteString(w, rr.String()) + w.Write([]byte("\n")) + } + for _, rr := range z.Apex.SIGNS { + io.WriteString(w, rr.String()) + w.Write([]byte("\n")) + } + err := z.Walk(func(e *tree.Elem, _ map[uint16][]dns.RR) error { + for _, r := range e.All() { + io.WriteString(w, r.String()) + w.Write([]byte("\n")) + } + return nil + }) + return err +} + +// Parse parses the zone in filename and returns a new Zone or an error. This +// is similar to the Parse function in the *file* plugin. However when parsing +// the record types DNSKEY, RRSIG, CDNSKEY and CDS are *not* included in the returned +// zone (if encountered). +func Parse(f io.Reader, origin, fileName string) (*file.Zone, error) { + zp := dns.NewZoneParser(f, dns.Fqdn(origin), fileName) + zp.SetIncludeAllowed(true) + z := file.NewZone(origin, fileName) + seenSOA := false + + for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { + if err := zp.Err(); err != nil { + return nil, err + } + + switch rr.(type) { + case *dns.DNSKEY, *dns.RRSIG, *dns.CDNSKEY, *dns.CDS: + continue + case *dns.SOA: + seenSOA = true + if err := z.Insert(rr); err != nil { + return nil, err + } + default: + if err := z.Insert(rr); err != nil { + return nil, err + } + } + } + if !seenSOA { + return nil, fmt.Errorf("file %q has no SOA record", fileName) + } + + return z, nil +} diff --git a/ag_201_coredns/plugin/sign/file_test.go b/ag_201_coredns/plugin/sign/file_test.go new file mode 100644 index 0000000..72d2b02 --- /dev/null +++ b/ag_201_coredns/plugin/sign/file_test.go @@ -0,0 +1,43 @@ +package sign + +import ( + "os" + "testing" + + "github.com/miekg/dns" +) + +func TestFileParse(t *testing.T) { + f, err := os.Open("testdata/db.miek.nl") + if err != nil { + t.Fatal(err) + } + z, err := Parse(f, "miek.nl.", "testdata/db.miek.nl") + if err != nil { + t.Fatal(err) + } + s := &Signer{ + directory: ".", + signedfile: "db.miek.nl.test", + } + + s.write(z) + defer os.Remove("db.miek.nl.test") + + f, err = os.Open("db.miek.nl.test") + if err != nil { + t.Fatal(err) + } + z, err = Parse(f, "miek.nl.", "db.miek.nl.test") + if err != nil { + t.Fatal(err) + } + if x := z.Apex.SOA.Header().Name; x != "miek.nl." { + t.Errorf("Expected SOA name to be %s, got %s", x, "miek.nl.") + } + apex, _ := z.Search("miek.nl.") + key := apex.Type(dns.TypeDNSKEY) + if key != nil { + t.Errorf("Expected no DNSKEYs, but got %d", len(key)) + } +} diff --git a/ag_201_coredns/plugin/sign/keys.go b/ag_201_coredns/plugin/sign/keys.go new file mode 100644 index 0000000..b999584 --- /dev/null +++ b/ag_201_coredns/plugin/sign/keys.go @@ -0,0 +1,119 @@ +package sign + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + + "github.com/miekg/dns" + "golang.org/x/crypto/ed25519" +) + +// Pair holds DNSSEC key information, both the public and private components are stored here. +type Pair struct { + Public *dns.DNSKEY + KeyTag uint16 + Private crypto.Signer +} + +// keyParse reads the public and private key from disk. +func keyParse(c *caddy.Controller) ([]Pair, error) { + if !c.NextArg() { + return nil, c.ArgErr() + } + pairs := []Pair{} + config := dnsserver.GetConfig(c) + + switch c.Val() { + case "file": + ks := c.RemainingArgs() + if len(ks) == 0 { + return nil, c.ArgErr() + } + for _, k := range ks { + base := k + // Kmiek.nl.+013+26205.key, handle .private or without extension: Kmiek.nl.+013+26205 + if strings.HasSuffix(k, ".key") { + base = k[:len(k)-4] + } + if strings.HasSuffix(k, ".private") { + base = k[:len(k)-8] + } + if !filepath.IsAbs(base) && config.Root != "" { + base = filepath.Join(config.Root, base) + } + + pair, err := readKeyPair(base+".key", base+".private") + if err != nil { + return nil, err + } + pairs = append(pairs, pair) + } + case "directory": + return nil, fmt.Errorf("directory: not implemented") + } + + return pairs, nil +} + +func readKeyPair(public, private string) (Pair, error) { + rk, err := os.Open(filepath.Clean(public)) + if err != nil { + return Pair{}, err + } + b, err := io.ReadAll(rk) + if err != nil { + return Pair{}, err + } + dnskey, err := dns.NewRR(string(b)) + if err != nil { + return Pair{}, err + } + if _, ok := dnskey.(*dns.DNSKEY); !ok { + return Pair{}, fmt.Errorf("RR in %q is not a DNSKEY: %d", public, dnskey.Header().Rrtype) + } + ksk := dnskey.(*dns.DNSKEY).Flags&(1<<8) == (1<<8) && dnskey.(*dns.DNSKEY).Flags&1 == 1 + if !ksk { + return Pair{}, fmt.Errorf("DNSKEY in %q is not a CSK/KSK", public) + } + + rp, err := os.Open(filepath.Clean(private)) + if err != nil { + return Pair{}, err + } + privkey, err := dnskey.(*dns.DNSKEY).ReadPrivateKey(rp, private) + if err != nil { + return Pair{}, err + } + switch signer := privkey.(type) { + case *ecdsa.PrivateKey: + return Pair{Public: dnskey.(*dns.DNSKEY), KeyTag: dnskey.(*dns.DNSKEY).KeyTag(), Private: signer}, nil + case ed25519.PrivateKey: + return Pair{Public: dnskey.(*dns.DNSKEY), KeyTag: dnskey.(*dns.DNSKEY).KeyTag(), Private: signer}, nil + case *rsa.PrivateKey: + return Pair{Public: dnskey.(*dns.DNSKEY), KeyTag: dnskey.(*dns.DNSKEY).KeyTag(), Private: signer}, nil + default: + return Pair{}, fmt.Errorf("unsupported algorithm %s", signer) + } +} + +// keyTag returns the key tags of the keys in ps as a formatted string. +func keyTag(ps []Pair) string { + if len(ps) == 0 { + return "" + } + s := "" + for _, p := range ps { + s += strconv.Itoa(int(p.KeyTag)) + "," + } + return s[:len(s)-1] +} diff --git a/ag_201_coredns/plugin/sign/log_test.go b/ag_201_coredns/plugin/sign/log_test.go new file mode 100644 index 0000000..2726cd1 --- /dev/null +++ b/ag_201_coredns/plugin/sign/log_test.go @@ -0,0 +1,5 @@ +package sign + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/sign/nsec.go b/ag_201_coredns/plugin/sign/nsec.go new file mode 100644 index 0000000..d7c6a30 --- /dev/null +++ b/ag_201_coredns/plugin/sign/nsec.go @@ -0,0 +1,36 @@ +package sign + +import ( + "sort" + + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/file/tree" + + "github.com/miekg/dns" +) + +// names returns the elements of the zone in nsec order. +func names(origin string, z *file.Zone) []string { + // There will also be apex records other than NS and SOA (who are kept separate), as we + // are adding DNSKEY and CDS/CDNSKEY records in the apex *before* we sign. + n := []string{} + z.AuthWalk(func(e *tree.Elem, _ map[uint16][]dns.RR, auth bool) error { + if !auth { + return nil + } + n = append(n, e.Name()) + return nil + }) + return n +} + +// NSEC returns an NSEC record according to name, next, ttl and bitmap. Note that the bitmap is sorted before use. +func NSEC(name, next string, ttl uint32, bitmap []uint16) *dns.NSEC { + sort.Slice(bitmap, func(i, j int) bool { return bitmap[i] < bitmap[j] }) + + return &dns.NSEC{ + Hdr: dns.RR_Header{Name: name, Ttl: ttl, Rrtype: dns.TypeNSEC, Class: dns.ClassINET}, + NextDomain: next, + TypeBitMap: bitmap, + } +} diff --git a/ag_201_coredns/plugin/sign/nsec_test.go b/ag_201_coredns/plugin/sign/nsec_test.go new file mode 100644 index 0000000..f272651 --- /dev/null +++ b/ag_201_coredns/plugin/sign/nsec_test.go @@ -0,0 +1,27 @@ +package sign + +import ( + "os" + "testing" + + "github.com/coredns/coredns/plugin/file" +) + +func TestNames(t *testing.T) { + f, err := os.Open("testdata/db.miek.nl_ns") + if err != nil { + t.Error(err) + } + z, err := file.Parse(f, "db.miek.nl_ns", "miek.nl", 0) + if err != nil { + t.Error(err) + } + + names := names("miek.nl.", z) + expected := []string{"miek.nl.", "child.miek.nl.", "www.miek.nl."} + for i := range names { + if names[i] != expected[i] { + t.Errorf("Expected %s, got %s", expected[i], names[i]) + } + } +} diff --git a/ag_201_coredns/plugin/sign/resign_test.go b/ag_201_coredns/plugin/sign/resign_test.go new file mode 100644 index 0000000..2f67f52 --- /dev/null +++ b/ag_201_coredns/plugin/sign/resign_test.go @@ -0,0 +1,40 @@ +package sign + +import ( + "strings" + "testing" + "time" +) + +func TestResignInception(t *testing.T) { + then := time.Date(2019, 7, 18, 22, 50, 0, 0, time.UTC) + // signed yesterday + zr := strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190808191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`) + if x := resign(zr, then); x != nil { + t.Errorf("Expected RRSIG to be valid for %s, got invalid: %s", then.Format(timeFmt), x) + } + // inception starts after this date. + zr = strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190808191936 20190731161936 59725 miek.nl. eU6gI1OkSEbyt`) + if x := resign(zr, then); x == nil { + t.Errorf("Expected RRSIG to be invalid for %s, got valid", then.Format(timeFmt)) + } +} + +func TestResignExpire(t *testing.T) { + then := time.Date(2019, 7, 18, 22, 50, 0, 0, time.UTC) + // expires tomorrow + zr := strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190717191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`) + if x := resign(zr, then); x == nil { + t.Errorf("Expected RRSIG to be invalid for %s, got valid", then.Format(timeFmt)) + } + // expire too far away + zr = strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190731191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`) + if x := resign(zr, then); x != nil { + t.Errorf("Expected RRSIG to be valid for %s, got invalid: %s", then.Format(timeFmt), x) + } + // expired yesterday + zr = strings.NewReader(`miek.nl. 1800 IN RRSIG SOA 13 2 1800 20190721191936 20190717161936 59725 miek.nl. eU6gI1OkSEbyt`) + if x := resign(zr, then); x == nil { + t.Errorf("Expected RRSIG to be invalid for %s, got valid", then.Format(timeFmt)) + } +} diff --git a/ag_201_coredns/plugin/sign/setup.go b/ag_201_coredns/plugin/sign/setup.go new file mode 100644 index 0000000..e5f5295 --- /dev/null +++ b/ag_201_coredns/plugin/sign/setup.go @@ -0,0 +1,100 @@ +package sign + +import ( + "fmt" + "math/rand" + "path/filepath" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("sign", setup) } + +func setup(c *caddy.Controller) error { + sign, err := parse(c) + if err != nil { + return plugin.Error("sign", err) + } + + c.OnStartup(sign.OnStartup) + c.OnStartup(func() error { + for _, signer := range sign.signers { + go signer.refresh(durationRefreshHours) + } + return nil + }) + c.OnShutdown(func() error { + for _, signer := range sign.signers { + close(signer.stop) + } + return nil + }) + + // Don't call AddPlugin, *sign* is not a plugin. + return nil +} + +func parse(c *caddy.Controller) (*Sign, error) { + sign := &Sign{} + config := dnsserver.GetConfig(c) + + for c.Next() { + if !c.NextArg() { + return nil, c.ArgErr() + } + dbfile := c.Val() + if !filepath.IsAbs(dbfile) && config.Root != "" { + dbfile = filepath.Join(config.Root, dbfile) + } + + origins := plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) + signers := make([]*Signer, len(origins)) + for i := range origins { + signers[i] = &Signer{ + dbfile: dbfile, + origin: origins[i], + jitterIncep: time.Duration(float32(durationInceptionJitter) * rand.Float32()), + jitterExpir: time.Duration(float32(durationExpirationDayJitter) * rand.Float32()), + directory: "/var/lib/coredns", + stop: make(chan struct{}), + signedfile: fmt.Sprintf("db.%ssigned", origins[i]), // origins[i] is a fqdn, so it ends with a dot, hence %ssigned. + } + } + + for c.NextBlock() { + switch c.Val() { + case "key": + pairs, err := keyParse(c) + if err != nil { + return sign, err + } + for i := range signers { + for _, p := range pairs { + p.Public.Header().Name = signers[i].origin + } + signers[i].keys = append(signers[i].keys, pairs...) + } + case "directory": + dir := c.RemainingArgs() + if len(dir) == 0 || len(dir) > 1 { + return sign, fmt.Errorf("can only be one argument after %q", "directory") + } + if !filepath.IsAbs(dir[0]) && config.Root != "" { + dir[0] = filepath.Join(config.Root, dir[0]) + } + for i := range signers { + signers[i].directory = dir[0] + signers[i].signedfile = fmt.Sprintf("db.%ssigned", signers[i].origin) + } + default: + return nil, c.Errf("unknown property '%s'", c.Val()) + } + } + sign.signers = append(sign.signers, signers...) + } + + return sign, nil +} diff --git a/ag_201_coredns/plugin/sign/setup_test.go b/ag_201_coredns/plugin/sign/setup_test.go new file mode 100644 index 0000000..93d779a --- /dev/null +++ b/ag_201_coredns/plugin/sign/setup_test.go @@ -0,0 +1,75 @@ +package sign + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestParse(t *testing.T) { + tests := []struct { + input string + shouldErr bool + exp *Signer + }{ + {`sign testdata/db.miek.nl miek.nl { + key file testdata/Kmiek.nl.+013+59725 + }`, + false, + &Signer{ + keys: []Pair{}, + origin: "miek.nl.", + dbfile: "testdata/db.miek.nl", + directory: "/var/lib/coredns", + signedfile: "db.miek.nl.signed", + }, + }, + {`sign testdata/db.miek.nl example.org { + key file testdata/Kmiek.nl.+013+59725 + directory testdata + }`, + false, + &Signer{ + keys: []Pair{}, + origin: "example.org.", + dbfile: "testdata/db.miek.nl", + directory: "testdata", + signedfile: "db.example.org.signed", + }, + }, + // errors + {`sign db.example.org { + key file /etc/coredns/keys/Kexample.org + }`, + true, + nil, + }, + } + for i, tc := range tests { + c := caddy.NewTestController("dns", tc.input) + sign, err := parse(c) + + if err == nil && tc.shouldErr { + t.Fatalf("Test %d expected errors, but got no error", i) + } + if err != nil && !tc.shouldErr { + t.Fatalf("Test %d expected no errors, but got '%v'", i, err) + } + if tc.shouldErr { + continue + } + signer := sign.signers[0] + if x := signer.origin; x != tc.exp.origin { + t.Errorf("Test %d expected %s as origin, got %s", i, tc.exp.origin, x) + } + if x := signer.dbfile; x != tc.exp.dbfile { + t.Errorf("Test %d expected %s as dbfile, got %s", i, tc.exp.dbfile, x) + } + if x := signer.directory; x != tc.exp.directory { + t.Errorf("Test %d expected %s as directory, got %s", i, tc.exp.directory, x) + } + if x := signer.signedfile; x != tc.exp.signedfile { + t.Errorf("Test %d expected %s as signedfile, got %s", i, tc.exp.signedfile, x) + } + } +} diff --git a/ag_201_coredns/plugin/sign/sign.go b/ag_201_coredns/plugin/sign/sign.go new file mode 100644 index 0000000..982d700 --- /dev/null +++ b/ag_201_coredns/plugin/sign/sign.go @@ -0,0 +1,38 @@ +// Package sign implements a zone signer as a plugin. +package sign + +import ( + "path/filepath" + "time" +) + +// Sign contains signers that sign the zones files. +type Sign struct { + signers []*Signer +} + +// OnStartup scans all signers and signs or resigns zones if needed. +func (s *Sign) OnStartup() error { + for _, signer := range s.signers { + why := signer.resign() + if why == nil { + log.Infof("Skipping signing zone %q in %q: signatures are valid", signer.origin, filepath.Join(signer.directory, signer.signedfile)) + continue + } + go signAndLog(signer, why) + } + return nil +} + +// Various duration constants for signing of the zones. +const ( + durationExpireDays = 7 * 24 * time.Hour // max time allowed before expiration + durationResignDays = 6 * 24 * time.Hour // if the last sign happened this long ago, sign again + durationSignatureExpireDays = 32 * 24 * time.Hour // sign for 32 days + durationRefreshHours = 5 * time.Hour // check zones every 5 hours + durationInceptionJitter = -18 * time.Hour // default max jitter for the inception + durationExpirationDayJitter = 5 * 24 * time.Hour // default max jitter for the expiration + durationSignatureInceptionHours = -3 * time.Hour // -(2+1) hours, be sure to catch daylight saving time and such, jitter is subtracted +) + +const timeFmt = "2006-01-02T15:04:05.000Z07:00" diff --git a/ag_201_coredns/plugin/sign/signer.go b/ag_201_coredns/plugin/sign/signer.go new file mode 100644 index 0000000..95ce94b --- /dev/null +++ b/ag_201_coredns/plugin/sign/signer.go @@ -0,0 +1,210 @@ +package sign + +import ( + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/file/tree" + clog "github.com/coredns/coredns/plugin/pkg/log" + + "github.com/miekg/dns" +) + +var log = clog.NewWithPlugin("sign") + +// Signer holds the data needed to sign a zone file. +type Signer struct { + keys []Pair + origin string + dbfile string + directory string + jitterIncep time.Duration + jitterExpir time.Duration + + signedfile string + stop chan struct{} +} + +// Sign signs a zone file according to the parameters in s. +func (s *Signer) Sign(now time.Time) (*file.Zone, error) { + rd, err := os.Open(s.dbfile) + if err != nil { + return nil, err + } + + z, err := Parse(rd, s.origin, s.dbfile) + if err != nil { + return nil, err + } + + mttl := z.Apex.SOA.Minttl + ttl := z.Apex.SOA.Header().Ttl + inception, expiration := lifetime(now, s.jitterIncep, s.jitterExpir) + z.Apex.SOA.Serial = uint32(now.Unix()) + + for _, pair := range s.keys { + pair.Public.Header().Ttl = ttl // set TTL on key so it matches the RRSIG. + z.Insert(pair.Public) + z.Insert(pair.Public.ToDS(dns.SHA1).ToCDS()) + z.Insert(pair.Public.ToDS(dns.SHA256).ToCDS()) + z.Insert(pair.Public.ToCDNSKEY()) + } + + names := names(s.origin, z) + ln := len(names) + + for _, pair := range s.keys { + rrsig, err := pair.signRRs([]dns.RR{z.Apex.SOA}, s.origin, ttl, inception, expiration) + if err != nil { + return nil, err + } + z.Insert(rrsig) + // NS apex may not be set if RR's have been discarded because the origin doesn't match. + if len(z.Apex.NS) > 0 { + rrsig, err = pair.signRRs(z.Apex.NS, s.origin, ttl, inception, expiration) + if err != nil { + return nil, err + } + z.Insert(rrsig) + } + } + + // We are walking the tree in the same direction, so names[] can be used here to indicated the next element. + i := 1 + err = z.AuthWalk(func(e *tree.Elem, zrrs map[uint16][]dns.RR, auth bool) error { + if !auth { + return nil + } + + if e.Name() == s.origin { + nsec := NSEC(e.Name(), names[(ln+i)%ln], mttl, append(e.Types(), dns.TypeNS, dns.TypeSOA, dns.TypeRRSIG, dns.TypeNSEC)) + z.Insert(nsec) + } else { + nsec := NSEC(e.Name(), names[(ln+i)%ln], mttl, append(e.Types(), dns.TypeRRSIG, dns.TypeNSEC)) + z.Insert(nsec) + } + + for t, rrs := range zrrs { + // RRSIGs are not signed and NS records are not signed because we are never authoratiative for them. + // The zone's apex nameservers records are not kept in this tree and are signed separately. + if t == dns.TypeRRSIG || t == dns.TypeNS { + continue + } + for _, pair := range s.keys { + rrsig, err := pair.signRRs(rrs, s.origin, rrs[0].Header().Ttl, inception, expiration) + if err != nil { + return err + } + e.Insert(rrsig) + } + } + i++ + return nil + }) + return z, err +} + +// resign checks if the signed zone exists, or needs resigning. +func (s *Signer) resign() error { + signedfile := filepath.Join(s.directory, s.signedfile) + rd, err := os.Open(filepath.Clean(signedfile)) + if err != nil && os.IsNotExist(err) { + return err + } + + now := time.Now().UTC() + return resign(rd, now) +} + +// resign will scan rd and check the signature on the SOA record. We will resign on the basis +// of 2 conditions: +// * either the inception is more than 6 days ago, or +// * we only have 1 week left on the signature +// +// All SOA signatures will be checked. If the SOA isn't found in the first 100 +// records, we will resign the zone. +func resign(rd io.Reader, now time.Time) (why error) { + zp := dns.NewZoneParser(rd, ".", "resign") + zp.SetIncludeAllowed(true) + i := 0 + + for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { + if err := zp.Err(); err != nil { + return err + } + + switch x := rr.(type) { + case *dns.RRSIG: + if x.TypeCovered != dns.TypeSOA { + continue + } + incep, _ := time.Parse("20060102150405", dns.TimeToString(x.Inception)) + // If too long ago, resign. + if now.Sub(incep) >= 0 && now.Sub(incep) > durationResignDays { + return fmt.Errorf("inception %q was more than: %s ago from %s: %s", incep.Format(timeFmt), durationResignDays, now.Format(timeFmt), now.Sub(incep)) + } + // Inception hasn't even start yet. + if now.Sub(incep) < 0 { + return fmt.Errorf("inception %q date is in the future: %s", incep.Format(timeFmt), now.Sub(incep)) + } + + expire, _ := time.Parse("20060102150405", dns.TimeToString(x.Expiration)) + if expire.Sub(now) < durationExpireDays { + return fmt.Errorf("expiration %q is less than: %s away from %s: %s", expire.Format(timeFmt), durationExpireDays, now.Format(timeFmt), expire.Sub(now)) + } + } + i++ + if i > 100 { + // 100 is a random number. A SOA record should be the first in the zonefile, but RFC 1035 doesn't actually mandate this. So it could + // be 3rd or even later. The number 100 looks crazy high enough that it will catch all weird zones, but not high enough to keep the CPU + // busy with parsing all the time. + return fmt.Errorf("no SOA RRSIG found in first 100 records") + } + } + + return nil +} + +func signAndLog(s *Signer, why error) { + now := time.Now().UTC() + z, err := s.Sign(now) + log.Infof("Signing %q because %s", s.origin, why) + if err != nil { + log.Warningf("Error signing %q with key tags %q in %s: %s, next: %s", s.origin, keyTag(s.keys), time.Since(now), err, now.Add(durationRefreshHours).Format(timeFmt)) + return + } + + if err := s.write(z); err != nil { + log.Warningf("Error signing %q: failed to move zone file into place: %s", s.origin, err) + return + } + log.Infof("Successfully signed zone %q in %q with key tags %q and %d SOA serial, elapsed %f, next: %s", s.origin, filepath.Join(s.directory, s.signedfile), keyTag(s.keys), z.Apex.SOA.Serial, time.Since(now).Seconds(), now.Add(durationRefreshHours).Format(timeFmt)) +} + +// refresh checks every val if some zones need to be resigned. +func (s *Signer) refresh(val time.Duration) { + tick := time.NewTicker(val) + defer tick.Stop() + for { + select { + case <-s.stop: + return + case <-tick.C: + why := s.resign() + if why == nil { + continue + } + signAndLog(s, why) + } + } +} + +func lifetime(now time.Time, jitterInception, jitterExpiration time.Duration) (uint32, uint32) { + incep := uint32(now.Add(durationSignatureInceptionHours).Add(jitterInception).Unix()) + expir := uint32(now.Add(durationSignatureExpireDays).Add(jitterExpiration).Unix()) + return incep, expir +} diff --git a/ag_201_coredns/plugin/sign/signer_test.go b/ag_201_coredns/plugin/sign/signer_test.go new file mode 100644 index 0000000..17f11ab --- /dev/null +++ b/ag_201_coredns/plugin/sign/signer_test.go @@ -0,0 +1,177 @@ +package sign + +import ( + "os" + "testing" + "time" + + "github.com/coredns/caddy" + + "github.com/miekg/dns" +) + +func TestSign(t *testing.T) { + input := `sign testdata/db.miek.nl miek.nl { + key file testdata/Kmiek.nl.+013+59725 + directory testdata + }` + c := caddy.NewTestController("dns", input) + sign, err := parse(c) + if err != nil { + t.Fatal(err) + } + if len(sign.signers) != 1 { + t.Fatalf("Expected 1 signer, got %d", len(sign.signers)) + } + z, err := sign.signers[0].Sign(time.Now().UTC()) + if err != nil { + t.Error(err) + } + + apex, _ := z.Search("miek.nl.") + if x := apex.Type(dns.TypeDS); len(x) != 0 { + t.Errorf("Expected %d DS records, got %d", 0, len(x)) + } + if x := apex.Type(dns.TypeCDS); len(x) != 2 { + t.Errorf("Expected %d CDS records, got %d", 2, len(x)) + } + if x := apex.Type(dns.TypeCDNSKEY); len(x) != 1 { + t.Errorf("Expected %d CDNSKEY record, got %d", 1, len(x)) + } + if x := apex.Type(dns.TypeDNSKEY); len(x) != 1 { + t.Errorf("Expected %d DNSKEY record, got %d", 1, len(x)) + } +} + +func TestSignApexZone(t *testing.T) { + apex := `$TTL 30M +$ORIGIN example.org. +@ IN SOA linode miek.miek.nl. ( 1282630060 4H 1H 7D 4H ) + IN NS linode +` + if err := os.WriteFile("db.apex-test.example.org", []byte(apex), 0644); err != nil { + t.Fatal(err) + } + defer os.Remove("db.apex-test.example.org") + input := `sign db.apex-test.example.org example.org { + key file testdata/Kmiek.nl.+013+59725 + directory testdata + }` + c := caddy.NewTestController("dns", input) + sign, err := parse(c) + if err != nil { + t.Fatal(err) + } + z, err := sign.signers[0].Sign(time.Now().UTC()) + if err != nil { + t.Error(err) + } + + el, _ := z.Search("example.org.") + nsec := el.Type(dns.TypeNSEC) + if len(nsec) != 1 { + t.Errorf("Expected 1 NSEC for %s, got %d", "example.org.", len(nsec)) + } + if x := nsec[0].(*dns.NSEC).NextDomain; x != "example.org." { + t.Errorf("Expected NSEC NextDomain %s, got %s", "example.org.", x) + } + if x := nsec[0].(*dns.NSEC).TypeBitMap; len(x) != 7 { + t.Errorf("Expected NSEC bitmap to be %d elements, got %d", 7, x) + } + if x := nsec[0].(*dns.NSEC).TypeBitMap; x[6] != dns.TypeCDNSKEY { + t.Errorf("Expected NSEC bitmap element 5 to be %d, got %d", dns.TypeCDNSKEY, x[6]) + } + if x := nsec[0].(*dns.NSEC).TypeBitMap; x[4] != dns.TypeDNSKEY { + t.Errorf("Expected NSEC bitmap element 4 to be %d, got %d", dns.TypeDNSKEY, x[4]) + } + dnskey := el.Type(dns.TypeDNSKEY) + if x := dnskey[0].Header().Ttl; x != 1800 { + t.Errorf("Expected DNSKEY TTL to be %d, got %d", 1800, x) + } + sigs := el.Type(dns.TypeRRSIG) + for _, s := range sigs { + if s.(*dns.RRSIG).TypeCovered == dns.TypeDNSKEY { + if s.(*dns.RRSIG).OrigTtl != dnskey[0].Header().Ttl { + t.Errorf("Expected RRSIG original TTL to match DNSKEY TTL, but %d != %d", s.(*dns.RRSIG).OrigTtl, dnskey[0].Header().Ttl) + } + if s.(*dns.RRSIG).SignerName != dnskey[0].Header().Name { + t.Errorf("Expected RRSIG signer name to match DNSKEY ownername, but %s != %s", s.(*dns.RRSIG).SignerName, dnskey[0].Header().Name) + } + } + } +} + +func TestSignGlue(t *testing.T) { + input := `sign testdata/db.miek.nl miek.nl { + key file testdata/Kmiek.nl.+013+59725 + directory testdata + }` + c := caddy.NewTestController("dns", input) + sign, err := parse(c) + if err != nil { + t.Fatal(err) + } + if len(sign.signers) != 1 { + t.Fatalf("Expected 1 signer, got %d", len(sign.signers)) + } + z, err := sign.signers[0].Sign(time.Now().UTC()) + if err != nil { + t.Error(err) + } + + e, _ := z.Search("ns2.bla.miek.nl.") + sigs := e.Type(dns.TypeRRSIG) + if len(sigs) != 0 { + t.Errorf("Expected no RRSIG for %s, got %d", "ns2.bla.miek.nl.", len(sigs)) + } +} + +func TestSignDS(t *testing.T) { + input := `sign testdata/db.miek.nl_ns miek.nl { + key file testdata/Kmiek.nl.+013+59725 + directory testdata + }` + c := caddy.NewTestController("dns", input) + sign, err := parse(c) + if err != nil { + t.Fatal(err) + } + if len(sign.signers) != 1 { + t.Fatalf("Expected 1 signer, got %d", len(sign.signers)) + } + z, err := sign.signers[0].Sign(time.Now().UTC()) + if err != nil { + t.Error(err) + } + + // dnssec-signzone outputs this for db.miek.nl_ns: + // + // child.miek.nl. 1800 IN NS ns.child.miek.nl. + // child.miek.nl. 1800 IN DS 34385 13 2 fc7397c77afbccb6742fc.... + // child.miek.nl. 1800 IN RRSIG DS 13 3 1800 20191223121229 20191123121229 59725 miek.nl. ZwptLzVVs.... + // child.miek.nl. 14400 IN NSEC www.miek.nl. NS DS RRSIG NSEC + // child.miek.nl. 14400 IN RRSIG NSEC 13 3 14400 20191223121229 20191123121229 59725 miek.nl. w+CcA8... + + name := "child.miek.nl." + e, _ := z.Search(name) + if x := len(e.Types()); x != 4 { // NS DS NSEC and 2x RRSIG + t.Errorf("Expected 4 records for %s, got %d", name, x) + } + + ds := e.Type(dns.TypeDS) + if len(ds) != 1 { + t.Errorf("Expected DS for %s, got %d", name, len(ds)) + } + sigs := e.Type(dns.TypeRRSIG) + if len(sigs) != 2 { + t.Errorf("Expected no RRSIG for %s, got %d", name, len(sigs)) + } + nsec := e.Type(dns.TypeNSEC) + if x := nsec[0].(*dns.NSEC).NextDomain; x != "www.miek.nl." { + t.Errorf("Expected no NSEC NextDomain to be %s for %s, got %s", "www.miek.nl.", name, x) + } + minttl := z.Apex.SOA.Minttl + if x := nsec[0].Header().Ttl; x != minttl { + t.Errorf("Expected no NSEC TTL to be %d for %s, got %d", minttl, "www.miek.nl.", x) + } +} diff --git a/ag_201_coredns/plugin/sign/testdata/Kmiek.nl.+013+59725.key b/ag_201_coredns/plugin/sign/testdata/Kmiek.nl.+013+59725.key new file mode 100644 index 0000000..b3e3654 --- /dev/null +++ b/ag_201_coredns/plugin/sign/testdata/Kmiek.nl.+013+59725.key @@ -0,0 +1,5 @@ +; This is a key-signing key, keyid 59725, for miek.nl. +; Created: 20190709192036 (Tue Jul 9 20:20:36 2019) +; Publish: 20190709192036 (Tue Jul 9 20:20:36 2019) +; Activate: 20190709192036 (Tue Jul 9 20:20:36 2019) +miek.nl. IN DNSKEY 257 3 13 sfzRg5nDVxbeUc51su4MzjgwpOpUwnuu81SlRHqJuXe3SOYOeypR69tZ 52XLmE56TAmPHsiB8Rgk+NTpf0o1Cw== diff --git a/ag_201_coredns/plugin/sign/testdata/Kmiek.nl.+013+59725.private b/ag_201_coredns/plugin/sign/testdata/Kmiek.nl.+013+59725.private new file mode 100644 index 0000000..2545ed9 --- /dev/null +++ b/ag_201_coredns/plugin/sign/testdata/Kmiek.nl.+013+59725.private @@ -0,0 +1,6 @@ +Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: rm7EdHRca//6xKpJzeoLt/mrfgQnltJ0WpQGtOG59yo= +Created: 20190709192036 +Publish: 20190709192036 +Activate: 20190709192036 diff --git a/ag_201_coredns/plugin/sign/testdata/db.miek.nl b/ag_201_coredns/plugin/sign/testdata/db.miek.nl new file mode 100644 index 0000000..4041b1b --- /dev/null +++ b/ag_201_coredns/plugin/sign/testdata/db.miek.nl @@ -0,0 +1,17 @@ +$TTL 30M +$ORIGIN miek.nl. +@ IN SOA linode.atoom.net. miek.miek.nl. ( 1282630060 4H 1H 7D 4H ) + IN NS linode.atoom.net. + IN MX 1 aspmx.l.google.com. + IN AAAA 2a01:7e00::f03c:91ff:fe79:234c + IN DNSKEY 257 3 13 sfzRg5nDVxbeUc51su4MzjgwpOpUwnuu81SlRHqJuXe3SOYOeypR69tZ52XLmE56TAmPHsiB8Rgk+NTpf0o1Cw== + +a IN AAAA 2a01:7e00::f03c:91ff:fe79:234c +www IN CNAME a + + +bla IN NS ns1.bla.com. +ns3.blaaat.miek.nl. IN AAAA ::1 ; non-glue, should be signed. +; in baliwick nameserver that requires glue, should not be signed +bla IN NS ns2.bla.miek.nl. +ns2.bla.miek.nl. IN A 127.0.0.1 diff --git a/ag_201_coredns/plugin/sign/testdata/db.miek.nl_ns b/ag_201_coredns/plugin/sign/testdata/db.miek.nl_ns new file mode 100644 index 0000000..bd2371f --- /dev/null +++ b/ag_201_coredns/plugin/sign/testdata/db.miek.nl_ns @@ -0,0 +1,10 @@ +$TTL 30M +$ORIGIN miek.nl. +@ IN SOA linode.atoom.net. miek.miek.nl. ( 1282630060 4H 1H 7D 4H ) + NS linode.atoom.net. + DNSKEY 257 3 13 sfzRg5nDVxbeUc51su4MzjgwpOpUwnuu81SlRHqJuXe3SOYOeypR69tZ52XLmE56TAmPHsiB8Rgk+NTpf0o1Cw== + +www AAAA ::1 +child NS ns.child +ns.child AAAA ::1 +child DS 34385 13 2 fc7397c77afbccb6742fcff19c7b1410d0044661e7085fc200ae1ab3d15a5842 diff --git a/ag_201_coredns/plugin/template/README.md b/ag_201_coredns/plugin/template/README.md new file mode 100644 index 0000000..5e14ae2 --- /dev/null +++ b/ag_201_coredns/plugin/template/README.md @@ -0,0 +1,294 @@ +# template + +## Name + +*template* - allows for dynamic responses based on the incoming query. + +## Description + +The *template* plugin allows you to dynamically respond to queries by just writing a (Go) template. + +## Syntax + +~~~ +template CLASS TYPE [ZONE...] { + match REGEX... + answer RR + additional RR + authority RR + rcode CODE + fallthrough [FALLTHROUGH-ZONE...] +} +~~~ + +* **CLASS** the query class (usually IN or ANY). +* **TYPE** the query type (A, PTR, ... can be ANY to match all types). +* **ZONE** the zone scope(s) for this template. Defaults to the server zones. +* `match` **REGEX** [Go regexp](https://golang.org/pkg/regexp/) that are matched against the incoming question name. + Specifying no regex matches everything (default: `.*`). First matching regex wins. +* `answer|additional|authority` **RR** A [RFC 1035](https://tools.ietf.org/html/rfc1035#section-5) style resource record fragment + built by a [Go template](https://golang.org/pkg/text/template/) that contains the reply. Specifying no answer will result + in a response with an empty answer section. +* `rcode` **CODE** A response code (`NXDOMAIN, SERVFAIL, ...`). The default is `NOERROR`. Valid response code values are + per the `RcodeToString` map defined by the `miekg/dns` package in `msg.go`. +* `fallthrough` Continue with the next _template_ instance if the _template_'s **ZONE** matches a query name but no regex match. + If there is no next _template_, continue resolution with the next plugin. If **[FALLTHROUGH-ZONE...]** are listed (for example + `in-addr.arpa` and `ip6.arpa`), then only queries for those zones will be subject to fallthrough. Without + `fallthrough`, when the _template_'s **ZONE** matches a query but no regex match then a `SERVFAIL` response is returned. + +[Also see](#also-see) contains an additional reading list. + +## Templates + +Each resource record is a full-featured [Go template](https://golang.org/pkg/text/template/) with the following predefined data + +* `.Zone` the matched zone string (e.g. `example.`). +* `.Name` the query name, as a string (lowercased). +* `.Class` the query class (usually `IN`). +* `.Type` the RR type requested (e.g. `PTR`). +* `.Match` an array of all matches. `index .Match 0` refers to the whole match. +* `.Group` a map of the named capture groups. +* `.Message` the complete incoming DNS message. +* `.Question` the matched question section. +* `.Remote` client’s IP address +* `.Meta` a function that takes a metadata name and returns the value, if the + metadata plugin is enabled. For example, `.Meta "kubernetes/client-namespace"` + +and the following predefined [template functions](https://golang.org/pkg/text/template#hdr-Functions) + +* `parseInt` interprets a string in the given base and bit size. Equivalent to [strconv.ParseUint](https://golang.org/pkg/strconv#ParseUint). + +The output of the template must be a [RFC 1035](https://tools.ietf.org/html/rfc1035) style resource record (commonly referred to as a "zone file"). + +**WARNING** there is a syntactical problem with Go templates and CoreDNS config files. Expressions + like `{{$var}}` will be interpreted as a reference to an environment variable by CoreDNS (and + Caddy) while `{{ $var }}` will work. See [Bugs](#bugs) and corefile(5). + +## Metrics + +If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported: + +* `coredns_template_matches_total{server, zone, view, class, type}` the total number of matched requests by regex. +* `coredns_template_template_failures_total{server, zone, view, class, type, section, template}` the number of times the Go templating failed. Regex, section and template label values can be used to map the error back to the config file. +* `coredns_template_rr_failures_total{server, zone, view, class, type, section, template}` the number of times the templated resource record was invalid and could not be parsed. Regex, section and template label values can be used to map the error back to the config file. + +Both failure cases indicate a problem with the template configuration. The `server` label indicates +the server incrementing the metric, see the *metrics* plugin for details. + +## Examples + +### Resolve everything to NXDOMAIN + +The most simplistic template is + +~~~ corefile +. { + template ANY ANY { + rcode NXDOMAIN + } +} +~~~ + +1. This template uses the default zone (`.` or all queries) +2. All queries will be answered (no `fallthrough`) +3. The answer is always NXDOMAIN + +### Resolve .invalid as NXDOMAIN + +The `.invalid` domain is a reserved TLD (see [RFC 2606 Reserved Top Level DNS Names](https://tools.ietf.org/html/rfc2606#section-2)) to indicate invalid domains. + +~~~ corefile +. { + forward . 8.8.8.8 + + template ANY ANY invalid { + rcode NXDOMAIN + authority "invalid. 60 {{ .Class }} SOA ns.invalid. hostmaster.invalid. (1 60 60 60 60)" + } +} +~~~ + +1. A query to .invalid will result in NXDOMAIN (rcode) +2. A dummy SOA record is sent to hand out a TTL of 60s for caching purposes +3. Querying `.invalid` in the `CH` class will also cause a NXDOMAIN/SOA response +4. The default regex is `.*` + +### Block invalid search domain completions + +Imagine you run `example.com` with a datacenter `dc1.example.com`. The datacenter domain +is part of the DNS search domain. +However `something.example.com.dc1.example.com` would indicate a fully qualified +domain name (`something.example.com`) that inadvertently has the default domain or search +path (`dc1.example.com`) added. + +~~~ corefile +. { + forward . 8.8.8.8 + + template IN ANY example.com.dc1.example.com { + rcode NXDOMAIN + authority "{{ .Zone }} 60 IN SOA ns.example.com hostmaster.example.com (1 60 60 60 60)" + } +} +~~~ + +A more verbose regex based equivalent would be + +~~~ corefile +. { + forward . 8.8.8.8 + + template IN ANY example.com { + match "example\.com\.(dc1\.example\.com\.)$" + rcode NXDOMAIN + authority "{{ index .Match 1 }} 60 IN SOA ns.{{ index .Match 1 }} hostmaster.{{ index .Match 1 }} (1 60 60 60 60)" + fallthrough + } +} +~~~ + +The regex-based version can do more complex matching/templating while zone-based templating is easier to read and use. + +### Resolve A/PTR for .example + +~~~ corefile +. { + forward . 8.8.8.8 + + # ip-a-b-c-d.example A a.b.c.d + + template IN A example { + match (^|[.])ip-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN A {{ .Group.a }}.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + fallthrough + } + + # d.c.b.a.in-addr.arpa PTR ip-a-b-c-d.example + + template IN PTR in-addr.arpa { + match ^(?P[0-9]*)[.](?P[0-9]*)[.](?P[0-9]*)[.](?P[0-9]*)[.]in-addr[.]arpa[.]$ + answer "{{ .Name }} 60 IN PTR ip-{{ .Group.a }}-{{ .Group.b }}-{{ .Group.c }}-{{ .Group.d }}.example." + } +} +~~~ + +An IPv4 address consists of 4 bytes, `a.b.c.d`. Named groups make it less error-prone to reverse the +IP address in the PTR case. Try to use named groups to explain what your regex and template are doing. + +Note that the A record is actually a wildcard: any subdomain of the IP address will resolve to the IP address. + +Having templates to map certain PTR/A pairs is a common pattern. + +Fallthrough is needed for mixed domains where only some responses are templated. + +### Resolve hexadecimal ip pattern using parseInt + +~~~ corefile +. { + forward . 8.8.8.8 + + template IN A example { + match "^ip0a(?P[a-f0-9]{2})(?P[a-f0-9]{2})(?P[a-f0-9]{2})[.]example[.]$" + answer "{{ .Name }} 60 IN A 10.{{ parseInt .Group.b 16 8 }}.{{ parseInt .Group.c 16 8 }}.{{ parseInt .Group.d 16 8 }}" + fallthrough + } +} +~~~ + +An IPv4 address can be expressed in a more compact form using its hexadecimal encoding. +For example `ip-10-123-123.example.` can instead be expressed as `ip0a7b7b7b.example.` + +### Resolve multiple ip patterns + +~~~ corefile +. { + forward . 8.8.8.8 + + template IN A example { + match "^ip-(?P10)-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]dc[.]example[.]$" + match "^(?P[0-9]*)[.](?P[0-9]*)[.](?P[0-9]*)[.](?P[0-9]*)[.]ext[.]example[.]$" + answer "{{ .Name }} 60 IN A {{ .Group.a}}.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + fallthrough + } +} +~~~ + +Named capture groups can be used to template one response for multiple patterns. + +### Resolve A and MX records for IP templates in .example + +~~~ corefile +. { + forward . 8.8.8.8 + + template IN A example { + match ^ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + fallthrough + } + template IN MX example { + match ^ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN MX 10 {{ .Name }}" + additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + fallthrough + } +} +~~~ + +### Adding authoritative nameservers to the response + +~~~ corefile +. { + forward . 8.8.8.8 + + template IN A example { + match ^ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + authority "example. 60 IN NS ns0.example." + authority "example. 60 IN NS ns1.example." + additional "ns0.example. 60 IN A 203.0.113.8" + additional "ns1.example. 60 IN A 198.51.100.8" + fallthrough + } + template IN MX example { + match ^ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN MX 10 {{ .Name }}" + additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + authority "example. 60 IN NS ns0.example." + authority "example. 60 IN NS ns1.example." + additional "ns0.example. 60 IN A 203.0.113.8" + additional "ns1.example. 60 IN A 198.51.100.8" + fallthrough + } +} +~~~ + +### Fabricate a CNAME + +This example responds with a CNAME to `google.com` for any DNS query made exactly for `foogle.com`. +The answer will also contain a record for `google.com` if the upstream nameserver can return a record for it of the +requested type. + +~~~ corefile +. { + template IN ANY foogle.com { + match "^foogle\.com\.$" + answer "foogle.com 60 IN CNAME google.com" + } + forward . 8.8.8.8 +} +~~~ + +## Also see + +* [Go regexp](https://golang.org/pkg/regexp/) for details about the regex implementation +* [RE2 syntax reference](https://github.com/google/re2/wiki/Syntax) for details about the regex syntax +* [RFC 1034](https://tools.ietf.org/html/rfc1034#section-3.6.1) and [RFC 1035](https://tools.ietf.org/html/rfc1035#section-5) for the resource record format +* [Go template](https://golang.org/pkg/text/template/) for the template language reference + +## Bugs + +CoreDNS supports [caddyfile environment variables](https://caddyserver.com/docs/caddyfile#env) +with notion of `{$ENV_VAR}`. This parser feature will break [Go template variables](https://golang.org/pkg/text/template/#hdr-Variables) notations like`{{$variable}}`. +The equivalent notation `{{ $variable }}` will work. +Try to avoid Go template variables in the context of this plugin. diff --git a/ag_201_coredns/plugin/template/cname_test.go b/ag_201_coredns/plugin/template/cname_test.go new file mode 100644 index 0000000..c7e81df --- /dev/null +++ b/ag_201_coredns/plugin/template/cname_test.go @@ -0,0 +1,96 @@ +package template + +import ( + "context" + "regexp" + "testing" + gotmpl "text/template" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +func TestTruncatedCNAME(t *testing.T) { + up := &Upstub{ + Qclass: dns.ClassINET, + Truncated: true, + Case: test.Case{ + Qname: "cname.test.", + Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.CNAME("cname.test. 600 IN CNAME test.up"), + test.A("test.up. 600 IN A 1.2.3.4"), + }, + }, + } + + handler := Handler{ + Zones: []string{"."}, + Templates: []template{{ + regex: []*regexp.Regexp{regexp.MustCompile("^cname\\.test\\.$")}, + answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse(up.Answer[0].String()))}, + qclass: dns.ClassINET, + qtype: dns.TypeA, + zones: []string{"test."}, + upstream: up, + }}, + } + + r := &dns.Msg{Question: []dns.Question{{Name: up.Qname, Qclass: up.Qclass, Qtype: up.Qtype}}} + w := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := handler.ServeDNS(context.TODO(), w, r) + + if err != nil { + t.Fatalf("Unexpecetd error %q", err) + } + if w.Msg == nil { + t.Fatalf("Unexpecetd empty response.") + } + if !w.Msg.Truncated { + t.Error("Expected reply to be marked truncated.") + } + err = test.SortAndCheck(w.Msg, up.Case) + if err != nil { + t.Error(err) + } +} + +// Upstub implements an Upstreamer that returns a set response for test purposes +type Upstub struct { + test.Case + Truncated bool + Qclass uint16 +} + +// Lookup returns a set response +func (t *Upstub) Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) { + var answer []dns.RR + // if query type is not CNAME, remove any CNAME with same name as qname from the answer + if t.Qtype != dns.TypeCNAME { + for _, a := range t.Answer { + if c, ok := a.(*dns.CNAME); ok && c.Header().Name == t.Qname { + continue + } + answer = append(answer, a) + } + } else { + answer = t.Answer + } + + return &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Response: true, + Truncated: t.Truncated, + Rcode: t.Rcode, + }, + Question: []dns.Question{{Name: t.Qname, Qtype: t.Qtype, Qclass: t.Qclass}}, + Answer: answer, + Extra: t.Extra, + Ns: t.Ns, + }, nil +} diff --git a/ag_201_coredns/plugin/template/log_test.go b/ag_201_coredns/plugin/template/log_test.go new file mode 100644 index 0000000..13d6e6b --- /dev/null +++ b/ag_201_coredns/plugin/template/log_test.go @@ -0,0 +1,5 @@ +package template + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/template/metrics.go b/ag_201_coredns/plugin/template/metrics.go new file mode 100644 index 0000000..6a6912a --- /dev/null +++ b/ag_201_coredns/plugin/template/metrics.go @@ -0,0 +1,32 @@ +package template + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // templateMatchesCount is the counter of template regex matches. + templateMatchesCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "template", + Name: "matches_total", + Help: "Counter of template regex matches.", + }, []string{"server", "zone", "view", "class", "type"}) + // templateFailureCount is the counter of go template failures. + templateFailureCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "template", + Name: "template_failures_total", + Help: "Counter of go template failures.", + }, []string{"server", "zone", "view", "class", "type", "section", "template"}) + // templateRRFailureCount is the counter of mis-templated RRs. + templateRRFailureCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "template", + Name: "rr_failures_total", + Help: "Counter of mis-templated RRs.", + }, []string{"server", "zone", "view", "class", "type", "section", "template"}) +) diff --git a/ag_201_coredns/plugin/template/setup.go b/ag_201_coredns/plugin/template/setup.go new file mode 100644 index 0000000..fc6b751 --- /dev/null +++ b/ag_201_coredns/plugin/template/setup.go @@ -0,0 +1,145 @@ +package template + +import ( + "regexp" + gotmpl "text/template" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/upstream" + + "github.com/miekg/dns" +) + +func init() { plugin.Register("template", setupTemplate) } + +func setupTemplate(c *caddy.Controller) error { + handler, err := templateParse(c) + if err != nil { + return plugin.Error("template", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + handler.Next = next + return handler + }) + + return nil +} + +func templateParse(c *caddy.Controller) (handler Handler, err error) { + handler.Templates = make([]template, 0) + + for c.Next() { + if !c.NextArg() { + return handler, c.ArgErr() + } + class, ok := dns.StringToClass[c.Val()] + if !ok { + return handler, c.Errf("invalid query class %s", c.Val()) + } + + if !c.NextArg() { + return handler, c.ArgErr() + } + qtype, ok := dns.StringToType[c.Val()] + if !ok { + return handler, c.Errf("invalid RR class %s", c.Val()) + } + + zones := plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) + handler.Zones = append(handler.Zones, zones...) + t := template{qclass: class, qtype: qtype, zones: zones} + + t.regex = make([]*regexp.Regexp, 0) + templatePrefix := "" + + t.answer = make([]*gotmpl.Template, 0) + t.upstream = upstream.New() + + for c.NextBlock() { + switch c.Val() { + case "match": + args := c.RemainingArgs() + if len(args) == 0 { + return handler, c.ArgErr() + } + for _, regex := range args { + r, err := regexp.Compile(regex) + if err != nil { + return handler, c.Errf("could not parse regex: %s, %v", regex, err) + } + templatePrefix = templatePrefix + regex + " " + t.regex = append(t.regex, r) + } + + case "answer": + args := c.RemainingArgs() + if len(args) == 0 { + return handler, c.ArgErr() + } + for _, answer := range args { + tmpl, err := newTemplate("answer", answer) + if err != nil { + return handler, c.Errf("could not compile template: %s, %v", c.Val(), err) + } + t.answer = append(t.answer, tmpl) + } + + case "additional": + args := c.RemainingArgs() + if len(args) == 0 { + return handler, c.ArgErr() + } + for _, additional := range args { + tmpl, err := newTemplate("additional", additional) + if err != nil { + return handler, c.Errf("could not compile template: %s, %v\n", c.Val(), err) + } + t.additional = append(t.additional, tmpl) + } + + case "authority": + args := c.RemainingArgs() + if len(args) == 0 { + return handler, c.ArgErr() + } + for _, authority := range args { + tmpl, err := newTemplate("authority", authority) + if err != nil { + return handler, c.Errf("could not compile template: %s, %v\n", c.Val(), err) + } + t.authority = append(t.authority, tmpl) + } + + case "rcode": + if !c.NextArg() { + return handler, c.ArgErr() + } + rcode, ok := dns.StringToRcode[c.Val()] + if !ok { + return handler, c.Errf("unknown rcode %s", c.Val()) + } + t.rcode = rcode + + case "fallthrough": + t.fall.SetZonesFromArgs(c.RemainingArgs()) + + case "upstream": + // remove soon + c.RemainingArgs() + default: + return handler, c.ArgErr() + } + } + + if len(t.regex) == 0 { + t.regex = append(t.regex, regexp.MustCompile(".*")) + } + + handler.Templates = append(handler.Templates, t) + } + + return +} diff --git a/ag_201_coredns/plugin/template/setup_test.go b/ag_201_coredns/plugin/template/setup_test.go new file mode 100644 index 0000000..9f03eeb --- /dev/null +++ b/ag_201_coredns/plugin/template/setup_test.go @@ -0,0 +1,176 @@ +package template + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", `template ANY ANY { + rcode + }`) + err := setupTemplate(c) + if err == nil { + t.Errorf("Expected setupTemplate to fail on broken template, got no error") + } + c = caddy.NewTestController("dns", `template ANY ANY { + rcode NXDOMAIN + }`) + err = setupTemplate(c) + if err != nil { + t.Errorf("Expected no errors, got: %v", err) + } +} + +func TestSetupParse(t *testing.T) { + serverBlockKeys := []string{"domain.com.:8053", "dynamic.domain.com.:8053"} + + tests := []struct { + inputFileRules string + shouldErr bool + }{ + // parse errors + {`template`, true}, + {`template X`, true}, + {`template ANY`, true}, + {`template ANY X`, true}, + { + `template ANY ANY .* { + notavailable + }`, + true, + }, + { + `template ANY ANY { + answer + }`, + true, + }, + { + `template ANY ANY { + additional + }`, + true, + }, + { + `template ANY ANY { + rcode + }`, + true, + }, + { + `template ANY ANY { + rcode UNDEFINED + }`, + true, + }, + { + `template ANY ANY { + answer "{{" + }`, + true, + }, + { + `template ANY ANY { + additional "{{" + }`, + true, + }, + { + `template ANY ANY { + authority "{{" + }`, + true, + }, + { + `template ANY ANY { + answer "{{ notAFunction }}" + }`, + true, + }, + { + `template ANY ANY { + answer "{{ parseInt }}" + additional "{{ parseInt }}" + authority "{{ parseInt }}" + }`, + false, + }, + // examples + {`template ANY ANY (?P`, false}, + { + `template ANY ANY { + + }`, + false, + }, + { + `template ANY A example.com { + match ip-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]com + answer "{{ .Name }} A {{ .Group.a }}.{{ .Group.b }}.{{ .Group.c }}.{{ .Grup.d }}." + fallthrough + }`, + false, + }, + { + `template ANY AAAA example.com { + match ip-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]com + authority "example.com 60 IN SOA ns.example.com hostmaster.example.com (1 60 60 60 60)" + fallthrough + }`, + false, + }, + { + `template IN ANY example.com { + match "[.](example[.]com[.]dc1[.]example[.]com[.])$" + rcode NXDOMAIN + authority "{{ index .Match 1 }} 60 IN SOA ns.{{ index .Match 1 }} hostmaster.example.com (1 60 60 60 60)" + fallthrough example.com + }`, + false, + }, + { + `template IN A example { + match ^ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + } + template IN MX example. { + match ^ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN MX 10 {{ .Name }}" + additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + }`, + false, + }, + { + `template IN A example { + match ^ip0a(?P[a-f0-9]{2})(?P[a-f0-9]{2})(?P[a-f0-9]{2})[.]example[.]$ + answer "{{ .Name }} 3600 IN A 10.{{ parseInt .Group.b 16 8 }}.{{ parseInt .Group.c 16 8 }}.{{ parseInt .Group.d 16 8 }}" + }`, + false, + }, + { + `template IN MX example { + match ^ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN MX 10 {{ .Name }}" + additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + authority "example. 60 IN NS ns0.example." + authority "example. 60 IN NS ns1.example." + additional "ns0.example. 60 IN A 203.0.113.8" + additional "ns1.example. 60 IN A 198.51.100.8" + }`, + false, + }, + } + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputFileRules) + c.ServerBlockKeys = serverBlockKeys + templates, err := templateParse(c) + + if err == nil && test.shouldErr { + t.Fatalf("Test %d expected errors, but got no error\n---\n%s\n---\n%v", i, test.inputFileRules, templates) + } else if err != nil && !test.shouldErr { + t.Fatalf("Test %d expected no errors, but got '%v'", i, err) + } + } +} diff --git a/ag_201_coredns/plugin/template/template.go b/ag_201_coredns/plugin/template/template.go new file mode 100644 index 0000000..aee1e1b --- /dev/null +++ b/ag_201_coredns/plugin/template/template.go @@ -0,0 +1,215 @@ +package template + +import ( + "bytes" + "context" + "regexp" + "strconv" + gotmpl "text/template" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/metrics" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Handler is a plugin handler that takes a query and templates a response. +type Handler struct { + Zones []string + + Next plugin.Handler + Templates []template +} + +type template struct { + zones []string + rcode int + regex []*regexp.Regexp + answer []*gotmpl.Template + additional []*gotmpl.Template + authority []*gotmpl.Template + qclass uint16 + qtype uint16 + fall fall.F + upstream Upstreamer +} + +// Upstreamer looks up targets of CNAME templates +type Upstreamer interface { + Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error) +} + +type templateData struct { + Zone string + Name string + Regex string + Match []string + Group map[string]string + Class string + Type string + Message *dns.Msg + Question *dns.Question + Remote string + md map[string]metadata.Func +} + +func (data *templateData) Meta(metaName string) string { + if data.md == nil { + return "" + } + + if f, ok := data.md[metaName]; ok { + return f() + } + + return "" +} + +// ServeDNS implements the plugin.Handler interface. +func (h Handler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + + zone := plugin.Zones(h.Zones).Matches(state.Name()) + if zone == "" { + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + + for _, template := range h.Templates { + data, match, fthrough := template.match(ctx, state) + if !match { + if !fthrough { + return dns.RcodeServerFailure, nil + } + continue + } + + templateMatchesCount.WithLabelValues(metrics.WithServer(ctx), data.Zone, metrics.WithView(ctx), data.Class, data.Type).Inc() + + if template.rcode == dns.RcodeServerFailure { + return template.rcode, nil + } + + msg := new(dns.Msg) + msg.SetReply(r) + msg.Authoritative = true + msg.Rcode = template.rcode + + for _, answer := range template.answer { + rr, err := executeRRTemplate(metrics.WithServer(ctx), metrics.WithView(ctx), "answer", answer, data) + if err != nil { + return dns.RcodeServerFailure, err + } + msg.Answer = append(msg.Answer, rr) + if template.upstream != nil && (state.QType() == dns.TypeA || state.QType() == dns.TypeAAAA) && rr.Header().Rrtype == dns.TypeCNAME { + if up, err := template.upstream.Lookup(ctx, state, rr.(*dns.CNAME).Target, state.QType()); err == nil && up != nil { + msg.Truncated = up.Truncated + msg.Answer = append(msg.Answer, up.Answer...) + } + } + } + for _, additional := range template.additional { + rr, err := executeRRTemplate(metrics.WithServer(ctx), metrics.WithView(ctx), "additional", additional, data) + if err != nil { + return dns.RcodeServerFailure, err + } + msg.Extra = append(msg.Extra, rr) + } + for _, authority := range template.authority { + rr, err := executeRRTemplate(metrics.WithServer(ctx), metrics.WithView(ctx), "authority", authority, data) + if err != nil { + return dns.RcodeServerFailure, err + } + msg.Ns = append(msg.Ns, rr) + } + + w.WriteMsg(msg) + return template.rcode, nil + } + + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) +} + +// Name implements the plugin.Handler interface. +func (h Handler) Name() string { return "template" } + +func executeRRTemplate(server, view, section string, template *gotmpl.Template, data *templateData) (dns.RR, error) { + buffer := &bytes.Buffer{} + err := template.Execute(buffer, data) + if err != nil { + templateFailureCount.WithLabelValues(server, data.Zone, view, data.Class, data.Type, section, template.Tree.Root.String()).Inc() + return nil, err + } + rr, err := dns.NewRR(buffer.String()) + if err != nil { + templateRRFailureCount.WithLabelValues(server, data.Zone, view, data.Class, data.Type, section, template.Tree.Root.String()).Inc() + return rr, err + } + return rr, nil +} + +func newTemplate(name, text string) (*gotmpl.Template, error) { + funcMap := gotmpl.FuncMap{ + "parseInt": strconv.ParseUint, + } + return gotmpl.New(name).Funcs(funcMap).Parse(text) +} + +func (t template) match(ctx context.Context, state request.Request) (*templateData, bool, bool) { + q := state.Req.Question[0] + data := &templateData{md: metadata.ValueFuncs(ctx), Remote: state.IP()} + + zone := plugin.Zones(t.zones).Matches(state.Name()) + if zone == "" { + return data, false, true + } + + if t.qclass != dns.ClassANY && q.Qclass != dns.ClassANY && q.Qclass != t.qclass { + return data, false, true + } + if t.qtype != dns.TypeANY && q.Qtype != dns.TypeANY && q.Qtype != t.qtype { + return data, false, true + } + + for _, regex := range t.regex { + if !regex.MatchString(state.Name()) { + continue + } + + data.Zone = zone + data.Regex = regex.String() + data.Name = state.Name() + data.Question = &q + data.Message = state.Req + if q.Qclass != dns.ClassANY { + data.Class = dns.ClassToString[q.Qclass] + } else { + data.Class = dns.ClassToString[t.qclass] + } + if q.Qtype != dns.TypeANY { + data.Type = dns.TypeToString[q.Qtype] + } else { + data.Type = dns.TypeToString[t.qtype] + } + + matches := regex.FindStringSubmatch(state.Name()) + data.Match = make([]string, len(matches)) + data.Group = make(map[string]string) + groupNames := regex.SubexpNames() + for i, m := range matches { + data.Match[i] = m + data.Group[strconv.Itoa(i)] = m + } + for i, m := range matches { + if len(groupNames[i]) > 0 { + data.Group[groupNames[i]] = m + } + } + + return data, true, false + } + + return data, false, t.fall.Through(state.Name()) +} diff --git a/ag_201_coredns/plugin/template/template_test.go b/ag_201_coredns/plugin/template/template_test.go new file mode 100644 index 0000000..c94deb8 --- /dev/null +++ b/ag_201_coredns/plugin/template/template_test.go @@ -0,0 +1,642 @@ +package template + +import ( + "context" + "fmt" + "regexp" + "testing" + gotmpl "text/template" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestHandler(t *testing.T) { + exampleDomainATemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + exampleDomainAParseIntTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("^ip0a(?P[a-f0-9]{2})(?P[a-f0-9]{2})(?P[a-f0-9]{2})[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 IN A 10.{{ parseInt .Group.b 16 8 }}.{{ parseInt .Group.c 16 8 }}.{{ parseInt .Group.d 16 8 }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + exampleDomainIPATemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile(".*")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 IN A {{ .Remote }}"))}, + qclass: dns.ClassINET, + qtype: dns.TypeA, + fall: fall.Root, + zones: []string{"."}, + } + exampleDomainANSTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))}, + additional: []*gotmpl.Template{gotmpl.Must(newTemplate("additional", "ns0.example. IN A 203.0.113.8"))}, + authority: []*gotmpl.Template{gotmpl.Must(newTemplate("authority", "example. IN NS ns0.example.com."))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + exampleDomainMXTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 MX 10 {{ .Name }}"))}, + additional: []*gotmpl.Template{gotmpl.Must(newTemplate("additional", "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + invalidDomainTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("[.]invalid[.]$")}, + rcode: dns.RcodeNameError, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "invalid. 60 {{ .Class }} SOA a.invalid. b.invalid. (1 60 60 60 60)"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + rcodeServfailTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile(".*")}, + rcode: dns.RcodeServerFailure, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + brokenTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 IN TXT \"{{ index .Match 2 }}\""))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + brokenParseIntTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 IN TXT \"{{ parseInt \"gg\" 16 8 }}\""))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + nonRRTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + nonRRAdditionalTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, + additional: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + nonRRAuthoritativeTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, + authority: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + cnameTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("example[.]net[.]")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "example.net 60 IN CNAME target.example.com"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + mdTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", `{{ .Meta "foo" }}-{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}`))}, + additional: []*gotmpl.Template{gotmpl.Must(newTemplate("additional", `{{ .Meta "bar" }}.example. IN A 203.0.113.8`))}, + authority: []*gotmpl.Template{gotmpl.Must(newTemplate("authority", `example. IN NS {{ .Meta "bar" }}.example.com.`))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + mdMissingTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", `{{ .Meta "foofoo" }}{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}`))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + + tests := []struct { + tmpl template + qname string + name string + qclass uint16 + qtype uint16 + expectedCode int + expectedErr string + verifyResponse func(*dns.Msg) error + md map[string]string + }{ + { + name: "RcodeServFail", + tmpl: rcodeServfailTemplate, + qname: "test.invalid.", + expectedCode: dns.RcodeServerFailure, + verifyResponse: func(r *dns.Msg) error { + return nil + }, + }, + { + name: "ExampleDomainNameMismatch", + tmpl: exampleDomainATemplate, + qclass: dns.ClassINET, + qtype: dns.TypeA, + qname: "test.invalid.", + expectedCode: rcodeFallthrough, + }, + { + name: "BrokenTemplate", + tmpl: brokenTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeANY, + qname: "test.example.", + expectedCode: dns.RcodeServerFailure, + expectedErr: `template: answer:1:26: executing "answer" at : error calling index: index out of range: 2`, + verifyResponse: func(r *dns.Msg) error { + return nil + }, + }, + { + name: "NonRRTemplate", + tmpl: nonRRTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeANY, + qname: "test.example.", + expectedCode: dns.RcodeServerFailure, + expectedErr: `dns: not a TTL: "test.example." at line: 1:13`, + verifyResponse: func(r *dns.Msg) error { + return nil + }, + }, + { + name: "NonRRAdditionalTemplate", + tmpl: nonRRAdditionalTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeANY, + qname: "test.example.", + expectedCode: dns.RcodeServerFailure, + expectedErr: `dns: not a TTL: "test.example." at line: 1:13`, + verifyResponse: func(r *dns.Msg) error { + return nil + }, + }, + { + name: "NonRRAuthorityTemplate", + tmpl: nonRRAuthoritativeTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeANY, + qname: "test.example.", + expectedCode: dns.RcodeServerFailure, + expectedErr: `dns: not a TTL: "test.example." at line: 1:13`, + verifyResponse: func(r *dns.Msg) error { + return nil + }, + }, + { + name: "ExampleIPMatch", + tmpl: exampleDomainIPATemplate, + qclass: dns.ClassINET, + qtype: dns.TypeA, + qname: "test.example.", + verifyResponse: func(r *dns.Msg) error { + if len(r.Answer) != 1 { + return fmt.Errorf("expected 1 answer, got %v", len(r.Answer)) + } + if r.Answer[0].Header().Rrtype != dns.TypeA { + return fmt.Errorf("expected an A record answer, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype]) + } + if r.Answer[0].(*dns.A).A.String() != "10.240.0.1" { + return fmt.Errorf("expected an A record for 10.95.12.8, got %v", r.Answer[0].String()) + } + return nil + }, + }, + { + name: "ExampleDomainMatch", + tmpl: exampleDomainATemplate, + qclass: dns.ClassINET, + qtype: dns.TypeA, + qname: "ip-10-95-12-8.example.", + verifyResponse: func(r *dns.Msg) error { + if len(r.Answer) != 1 { + return fmt.Errorf("expected 1 answer, got %v", len(r.Answer)) + } + if r.Answer[0].Header().Rrtype != dns.TypeA { + return fmt.Errorf("expected an A record answer, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype]) + } + if r.Answer[0].(*dns.A).A.String() != "10.95.12.8" { + return fmt.Errorf("expected an A record for 10.95.12.8, got %v", r.Answer[0].String()) + } + return nil + }, + }, + { + name: "ExampleDomainMatchHexIp", + tmpl: exampleDomainAParseIntTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeA, + qname: "ip0a5f0c09.example.", + verifyResponse: func(r *dns.Msg) error { + if len(r.Answer) != 1 { + return fmt.Errorf("expected 1 answer, got %v", len(r.Answer)) + } + if r.Answer[0].Header().Rrtype != dns.TypeA { + return fmt.Errorf("expected an A record answer, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype]) + } + if r.Answer[0].(*dns.A).A.String() != "10.95.12.9" { + return fmt.Errorf("expected an A record for 10.95.12.9, got %v", r.Answer[0].String()) + } + return nil + }, + }, + { + name: "BrokenParseIntTemplate", + tmpl: brokenParseIntTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeANY, + qname: "test.example.", + expectedCode: dns.RcodeServerFailure, + expectedErr: "template: answer:1:26: executing \"answer\" at : error calling parseInt: strconv.ParseUint: parsing \"gg\": invalid syntax", + verifyResponse: func(r *dns.Msg) error { + return nil + }, + }, + { + name: "ExampleDomainMXMatch", + tmpl: exampleDomainMXTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeMX, + qname: "ip-10-95-12-8.example.", + verifyResponse: func(r *dns.Msg) error { + if len(r.Answer) != 1 { + return fmt.Errorf("expected 1 answer, got %v", len(r.Answer)) + } + if r.Answer[0].Header().Rrtype != dns.TypeMX { + return fmt.Errorf("expected an A record answer, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype]) + } + if len(r.Extra) != 1 { + return fmt.Errorf("expected 1 extra record, got %v", len(r.Extra)) + } + if r.Extra[0].Header().Rrtype != dns.TypeA { + return fmt.Errorf("expected an additional A record, got %v", dns.TypeToString[r.Extra[0].Header().Rrtype]) + } + return nil + }, + }, + { + name: "ExampleDomainANSMatch", + tmpl: exampleDomainANSTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeA, + qname: "ip-10-95-12-8.example.", + verifyResponse: func(r *dns.Msg) error { + if len(r.Answer) != 1 { + return fmt.Errorf("expected 1 answer, got %v", len(r.Answer)) + } + if r.Answer[0].Header().Rrtype != dns.TypeA { + return fmt.Errorf("expected an A record answer, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype]) + } + if len(r.Extra) != 1 { + return fmt.Errorf("expected 1 extra record, got %v", len(r.Extra)) + } + if r.Extra[0].Header().Rrtype != dns.TypeA { + return fmt.Errorf("expected an additional A record, got %v", dns.TypeToString[r.Extra[0].Header().Rrtype]) + } + if len(r.Ns) != 1 { + return fmt.Errorf("expected 1 authoritative record, got %v", len(r.Extra)) + } + if r.Ns[0].Header().Rrtype != dns.TypeNS { + return fmt.Errorf("expected an authoritative NS record, got %v", dns.TypeToString[r.Extra[0].Header().Rrtype]) + } + return nil + }, + }, + { + name: "ExampleInvalidNXDOMAIN", + tmpl: invalidDomainTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeMX, + qname: "test.invalid.", + expectedCode: dns.RcodeNameError, + verifyResponse: func(r *dns.Msg) error { + if len(r.Answer) != 1 { + return fmt.Errorf("expected 1 answer, got %v", len(r.Answer)) + } + if r.Answer[0].Header().Rrtype != dns.TypeSOA { + return fmt.Errorf("expected an SOA record answer, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype]) + } + return nil + }, + }, + { + name: "CNAMEWithoutUpstream", + tmpl: cnameTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeA, + qname: "example.net.", + expectedCode: dns.RcodeSuccess, + verifyResponse: func(r *dns.Msg) error { + if len(r.Answer) != 1 { + return fmt.Errorf("expected 1 answer, got %v", len(r.Answer)) + } + return nil + }, + }, + { + name: "mdMatch", + tmpl: mdTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeA, + qname: "ip-10-95-12-8.example.", + verifyResponse: func(r *dns.Msg) error { + if len(r.Answer) != 1 { + return fmt.Errorf("expected 1 answer, got %v", len(r.Answer)) + } + if r.Answer[0].Header().Rrtype != dns.TypeA { + return fmt.Errorf("expected an A record answer, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype]) + } + name := "myfoo-ip-10-95-12-8.example." + if r.Answer[0].Header().Name != name { + return fmt.Errorf("expected answer name %q, got %q", name, r.Answer[0].Header().Name) + } + if len(r.Extra) != 1 { + return fmt.Errorf("expected 1 extra record, got %v", len(r.Extra)) + } + if r.Extra[0].Header().Rrtype != dns.TypeA { + return fmt.Errorf("expected an additional A record, got %v", dns.TypeToString[r.Extra[0].Header().Rrtype]) + } + name = "mybar.example." + if r.Extra[0].Header().Name != name { + return fmt.Errorf("expected additional name %q, got %q", name, r.Extra[0].Header().Name) + } + if len(r.Ns) != 1 { + return fmt.Errorf("expected 1 authoritative record, got %v", len(r.Extra)) + } + if r.Ns[0].Header().Rrtype != dns.TypeNS { + return fmt.Errorf("expected an authoritative NS record, got %v", dns.TypeToString[r.Extra[0].Header().Rrtype]) + } + ns, ok := r.Ns[0].(*dns.NS) + if !ok { + return fmt.Errorf("expected NS record to be type NS, got %v", r.Ns[0]) + } + rdata := "mybar.example.com." + if ns.Ns != rdata { + return fmt.Errorf("expected ns rdata %q, got %q", rdata, ns.Ns) + } + return nil + }, + md: map[string]string{ + "foo": "myfoo", + "bar": "mybar", + "foobar": "myfoobar", + }, + }, + { + name: "mdMissing", + tmpl: mdMissingTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeA, + qname: "ip-10-95-12-8.example.", + verifyResponse: func(r *dns.Msg) error { + if len(r.Answer) != 1 { + return fmt.Errorf("expected 1 answer, got %v", len(r.Answer)) + } + if r.Answer[0].Header().Rrtype != dns.TypeA { + return fmt.Errorf("expected an A record answer, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype]) + } + name := "ip-10-95-12-8.example." + if r.Answer[0].Header().Name != name { + return fmt.Errorf("expected answer name %q, got %q", name, r.Answer[0].Header().Name) + } + return nil + }, + md: map[string]string{ + "foo": "myfoo", + }, + }, + } + + ctx := context.TODO() + + for _, tr := range tests { + handler := Handler{ + Next: test.NextHandler(rcodeFallthrough, nil), + Zones: []string{"."}, + Templates: []template{tr.tmpl}, + } + req := &dns.Msg{ + Question: []dns.Question{{ + Name: tr.qname, + Qclass: tr.qclass, + Qtype: tr.qtype, + }}, + } + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + if tr.md != nil { + ctx = metadata.ContextWithMetadata(context.Background()) + + for k, v := range tr.md { + // Go requires copying to a local variable for the closure to work + kk := k + vv := v + metadata.SetValueFunc(ctx, kk, func() string { + return vv + }) + } + } + + code, err := handler.ServeDNS(ctx, rec, req) + if err == nil && tr.expectedErr != "" { + t.Errorf("Test %v expected error: %v, got nothing", tr.name, tr.expectedErr) + } + if err != nil && tr.expectedErr == "" { + t.Errorf("Test %v expected no error got: %v", tr.name, err) + } + if err != nil && tr.expectedErr != "" && err.Error() != tr.expectedErr { + t.Errorf("Test %v expected error: %v, got: %v", tr.name, tr.expectedErr, err) + } + if code != tr.expectedCode { + t.Errorf("Test %v expected response code %v, got %v", tr.name, tr.expectedCode, code) + } + if err == nil && code != rcodeFallthrough { + // only verify if we got no error and expected no error + if err := tr.verifyResponse(rec.Msg); err != nil { + t.Errorf("Test %v could not verify the response: %v", tr.name, err) + } + } + } +} + +// TestMultiSection verifies that a corefile with multiple but different template sections works +func TestMultiSection(t *testing.T) { + ctx := context.TODO() + + multisectionConfig := ` + # Implicit section (see c.ServerBlockKeys) + # test.:8053 { + + # REFUSE IN A for the server zone (test.) + template IN A { + rcode REFUSED + } + # Fallthrough everything IN TXT for test. + template IN TXT { + match "$^" + rcode SERVFAIL + fallthrough + } + # Answer CH TXT *.coredns.invalid. / coredns.invalid. + template CH TXT coredns.invalid { + answer "{{ .Name }} 60 CH TXT \"test\"" + } + # Answer example. ip templates and fallthrough otherwise + template IN A example { + match ^ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + fallthrough + } + # Answer MX record requests for ip templates in example. and never fall through + template IN MX example { + match ^ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$ + answer "{{ .Name }} 60 IN MX 10 {{ .Name }}" + additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" + } + ` + c := caddy.NewTestController("dns", multisectionConfig) + c.ServerBlockKeys = []string{"test.:8053"} + + handler, err := templateParse(c) + if err != nil { + t.Fatalf("TestMultiSection could not parse config: %v", err) + } + + handler.Next = test.NextHandler(rcodeFallthrough, nil) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + // Asking for test. IN A -> REFUSED + + req := &dns.Msg{Question: []dns.Question{{Name: "some.test.", Qclass: dns.ClassINET, Qtype: dns.TypeA}}} + code, err := handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving some.test. A, got: %v", err) + } + if code != dns.RcodeRefused { + t.Fatalf("TestMultiSection expected response code REFUSED got: %v", code) + } + + // Asking for test. IN TXT -> fallthrough + + req = &dns.Msg{Question: []dns.Question{{Name: "some.test.", Qclass: dns.ClassINET, Qtype: dns.TypeTXT}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving some.test. TXT, got: %v", err) + } + if code != rcodeFallthrough { + t.Fatalf("TestMultiSection expected response code fallthrough got: %v", code) + } + + // Asking for coredns.invalid. CH TXT -> TXT "test" + + req = &dns.Msg{Question: []dns.Question{{Name: "coredns.invalid.", Qclass: dns.ClassCHAOS, Qtype: dns.TypeTXT}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving coredns.invalid. TXT, got: %v", err) + } + if code != dns.RcodeSuccess { + t.Fatalf("TestMultiSection expected success response for coredns.invalid. TXT got: %v", code) + } + if len(rec.Msg.Answer) != 1 { + t.Fatalf("TestMultiSection expected one answer for coredns.invalid. TXT got: %v", rec.Msg.Answer) + } + if rec.Msg.Answer[0].Header().Rrtype != dns.TypeTXT || rec.Msg.Answer[0].(*dns.TXT).Txt[0] != "test" { + t.Fatalf("TestMultiSection a \"test\" answer for coredns.invalid. TXT got: %v", rec.Msg.Answer[0]) + } + + // Asking for an ip template in example + + req = &dns.Msg{Question: []dns.Question{{Name: "ip-10-11-12-13.example.", Qclass: dns.ClassINET, Qtype: dns.TypeA}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving ip-10-11-12-13.example. IN A, got: %v", err) + } + if code != dns.RcodeSuccess { + t.Fatalf("TestMultiSection expected success response ip-10-11-12-13.example. IN A got: %v, %v", code, dns.RcodeToString[code]) + } + if len(rec.Msg.Answer) != 1 { + t.Fatalf("TestMultiSection expected one answer for ip-10-11-12-13.example. IN A got: %v", rec.Msg.Answer) + } + if rec.Msg.Answer[0].Header().Rrtype != dns.TypeA { + t.Fatalf("TestMultiSection an A RR answer for ip-10-11-12-13.example. IN A got: %v", rec.Msg.Answer[0]) + } + + // Asking for an MX ip template in example + + req = &dns.Msg{Question: []dns.Question{{Name: "ip-10-11-12-13.example.", Qclass: dns.ClassINET, Qtype: dns.TypeMX}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving ip-10-11-12-13.example. IN MX, got: %v", err) + } + if code != dns.RcodeSuccess { + t.Fatalf("TestMultiSection expected success response ip-10-11-12-13.example. IN MX got: %v, %v", code, dns.RcodeToString[code]) + } + if len(rec.Msg.Answer) != 1 { + t.Fatalf("TestMultiSection expected one answer for ip-10-11-12-13.example. IN MX got: %v", rec.Msg.Answer) + } + if rec.Msg.Answer[0].Header().Rrtype != dns.TypeMX { + t.Fatalf("TestMultiSection an A RR answer for ip-10-11-12-13.example. IN MX got: %v", rec.Msg.Answer[0]) + } + + // Test that something.example. A does fall through but something.example. MX does not + + req = &dns.Msg{Question: []dns.Question{{Name: "something.example.", Qclass: dns.ClassINET, Qtype: dns.TypeA}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving something.example. IN A, got: %v", err) + } + if code != rcodeFallthrough { + t.Fatalf("TestMultiSection expected a fall through resolving something.example. IN A, got: %v, %v", code, dns.RcodeToString[code]) + } + + req = &dns.Msg{Question: []dns.Question{{Name: "something.example.", Qclass: dns.ClassINET, Qtype: dns.TypeMX}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving something.example. IN MX, got: %v", err) + } + if code == rcodeFallthrough { + t.Fatalf("TestMultiSection expected no fall through resolving something.example. IN MX") + } + if code != dns.RcodeServerFailure { + t.Fatalf("TestMultiSection expected SERVFAIL resolving something.example. IN MX, got %v, %v", code, dns.RcodeToString[code]) + } +} + +const rcodeFallthrough = 3841 // reserved for private use, used to indicate a fallthrough diff --git a/ag_201_coredns/plugin/test/doc.go b/ag_201_coredns/plugin/test/doc.go new file mode 100644 index 0000000..75281ed --- /dev/null +++ b/ag_201_coredns/plugin/test/doc.go @@ -0,0 +1,2 @@ +// Package test contains helper functions for writing plugin tests. +package test diff --git a/ag_201_coredns/plugin/test/file.go b/ag_201_coredns/plugin/test/file.go new file mode 100644 index 0000000..969406e --- /dev/null +++ b/ag_201_coredns/plugin/test/file.go @@ -0,0 +1,106 @@ +package test + +import ( + "os" + "path/filepath" +) + +// TempFile will create a temporary file on disk and returns the name and a cleanup function to remove it later. +func TempFile(dir, content string) (string, func(), error) { + f, err := os.CreateTemp(dir, "go-test-tmpfile") + if err != nil { + return "", nil, err + } + if err := os.WriteFile(f.Name(), []byte(content), 0644); err != nil { + return "", nil, err + } + rmFunc := func() { os.Remove(f.Name()) } + return f.Name(), rmFunc, nil +} + +// WritePEMFiles creates a tmp dir with ca.pem, cert.pem, and key.pem and the func to remove it +func WritePEMFiles(dir string) (string, func(), error) { + tempDir, err := os.MkdirTemp(dir, "go-test-pemfiles") + if err != nil { + return "", nil, err + } + + data := `-----BEGIN CERTIFICATE----- +MIIC9zCCAd+gAwIBAgIJALGtqdMzpDemMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV +BAMMB2t1YmUtY2EwHhcNMTYxMDE5MTU1NDI0WhcNNDQwMzA2MTU1NDI0WjASMRAw +DgYDVQQDDAdrdWJlLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +pa4Wu/WkpJNRr8pMVE6jjwzNUOx5mIyoDr8WILSxVQcEeyVPPmAqbmYXtVZO11p9 +jTzoEqF7Kgts3HVYGCk5abqbE14a8Ru/DmV5avU2hJ/NvSjtNi/O+V6SzCbg5yR9 +lBR53uADDlzuJEQT9RHq7A5KitFkx4vUcXnjOQCbDogWFoYuOgNEwJPy0Raz3NJc +ViVfDqSJ0QHg02kCOMxcGFNRQ9F5aoW7QXZXZXD0tn3wLRlu4+GYyqt8fw5iNdLJ +t79yKp8I+vMTmMPz4YKUO+eCl5EY10Qs7wvoG/8QNbjH01BRN3L8iDT2WfxdvjTu +1RjPxFL92i+B7HZO7jGLfQIDAQABo1AwTjAdBgNVHQ4EFgQUZTrg+Xt87tkxDhlB +gKk9FdTOW3IwHwYDVR0jBBgwFoAUZTrg+Xt87tkxDhlBgKk9FdTOW3IwDAYDVR0T +BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEApB7JFVrZpGSOXNO3W7SlN6OCPXv9 +C7rIBc8rwOrzi2mZWcBmWheQrqBo8xHif2rlFNVQxtq3JcQ8kfg/m1fHeQ/Ygzel +Z+U1OqozynDySBZdNn9i+kXXgAUCqDPp3hEQWe0os/RRpIwo9yOloBxdiX6S0NIf +VB8n8kAynFPkH7pYrGrL1HQgDFCSfa4tUJ3+9sppnCu0pNtq5AdhYx9xFb2sn+8G +xGbtCkhVk2VQ+BiCWnjYXJ6ZMzabP7wiOFDP9Pvr2ik22PRItsW/TLfHFXM1jDmc +I1rs/VUGKzcJGVIWbHrgjP68CTStGAvKgbsTqw7aLXTSqtPw88N9XVSyRg== +-----END CERTIFICATE-----` + path := filepath.Join(tempDir, "ca.pem") + if err := os.WriteFile(path, []byte(data), 0644); err != nil { + return "", nil, err + } + data = `-----BEGIN CERTIFICATE----- +MIICozCCAYsCCQCRlf5BrvPuqjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdr +dWJlLWNhMB4XDTE2MTAxOTE2MDUxOFoXDTE3MTAxOTE2MDUxOFowFTETMBEGA1UE +AwwKa3ViZS1hZG1pbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMTw +a7wCFoiCad/N53aURfjrme+KR7FS0yf5Ur9OR/oM3BoS9stYu5Flzr35oL5T6t5G +c2ey78mUs/Cs07psnjUdKH55bDpJSdG7zW9mXNyeLwIefFcj/38SS5NBSotmLo8u +scJMGXeQpCQtfVuVJSP2bfU5u5d0KTLSg/Cor6UYonqrRB82HbOuuk8Wjaww4VHo +nCq7X8o948V6HN5ZibQOgMMo+nf0wORREHBjvwc4W7ewbaTcfoe1VNAo/QnkqxTF +ueMb2HxgghArqQSK8b44O05V0zrde25dVnmnte6sPjcV0plqMJ37jViISxsOPUFh +/ZW7zbIM/7CMcDekCiECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYZE8OxwRR7GR +kdd5aIriDwWfcl56cq5ICyx87U8hAZhBxk46a6a901LZPzt3xKyWIFQSRj/NYiQ+ +/thjGLZI2lhkVgYtyAD4BNxDiuppQSCbkjY9tLVDdExGttEVN7+UYDWJBHy6X16Y +xSG9FE3Dvp9LI89Nq8E3dRh+Q8wu52q9HaQXjS5YtzQOtDFKPBkihXu/c6gEHj4Y +bZVk8rFiH8/CvcQxAuvNI3VVCFUKd2LeQtqwYQQ//qoiuA15krTq5Ut9eXJ8zxAw +zhDEPP4FhY+Sz+y1yWirphl7A1aZwhXVPcfWIGqpQ3jzNwUeocbH27kuLh+U4hQo +qeg10RdFnw== +-----END CERTIFICATE-----` + path = filepath.Join(tempDir, "cert.pem") + if err = os.WriteFile(path, []byte(data), 0644); err != nil { + return "", nil, err + } + + data = `-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEAxPBrvAIWiIJp383ndpRF+OuZ74pHsVLTJ/lSv05H+gzcGhL2 +y1i7kWXOvfmgvlPq3kZzZ7LvyZSz8KzTumyeNR0ofnlsOklJ0bvNb2Zc3J4vAh58 +VyP/fxJLk0FKi2Yujy6xwkwZd5CkJC19W5UlI/Zt9Tm7l3QpMtKD8KivpRiieqtE +HzYds666TxaNrDDhUeicKrtfyj3jxXoc3lmJtA6Awyj6d/TA5FEQcGO/Bzhbt7Bt +pNx+h7VU0Cj9CeSrFMW54xvYfGCCECupBIrxvjg7TlXTOt17bl1Weae17qw+NxXS +mWownfuNWIhLGw49QWH9lbvNsgz/sIxwN6QKIQIDAQABAoIBAQDCXq9V7ZGjxWMN +OkFaLVkqJg3V91puztoMt+xNV8t+JTcOnOzrIXZuOFbl9PwLHPPP0SSRkm9LOvKl +dU26zv0OWureeKSymia7U2mcqyC3tX+bzc7WinbeSYZBnc0e7AjD1EgpBcaU1TLL +agIxY3A2oD9CKmrVPhZzTIZf/XztqTYjhvs5I2kBeT0imdYGpXkdndRyGX4I5/JQ +fnp3Czj+AW3zX7RvVnXOh4OtIAcfoG9xoNyD5LOSlJkkX0MwTS8pEBeZA+A4nb+C +ivjnOSgXWD+liisI+LpBgBbwYZ/E49x5ghZYrJt8QXSk7Bl/+UOyv6XZAm2mev6j +RLAZtoABAoGBAP2P+1PoKOwsk+d/AmHqyTCUQm0UG18LOLB/5PyWfXs/6caDmdIe +DZWeZWng1jUQLEadmoEw/CBY5+tPfHlzwzMNhT7KwUfIDQCIBoS7dzHYnwrJ3VZh +qYA05cuGHAAHqwb6UWz3y6Pa4AEVSHX6CM83CAi9jdWZ1rdZybWG+qYBAoGBAMbV +FsR/Ft+tK5ALgXGoG83TlmxzZYuZ1SnNje1OSdCQdMFCJB10gwoaRrw1ICzi40Xk +ydJwV1upGz1om9ReDAD1zQM9artmQx6+TVLiVPALuARdZE70+NrA6w3ZvxUgJjdN +ngvXUr+8SdvaYUAwFu7BulfJlwXjUS711hHW/KQhAoGBALY41QuV2mLwHlLNie7I +hlGtGpe9TXZeYB0nrG6B0CfU5LJPPSotguG1dXhDpm138/nDpZeWlnrAqdsHwpKd +yPhVjR51I7XsZLuvBdA50Q03egSM0c4UXXXPjh1XgaPb3uMi3YWMBwL4ducQXoS6 +bb5M9C8j2lxZNF+L3VPhbxwBAoGBAIEWDvX7XKpTDxkxnxRfA84ZNGusb5y2fsHp +Bd+vGBUj8+kUO8Yzwm9op8vA4ebCVrMl2jGZZd3IaDryE1lIxZpJ+pPD5+tKdQEc +o67P6jz+HrYWu+zW9klvPit71qasfKMi7Rza6oo4f+sQWFsH3ZucgpJD+pyD/Ez0 +pcpnPRaBAoGBANT/xgHBfIWt4U2rtmRLIIiZxKr+3mGnQdpA1J2BCh+/6AvrEx// +E/WObVJXDnBdViu0L9abE9iaTToBVri4cmlDlZagLuKVR+TFTCN/DSlVZTDkqkLI +8chzqtkH6b2b2R73hyRysWjsomys34ma3mEEPTX/aXeAF2MSZ/EWT9yL +-----END RSA PRIVATE KEY-----` + path = filepath.Join(tempDir, "key.pem") + if err = os.WriteFile(path, []byte(data), 0644); err != nil { + return "", nil, err + } + + rmFunc := func() { os.RemoveAll(tempDir) } + return tempDir, rmFunc, nil +} diff --git a/ag_201_coredns/plugin/test/file_test.go b/ag_201_coredns/plugin/test/file_test.go new file mode 100644 index 0000000..b225ace --- /dev/null +++ b/ag_201_coredns/plugin/test/file_test.go @@ -0,0 +1,11 @@ +package test + +import "testing" + +func TestTempFile(t *testing.T) { + _, f, e := TempFile(".", "test") + if e != nil { + t.Fatalf("Failed to create temp file: %s", e) + } + defer f() +} diff --git a/ag_201_coredns/plugin/test/helpers.go b/ag_201_coredns/plugin/test/helpers.go new file mode 100644 index 0000000..8145b60 --- /dev/null +++ b/ag_201_coredns/plugin/test/helpers.go @@ -0,0 +1,329 @@ +package test + +import ( + "context" + "fmt" + "sort" + + "github.com/miekg/dns" +) + +type sect int + +const ( + // Answer is the answer section in an Msg. + Answer sect = iota + // Ns is the authoritative section in an Msg. + Ns + // Extra is the additional section in an Msg. + Extra +) + +// RRSet represents a list of RRs. +type RRSet []dns.RR + +func (p RRSet) Len() int { return len(p) } +func (p RRSet) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p RRSet) Less(i, j int) bool { return p[i].String() < p[j].String() } + +// Case represents a test case that encapsulates various data from a query and response. +// Note that is the TTL of a record is 303 we don't compare it with the TTL. +type Case struct { + Qname string + Qtype uint16 + Rcode int + Do bool + AuthenticatedData bool + Answer []dns.RR + Ns []dns.RR + Extra []dns.RR + Error error +} + +// Msg returns a *dns.Msg embedded in c. +func (c Case) Msg() *dns.Msg { + m := new(dns.Msg) + m.SetQuestion(dns.Fqdn(c.Qname), c.Qtype) + if c.Do { + o := new(dns.OPT) + o.Hdr.Name = "." + o.Hdr.Rrtype = dns.TypeOPT + o.SetDo() + o.SetUDPSize(4096) + m.Extra = []dns.RR{o} + } + return m +} + +// A returns an A record from rr. It panics on errors. +func A(rr string) *dns.A { r, _ := dns.NewRR(rr); return r.(*dns.A) } + +// AAAA returns an AAAA record from rr. It panics on errors. +func AAAA(rr string) *dns.AAAA { r, _ := dns.NewRR(rr); return r.(*dns.AAAA) } + +// CNAME returns a CNAME record from rr. It panics on errors. +func CNAME(rr string) *dns.CNAME { r, _ := dns.NewRR(rr); return r.(*dns.CNAME) } + +// DNAME returns a DNAME record from rr. It panics on errors. +func DNAME(rr string) *dns.DNAME { r, _ := dns.NewRR(rr); return r.(*dns.DNAME) } + +// SRV returns a SRV record from rr. It panics on errors. +func SRV(rr string) *dns.SRV { r, _ := dns.NewRR(rr); return r.(*dns.SRV) } + +// SOA returns a SOA record from rr. It panics on errors. +func SOA(rr string) *dns.SOA { r, _ := dns.NewRR(rr); return r.(*dns.SOA) } + +// NS returns an NS record from rr. It panics on errors. +func NS(rr string) *dns.NS { r, _ := dns.NewRR(rr); return r.(*dns.NS) } + +// PTR returns a PTR record from rr. It panics on errors. +func PTR(rr string) *dns.PTR { r, _ := dns.NewRR(rr); return r.(*dns.PTR) } + +// TXT returns a TXT record from rr. It panics on errors. +func TXT(rr string) *dns.TXT { r, _ := dns.NewRR(rr); return r.(*dns.TXT) } + +// CAA returns a CAA record from rr. It panics on errors. +func CAA(rr string) *dns.CAA { r, _ := dns.NewRR(rr); return r.(*dns.CAA) } + +// HINFO returns a HINFO record from rr. It panics on errors. +func HINFO(rr string) *dns.HINFO { r, _ := dns.NewRR(rr); return r.(*dns.HINFO) } + +// MX returns an MX record from rr. It panics on errors. +func MX(rr string) *dns.MX { r, _ := dns.NewRR(rr); return r.(*dns.MX) } + +// RRSIG returns an RRSIG record from rr. It panics on errors. +func RRSIG(rr string) *dns.RRSIG { r, _ := dns.NewRR(rr); return r.(*dns.RRSIG) } + +// NSEC returns an NSEC record from rr. It panics on errors. +func NSEC(rr string) *dns.NSEC { r, _ := dns.NewRR(rr); return r.(*dns.NSEC) } + +// DNSKEY returns a DNSKEY record from rr. It panics on errors. +func DNSKEY(rr string) *dns.DNSKEY { r, _ := dns.NewRR(rr); return r.(*dns.DNSKEY) } + +// DS returns a DS record from rr. It panics on errors. +func DS(rr string) *dns.DS { r, _ := dns.NewRR(rr); return r.(*dns.DS) } + +// NAPTR returns a NAPTR record from rr. It panics on errors. +func NAPTR(rr string) *dns.NAPTR { r, _ := dns.NewRR(rr); return r.(*dns.NAPTR) } + +// OPT returns an OPT record with UDP buffer size set to bufsize and the DO bit set to do. +func OPT(bufsize int, do bool) *dns.OPT { + o := new(dns.OPT) + o.Hdr.Name = "." + o.Hdr.Rrtype = dns.TypeOPT + o.SetVersion(0) + o.SetUDPSize(uint16(bufsize)) + if do { + o.SetDo() + } + return o +} + +// Header tests if the header in resp matches the header as defined in tc. +func Header(tc Case, resp *dns.Msg) error { + if resp.Rcode != tc.Rcode { + return fmt.Errorf("rcode is %q, expected %q", dns.RcodeToString[resp.Rcode], dns.RcodeToString[tc.Rcode]) + } + + if len(resp.Answer) != len(tc.Answer) { + return fmt.Errorf("answer for %q contained %d results, %d expected", tc.Qname, len(resp.Answer), len(tc.Answer)) + } + if len(resp.Ns) != len(tc.Ns) { + return fmt.Errorf("authority for %q contained %d results, %d expected", tc.Qname, len(resp.Ns), len(tc.Ns)) + } + if len(resp.Extra) != len(tc.Extra) { + return fmt.Errorf("additional for %q contained %d results, %d expected", tc.Qname, len(resp.Extra), len(tc.Extra)) + } + return nil +} + +// Section tests if the section in tc matches rr. +func Section(tc Case, sec sect, rr []dns.RR) error { + section := []dns.RR{} + switch sec { + case 0: + section = tc.Answer + case 1: + section = tc.Ns + case 2: + section = tc.Extra + } + + for i, a := range rr { + if a.Header().Name != section[i].Header().Name { + return fmt.Errorf("RR %d should have a Header Name of %q, but has %q", i, section[i].Header().Name, a.Header().Name) + } + // 303 signals: don't care what the ttl is. + if section[i].Header().Ttl != 303 && a.Header().Ttl != section[i].Header().Ttl { + if _, ok := section[i].(*dns.OPT); !ok { + // we check edns0 bufize on this one + return fmt.Errorf("RR %d should have a Header TTL of %d, but has %d", i, section[i].Header().Ttl, a.Header().Ttl) + } + } + if a.Header().Rrtype != section[i].Header().Rrtype { + return fmt.Errorf("RR %d should have a header rr type of %d, but has %d", i, section[i].Header().Rrtype, a.Header().Rrtype) + } + + switch x := a.(type) { + case *dns.SRV: + if x.Priority != section[i].(*dns.SRV).Priority { + return fmt.Errorf("RR %d should have a Priority of %d, but has %d", i, section[i].(*dns.SRV).Priority, x.Priority) + } + if x.Weight != section[i].(*dns.SRV).Weight { + return fmt.Errorf("RR %d should have a Weight of %d, but has %d", i, section[i].(*dns.SRV).Weight, x.Weight) + } + if x.Port != section[i].(*dns.SRV).Port { + return fmt.Errorf("RR %d should have a Port of %d, but has %d", i, section[i].(*dns.SRV).Port, x.Port) + } + if x.Target != section[i].(*dns.SRV).Target { + return fmt.Errorf("RR %d should have a Target of %q, but has %q", i, section[i].(*dns.SRV).Target, x.Target) + } + case *dns.RRSIG: + if x.TypeCovered != section[i].(*dns.RRSIG).TypeCovered { + return fmt.Errorf("RR %d should have a TypeCovered of %d, but has %d", i, section[i].(*dns.RRSIG).TypeCovered, x.TypeCovered) + } + if x.Labels != section[i].(*dns.RRSIG).Labels { + return fmt.Errorf("RR %d should have a Labels of %d, but has %d", i, section[i].(*dns.RRSIG).Labels, x.Labels) + } + if x.SignerName != section[i].(*dns.RRSIG).SignerName { + return fmt.Errorf("RR %d should have a SignerName of %s, but has %s", i, section[i].(*dns.RRSIG).SignerName, x.SignerName) + } + case *dns.NSEC: + if x.NextDomain != section[i].(*dns.NSEC).NextDomain { + return fmt.Errorf("RR %d should have a NextDomain of %s, but has %s", i, section[i].(*dns.NSEC).NextDomain, x.NextDomain) + } + // TypeBitMap + case *dns.A: + if x.A.String() != section[i].(*dns.A).A.String() { + return fmt.Errorf("RR %d should have a Address of %q, but has %q", i, section[i].(*dns.A).A.String(), x.A.String()) + } + case *dns.AAAA: + if x.AAAA.String() != section[i].(*dns.AAAA).AAAA.String() { + return fmt.Errorf("RR %d should have a Address of %q, but has %q", i, section[i].(*dns.AAAA).AAAA.String(), x.AAAA.String()) + } + case *dns.TXT: + for j, txt := range x.Txt { + if txt != section[i].(*dns.TXT).Txt[j] { + return fmt.Errorf("RR %d should have a Txt of %q, but has %q", i, section[i].(*dns.TXT).Txt[j], txt) + } + } + case *dns.HINFO: + if x.Cpu != section[i].(*dns.HINFO).Cpu { + return fmt.Errorf("RR %d should have a Cpu of %s, but has %s", i, section[i].(*dns.HINFO).Cpu, x.Cpu) + } + if x.Os != section[i].(*dns.HINFO).Os { + return fmt.Errorf("RR %d should have a Os of %s, but has %s", i, section[i].(*dns.HINFO).Os, x.Os) + } + case *dns.SOA: + tt := section[i].(*dns.SOA) + if x.Ns != tt.Ns { + return fmt.Errorf("SOA nameserver should be %q, but is %q", tt.Ns, x.Ns) + } + case *dns.PTR: + tt := section[i].(*dns.PTR) + if x.Ptr != tt.Ptr { + return fmt.Errorf("PTR ptr should be %q, but is %q", tt.Ptr, x.Ptr) + } + case *dns.CNAME: + tt := section[i].(*dns.CNAME) + if x.Target != tt.Target { + return fmt.Errorf("CNAME target should be %q, but is %q", tt.Target, x.Target) + } + case *dns.MX: + tt := section[i].(*dns.MX) + if x.Mx != tt.Mx { + return fmt.Errorf("MX Mx should be %q, but is %q", tt.Mx, x.Mx) + } + if x.Preference != tt.Preference { + return fmt.Errorf("MX Preference should be %q, but is %q", tt.Preference, x.Preference) + } + case *dns.NS: + tt := section[i].(*dns.NS) + if x.Ns != tt.Ns { + return fmt.Errorf("NS nameserver should be %q, but is %q", tt.Ns, x.Ns) + } + case *dns.OPT: + tt := section[i].(*dns.OPT) + if x.UDPSize() != tt.UDPSize() { + return fmt.Errorf("OPT UDPSize should be %d, but is %d", tt.UDPSize(), x.UDPSize()) + } + if x.Do() != tt.Do() { + return fmt.Errorf("OPT DO should be %t, but is %t", tt.Do(), x.Do()) + } + } + } + return nil +} + +// CNAMEOrder makes sure that CNAMES do not appear after their target records. +func CNAMEOrder(res *dns.Msg) error { + for i, c := range res.Answer { + if c.Header().Rrtype != dns.TypeCNAME { + continue + } + for _, a := range res.Answer[:i] { + if a.Header().Name != c.(*dns.CNAME).Target { + continue + } + return fmt.Errorf("CNAME found after target record") + } + } + return nil +} + +// SortAndCheck sorts resp and the checks the header and three sections against the testcase in tc. +func SortAndCheck(resp *dns.Msg, tc Case) error { + sort.Sort(RRSet(resp.Answer)) + sort.Sort(RRSet(resp.Ns)) + sort.Sort(RRSet(resp.Extra)) + + if err := Header(tc, resp); err != nil { + return err + } + if err := Section(tc, Answer, resp.Answer); err != nil { + return err + } + if err := Section(tc, Ns, resp.Ns); err != nil { + return err + } + return Section(tc, Extra, resp.Extra) +} + +// ErrorHandler returns a Handler that returns ServerFailure error when called. +func ErrorHandler() Handler { + return HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + m := new(dns.Msg) + m.SetRcode(r, dns.RcodeServerFailure) + w.WriteMsg(m) + return dns.RcodeServerFailure, nil + }) +} + +// NextHandler returns a Handler that returns rcode and err. +func NextHandler(rcode int, err error) Handler { + return HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + return rcode, err + }) +} + +// Copied here to prevent an import cycle, so that we can define to above handlers. + +type ( + // HandlerFunc is a convenience type like dns.HandlerFunc, except + // ServeDNS returns an rcode and an error. + HandlerFunc func(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) + + // Handler interface defines a plugin. + Handler interface { + ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) + Name() string + } +) + +// ServeDNS implements the Handler interface. +func (f HandlerFunc) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + return f(ctx, w, r) +} + +// Name implements the Handler interface. +func (f HandlerFunc) Name() string { return "handlerfunc" } diff --git a/ag_201_coredns/plugin/test/responsewriter.go b/ag_201_coredns/plugin/test/responsewriter.go new file mode 100644 index 0000000..3216700 --- /dev/null +++ b/ag_201_coredns/plugin/test/responsewriter.go @@ -0,0 +1,80 @@ +package test + +import ( + "net" + + "github.com/miekg/dns" +) + +// ResponseWriter is useful for writing tests. It uses some fixed values for the client. The +// remote will always be 10.240.0.1 and port 40212. The local address is always 127.0.0.1 and +// port 53. +type ResponseWriter struct { + TCP bool // if TCP is true we return an TCP connection instead of an UDP one. + RemoteIP string + Zone string +} + +// LocalAddr returns the local address, 127.0.0.1:53 (UDP, TCP if t.TCP is true). +func (t *ResponseWriter) LocalAddr() net.Addr { + ip := net.ParseIP("127.0.0.1") + port := 53 + if t.TCP { + return &net.TCPAddr{IP: ip, Port: port, Zone: ""} + } + return &net.UDPAddr{IP: ip, Port: port, Zone: ""} +} + +// RemoteAddr returns the remote address, defaults to 10.240.0.1:40212 (UDP, TCP is t.TCP is true). +func (t *ResponseWriter) RemoteAddr() net.Addr { + remoteIP := "10.240.0.1" + if t.RemoteIP != "" { + remoteIP = t.RemoteIP + } + ip := net.ParseIP(remoteIP) + port := 40212 + if t.TCP { + return &net.TCPAddr{IP: ip, Port: port, Zone: t.Zone} + } + return &net.UDPAddr{IP: ip, Port: port, Zone: t.Zone} +} + +// WriteMsg implements dns.ResponseWriter interface. +func (t *ResponseWriter) WriteMsg(m *dns.Msg) error { return nil } + +// Write implements dns.ResponseWriter interface. +func (t *ResponseWriter) Write(buf []byte) (int, error) { return len(buf), nil } + +// Close implements dns.ResponseWriter interface. +func (t *ResponseWriter) Close() error { return nil } + +// TsigStatus implements dns.ResponseWriter interface. +func (t *ResponseWriter) TsigStatus() error { return nil } + +// TsigTimersOnly implements dns.ResponseWriter interface. +func (t *ResponseWriter) TsigTimersOnly(bool) {} + +// Hijack implements dns.ResponseWriter interface. +func (t *ResponseWriter) Hijack() {} + +// ResponseWriter6 returns fixed client and remote address in IPv6. The remote +// address is always fe80::42:ff:feca:4c65 and port 40212. The local address is always ::1 and port 53. +type ResponseWriter6 struct { + ResponseWriter +} + +// LocalAddr returns the local address, always ::1, port 53 (UDP, TCP is t.TCP is true). +func (t *ResponseWriter6) LocalAddr() net.Addr { + if t.TCP { + return &net.TCPAddr{IP: net.ParseIP("::1"), Port: 53, Zone: ""} + } + return &net.UDPAddr{IP: net.ParseIP("::1"), Port: 53, Zone: ""} +} + +// RemoteAddr returns the remote address, always fe80::42:ff:feca:4c65 port 40212 (UDP, TCP is t.TCP is true). +func (t *ResponseWriter6) RemoteAddr() net.Addr { + if t.TCP { + return &net.TCPAddr{IP: net.ParseIP("fe80::42:ff:feca:4c65"), Port: 40212, Zone: ""} + } + return &net.UDPAddr{IP: net.ParseIP("fe80::42:ff:feca:4c65"), Port: 40212, Zone: ""} +} diff --git a/ag_201_coredns/plugin/test/scrape.go b/ag_201_coredns/plugin/test/scrape.go new file mode 100644 index 0000000..7847e39 --- /dev/null +++ b/ag_201_coredns/plugin/test/scrape.go @@ -0,0 +1,263 @@ +// Adapted by Miek Gieben for CoreDNS testing. +// +// License from prom2json +// Copyright 2014 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package test will scrape a target and you can inspect the variables. +// Basic usage: +// +// result := Scrape("http://localhost:9153/metrics") +// v := MetricValue("coredns_cache_capacity", result) +// +package test + +import ( + "fmt" + "io" + "mime" + "net/http" + "strconv" + + "github.com/matttproud/golang_protobuf_extensions/pbutil" + dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" +) + +type ( + // MetricFamily holds a prometheus metric. + MetricFamily struct { + Name string `json:"name"` + Help string `json:"help"` + Type string `json:"type"` + Metrics []interface{} `json:"metrics,omitempty"` // Either metric or summary. + } + + // metric is for all "single value" metrics. + metric struct { + Labels map[string]string `json:"labels,omitempty"` + Value string `json:"value"` + } + + summary struct { + Labels map[string]string `json:"labels,omitempty"` + Quantiles map[string]string `json:"quantiles,omitempty"` + Count string `json:"count"` + Sum string `json:"sum"` + } + + histogram struct { + Labels map[string]string `json:"labels,omitempty"` + Buckets map[string]string `json:"buckets,omitempty"` + Count string `json:"count"` + Sum string `json:"sum"` + } +) + +// Scrape returns the all the vars a []*metricFamily. +func Scrape(url string) []*MetricFamily { + mfChan := make(chan *dto.MetricFamily, 1024) + + go fetchMetricFamilies(url, mfChan) + + result := []*MetricFamily{} + for mf := range mfChan { + result = append(result, newMetricFamily(mf)) + } + return result +} + +// ScrapeMetricAsInt provides a sum of all metrics collected for the name and label provided. +// if the metric is not a numeric value, it will be counted a 0. +func ScrapeMetricAsInt(addr string, name string, label string, nometricvalue int) int { + valueToInt := func(m metric) int { + v := m.Value + r, err := strconv.Atoi(v) + if err != nil { + return 0 + } + return r + } + + met := Scrape(fmt.Sprintf("http://%s/metrics", addr)) + found := false + tot := 0 + for _, mf := range met { + if mf.Name == name { + // Sum all metrics available + for _, m := range mf.Metrics { + if label == "" { + tot += valueToInt(m.(metric)) + found = true + continue + } + for _, v := range m.(metric).Labels { + if v == label { + tot += valueToInt(m.(metric)) + found = true + } + } + } + } + } + + if !found { + return nometricvalue + } + return tot +} + +// MetricValue returns the value associated with name as a string as well as the labels. +// It only returns the first metrics of the slice. +func MetricValue(name string, mfs []*MetricFamily) (string, map[string]string) { + for _, mf := range mfs { + if mf.Name == name { + // Only works with Gauge and Counter... + return mf.Metrics[0].(metric).Value, mf.Metrics[0].(metric).Labels + } + } + return "", nil +} + +// MetricValueLabel returns the value for name *and* label *value*. +func MetricValueLabel(name, label string, mfs []*MetricFamily) (string, map[string]string) { + // bit hacky is this really handy...? + for _, mf := range mfs { + if mf.Name == name { + for _, m := range mf.Metrics { + for _, v := range m.(metric).Labels { + if v == label { + return m.(metric).Value, m.(metric).Labels + } + } + } + } + } + return "", nil +} + +func newMetricFamily(dtoMF *dto.MetricFamily) *MetricFamily { + mf := &MetricFamily{ + Name: dtoMF.GetName(), + Help: dtoMF.GetHelp(), + Type: dtoMF.GetType().String(), + Metrics: make([]interface{}, len(dtoMF.Metric)), + } + for i, m := range dtoMF.Metric { + if dtoMF.GetType() == dto.MetricType_SUMMARY { + mf.Metrics[i] = summary{ + Labels: makeLabels(m), + Quantiles: makeQuantiles(m), + Count: fmt.Sprint(m.GetSummary().GetSampleCount()), + Sum: fmt.Sprint(m.GetSummary().GetSampleSum()), + } + } else if dtoMF.GetType() == dto.MetricType_HISTOGRAM { + mf.Metrics[i] = histogram{ + Labels: makeLabels(m), + Buckets: makeBuckets(m), + Count: fmt.Sprint(m.GetHistogram().GetSampleCount()), + Sum: fmt.Sprint(m.GetSummary().GetSampleSum()), + } + } else { + mf.Metrics[i] = metric{ + Labels: makeLabels(m), + Value: fmt.Sprint(value(m)), + } + } + } + return mf +} + +func value(m *dto.Metric) float64 { + if m.Gauge != nil { + return m.GetGauge().GetValue() + } + if m.Counter != nil { + return m.GetCounter().GetValue() + } + if m.Untyped != nil { + return m.GetUntyped().GetValue() + } + return 0. +} + +func makeLabels(m *dto.Metric) map[string]string { + result := map[string]string{} + for _, lp := range m.Label { + result[lp.GetName()] = lp.GetValue() + } + return result +} + +func makeQuantiles(m *dto.Metric) map[string]string { + result := map[string]string{} + for _, q := range m.GetSummary().Quantile { + result[fmt.Sprint(q.GetQuantile())] = fmt.Sprint(q.GetValue()) + } + return result +} + +func makeBuckets(m *dto.Metric) map[string]string { + result := map[string]string{} + for _, b := range m.GetHistogram().Bucket { + result[fmt.Sprint(b.GetUpperBound())] = fmt.Sprint(b.GetCumulativeCount()) + } + return result +} + +func fetchMetricFamilies(url string, ch chan<- *dto.MetricFamily) { + defer close(ch) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return + } + req.Header.Add("Accept", acceptHeader) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return + } + + mediatype, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err == nil && mediatype == "application/vnd.google.protobuf" && + params["encoding"] == "delimited" && + params["proto"] == "io.prometheus.client.MetricFamily" { + for { + mf := &dto.MetricFamily{} + if _, err = pbutil.ReadDelimited(resp.Body, mf); err != nil { + if err == io.EOF { + break + } + return + } + ch <- mf + } + } else { + // We could do further content-type checks here, but the + // fallback for now will anyway be the text format + // version 0.0.4, so just go for it and see if it works. + var parser expfmt.TextParser + metricFamilies, err := parser.TextToMetricFamilies(resp.Body) + if err != nil { + return + } + for _, mf := range metricFamilies { + ch <- mf + } + } +} + +const acceptHeader = `application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.7,text/plain;version=0.0.4;q=0.3` diff --git a/ag_201_coredns/plugin/tls/README.md b/ag_201_coredns/plugin/tls/README.md new file mode 100644 index 0000000..9d945b8 --- /dev/null +++ b/ag_201_coredns/plugin/tls/README.md @@ -0,0 +1,73 @@ +# tls + +## Name + +*tls* - allows you to configure the server certificates for the TLS, gRPC, DoH servers. + +## Description + +CoreDNS supports queries that are encrypted using TLS (DNS over Transport Layer Security, RFC 7858) +or are using gRPC (https://grpc.io/, not an IETF standard). Normally DNS traffic isn't encrypted at +all (DNSSEC only signs resource records). + +The *tls* "plugin" allows you to configure the cryptographic keys that are needed for both +DNS-over-TLS and DNS-over-gRPC. If the *tls* plugin is omitted, then no encryption takes place. + +The gRPC protobuffer is defined in `pb/dns.proto`. It defines the proto as a simple wrapper for the +wire data of a DNS message. + +## Syntax + +~~~ txt +tls CERT KEY [CA] +~~~ + +Parameter CA is optional. If not set, system CAs can be used to verify the client certificate + +~~~ txt +tls CERT KEY [CA] { + client_auth nocert|request|require|verify_if_given|require_and_verify +} +~~~ + +If client\_auth option is specified, it controls the client authentication policy. +The option value corresponds to the [ClientAuthType values of the Go tls package](https://golang.org/pkg/crypto/tls/#ClientAuthType): NoClientCert, RequestClientCert, RequireAnyClientCert, VerifyClientCertIfGiven, and RequireAndVerifyClientCert, respectively. +The default is "nocert". Note that it makes no sense to specify parameter CA unless this option is +set to verify\_if\_given or require\_and\_verify. + +## Examples + +Start a DNS-over-TLS server that picks up incoming DNS-over-TLS queries on port 5553 and uses the +nameservers defined in `/etc/resolv.conf` to resolve the query. This proxy path uses plain old DNS. + +~~~ +tls://.:5553 { + tls cert.pem key.pem ca.pem + forward . /etc/resolv.conf +} +~~~ + +Start a DNS-over-gRPC server that is similar to the previous example, but using DNS-over-gRPC for +incoming queries. + +~~~ +grpc://. { + tls cert.pem key.pem ca.pem + forward . /etc/resolv.conf +} +~~~ + +Start a DoH server on port 443 that is similar to the previous example, but using DoH for incoming queries. +~~~ +https://. { + tls cert.pem key.pem ca.pem + forward . /etc/resolv.conf +} +~~~ + +Only Knot DNS' `kdig` supports DNS-over-TLS queries, no command line client supports gRPC making +debugging these transports harder than it should be. + +## See Also + +RFC 7858 and https://grpc.io. diff --git a/ag_201_coredns/plugin/tls/log_test.go b/ag_201_coredns/plugin/tls/log_test.go new file mode 100644 index 0000000..017affd --- /dev/null +++ b/ag_201_coredns/plugin/tls/log_test.go @@ -0,0 +1,5 @@ +package tls + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/tls/test_ca.pem b/ag_201_coredns/plugin/tls/test_ca.pem new file mode 100644 index 0000000..cfcd5cc --- /dev/null +++ b/ag_201_coredns/plugin/tls/test_ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDPzCCAiegAwIBAgIJAPjCWTu1wGapMA0GCSqGSIb3DQEBCwUAMDUxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMREwDwYDVQQKDAhJbmZvYmxveDAg +Fw0xOTA1MTEwMDI3NDRaGA8yMTE5MDQxNzAwMjc0NFowNTELMAkGA1UEBhMCVVMx +EzARBgNVBAgMCkNhbGlmb3JuaWExETAPBgNVBAoMCEluZm9ibG94MIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArAYiw1UjlYj+nITRUlj5hA7j8U2qWcyN +YcDfqQnt173Z8yR7NJokqt3Bd3PlrBZS2XtYSNohxRr4qeJu/g7UBre/fSEU/ZOM +Gl7NjBGKQEymJ0d8rBg52iiGNwU+ERI9pcQRA6DCEjVbOmjDiUd5yzuVotG/Sxep +GUJ2puJ0p0gWCMEL9sdqY6HHd/hdj6B6+u2xD9NUCkX9pLC7CPFJHnP0vLO4WIWL +z5C7yzpeLO9r7Nfnu+2HcRLmuFZVPNxkMq7UymqR1w5ZYJQ5p9E7pyxDVXxHnTqQ +yLaAS2/9umrOwVnD1NaN3OdAhDedXbH0cF08GcIQD9rnlkLMW4CKtwIDAQABo1Aw +TjAdBgNVHQ4EFgQUHcxJPBmHF0nSv+FJJI/kwrSThf8wHwYDVR0jBBgwFoAUHcxJ +PBmHF0nSv+FJJI/kwrSThf8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEAByItgyhlXDv2wnnMVXHHlUCbsKCOtBJZ8EumvKjeOx5G4gqJpQIQPNeBv1Od +QT7d15HfT7RQqHSL0uAoGuNuyGjZGWWbLMkVt8T0tXY2v9Dd8eWC/lFaaA0vkqTG +GpADSmH+SoFAdPPcYN/sXmEHvZcIQ0wUxuF48ZMwOh7ZOcrZggxlA9+BKHU4fO03 +o7krzpQZQmEDXNN8bt1R0DIhVADw/G2oJAzK0LGhh4eu6hj6k/cAWS6ujRBGqN0Z +fURCrMEyjzbNybhkU1KqSr7eSJOWkl4UJ5Ns/dt9/yw2BBrKH3Mijch7UA8mlbEE +29M28u2W7GMXLSSwmtCqDBRNhg== +-----END CERTIFICATE----- diff --git a/ag_201_coredns/plugin/tls/test_cert.pem b/ag_201_coredns/plugin/tls/test_cert.pem new file mode 100644 index 0000000..8cc47eb --- /dev/null +++ b/ag_201_coredns/plugin/tls/test_cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDPzCCAiegAwIBAgIJAPezzzshGRiTMA0GCSqGSIb3DQEBCwUAMDUxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMREwDwYDVQQKDAhJbmZvYmxveDAg +Fw0xOTA1MTEwMDI2MjNaGA8yMTE5MDQxNzAwMjYyM1owNTELMAkGA1UEBhMCVVMx +EzARBgNVBAgMCkNhbGlmb3JuaWExETAPBgNVBAoMCEluZm9ibG94MIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArAYiw1UjlYj+nITRUlj5hA7j8U2qWcyN +YcDfqQnt173Z8yR7NJokqt3Bd3PlrBZS2XtYSNohxRr4qeJu/g7UBre/fSEU/ZOM +Gl7NjBGKQEymJ0d8rBg52iiGNwU+ERI9pcQRA6DCEjVbOmjDiUd5yzuVotG/Sxep +GUJ2puJ0p0gWCMEL9sdqY6HHd/hdj6B6+u2xD9NUCkX9pLC7CPFJHnP0vLO4WIWL +z5C7yzpeLO9r7Nfnu+2HcRLmuFZVPNxkMq7UymqR1w5ZYJQ5p9E7pyxDVXxHnTqQ +yLaAS2/9umrOwVnD1NaN3OdAhDedXbH0cF08GcIQD9rnlkLMW4CKtwIDAQABo1Aw +TjAdBgNVHQ4EFgQUHcxJPBmHF0nSv+FJJI/kwrSThf8wHwYDVR0jBBgwFoAUHcxJ +PBmHF0nSv+FJJI/kwrSThf8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEAQyN9nLImdtufuSjXcrCJ3alt/vffHJIzlPgDsNw8+tjI7aRX7CzuurOOEQUC +fJ9A6O+dat5k5yqVb9hDcD42HXtOjRQDYpQ6dOGirLFThIFSMC/7RiqHk0YtxojM +ZNBbgXo4o1d+P9b25oc/+pRDzlOvqNL7IzW/LDHnJ4j6tBNguujCB5QFUF5dOa1z +UR5rupMvv2KpEgRcfW/d3kwcAxH9nI0SHKJenhtweyajUgInK88TC+aT4909c2XA +EADYyWxj1DMz3/sMpvGegHsfTPegNoDgz2yEKdu53dr4BUpF6E+eoCX9Hv78SWH3 +/rAlkbffzCL5d+I8y0jzEpLEqA== +-----END CERTIFICATE----- diff --git a/ag_201_coredns/plugin/tls/test_key.pem b/ag_201_coredns/plugin/tls/test_key.pem new file mode 100644 index 0000000..2ca2b21 --- /dev/null +++ b/ag_201_coredns/plugin/tls/test_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsBiLDVSOViP6c +hNFSWPmEDuPxTapZzI1hwN+pCe3XvdnzJHs0miSq3cF3c+WsFlLZe1hI2iHFGvip +4m7+DtQGt799IRT9k4waXs2MEYpATKYnR3ysGDnaKIY3BT4REj2lxBEDoMISNVs6 +aMOJR3nLO5Wi0b9LF6kZQnam4nSnSBYIwQv2x2pjocd3+F2PoHr67bEP01QKRf2k +sLsI8Ukec/S8s7hYhYvPkLvLOl4s72vs1+e77YdxEua4VlU83GQyrtTKapHXDllg +lDmn0TunLENVfEedOpDItoBLb/26as7BWcPU1o3c50CEN51dsfRwXTwZwhAP2ueW +QsxbgIq3AgMBAAECggEAF3FCnYHltoQTxnqnF+S+JAvvbjvaQiCJB9BD6oJK4kKi +B+tpytJSuuI7ci7eFqR4J+ESN+NaBMVXK7eKzp5wsHWr575xYNkRl6phsnvVbkvD +vMiWKdGnWJ57I9ZYDfWBZyyf8PGgYODajMwoEXYnF9YH30dcHTydM68GAloL8Zu9 +CtGCmlu4TER0BvG+rK2OD5lt8ORK56eMwzTTqMy0hCkP5VEq8j9RmekEzrgtWKm8 +OI3i8VnpOA0RCVhJ0q5a5jt/xbKRjFNsUNmy9HBRYg7Iw3SCEHmDtz1R9A9rvaJC +WXqwKbGZPY8W69h8BhKcJ5RrKt2PZyJxw+LB610XSQKBgQDR/LIGXdJR/90epiGC +p68W9Vc3eWxJlAtLDQCSULphLi6j7D+jesmhD3z2woBPjxkd4TaZa2t94Q1MzSeC +ON/Aux1huto9ddxvijUQJN3Ep4zPkHdNzHfRwIZsgGH8u77VY/5I4V7IgxKjWlJ6 +Ii8ez8xpWj1rnQ0azSaYIcVl7QKBgQDRt+J+iRjKxHWuXoBFfv8oMfl+iYaMdJxu +PELWb3RLsZ92hobSAmNR/gC3T7p8NFJlQVCoxZr8zt/Rvqh4aK3aSOuKeUvYAjs1 +/YbPcdSn6uTTIOi6CcHaJ8ZUXNvY5FuoT0+Q9Eb8fw5NGzxsgsfhScELLgbFKb5E +Tkw43ZqeswKBgQCxXBgZnIEaVVw0mOlQ68TNRWfnKR23f92SBGdpLdpeXp1yQwb1 +U66d5PENkvbBPAJg5GozZzGhXsbXCajHKraCmQiWFTZkFvqbE0cCXcEaatJaNpEu +GvdRKKXhWwZoa0MiBZUvhXuDLII/iviCxAC8q5LhoSCjlkENVB22/T83eQKBgQC4 +c3wRALG+fWZns5QsC5ONnc6rXXfqhxGi3vuGMMbfYF05WP6xLQp/7eBhWg1R+o7R +oc24cvxrB+TRTFhOdvsZtvL7es2bMfMz/EUapSp9edpCW3p1Temi30LPplByhf6b +nQ4FFuRsZa+FX8QYSDpWypCwLY4k0R8YYqklhrrcgwKBgFiM/GnRc230nj0GGWf1 ++Ve2M/TQCgS6ufr2F0vU7QkEWfeiN9iunhmhsggqWxOEOU77FhCkQRtztm93hG0K +eKoHNh/1HvHGBWsR0TaMDw3n8t7Yg5NmQb617nBELZbxxpd358muLiHDoix86W9Q +xM6hB159G1gOEJsi8exm5AlZ +-----END PRIVATE KEY----- diff --git a/ag_201_coredns/plugin/tls/tls.go b/ag_201_coredns/plugin/tls/tls.go new file mode 100644 index 0000000..2658159 --- /dev/null +++ b/ag_201_coredns/plugin/tls/tls.go @@ -0,0 +1,71 @@ +package tls + +import ( + ctls "crypto/tls" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/tls" +) + +func init() { plugin.Register("tls", setup) } + +func setup(c *caddy.Controller) error { + err := parseTLS(c) + if err != nil { + return plugin.Error("tls", err) + } + return nil +} + +func parseTLS(c *caddy.Controller) error { + config := dnsserver.GetConfig(c) + + if config.TLSConfig != nil { + return plugin.Error("tls", c.Errf("TLS already configured for this server instance")) + } + + for c.Next() { + args := c.RemainingArgs() + if len(args) < 2 || len(args) > 3 { + return plugin.Error("tls", c.ArgErr()) + } + clientAuth := ctls.NoClientCert + for c.NextBlock() { + switch c.Val() { + case "client_auth": + authTypeArgs := c.RemainingArgs() + if len(authTypeArgs) != 1 { + return c.ArgErr() + } + switch authTypeArgs[0] { + case "nocert": + clientAuth = ctls.NoClientCert + case "request": + clientAuth = ctls.RequestClientCert + case "require": + clientAuth = ctls.RequireAnyClientCert + case "verify_if_given": + clientAuth = ctls.VerifyClientCertIfGiven + case "require_and_verify": + clientAuth = ctls.RequireAndVerifyClientCert + default: + return c.Errf("unknown authentication type '%s'", authTypeArgs[0]) + } + default: + return c.Errf("unknown option '%s'", c.Val()) + } + } + tls, err := tls.NewTLSConfigFromArgs(args...) + if err != nil { + return err + } + tls.ClientAuth = clientAuth + // NewTLSConfigFromArgs only sets RootCAs, so we need to let ClientCAs refer to it. + tls.ClientCAs = tls.RootCAs + + config.TLSConfig = tls + } + return nil +} diff --git a/ag_201_coredns/plugin/tls/tls_test.go b/ag_201_coredns/plugin/tls/tls_test.go new file mode 100644 index 0000000..7deb837 --- /dev/null +++ b/ag_201_coredns/plugin/tls/tls_test.go @@ -0,0 +1,87 @@ +package tls + +import ( + "crypto/tls" + "strings" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" +) + +func TestTLS(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedRoot string // expected root, set to the controller. Empty for negative cases. + expectedErrContent string // substring from the expected error. Empty for positive cases. + }{ + // positive + {"tls test_cert.pem test_key.pem test_ca.pem", false, "", ""}, + {"tls test_cert.pem test_key.pem test_ca.pem {\nclient_auth nocert\n}", false, "", ""}, + {"tls test_cert.pem test_key.pem test_ca.pem {\nclient_auth request\n}", false, "", ""}, + {"tls test_cert.pem test_key.pem test_ca.pem {\nclient_auth require\n}", false, "", ""}, + {"tls test_cert.pem test_key.pem test_ca.pem {\nclient_auth verify_if_given\n}", false, "", ""}, + {"tls test_cert.pem test_key.pem test_ca.pem {\nclient_auth require_and_verify\n}", false, "", ""}, + // negative + {"tls test_cert.pem test_key.pem test_ca.pem {\nunknown\n}", true, "", "unknown option"}, + // client_auth takes exactly one parameter, which must be one of known keywords. + {"tls test_cert.pem test_key.pem test_ca.pem {\nclient_auth\n}", true, "", "Wrong argument"}, + {"tls test_cert.pem test_key.pem test_ca.pem {\nclient_auth none bogus\n}", true, "", "Wrong argument"}, + {"tls test_cert.pem test_key.pem test_ca.pem {\nclient_auth bogus\n}", true, "", "unknown authentication type"}, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + err := setup(c) + //cfg := dnsserver.GetConfig(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, 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) + } + } + } +} + +func TestTLSClientAuthentication(t *testing.T) { + // Invalid configurations are tested in the general test case. In this test we only look into specific details of valid client_auth options. + tests := []struct { + option string // tls plugin option(s) + expectedType tls.ClientAuthType // expected authentication type. + }{ + // By default, or if 'nocert' is specified, no cert should be requested. + // Other cases should be a straightforward mapping from the keyword to the type value. + {"", tls.NoClientCert}, + {"{\nclient_auth nocert\n}", tls.NoClientCert}, + {"{\nclient_auth request\n}", tls.RequestClientCert}, + {"{\nclient_auth require\n}", tls.RequireAnyClientCert}, + {"{\nclient_auth verify_if_given\n}", tls.VerifyClientCertIfGiven}, + {"{\nclient_auth require_and_verify\n}", tls.RequireAndVerifyClientCert}, + } + + for i, test := range tests { + input := "tls test_cert.pem test_key.pem test_ca.pem " + test.option + c := caddy.NewTestController("dns", input) + err := setup(c) + if err != nil { + t.Errorf("Test %d: TLS config is unexpectedly rejected: %v", i, err) + continue // there's no point in the rest of the tests. + } + cfg := dnsserver.GetConfig(c) + if cfg.TLSConfig.ClientCAs == nil { + t.Errorf("Test %d: Client CA is not configured", i) + } + if cfg.TLSConfig.ClientAuth != test.expectedType { + t.Errorf("Test %d: Unexpected client auth type: %d", i, cfg.TLSConfig.ClientAuth) + } + } +} diff --git a/ag_201_coredns/plugin/trace/README.md b/ag_201_coredns/plugin/trace/README.md new file mode 100644 index 0000000..eac8a7b --- /dev/null +++ b/ag_201_coredns/plugin/trace/README.md @@ -0,0 +1,113 @@ +# trace + +## Name + +*trace* - enables OpenTracing-based tracing of DNS requests as they go through the plugin chain. + +## Description + +With *trace* you enable OpenTracing of how a request flows through CoreDNS. Enable the *debug* +plugin to get logs from the trace plugin. + +## Syntax + +The simplest form is just: + +~~~ +trace [ENDPOINT-TYPE] [ENDPOINT] +~~~ + +* **ENDPOINT-TYPE** is the type of tracing destination. Currently only `zipkin` and `datadog` are supported. + Defaults to `zipkin`. +* **ENDPOINT** is the tracing destination, and defaults to `localhost:9411`. For Zipkin, if + **ENDPOINT** does not begin with `http`, then it will be transformed to `http://ENDPOINT/api/v1/spans`. + +With this form, all queries will be traced. + +Additional features can be enabled with this syntax: + +~~~ +trace [ENDPOINT-TYPE] [ENDPOINT] { + every AMOUNT + service NAME + client_server + datadog_analytics_rate RATE + zipkin_max_backlog_size SIZE + zipkin_max_batch_size SIZE + zipkin_max_batch_interval DURATION +} +~~~ + +* `every` **AMOUNT** will only trace one query of each AMOUNT queries. For example, to trace 1 in every + 100 queries, use AMOUNT of 100. The default is 1. +* `service` **NAME** allows you to specify the service name reported to the tracing server. + Default is `coredns`. +* `client_server` will enable the `ClientServerSameSpan` OpenTracing feature. +* `datadog_analytics_rate` **RATE** will enable [trace analytics](https://docs.datadoghq.com/tracing/app_analytics) on the traces sent + from *0* to *1*, *1* being every trace sent will be analyzed. This is a datadog only feature + (**ENDPOINT-TYPE** needs to be `datadog`) +* `zipkin_max_backlog_size` configures the maximum backlog size for Zipkin HTTP reporter. When batch size reaches this threshold, + spans from the beginning of the batch will be disposed. Default is 1000 backlog size. +* `zipkin_max_batch_size` configures the maximum batch size for Zipkin HTTP reporter, after which a collect will be triggered. The default batch size is 100 traces. +* `zipkin_max_batch_interval` configures the maximum duration we will buffer traces before emitting them to the collector using Zipkin HTTP reporter. + The default batch interval is 1 second. + +## Zipkin + +You can run Zipkin on a Docker host like this: + +``` +docker run -d -p 9411:9411 openzipkin/zipkin +``` + +Note the zipkin provider does not support the v1 API since coredns 1.7.1. + +## Examples + +Use an alternative Zipkin address: + +~~~ +trace tracinghost:9253 +~~~ + +or + +~~~ corefile +. { + trace zipkin tracinghost:9253 +} +~~~ + +If for some reason you are using an API reverse proxy or something and need to remap +the standard Zipkin URL you can do something like: + +~~~ +trace http://tracinghost:9411/zipkin/api/v1/spans +~~~ + +Using DataDog: + +~~~ +trace datadog localhost:8126 +~~~ + +Trace one query every 10000 queries, rename the service, and enable same span: + +~~~ +trace tracinghost:9411 { + every 10000 + service dnsproxy + client_server +} +~~~ + +## Metadata + +The trace plugin will publish the following metadata, if the *metadata* +plugin is also enabled: + +* `trace/traceid`: identifier of (zipkin/datadog) trace of processed request + +## See Also + +See the *debug* plugin for more information about debug logging. diff --git a/ag_201_coredns/plugin/trace/log_test.go b/ag_201_coredns/plugin/trace/log_test.go new file mode 100644 index 0000000..a0fe761 --- /dev/null +++ b/ag_201_coredns/plugin/trace/log_test.go @@ -0,0 +1,5 @@ +package trace + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/trace/logger.go b/ag_201_coredns/plugin/trace/logger.go new file mode 100644 index 0000000..6499387 --- /dev/null +++ b/ag_201_coredns/plugin/trace/logger.go @@ -0,0 +1,20 @@ +package trace + +import ( + clog "github.com/coredns/coredns/plugin/pkg/log" +) + +// loggerAdapter is a simple adapter around plugin logger made to implement io.Writer and ddtrace.Logger interface +// in order to log errors from span reporters as warnings +type loggerAdapter struct { + clog.P +} + +func (l *loggerAdapter) Write(p []byte) (n int, err error) { + l.P.Warning(string(p)) + return len(p), nil +} + +func (l *loggerAdapter) Log(msg string) { + l.P.Warning(msg) +} diff --git a/ag_201_coredns/plugin/trace/setup.go b/ag_201_coredns/plugin/trace/setup.go new file mode 100644 index 0000000..8672dcc --- /dev/null +++ b/ag_201_coredns/plugin/trace/setup.go @@ -0,0 +1,163 @@ +package trace + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("trace", setup) } + +func setup(c *caddy.Controller) error { + t, err := traceParse(c) + if err != nil { + return plugin.Error("trace", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + t.Next = next + return t + }) + + c.OnStartup(t.OnStartup) + + return nil +} + +func traceParse(c *caddy.Controller) (*trace, error) { + var ( + tr = &trace{every: 1, serviceName: defServiceName} + err error + ) + + cfg := dnsserver.GetConfig(c) + if cfg.ListenHosts[0] != "" { + tr.serviceEndpoint = cfg.ListenHosts[0] + ":" + cfg.Port + } + + for c.Next() { // trace + var err error + args := c.RemainingArgs() + switch len(args) { + case 0: + tr.EndpointType, tr.Endpoint, err = normalizeEndpoint(defEpType, "") + case 1: + tr.EndpointType, tr.Endpoint, err = normalizeEndpoint(defEpType, args[0]) + case 2: + epType := strings.ToLower(args[0]) + tr.EndpointType, tr.Endpoint, err = normalizeEndpoint(epType, args[1]) + default: + err = c.ArgErr() + } + if err != nil { + return tr, err + } + for c.NextBlock() { + switch c.Val() { + case "every": + args := c.RemainingArgs() + if len(args) != 1 { + return nil, c.ArgErr() + } + tr.every, err = strconv.ParseUint(args[0], 10, 64) + if err != nil { + return nil, err + } + case "service": + args := c.RemainingArgs() + if len(args) != 1 { + return nil, c.ArgErr() + } + tr.serviceName = args[0] + case "client_server": + args := c.RemainingArgs() + if len(args) > 1 { + return nil, c.ArgErr() + } + tr.clientServer = true + if len(args) == 1 { + tr.clientServer, err = strconv.ParseBool(args[0]) + } + if err != nil { + return nil, err + } + case "datadog_analytics_rate": + args := c.RemainingArgs() + if len(args) > 1 { + return nil, c.ArgErr() + } + tr.datadogAnalyticsRate = 0 + if len(args) == 1 { + tr.datadogAnalyticsRate, err = strconv.ParseFloat(args[0], 64) + } + if err != nil { + return nil, err + } + if tr.datadogAnalyticsRate > 1 || tr.datadogAnalyticsRate < 0 { + return nil, fmt.Errorf("datadog analytics rate must be between 0 and 1, '%f' is not supported", tr.datadogAnalyticsRate) + } + case "zipkin_max_backlog_size": + args := c.RemainingArgs() + if len(args) != 1 { + return nil, c.ArgErr() + } + tr.zipkinMaxBacklogSize, err = strconv.Atoi(args[0]) + if err != nil { + return nil, err + } + case "zipkin_max_batch_size": + args := c.RemainingArgs() + if len(args) != 1 { + return nil, c.ArgErr() + } + tr.zipkinMaxBatchSize, err = strconv.Atoi(args[0]) + if err != nil { + return nil, err + } + case "zipkin_max_batch_interval": + args := c.RemainingArgs() + if len(args) != 1 { + return nil, c.ArgErr() + } + tr.zipkinMaxBatchInterval, err = time.ParseDuration(args[0]) + if err != nil { + return nil, err + } + } + } + } + return tr, err +} + +func normalizeEndpoint(epType, ep string) (string, string, error) { + if _, ok := supportedProviders[epType]; !ok { + return "", "", fmt.Errorf("tracing endpoint type '%s' is not supported", epType) + } + + if ep == "" { + ep = supportedProviders[epType] + } + + if epType == "zipkin" { + if !strings.Contains(ep, "http") { + ep = "http://" + ep + "/api/v2/spans" + } + } + + return epType, ep, nil +} + +var supportedProviders = map[string]string{ + "zipkin": "localhost:9411", + "datadog": "localhost:8126", +} + +const ( + defEpType = "zipkin" + defServiceName = "coredns" +) diff --git a/ag_201_coredns/plugin/trace/setup_test.go b/ag_201_coredns/plugin/trace/setup_test.go new file mode 100644 index 0000000..72de4ab --- /dev/null +++ b/ag_201_coredns/plugin/trace/setup_test.go @@ -0,0 +1,88 @@ +package trace + +import ( + "testing" + "time" + + "github.com/coredns/caddy" +) + +func TestTraceParse(t *testing.T) { + tests := []struct { + input string + shouldErr bool + endpoint string + every uint64 + serviceName string + clientServer bool + zipkinMaxBacklogSize int + zipkinMaxBatchSize int + zipkinMaxBatchInterval time.Duration + }{ + // oks + {`trace`, false, "http://localhost:9411/api/v2/spans", 1, `coredns`, false, 0, 0, 0}, + {`trace localhost:1234`, false, "http://localhost:1234/api/v2/spans", 1, `coredns`, false, 0, 0, 0}, + {`trace http://localhost:1234/somewhere/else`, false, "http://localhost:1234/somewhere/else", 1, `coredns`, false, 0, 0, 0}, + {`trace zipkin localhost:1234`, false, "http://localhost:1234/api/v2/spans", 1, `coredns`, false, 0, 0, 0}, + {`trace datadog localhost`, false, "localhost", 1, `coredns`, false, 0, 0, 0}, + {`trace datadog http://localhost:8127`, false, "http://localhost:8127", 1, `coredns`, false, 0, 0, 0}, + {"trace datadog localhost {\n datadog_analytics_rate 0.1\n}", false, "localhost", 1, `coredns`, false, 0, 0, 0}, + {"trace {\n every 100\n}", false, "http://localhost:9411/api/v2/spans", 100, `coredns`, false, 0, 0, 0}, + {"trace {\n every 100\n service foobar\nclient_server\n}", false, "http://localhost:9411/api/v2/spans", 100, `foobar`, true, 0, 0, 0}, + {"trace {\n every 2\n client_server true\n}", false, "http://localhost:9411/api/v2/spans", 2, `coredns`, true, 0, 0, 0}, + {"trace {\n client_server false\n}", false, "http://localhost:9411/api/v2/spans", 1, `coredns`, false, 0, 0, 0}, + {"trace {\n zipkin_max_backlog_size 100\n zipkin_max_batch_size 200\n zipkin_max_batch_interval 10s\n}", false, + "http://localhost:9411/api/v2/spans", 1, `coredns`, false, 100, 200, 10 * time.Second}, + + // fails + {`trace footype localhost:4321`, true, "", 1, "", false, 0, 0, 0}, + {"trace {\n every 2\n client_server junk\n}", true, "", 1, "", false, 0, 0, 0}, + {"trace datadog localhost {\n datadog_analytics_rate 2\n}", true, "", 1, "", false, 0, 0, 0}, + {"trace {\n zipkin_max_backlog_size wrong\n}", true, "", 1, `coredns`, false, 0, 0, 0}, + {"trace {\n zipkin_max_batch_size wrong\n}", true, "", 1, `coredns`, false, 0, 0, 0}, + {"trace {\n zipkin_max_batch_interval wrong\n}", true, "", 1, `coredns`, false, 0, 0, 0}, + {"trace {\n zipkin_max_backlog_size\n}", true, "", 1, `coredns`, false, 0, 0, 0}, + {"trace {\n zipkin_max_batch_size\n}", true, "", 1, `coredns`, false, 0, 0, 0}, + {"trace {\n zipkin_max_batch_interval\n}", true, "", 1, `coredns`, false, 0, 0, 0}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + m, err := traceParse(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 "" != m.serviceEndpoint { + t.Errorf("Test %v: Expected serviceEndpoint to be '' but found: %s", i, m.serviceEndpoint) + } + if test.endpoint != m.Endpoint { + t.Errorf("Test %v: Expected endpoint %s but found: %s", i, test.endpoint, m.Endpoint) + } + if test.every != m.every { + t.Errorf("Test %v: Expected every %d but found: %d", i, test.every, m.every) + } + if test.serviceName != m.serviceName { + t.Errorf("Test %v: Expected service name %s but found: %s", i, test.serviceName, m.serviceName) + } + if test.clientServer != m.clientServer { + t.Errorf("Test %v: Expected client_server %t but found: %t", i, test.clientServer, m.clientServer) + } + if test.zipkinMaxBacklogSize != m.zipkinMaxBacklogSize { + t.Errorf("Test %v: Expected zipkin_max_backlog_size %d but found: %d", i, test.zipkinMaxBacklogSize, m.zipkinMaxBacklogSize) + } + if test.zipkinMaxBatchSize != m.zipkinMaxBatchSize { + t.Errorf("Test %v: Expected zipkin_max_batch_size %d but found: %d", i, test.zipkinMaxBatchSize, m.zipkinMaxBatchSize) + } + if test.zipkinMaxBatchInterval != m.zipkinMaxBatchInterval { + t.Errorf("Test %v: Expected zipkin_max_batch_interval %v but found: %v", i, test.zipkinMaxBatchInterval, m.zipkinMaxBatchInterval) + } + } +} diff --git a/ag_201_coredns/plugin/trace/trace.go b/ag_201_coredns/plugin/trace/trace.go new file mode 100644 index 0000000..f740967 --- /dev/null +++ b/ag_201_coredns/plugin/trace/trace.go @@ -0,0 +1,204 @@ +// Package trace implements OpenTracing-based tracing +package trace + +import ( + "context" + "fmt" + stdlog "log" + "net/http" + "sync" + "sync/atomic" + "time" + + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/pkg/dnstest" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/rcode" + _ "github.com/coredns/coredns/plugin/pkg/trace" // Plugin the trace package. + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + ot "github.com/opentracing/opentracing-go" + otext "github.com/opentracing/opentracing-go/ext" + otlog "github.com/opentracing/opentracing-go/log" + zipkinot "github.com/openzipkin-contrib/zipkin-go-opentracing" + "github.com/openzipkin/zipkin-go" + zipkinhttp "github.com/openzipkin/zipkin-go/reporter/http" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +const ( + defaultTopLevelSpanName = "servedns" + metaTraceIdKey = "trace/traceid" +) + +var log = clog.NewWithPlugin("trace") + +type traceTags struct { + Name string + Type string + Rcode string + Proto string + Remote string +} + +var tagByProvider = map[string]traceTags{ + "default": { + Name: "coredns.io/name", + Type: "coredns.io/type", + Rcode: "coredns.io/rcode", + Proto: "coredns.io/proto", + Remote: "coredns.io/remote", + }, + "datadog": { + Name: "coredns.io@name", + Type: "coredns.io@type", + Rcode: "coredns.io@rcode", + Proto: "coredns.io@proto", + Remote: "coredns.io@remote", + }, +} + +type trace struct { + count uint64 // as per Go spec, needs to be first element in a struct + + Next plugin.Handler + Endpoint string + EndpointType string + tracer ot.Tracer + serviceEndpoint string + serviceName string + clientServer bool + every uint64 + datadogAnalyticsRate float64 + zipkinMaxBacklogSize int + zipkinMaxBatchSize int + zipkinMaxBatchInterval time.Duration + Once sync.Once + tagSet traceTags +} + +func (t *trace) Tracer() ot.Tracer { + return t.tracer +} + +// OnStartup sets up the tracer +func (t *trace) OnStartup() error { + var err error + t.Once.Do(func() { + switch t.EndpointType { + case "zipkin": + err = t.setupZipkin() + case "datadog": + tracer := opentracer.New( + tracer.WithAgentAddr(t.Endpoint), + tracer.WithDebugMode(clog.D.Value()), + tracer.WithGlobalTag(ext.SpanTypeDNS, true), + tracer.WithServiceName(t.serviceName), + tracer.WithAnalyticsRate(t.datadogAnalyticsRate), + tracer.WithLogger(&loggerAdapter{log}), + ) + t.tracer = tracer + t.tagSet = tagByProvider["datadog"] + default: + err = fmt.Errorf("unknown endpoint type: %s", t.EndpointType) + } + }) + return err +} + +func (t *trace) setupZipkin() error { + var opts []zipkinhttp.ReporterOption + opts = append(opts, zipkinhttp.Logger(stdlog.New(&loggerAdapter{log}, "", 0))) + if t.zipkinMaxBacklogSize != 0 { + opts = append(opts, zipkinhttp.MaxBacklog(t.zipkinMaxBacklogSize)) + } + if t.zipkinMaxBatchSize != 0 { + opts = append(opts, zipkinhttp.BatchSize(t.zipkinMaxBatchSize)) + } + if t.zipkinMaxBatchInterval != 0 { + opts = append(opts, zipkinhttp.BatchInterval(t.zipkinMaxBatchInterval)) + } + reporter := zipkinhttp.NewReporter(t.Endpoint, opts...) + recorder, err := zipkin.NewEndpoint(t.serviceName, t.serviceEndpoint) + if err != nil { + log.Warningf("build Zipkin endpoint found err: %v", err) + } + tracer, err := zipkin.NewTracer( + reporter, + zipkin.WithLocalEndpoint(recorder), + zipkin.WithSharedSpans(t.clientServer), + ) + if err != nil { + return err + } + t.tracer = zipkinot.Wrap(tracer) + + t.tagSet = tagByProvider["default"] + return err +} + +// Name implements the Handler interface. +func (t *trace) Name() string { return "trace" } + +// ServeDNS implements the plugin.Handle interface. +func (t *trace) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + trace := false + if t.every > 0 { + queryNr := atomic.AddUint64(&t.count, 1) + + if queryNr%t.every == 0 { + trace = true + } + } + span := ot.SpanFromContext(ctx) + if !trace || span != nil { + return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r) + } + + var spanCtx ot.SpanContext + if val := ctx.Value(dnsserver.HTTPRequestKey{}); val != nil { + if httpReq, ok := val.(*http.Request); ok { + spanCtx, _ = t.Tracer().Extract(ot.HTTPHeaders, ot.HTTPHeadersCarrier(httpReq.Header)) + } + } + + req := request.Request{W: w, Req: r} + span = t.Tracer().StartSpan(defaultTopLevelSpanName, otext.RPCServerOption(spanCtx)) + defer span.Finish() + + switch spanCtx := span.Context().(type) { + case zipkinot.SpanContext: + metadata.SetValueFunc(ctx, metaTraceIdKey, func() string { return spanCtx.TraceID.String() }) + case ddtrace.SpanContext: + metadata.SetValueFunc(ctx, metaTraceIdKey, func() string { return fmt.Sprint(spanCtx.TraceID()) }) + } + + rw := dnstest.NewRecorder(w) + ctx = ot.ContextWithSpan(ctx, span) + status, err := plugin.NextOrFailure(t.Name(), t.Next, ctx, rw, r) + + span.SetTag(t.tagSet.Name, req.Name()) + span.SetTag(t.tagSet.Type, req.Type()) + span.SetTag(t.tagSet.Proto, req.Proto()) + span.SetTag(t.tagSet.Remote, req.IP()) + rc := rw.Rcode + if !plugin.ClientWrite(status) { + // when no response was written, fallback to status returned from next plugin as this status + // is actually used as rcode of DNS response + // see https://github.com/coredns/coredns/blob/master/core/dnsserver/server.go#L318 + rc = status + } + span.SetTag(t.tagSet.Rcode, rcode.ToString(rc)) + if err != nil { + otext.Error.Set(span, true) + span.LogFields(otlog.Event("error"), otlog.Error(err)) + } + + return status, err +} diff --git a/ag_201_coredns/plugin/trace/trace_test.go b/ag_201_coredns/plugin/trace/trace_test.go new file mode 100644 index 0000000..940eb6b --- /dev/null +++ b/ag_201_coredns/plugin/trace/trace_test.go @@ -0,0 +1,173 @@ +package trace + +import ( + "context" + "errors" + "net/http/httptest" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/rcode" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/mocktracer" +) + +func TestStartup(t *testing.T) { + m, err := traceParse(caddy.NewTestController("dns", `trace`)) + if err != nil { + t.Errorf("Error parsing test input: %s", err) + return + } + if m.Name() != "trace" { + t.Errorf("Wrong name from GetName: %s", m.Name()) + } + err = m.OnStartup() + if err != nil { + t.Errorf("Error starting tracing plugin: %s", err) + return + } + + if m.tagSet != tagByProvider["default"] { + t.Errorf("TagSet by proviser hasn't been corectly initialized") + } + + if m.Tracer() == nil { + t.Errorf("Error, no tracer created") + } +} + +func TestTrace(t *testing.T) { + cases := []struct { + name string + rcode int + status int + question *dns.Msg + err error + }{ + { + name: "NXDOMAIN", + rcode: dns.RcodeNameError, + status: dns.RcodeSuccess, + question: new(dns.Msg).SetQuestion("example.org.", dns.TypeA), + }, + { + name: "NOERROR", + rcode: dns.RcodeSuccess, + status: dns.RcodeSuccess, + question: new(dns.Msg).SetQuestion("example.net.", dns.TypeCNAME), + }, + { + name: "SERVFAIL", + rcode: dns.RcodeServerFailure, + status: dns.RcodeSuccess, + question: new(dns.Msg).SetQuestion("example.net.", dns.TypeA), + err: errors.New("test error"), + }, + { + name: "No response written", + rcode: dns.RcodeServerFailure, + status: dns.RcodeServerFailure, + question: new(dns.Msg).SetQuestion("example.net.", dns.TypeA), + err: errors.New("test error"), + }, + } + defaultTagSet := tagByProvider["default"] + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + w := dnstest.NewRecorder(&test.ResponseWriter{}) + m := mocktracer.New() + tr := &trace{ + Next: test.HandlerFunc(func(_ context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if plugin.ClientWrite(tc.status) { + m := new(dns.Msg) + m.SetRcode(r, tc.rcode) + w.WriteMsg(m) + } + return tc.status, tc.err + }), + every: 1, + tracer: m, + tagSet: defaultTagSet, + } + ctx := context.TODO() + if _, err := tr.ServeDNS(ctx, w, tc.question); err != nil && tc.err == nil { + t.Fatalf("Error during tr.ServeDNS(ctx, w, %v): %v", tc.question, err) + } + + fs := m.FinishedSpans() + // Each trace consists of two spans; the root and the Next function. + if len(fs) != 2 { + t.Fatalf("Unexpected span count: len(fs): want 2, got %v", len(fs)) + } + + rootSpan := fs[1] + req := request.Request{W: w, Req: tc.question} + if rootSpan.OperationName != defaultTopLevelSpanName { + t.Errorf("Unexpected span name: rootSpan.Name: want %v, got %v", defaultTopLevelSpanName, rootSpan.OperationName) + } + + if rootSpan.Tag(defaultTagSet.Name) != req.Name() { + t.Errorf("Unexpected span tag: rootSpan.Tag(%v): want %v, got %v", defaultTagSet.Name, req.Name(), rootSpan.Tag(defaultTagSet.Name)) + } + if rootSpan.Tag(defaultTagSet.Type) != req.Type() { + t.Errorf("Unexpected span tag: rootSpan.Tag(%v): want %v, got %v", defaultTagSet.Type, req.Type(), rootSpan.Tag(defaultTagSet.Type)) + } + if rootSpan.Tag(defaultTagSet.Proto) != req.Proto() { + t.Errorf("Unexpected span tag: rootSpan.Tag(%v): want %v, got %v", defaultTagSet.Proto, req.Proto(), rootSpan.Tag(defaultTagSet.Proto)) + } + if rootSpan.Tag(defaultTagSet.Remote) != req.IP() { + t.Errorf("Unexpected span tag: rootSpan.Tag(%v): want %v, got %v", defaultTagSet.Remote, req.IP(), rootSpan.Tag(defaultTagSet.Remote)) + } + if rootSpan.Tag(defaultTagSet.Rcode) != rcode.ToString(tc.rcode) { + t.Errorf("Unexpected span tag: rootSpan.Tag(%v): want %v, got %v", defaultTagSet.Rcode, rcode.ToString(tc.rcode), rootSpan.Tag(defaultTagSet.Rcode)) + } + if tc.err != nil && rootSpan.Tag("error") != true { + t.Errorf("Unexpected span tag: rootSpan.Tag(%v): want %v, got %v", "error", true, rootSpan.Tag("error")) + } + }) + } +} + +func TestTrace_DOH_TraceHeaderExtraction(t *testing.T) { + w := dnstest.NewRecorder(&test.ResponseWriter{}) + m := mocktracer.New() + tr := &trace{ + Next: test.HandlerFunc(func(_ context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if plugin.ClientWrite(dns.RcodeSuccess) { + m := new(dns.Msg) + m.SetRcode(r, dns.RcodeSuccess) + w.WriteMsg(m) + } + return dns.RcodeSuccess, nil + }), + every: 1, + tracer: m, + } + q := new(dns.Msg).SetQuestion("example.net.", dns.TypeA) + + req := httptest.NewRequest("POST", "/dns-query", nil) + + outsideSpan := m.StartSpan("test-header-span") + outsideSpan.Tracer().Inject(outsideSpan.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header)) + defer outsideSpan.Finish() + + ctx := context.TODO() + ctx = context.WithValue(ctx, dnsserver.HTTPRequestKey{}, req) + + tr.ServeDNS(ctx, w, q) + + fs := m.FinishedSpans() + rootCoreDNSspan := fs[1] + rootCoreDNSTraceID := rootCoreDNSspan.Context().(mocktracer.MockSpanContext).TraceID + outsideSpanTraceID := outsideSpan.Context().(mocktracer.MockSpanContext).TraceID + if rootCoreDNSTraceID != outsideSpanTraceID { + t.Errorf("Unexpected traceID: rootSpan.TraceID: want %v, got %v", rootCoreDNSTraceID, outsideSpanTraceID) + } +} diff --git a/ag_201_coredns/plugin/transfer/README.md b/ag_201_coredns/plugin/transfer/README.md new file mode 100644 index 0000000..43c1623 --- /dev/null +++ b/ag_201_coredns/plugin/transfer/README.md @@ -0,0 +1,59 @@ +# transfer + +## Name + +*transfer* - perform (outgoing) zone transfers for other plugins. + +## Description + +This plugin answers zone transfers for authoritative plugins that implement `transfer.Transferer`. + +*transfer* answers full zone transfer (AXFR) requests and incremental zone transfer (IXFR) requests +with AXFR fallback if the zone has changed. + +When a plugin wants to notify it's secondaries it will call back into the *transfer* plugin. + +The following plugins implement zone transfers using this plugin: *file*, *auto*, *secondary*, and +*kubernetes*. See `transfer.go` for implementation details if you are a plugin author that wants to +use this plugin. + +## Syntax + +~~~ +transfer [ZONE...] { + to ADDRESS... +} +~~~ + + * **ZONE** The zones *transfer* will answer zone transfer requests for. If left blank, the zones + are inherited from the enclosing server block. To answer zone transfers for a given zone, + there must be another plugin in the same server block that serves the same zone, and implements + `transfer.Transferer`. + + * `to` **ADDRESS...** The hosts *transfer* will transfer to. Use `*` to permit transfers to all + addresses. Zone change notifications are sent to all **ADDRESS** that are an IP address or + an IP address and port e.g. `1.2.3.4`, `12:34::56`, `1.2.3.4:5300`, `[12:34::56]:5300`. + `to` may be specified multiple times. + +You can use the _acl_ plugin to further restrict hosts permitted to receive a zone transfer. +See example below. + +## Examples + +Use in conjunction with the _acl_ plugin to restrict access to subnet 10.1.0.0/16. + +``` +... + acl { + allow type AXFR net 10.1.0.0/16 + allow type IXFR net 10.1.0.0/16 + block type AXFR net * + block type IXFR net * + } + transfer { + to * + } +... +``` + +Each plugin that can use _transfer_ includes an example of use in their respective documentation. \ No newline at end of file diff --git a/ag_201_coredns/plugin/transfer/failed_write_test.go b/ag_201_coredns/plugin/transfer/failed_write_test.go new file mode 100644 index 0000000..e60fd50 --- /dev/null +++ b/ag_201_coredns/plugin/transfer/failed_write_test.go @@ -0,0 +1,30 @@ +package transfer + +import ( + "context" + "fmt" + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +type badwriter struct { + dns.ResponseWriter +} + +func (w *badwriter) WriteMsg(_ *dns.Msg) error { return fmt.Errorf("failed to write msg") } + +func TestWriteMessageFailed(t *testing.T) { + transfer := newTestTransfer() + ctx := context.TODO() + w := &badwriter{ResponseWriter: &test.ResponseWriter{TCP: true}} + m := &dns.Msg{} + m.SetAxfr("example.org.") + + _, err := transfer.ServeDNS(ctx, w, m) + if err == nil { + t.Error("Expected error, got none") + } +} diff --git a/ag_201_coredns/plugin/transfer/notify.go b/ag_201_coredns/plugin/transfer/notify.go new file mode 100644 index 0000000..b024a3a --- /dev/null +++ b/ag_201_coredns/plugin/transfer/notify.go @@ -0,0 +1,58 @@ +package transfer + +import ( + "fmt" + + "github.com/coredns/coredns/plugin/pkg/rcode" + + "github.com/miekg/dns" +) + +// Notify will send notifies to all configured to hosts IP addresses. If the zone isn't known +// to t an error will be returned. The string zone must be lowercased. +func (t *Transfer) Notify(zone string) error { + if t == nil { // t might be nil, mostly expected in tests, so intercept and to a noop in that case + return nil + } + + m := new(dns.Msg) + m.SetNotify(zone) + c := new(dns.Client) + + x := longestMatch(t.xfrs, zone) + if x == nil { + return fmt.Errorf("no such zone registred in the transfer plugin: %s", zone) + } + + var err1 error + for _, t := range x.to { + if t == "*" { + continue + } + if err := sendNotify(c, m, t); err != nil { + err1 = err + } + } + log.Debugf("Sent notifies for zone %q to %v", zone, x.to) + return err1 // this only captures the last error +} + +func sendNotify(c *dns.Client, m *dns.Msg, s string) error { + var err error + + code := dns.RcodeServerFailure + for i := 0; i < 3; i++ { + ret, _, err := c.Exchange(m, s) + if err != nil { + continue + } + code = ret.Rcode + if code == dns.RcodeSuccess { + return nil + } + } + if err != nil { + return fmt.Errorf("notify for zone %q was not accepted by %q: %q", m.Question[0].Name, s, err) + } + return fmt.Errorf("notify for zone %q was not accepted by %q: rcode was %q", m.Question[0].Name, s, rcode.ToString(code)) +} diff --git a/ag_201_coredns/plugin/transfer/select_test.go b/ag_201_coredns/plugin/transfer/select_test.go new file mode 100644 index 0000000..a064b00 --- /dev/null +++ b/ag_201_coredns/plugin/transfer/select_test.go @@ -0,0 +1,58 @@ +package transfer + +import ( + "context" + "fmt" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +type ( + t1 struct{} + t2 struct{} +) + +func (t t1) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) { + const z = "example.org." + if zone != z { + return nil, ErrNotAuthoritative + } + return nil, fmt.Errorf(z) +} +func (t t2) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) { + const z = "sub.example.org." + if zone != z { + return nil, ErrNotAuthoritative + } + return nil, fmt.Errorf(z) +} + +func TestZoneSelection(t *testing.T) { + tr := &Transfer{ + Transferers: []Transferer{t1{}, t2{}}, + xfrs: []*xfr{ + { + Zones: []string{"example.org."}, + to: []string{"192.0.2.1"}, // RFC 5737 IP, no interface should have this address. + }, + { + Zones: []string{"sub.example.org."}, + to: []string{"*"}, + }, + }, + } + r := new(dns.Msg) + r.SetAxfr("sub.example.org.") + w := dnstest.NewRecorder(&test.ResponseWriter{TCP: true}) + _, err := tr.ServeDNS(context.TODO(), w, r) + if err == nil { + t.Fatal("Expected error, got nil") + } + if x := err.Error(); x != "sub.example.org." { + t.Errorf("Expected transfer for zone %s, got %s", "sub.example.org", x) + } +} diff --git a/ag_201_coredns/plugin/transfer/setup.go b/ag_201_coredns/plugin/transfer/setup.go new file mode 100644 index 0000000..cd7d209 --- /dev/null +++ b/ag_201_coredns/plugin/transfer/setup.go @@ -0,0 +1,81 @@ +package transfer + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/parse" + "github.com/coredns/coredns/plugin/pkg/transport" +) + +func init() { + caddy.RegisterPlugin("transfer", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + t, err := parseTransfer(c) + + if err != nil { + return plugin.Error("transfer", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + t.Next = next + return t + }) + + c.OnStartup(func() error { + config := dnsserver.GetConfig(c) + t.tsigSecret = config.TsigSecret + // find all plugins that implement Transferer and add them to Transferers + plugins := config.Handlers() + for _, pl := range plugins { + tr, ok := pl.(Transferer) + if !ok { + continue + } + t.Transferers = append(t.Transferers, tr) + } + return nil + }) + + return nil +} + +func parseTransfer(c *caddy.Controller) (*Transfer, error) { + t := &Transfer{} + for c.Next() { + x := &xfr{} + x.Zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) + for c.NextBlock() { + switch c.Val() { + case "to": + args := c.RemainingArgs() + if len(args) == 0 { + return nil, c.ArgErr() + } + for _, host := range args { + if host == "*" { + x.to = append(x.to, host) + continue + } + normalized, err := parse.HostPort(host, transport.Port) + if err != nil { + return nil, err + } + x.to = append(x.to, normalized) + } + default: + return nil, plugin.Error("transfer", c.Errf("unknown property %q", c.Val())) + } + } + if len(x.to) == 0 { + return nil, plugin.Error("transfer", c.Err("'to' is required")) + } + t.xfrs = append(t.xfrs, x) + } + return t, nil +} diff --git a/ag_201_coredns/plugin/transfer/setup_test.go b/ag_201_coredns/plugin/transfer/setup_test.go new file mode 100644 index 0000000..ebfe99c --- /dev/null +++ b/ag_201_coredns/plugin/transfer/setup_test.go @@ -0,0 +1,131 @@ +package transfer + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestParse(t *testing.T) { + tests := []struct { + input string + zones []string + shouldErr bool + exp *Transfer + }{ + {`transfer example.net example.org { + to 1.2.3.4 5.6.7.8:1053 [1::2]:34 + } + transfer example.com example.edu { + to * 1.2.3.4 + }`, + nil, + false, + &Transfer{ + xfrs: []*xfr{{ + Zones: []string{"example.net.", "example.org."}, + to: []string{"1.2.3.4:53", "5.6.7.8:1053", "[1::2]:34"}, + }, { + Zones: []string{"example.com.", "example.edu."}, + to: []string{"*", "1.2.3.4:53"}, + }}, + }, + }, + // errors + {`transfer example.net example.org { + }`, + nil, + true, + nil, + }, + {`transfer example.net example.org { + invalid option + }`, + nil, + true, + nil, + }, + { + ` + transfer example.com example.edu { + to example.com 1.2.3.4 + }`, + nil, + true, + nil, + }, + { + `transfer { + to 1.2.3.4 5.6.7.8:1053 [1::2]:34 + }`, + []string{"."}, + false, + &Transfer{ + xfrs: []*xfr{{ + Zones: []string{"."}, + to: []string{"1.2.3.4:53", "5.6.7.8:1053", "[1::2]:34"}, + }}, + }, + }, + } + for i, tc := range tests { + c := caddy.NewTestController("dns", tc.input) + c.ServerBlockKeys = append(c.ServerBlockKeys, tc.zones...) + + transfer, err := parseTransfer(c) + + if err == nil && tc.shouldErr { + t.Fatalf("Test %d expected errors, but got no error", i) + } + if err != nil && !tc.shouldErr { + t.Fatalf("Test %d expected no errors, but got '%v'", i, err) + } + if tc.exp == nil && transfer != nil { + t.Fatalf("Test %d expected %v xfrs, got %#v", i, tc.exp, transfer) + } + if tc.shouldErr { + continue + } + + if len(tc.exp.xfrs) != len(transfer.xfrs) { + t.Fatalf("Test %d expected %d xfrs, got %d", i, len(tc.exp.xfrs), len(transfer.xfrs)) + } + for j, x := range transfer.xfrs { + // Check Zones + if len(tc.exp.xfrs[j].Zones) != len(x.Zones) { + t.Fatalf("Test %d expected %d zones, got %d", i, len(tc.exp.xfrs[i].Zones), len(x.Zones)) + } + for k, zone := range x.Zones { + if tc.exp.xfrs[j].Zones[k] != zone { + t.Errorf("Test %d expected zone %v, got %v", i, tc.exp.xfrs[j].Zones[k], zone) + } + } + // Check to + if len(tc.exp.xfrs[j].to) != len(x.to) { + t.Fatalf("Test %d expected %d 'to' values, got %d", i, len(tc.exp.xfrs[i].to), len(x.to)) + } + for k, to := range x.to { + if tc.exp.xfrs[j].to[k] != to { + t.Errorf("Test %d expected %v in 'to', got %v", i, tc.exp.xfrs[j].to[k], to) + } + } + } + } +} + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", "transfer") + if err := setup(c); err == nil { + t.Fatal("Expected errors, but got nil") + } + + c = caddy.NewTestController("dns", `transfer example.net example.org { + to 1.2.3.4 5.6.7.8:1053 [1::2]:34 + } + transfer example.com example.edu { + to * 1.2.3.4 + }`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got %v", err) + } +} diff --git a/ag_201_coredns/plugin/transfer/transfer.go b/ag_201_coredns/plugin/transfer/transfer.go new file mode 100644 index 0000000..71136e1 --- /dev/null +++ b/ag_201_coredns/plugin/transfer/transfer.go @@ -0,0 +1,221 @@ +package transfer + +import ( + "context" + "errors" + "net" + + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +var log = clog.NewWithPlugin("transfer") + +// Transfer is a plugin that handles zone transfers. +type Transfer struct { + Transferers []Transferer // List of plugins that implement Transferer + xfrs []*xfr + tsigSecret map[string]string + Next plugin.Handler +} + +type xfr struct { + Zones []string + to []string +} + +// Transferer may be implemented by plugins to enable zone transfers +type Transferer interface { + // Transfer returns a channel to which it writes responses to the transfer request. + // If the plugin is not authoritative for the zone, it should immediately return the + // transfer.ErrNotAuthoritative error. This is important otherwise the transfer plugin can + // use plugin X while it should transfer the data from plugin Y. + // + // If serial is 0, handle as an AXFR request. Transfer should send all records + // in the zone to the channel. The SOA should be written to the channel first, followed + // by all other records, including all NS + glue records. The implemenation is also responsible + // for sending the last SOA record (to signal end of the transfer). This plugin will just grab + // these records and send them back to the requester, there is little validation done. + // + // If serial is not 0, it will be handled as an IXFR request. If the serial is equal to or greater (newer) than + // the current serial for the zone, send a single SOA record to the channel and then close it. + // If the serial is less (older) than the current serial for the zone, perform an AXFR fallback + // by proceeding as if an AXFR was requested (as above). + Transfer(zone string, serial uint32) (<-chan []dns.RR, error) +} + +var ( + // ErrNotAuthoritative is returned by Transfer() when the plugin is not authoritative for the zone. + ErrNotAuthoritative = errors.New("not authoritative for zone") +) + +// ServeDNS implements the plugin.Handler interface. +func (t *Transfer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + if state.QType() != dns.TypeAXFR && state.QType() != dns.TypeIXFR { + return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r) + } + + if state.Proto() != "tcp" { + return dns.RcodeRefused, nil + } + + x := longestMatch(t.xfrs, state.QName()) + if x == nil { + return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r) + } + + if !x.allowed(state) { + // write msg here, so logging will pick it up + m := new(dns.Msg) + m.SetRcode(r, dns.RcodeRefused) + w.WriteMsg(m) + return 0, nil + } + + // Get serial from request if this is an IXFR. + var serial uint32 + if state.QType() == dns.TypeIXFR { + if len(r.Ns) != 1 { + return dns.RcodeServerFailure, nil + } + soa, ok := r.Ns[0].(*dns.SOA) + if !ok { + return dns.RcodeServerFailure, nil + } + serial = soa.Serial + } + + // Get a receiving channel from the first Transferer plugin that returns one. + var pchan <-chan []dns.RR + var err error + for _, p := range t.Transferers { + pchan, err = p.Transfer(state.QName(), serial) + if err == ErrNotAuthoritative { + // plugin was not authoritative for the zone, try next plugin + continue + } + if err != nil { + return dns.RcodeServerFailure, err + } + break + } + + if pchan == nil { + return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r) + } + + // Send response to client + ch := make(chan *dns.Envelope) + tr := new(dns.Transfer) + if r.IsTsig() != nil { + tr.TsigSecret = t.tsigSecret + } + errCh := make(chan error) + go func() { + if err := tr.Out(w, r, ch); err != nil { + errCh <- err + } + close(errCh) + }() + + rrs := []dns.RR{} + l := 0 + var soa *dns.SOA + for records := range pchan { + if x, ok := records[0].(*dns.SOA); ok && soa == nil { + soa = x + } + rrs = append(rrs, records...) + if len(rrs) > 500 { + select { + case ch <- &dns.Envelope{RR: rrs}: + case err := <-errCh: + return dns.RcodeServerFailure, err + } + l += len(rrs) + rrs = []dns.RR{} + } + } + + // if we are here and we only hold 1 soa (len(rrs) == 1) and soa != nil, and IXFR fallback should + // be performed. We haven't send anything on ch yet, so that can be closed (and waited for), and we only + // need to return the SOA back to the client and return. + if len(rrs) == 1 && soa != nil { // soa should never be nil... + close(ch) + err := <-errCh + if err != nil { + return dns.RcodeServerFailure, err + } + + m := new(dns.Msg) + m.SetReply(r) + m.Answer = []dns.RR{soa} + w.WriteMsg(m) + + log.Infof("Outgoing noop, incremental transfer for up to date zone %q to %s for %d SOA serial", state.QName(), state.IP(), soa.Serial) + return 0, nil + } + + if len(rrs) > 0 { + select { + case ch <- &dns.Envelope{RR: rrs}: + case err := <-errCh: + return dns.RcodeServerFailure, err + } + l += len(rrs) + } + + close(ch) // Even though we close the channel here, we still have + err = <-errCh // to wait before we can return and close the connection. + if err != nil { + return dns.RcodeServerFailure, err + } + + logserial := uint32(0) + if soa != nil { + logserial = soa.Serial + } + log.Infof("Outgoing transfer of %d records of zone %q to %s for %d SOA serial", l, state.QName(), state.IP(), logserial) + return 0, nil +} + +func (x xfr) allowed(state request.Request) bool { + for _, h := range x.to { + if h == "*" { + return true + } + to, _, err := net.SplitHostPort(h) + if err != nil { + return false + } + // If remote IP matches we accept. TODO(): make this works with ranges + if to == state.IP() { + return true + } + } + return false +} + +// Find the first transfer instance for which the queried zone is the longest match. When nothing +// is found nil is returned. +func longestMatch(xfrs []*xfr, name string) *xfr { + // TODO(xxx): optimize and make it a map (or maps) + var x *xfr + zone := "" // longest zone match wins + for _, xfr := range xfrs { + if z := plugin.Zones(xfr.Zones).Matches(name); z != "" { + if z > zone { + zone = z + x = xfr + } + } + } + return x +} + +// Name implements the Handler interface. +func (Transfer) Name() string { return "transfer" } diff --git a/ag_201_coredns/plugin/transfer/transfer_test.go b/ag_201_coredns/plugin/transfer/transfer_test.go new file mode 100644 index 0000000..79233d1 --- /dev/null +++ b/ag_201_coredns/plugin/transfer/transfer_test.go @@ -0,0 +1,278 @@ +package transfer + +import ( + "context" + "fmt" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +// transfererPlugin implements transfer.Transferer and plugin.Handler. +type transfererPlugin struct { + Zone string + Serial uint32 + Next plugin.Handler +} + +// Name implements plugin.Handler. +func (*transfererPlugin) Name() string { return "transfererplugin" } + +// ServeDNS implements plugin.Handler. +func (p *transfererPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if r.Question[0].Name != p.Zone { + return p.Next.ServeDNS(ctx, w, r) + } + return 0, nil +} + +// Transfer implements transfer.Transferer - it returns a static AXFR response, or +// if serial is current, an abbreviated IXFR response. +func (p *transfererPlugin) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) { + if zone != p.Zone { + return nil, ErrNotAuthoritative + } + ch := make(chan []dns.RR, 3) // sending 3 bits and don't want to block, nor do a waitgroup + defer close(ch) + ch <- []dns.RR{test.SOA(fmt.Sprintf("%s 100 IN SOA ns.dns.%s hostmaster.%s %d 7200 1800 86400 100", p.Zone, p.Zone, p.Zone, p.Serial))} + if serial >= p.Serial { + return ch, nil + } + ch <- []dns.RR{ + test.NS(fmt.Sprintf("%s 100 IN NS ns.dns.%s", p.Zone, p.Zone)), + test.A(fmt.Sprintf("ns.dns.%s 100 IN A 1.2.3.4", p.Zone)), + } + ch <- []dns.RR{test.SOA(fmt.Sprintf("%s 100 IN SOA ns.dns.%s hostmaster.%s %d 7200 1800 86400 100", p.Zone, p.Zone, p.Zone, p.Serial))} + return ch, nil +} + +type terminatingPlugin struct{} + +// Name implements plugin.Handler. +func (*terminatingPlugin) Name() string { return "testplugin" } + +// ServeDNS implements plugin.Handler that returns NXDOMAIN for all requests. +func (*terminatingPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + m := new(dns.Msg) + m.SetRcode(r, dns.RcodeNameError) + w.WriteMsg(m) + return dns.RcodeNameError, nil +} + +func newTestTransfer() *Transfer { + nextPlugin1 := transfererPlugin{Zone: "example.com.", Serial: 12345} + nextPlugin2 := transfererPlugin{Zone: "example.org.", Serial: 12345} + nextPlugin2.Next = &terminatingPlugin{} + nextPlugin1.Next = &nextPlugin2 + + transfer := &Transfer{ + Transferers: []Transferer{&nextPlugin1, &nextPlugin2}, + xfrs: []*xfr{ + { + Zones: []string{"example.org."}, + to: []string{"*"}, + }, + { + Zones: []string{"example.com."}, + to: []string{"*"}, + }, + }, + Next: &nextPlugin1, + } + return transfer +} + +func TestTransferNonZone(t *testing.T) { + transfer := newTestTransfer() + ctx := context.TODO() + + for _, tc := range []string{"sub.example.org.", "example.test."} { + w := dnstest.NewRecorder(&test.ResponseWriter{TCP: true}) + m := &dns.Msg{} + m.SetAxfr(tc) + + _, err := transfer.ServeDNS(ctx, w, m) + if err != nil { + t.Error(err) + } + + if w.Msg == nil { + t.Fatalf("Got nil message for AXFR %s", tc) + } + + if w.Msg.Rcode != dns.RcodeNameError { + t.Errorf("Expected NXDOMAIN for AXFR %s got %s", tc, dns.RcodeToString[w.Msg.Rcode]) + } + } +} + +func TestTransferNotAXFRorIXFR(t *testing.T) { + transfer := newTestTransfer() + + ctx := context.TODO() + w := dnstest.NewRecorder(&test.ResponseWriter{TCP: true}) + m := &dns.Msg{} + m.SetQuestion("test.domain.", dns.TypeA) + + _, err := transfer.ServeDNS(ctx, w, m) + if err != nil { + t.Error(err) + } + + if w.Msg == nil { + t.Fatal("Got nil message") + } + + if w.Msg.Rcode != dns.RcodeNameError { + t.Errorf("Expected NXDOMAIN got %s", dns.RcodeToString[w.Msg.Rcode]) + } +} + +func TestTransferAXFRExampleOrg(t *testing.T) { + transfer := newTestTransfer() + + ctx := context.TODO() + w := dnstest.NewMultiRecorder(&test.ResponseWriter{TCP: true}) + m := &dns.Msg{} + m.SetAxfr(transfer.xfrs[0].Zones[0]) + + _, err := transfer.ServeDNS(ctx, w, m) + if err != nil { + t.Error(err) + } + + validateAXFRResponse(t, w) +} + +func TestTransferAXFRExampleCom(t *testing.T) { + transfer := newTestTransfer() + + ctx := context.TODO() + w := dnstest.NewMultiRecorder(&test.ResponseWriter{TCP: true}) + m := &dns.Msg{} + m.SetAxfr(transfer.xfrs[1].Zones[0]) + + _, err := transfer.ServeDNS(ctx, w, m) + if err != nil { + t.Error(err) + } + + validateAXFRResponse(t, w) +} + +func TestTransferIXFRCurrent(t *testing.T) { + transfer := newTestTransfer() + + testPlugin := transfer.Transferers[0].(*transfererPlugin) + + ctx := context.TODO() + w := dnstest.NewMultiRecorder(&test.ResponseWriter{TCP: true}) + m := &dns.Msg{} + m.SetIxfr(transfer.xfrs[0].Zones[0], testPlugin.Serial, "ns.dns."+testPlugin.Zone, "hostmaster.dns."+testPlugin.Zone) + + _, err := transfer.ServeDNS(ctx, w, m) + if err != nil { + t.Error(err) + } + + if len(w.Msgs) == 0 { + t.Fatal("Did not get back a zone response") + } + + if len(w.Msgs[0].Answer) != 1 { + t.Logf("%+v\n", w) + t.Fatalf("Expected 1 answer, got %d", len(w.Msgs[0].Answer)) + } + + // Ensure the answer is the SOA + if w.Msgs[0].Answer[0].Header().Rrtype != dns.TypeSOA { + t.Error("Answer does not contain the SOA record") + } +} + +func TestTransferIXFRFallback(t *testing.T) { + transfer := newTestTransfer() + + testPlugin := transfer.Transferers[0].(*transfererPlugin) + + ctx := context.TODO() + w := dnstest.NewMultiRecorder(&test.ResponseWriter{TCP: true}) + m := &dns.Msg{} + m.SetIxfr( + transfer.xfrs[0].Zones[0], + testPlugin.Serial-1, + "ns.dns."+testPlugin.Zone, + "hostmaster.dns."+testPlugin.Zone, + ) + + _, err := transfer.ServeDNS(ctx, w, m) + if err != nil { + t.Error(err) + } + + validateAXFRResponse(t, w) +} + +func validateAXFRResponse(t *testing.T, w *dnstest.MultiRecorder) { + if len(w.Msgs) == 0 { + t.Fatal("Did not get back a zone response") + } + + if len(w.Msgs[0].Answer) == 0 { + t.Logf("%+v\n", w) + t.Fatal("Did not get back an answer") + } + + // Ensure the answer starts with SOA + if w.Msgs[0].Answer[0].Header().Rrtype != dns.TypeSOA { + t.Error("Answer does not start with SOA record") + } + + // Ensure the answer ends with SOA + if w.Msgs[len(w.Msgs)-1].Answer[len(w.Msgs[len(w.Msgs)-1].Answer)-1].Header().Rrtype != dns.TypeSOA { + t.Error("Answer does not end with SOA record") + } + + // Ensure the answer is the expected length + c := 0 + for _, m := range w.Msgs { + c += len(m.Answer) + } + if c != 4 { + t.Errorf("Answer is not the expected length (expected 4, got %d)", c) + } +} + +func TestTransferNotAllowed(t *testing.T) { + nextPlugin := transfererPlugin{Zone: "example.org.", Serial: 12345} + + transfer := Transfer{ + Transferers: []Transferer{&nextPlugin}, + xfrs: []*xfr{ + { + Zones: []string{"example.org."}, + to: []string{"1.2.3.4"}, + }, + }, + Next: &nextPlugin, + } + + ctx := context.TODO() + w := dnstest.NewRecorder(&test.ResponseWriter{TCP: true}) + m := &dns.Msg{} + m.SetAxfr(transfer.xfrs[0].Zones[0]) + + _, err := transfer.ServeDNS(ctx, w, m) + + if err != nil { + t.Error(err) + } + + if w.Msg.Rcode != dns.RcodeRefused { + t.Errorf("Expected REFUSED response code, got %s", dns.RcodeToString[w.Msg.Rcode]) + } +} diff --git a/ag_201_coredns/plugin/tsig/README.md b/ag_201_coredns/plugin/tsig/README.md new file mode 100644 index 0000000..d73b9ca --- /dev/null +++ b/ag_201_coredns/plugin/tsig/README.md @@ -0,0 +1,118 @@ +# tsig + +## Name + +*tsig* - define TSIG keys, validate incoming TSIG signed requests and sign responses. + +## Description + +With *tsig*, you can define CoreDNS's TSIG secret keys. Using those keys, *tsig* validates incoming TSIG requests and signs +responses to those requests. It does not itself sign requests outgoing from CoreDNS; it is up to the +respective plugins sending those requests to sign them using the keys defined by *tsig*. + +The *tsig* plugin can also require that incoming requests be signed for certain query types, refusing requests that do not comply. + +## Syntax + +~~~ +tsig [ZONE...] { + secret NAME KEY + secrets FILE + require [QTYPE...] +} +~~~ + + * **ZONE** - the zones *tsig* will TSIG. By default, the zones from the server block are used. + + * `secret` **NAME** **KEY** - specifies a TSIG secret for **NAME** with **KEY**. Use this option more than once + to define multiple secrets. Secrets are global to the server instance, not just for the enclosing **ZONE**. + + * `secrets` **FILE** - same as `secret`, but load the secrets from a file. The file may define any number + of unique keys, each in the following `named.conf` format: + ```cgo + key "example." { + secret "X28hl0BOfAL5G0jsmJWSacrwn7YRm2f6U5brnzwWEus="; + }; + ``` + Each key may also specify an `algorithm` e.g. `algorithm hmac-sha256;`, but this is currently ignored by the plugin. + + * `require` **QTYPE...** - the query types that must be TSIG'd. Requests of the specified types + will be `REFUSED` if they are not signed.`require all` will require requests of all types to be + signed. `require none` will not require requests any types to be signed. Default behavior is to not require. + +## Examples + +Require TSIG signed transactions for transfer requests to `example.zone`. + +``` +example.zone { + tsig { + secret example.zone.key. NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk= + require AXFR IXFR + } + transfer { + to * + } +} +``` + +Require TSIG signed transactions for all requests to `auth.zone`. + +``` +auth.zone { + tsig { + secret auth.zone.key. NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk= + require all + } + forward . 10.1.0.2 +} +``` + +## Bugs + +### Secondary + +TSIG transfers are not yet implemented for the *secondary* plugin. The *secondary* plugin will not sign its zone transfer requests. + +### Zone Transfer Notifies + +With the *transfer* plugin, zone transfer notifications from CoreDNS are not TSIG signed. + +### Special Considerations for Forwarding Servers (RFC 8945 5.5) + +https://datatracker.ietf.org/doc/html/rfc8945#section-5.5 + +CoreDNS does not implement this section as follows ... + +* 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. + + CoreDNS behavior: +If ths zone of the request matches the _tsig_ plugin zones, then the TSIG record +is always stripped. But even when the _tsig_ plugin is not involved, the _forward_ plugin +may alter the message with compression, which would cause validation failure +at the destination. + + +* 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. + + CoreDNS behavior: +If ths zone of the request matches the _tsig_ plugin zones, _forward_ plugin will +proxy the request upstream without TSIG. + + +* 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. + + CoreDNS behavior: +The AD flag is not cleared. diff --git a/ag_201_coredns/plugin/tsig/setup.go b/ag_201_coredns/plugin/tsig/setup.go new file mode 100644 index 0000000..a187a4b --- /dev/null +++ b/ag_201_coredns/plugin/tsig/setup.go @@ -0,0 +1,168 @@ +package tsig + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + + "github.com/miekg/dns" +) + +func init() { + caddy.RegisterPlugin(pluginName, caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + t, err := parse(c) + if err != nil { + return plugin.Error(pluginName, c.ArgErr()) + } + + config := dnsserver.GetConfig(c) + + config.TsigSecret = t.secrets + + config.AddPlugin(func(next plugin.Handler) plugin.Handler { + t.Next = next + return t + }) + + return nil +} + +func parse(c *caddy.Controller) (*TSIGServer, error) { + t := &TSIGServer{ + secrets: make(map[string]string), + types: defaultQTypes, + } + + for i := 0; c.Next(); i++ { + if i > 0 { + return nil, plugin.ErrOnce + } + + t.Zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) + for c.NextBlock() { + switch c.Val() { + case "secret": + args := c.RemainingArgs() + if len(args) != 2 { + return nil, c.ArgErr() + } + k := plugin.Name(args[0]).Normalize() + if _, exists := t.secrets[k]; exists { + return nil, fmt.Errorf("key %q redefined", k) + } + t.secrets[k] = args[1] + case "secrets": + args := c.RemainingArgs() + if len(args) != 1 { + return nil, c.ArgErr() + } + f, err := os.Open(args[0]) + if err != nil { + return nil, err + } + secrets, err := parseKeyFile(f) + if err != nil { + return nil, err + } + for k, s := range secrets { + if _, exists := t.secrets[k]; exists { + return nil, fmt.Errorf("key %q redefined", k) + } + t.secrets[k] = s + } + case "require": + t.types = qTypes{} + args := c.RemainingArgs() + if len(args) == 0 { + return nil, c.ArgErr() + } + if args[0] == "all" { + t.all = true + continue + } + if args[0] == "none" { + continue + } + for _, str := range args { + qt, ok := dns.StringToType[str] + if !ok { + return nil, c.Errf("unknown query type '%s'", str) + } + t.types[qt] = struct{}{} + } + default: + return nil, c.Errf("unknown property '%s'", c.Val()) + } + } + } + return t, nil +} + +func parseKeyFile(f io.Reader) (map[string]string, error) { + secrets := make(map[string]string) + s := bufio.NewScanner(f) + for s.Scan() { + fields := strings.Fields(s.Text()) + if len(fields) == 0 { + continue + } + if fields[0] != "key" { + return nil, fmt.Errorf("unexpected token %q", fields[0]) + } + if len(fields) < 2 { + return nil, fmt.Errorf("expected key name %q", s.Text()) + } + key := strings.Trim(fields[1], "\"{") + if len(key) == 0 { + return nil, fmt.Errorf("expected key name %q", s.Text()) + } + key = plugin.Name(key).Normalize() + if _, ok := secrets[key]; ok { + return nil, fmt.Errorf("key %q redefined", key) + } + key: + for s.Scan() { + fields := strings.Fields(s.Text()) + if len(fields) == 0 { + continue + } + switch fields[0] { + case "algorithm": + continue + case "secret": + if len(fields) < 2 { + return nil, fmt.Errorf("expected secret key %q", s.Text()) + } + secret := strings.Trim(fields[1], "\";") + if len(secret) == 0 { + return nil, fmt.Errorf("expected secret key %q", s.Text()) + } + secrets[key] = secret + case "}": + fallthrough + case "};": + break key + default: + return nil, fmt.Errorf("unexpected token %q", fields[0]) + } + } + if _, ok := secrets[key]; !ok { + return nil, fmt.Errorf("expected secret for key %q", key) + } + } + return secrets, nil +} + +var defaultQTypes = qTypes{} diff --git a/ag_201_coredns/plugin/tsig/setup_test.go b/ag_201_coredns/plugin/tsig/setup_test.go new file mode 100644 index 0000000..0d74339 --- /dev/null +++ b/ag_201_coredns/plugin/tsig/setup_test.go @@ -0,0 +1,245 @@ +package tsig + +import ( + "fmt" + "strings" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestParse(t *testing.T) { + secrets := map[string]string{ + "name.key.": "test-key", + "name2.key.": "test-key-2", + } + secretConfig := "" + for k, s := range secrets { + secretConfig += fmt.Sprintf("secret %s %s\n", k, s) + } + secretsFile, cleanup, err := test.TempFile(".", `key "name.key." { + secret "test-key"; +}; +key "name2.key." { + secret "test-key2"; +};`) + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer cleanup() + + tests := []struct { + input string + shouldErr bool + expectedZones []string + expectedQTypes qTypes + expectedSecrets map[string]string + expectedAll bool + }{ + { + input: "tsig {\n " + secretConfig + "}", + expectedZones: []string{"."}, + expectedQTypes: defaultQTypes, + expectedSecrets: secrets, + }, + { + input: "tsig {\n secrets " + secretsFile + "\n}", + expectedZones: []string{"."}, + expectedQTypes: defaultQTypes, + expectedSecrets: secrets, + }, + { + input: "tsig example.com {\n " + secretConfig + "}", + expectedZones: []string{"example.com."}, + expectedQTypes: defaultQTypes, + expectedSecrets: secrets, + }, + { + input: "tsig {\n " + secretConfig + " require all \n}", + expectedZones: []string{"."}, + expectedQTypes: qTypes{}, + expectedAll: true, + expectedSecrets: secrets, + }, + { + input: "tsig {\n " + secretConfig + " require none \n}", + expectedZones: []string{"."}, + expectedQTypes: qTypes{}, + expectedAll: false, + expectedSecrets: secrets, + }, + { + input: "tsig {\n " + secretConfig + " \n require A AAAA \n}", + expectedZones: []string{"."}, + expectedQTypes: qTypes{dns.TypeA: {}, dns.TypeAAAA: {}}, + expectedSecrets: secrets, + }, + { + input: "tsig {\n blah \n}", + shouldErr: true, + }, + { + input: "tsig {\n secret name. too many parameters \n}", + shouldErr: true, + }, + { + input: "tsig {\n require \n}", + shouldErr: true, + }, + { + input: "tsig {\n require invalid-qtype \n}", + shouldErr: true, + }, + } + + serverBlockKeys := []string{"."} + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + c.ServerBlockKeys = serverBlockKeys + ts, err := parse(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) + } + + if test.shouldErr { + continue + } + + if len(test.expectedZones) != len(ts.Zones) { + t.Fatalf("Test %d expected zones '%v', but got '%v'.", i, test.expectedZones, ts.Zones) + } + for j := range test.expectedZones { + if test.expectedZones[j] != ts.Zones[j] { + t.Errorf("Test %d expected zones '%v', but got '%v'.", i, test.expectedZones, ts.Zones) + break + } + } + + if test.expectedAll != ts.all { + t.Errorf("Test %d expected require all to be '%v', but got '%v'.", i, test.expectedAll, ts.all) + } + + if len(test.expectedQTypes) != len(ts.types) { + t.Fatalf("Test %d expected required types '%v', but got '%v'.", i, test.expectedQTypes, ts.types) + } + for qt := range test.expectedQTypes { + if _, ok := ts.types[qt]; !ok { + t.Errorf("Test %d required types '%v', but got '%v'.", i, test.expectedQTypes, ts.types) + break + } + } + + if len(test.expectedSecrets) != len(ts.secrets) { + t.Fatalf("Test %d expected secrets '%v', but got '%v'.", i, test.expectedSecrets, ts.secrets) + } + for qt := range test.expectedSecrets { + secret, ok := ts.secrets[qt] + if !ok { + t.Errorf("Test %d required secrets '%v', but got '%v'.", i, test.expectedSecrets, ts.secrets) + break + } + if secret != ts.secrets[qt] { + t.Errorf("Test %d required secrets '%v', but got '%v'.", i, test.expectedSecrets, ts.secrets) + break + } + } + } +} + +func TestParseKeyFile(t *testing.T) { + var reader = strings.NewReader(`key "foo" { + algorithm hmac-sha256; + secret "36eowrtmxceNA3T5AdE+JNUOWFCw3amtcyHACnrDVgQ="; +}; +key "bar" { + algorithm hmac-sha256; + secret "X28hl0BOfAL5G0jsmJWSacrwn7YRm2f6U5brnzwWEus="; +}; +key "baz" { + secret "BycDPXSx/5YCD44Q4g5Nd2QNxNRDKwWTXddrU/zpIQM="; +};`) + + secrets, err := parseKeyFile(reader) + if err != nil { + t.Fatalf("Unexpected error: %q", err) + } + expectedSecrets := map[string]string{ + "foo.": "36eowrtmxceNA3T5AdE+JNUOWFCw3amtcyHACnrDVgQ=", + "bar.": "X28hl0BOfAL5G0jsmJWSacrwn7YRm2f6U5brnzwWEus=", + "baz.": "BycDPXSx/5YCD44Q4g5Nd2QNxNRDKwWTXddrU/zpIQM=", + } + + if len(secrets) != len(expectedSecrets) { + t.Fatalf("result has %d keys. expected %d", len(secrets), len(expectedSecrets)) + } + + for k, sec := range secrets { + expectedSec, ok := expectedSecrets[k] + if !ok { + t.Errorf("unexpected key in result. %q", k) + continue + } + if sec != expectedSec { + t.Errorf("incorrect secret in result for key %q. expected %q got %q ", k, expectedSec, sec) + } + } +} + +func TestParseKeyFileErrors(t *testing.T) { + tests := []struct { + in string + err string + }{ + {in: `key {`, err: "expected key name \"key {\""}, + {in: `foo "key" {`, err: "unexpected token \"foo\""}, + { + in: `key "foo" { + secret "36eowrtmxceNA3T5AdE+JNUOWFCw3amtcyHACnrDVgQ="; + }; + key "foo" { + secret "X28hl0BOfAL5G0jsmJWSacrwn7YRm2f6U5brnzwWEus="; + }; `, + err: "key \"foo.\" redefined", + }, + {in: `key "foo" { + schmalgorithm hmac-sha256;`, + err: "unexpected token \"schmalgorithm\"", + }, + { + in: `key "foo" { + schmecret "36eowrtmxceNA3T5AdE+JNUOWFCw3amtcyHACnrDVgQ=";`, + err: "unexpected token \"schmecret\"", + }, + { + in: `key "foo" { + secret`, + err: "expected secret key \"\\tsecret\"", + }, + { + in: `key "foo" { + secret ;`, + err: "expected secret key \"\\tsecret ;\"", + }, + { + in: `key "foo" { + };`, + err: "expected secret for key \"foo.\"", + }, + } + for i, testcase := range tests { + _, err := parseKeyFile(strings.NewReader(testcase.in)) + if err == nil { + t.Errorf("Test %d: expected error, got no error", i) + continue + } + if err.Error() != testcase.err { + t.Errorf("Test %d: Expected error: %q, got %q", i, testcase.err, err.Error()) + } + } +} diff --git a/ag_201_coredns/plugin/tsig/tsig.go b/ag_201_coredns/plugin/tsig/tsig.go new file mode 100644 index 0000000..6441c8a --- /dev/null +++ b/ag_201_coredns/plugin/tsig/tsig.go @@ -0,0 +1,140 @@ +package tsig + +import ( + "context" + "encoding/binary" + "encoding/hex" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// TSIGServer verifies tsig status and adds tsig to responses +type TSIGServer struct { + Zones []string + secrets map[string]string // [key-name]secret + types qTypes + all bool + Next plugin.Handler +} + +type qTypes map[uint16]struct{} + +// Name implements plugin.Handler +func (t TSIGServer) Name() string { return pluginName } + +// ServeDNS implements plugin.Handler +func (t *TSIGServer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + var err error + state := request.Request{Req: r, W: w} + if z := plugin.Zones(t.Zones).Matches(state.Name()); z == "" { + return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r) + } + + var tsigRR = r.IsTsig() + rcode := dns.RcodeSuccess + if !t.tsigRequired(state.QType()) && tsigRR == nil { + return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r) + } + + if tsigRR == nil { + log.Debugf("rejecting '%s' request without TSIG\n", dns.TypeToString[state.QType()]) + rcode = dns.RcodeRefused + } + + // wrap the response writer so the response will be TSIG signed. + w = &restoreTsigWriter{w, r, tsigRR} + + tsigStatus := w.TsigStatus() + if tsigStatus != nil { + log.Debugf("TSIG validation failed: %v %v", dns.TypeToString[state.QType()], tsigStatus) + rcode = dns.RcodeNotAuth + switch tsigStatus { + case dns.ErrSecret: + tsigRR.Error = dns.RcodeBadKey + case dns.ErrTime: + tsigRR.Error = dns.RcodeBadTime + default: + tsigRR.Error = dns.RcodeBadSig + } + resp := new(dns.Msg).SetRcode(r, rcode) + w.WriteMsg(resp) + return dns.RcodeSuccess, nil + } + + // strip the TSIG RR. Next, and subsequent plugins will not see the TSIG RRs. + // This violates forwarding cases (RFC 8945 5.5). See README.md Bugs + if len(r.Extra) > 1 { + r.Extra = r.Extra[0 : len(r.Extra)-1] + } else { + r.Extra = []dns.RR{} + } + + if rcode == dns.RcodeSuccess { + rcode, err = plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r) + if err != nil { + log.Errorf("request handler returned an error: %v\n", err) + } + } + // If the plugin chain result was not an error, restore the TSIG and write the response. + if !plugin.ClientWrite(rcode) { + resp := new(dns.Msg).SetRcode(r, rcode) + w.WriteMsg(resp) + } + return dns.RcodeSuccess, nil +} + +func (t *TSIGServer) tsigRequired(qtype uint16) bool { + if t.all { + return true + } + if _, ok := t.types[qtype]; ok { + return true + } + return false +} + +// restoreTsigWriter Implement Response Writer, and adds a TSIG RR to a response +type restoreTsigWriter struct { + dns.ResponseWriter + req *dns.Msg // original request excluding TSIG if it has one + reqTSIG *dns.TSIG // original TSIG +} + +// WriteMsg adds a TSIG RR to the response +func (r *restoreTsigWriter) WriteMsg(m *dns.Msg) error { + // Make sure the response has an EDNS OPT RR if the request had it. + // Otherwise ScrubWriter would append it *after* TSIG, making it a non-compliant DNS message. + state := request.Request{Req: r.req, W: r.ResponseWriter} + state.SizeAndDo(m) + + repTSIG := m.IsTsig() + if r.reqTSIG != nil && repTSIG == nil { + repTSIG = new(dns.TSIG) + repTSIG.Hdr = dns.RR_Header{Name: r.reqTSIG.Hdr.Name, Rrtype: dns.TypeTSIG, Class: dns.ClassANY} + repTSIG.Algorithm = r.reqTSIG.Algorithm + repTSIG.OrigId = m.MsgHdr.Id + repTSIG.Error = r.reqTSIG.Error + repTSIG.MAC = r.reqTSIG.MAC + repTSIG.MACSize = r.reqTSIG.MACSize + if repTSIG.Error == dns.RcodeBadTime { + // per RFC 8945 5.2.3. client time goes into TimeSigned, server time in OtherData, OtherLen = 6 ... + repTSIG.TimeSigned = r.reqTSIG.TimeSigned + b := make([]byte, 8) + // TimeSigned is network byte order. + binary.BigEndian.PutUint64(b, uint64(time.Now().Unix())) + // truncate to 48 least significant bits (network order 6 rightmost bytes) + repTSIG.OtherData = hex.EncodeToString(b[2:]) + repTSIG.OtherLen = 6 + } + m.Extra = append(m.Extra, repTSIG) + } + + return r.ResponseWriter.WriteMsg(m) +} + +const pluginName = "tsig" diff --git a/ag_201_coredns/plugin/tsig/tsig_test.go b/ag_201_coredns/plugin/tsig/tsig_test.go new file mode 100644 index 0000000..f7ec1fd --- /dev/null +++ b/ag_201_coredns/plugin/tsig/tsig_test.go @@ -0,0 +1,255 @@ +package tsig + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +func TestServeDNS(t *testing.T) { + cases := []struct { + zones []string + reqTypes qTypes + qType uint16 + qTsig, all bool + expectRcode int + expectTsig bool + statusError bool + }{ + { + zones: []string{"."}, + all: true, + qType: dns.TypeA, + qTsig: true, + expectRcode: dns.RcodeSuccess, + expectTsig: true, + }, + { + zones: []string{"."}, + all: true, + qType: dns.TypeA, + qTsig: false, + expectRcode: dns.RcodeRefused, + expectTsig: false, + }, + { + zones: []string{"another.domain."}, + all: true, + qType: dns.TypeA, + qTsig: false, + expectRcode: dns.RcodeSuccess, + expectTsig: false, + }, + { + zones: []string{"another.domain."}, + all: true, + qType: dns.TypeA, + qTsig: true, + expectRcode: dns.RcodeSuccess, + expectTsig: false, + }, + { + zones: []string{"."}, + reqTypes: qTypes{dns.TypeAXFR: {}}, + qType: dns.TypeAXFR, + qTsig: true, + expectRcode: dns.RcodeSuccess, + expectTsig: true, + }, + { + zones: []string{"."}, + reqTypes: qTypes{}, + qType: dns.TypeA, + qTsig: false, + expectRcode: dns.RcodeSuccess, + expectTsig: false, + }, + { + zones: []string{"."}, + reqTypes: qTypes{}, + qType: dns.TypeA, + qTsig: true, + expectRcode: dns.RcodeSuccess, + expectTsig: true, + }, + { + zones: []string{"."}, + all: true, + qType: dns.TypeA, + qTsig: true, + expectRcode: dns.RcodeNotAuth, + expectTsig: true, + statusError: true, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + tsig := TSIGServer{ + Zones: tc.zones, + all: tc.all, + types: tc.reqTypes, + Next: testHandler(), + } + + ctx := context.TODO() + + var w *dnstest.Recorder + if tc.statusError { + w = dnstest.NewRecorder(&ErrWriter{err: dns.ErrSig}) + } else { + w = dnstest.NewRecorder(&test.ResponseWriter{}) + } + r := new(dns.Msg) + r.SetQuestion("test.example.", tc.qType) + if tc.qTsig { + r.SetTsig("test.key.", dns.HmacSHA256, 300, time.Now().Unix()) + } + + _, err := tsig.ServeDNS(ctx, w, r) + if err != nil { + t.Fatal(err) + } + + if w.Msg.Rcode != tc.expectRcode { + t.Fatalf("expected rcode %v, got %v", tc.expectRcode, w.Msg.Rcode) + } + + if ts := w.Msg.IsTsig(); ts == nil && tc.expectTsig { + t.Fatal("expected TSIG in response") + } + if ts := w.Msg.IsTsig(); ts != nil && !tc.expectTsig { + t.Fatal("expected no TSIG in response") + } + }) + } +} + +func TestServeDNSTsigErrors(t *testing.T) { + clientNow := time.Now().Unix() + + cases := []struct { + desc string + tsigErr error + expectRcode int + expectError int + expectOtherLength int + expectTimeSigned int64 + }{ + { + desc: "Unknown Key", + tsigErr: dns.ErrSecret, + expectRcode: dns.RcodeNotAuth, + expectError: dns.RcodeBadKey, + expectOtherLength: 0, + expectTimeSigned: 0, + }, + { + desc: "Bad Signature", + tsigErr: dns.ErrSig, + expectRcode: dns.RcodeNotAuth, + expectError: dns.RcodeBadSig, + expectOtherLength: 0, + expectTimeSigned: 0, + }, + { + desc: "Bad Time", + tsigErr: dns.ErrTime, + expectRcode: dns.RcodeNotAuth, + expectError: dns.RcodeBadTime, + expectOtherLength: 6, + expectTimeSigned: clientNow, + }, + } + + tsig := TSIGServer{ + Zones: []string{"."}, + all: true, + Next: testHandler(), + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + ctx := context.TODO() + + var w *dnstest.Recorder + + w = dnstest.NewRecorder(&ErrWriter{err: tc.tsigErr}) + + r := new(dns.Msg) + r.SetQuestion("test.example.", dns.TypeA) + r.SetTsig("test.key.", dns.HmacSHA256, 300, clientNow) + + // set a fake MAC and Size in request + rtsig := r.IsTsig() + rtsig.MAC = "0123456789012345678901234567890101234567890123456789012345678901" + rtsig.MACSize = 32 + + _, err := tsig.ServeDNS(ctx, w, r) + if err != nil { + t.Fatal(err) + } + + if w.Msg.Rcode != tc.expectRcode { + t.Fatalf("expected rcode %v, got %v", tc.expectRcode, w.Msg.Rcode) + } + + ts := w.Msg.IsTsig() + + if ts == nil { + t.Fatal("expected TSIG in response") + } + + if int(ts.Error) != tc.expectError { + t.Errorf("expected TSIG error code %v, got %v", tc.expectError, ts.Error) + } + + if len(ts.OtherData)/2 != tc.expectOtherLength { + t.Errorf("expected Other of length %v, got %v", tc.expectOtherLength, len(ts.OtherData)) + } + + if int(ts.OtherLen) != tc.expectOtherLength { + t.Errorf("expected OtherLen %v, got %v", tc.expectOtherLength, ts.OtherLen) + } + + if ts.TimeSigned != uint64(tc.expectTimeSigned) { + t.Errorf("expected TimeSigned to be %v, got %v", tc.expectTimeSigned, ts.TimeSigned) + } + }) + } +} + +func testHandler() test.HandlerFunc { + return func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.Name() + m := new(dns.Msg) + rcode := dns.RcodeServerFailure + if qname == "test.example." { + m.SetReply(r) + rr := test.A("test.example. 300 IN A 1.2.3.48") + m.Answer = []dns.RR{rr} + m.Authoritative = true + rcode = dns.RcodeSuccess + } + m.SetRcode(r, rcode) + w.WriteMsg(m) + return rcode, nil + } +} + +// a test.ResponseWriter that always returns err as the TSIG status error +type ErrWriter struct { + err error + test.ResponseWriter +} + +// TsigStatus always returns an error. +func (t *ErrWriter) TsigStatus() error { return t.err } diff --git a/ag_201_coredns/plugin/view/README.md b/ag_201_coredns/plugin/view/README.md new file mode 100644 index 0000000..8522727 --- /dev/null +++ b/ag_201_coredns/plugin/view/README.md @@ -0,0 +1,135 @@ +# view + +## Name + +*view* - defines conditions that must be met for a DNS request to be routed to the server block. + +## Description + +*view* 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. + +## Syntax +``` +view NAME { + expr EXPRESSION +} +``` + +* `view` **NAME** - The name of the view used by metrics and exported as metadata for requests that match the + view's expression +* `expr` **EXPRESSION** - CoreDNS will only route incoming queries to the enclosing server block + if the **EXPRESSION** evaluates to true. See the **Expressions** section for available variables and functions. + If multiple instances of view are defined, all **EXPRESSION** must evaluate to true for CoreDNS will only route + incoming queries to the enclosing server block. + +For expression syntax and examples, see the Expressions and Examples sections. + +## Examples + +Implement CIDR based split DNS routing. This will return a different +answer for `test.` depending on client's IP address. It returns ... +* `test. 3600 IN A 1.1.1.1`, for queries with a source address in 127.0.0.0/24 +* `test. 3600 IN A 2.2.2.2`, for queries with a source address in 192.168.0.0/16 +* `test. 3600 IN A 3.3.3.3`, for all others + +``` +. { + 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 + } +} + +. { + hosts { + 3.3.3.3 test + } +} +``` + +Send all `A` and `AAAA` requests to `10.0.0.6`, and all other requests to `10.0.0.1`. + +``` +. { + view example { + expr type() in ['A', 'AAAA'] + } + forward . 10.0.0.6 +} + +. { + forward . 10.0.0.1 +} +``` + +Send all requests for `abc.*.example.com` (where * can be any number of labels), to `10.0.0.2`, and all other +requests to `10.0.0.1`. +Note that the regex pattern is enclosed in single quotes, and backslashes are escaped with backslashes. + +``` +. { + view example { + expr name() matches '^abc\\..*\\.example\\.com\\.$' + } + forward . 10.0.0.2 +} + +. { + forward . 10.0.0.1 +} +``` + +## Expressions + +To evaluate expressions, *view* uses the antonmedv/expr package (https://github.com/antonmedv/expr). +For example, an expression could look like: +`(type() == 'A' && name() == 'example.com') || client_ip() == '1.2.3.4'`. + +All expressions should be written to evaluate to a boolean value. + +See https://github.com/antonmedv/expr/blob/master/docs/Language-Definition.md as a detailed reference for valid syntax. + +### Available Expression Functions + +In the context of the *view* plugin, expressions can reference DNS query information by using utility +functions defined below. + +#### DNS Query Functions + +* `bufsize() int`: the EDNS0 buffer size advertised in the query +* `class() string`: class of the request (IN, CH, ...) +* `client_ip() string`: client's IP address, for IPv6 addresses these are enclosed in brackets: `[::1]` +* `do() bool`: the EDNS0 DO (DNSSEC OK) bit set in the query +* `id() int`: query ID +* `name() string`: name of the request (the domain name requested) +* `opcode() int`: query OPCODE +* `port() string`: client's port +* `proto() string`: protocol used (tcp or udp) +* `server_ip() string`: server's IP address; for IPv6 addresses these are enclosed in brackets: `[::1]` +* `server_port() string` : server's port +* `size() int`: request size in bytes +* `type() string`: type of the request (A, AAAA, TXT, ...) + +#### Utility Functions + +* `incidr(ip string, cidr string) bool`: returns true if _ip_ is within _cidr_ +* `metadata(label string)` - returns the value for the metadata matching _label_ + +## Metadata + +The view plugin will publish the following metadata, if the *metadata* +plugin is also enabled: + +* `view/name`: the name of the view handling the current request diff --git a/ag_201_coredns/plugin/view/metadata.go b/ag_201_coredns/plugin/view/metadata.go new file mode 100644 index 0000000..6ee9bc0 --- /dev/null +++ b/ag_201_coredns/plugin/view/metadata.go @@ -0,0 +1,16 @@ +package view + +import ( + "context" + + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/request" +) + +// Metadata implements the metadata.Provider interface. +func (v *View) Metadata(ctx context.Context, state request.Request) context.Context { + metadata.SetValueFunc(ctx, "view/name", func() string { + return v.viewName + }) + return ctx +} diff --git a/ag_201_coredns/plugin/view/setup.go b/ag_201_coredns/plugin/view/setup.go new file mode 100644 index 0000000..34ecc79 --- /dev/null +++ b/ag_201_coredns/plugin/view/setup.go @@ -0,0 +1,65 @@ +package view + +import ( + "context" + "strings" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/expression" + + "github.com/antonmedv/expr" +) + +func init() { plugin.Register("view", setup) } + +func setup(c *caddy.Controller) error { + cond, err := parse(c) + if err != nil { + return plugin.Error("view", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + cond.Next = next + return cond + }) + + return nil +} + +func parse(c *caddy.Controller) (*View, error) { + v := new(View) + + i := 0 + for c.Next() { + i++ + if i > 1 { + return nil, plugin.ErrOnce + } + args := c.RemainingArgs() + if len(args) != 1 { + return nil, c.ArgErr() + } + v.viewName = args[0] + + for c.NextBlock() { + switch c.Val() { + case "expr": + args := c.RemainingArgs() + prog, err := expr.Compile(strings.Join(args, " "), expr.Env(expression.DefaultEnv(context.Background(), nil))) + if err != nil { + return v, err + } + v.progs = append(v.progs, prog) + if err != nil { + return nil, err + } + continue + default: + return nil, c.Errf("unknown property '%s'", c.Val()) + } + } + } + return v, nil +} diff --git a/ag_201_coredns/plugin/view/setup_test.go b/ag_201_coredns/plugin/view/setup_test.go new file mode 100644 index 0000000..7c78380 --- /dev/null +++ b/ag_201_coredns/plugin/view/setup_test.go @@ -0,0 +1,38 @@ +package view + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + tests := []struct { + input string + shouldErr bool + progCount int + }{ + {"view example {\n expr name() == 'example.com.'\n}", false, 1}, + {"view example {\n expr incidr(client_ip(), '10.0.0.0/24')\n}", false, 1}, + {"view example {\n expr name() == 'example.com.'\n expr name() == 'example2.com.'\n}", false, 2}, + {"view", true, 0}, + {"view example {\n expr invalid expression\n}", true, 0}, + } + + for i, test := range tests { + v, err := parse(caddy.NewTestController("dns", test.input)) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found none for input %s", i, test.input) + } + if err != nil && !test.shouldErr { + t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) + } + if test.shouldErr { + continue + } + if test.progCount != len(v.progs) { + t.Errorf("Test %d: Expected prog length %d, but got %d for %s.", i, test.progCount, len(v.progs), test.input) + } + } +} diff --git a/ag_201_coredns/plugin/view/view.go b/ag_201_coredns/plugin/view/view.go new file mode 100644 index 0000000..448a63a --- /dev/null +++ b/ag_201_coredns/plugin/view/view.go @@ -0,0 +1,48 @@ +package view + +import ( + "context" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/expression" + "github.com/coredns/coredns/request" + + "github.com/antonmedv/expr" + "github.com/antonmedv/expr/vm" + "github.com/miekg/dns" +) + +// View is a plugin that enables configuring expression based advanced routing +type View struct { + progs []*vm.Program + viewName string + Next plugin.Handler +} + +// Filter implements dnsserver.Viewer. It returns true if all View rules evaluate to true for the given state. +func (v *View) Filter(ctx context.Context, state *request.Request) bool { + env := expression.DefaultEnv(ctx, state) + for _, prog := range v.progs { + result, err := expr.Run(prog, env) + if err != nil { + return false + } + if b, ok := result.(bool); ok && b { + continue + } + // anything other than a boolean true result is considered false + return false + } + return true +} + +// ViewName implements dnsserver.Viewer. It returns the view name +func (v *View) ViewName() string { return v.viewName } + +// Name implements the Handler interface +func (*View) Name() string { return "view" } + +// ServeDNS implements the Handler interface. +func (v *View) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + return plugin.NextOrFailure(v.Name(), v.Next, ctx, w, r) +} diff --git a/ag_201_coredns/plugin/whoami/README.md b/ag_201_coredns/plugin/whoami/README.md new file mode 100644 index 0000000..55d0388 --- /dev/null +++ b/ag_201_coredns/plugin/whoami/README.md @@ -0,0 +1,58 @@ +# whoami + +## Name + +*whoami* - returns your resolver's local IP address, port and transport. + +## Description + +The *whoami* plugin is not really that useful, but can be used for having a simple (fast) endpoint +to test clients against. When *whoami* returns a response it will have your client's IP address in +the additional section as either an A or AAAA record. + +The reply always has an empty answer section. The port and transport are included in the additional +section as a SRV record, transport can be "tcp" or "udp". + +~~~ txt +._.qname. 0 IN SRV 0 0 . +~~~ + +The *whoami* plugin will respond to every A or AAAA query, regardless of the query name. + +If CoreDNS can't find a Corefile on startup this is the _default_ plugin that gets loaded. As such +it can be used to check that CoreDNS is responding to queries. Other than that this plugin is of +limited use in production. + +## Syntax + +~~~ txt +whoami +~~~ + +## Examples + +Start a server on the default port and load the *whoami* plugin. + +~~~ corefile +example.org { + whoami +} +~~~ + +When queried for "example.org A", CoreDNS will respond with: + +~~~ txt +;; QUESTION SECTION: +;example.org. IN A + +;; ADDITIONAL SECTION: +example.org. 0 IN A 10.240.0.1 +_udp.example.org. 0 IN SRV 0 0 40212 +~~~ + +## See Also + +[Read the blog post][blog] on how this plugin is built, or [explore the source code][code]. + +[blog]: https://coredns.io/2017/03/01/how-to-add-plugins-to-coredns/ +[code]: https://github.com/coredns/coredns/blob/master/plugin/whoami/ diff --git a/ag_201_coredns/plugin/whoami/fuzz.go b/ag_201_coredns/plugin/whoami/fuzz.go new file mode 100644 index 0000000..0525398 --- /dev/null +++ b/ag_201_coredns/plugin/whoami/fuzz.go @@ -0,0 +1,13 @@ +//go:build gofuzz + +package whoami + +import ( + "github.com/coredns/coredns/plugin/pkg/fuzz" +) + +// Fuzz fuzzes cache. +func Fuzz(data []byte) int { + w := Whoami{} + return fuzz.Do(w, data) +} diff --git a/ag_201_coredns/plugin/whoami/log_test.go b/ag_201_coredns/plugin/whoami/log_test.go new file mode 100644 index 0000000..460c11c --- /dev/null +++ b/ag_201_coredns/plugin/whoami/log_test.go @@ -0,0 +1,5 @@ +package whoami + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/plugin/whoami/setup.go b/ag_201_coredns/plugin/whoami/setup.go new file mode 100644 index 0000000..1602740 --- /dev/null +++ b/ag_201_coredns/plugin/whoami/setup.go @@ -0,0 +1,22 @@ +package whoami + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("whoami", setup) } + +func setup(c *caddy.Controller) error { + c.Next() // 'whoami' + if c.NextArg() { + return plugin.Error("whoami", c.ArgErr()) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return Whoami{} + }) + + return nil +} diff --git a/ag_201_coredns/plugin/whoami/setup_test.go b/ag_201_coredns/plugin/whoami/setup_test.go new file mode 100644 index 0000000..18a5b94 --- /dev/null +++ b/ag_201_coredns/plugin/whoami/setup_test.go @@ -0,0 +1,19 @@ +package whoami + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", `whoami`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `whoami example.org`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } +} diff --git a/ag_201_coredns/plugin/whoami/whoami.go b/ag_201_coredns/plugin/whoami/whoami.go new file mode 100644 index 0000000..b46736c --- /dev/null +++ b/ag_201_coredns/plugin/whoami/whoami.go @@ -0,0 +1,60 @@ +// Package whoami implements a plugin that returns details about the resolving +// querying it. +package whoami + +import ( + "context" + "net" + "strconv" + + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +const name = "whoami" + +// Whoami is a plugin that returns your IP address, port and the protocol used for connecting +// to CoreDNS. +type Whoami struct{} + +// ServeDNS implements the plugin.Handler interface. +func (wh Whoami) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + + a := new(dns.Msg) + a.SetReply(r) + a.Authoritative = true + + ip := state.IP() + var rr dns.RR + + switch state.Family() { + case 1: + rr = new(dns.A) + rr.(*dns.A).Hdr = dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeA, Class: state.QClass()} + rr.(*dns.A).A = net.ParseIP(ip).To4() + case 2: + rr = new(dns.AAAA) + rr.(*dns.AAAA).Hdr = dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeAAAA, Class: state.QClass()} + rr.(*dns.AAAA).AAAA = net.ParseIP(ip) + } + + srv := new(dns.SRV) + srv.Hdr = dns.RR_Header{Name: "_" + state.Proto() + "." + state.QName(), Rrtype: dns.TypeSRV, Class: state.QClass()} + if state.QName() == "." { + srv.Hdr.Name = "_" + state.Proto() + state.QName() + } + port, _ := strconv.ParseUint(state.Port(), 10, 16) + srv.Port = uint16(port) + srv.Target = "." + + a.Extra = []dns.RR{rr, srv} + + w.WriteMsg(a) + + return 0, nil +} + +// Name implements the Handler interface. +func (wh Whoami) Name() string { return name } diff --git a/ag_201_coredns/plugin/whoami/whoami_test.go b/ag_201_coredns/plugin/whoami/whoami_test.go new file mode 100644 index 0000000..055f267 --- /dev/null +++ b/ag_201_coredns/plugin/whoami/whoami_test.go @@ -0,0 +1,81 @@ +package whoami + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestWhoami(t *testing.T) { + wh := Whoami{} + if wh.Name() != name { + t.Errorf("expected plugin name: %s, got %s", wh.Name(), name) + } + tests := []struct { + qname string + qtype uint16 + remote string + expectedCode int + expectedReply []string // ownernames for the records in the additional section. + expectedErr error + }{ + { + qname: "example.org", + qtype: dns.TypeA, + expectedCode: dns.RcodeSuccess, + expectedReply: []string{"example.org.", "_udp.example.org."}, + expectedErr: nil, + }, + // Case insensitive and case preserving + { + qname: "Example.ORG", + qtype: dns.TypeA, + expectedCode: dns.RcodeSuccess, + expectedReply: []string{"Example.ORG.", "_udp.Example.ORG."}, + expectedErr: nil, + }, + { + qname: "example.org", + qtype: dns.TypeA, + remote: "2003::1/64", + expectedCode: dns.RcodeSuccess, + expectedReply: []string{"example.org.", "_udp.example.org."}, + expectedErr: nil, + }, + { + qname: "Example.ORG", + qtype: dns.TypeA, + remote: "2003::1/64", + expectedCode: dns.RcodeSuccess, + expectedReply: []string{"Example.ORG.", "_udp.Example.ORG."}, + expectedErr: nil, + }, + } + + ctx := context.TODO() + + for i, tc := range tests { + req := new(dns.Msg) + req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype) + rec := dnstest.NewRecorder(&test.ResponseWriter{RemoteIP: tc.remote}) + code, err := wh.ServeDNS(ctx, rec, req) + if err != tc.expectedErr { + t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expectedErr, err) + } + if code != int(tc.expectedCode) { + t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code) + } + if len(tc.expectedReply) != 0 { + for i, expected := range tc.expectedReply { + actual := rec.Msg.Extra[i].Header().Name + if actual != expected { + t.Errorf("Test %d: Expected answer %s, but got %s", i, expected, actual) + } + } + } + } +} diff --git a/ag_201_coredns/request/edns0.go b/ag_201_coredns/request/edns0.go new file mode 100644 index 0000000..89eb6b4 --- /dev/null +++ b/ag_201_coredns/request/edns0.go @@ -0,0 +1,31 @@ +package request + +import ( + "github.com/coredns/coredns/plugin/pkg/edns" + + "github.com/miekg/dns" +) + +func supportedOptions(o []dns.EDNS0) []dns.EDNS0 { + var supported = make([]dns.EDNS0, 0, 3) + // For as long as possible try avoid looking up in the map, because that need an Rlock. + for _, opt := range o { + switch code := opt.Option(); code { + case dns.EDNS0NSID: + fallthrough + case dns.EDNS0EXPIRE: + fallthrough + case dns.EDNS0COOKIE: + fallthrough + case dns.EDNS0TCPKEEPALIVE: + fallthrough + case dns.EDNS0PADDING: + supported = append(supported, opt) + default: + if edns.SupportedOption(code) { + supported = append(supported, opt) + } + } + } + return supported +} diff --git a/ag_201_coredns/request/request.go b/ag_201_coredns/request/request.go new file mode 100644 index 0000000..9188888 --- /dev/null +++ b/ag_201_coredns/request/request.go @@ -0,0 +1,363 @@ +// Package request abstracts a client's request so that all plugins will handle them in an unified way. +package request + +import ( + "net" + "strings" + + "github.com/coredns/coredns/plugin/pkg/edns" + + "github.com/miekg/dns" +) + +// Request contains some connection state and is useful in plugin. +type Request struct { + Req *dns.Msg + W dns.ResponseWriter + + // Optional lowercased zone of this query. + Zone string + + // Cache size after first call to Size or Do. If size is zero nothing has been cached yet. + // Both Size and Do set these values (and cache them). + size uint16 // UDP buffer size, or 64K in case of TCP. + do bool // DNSSEC OK value + + // Caches + family int8 // transport's family. + name string // lowercase qname. + ip string // client's ip. + port string // client's port. + localPort string // server's port. + localIP string // server's ip. +} + +// NewWithQuestion returns a new request based on the old, but with a new question +// section in the request. +func (r *Request) NewWithQuestion(name string, typ uint16) Request { + req1 := Request{W: r.W, Req: r.Req.Copy()} + req1.Req.Question[0] = dns.Question{Name: dns.Fqdn(name), Qclass: dns.ClassINET, Qtype: typ} + return req1 +} + +// IP gets the (remote) IP address of the client making the request. +func (r *Request) IP() string { + if r.ip != "" { + return r.ip + } + + ip, _, err := net.SplitHostPort(r.W.RemoteAddr().String()) + if err != nil { + r.ip = r.W.RemoteAddr().String() + return r.ip + } + + r.ip = ip + return r.ip +} + +// LocalIP gets the (local) IP address of server handling the request. +func (r *Request) LocalIP() string { + if r.localIP != "" { + return r.localIP + } + + ip, _, err := net.SplitHostPort(r.W.LocalAddr().String()) + if err != nil { + r.localIP = r.W.LocalAddr().String() + return r.localIP + } + + r.localIP = ip + return r.localIP +} + +// Port gets the (remote) port of the client making the request. +func (r *Request) Port() string { + if r.port != "" { + return r.port + } + + _, port, err := net.SplitHostPort(r.W.RemoteAddr().String()) + if err != nil { + r.port = "0" + return r.port + } + + r.port = port + return r.port +} + +// LocalPort gets the local port of the server handling the request. +func (r *Request) LocalPort() string { + if r.localPort != "" { + return r.localPort + } + + _, port, err := net.SplitHostPort(r.W.LocalAddr().String()) + if err != nil { + r.localPort = "0" + return r.localPort + } + + r.localPort = port + return r.localPort +} + +// RemoteAddr returns the net.Addr of the client that sent the current request. +func (r *Request) RemoteAddr() string { return r.W.RemoteAddr().String() } + +// LocalAddr returns the net.Addr of the server handling the current request. +func (r *Request) LocalAddr() string { return r.W.LocalAddr().String() } + +// Proto gets the protocol used as the transport. This will be udp or tcp. +func (r *Request) Proto() string { + if _, ok := r.W.RemoteAddr().(*net.UDPAddr); ok { + return "udp" + } + if _, ok := r.W.RemoteAddr().(*net.TCPAddr); ok { + return "tcp" + } + return "udp" +} + +// Family returns the family of the transport, 1 for IPv4 and 2 for IPv6. +func (r *Request) Family() int { + if r.family != 0 { + return int(r.family) + } + + var a net.IP + ip := r.W.RemoteAddr() + if i, ok := ip.(*net.UDPAddr); ok { + a = i.IP + } + if i, ok := ip.(*net.TCPAddr); ok { + a = i.IP + } + + if a.To4() != nil { + r.family = 1 + return 1 + } + r.family = 2 + return 2 +} + +// Do returns true if the request has the DO (DNSSEC OK) bit set. +func (r *Request) Do() bool { + if r.size != 0 { + return r.do + } + + r.Size() + return r.do +} + +// Len returns the length in bytes in the request. +func (r *Request) Len() int { return r.Req.Len() } + +// Size returns if buffer size *advertised* in the requests OPT record. +// Or when the request was over TCP, we return the maximum allowed size of 64K. +func (r *Request) Size() int { + if r.size != 0 { + return int(r.size) + } + + size := uint16(0) + if o := r.Req.IsEdns0(); o != nil { + r.do = o.Do() + size = o.UDPSize() + } + + // normalize size + size = edns.Size(r.Proto(), size) + r.size = size + return int(size) +} + +// SizeAndDo adds an OPT record that the reflects the intent from request. +// The returned bool indicates if an record was found and normalised. +func (r *Request) SizeAndDo(m *dns.Msg) bool { + o := r.Req.IsEdns0() + if o == nil { + return false + } + + if mo := m.IsEdns0(); mo != nil { + mo.Hdr.Name = "." + mo.Hdr.Rrtype = dns.TypeOPT + mo.SetVersion(0) + mo.SetUDPSize(o.UDPSize()) + mo.Hdr.Ttl &= 0xff00 // clear flags + + // Assume if the message m has options set, they are OK and represent what an upstream can do. + + if o.Do() { + mo.SetDo() + } + return true + } + + // Reuse the request's OPT record and tack it to m. + o.Hdr.Name = "." + o.Hdr.Rrtype = dns.TypeOPT + o.SetVersion(0) + o.Hdr.Ttl &= 0xff00 // clear flags + + if len(o.Option) > 0 { + o.Option = supportedOptions(o.Option) + } + + m.Extra = append(m.Extra, o) + return true +} + +// Scrub scrubs the reply message so that it will fit the client's buffer. It will first +// check if the reply fits without compression and then *with* compression. +// Note, the TC bit will be set regardless of protocol, even TCP message will +// get the bit, the client should then retry with pigeons. +func (r *Request) Scrub(reply *dns.Msg) *dns.Msg { + reply.Truncate(r.Size()) + + if reply.Compress { + return reply + } + + if r.Proto() == "udp" { + rl := reply.Len() + // Last ditch attempt to avoid fragmentation, if the size is bigger than the v4/v6 UDP fragmentation + // limit and sent via UDP compress it (in the hope we go under that limit). Limits taken from NSD: + // + // .., 1480 (EDNS/IPv4), 1220 (EDNS/IPv6), or the advertised EDNS buffer size if that is + // smaller than the EDNS default. + // See: https://open.nlnetlabs.nl/pipermail/nsd-users/2011-November/001278.html + if rl > 1480 && r.Family() == 1 { + reply.Compress = true + } + if rl > 1220 && r.Family() == 2 { + reply.Compress = true + } + } + + return reply +} + +// Type returns the type of the question as a string. If the request is malformed the empty string is returned. +func (r *Request) Type() string { + if r.Req == nil { + return "" + } + if len(r.Req.Question) == 0 { + return "" + } + + return dns.Type(r.Req.Question[0].Qtype).String() +} + +// QType returns the type of the question as an uint16. If the request is malformed +// 0 is returned. +func (r *Request) QType() uint16 { + if r.Req == nil { + return 0 + } + if len(r.Req.Question) == 0 { + return 0 + } + + return r.Req.Question[0].Qtype +} + +// Name returns the name of the question in the request. Note +// this name will always have a closing dot and will be lower cased. After a call Name +// the value will be cached. To clear this caching call Clear. +// If the request is malformed the root zone is returned. +func (r *Request) Name() string { + if r.name != "" { + return r.name + } + if r.Req == nil { + r.name = "." + return "." + } + if len(r.Req.Question) == 0 { + r.name = "." + return "." + } + + r.name = strings.ToLower(dns.Name(r.Req.Question[0].Name).String()) + return r.name +} + +// QName returns the name of the question in the request. +// If the request is malformed the root zone is returned. +func (r *Request) QName() string { + if r.Req == nil { + return "." + } + if len(r.Req.Question) == 0 { + return "." + } + + return dns.Name(r.Req.Question[0].Name).String() +} + +// Class returns the class of the question in the request. +// If the request is malformed the empty string is returned. +func (r *Request) Class() string { + if r.Req == nil { + return "" + } + if len(r.Req.Question) == 0 { + return "" + } + + return dns.Class(r.Req.Question[0].Qclass).String() +} + +// QClass returns the class of the question in the request. +// If the request is malformed 0 returned. +func (r *Request) QClass() uint16 { + if r.Req == nil { + return 0 + } + if len(r.Req.Question) == 0 { + return 0 + } + + return r.Req.Question[0].Qclass +} + +// Clear clears all caching from Request s. +func (r *Request) Clear() { + r.name = "" + r.ip = "" + r.localIP = "" + r.port = "" + r.localPort = "" + r.family = 0 + r.size = 0 + r.do = false +} + +// Match checks if the reply matches the qname and qtype from the request, it returns +// false when they don't match. +func (r *Request) Match(reply *dns.Msg) bool { + if len(reply.Question) != 1 { + return false + } + + if !reply.Response { + return false + } + + if strings.ToLower(reply.Question[0].Name) != r.Name() { + return false + } + + if reply.Question[0].Qtype != r.QType() { + return false + } + + return true +} diff --git a/ag_201_coredns/request/request_test.go b/ag_201_coredns/request/request_test.go new file mode 100644 index 0000000..0a3b1f2 --- /dev/null +++ b/ag_201_coredns/request/request_test.go @@ -0,0 +1,283 @@ +package request + +import ( + "fmt" + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestRequestDo(t *testing.T) { + st := testRequest() + + st.Do() + if !st.do { + t.Errorf("Expected st.do to be set") + } +} + +func TestRequestRemote(t *testing.T) { + st := testRequest() + if st.IP() != "10.240.0.1" { + t.Errorf("Wrong IP from request") + } + p := st.Port() + if p == "" { + t.Errorf("Failed to get Port from request") + } + if p != "40212" { + t.Errorf("Wrong port from request") + } +} + +func TestRequestMalformed(t *testing.T) { + m := new(dns.Msg) + st := Request{Req: m} + + if x := st.QType(); x != 0 { + t.Errorf("Expected 0 Qtype, got %d", x) + } + + if x := st.QClass(); x != 0 { + t.Errorf("Expected 0 QClass, got %d", x) + } + + if x := st.QName(); x != "." { + t.Errorf("Expected . Qname, got %s", x) + } + + if x := st.Name(); x != "." { + t.Errorf("Expected . Name, got %s", x) + } + + if x := st.Type(); x != "" { + t.Errorf("Expected empty Type, got %s", x) + } + + if x := st.Class(); x != "" { + t.Errorf("Expected empty Class, got %s", x) + } +} + +func TestRequestScrubAnswer(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion("large.example.com.", dns.TypeSRV) + req := Request{W: &test.ResponseWriter{}, Req: m} + + reply := new(dns.Msg) + reply.SetReply(m) + for i := 1; i < 200; i++ { + reply.Answer = append(reply.Answer, test.SRV( + fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i))) + } + + req.Scrub(reply) + if want, got := req.Size(), reply.Len(); want < got { + t.Errorf("Want scrub to reduce message length below %d bytes, got %d bytes", want, got) + } + if !reply.Truncated { + t.Errorf("Want scrub to set truncated bit") + } +} + +func TestRequestScrubExtra(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion("large.example.com.", dns.TypeSRV) + req := Request{W: &test.ResponseWriter{}, Req: m} + + reply := new(dns.Msg) + reply.SetReply(m) + for i := 1; i < 200; i++ { + reply.Extra = append(reply.Extra, test.SRV( + fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i))) + } + + req.Scrub(reply) + if want, got := req.Size(), reply.Len(); want < got { + t.Errorf("Want scrub to reduce message length below %d bytes, got %d bytes", want, got) + } + if !reply.Truncated { + t.Errorf("Want scrub to set truncated bit") + } +} + +func TestRequestScrubExtraEdns0(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion("large.example.com.", dns.TypeSRV) + m.SetEdns0(4096, true) + req := Request{W: &test.ResponseWriter{}, Req: m} + + reply := new(dns.Msg) + reply.SetReply(m) + for i := 1; i < 200; i++ { + reply.Extra = append(reply.Extra, test.SRV( + fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i))) + } + + req.Scrub(reply) + if want, got := req.Size(), reply.Len(); want < got { + t.Errorf("Want scrub to reduce message length below %d bytes, got %d bytes", want, got) + } + if !reply.Truncated { + t.Errorf("Want scrub to set truncated bit") + } +} + +func TestRequestScrubExtraRegression(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion("large.example.com.", dns.TypeSRV) + m.SetEdns0(2048, true) + req := Request{W: &test.ResponseWriter{}, Req: m} + + reply := new(dns.Msg) + reply.SetReply(m) + for i := 1; i < 33; i++ { + reply.Answer = append(reply.Answer, test.SRV( + fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i))) + } + for i := 1; i < 33; i++ { + reply.Extra = append(reply.Extra, test.A( + fmt.Sprintf("10-0-0-%d.default.pod.k8s.example.com. 10 IN A 10.0.0.%d", i, i))) + } + + reply = req.Scrub(reply) + if want, got := req.Size(), reply.Len(); want < got { + t.Errorf("Want scrub to reduce message length below %d bytes, got %d bytes", want, got) + } + if !reply.Truncated { + t.Errorf("Want scrub to set truncated bit") + } +} + +func TestTruncation(t *testing.T) { + for bufsize := 1024; bufsize <= 4096; bufsize += 12 { + m := new(dns.Msg) + m.SetQuestion("http.service.tcp.srv.k8s.example.org", dns.TypeSRV) + m.SetEdns0(uint16(bufsize), true) + req := Request{W: &test.ResponseWriter{}, Req: m} + + reply := new(dns.Msg) + reply.SetReply(m) + + for i := 0; i < 61; i++ { + reply.Answer = append(reply.Answer, test.SRV(fmt.Sprintf("http.service.tcp.srv.k8s.example.org. 5 IN SRV 0 0 80 10-144-230-%d.default.pod.k8s.example.org.", i))) + } + + for i := 0; i < 5; i++ { + reply.Extra = append(reply.Extra, test.A(fmt.Sprintf("ip-10-10-52-5%d.subdomain.example.org. 5 IN A 10.10.52.5%d", i, i))) + } + + for i := 0; i < 5; i++ { + reply.Ns = append(reply.Ns, test.NS(fmt.Sprintf("srv.subdomain.example.org. 5 IN NS ip-10-10-33-6%d.subdomain.example.org.", i))) + } + + req.Scrub(reply) + want, got := req.Size(), reply.Len() + if want < got { + t.Fatalf("Want scrub to reduce message length below %d bytes, got %d bytes", want, got) + } + } +} + +func TestRequestScrubAnswerExact(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion("large.example.com.", dns.TypeSRV) + m.SetEdns0(867, false) // Bit fiddly, but this hits the rl == size break clause in Scrub, 52 RRs should remain. + req := Request{W: &test.ResponseWriter{}, Req: m} + + reply := new(dns.Msg) + reply.SetReply(m) + for i := 1; i < 200; i++ { + reply.Answer = append(reply.Answer, test.A(fmt.Sprintf("large.example.com. 10 IN A 127.0.0.%d", i))) + } + + req.Scrub(reply) + if want, got := req.Size(), reply.Len(); want < got { + t.Errorf("Want scrub to reduce message length below %d bytes, got %d bytes", want, got) + } +} + +func TestRequestMatch(t *testing.T) { + st := testRequest() + reply := new(dns.Msg) + reply.Response = true + + reply.SetQuestion("example.com.", dns.TypeMX) + if b := st.Match(reply); b { + t.Errorf("Failed to match %s %d, got %t, expected %t", "example.com.", dns.TypeMX, b, false) + } + + reply.SetQuestion("example.com.", dns.TypeA) + if b := st.Match(reply); !b { + t.Errorf("Failed to match %s %d, got %t, expected %t", "example.com.", dns.TypeA, b, true) + } + + reply.SetQuestion("example.org.", dns.TypeA) + if b := st.Match(reply); b { + t.Errorf("Failed to match %s %d, got %t, expected %t", "example.org.", dns.TypeA, b, false) + } +} + +func BenchmarkRequestDo(b *testing.B) { + st := testRequest() + + for i := 0; i < b.N; i++ { + st.Do() + } +} + +func BenchmarkRequestSize(b *testing.B) { + st := testRequest() + + for i := 0; i < b.N; i++ { + st.Size() + } +} + +func BenchmarkRequestScrub(b *testing.B) { + st := testRequest() + + reply := new(dns.Msg) + reply.SetReply(st.Req) + for i := 1; i < 33; i++ { + reply.Answer = append(reply.Answer, test.SRV( + fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i))) + } + for i := 1; i < 33; i++ { + reply.Extra = append(reply.Extra, test.A( + fmt.Sprintf("10-0-0-%d.default.pod.k8s.example.com. 10 IN A 10.0.0.%d", i, i))) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + st.Scrub(reply.Copy()) + } +} + +func testRequest() Request { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + m.SetEdns0(4096, true) + return Request{W: &test.ResponseWriter{}, Req: m} +} + +func TestRequestClear(t *testing.T) { + st := testRequest() + if st.IP() != "10.240.0.1" { + t.Errorf("Wrong IP from request") + } + p := st.Port() + if p == "" { + t.Errorf("Failed to get Port from request") + } + st.Clear() + if st.ip != "" { + t.Errorf("Expected st.ip to be cleared after Clear") + } + + if st.port != "" { + t.Errorf("Expected st.port to be cleared after Clear") + } +} diff --git a/ag_201_coredns/request/writer.go b/ag_201_coredns/request/writer.go new file mode 100644 index 0000000..587b3b5 --- /dev/null +++ b/ag_201_coredns/request/writer.go @@ -0,0 +1,21 @@ +package request + +import "github.com/miekg/dns" + +// ScrubWriter will, when writing the message, call scrub to make it fit the client's buffer. +type ScrubWriter struct { + dns.ResponseWriter + req *dns.Msg // original request +} + +// NewScrubWriter returns a new and initialized ScrubWriter. +func NewScrubWriter(req *dns.Msg, w dns.ResponseWriter) *ScrubWriter { return &ScrubWriter{w, req} } + +// WriteMsg overrides the default implementation of the underlying dns.ResponseWriter and calls +// scrub on the message m and will then write it to the client. +func (s *ScrubWriter) WriteMsg(m *dns.Msg) error { + state := Request{Req: s.req, W: s.ResponseWriter} + state.SizeAndDo(m) + state.Scrub(m) + return s.ResponseWriter.WriteMsg(m) +} diff --git a/ag_201_coredns/test/auto_test.go b/ag_201_coredns/test/auto_test.go new file mode 100644 index 0000000..b80c334 --- /dev/null +++ b/ag_201_coredns/test/auto_test.go @@ -0,0 +1,169 @@ +package test + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/miekg/dns" +) + +func TestAuto(t *testing.T) { + t.Parallel() + tmpdir, err := os.MkdirTemp(os.TempDir(), "coredns") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + corefile := `org:0 { + auto { + directory ` + tmpdir + ` db\.(.*) {1} + reload 0.01s + } + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("www.example.org.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + if resp.Rcode != dns.RcodeServerFailure { + t.Fatalf("Expected reply to be a SERVFAIL, got %d", resp.Rcode) + } + + // Write db.example.org to get example.org. + if err = os.WriteFile(filepath.Join(tmpdir, "db.example.org"), []byte(zoneContent), 0644); err != nil { + t.Fatal(err) + } + + time.Sleep(50 * time.Millisecond) // wait for it to be picked up + + resp, err = dns.Exchange(m, udp) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + if len(resp.Answer) != 1 { + t.Fatalf("Expected 1 RR in the answer section, got %d", len(resp.Answer)) + } + + // Remove db.example.org again. + os.Remove(filepath.Join(tmpdir, "db.example.org")) + + time.Sleep(50 * time.Millisecond) // wait for it to be picked up + resp, err = dns.Exchange(m, udp) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + if resp.Rcode != dns.RcodeServerFailure { + t.Fatalf("Expected reply to be a SERVFAIL, got %d", resp.Rcode) + } +} + +func TestAutoNonExistentZone(t *testing.T) { + t.Parallel() + tmpdir, err := os.MkdirTemp(os.TempDir(), "coredns") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + corefile := `.:0 { + auto { + directory ` + tmpdir + ` (.*) {1} + reload 0.01s + } + errors stdout + }` + + i, err := CoreDNSServer(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + udp, _ := CoreDNSServerPorts(i, 0) + if udp == "" { + t.Fatal("Could not get UDP listening port") + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + if resp.Rcode != dns.RcodeServerFailure { + t.Fatalf("Expected reply to be a SERVFAIL, got %d", resp.Rcode) + } +} + +func TestAutoAXFR(t *testing.T) { + t.Parallel() + + tmpdir, err := os.MkdirTemp(os.TempDir(), "coredns") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + corefile := `org:0 { + auto { + directory ` + tmpdir + ` db\.(.*) {1} + reload 0.01s + } + transfer { + to * + } + }` + + i, err := CoreDNSServer(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + _, tcp := CoreDNSServerPorts(i, 0) + if tcp == "" { + t.Fatal("Could not get TCP listening port") + } + defer i.Stop() + + // Write db.example.org to get example.org. + if err = os.WriteFile(filepath.Join(tmpdir, "db.example.org"), []byte(zoneContent), 0644); err != nil { + t.Fatal(err) + } + + time.Sleep(50 * time.Millisecond) // wait for it to be picked up + + tr := new(dns.Transfer) + m := new(dns.Msg) + m.SetAxfr("example.org.") + c, err := tr.In(m, tcp) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + l := 0 + for e := range c { + l += len(e.RR) + } + + if l != 5 { + t.Fatalf("Expected response with %d RRs, got %d", 5, l) + } +} + +const zoneContent = `; testzone +@ IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082534 7200 3600 1209600 3600 + IN NS a.iana-servers.net. + IN NS b.iana-servers.net. + +www IN A 127.0.0.1 +` diff --git a/ag_201_coredns/test/cache_test.go b/ag_201_coredns/test/cache_test.go new file mode 100644 index 0000000..831c39f --- /dev/null +++ b/ag_201_coredns/test/cache_test.go @@ -0,0 +1,157 @@ +package test + +import ( + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestLookupCache(t *testing.T) { + // Start auth. CoreDNS holding the auth zone. + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + corefile := `example.org:0 { + file ` + name + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + // Start caching forward CoreDNS that we want to test. + corefile = `example.org:0 { + forward . ` + udp + ` + cache 10 + }` + + i, udp, _, err = CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + t.Run("Long TTL", func(t *testing.T) { + testCase(t, "example.org.", udp, 2, 10) + }) + + t.Run("Short TTL", func(t *testing.T) { + testCase(t, "short.example.org.", udp, 1, 5) + }) + + t.Run("DNSSEC OPT", func(t *testing.T) { + testCaseDNSSEC(t, "example.org.", udp, 4096) + }) + + t.Run("DNSSEC OPT", func(t *testing.T) { + testCaseDNSSEC(t, "example.org.", udp, 0) + }) +} + +func testCase(t *testing.T, name, addr string, expectAnsLen int, expectTTL uint32) { + m := new(dns.Msg) + m.SetQuestion(name, dns.TypeA) + resp, err := dns.Exchange(m, addr) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + + if len(resp.Answer) != expectAnsLen { + t.Fatalf("Expected %v RR in the answer section, got %v.", expectAnsLen, len(resp.Answer)) + } + + ttl := resp.Answer[0].Header().Ttl + if ttl != expectTTL { + t.Errorf("Expected TTL to be %d, got %d", expectTTL, ttl) + } +} + +func testCaseDNSSEC(t *testing.T, name, addr string, bufsize int) { + m := new(dns.Msg) + m.SetQuestion(name, dns.TypeA) + + if bufsize > 0 { + o := &dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}} + o.SetDo() + o.SetUDPSize(uint16(bufsize)) + m.Extra = append(m.Extra, o) + } + resp, err := dns.Exchange(m, addr) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + + if len(resp.Extra) == 0 && bufsize == 0 { + // no OPT, this is OK + return + } + + opt := resp.Extra[len(resp.Extra)-1] + if x, ok := opt.(*dns.OPT); !ok && bufsize > 0 { + t.Fatalf("Expected OPT RR, got %T", x) + } + if bufsize > 0 { + if !opt.(*dns.OPT).Do() { + t.Errorf("Expected DO bit to be set, got false") + } + if x := opt.(*dns.OPT).UDPSize(); int(x) != bufsize { + t.Errorf("Expected %d bufsize, got %d", bufsize, x) + } + } else { + if opt.Header().Rrtype == dns.TypeOPT { + t.Errorf("Expected no OPT RR, but got one: %s", opt) + } + } +} + +func TestLookupCacheWithoutEdns(t *testing.T) { + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + corefile := `example.org:0 { + file ` + name + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + // Start caching forward CoreDNS that we want to test. + corefile = `example.org:0 { + forward . ` + udp + ` + cache 10 + }` + + i, udp, _, err = CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + if len(resp.Extra) == 0 { + return + } + + if resp.Extra[0].Header().Rrtype == dns.TypeOPT { + t.Fatalf("Expected no OPT RR, but got: %s", resp.Extra[0]) + } + t.Fatalf("Expected empty additional section, got %v", resp.Extra) +} diff --git a/ag_201_coredns/test/chaos_test.go b/ag_201_coredns/test/chaos_test.go new file mode 100644 index 0000000..eb83a27 --- /dev/null +++ b/ag_201_coredns/test/chaos_test.go @@ -0,0 +1,37 @@ +package test + +import ( + "testing" + + // Plug in CoreDNS, needed for AppVersion and AppName in this test. + "github.com/coredns/caddy" + _ "github.com/coredns/coredns/coremain" + + "github.com/miekg/dns" +) + +func TestChaos(t *testing.T) { + corefile := `.:0 { + chaos + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("version.bind.", dns.TypeTXT) + m.Question[0].Qclass = dns.ClassCHAOS + + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %v", err) + } + chTxt := resp.Answer[0].(*dns.TXT).Txt[0] + version := caddy.AppName + "-" + caddy.AppVersion + if chTxt != version { + t.Fatalf("Expected version to be %s, got %s", version, chTxt) + } +} diff --git a/ag_201_coredns/test/compression_scrub_test.go b/ag_201_coredns/test/compression_scrub_test.go new file mode 100644 index 0000000..311d29a --- /dev/null +++ b/ag_201_coredns/test/compression_scrub_test.go @@ -0,0 +1,60 @@ +package test + +import ( + "net" + "testing" + + "github.com/miekg/dns" +) + +func TestCompressScrub(t *testing.T) { + corefile := `example.org:0 { + erratic { + drop 0 + delay 0 + large + } + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + c, err := net.Dial("udp", udp) + if err != nil { + t.Fatalf("Could not dial %s", err) + } + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + q, _ := m.Pack() + + c.Write(q) + buf := make([]byte, 1024) + n, err := c.Read(buf) + if err != nil || n == 0 { + t.Errorf("Expected reply, got: %s", err) + return + } + if n >= 512 { + t.Fatalf("Expected returned packet to be < 512, got %d", n) + } + buf = buf[:n] + // If there is compression in the returned packet we should look for compression pointers, if found + // the pointers should return to the domain name in the query (the first domain name that's available for + // compression. This means we're looking for a combo where the pointers is detected and the offset is 12 + // the position of the first name after the header. The erratic plugin adds 30 RRs that should all be compressed. + found := 0 + for i := 0; i < len(buf)-1; i++ { + if buf[i]&0xC0 == 0xC0 { + off := (int(buf[i])^0xC0)<<8 | int(buf[i+1]) + if off == 12 { + found++ + } + } + } + if found != 30 { + t.Errorf("Failed to find all compression pointers in the packet, wanted 30, got %d", found) + } +} diff --git a/ag_201_coredns/test/corefile_test.go b/ag_201_coredns/test/corefile_test.go new file mode 100644 index 0000000..1f08ab2 --- /dev/null +++ b/ag_201_coredns/test/corefile_test.go @@ -0,0 +1,17 @@ +package test + +import ( + "testing" +) + +func TestCorefile1(t *testing.T) { + corefile := `ȶ +acl +` + // this crashed, now it should return an error. + i, _, _, err := CoreDNSServerAndPorts(corefile) + if err == nil { + defer i.Stop() + t.Fatalf("Expected an error got none") + } +} diff --git a/ag_201_coredns/test/doc.go b/ag_201_coredns/test/doc.go new file mode 100644 index 0000000..528092a --- /dev/null +++ b/ag_201_coredns/test/doc.go @@ -0,0 +1,2 @@ +// Package test contains function and types useful for writing tests. +package test diff --git a/ag_201_coredns/test/ds_file_test.go b/ag_201_coredns/test/ds_file_test.go new file mode 100644 index 0000000..9f0d32f --- /dev/null +++ b/ag_201_coredns/test/ds_file_test.go @@ -0,0 +1,59 @@ +package test + +import ( + "testing" + + mtest "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +// Using miek.nl here because this is the easiest zone to get access to and its masters +// run both NSD and BIND9, making checks like "what should we actually return" super easy. +var dsTestCases = []mtest.Case{ + { + Qname: "_udp.miek.nl.", Qtype: dns.TypeDS, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + mtest.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeDS, + Ns: []dns.RR{ + mtest.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"), + }, + }, +} + +func TestLookupDS(t *testing.T) { + t.Parallel() + name, rm, err := mtest.TempFile(".", miekNL) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + corefile := `miek.nl:0 { + file ` + name + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + for _, tc := range dsTestCases { + m := new(dns.Msg) + m.SetQuestion(tc.Qname, tc.Qtype) + resp, err := dns.Exchange(m, udp) + if err != nil || resp == nil { + t.Fatalf("Expected to receive reply, but didn't for %s %d", tc.Qname, tc.Qtype) + } + + if err := mtest.SortAndCheck(resp, tc); err != nil { + t.Error(err) + } + } +} diff --git a/ag_201_coredns/test/edns0_test.go b/ag_201_coredns/test/edns0_test.go new file mode 100644 index 0000000..5d86faa --- /dev/null +++ b/ag_201_coredns/test/edns0_test.go @@ -0,0 +1,32 @@ +package test + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestEDNS0(t *testing.T) { + corefile := `.:0 { + whoami + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeSOA) + m.SetEdns0(4096, true) + + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %v", err) + } + opt := resp.Extra[len(resp.Extra)-1] + if opt.Header().Rrtype != dns.TypeOPT { + t.Errorf("Last RR must be OPT record") + } +} diff --git a/ag_201_coredns/test/erratic_autopath_test.go b/ag_201_coredns/test/erratic_autopath_test.go new file mode 100644 index 0000000..1c6364a --- /dev/null +++ b/ag_201_coredns/test/erratic_autopath_test.go @@ -0,0 +1,119 @@ +package test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/miekg/dns" +) + +func setupProxyTargetCoreDNS(t *testing.T, fn func(string)) { + tmpdir, err := os.MkdirTemp(os.TempDir(), "coredns") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + content := ` +example.org. IN SOA sns.dns.icann.org. noc.dns.icann.org. 1 3600 3600 3600 3600 + +google.com. IN SOA ns1.google.com. dns-admin.google.com. 1 3600 3600 3600 3600 +google.com. IN A 172.217.25.110 +` + + path := filepath.Join(tmpdir, "file") + if err = os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatalf("Could not write to temp file: %s", err) + } + defer os.Remove(path) + + corefile := `.:0 { + file ` + path + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get proxy target CoreDNS serving instance: %s", err) + } + defer i.Stop() + + fn(udp) +} + +func TestLookupAutoPathErratic(t *testing.T) { + setupProxyTargetCoreDNS(t, func(proxyPath string) { + corefile := `.:0 { + erratic + autopath @erratic + forward . ` + proxyPath + ` + debug + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + tests := []struct { + qname string + expectedAnswer string + expectedType uint16 + }{ + {"google.com.a.example.org.", "google.com.a.example.org.", dns.TypeCNAME}, + {"google.com.", "google.com.", dns.TypeA}, + } + + for i, tc := range tests { + m := new(dns.Msg) + // erratic always returns this search path: "a.example.org.", "b.example.org.", "". + m.SetQuestion(tc.qname, dns.TypeA) + + r, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Test %d, failed to sent query: %q", i, err) + } + if len(r.Answer) == 0 { + t.Fatalf("Test %d, answer section should have RRs", i) + } + if x := r.Answer[0].Header().Name; x != tc.expectedAnswer { + t.Fatalf("Test %d, expected answer %s, got %s", i, tc.expectedAnswer, x) + } + if x := r.Answer[0].Header().Rrtype; x != tc.expectedType { + t.Fatalf("Test %d, expected answer type %d, got %d", i, tc.expectedType, x) + } + } + }) +} + +func TestAutoPathErraticNotLoaded(t *testing.T) { + setupProxyTargetCoreDNS(t, func(proxyPath string) { + corefile := `.:0 { + autopath @erratic + forward . ` + proxyPath + ` + debug + }` + + i, err := CoreDNSServer(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + udp, _ := CoreDNSServerPorts(i, 0) + if udp == "" { + t.Fatalf("Could not get UDP listening port") + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("google.com.a.example.org.", dns.TypeA) + r, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Failed to sent query: %q", err) + } + if r.Rcode != dns.RcodeNameError { + t.Fatalf("Expected NXDOMAIN, got %d", r.Rcode) + } + }) +} diff --git a/ag_201_coredns/test/etcd_cache_test.go b/ag_201_coredns/test/etcd_cache_test.go new file mode 100644 index 0000000..a6e9cb5 --- /dev/null +++ b/ag_201_coredns/test/etcd_cache_test.go @@ -0,0 +1,70 @@ +//go:build etcd + +package test + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/etcd/msg" + + "github.com/miekg/dns" +) + +// uses some stuff from etcd_tests.go + +func TestEtcdCache(t *testing.T) { + corefile := `.:0 { + etcd skydns.test { + path /skydns + } + cache skydns.test + }` + + ex, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer ex.Stop() + + etc := etcdPlugin() + + var ctx = context.TODO() + for _, serv := range servicesCacheTest { + set(ctx, t, etc, serv.Key, 0, serv) + defer delete(ctx, t, etc, serv.Key) + } + + m := new(dns.Msg) + m.SetQuestion("b.example.skydns.test.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Errorf("Expected to receive reply, but didn't: %s", err) + } + checkResponse(t, resp) + + resp, err = dns.Exchange(m, udp) + if err != nil { + t.Errorf("Expected to receive reply, but didn't: %s", err) + } + checkResponse(t, resp) + if len(resp.Extra) != 0 { + t.Errorf("Expected no RRs in additional section, got: %d", len(resp.Extra)) + } +} + +func checkResponse(t *testing.T, resp *dns.Msg) { + if len(resp.Answer) == 0 { + t.Fatal("Expected to at least one RR in the answer section, got none") + } + if resp.Answer[0].Header().Rrtype != dns.TypeA { + t.Errorf("Expected RR to A, got: %d", resp.Answer[0].Header().Rrtype) + } + if resp.Answer[0].(*dns.A).A.String() != "127.0.0.1" { + t.Errorf("Expected 127.0.0.1, got: %s", resp.Answer[0].(*dns.A).A.String()) + } +} + +var servicesCacheTest = []*msg.Service{ + {Host: "127.0.0.1", Port: 666, Key: "b.example.skydns.test."}, +} diff --git a/ag_201_coredns/test/etcd_credentials_test.go b/ag_201_coredns/test/etcd_credentials_test.go new file mode 100644 index 0000000..a51e44e --- /dev/null +++ b/ag_201_coredns/test/etcd_credentials_test.go @@ -0,0 +1,84 @@ +//go:build etcd + +package test + +import ( + "context" + "testing" +) + +// uses some stuff from etcd_tests.go + +func TestEtcdCredentials(t *testing.T) { + corefile := `.:0 { + etcd skydns.test { + path /skydns + } + }` + + ex, _, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer ex.Stop() + + etc := etcdPlugin() + username := "root" + password := "password" + key := "foo" + value := "bar" + + var ctx = context.TODO() + + if _, err := etc.Client.Put(ctx, key, value); err != nil { + t.Errorf("Failed to put dummy value un etcd: %v", err) + } + + if _, err := etc.Client.RoleAdd(ctx, "root"); err != nil { + t.Errorf("Failed to create root role: %s", err) + } + defer func() { + if _, err := etc.Client.RoleDelete(ctx, "root"); err != nil { + t.Errorf("Failed to delete root role: %s", err) + } + }() + + if _, err := etc.Client.UserAdd(ctx, username, password); err != nil { + t.Errorf("Failed to create user: %s", err) + } + defer func() { + if _, err := etc.Client.UserDelete(ctx, username); err != nil { + t.Errorf("Failed to delete user: %s", err) + } + }() + + if _, err := etc.Client.UserGrantRole(ctx, username, "root"); err != nil { + t.Errorf("Failed to assign role to root user: %v", err) + } + if _, err := etc.Client.AuthEnable(ctx); err != nil { + t.Errorf("Failed to enable authentication: %s", err) + } + + etc2 := etcdPluginWithCredentials(username, password) + + defer func() { + if _, err := etc2.Client.AuthDisable(ctx); err != nil { + t.Errorf("Fail to disable authentication: %v", err) + } + }() + + resp, err := etc2.Client.Get(ctx, key) + if err != nil { + t.Errorf("Fail to retrieve value from etcd: %v", err) + } + + if len(resp.Kvs) != 1 { + t.Errorf("Too many response found: %+v", resp) + return + } + actual := resp.Kvs[0].Value + expected := "bar" + if string(resp.Kvs[0].Value) != expected { + t.Errorf("Value doesn't match, expected:%s actual:%s", actual, expected) + } +} diff --git a/ag_201_coredns/test/etcd_test.go b/ag_201_coredns/test/etcd_test.go new file mode 100644 index 0000000..e54a2bd --- /dev/null +++ b/ag_201_coredns/test/etcd_test.go @@ -0,0 +1,107 @@ +//go:build etcd + +package test + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/coredns/coredns/plugin/etcd" + "github.com/coredns/coredns/plugin/etcd/msg" + + "github.com/miekg/dns" + etcdcv3 "go.etcd.io/etcd/client/v3" +) + +func etcdPlugin() *etcd.Etcd { + etcdCfg := etcdcv3.Config{ + Endpoints: []string{"http://localhost:2379"}, + } + cli, _ := etcdcv3.New(etcdCfg) + return &etcd.Etcd{Client: cli, PathPrefix: "/skydns"} +} + +func etcdPluginWithCredentials(username, password string) *etcd.Etcd { + etcdCfg := etcdcv3.Config{ + Endpoints: []string{"http://localhost:2379"}, + Username: username, + Password: password, + } + cli, _ := etcdcv3.New(etcdCfg) + return &etcd.Etcd{Client: cli, PathPrefix: "/skydns"} +} + +// This test starts two coredns servers (and needs etcd). Configure a stubzones in both (that will loop) and +// will then test if we detect this loop. +func TestEtcdStubLoop(t *testing.T) { + // TODO(miek) +} + +func TestEtcdStubAndProxyLookup(t *testing.T) { + corefile := `.:0 { + etcd skydns.local { + stubzones + path /skydns + endpoint http://localhost:2379 + upstream + fallthrough + } + forward . 8.8.8.8:53 + }` + + ex, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer ex.Stop() + + etc := etcdPlugin() + + var ctx = context.TODO() + for _, serv := range servicesStub { // adds example.{net,org} as stubs + set(ctx, t, etc, serv.Key, 0, serv) + defer delete(ctx, t, etc, serv.Key) + } + + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %v", err) + } + if len(resp.Answer) == 0 { + t.Fatalf("Expected to at least one RR in the answer section, got none") + } + if resp.Answer[0].Header().Rrtype != dns.TypeA { + t.Errorf("Expected RR to A, got: %d", resp.Answer[0].Header().Rrtype) + } + if resp.Answer[0].(*dns.A).A.String() != "93.184.216.34" { + t.Errorf("Expected 93.184.216.34, got: %s", resp.Answer[0].(*dns.A).A.String()) + } +} + +var servicesStub = []*msg.Service{ + // Two tests, ask a question that should return servfail because remote it no accessible + // and one with edns0 option added, that should return refused. + {Host: "127.0.0.1", Port: 666, Key: "b.example.org.stub.dns.skydns.test."}, + // Actual test that goes out to the internet. + {Host: "199.43.132.53", Key: "a.example.net.stub.dns.skydns.test."}, +} + +// Copied from plugin/etcd/setup_test.go +func set(ctx context.Context, t *testing.T, e *etcd.Etcd, k string, ttl time.Duration, m *msg.Service) { + b, err := json.Marshal(m) + if err != nil { + t.Fatal(err) + } + path, _ := msg.PathWithWildcard(k, e.PathPrefix) + e.Client.KV.Put(ctx, path, string(b)) +} + +// Copied from plugin/etcd/setup_test.go +func delete(ctx context.Context, t *testing.T, e *etcd.Etcd, k string) { + path, _ := msg.PathWithWildcard(k, e.PathPrefix) + e.Client.Delete(ctx, path) +} diff --git a/ag_201_coredns/test/example_test.go b/ag_201_coredns/test/example_test.go new file mode 100644 index 0000000..863b69f --- /dev/null +++ b/ag_201_coredns/test/example_test.go @@ -0,0 +1,16 @@ +package test + +const exampleOrg = `; example.org test file +$TTL 3600 +@ IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600 +@ IN NS b.iana-servers.net. +@ IN NS a.iana-servers.net. +@ IN A 127.0.0.1 +@ IN A 127.0.0.2 +short 1 IN A 127.0.0.3 + +*.w 3600 IN TXT "Wildcard" +a.b.c.w IN TXT "Not a wildcard" +cname IN CNAME www.example.net. +service IN SRV 8080 10 10 @ +` diff --git a/ag_201_coredns/test/file_cname_proxy_test.go b/ag_201_coredns/test/file_cname_proxy_test.go new file mode 100644 index 0000000..a8714d5 --- /dev/null +++ b/ag_201_coredns/test/file_cname_proxy_test.go @@ -0,0 +1,77 @@ +package test + +import ( + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestZoneExternalCNAMELookupWithoutProxy(t *testing.T) { + t.Parallel() + + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + // Corefile with for example without proxy section. + corefile := `example.org:0 { + file ` + name + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("cname.example.org.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + // There should only be a CNAME in the answer section. + if len(resp.Answer) != 1 { + t.Fatalf("Expected 1 RR in answer section got %d", len(resp.Answer)) + } +} + +func TestZoneExternalCNAMELookupWithProxy(t *testing.T) { + t.Parallel() + + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + // Corefile with for example proxy section. + corefile := `.:0 { + file ` + name + ` example.org { + upstream + } + forward . 8.8.8.8 8.8.4.4 + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("cname.example.org.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + // There should be a CNAME *and* an IP address in the answer section. + // For now, just check that we have 2 RRs + if len(resp.Answer) != 2 { + t.Fatalf("Expected 2 RRs in answer section got %d", len(resp.Answer)) + } +} diff --git a/ag_201_coredns/test/file_loop_test.go b/ag_201_coredns/test/file_loop_test.go new file mode 100644 index 0000000..2452cc0 --- /dev/null +++ b/ag_201_coredns/test/file_loop_test.go @@ -0,0 +1,48 @@ +package test + +import ( + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +const loopDB = `example.com. 500 IN SOA ns1.outside.com. root.example.com. 3 604800 86400 2419200 604800 +example.com. 500 IN NS ns1.outside.com. +a.example.com. 500 IN CNAME b.example.com. +*.foo.example.com. 500 IN CNAME bar.foo.example.com.` + +func TestFileLoop(t *testing.T) { + name, rm, err := test.TempFile(".", loopDB) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + // Corefile with for example without proxy section. + corefile := `example.com:0 { + file ` + name + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("something.foo.example.com.", dns.TypeA) + + r, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not exchange msg: %s", err) + } + + // This should not loop, don't really care about the correctness of the answer. + // Currently we return servfail in the file lookup.go file. + // For now: document current behavior in this test. + if r.Rcode != dns.RcodeServerFailure { + t.Errorf("Rcode should be dns.RcodeServerFailure: %d", r.Rcode) + } +} diff --git a/ag_201_coredns/test/file_reload_test.go b/ag_201_coredns/test/file_reload_test.go new file mode 100644 index 0000000..254ef94 --- /dev/null +++ b/ag_201_coredns/test/file_reload_test.go @@ -0,0 +1,67 @@ +package test + +import ( + "os" + "testing" + "time" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestZoneReload(t *testing.T) { + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + // Corefile with two stanzas + corefile := ` + example.org:0 { + file ` + name + ` { + reload 0.01s + } + } + example.net:0 { + file ` + name + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + if len(resp.Answer) != 2 { + t.Fatalf("Expected two RR in answer section got %d", len(resp.Answer)) + } + + // Remove RR from the Apex + os.WriteFile(name, []byte(exampleOrgUpdated), 0644) + + time.Sleep(20 * time.Millisecond) // reload time, with some race insurance + + resp, err = dns.Exchange(m, udp) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + + if len(resp.Answer) != 1 { + t.Fatalf("Expected one RR in answer section got %d", len(resp.Answer)) + } +} + +const exampleOrgUpdated = `; example.org test file +example.org. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082541 7200 3600 1209600 3600 +example.org. IN NS b.iana-servers.net. +example.org. IN NS a.iana-servers.net. +example.org. IN A 127.0.0.2 +` diff --git a/ag_201_coredns/test/file_serve_test.go b/ag_201_coredns/test/file_serve_test.go new file mode 100644 index 0000000..4ec4460 --- /dev/null +++ b/ag_201_coredns/test/file_serve_test.go @@ -0,0 +1,100 @@ +package test + +import ( + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestZoneEDNS0Lookup(t *testing.T) { + t.Parallel() + + name, rm, err := test.TempFile(".", `$ORIGIN example.org. +@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. ( + 2017042745 ; serial + 7200 ; refresh (2 hours) + 3600 ; retry (1 hour) + 1209600 ; expire (2 weeks) + 3600 ; minimum (1 hour) +) + + 3600 IN NS a.iana-servers.net. + 3600 IN NS b.iana-servers.net. + +www IN A 127.0.0.1 +www IN AAAA ::1 +`) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + // Corefile with for example without proxy section. + corefile := `example.org:0 { + file ` + name + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeMX) + m.SetEdns0(4096, true) + + r, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not exchange msg: %s", err) + } + if r.Rcode == dns.RcodeServerFailure { + t.Fatalf("Rcode should not be dns.RcodeServerFailure") + } +} + +func TestZoneNoNS(t *testing.T) { + t.Parallel() + + name, rm, err := test.TempFile(".", `$ORIGIN example.org. +@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. ( + 2017042745 ; serial + 7200 ; refresh (2 hours) + 3600 ; retry (1 hour) + 1209600 ; expire (2 weeks) + 3600 ; minimum (1 hour) + ) + +www IN A 127.0.0.1 +www IN AAAA ::1 +`) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + // Corefile with for example without proxy section. + corefile := `example.org:0 { + file ` + name + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeMX) + m.SetEdns0(4096, true) + + r, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not exchange msg: %s", err) + } + if r.Rcode == dns.RcodeServerFailure { + t.Fatalf("Rcode should not be dns.RcodeServerFailure") + } +} diff --git a/ag_201_coredns/test/file_srv_additional_test.go b/ag_201_coredns/test/file_srv_additional_test.go new file mode 100644 index 0000000..6da81a9 --- /dev/null +++ b/ag_201_coredns/test/file_srv_additional_test.go @@ -0,0 +1,42 @@ +package test + +import ( + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestZoneSRVAdditional(t *testing.T) { + t.Parallel() + + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + // Corefile with for example without proxy section. + corefile := `example.org:0 { + file ` + name + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("service.example.org.", dns.TypeSRV) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + + // There should be 2 A records in the additional section. + if len(resp.Extra) != 2 { + t.Fatalf("Expected 2 RR in additional section got %d", len(resp.Extra)) + } +} diff --git a/ag_201_coredns/test/file_test.go b/ag_201_coredns/test/file_test.go new file mode 100644 index 0000000..babee27 --- /dev/null +++ b/ag_201_coredns/test/file_test.go @@ -0,0 +1,16 @@ +package test + +import ( + "testing" + + "github.com/coredns/coredns/plugin/test" +) + +func TestTempFile(t *testing.T) { + t.Parallel() + _, f, e := test.TempFile(".", "test") + if e != nil { + t.Fatalf("Failed to create temp file: %s", e) + } + defer f() +} diff --git a/ag_201_coredns/test/file_upstream_test.go b/ag_201_coredns/test/file_upstream_test.go new file mode 100644 index 0000000..77ffa1d --- /dev/null +++ b/ag_201_coredns/test/file_upstream_test.go @@ -0,0 +1,229 @@ +package test + +import ( + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestFileUpstream(t *testing.T) { + name, rm, err := test.TempFile(".", `$ORIGIN example.org. +@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. ( + 2017042745 ; serial + 7200 ; refresh (2 hours) + 3600 ; retry (1 hour) + 1209600 ; expire (2 weeks) + 3600 ; minimum (1 hour) +) + + 3600 IN NS a.iana-servers.net. + 3600 IN NS b.iana-servers.net. + +www 3600 IN CNAME www.example.net. +`) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + corefile := `.:0 { + file ` + name + ` example.org + hosts { + 10.0.0.1 www.example.net. + fallthrough + } + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("www.example.org.", dns.TypeA) + m.SetEdns0(4096, true) + + r, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not exchange msg: %s", err) + } + if r.Rcode == dns.RcodeServerFailure { + t.Fatalf("Rcode should not be dns.RcodeServerFailure") + } + if x := r.Answer[1].(*dns.A).A.String(); x != "10.0.0.1" { + t.Errorf("Failed to get address for CNAME, expected 10.0.0.1 got %s", x) + } +} + +func TestFileUpstreamError(t *testing.T) { + cases := map[string]test.Case{ + "nxdomain": { + Qname: "nxdomain.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("nxdomain.example.org. 3600 IN CNAME nxdomain.example.net"), + }, + Rcode: dns.RcodeNameError, + }, + "nxdomain-chain": { + Qname: "chain1.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("chain1.example.org. 3600 IN CNAME nxdomain.example.org"), + test.CNAME("nxdomain.example.org. 3600 IN CNAME nxdomain.example.net"), + }, + Rcode: dns.RcodeNameError, + }, + "srvfail": { + Qname: "srvfail.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("srvfail.example.org. 3600 IN CNAME srvfail.example.net."), + }, + Rcode: dns.RcodeServerFailure, + }, + "srvfail-chain": { + Qname: "chain2.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("chain2.example.org. 3600 IN CNAME srvfail.example.org."), + test.CNAME("srvfail.example.org. 3600 IN CNAME srvfail.example.net."), + }, + Rcode: dns.RcodeServerFailure, + }, + "nodata": { + Qname: "nodata.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("nodata.example.org. 3600 IN CNAME nodata.example.net"), + }, + Rcode: dns.RcodeSuccess, + }, + "nodata-chain": { + Qname: "chain3.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("chain3.example.org. 3600 IN CNAME nodata.example.org"), + test.CNAME("nodata.example.org. 3600 IN CNAME nodata.example.net"), + }, + Rcode: dns.RcodeSuccess, + }, + } + name, rm, err := test.TempFile(".", `$ORIGIN example.org. +@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. ( + 2017042745 ; serial + 7200 ; refresh (2 hours) + 3600 ; retry (1 hour) + 1209600 ; expire (2 weeks) + 3600 ; minimum (1 hour) +) + + 3600 IN NS a.iana-servers.net. + 3600 IN NS b.iana-servers.net. + +chain1 3600 IN CNAME nxdomain +nxdomain 3600 IN CNAME nxdomain.example.net. +chain2 3600 IN CNAME srvfail +srvfail 3600 IN CNAME srvfail.example.net. +chain3 3600 IN CNAME nodata +nodata 3600 IN CNAME nodata.example.net. + +`) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + corefile := `.:0 { + template ANY A nxdomain.example.net. { + rcode NXDOMAIN + } + template ANY A srvfail.example.net. { + rcode SERVFAIL + } + template ANY A nodata.example.net. { + } + file ` + name + ` example.org +}` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + for n, tc := range cases { + t.Run(n, func(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion(tc.Qname, tc.Qtype) + m.SetEdns0(4096, true) + + r, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not exchange msg: %s", err) + } + if r.Rcode != tc.Rcode { + t.Fatalf("expected rcode %v, got %v", tc.Rcode, r.Rcode) + } + if n := len(r.Answer); n != len(tc.Answer) { + t.Fatalf("Expected %v answers, got %v", len(tc.Answer), n) + } + if err := test.Section(tc, test.Answer, r.Answer); err != nil { + t.Error(err) + } + }) + } +} + +// TestFileUpstreamAdditional runs two CoreDNS servers that serve example.org and foo.example.org. +// example.org contains a cname to foo.example.org; this should be resolved via upstream.Self. +func TestFileUpstreamAdditional(t *testing.T) { + name, rm, err := test.TempFile(".", `$ORIGIN example.org. +@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2017042745 7200 3600 1209600 3600 + + 3600 IN NS b.iana-servers.net. + +www 3600 IN CNAME www.foo +`) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + name2, rm2, err2 := test.TempFile(".", `$ORIGIN foo.example.org. +@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2017042745 7200 3600 1209600 3600 + + 3600 IN NS b.iana-servers.net. + +www 3600 IN A 127.0.0.53 +`) + if err2 != nil { + t.Fatalf("Failed to create zone: %s", err2) + } + defer rm2() + + corefile := `.:0 { + file ` + name + ` example.org + file ` + name2 + ` foo.example.org + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("www.example.org.", dns.TypeA) + + r, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not exchange msg: %s", err) + } + if r.Rcode == dns.RcodeServerFailure { + t.Fatalf("Rcode should not be dns.RcodeServerFailure") + } + if x := len(r.Answer); x != 2 { + t.Errorf("Expected 2 RR in reply, got %d", x) + } + if x := r.Answer[1].(*dns.A).A.String(); x != "127.0.0.53" { + t.Errorf("Failed to get address for CNAME, expected 127.0.0.53, got %s", x) + } +} diff --git a/ag_201_coredns/test/file_xfr_test.go b/ag_201_coredns/test/file_xfr_test.go new file mode 100644 index 0000000..5597c82 --- /dev/null +++ b/ag_201_coredns/test/file_xfr_test.go @@ -0,0 +1,102 @@ +package test + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestLargeAXFR(t *testing.T) { + // Build a zone in text format. It contains 6.4K AAAA RRs. (this number is rather random) + var sb strings.Builder + const numAAAAs = 6553 + sb.WriteString("example.com. IN SOA . . 1 60 60 60 60\n") + sb.WriteString("example.com. IN NS ns.example.\n") + for i := 0; i < numAAAAs; i++ { + sb.WriteString(fmt.Sprintf("%d.example.com. IN AAAA 2001:db8::1\n", i)) + } + + // Setup the zone file and CoreDNS to serve the zone, allowing zone transfer + name, rm, err := test.TempFile(".", sb.String()) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + corefile := `example.com:0 { + file ` + name + ` + transfer { + to * + } + }` + + // Start server, and send an AXFR query to the TCP port. We set the deadline to prevent the test from hanging. + i, _, tcp, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeAXFR) + co, err := dns.DialTimeout("tcp", tcp, 5*time.Second) + if err != nil { + t.Fatalf("Expected to establish TCP connection, but didn't: %s", err) + } + defer co.Close() + co.SetWriteDeadline(time.Now().Add(5 * time.Second)) + err = co.WriteMsg(m) + if err != nil { + t.Fatalf("Unable to send AXFR/TCP query: %s", err) + } + + // Then send another query on the same connection. We use this to confirm that multiple outstanding queries won't cause a race. + m.SetQuestion("0.example.com.", dns.TypeAAAA) + err = co.WriteMsg(m) + if err != nil { + t.Fatalf("Unable to send AAAA/TCP query: %s", err) + } + + // The AXFR query should be responded first. + nrr := 0 // total number of transferred RRs + for { + resp, err := co.ReadMsg() + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + if len(resp.Answer) == 0 { + continue + } + // First RR should be SOA. + if nrr == 0 && resp.Answer[0].Header().Rrtype != dns.TypeSOA { + t.Fatalf("Expected SOA, but got type %d", resp.Answer[0].Header().Rrtype) + } + nrr += len(resp.Answer) + // If we see another SOA at the end of the message, we are done. + // Note that this check is not enough to detect all invalid responses, but checking those is not the purpose of this test. + if nrr > 1 && resp.Answer[len(resp.Answer)-1].Header().Rrtype == dns.TypeSOA { + break + } + } + // On successful completion, 2 SOA, 1 NS, and all AAAAs should have been transferred. + if nrr != numAAAAs+3 { + t.Fatalf("Got an unexpected number of RRs: %d", nrr) + } + + // The file plugin shouldn't hijack or (yet) close the connection, so the second query should also be responded. + resp, err := co.ReadMsg() + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + if len(resp.Answer) < 1 { + t.Fatalf("Expected a non-empty answer, but it was empty") + } + if resp.Answer[len(resp.Answer)-1].Header().Rrtype != dns.TypeAAAA { + t.Fatalf("Expected a AAAA answer, but it wasn't: type %d", resp.Answer[len(resp.Answer)-1].Header().Rrtype) + } +} diff --git a/ag_201_coredns/test/fuzz_corefile.go b/ag_201_coredns/test/fuzz_corefile.go new file mode 100644 index 0000000..88d0df3 --- /dev/null +++ b/ag_201_coredns/test/fuzz_corefile.go @@ -0,0 +1,12 @@ +//go:build gofuzz + +package test + +// Fuzz fuzzes a corefile. +func Fuzz(data []byte) int { + _, _, _, err := CoreDNSServerAndPorts(string(data)) + if err != nil { + return 1 + } + return 0 +} diff --git a/ag_201_coredns/test/grpc_test.go b/ag_201_coredns/test/grpc_test.go new file mode 100644 index 0000000..37504c9 --- /dev/null +++ b/ag_201_coredns/test/grpc_test.go @@ -0,0 +1,58 @@ +package test + +import ( + "context" + "testing" + "time" + + "github.com/coredns/coredns/pb" + + "github.com/miekg/dns" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func TestGrpc(t *testing.T) { + corefile := `grpc://.:0 { + whoami + }` + + g, _, tcp, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer g.Stop() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + conn, err := grpc.DialContext(ctx, tcp, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) + if err != nil { + t.Fatalf("Expected no error but got: %s", err) + } + defer conn.Close() + + client := pb.NewDnsServiceClient(conn) + + m := new(dns.Msg) + m.SetQuestion("whoami.example.org.", dns.TypeA) + msg, _ := m.Pack() + + reply, err := client.Query(context.TODO(), &pb.DnsPacket{Msg: msg}) + if err != nil { + t.Errorf("Expected no error but got: %s", err) + } + + d := new(dns.Msg) + err = d.Unpack(reply.Msg) + if err != nil { + t.Errorf("Expected no error but got: %s", err) + } + + if d.Rcode != dns.RcodeSuccess { + t.Errorf("Expected success but got %d", d.Rcode) + } + + if len(d.Extra) != 2 { + t.Errorf("Expected 2 RRs in additional section, but got %d", len(d.Extra)) + } +} diff --git a/ag_201_coredns/test/hosts_file_test.go b/ag_201_coredns/test/hosts_file_test.go new file mode 100644 index 0000000..1c29f53 --- /dev/null +++ b/ag_201_coredns/test/hosts_file_test.go @@ -0,0 +1,39 @@ +package test + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestHostsInlineLookup(t *testing.T) { + corefile := `example.org:0 { + hosts highly_unlikely_to_exist_hosts_file example.org { + 10.0.0.1 example.org + fallthrough + } + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + // expect answer section with A record in it + if len(resp.Answer) == 0 { + t.Fatal("Expected to at least one RR in the answer section, got none") + } + if resp.Answer[0].Header().Rrtype != dns.TypeA { + t.Errorf("Expected RR to A, got: %d", resp.Answer[0].Header().Rrtype) + } + if resp.Answer[0].(*dns.A).A.String() != "10.0.0.1" { + t.Errorf("Expected 10.0.0.1, got: %s", resp.Answer[0].(*dns.A).A.String()) + } +} diff --git a/ag_201_coredns/test/log_test.go b/ag_201_coredns/test/log_test.go new file mode 100644 index 0000000..51d972d --- /dev/null +++ b/ag_201_coredns/test/log_test.go @@ -0,0 +1,5 @@ +package test + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/ag_201_coredns/test/metric_naming_test.go b/ag_201_coredns/test/metric_naming_test.go new file mode 100644 index 0000000..7fcfee5 --- /dev/null +++ b/ag_201_coredns/test/metric_naming_test.go @@ -0,0 +1,163 @@ +package test + +import ( + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil/promlint" + dto "github.com/prometheus/client_model/go" +) + +func TestMetricNaming(t *testing.T) { + walker := validMetricWalker{} + err := filepath.Walk("..", walker.walk) + + if err != nil { + t.Fatal(err) + } + + if len(walker.Metrics) > 0 { + l := promlint.NewWithMetricFamilies(walker.Metrics) + problems, err := l.Lint() + if err != nil { + t.Fatalf("Link found error: %s", err) + } + + if len(problems) > 0 { + t.Fatalf("A slice of Problems indicating any issues found in the metrics stream: %s", problems) + } + } +} + +type validMetricWalker struct { + Metrics []*dto.MetricFamily +} + +func (w *validMetricWalker) walk(path string, info os.FileInfo, _ error) error { + // only for regular files, not starting with a . and those that are go files. + if !info.Mode().IsRegular() { + return nil + } + // Is it appropriate to compare the file name equals metrics.go directly? + if strings.HasPrefix(path, "../.") { + return nil + } + if strings.HasSuffix(path, "_test.go") { + return nil + } + if !strings.HasSuffix(path, ".go") { + return nil + } + + fs := token.NewFileSet() + f, err := parser.ParseFile(fs, path, nil, parser.AllErrors) + if err != nil { + return err + } + l := &metric{} + ast.Walk(l, f) + if l.Metric != nil { + w.Metrics = append(w.Metrics, l.Metric) + } + return nil +} + +type metric struct { + Metric *dto.MetricFamily +} + +func (l *metric) Visit(n ast.Node) ast.Visitor { + if n == nil { + return nil + } + ce, ok := n.(*ast.CallExpr) + if !ok { + return l + } + se, ok := ce.Fun.(*ast.SelectorExpr) + if !ok { + return l + } + id, ok := se.X.(*ast.Ident) + if !ok { + return l + } + if id.Name != "prometheus" { //prometheus + return l + } + var metricsType dto.MetricType + switch se.Sel.Name { + case "NewCounterVec", "NewCounter": + metricsType = dto.MetricType_COUNTER + case "NewGaugeVec", "NewGauge": + metricsType = dto.MetricType_GAUGE + case "NewHistogramVec", "NewHistogram": + metricsType = dto.MetricType_HISTOGRAM + case "NewSummaryVec", "NewSummary": + metricsType = dto.MetricType_SUMMARY + default: + return l + } + // Check first arg, that should have basic lit with capital + if len(ce.Args) < 1 { + return l + } + bl, ok := ce.Args[0].(*ast.CompositeLit) + if !ok { + return l + } + + // parse Namespace Subsystem Name Help + var subsystem, name, help string + for _, elt := range bl.Elts { + expr, ok := elt.(*ast.KeyValueExpr) + if !ok { + continue + } + object, ok := expr.Key.(*ast.Ident) + if !ok { + continue + } + value, ok := expr.Value.(*ast.BasicLit) + if !ok { + continue + } + + // remove quotes + stringLiteral, err := strconv.Unquote(value.Value) + if err != nil { + return l + } + + switch object.Name { + case "Subsystem": + subsystem = stringLiteral + case "Name": + name = stringLiteral + case "Help": + help = stringLiteral + } + } + + // validate metrics field + if len(name) == 0 || len(help) == 0 { + return l + } + + metricName := prometheus.BuildFQName(plugin.Namespace, subsystem, name) + l.Metric = &dto.MetricFamily{ + Name: &metricName, + Help: &help, + Type: &metricsType, + } + return l +} diff --git a/ag_201_coredns/test/metrics_test.go b/ag_201_coredns/test/metrics_test.go new file mode 100644 index 0000000..4de9dad --- /dev/null +++ b/ag_201_coredns/test/metrics_test.go @@ -0,0 +1,257 @@ +package test + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/coredns/coredns/plugin/metrics" + "github.com/coredns/coredns/plugin/metrics/vars" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +// Because we don't properly shutdown the metrics servers we are re-using the metrics between tests, not a superbad issue +// but depending on the ordering of the tests this trips up stuff. + +// Start test server that has metrics enabled. Then tear it down again. +func TestMetricsServer(t *testing.T) { + corefile := ` + example.org:0 { + chaos CoreDNS-001 miek@miek.nl + prometheus localhost:0 + } + example.com:0 { + log + prometheus localhost:0 + }` + + srv, err := CoreDNSServer(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer srv.Stop() +} + +func TestMetricsRefused(t *testing.T) { + metricName := "coredns_dns_responses_total" + corefile := `example.org:0 { + whoami + prometheus localhost:0 + }` + + srv, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer srv.Stop() + + m := new(dns.Msg) + m.SetQuestion("google.com.", dns.TypeA) + + if _, err = dns.Exchange(m, udp); err != nil { + t.Fatalf("Could not send message: %s", err) + } + + data := test.Scrape("http://" + metrics.ListenAddr + "/metrics") + got, labels := test.MetricValue(metricName, data) + + if got != "1" { + t.Errorf("Expected value %s for refused, but got %s", "1", got) + } + if labels["zone"] != vars.Dropped { + t.Errorf("Expected zone value %s for refused, but got %s", vars.Dropped, labels["zone"]) + } + if labels["rcode"] != "REFUSED" { + t.Errorf("Expected zone value %s for refused, but got %s", "REFUSED", labels["rcode"]) + } +} + +func TestMetricsAuto(t *testing.T) { + tmpdir, err := os.MkdirTemp(os.TempDir(), "coredns") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + corefile := `org:0 { + auto { + directory ` + tmpdir + ` db\.(.*) {1} + reload 0.1s + } + prometheus localhost:0 + }` + + i, err := CoreDNSServer(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + udp, _ := CoreDNSServerPorts(i, 0) + if udp == "" { + t.Fatalf("Could not get UDP listening port") + } + defer i.Stop() + + // Write db.example.org to get example.org. + if err = os.WriteFile(filepath.Join(tmpdir, "db.example.org"), []byte(zoneContent), 0644); err != nil { + t.Fatal(err) + } + time.Sleep(110 * time.Millisecond) // wait for it to be picked up + + m := new(dns.Msg) + m.SetQuestion("www.example.org.", dns.TypeA) + + if _, err := dns.Exchange(m, udp); err != nil { + t.Fatalf("Could not send message: %s", err) + } + + metricName := "coredns_dns_requests_total" // {zone, proto, family, type} + + data := test.Scrape("http://" + metrics.ListenAddr + "/metrics") + // Get the value for the metrics where the one of the labels values matches "example.org." + got, _ := test.MetricValueLabel(metricName, "example.org.", data) + + if got == "0" { + t.Errorf("Expected value %s for %s, but got %s", "> 1", metricName, got) + } + + // Remove db.example.org again. And see if the metric stops increasing. + os.Remove(filepath.Join(tmpdir, "db.example.org")) + time.Sleep(110 * time.Millisecond) // wait for it to be picked up + if _, err := dns.Exchange(m, udp); err != nil { + t.Fatalf("Could not send message: %s", err) + } + + data = test.Scrape("http://" + metrics.ListenAddr + "/metrics") + got, _ = test.MetricValueLabel(metricName, "example.org.", data) + + if got == "0" { + t.Errorf("Expected value %s for %s, but got %s", "> 1", metricName, got) + } +} + +// Show that when 2 blocs share the same metric listener (they have a prometheus plugin on the same listening address), +// ALL the metrics of the second bloc in order are declared in prometheus, especially the plugins that are used ONLY in the second bloc +func TestMetricsSeveralBlocs(t *testing.T) { + cacheSizeMetricName := "coredns_cache_entries" + addrMetrics := "localhost:9155" + corefile := ` + example.org:0 { + prometheus ` + addrMetrics + ` + forward . 8.8.8.8:53 { + force_tcp + } + } + google.com:0 { + prometheus ` + addrMetrics + ` + whoami + cache + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + // send an initial query to setup properly the cache size + m := new(dns.Msg) + m.SetQuestion("google.com.", dns.TypeA) + if _, err = dns.Exchange(m, udp); err != nil { + t.Fatalf("Could not send message: %s", err) + } + + beginCacheSize := test.ScrapeMetricAsInt(addrMetrics, cacheSizeMetricName, "", 0) + + // send an query, different from initial to ensure we have another add to the cache + m = new(dns.Msg) + m.SetQuestion("www.google.com.", dns.TypeA) + + if _, err = dns.Exchange(m, udp); err != nil { + t.Fatalf("Could not send message: %s", err) + } + + endCacheSize := test.ScrapeMetricAsInt(addrMetrics, cacheSizeMetricName, "", 0) + if err != nil { + t.Errorf("Unexpected metric data retrieved for %s : %s", cacheSizeMetricName, err) + } + if endCacheSize-beginCacheSize != 1 { + t.Errorf("Expected metric data retrieved for %s, expected %d, got %d", cacheSizeMetricName, 1, endCacheSize-beginCacheSize) + } +} + +func TestMetricsPluginEnabled(t *testing.T) { + corefile := ` + example.org:0 { + chaos CoreDNS-001 miek@miek.nl + prometheus localhost:0 + } + example.com:0 { + whoami + prometheus localhost:0 + }` + + srv, err := CoreDNSServer(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer srv.Stop() + + metricName := "coredns_plugin_enabled" //{server, zone, name} + + data := test.Scrape("http://" + metrics.ListenAddr + "/metrics") + + // Get the value for the metrics where the one of the labels values matches "chaos". + got, _ := test.MetricValueLabel(metricName, "chaos", data) + + if got != "1" { + t.Errorf("Expected value %s for %s, but got %s", "1", metricName, got) + } + + // Get the value for the metrics where the one of the labels values matches "erratic". + got, _ = test.MetricValueLabel(metricName, "erratic", data) // none of these tests use 'erratic' + + if got != "" { + t.Errorf("Expected value %s for %s, but got %s", "", metricName, got) + } +} + +func TestMetricsAvailable(t *testing.T) { + procMetric := "coredns_build_info" + procCache := "coredns_cache_entries" + procCacheMiss := "coredns_cache_misses_total" + procForward := "coredns_dns_request_duration_seconds" + corefileWithMetrics := `.:0 { + prometheus localhost:0 + cache + forward . 8.8.8.8 { + force_tcp + } + }` + + inst, _, tcp, err := CoreDNSServerAndPorts(corefileWithMetrics) + defer inst.Stop() + if err != nil { + if strings.Contains(err.Error(), inUse) { + return + } + t.Errorf("Could not get service instance: %s", err) + } + // send a query and check we can scrap corresponding metrics + cl := dns.Client{Net: "tcp"} + m := new(dns.Msg) + m.SetQuestion("www.example.org.", dns.TypeA) + + if _, _, err := cl.Exchange(m, tcp); err != nil { + t.Fatalf("Could not send message: %s", err) + } + + // we should have metrics from forward, cache, and metrics itself + if err := collectMetricsInfo(metrics.ListenAddr, procMetric, procCache, procCacheMiss, procForward); err != nil { + t.Errorf("Could not scrap one of expected stats : %s", err) + } +} diff --git a/ag_201_coredns/test/miek_test.go b/ag_201_coredns/test/miek_test.go new file mode 100644 index 0000000..3a1cdbc --- /dev/null +++ b/ag_201_coredns/test/miek_test.go @@ -0,0 +1,31 @@ +package test + +const miekNL = `; miek.nl test zone +$TTL 30M +$ORIGIN miek.nl. +@ IN SOA linode.atoom.net. miek.miek.nl. ( + 1282630059 ; Serial + 4H ; Refresh + 1H ; Retry + 7D ; Expire + 4H ) ; Negative Cache TTL + IN NS linode.atoom.net. + IN NS ns-ext.nlnetlabs.nl. + IN NS omval.tednet.nl. + IN NS ext.ns.whyscream.net. + + IN MX 1 aspmx.l.google.com. + IN MX 5 alt1.aspmx.l.google.com. + IN MX 5 alt2.aspmx.l.google.com. + IN MX 10 aspmx2.googlemail.com. + IN MX 10 aspmx3.googlemail.com. + + IN A 176.58.119.54 + IN AAAA 2a01:7e00::f03c:91ff:fe79:234c + IN HINFO "Please stop asking for ANY" "See draft-ietf-dnsop-refuse-any" + +a IN A 176.58.119.54 + IN AAAA 2a01:7e00::f03c:91ff:fe79:234c +www IN CNAME a +archive IN CNAME a +` diff --git a/ag_201_coredns/test/no_plugins_test.go b/ag_201_coredns/test/no_plugins_test.go new file mode 100644 index 0000000..51d546d --- /dev/null +++ b/ag_201_coredns/test/no_plugins_test.go @@ -0,0 +1,28 @@ +package test + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestNoPlugins(t *testing.T) { + corefile := `example.org:0 { + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %v", err) + } + if resp.Rcode != dns.RcodeRefused { + t.Fatalf("Expected rcode to be %d, got %d", dns.RcodeRefused, resp.Rcode) + } +} diff --git a/ag_201_coredns/test/plugin_dnssec_test.go b/ag_201_coredns/test/plugin_dnssec_test.go new file mode 100644 index 0000000..48e032c --- /dev/null +++ b/ag_201_coredns/test/plugin_dnssec_test.go @@ -0,0 +1,75 @@ +package test + +import ( + "os" + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestLookupBalanceRewriteCacheDnssec(t *testing.T) { + t.Parallel() + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + rm1 := createKeyFile(t) + defer rm1() + + corefile := `example.org:0 { + file ` + name + ` + rewrite type ANY HINFO + dnssec { + key file ` + base + ` + } + loadbalance + }` + + ex, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer ex.Stop() + + c := new(dns.Client) + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + m.SetEdns0(4096, true) + res, _, err := c.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not send query: %s", err) + } + sig := 0 + for _, a := range res.Answer { + if a.Header().Rrtype == dns.TypeRRSIG { + sig++ + } + } + if sig == 0 { + t.Errorf("Expected RRSIGs, got none") + t.Logf("%v\n", res) + } +} + +func createKeyFile(t *testing.T) func() { + os.WriteFile(base+".key", + []byte(`example.org. IN DNSKEY 256 3 13 tDyI0uEIDO4SjhTJh1AVTFBLpKhY3He5BdAlKztewiZ7GecWj94DOodg ovpN73+oJs+UfZ+p9zOSN5usGAlHrw==`), + 0644) + os.WriteFile(base+".private", + []byte(`Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: HPmldSNfrkj/aDdUMFwuk/lgzaC5KIsVEG3uoYvF4pQ= +Created: 20160426083115 +Publish: 20160426083115 +Activate: 20160426083115`), + 0644) + return func() { + os.Remove(base + ".key") + os.Remove(base + ".private") + } +} + +const base = "Kexample.org.+013+44563" diff --git a/ag_201_coredns/test/presubmit_test.go b/ag_201_coredns/test/presubmit_test.go new file mode 100644 index 0000000..79f6127 --- /dev/null +++ b/ag_201_coredns/test/presubmit_test.go @@ -0,0 +1,362 @@ +package test + +// These tests check for meta level items, like trailing whitespace, correct file naming etc. + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "unicode" +) + +func TestFileNameHyphen(t *testing.T) { + walker := hasHyphenWalker{} + err := filepath.Walk("..", walker.walk) + + if err != nil { + t.Fatal(err) + } + + if len(walker.Errors) > 0 { + for _, err = range walker.Errors { + t.Error(err) + } + } +} + +type hasHyphenWalker struct { + Errors []error +} + +func (w *hasHyphenWalker) walk(path string, info os.FileInfo, _ error) error { + // only for regular files, not starting with a . and those that are go files. + if !info.Mode().IsRegular() { + return nil + } + if strings.HasPrefix(path, "../.") { + return nil + } + if strings.Contains(path, "/vendor") { + return nil + } + if filepath.Ext(path) != ".go" { + return nil + } + + if strings.Index(path, "-") > 0 { + absPath, _ := filepath.Abs(path) + w.Errors = append(w.Errors, fmt.Errorf("file %q has a hyphen, please use underscores in file names", absPath)) + } + + return nil +} + +// Test if error messages start with an upper case. +func TestLowercaseLog(t *testing.T) { + walker := hasLowercaseWalker{} + err := filepath.Walk("..", walker.walk) + + if err != nil { + t.Fatal(err) + } + + if len(walker.Errors) > 0 { + for _, err = range walker.Errors { + t.Error(err) + } + } +} + +type hasLowercaseWalker struct { + Errors []error +} + +func (w *hasLowercaseWalker) walk(path string, info os.FileInfo, _ error) error { + // only for regular files, not starting with a . and those that are go files. + if !info.Mode().IsRegular() { + return nil + } + if strings.HasPrefix(path, "../.") { + return nil + } + if strings.Contains(path, "/vendor") { + return nil + } + if !strings.HasSuffix(path, "_test.go") { + return nil + } + + fs := token.NewFileSet() + f, err := parser.ParseFile(fs, path, nil, parser.AllErrors) + if err != nil { + return err + } + l := &logfmt{} + ast.Walk(l, f) + if l.err != nil { + w.Errors = append(w.Errors, l.err) + } + return nil +} + +type logfmt struct { + err error +} + +func (l logfmt) Visit(n ast.Node) ast.Visitor { + if n == nil { + return nil + } + ce, ok := n.(*ast.CallExpr) + if !ok { + return l + } + se, ok := ce.Fun.(*ast.SelectorExpr) + if !ok { + return l + } + id, ok := se.X.(*ast.Ident) + if !ok { + return l + } + if id.Name != "t" { //t *testing.T + return l + } + + switch se.Sel.Name { + case "Errorf": + case "Logf": + case "Log": + case "Fatalf": + case "Fatal": + default: + return l + } + // Check first arg, that should have basic lit with capital + if len(ce.Args) < 1 { + return l + } + bl, ok := ce.Args[0].(*ast.BasicLit) + if !ok { + return l + } + if bl.Kind != token.STRING { + return l + } + if strings.HasPrefix(bl.Value, "\"%s") || strings.HasPrefix(bl.Value, "\"%d") { + return l + } + if strings.HasPrefix(bl.Value, "\"%v") || strings.HasPrefix(bl.Value, "\"%+v") { + return l + } + for i, u := range bl.Value { + // disregard " + if i == 1 && !unicode.IsUpper(u) { + return nil + } + if i == 1 { + break + } + } + return l +} + +func TestImportTesting(t *testing.T) { + walker := hasLowercaseWalker{} + err := filepath.Walk("..", walker.walk) + + if err != nil { + t.Fatal(err) + } + + if len(walker.Errors) > 0 { + for _, err = range walker.Errors { + t.Error(err) + } + } +} + +type hasImportTestingWalker struct { + Errors []error +} + +func (w *hasImportTestingWalker) walk(path string, info os.FileInfo, _ error) error { + // only for regular files, not starting with a . and those that are go files. + if !info.Mode().IsRegular() { + return nil + } + if strings.HasPrefix(path, "../.") { + return nil + } + if strings.Contains(path, "/vendor") { + return nil + } + if strings.HasSuffix(path, "_test.go") { + return nil + } + + if strings.HasSuffix(path, ".go") { + fs := token.NewFileSet() + f, err := parser.ParseFile(fs, path, nil, parser.AllErrors) + if err != nil { + return err + } + for _, im := range f.Imports { + if im.Path.Value == `"testing"` { + absPath, _ := filepath.Abs(path) + w.Errors = append(w.Errors, fmt.Errorf("file %q is importing %q", absPath, "testing")) + } + } + } + return nil +} + +func TestImportOrdering(t *testing.T) { + walker := testImportOrderingWalker{} + err := filepath.Walk("..", walker.walk) + + if err != nil { + t.Fatal(err) + } + + if len(walker.Errors) > 0 { + for _, err = range walker.Errors { + t.Error(err) + } + } +} + +type testImportOrderingWalker struct { + Errors []error +} + +func (w *testImportOrderingWalker) walk(path string, info os.FileInfo, _ error) error { + if !info.Mode().IsRegular() { + return nil + } + if strings.HasPrefix(path, "../.") { + return nil + } + if strings.Contains(path, "/vendor") { + return nil + } + if filepath.Ext(path) != ".go" { + return nil + } + // pb files are autogenerated by protoc + if strings.HasPrefix(path, "../pb/") { + return nil + } + + fs := token.NewFileSet() + f, err := parser.ParseFile(fs, path, nil, parser.AllErrors) + if err != nil { + return err + } + if len(f.Imports) == 0 { + return nil + } + + // 3 blocks total, if + // 3 blocks: std + coredns + 3rd party + // 2 blocks: std + coredns, std + 3rd party, coredns + 3rd party + // 1 block: std, coredns, 3rd party + // first entry in a block specifies the type (std, coredns, 3rd party) + // we want: std, coredns, 3rd party + // more than 3 blocks as an error + blocks := [3][]*ast.ImportSpec{} + prevpos := 0 + bl := 0 + for _, im := range f.Imports { + line := fs.Position(im.Path.Pos()).Line + if line-prevpos > 1 && prevpos > 0 { + bl++ + } + if bl > 2 { + absPath, _ := filepath.Abs(path) + w.Errors = append(w.Errors, fmt.Errorf("more than %d import blocks in %q", bl, absPath)) + } + blocks[bl] = append(blocks[bl], im) + prevpos = line + } + // if it: + // contains strings github.com/coredns -> coredns + // contains dots -> 3rd + // no dots -> std + ip := [3]string{} // type per block, just string, either std, coredns, 3rd + for i := 0; i <= bl; i++ { + ip[i] = importtype(blocks[i][0].Path.Value) + } + + // Ok, now that we have the type, let's see if all members adhere to it. + // After that we check if the are in the right order. + for i := 0; i <= bl; i++ { + for _, p := range blocks[i] { + t := importtype(p.Path.Value) + if t != ip[i] { + absPath, _ := filepath.Abs(path) + w.Errors = append(w.Errors, fmt.Errorf("import path for %s is not of the same type %q in %q", p.Path.Value, ip[i], absPath)) + } + } + } + + // check order + switch bl { + case 0: + // we don't care + case 1: + if ip[0] == "std" && ip[1] == "coredns" { + break // OK + } + if ip[0] == "std" && ip[1] == "3rd" { + break // OK + } + if ip[0] == "coredns" && ip[1] == "3rd" { + break // OK + } + absPath, _ := filepath.Abs(path) + w.Errors = append(w.Errors, fmt.Errorf("import path in %q are not in the right order (std -> coredns -> 3rd)", absPath)) + case 2: + if ip[0] == "std" && ip[1] == "coredns" && ip[2] == "3rd" { + break // OK + } + absPath, _ := filepath.Abs(path) + w.Errors = append(w.Errors, fmt.Errorf("import path in %q are not in the right order (std -> coredns -> 3rd)", absPath)) + } + + return nil +} + +func importtype(s string) string { + if strings.Contains(s, "github.com/coredns") { + return "coredns" + } + if strings.Contains(s, ".") { + return "3rd" + } + return "std" +} + +// TestMetricNaming tests the imports path used for metrics. It depends on faillint to be installed: go install github.com/fatih/faillint +func TestPrometheusImports(t *testing.T) { + if _, err := exec.LookPath("faillint"); err != nil { + fmt.Fprintf(os.Stderr, "Not executing TestPrometheusImports: faillint not found\n") + return + } + + // make this multiline? + p := `github.com/prometheus/client_golang/prometheus.{NewCounter,NewCounterVec,NewCounterVec,NewGauge,NewGaugeVec,NewGaugeFunc,NewHistorgram,NewHistogramVec,NewSummary,NewSummaryVec}=github.com/prometheus/client_golang/prometheus/promauto.{NewCounter,NewCounterVec,NewCounterVec,NewGauge,NewGaugeVec,NewGaugeFunc,NewHistorgram,NewHistogramVec,NewSummary,NewSummaryVec}` + + cmd := exec.Command("faillint", "-paths", p, "./...") + cmd.Dir = ".." + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Failed: %s\n%s", err, out) + } +} diff --git a/ag_201_coredns/test/proxy_health_test.go b/ag_201_coredns/test/proxy_health_test.go new file mode 100644 index 0000000..877af28 --- /dev/null +++ b/ag_201_coredns/test/proxy_health_test.go @@ -0,0 +1,80 @@ +package test + +import ( + "testing" + "time" + + "github.com/miekg/dns" +) + +func TestProxyThreeWay(t *testing.T) { + // Run 3 CoreDNS server, 2 upstream ones and a proxy. 1 Upstream is unhealthy after 1 query, but after + // that we should still be able to send to the other one. + + // Backend CoreDNS's. + corefileUp1 := `example.org:0 { + erratic { + drop 2 + } + }` + + up1, err := CoreDNSServer(corefileUp1) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer up1.Stop() + + corefileUp2 := `example.org:0 { + whoami + }` + + up2, err := CoreDNSServer(corefileUp2) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer up2.Stop() + + addr1, _ := CoreDNSServerPorts(up1, 0) + if addr1 == "" { + t.Fatalf("Could not get UDP listening port") + } + addr2, _ := CoreDNSServerPorts(up2, 0) + if addr2 == "" { + t.Fatalf("Could not get UDP listening port") + } + + // Proxying CoreDNS. + corefileProxy := `example.org:0 { + forward . ` + addr1 + " " + addr2 + ` { + max_fails 1 + } + }` + + prx, err := CoreDNSServer(corefileProxy) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer prx.Stop() + addr, _ := CoreDNSServerPorts(prx, 0) + if addr == "" { + t.Fatalf("Could not get UDP listening port") + } + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + c := new(dns.Client) + c.Timeout = 10 * time.Millisecond + + for i := 0; i < 10; i++ { + r, _, err := c.Exchange(m, addr) + if err != nil { + continue + } + // We would previously get SERVFAIL, so just getting answers here + // is a good sign. The actual timeouts are handled in the err != nil case + // above. + if r.Rcode != dns.RcodeSuccess { + t.Fatalf("Expected success rcode, got %d", r.Rcode) + } + } +} diff --git a/ag_201_coredns/test/proxy_test.go b/ag_201_coredns/test/proxy_test.go new file mode 100644 index 0000000..fd74100 --- /dev/null +++ b/ag_201_coredns/test/proxy_test.go @@ -0,0 +1,79 @@ +package test + +import ( + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestLookupProxy(t *testing.T) { + t.Parallel() + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + corefile := `example.org:0 { + file ` + name + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + // expect answer section with A record in it + if len(resp.Answer) == 0 { + t.Fatalf("Expected to at least one RR in the answer section, got none: %s", resp) + } + if resp.Answer[0].Header().Rrtype != dns.TypeA { + t.Errorf("Expected RR to A, got: %d", resp.Answer[0].Header().Rrtype) + } + if resp.Answer[0].(*dns.A).A.String() != "127.0.0.1" { + t.Errorf("Expected 127.0.0.1, got: %s", resp.Answer[0].(*dns.A).A.String()) + } +} + +func BenchmarkProxyLookup(b *testing.B) { + t := new(testing.T) + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to created zone: %s", err) + } + defer rm() + + corefile := `example.org:0 { + file ` + name + ` + }` + + i, err := CoreDNSServer(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + udp, _ := CoreDNSServerPorts(i, 0) + if udp == "" { + t.Fatalf("Could not get udp listening port") + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeA) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := dns.Exchange(m, udp); err != nil { + b.Fatal("Expected to receive reply, but didn't") + } + } +} diff --git a/ag_201_coredns/test/readme_test.go b/ag_201_coredns/test/readme_test.go new file mode 100644 index 0000000..4012a21 --- /dev/null +++ b/ag_201_coredns/test/readme_test.go @@ -0,0 +1,182 @@ +package test + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" +) + +// As we use the filesystem as-is, these files need to exist ON DISK for the readme test to work. This is especially +// useful for the *file* and *dnssec* plugins as their Corefiles are now tested as well. We create files in the +// current dir for all these, meaning the example READMEs MUST use relative path in their READMEs. +var contents = map[string]string{ + "Kexample.org.+013+45330.key": examplePub, + "Kexample.org.+013+45330.private": examplePriv, + "example.org.signed": exampleOrg, // not signed, but does not matter for this test. +} + +const ( + examplePub = `example.org. IN DNSKEY 256 3 13 eNMYFZYb6e0oJOV47IPo5f/UHy7wY9aBebotvcKakIYLyyGscBmXJQhbKLt/LhrMNDE2Q96hQnI5PdTBeOLzhQ== +` + examplePriv = `Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: f03VplaIEA+KHI9uizlemUSbUJH86hPBPjmcUninPoM= +` +) + +// TestReadme parses all README.mds of the plugins and checks if every example Corefile +// actually works. Each corefile snippet is only used if the language is set to 'corefile': +// +// ~~~ corefile +// . { +// # check-this-please +// } +// ~~~ +// +// While we're at it - we also check the README.md itself. It should at least have the sections: +// Name, Description, Syntax and Examples. See plugin.md for more details. +func TestReadme(t *testing.T) { + port := 30053 + caddy.Quiet = true + dnsserver.Quiet = true + + create(contents) + defer remove(contents) + + middle := filepath.Join("..", "plugin") + dirs, err := os.ReadDir(middle) + if err != nil { + t.Fatalf("Could not read %s: %q", middle, err) + } + for _, d := range dirs { + if !d.IsDir() { + continue + } + readme := filepath.Join(middle, d.Name()) + readme = filepath.Join(readme, "README.md") + + if err := sectionsFromReadme(readme); err != nil { + t.Fatal(err) + } + + inputs, err := corefileFromReadme(readme) + if err != nil { + continue + } + + // Test each snippet. + for _, in := range inputs { + dnsserver.Port = strconv.Itoa(port) + server, err := caddy.Start(in) + if err != nil { + t.Errorf("Failed to start server with %s, for input %q:\n%s", readme, err, in.Body()) + } + server.Stop() + port++ + } + } +} + +// corefileFromReadme parses a readme and returns all fragments that +// have ~~~ corefile (or ``` corefile). +func corefileFromReadme(readme string) ([]*Input, error) { + f, err := os.Open(readme) + if err != nil { + return nil, err + } + defer f.Close() + + s := bufio.NewScanner(f) + input := []*Input{} + corefile := false + temp := "" + + for s.Scan() { + line := s.Text() + if line == "~~~ corefile" || line == "``` corefile" { + corefile = true + continue + } + + if corefile && (line == "~~~" || line == "```") { + // last line + input = append(input, NewInput(temp)) + + temp = "" + corefile = false + continue + } + + if corefile { + temp += line + "\n" // read newline stripped by s.Text() + } + } + + if err := s.Err(); err != nil { + return nil, err + } + return input, nil +} + +// sectionsFromReadme returns an error if the readme doesn't contains all +// mandatory sections. The check is basic, as we match each line, this mostly +// works, because markdown is such a simple format. +// We want: Name, Description, Syntax, Examples - in this order. +func sectionsFromReadme(readme string) error { + f, err := os.Open(readme) + if err != nil { + return nil // don't error when we can read the file + } + defer f.Close() + + section := 0 + s := bufio.NewScanner(f) + for s.Scan() { + line := s.Text() + if strings.HasPrefix(line, "## Also See") { + return fmt.Errorf("Please use %q instead of %q", "See Also", "Also See") + } + + switch section { + case 0: + if strings.HasPrefix(line, "## Name") { + section++ + } + case 1: + if strings.HasPrefix(line, "## Description") { + section++ + } + case 2: + if strings.HasPrefix(line, "## Syntax") { + section++ + } + case 3: + if strings.HasPrefix(line, "## Examples") { + section++ + } + } + } + if section != 4 { + return fmt.Errorf("Sections incomplete or ordered wrong: %q, want (at least): Name, Descripion, Syntax and Examples", readme) + } + return nil +} + +func create(c map[string]string) { + for name, content := range c { + os.WriteFile(name, []byte(content), 0644) + } +} + +func remove(c map[string]string) { + for name := range c { + os.Remove(name) + } +} diff --git a/ag_201_coredns/test/reload_test.go b/ag_201_coredns/test/reload_test.go new file mode 100644 index 0000000..3c70163 --- /dev/null +++ b/ag_201_coredns/test/reload_test.go @@ -0,0 +1,392 @@ +package test + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "strings" + "testing" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + + "github.com/miekg/dns" +) + +func TestReload(t *testing.T) { + corefile := `.:0 { + whoami + }` + + coreInput := NewInput(corefile) + + c, err := CoreDNSServer(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + udp, _ := CoreDNSServerPorts(c, 0) + + send(t, udp) + + c1, err := c.Restart(coreInput) + if err != nil { + t.Fatal(err) + } + udp, _ = CoreDNSServerPorts(c1, 0) + + send(t, udp) + + c1.Stop() +} + +func send(t *testing.T, server string) { + m := new(dns.Msg) + m.SetQuestion("whoami.example.org.", dns.TypeSRV) + + r, err := dns.Exchange(m, server) + if err != nil { + // This seems to fail a lot on travis, quick'n dirty: redo + r, err = dns.Exchange(m, server) + if err != nil { + return + } + } + if r.Rcode != dns.RcodeSuccess { + t.Fatalf("Expected successful reply, got %s", dns.RcodeToString[r.Rcode]) + } + if len(r.Extra) != 2 { + t.Fatalf("Expected 2 RRs in additional, got %d", len(r.Extra)) + } +} + +func TestReloadHealth(t *testing.T) { + corefile := `.:0 { + health 127.0.0.1:52182 + whoami + }` + + c, err := CoreDNSServer(corefile) + if err != nil { + if strings.Contains(err.Error(), inUse) { + return // meh, but don't error + } + t.Fatalf("Could not get service instance: %s", err) + } + + if c1, err := c.Restart(NewInput(corefile)); err != nil { + t.Fatal(err) + } else { + c1.Stop() + } +} + +func TestReloadMetricsHealth(t *testing.T) { + corefile := `.:0 { + prometheus 127.0.0.1:53183 + health 127.0.0.1:53184 + whoami + }` + + c, err := CoreDNSServer(corefile) + if err != nil { + if strings.Contains(err.Error(), inUse) { + return // meh, but don't error + } + t.Fatalf("Could not get service instance: %s", err) + } + + c1, err := c.Restart(NewInput(corefile)) + if err != nil { + t.Fatal(err) + } + defer c1.Stop() + + // Health + resp, err := http.Get("http://localhost:53184/health") + if err != nil { + t.Fatal(err) + } + ok, _ := io.ReadAll(resp.Body) + resp.Body.Close() + if string(ok) != http.StatusText(http.StatusOK) { + t.Errorf("Failed to receive OK, got %s", ok) + } + + // Metrics + resp, err = http.Get("http://localhost:53183/metrics") + if err != nil { + t.Fatal(err) + } + const proc = "coredns_build_info" + metrics, _ := io.ReadAll(resp.Body) + if !bytes.Contains(metrics, []byte(proc)) { + t.Errorf("Failed to see %s in metric output", proc) + } +} + +func collectMetricsInfo(addr string, procs ...string) error { + cl := &http.Client{} + resp, err := cl.Get(fmt.Sprintf("http://%s/metrics", addr)) + if err != nil { + return err + } + metrics, _ := io.ReadAll(resp.Body) + for _, p := range procs { + if !bytes.Contains(metrics, []byte(p)) { + return fmt.Errorf("failed to see %s in metric output \n%s", p, metrics) + } + } + return nil +} + +// TestReloadSeveralTimeMetrics ensures that metrics are not pushed to +// prometheus once the metrics plugin is removed and a coredns +// reload is triggered +// 1. check that metrics have not been exported to prometheus before coredns starts +// 2. ensure that build-related metrics have been exported once coredns starts +// 3. trigger multiple reloads without changing the corefile +// 4. remove the metrics plugin and trigger a final reload +// 5. ensure the original prometheus exporter has not received more metrics +func TestReloadSeveralTimeMetrics(t *testing.T) { + //TODO: add a tool that find an available port because this needs to be a port + // that is not used in another test + promAddress := "127.0.0.1:53185" + proc := "coredns_build_info" + corefileWithMetrics := `.:0 { + prometheus ` + promAddress + ` + whoami + }` + corefileWithoutMetrics := `.:0 { + whoami + }` + + if err := collectMetricsInfo(promAddress, proc); err == nil { + t.Errorf("Prometheus is listening before the test started") + } + serverWithMetrics, err := CoreDNSServer(corefileWithMetrics) + if err != nil { + if strings.Contains(err.Error(), inUse) { + return + } + t.Errorf("Could not get service instance: %s", err) + } + // verify prometheus is running + if err := collectMetricsInfo(promAddress, proc); err != nil { + t.Errorf("Prometheus is not listening : %s", err) + } + reloadCount := 2 + for i := 0; i < reloadCount; i++ { + serverReload, err := serverWithMetrics.Restart( + NewInput(corefileWithMetrics), + ) + if err != nil { + t.Errorf("Could not restart CoreDNS : %s, at loop %v", err, i) + } + if err := collectMetricsInfo(promAddress, proc); err != nil { + t.Errorf("Prometheus is not listening : %s", err) + } + serverWithMetrics = serverReload + } + // reload without prometheus + serverWithoutMetrics, err := serverWithMetrics.Restart( + NewInput(corefileWithoutMetrics), + ) + if err != nil { + t.Errorf("Could not restart a second time CoreDNS : %s", err) + } + serverWithoutMetrics.Stop() + // verify that metrics have not been pushed + if err := collectMetricsInfo(promAddress, proc); err == nil { + t.Errorf("Prometheus is still listening") + } +} + +func TestMetricsAvailableAfterReload(t *testing.T) { + //TODO: add a tool that find an available port because this needs to be a port + // that is not used in another test + promAddress := "127.0.0.1:53186" + procMetric := "coredns_build_info" + procCache := "coredns_cache_entries" + procForward := "coredns_dns_request_duration_seconds" + corefileWithMetrics := `.:0 { + prometheus ` + promAddress + ` + cache + forward . 8.8.8.8 { + force_tcp + } + }` + + inst, _, tcp, err := CoreDNSServerAndPorts(corefileWithMetrics) + if err != nil { + if strings.Contains(err.Error(), inUse) { + return + } + t.Errorf("Could not get service instance: %s", err) + } + // send a query and check we can scrap corresponding metrics + cl := dns.Client{Net: "tcp"} + m := new(dns.Msg) + m.SetQuestion("www.example.org.", dns.TypeA) + + if _, _, err := cl.Exchange(m, tcp); err != nil { + t.Fatalf("Could not send message: %s", err) + } + + // we should have metrics from forward, cache, and metrics itself + if err := collectMetricsInfo(promAddress, procMetric, procCache, procForward); err != nil { + t.Errorf("Could not scrap one of expected stats : %s", err) + } + + // now reload + instReload, err := inst.Restart( + NewInput(corefileWithMetrics), + ) + if err != nil { + t.Errorf("Could not restart CoreDNS : %s", err) + instReload = inst + } + + // check the metrics are available still + if err := collectMetricsInfo(promAddress, procMetric, procCache, procForward); err != nil { + t.Errorf("Could not scrap one of expected stats : %s", err) + } + + instReload.Stop() + // verify that metrics have not been pushed +} + +func TestMetricsAvailableAfterReloadAndFailedReload(t *testing.T) { + //TODO: add a tool that find an available port because this needs to be a port + // that is not used in another test + promAddress := "127.0.0.1:53187" + procMetric := "coredns_build_info" + procCache := "coredns_cache_entries" + procForward := "coredns_dns_request_duration_seconds" + corefileWithMetrics := `.:0 { + prometheus ` + promAddress + ` + cache + forward . 8.8.8.8 { + force_tcp + } + }` + invalidCorefileWithMetrics := `.:0 { + prometheus ` + promAddress + ` + cache + forward . 8.8.8.8 { + force_tcp + } + invalid + }` + + inst, _, tcp, err := CoreDNSServerAndPorts(corefileWithMetrics) + if err != nil { + if strings.Contains(err.Error(), inUse) { + return + } + t.Errorf("Could not get service instance: %s", err) + } + // send a query and check we can scrap corresponding metrics + cl := dns.Client{Net: "tcp"} + m := new(dns.Msg) + m.SetQuestion("www.example.org.", dns.TypeA) + + if _, _, err := cl.Exchange(m, tcp); err != nil { + t.Fatalf("Could not send message: %s", err) + } + + // we should have metrics from forward, cache, and metrics itself + if err := collectMetricsInfo(promAddress, procMetric, procCache, procForward); err != nil { + t.Errorf("Could not scrap one of expected stats : %s", err) + } + + for i := 0; i < 2; i++ { + // now provide a failed reload + invInst, err := inst.Restart( + NewInput(invalidCorefileWithMetrics), + ) + if err == nil { + t.Errorf("Invalid test - this reload should fail") + inst = invInst + } + } + + // now reload with correct corefile + instReload, err := inst.Restart( + NewInput(corefileWithMetrics), + ) + if err != nil { + t.Errorf("Could not restart CoreDNS : %s", err) + instReload = inst + } + + // check the metrics are available still + if err := collectMetricsInfo(promAddress, procMetric, procCache, procForward); err != nil { + t.Errorf("Could not scrap one of expected stats : %s", err) + } + + instReload.Stop() + // verify that metrics have not been pushed +} + +// TestReloadUnreadyPlugin tests that the ready plugin properly resets the list of readiness implementors during a reload. +// If it fails to do so, ready will respond with duplicate plugin names after a reload (e.g. in this test "unready,unready"). +func TestReloadUnreadyPlugin(t *testing.T) { + // Add/Register a perpetually unready plugin + dnsserver.Directives = append([]string{"unready"}, dnsserver.Directives...) + u := new(unready) + plugin.Register("unready", func(c *caddy.Controller) error { + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + u.next = next + return u + }) + return nil + }) + + corefile := `.:0 { + unready + whoami + ready 127.0.0.1:53185 + }` + + coreInput := NewInput(corefile) + + c, err := CoreDNSServer(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + c1, err := c.Restart(coreInput) + if err != nil { + t.Fatal(err) + } + + resp, err := http.Get("http://127.0.0.1:53185/ready") + if err != nil { + t.Fatal(err) + } + bod, _ := io.ReadAll(resp.Body) + resp.Body.Close() + if string(bod) != u.Name() { + t.Errorf("Expected /ready endpoint response body %q, got %q", u.Name(), bod) + } + + c1.Stop() +} + +type unready struct { + next plugin.Handler +} + +func (u *unready) Ready() bool { return false } + +func (u *unready) Name() string { return "unready" } + +func (u *unready) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + return u.next.ServeDNS(ctx, w, r) +} + +const inUse = "address already in use" diff --git a/ag_201_coredns/test/reverse_test.go b/ag_201_coredns/test/reverse_test.go new file mode 100644 index 0000000..f778911 --- /dev/null +++ b/ag_201_coredns/test/reverse_test.go @@ -0,0 +1,42 @@ +package test + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestReverseCorefile(t *testing.T) { + corefile := `10.0.0.0/24:0 { + whoami + }` + + i, err := CoreDNSServer(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + udp, _ := CoreDNSServerPorts(i, 0) + if udp == "" { + t.Fatalf("Could not get UDP listening port") + } + + m := new(dns.Msg) + m.SetQuestion("17.0.0.10.in-addr.arpa.", dns.TypePTR) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + + if len(resp.Extra) != 2 { + t.Fatal("Expected to at least two RRs in the extra section, got none") + } + // Second one is SRV, first one can be A or AAAA depending on system. + if resp.Extra[1].Header().Rrtype != dns.TypeSRV { + t.Errorf("Expected RR to SRV, got: %d", resp.Extra[1].Header().Rrtype) + } + if resp.Extra[1].Header().Name != "_udp.17.0.0.10.in-addr.arpa." { + t.Errorf("Expected _udp.17.0.0.10.in-addr.arpa. got: %s", resp.Extra[1].Header().Name) + } +} diff --git a/ag_201_coredns/test/rewrite_test.go b/ag_201_coredns/test/rewrite_test.go new file mode 100644 index 0000000..d9ef09b --- /dev/null +++ b/ag_201_coredns/test/rewrite_test.go @@ -0,0 +1,114 @@ +package test + +import ( + "bytes" + "testing" + + "github.com/miekg/dns" +) + +func TestRewriteFailure(t *testing.T) { + t.Parallel() + i, udp, _, err := CoreDNSServerAndPorts(`.:0 { + rewrite name regex (.*)\.test\.$ {1}. answer auto + # no next plugin to induce SERVFAIL + }`) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.test.", dns.TypeMX) + + r, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + + if len(r.Question) == 0 { + t.Error("Invalid empty question section") + } + if r.Question[0].Name != "example.test." { + t.Errorf("Question section mismatch. expected \"example.test.\" got %q", r.Question[0].Name) + } +} + +func TestRewrite(t *testing.T) { + t.Parallel() + corefile := `.:0 { + rewrite type MX a + rewrite edns0 local set 0xffee hello-world + erratic . { + drop 0 + } + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + defer i.Stop() + + testMX(t, udp) + testEdns0(t, udp) +} + +func testMX(t *testing.T, server string) { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeMX) + + r, err := dns.Exchange(m, server) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + + // expect answer section with A record in it + if len(r.Answer) == 0 { + t.Error("Expected to at least one RR in the answer section, got none") + } + if r.Answer[0].Header().Rrtype != dns.TypeA { + t.Errorf("Expected RR to A, got: %d", r.Answer[0].Header().Rrtype) + } + if r.Answer[0].(*dns.A).A.String() != "192.0.2.53" { + t.Errorf("Expected 192.0.2.53, got: %s", r.Answer[0].(*dns.A).A.String()) + } +} + +func testEdns0(t *testing.T, server string) { + m := new(dns.Msg) + m.SetQuestion("example.com.", dns.TypeA) + + r, err := dns.Exchange(m, server) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + + // expect answer section with A record in it + if len(r.Answer) == 0 { + t.Error("Expected to at least one RR in the answer section, got none") + } + if r.Answer[0].Header().Rrtype != dns.TypeA { + t.Errorf("Expected RR to A, got: %d", r.Answer[0].Header().Rrtype) + } + if r.Answer[0].(*dns.A).A.String() != "192.0.2.53" { + t.Errorf("Expected 192.0.2.53, got: %s", r.Answer[0].(*dns.A).A.String()) + } + o := r.IsEdns0() + if o == nil || len(o.Option) == 0 { + t.Error("Expected EDNS0 options but got none") + } else { + if e, ok := o.Option[0].(*dns.EDNS0_LOCAL); ok { + if e.Code != 0xffee { + t.Errorf("Expected EDNS_LOCAL code 0xffee but got %x", e.Code) + } + if !bytes.Equal(e.Data, []byte("hello-world")) { + t.Errorf("Expected EDNS_LOCAL data 'hello-world' but got %q", e.Data) + } + } else { + t.Errorf("Expected EDNS0_LOCAL but got %v", o.Option[0]) + } + } +} diff --git a/ag_201_coredns/test/secondary_test.go b/ag_201_coredns/test/secondary_test.go new file mode 100644 index 0000000..8e99f0d --- /dev/null +++ b/ag_201_coredns/test/secondary_test.go @@ -0,0 +1,217 @@ +package test + +import ( + "testing" + "time" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestEmptySecondaryZone(t *testing.T) { + // Corefile that fails to transfer example.org. + corefile := `example.org:0 { + secondary { + transfer from 127.0.0.1:1717 + } + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("www.example.org.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + if resp.Rcode != dns.RcodeServerFailure { + t.Fatalf("Expected reply to be a SERVFAIL, got %d", resp.Rcode) + } +} + +func TestSecondaryZoneTransfer(t *testing.T) { + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + corefile := `example.org:0 { + file ` + name + ` { + } + transfer { + to * + } + }` + + i, _, tcp, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + corefile = `example.org:0 { + secondary { + transfer from ` + tcp + ` + } + }` + + i1, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i1.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeSOA) + + var r *dns.Msg + // This is now async; we need to wait for it to be transferred. + for i := 0; i < 10; i++ { + r, _ = dns.Exchange(m, udp) + if len(r.Answer) != 0 { + break + } + time.Sleep(100 * time.Microsecond) + } + if len(r.Answer) == 0 { + t.Fatalf("Expected answer section") + } +} + +func TestIxfrResponse(t *testing.T) { + // ixfr query with current soa should return single packet with that soa (no transfer needed). + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + corefile := `example.org:0 { + file ` + name + ` { + } + transfer { + to * + } + }` + + i, _, tcp, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("example.org.", dns.TypeIXFR) + m.Ns = []dns.RR{test.SOA("example.org. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600")} // copied from exampleOrg + + var r *dns.Msg + c := new(dns.Client) + c.Net = "tcp" + // This is now async; we need to wait for it to be transferred. + for i := 0; i < 10; i++ { + r, _, _ = c.Exchange(m, tcp) + if len(r.Answer) != 0 { + break + } + time.Sleep(100 * time.Microsecond) + } + if len(r.Answer) != 1 { + t.Fatalf("Expected answer section with single RR") + } + soa, ok := r.Answer[0].(*dns.SOA) + if !ok { + t.Fatalf("Expected answer section with SOA RR") + } + if soa.Serial != 2015082541 { + t.Fatalf("Serial should be %d, got %d", 2015082541, soa.Serial) + } +} + +func TestRetryInitialTransfer(t *testing.T) { + // Start up a secondary that expects to transfer from a master that doesn't exist yet + corefile := `example.org:0 { + secondary { + transfer from 127.0.0.1:5399 + } + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("www.example.org.", dns.TypeA) + resp, err := dns.Exchange(m, udp) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + // Expect that the query will fail + if resp.Rcode != dns.RcodeServerFailure { + t.Fatalf("Expected reply to be a SERVFAIL, got %d", resp.Rcode) + } + + // Now spin up the master server + name, rm, err := test.TempFile(".", `$ORIGIN example.org. +@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. ( + 2017042745 ; serial + 7200 ; refresh (2 hours) + 3600 ; retry (1 hour) + 1209600 ; expire (2 weeks) + 3600 ; minimum (1 hour) +) + + 3600 IN NS a.iana-servers.net. + 3600 IN NS b.iana-servers.net. + +www IN A 127.0.0.1 +www IN AAAA ::1 +`) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + corefileMaster := `example.org:5399 { + file ` + name + ` + transfer { + to * + } + }` + + master, _, _, err := CoreDNSServerAndPorts(corefileMaster) + if err != nil { + t.Fatalf("Could not start CoreDNS master: %s", err) + } + defer master.Stop() + + retry := time.Tick(time.Millisecond * 100) + timeout := time.Tick(time.Second * 5) + + for { + select { + case <-retry: + m = new(dns.Msg) + m.SetQuestion("www.example.org.", dns.TypeA) + resp, err = dns.Exchange(m, udp) + if err != nil { + continue + } + // Expect the query to succeed + if resp.Rcode != dns.RcodeSuccess { + continue + } + return + case <-timeout: + t.Fatal("Timed out trying for successful response.") + return + } + } +} diff --git a/ag_201_coredns/test/server.go b/ag_201_coredns/test/server.go new file mode 100644 index 0000000..ac020d3 --- /dev/null +++ b/ag_201_coredns/test/server.go @@ -0,0 +1,74 @@ +package test + +import ( + "sync" + + "github.com/coredns/caddy" + _ "github.com/coredns/coredns/core" // Hook in CoreDNS. + "github.com/coredns/coredns/core/dnsserver" + _ "github.com/coredns/coredns/core/plugin" // Load all managed plugins in github.com/coredns/coredns. +) + +var mu sync.Mutex + +// CoreDNSServer returns a CoreDNS test server. It just takes a normal Corefile as input. +func CoreDNSServer(corefile string) (*caddy.Instance, error) { + mu.Lock() + defer mu.Unlock() + caddy.Quiet = true + dnsserver.Quiet = true + + return caddy.Start(NewInput(corefile)) +} + +// CoreDNSServerStop stops a server. +func CoreDNSServerStop(i *caddy.Instance) { i.Stop() } + +// CoreDNSServerPorts returns the ports the instance is listening on. The integer k indicates +// which ServerListener you want. +func CoreDNSServerPorts(i *caddy.Instance, k int) (udp, tcp string) { + srvs := i.Servers() + if len(srvs) < k+1 { + return "", "" + } + u := srvs[k].LocalAddr() + t := srvs[k].Addr() + + if u != nil { + udp = u.String() + } + if t != nil { + tcp = t.String() + } + return +} + +// CoreDNSServerAndPorts combines CoreDNSServer and CoreDNSServerPorts to start a CoreDNS +// server and returns the udp and tcp ports of the first instance. +func CoreDNSServerAndPorts(corefile string) (i *caddy.Instance, udp, tcp string, err error) { + i, err = CoreDNSServer(corefile) + if err != nil { + return nil, "", "", err + } + udp, tcp = CoreDNSServerPorts(i, 0) + return i, udp, tcp, nil +} + +// Input implements the caddy.Input interface and acts as an easy way to use a string as a Corefile. +type Input struct { + corefile []byte +} + +// NewInput returns a pointer to Input, containing the corefile string as input. +func NewInput(corefile string) *Input { + return &Input{corefile: []byte(corefile)} +} + +// Body implements the Input interface. +func (i *Input) Body() []byte { return i.corefile } + +// Path implements the Input interface. +func (i *Input) Path() string { return "Corefile" } + +// ServerType implements the Input interface. +func (i *Input) ServerType() string { return "dns" } diff --git a/ag_201_coredns/test/server_reverse_test.go b/ag_201_coredns/test/server_reverse_test.go new file mode 100644 index 0000000..4d69b1b --- /dev/null +++ b/ag_201_coredns/test/server_reverse_test.go @@ -0,0 +1,141 @@ +package test + +import ( + "strings" + "testing" + + "github.com/miekg/dns" +) + +func TestClasslessReverse(t *testing.T) { + // 25 -> so anything above 1.127 won't be answered, below is OK. + corefile := `192.168.1.0/25:0 { + whoami + }` + + s, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer s.Stop() + + tests := []struct { + addr string + rcode int + }{ + {"192.168.1.0", dns.RcodeSuccess}, // in range + {"192.168.1.1", dns.RcodeSuccess}, // in range + {"192.168.1.127", dns.RcodeSuccess}, // in range + + {"192.168.1.128", dns.RcodeRefused}, // out of range + {"192.168.1.129", dns.RcodeRefused}, // out of range + {"192.168.1.255", dns.RcodeRefused}, // out of range + {"192.168.2.0", dns.RcodeRefused}, // different zone + } + + m := new(dns.Msg) + for i, tc := range tests { + inaddr, _ := dns.ReverseAddr(tc.addr) + m.SetQuestion(inaddr, dns.TypeA) + + r, e := dns.Exchange(m, udp) + if e != nil { + t.Errorf("Test %d, expected no error, got %q", i, e) + } + if r.Rcode != tc.rcode { + t.Errorf("Test %d, expected %d, got %d for %s", i, tc.rcode, r.Rcode, tc.addr) + } + } +} + +func TestReverse(t *testing.T) { + corefile := `192.168.1.0/24:0 { + whoami + }` + + s, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer s.Stop() + + tests := []struct { + addr string + rcode int + }{ + {"192.168.1.0", dns.RcodeSuccess}, + {"192.168.1.1", dns.RcodeSuccess}, + {"192.168.1.127", dns.RcodeSuccess}, + {"192.168.1.128", dns.RcodeSuccess}, + {"1.168.192.in-addr.arpa.", dns.RcodeSuccess}, + + {"2.168.192.in-addr.arpa.", dns.RcodeRefused}, + } + + m := new(dns.Msg) + for i, tc := range tests { + if !strings.HasSuffix(tc.addr, ".arpa.") { + inaddr, err := dns.ReverseAddr(tc.addr) + if err != nil { + t.Fatalf("Test %d, failed to convert %s", i, tc.addr) + } + tc.addr = inaddr + } + + m.SetQuestion(tc.addr, dns.TypeA) + + r, e := dns.Exchange(m, udp) + if e != nil { + t.Errorf("Test %d, expected no error, got %q", i, e) + } + if r.Rcode != tc.rcode { + t.Errorf("Test %d, expected %d, got %d for %s", i, tc.rcode, r.Rcode, tc.addr) + } + } +} + +func TestReverseInAddr(t *testing.T) { + corefile := `1.168.192.in-addr.arpa:0 { + whoami + }` + + s, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer s.Stop() + + tests := []struct { + addr string + rcode int + }{ + {"192.168.1.0", dns.RcodeSuccess}, + {"192.168.1.1", dns.RcodeSuccess}, + {"192.168.1.127", dns.RcodeSuccess}, + {"192.168.1.128", dns.RcodeSuccess}, + {"1.168.192.in-addr.arpa.", dns.RcodeSuccess}, + + {"2.168.192.in-addr.arpa.", dns.RcodeRefused}, + } + + m := new(dns.Msg) + for i, tc := range tests { + if !strings.HasSuffix(tc.addr, ".arpa.") { + inaddr, err := dns.ReverseAddr(tc.addr) + if err != nil { + t.Fatalf("Test %d, failed to convert %s", i, tc.addr) + } + tc.addr = inaddr + } + + m.SetQuestion(tc.addr, dns.TypeA) + + r, e := dns.Exchange(m, udp) + if e != nil { + t.Errorf("Test %d, expected no error, got %q", i, e) + } + if r.Rcode != tc.rcode { + t.Errorf("Test %d, expected %d, got %d for %s", i, tc.rcode, r.Rcode, tc.addr) + } + } +} diff --git a/ag_201_coredns/test/server_test.go b/ag_201_coredns/test/server_test.go new file mode 100644 index 0000000..6fe1472 --- /dev/null +++ b/ag_201_coredns/test/server_test.go @@ -0,0 +1,160 @@ +package test + +import ( + "fmt" + "math/rand" + "reflect" + "testing" + "unsafe" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + + "github.com/miekg/dns" +) + +// Start 2 tests server, server A will proxy to B, server B is an CH server. +func TestProxyToChaosServer(t *testing.T) { + t.Parallel() + corefile := `.:0 { + chaos CoreDNS-001 miek@miek.nl + }` + + chaos, udpChaos, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + defer chaos.Stop() + + corefileProxy := `.:0 { + forward . ` + udpChaos + ` + }` + + proxy, udp, _, err := CoreDNSServerAndPorts(corefileProxy) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance") + } + defer proxy.Stop() + + chaosTest(t, udpChaos) + + chaosTest(t, udp) + // chaosTest(t, tcp, "tcp"), commented out because we use the original transport to reach the + // proxy and we only forward to the udp port. +} + +func chaosTest(t *testing.T, server string) { + m := new(dns.Msg) + m.Question = make([]dns.Question, 1) + m.Question[0] = dns.Question{Qclass: dns.ClassCHAOS, Name: "version.bind.", Qtype: dns.TypeTXT} + + r, err := dns.Exchange(m, server) + if err != nil { + t.Fatalf("Could not send message: %s", err) + } + if r.Rcode != dns.RcodeSuccess || len(r.Answer) == 0 { + t.Fatalf("Expected successful reply, got %s", dns.RcodeToString[r.Rcode]) + } + if r.Answer[0].String() != `version.bind. 0 CH TXT "CoreDNS-001"` { + t.Fatalf("Expected version.bind. reply, got %s", r.Answer[0].String()) + } +} + +func TestReverseExpansion(t *testing.T) { + // this test needs a fixed port, because with :0 the expanded reverse zone will listen on different + // addresses and we can't check which ones... + corefile := `10.0.0.0/15:5053 { + whoami + }` + + server, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + defer server.Stop() + + m := new(dns.Msg) + m.SetQuestion("whoami.0.10.in-addr.arpa.", dns.TypeA) + + r, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not send message: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + t.Errorf("Expected NOERROR, got %d", r.Rcode) + } + if len(r.Extra) != 2 { + t.Errorf("Expected 2 RRs in additional section, got %d", len(r.Extra)) + } + + m.SetQuestion("whoami.1.10.in-addr.arpa.", dns.TypeA) + r, err = dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not send message: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + t.Errorf("Expected NOERROR, got %d", r.Rcode) + } + if len(r.Extra) != 2 { + t.Errorf("Expected 2 RRs in additional section, got %d", len(r.Extra)) + } + + // should be refused + m.SetQuestion("whoami.2.10.in-addr.arpa.", dns.TypeA) + r, err = dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not send message: %s", err) + } + if r.Rcode != dns.RcodeRefused { + t.Errorf("Expected REFUSED, got %d", r.Rcode) + } + if len(r.Extra) != 0 { + t.Errorf("Expected 0 RRs in additional section, got %d", len(r.Extra)) + } +} + +func TestMultiZoneBlockConfigs(t *testing.T) { + // We need fixed port numbers here to have multiple serving instances, using ".:0" wont work because that + // leads to a 'duplicate server instances' because '0' is used literary (only the kernel knows what port will + // be assigned). + // + // This makes the test flaky because we don't know if there are in-use or not. We add a random number to each base and + // retry when we fail to get a serving instance (up to 3 times). + + var ( + server *caddy.Instance + err error + ) + for j := 0; j < 3; j++ { + corefile := `.:%d .:%d .:%d { + debug + }` + corefile = fmt.Sprintf(corefile, 40000+rand.Intn(9000), 50000+rand.Intn(9000), 60000+rand.Intn(9000)) + + if server, err = CoreDNSServer(corefile); err != nil { + continue + } + t.Logf("Got running CoreDNS serving instance, after %d tries", j+1) + break // success + } + if server == nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer server.Stop() + + // unsafe reflection to read unexported fields "context" and "configs" within context + ctxVal := reflect.ValueOf(server).Elem().FieldByName("context") + ctxVal2 := reflect.NewAt(ctxVal.Type(), unsafe.Pointer(ctxVal.UnsafeAddr())).Elem() + configs := reflect.ValueOf(ctxVal2.Interface()).Elem().FieldByName("configs") + configs2 := reflect.NewAt(configs.Type(), unsafe.Pointer(configs.UnsafeAddr())).Elem() + + for i := 0; i < 3; i++ { + v := configs2.Index(i) + config := v.Interface().(*dnsserver.Config) + if !config.Debug { + t.Fatalf("Debug was not set for %s://%s:%s", config.Transport, config.Zone, config.Port) + } + } +} diff --git a/ag_201_coredns/test/template_upstream_test.go b/ag_201_coredns/test/template_upstream_test.go new file mode 100644 index 0000000..7939bff --- /dev/null +++ b/ag_201_coredns/test/template_upstream_test.go @@ -0,0 +1,71 @@ +package test + +import ( + "testing" + + "github.com/miekg/dns" +) + +func TestTemplateUpstream(t *testing.T) { + corefile := `.:0 { + # CNAME + template IN ANY cname.example.net. { + match ".*" + answer "cname.example.net. 60 IN CNAME target.example.net." + upstream + } + + # Target + template IN A target.example.net. { + match ".*" + answer "target.example.net. 60 IN A 1.2.3.4" + } + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + // Test that an A query returns a CNAME and an A record + m := new(dns.Msg) + m.SetQuestion("cname.example.net.", dns.TypeA) + m.SetEdns0(4096, true) // need this? + + r, err := dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not send msg: %s", err) + } + if r.Rcode == dns.RcodeServerFailure { + t.Fatalf("Rcode should not be dns.RcodeServerFailure") + } + if len(r.Answer) < 2 { + t.Fatalf("Expected 2 answers, got %v", len(r.Answer)) + } + if x := r.Answer[0].(*dns.CNAME).Target; x != "target.example.net." { + t.Fatalf("Failed to get address for CNAME, expected target.example.net. got %s", x) + } + if x := r.Answer[1].(*dns.A).A.String(); x != "1.2.3.4" { + t.Fatalf("Failed to get address for CNAME, expected 1.2.3.4 got %s", x) + } + + // Test that a CNAME query only returns a CNAME + m = new(dns.Msg) + m.SetQuestion("cname.example.net.", dns.TypeCNAME) + m.SetEdns0(4096, true) // need this? + + r, err = dns.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not send msg: %s", err) + } + if r.Rcode == dns.RcodeServerFailure { + t.Fatalf("Rcode should not be dns.RcodeServerFailure") + } + if len(r.Answer) < 1 { + t.Fatalf("Expected 1 answer, got %v", len(r.Answer)) + } + if x := r.Answer[0].(*dns.CNAME).Target; x != "target.example.net." { + t.Fatalf("Failed to get address for CNAME, expected target.example.net. got %s", x) + } +} diff --git a/ag_201_coredns/test/tls_test.go b/ag_201_coredns/test/tls_test.go new file mode 100644 index 0000000..f302d51 --- /dev/null +++ b/ag_201_coredns/test/tls_test.go @@ -0,0 +1,46 @@ +package test + +import ( + "crypto/tls" + "testing" + + "github.com/miekg/dns" +) + +func TestDNSoverTLS(t *testing.T) { + corefile := `tls://.:1053 { + tls ../plugin/tls/test_cert.pem ../plugin/tls/test_key.pem + whoami + }` + qname := "example.com." + qtype := dns.TypeA + answerLength := 0 + + ex, _, tcp, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer ex.Stop() + + m := new(dns.Msg) + m.SetQuestion(qname, qtype) + client := dns.Client{ + Net: "tcp-tls", + TLSConfig: &tls.Config{InsecureSkipVerify: true}, + } + r, _, err := client.Exchange(m, tcp) + + if err != nil { + t.Fatalf("Could not exchange msg: %s", err) + } + + if n := len(r.Answer); n != answerLength { + t.Fatalf("Expected %v answers, got %v", answerLength, n) + } + if n := len(r.Extra); n != 2 { + t.Errorf("Expected 2 RRs in additional section, but got %d", n) + } + if r.Rcode != dns.RcodeSuccess { + t.Errorf("Expected success but got %d", r.Rcode) + } +} diff --git a/ag_201_coredns/test/tsig_test.go b/ag_201_coredns/test/tsig_test.go new file mode 100644 index 0000000..19d5d03 --- /dev/null +++ b/ag_201_coredns/test/tsig_test.go @@ -0,0 +1,166 @@ +package test + +import ( + "testing" + "time" + + "github.com/miekg/dns" +) + +var tsigKey = "tsig.key." +var tsigSecret = "i9M+00yrECfVZG2qCjr4mPpaGim/Bq+IWMiNrLjUO4Y=" + +var corefile = `.:0 { + tsig { + secret ` + tsigKey + ` ` + tsigSecret + ` + } + hosts { + 1.2.3.4 test + } + }` + +func TestTsig(t *testing.T) { + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("test.", dns.TypeA) + m.SetTsig(tsigKey, dns.HmacSHA256, 300, time.Now().Unix()) + + client := dns.Client{Net: "udp", TsigSecret: map[string]string{tsigKey: tsigSecret}} + r, _, err := client.Exchange(m, udp) + if err != nil { + t.Fatalf("Could not send msg: %s", err) + } + if r.Rcode != dns.RcodeSuccess { + t.Fatalf("Rcode should be dns.RcodeSuccess") + } + tsig := r.IsTsig() + if tsig == nil { + t.Fatalf("Respose was not TSIG") + } + if tsig.Error != dns.RcodeSuccess { + t.Fatalf("TSIG Error code should be dns.RcodeSuccess") + } +} + +func TestTsigBadKey(t *testing.T) { + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("test.", dns.TypeA) + m.SetTsig("bad.key.", dns.HmacSHA256, 300, time.Now().Unix()) + + // rename client key to a key name the server doesnt have + client := dns.Client{Net: "udp", TsigSecret: map[string]string{"bad.key.": tsigSecret}} + r, _, err := client.Exchange(m, udp) + + if err != dns.ErrAuth { + t.Fatalf("Expected \"dns: bad authentication\" error, got: %s", err) + } + if r.Rcode != dns.RcodeNotAuth { + t.Fatalf("Rcode should be dns.RcodeNotAuth") + } + tsig := r.IsTsig() + if tsig == nil { + t.Fatalf("Respose was not TSIG") + } + if tsig.Error != dns.RcodeBadKey { + t.Fatalf("TSIG Error code should be dns.RcodeBadKey") + } + if tsig.MAC != "" { + t.Fatalf("TSIG MAC should be empty") + } + if tsig.MACSize != 0 { + t.Fatalf("TSIG MACSize should be 0") + } + if tsig.TimeSigned != 0 { + t.Fatalf("TSIG TimeSigned should be 0") + } +} + +func TestTsigBadSig(t *testing.T) { + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("test.", dns.TypeA) + m.SetTsig(tsigKey, dns.HmacSHA256, 300, time.Now().Unix()) + + // mangle the client secret so the sig wont match the server sig + client := dns.Client{Net: "udp", TsigSecret: map[string]string{tsigKey: "BADSIG00ECfVZG2qCjr4mPpaGim/Bq+IWMiNrLjUO4Y="}} + r, _, err := client.Exchange(m, udp) + + if err != dns.ErrAuth { + t.Fatalf("Expected \"dns: bad authentication\" error, got: %s", err) + } + if r.Rcode != dns.RcodeNotAuth { + t.Fatalf("Rcode should be dns.RcodeNotAuth") + } + tsig := r.IsTsig() + if tsig == nil { + t.Fatalf("Respose was not TSIG") + } + if tsig.Error != dns.RcodeBadSig { + t.Fatalf("TSIG Error code should be dns.RcodeBadSig") + } + if tsig.MAC != "" { + t.Fatalf("TSIG MAC should be empty") + } + if tsig.MACSize != 0 { + t.Fatalf("TSIG MACSize should be 0") + } + if tsig.TimeSigned != 0 { + t.Fatalf("TSIG TimeSigned should be 0") + } +} + +func TestTsigBadTime(t *testing.T) { + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + m := new(dns.Msg) + m.SetQuestion("test.", dns.TypeA) + + // set time to be older by > fudge seconds + m.SetTsig(tsigKey, dns.HmacSHA256, 300, time.Now().Unix()-600) + + client := dns.Client{Net: "udp", TsigSecret: map[string]string{tsigKey: tsigSecret}} + r, _, err := client.Exchange(m, udp) + + if err != dns.ErrAuth { + t.Fatalf("Expected \"dns: bad authentication\" error, got: %s", err) + } + if r.Rcode != dns.RcodeNotAuth { + t.Fatalf("Rcode should be dns.RcodeNotAuth") + } + tsig := r.IsTsig() + if tsig == nil { + t.Fatalf("Respose was not TSIG") + } + if tsig.Error != dns.RcodeBadTime { + t.Fatalf("TSIG Error code should be dns.RcodeBadTime") + } + if tsig.MAC == "" { + t.Fatalf("TSIG MAC should not be empty") + } + if tsig.MACSize != 32 { + t.Fatalf("TSIG MACSize should be 32") + } + if tsig.TimeSigned == 0 { + t.Fatalf("TSIG TimeSigned should not be 0") + } +} diff --git a/ag_201_coredns/test/view_test.go b/ag_201_coredns/test/view_test.go new file mode 100644 index 0000000..f685953 --- /dev/null +++ b/ag_201_coredns/test/view_test.go @@ -0,0 +1,163 @@ +package test + +import ( + "strings" + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestView(t *testing.T) { + // Hack to get an available port - We spin up a temporary dummy coredns on :0 to get the port number, then we re-use + // that one port consistently across all server blocks. + corefile := `example.org:0 { + erratic + }` + tmp, addr, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + port := addr[strings.LastIndex(addr, ":")+1:] + + // Corefile with test views + corefile = ` + # split-type config: splits quries for A/AAAA into separate views + split-type:` + port + ` { + view test-view-a { + expr type() == 'A' + } + hosts { + 1.2.3.4 test.split-type + } + } + split-type:` + port + ` { + view test-view-aaaa { + expr type() == 'AAAA' + } + hosts { + 1:2:3::4 test.split-type + } + } + + # split-name config: splits queries into separate views based on first label in query name ("one", "two") + split-name:` + port + ` { + view test-view-1 { + expr name() matches '^one\\..*\\.split-name\\.$' + } + hosts { + 1.1.1.1 one.test.split-name one.test.test.test.split-name + } + } + split-name:` + port + ` { + view test-view-2 { + expr name() matches '^two\\..*\\.split-name\\.$' + } + hosts { + 2.2.2.2 two.test.split-name two.test.test.test.split-name + } + } + split-name:` + port + ` { + hosts { + 3.3.3.3 default.test.split-name + } + } + + # metadata config: verifies that metadata is properly collected by the server, + # and that metadata function correctly looks up the value of the metadata. + metadata:` + port + ` { + metadata + view test-view-meta1 { + # This is never true + expr metadata('view/name') == 'not-the-view-name' + } + hosts { + 1.1.1.1 test.metadata + } + } + metadata:` + port + ` { + view test-view-meta2 { + # This is never true. The metadata plugin is not enabled in this server block so the metadata function returns + # an empty string + expr metadata('view/name') == 'test-view-meta2' + } + hosts { + 2.2.2.2 test.metadata + } + } + metadata:` + port + ` { + metadata + view test-view-meta3 { + # This is always true. Queries in the zone 'metadata.' should always be served using this view. + expr metadata('view/name') == 'test-view-meta3' + } + hosts { + 2.2.2.2 test.metadata + } + } + metadata:` + port + ` { + # This block should never be reached since the prior view in the same zone is always true + hosts { + 3.3.3.3 test.metadata + } + } + ` + + i, addr, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + // there are multiple sever blocks, but they are all on the same port, so it's a single server instance to stop + defer i.Stop() + // stop the temporary instance before starting tests. + tmp.Stop() + + viewTest(t, "split-type A", addr, "test.split-type.", dns.TypeA, dns.RcodeSuccess, + []dns.RR{test.A("test.split-type. 303 IN A 1.2.3.4")}) + + viewTest(t, "split-type AAAA", addr, "test.split-type.", dns.TypeAAAA, dns.RcodeSuccess, + []dns.RR{test.AAAA("test.split-type. 303 IN AAAA 1:2:3::4")}) + + viewTest(t, "split-name one.test.test.test.split-name", addr, "one.test.test.test.split-name.", dns.TypeA, dns.RcodeSuccess, + []dns.RR{test.A("one.test.test.test.split-name. 303 IN A 1.1.1.1")}) + + viewTest(t, "split-name one.test.split-name", addr, "one.test.split-name.", dns.TypeA, dns.RcodeSuccess, + []dns.RR{test.A("one.test.split-name. 303 IN A 1.1.1.1")}) + + viewTest(t, "split-name two.test.test.test.split-name", addr, "two.test.test.test.split-name.", dns.TypeA, dns.RcodeSuccess, + []dns.RR{test.A("two.test.test.test.split-name. 303 IN A 2.2.2.2")}) + + viewTest(t, "split-name two.test.split-name", addr, "two.test.split-name.", dns.TypeA, dns.RcodeSuccess, + []dns.RR{test.A("two.test.split-name. 303 IN A 2.2.2.2")}) + + viewTest(t, "split-name default.test.split-name", addr, "default.test.split-name.", dns.TypeA, dns.RcodeSuccess, + []dns.RR{test.A("default.test.split-name. 303 IN A 3.3.3.3")}) + + viewTest(t, "metadata test.metadata", addr, "test.metadata.", dns.TypeA, dns.RcodeSuccess, + []dns.RR{test.A("test.metadata. 303 IN A 2.2.2.2")}) +} + +func viewTest(t *testing.T, testName, addr, qname string, qtype uint16, expectRcode int, expectAnswers []dns.RR) { + t.Run(testName, func(t *testing.T) { + m := new(dns.Msg) + + m.SetQuestion(qname, qtype) + resp, err := dns.Exchange(m, addr) + if err != nil { + t.Fatalf("Expected to receive reply, but didn't: %s", err) + } + + tc := test.Case{ + Qname: qname, Qtype: qtype, + Rcode: expectRcode, + Answer: expectAnswers, + } + + err = test.SortAndCheck(resp, tc) + if err != nil { + t.Error(err) + } + }) +} diff --git a/ag_201_coredns/test/wildcard_test.go b/ag_201_coredns/test/wildcard_test.go new file mode 100644 index 0000000..85b0933 --- /dev/null +++ b/ag_201_coredns/test/wildcard_test.go @@ -0,0 +1,89 @@ +package test + +import ( + "testing" + + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestLookupWildcard(t *testing.T) { + t.Parallel() + name, rm, err := test.TempFile(".", exampleOrg) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + defer rm() + + corefile := `example.org:0 { + file ` + name + ` + }` + + i, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer i.Stop() + + for _, lookup := range []string{"a.w.example.org.", "a.a.w.example.org."} { + m := new(dns.Msg) + m.SetQuestion(lookup, dns.TypeTXT) + resp, err := dns.Exchange(m, udp) + if err != nil || resp == nil { + t.Fatalf("Expected to receive reply, but didn't for %s", lookup) + } + + // ;; ANSWER SECTION: + // a.w.example.org. 1800 IN TXT "Wildcard" + if resp.Rcode != dns.RcodeSuccess { + t.Errorf("Expected NOERROR RCODE, got %s for %s", dns.RcodeToString[resp.Rcode], lookup) + continue + } + if len(resp.Answer) == 0 { + t.Errorf("Expected to at least one RR in the answer section, got none for %s TXT", lookup) + t.Logf("%s", resp) + continue + } + if resp.Answer[0].Header().Name != lookup { + t.Errorf("Expected name to be %s, got: %s for TXT", lookup, resp.Answer[0].Header().Name) + continue + } + if resp.Answer[0].Header().Rrtype != dns.TypeTXT { + t.Errorf("Expected RR to be TXT, got: %d, for %s TXT", resp.Answer[0].Header().Rrtype, lookup) + continue + } + if resp.Answer[0].(*dns.TXT).Txt[0] != "Wildcard" { + t.Errorf("Expected Wildcard, got: %s, for %s TXT", resp.Answer[0].(*dns.TXT).Txt[0], lookup) + continue + } + } + + for _, lookup := range []string{"w.example.org.", "a.w.example.org.", "a.a.w.example.org."} { + m := new(dns.Msg) + m.SetQuestion(lookup, dns.TypeSRV) + resp, err := dns.Exchange(m, udp) + if err != nil || resp == nil { + t.Fatal("Expected to receive reply, but didn't", lookup) + } + + // ;; AUTHORITY SECTION: + // example.org. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1454960557 14400 3600 604800 14400 + if resp.Rcode != dns.RcodeSuccess { + t.Errorf("Expected NOERROR RCODE, got %s for %s", dns.RcodeToString[resp.Rcode], lookup) + continue + } + if len(resp.Answer) != 0 { + t.Errorf("Expected zero RRs in the answer section, got some, for %s SRV", lookup) + continue + } + if len(resp.Ns) == 0 { + t.Errorf("Expected to at least one RR in the authority section, got none, for %s SRV", lookup) + continue + } + if resp.Ns[0].Header().Rrtype != dns.TypeSOA { + t.Errorf("Expected RR to be SOA, got: %d, for %s SRV", resp.Ns[0].Header().Rrtype, lookup) + continue + } + } +} diff --git a/ag_201_coredns/testprivkey.pem b/ag_201_coredns/testprivkey.pem new file mode 100644 index 0000000..769aeee --- /dev/null +++ b/ag_201_coredns/testprivkey.pem @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,9220D427880152F2 + +lbM8H6wEjuJVB2lrn7tEHMuW+2akJ0iJqYW//c331k9h1z3jFmZ3habaNaNcoY9m +PTZEM6C8cCmPIIm0IIrCo5o5hi74/WJSj9Uj7TIAPP4/5ZHUNyl87ZWFN7fknnzZ +DyT583d3Q3oqRgxscJcsPAcVhYxuB7X/aSdFUExgpwmxMg4HOdvGifWblsGqfaYZ +Xy+0Bw2m8FoViaBrTItn+jBhy8kEZnat6X4SY03ZKuwL/NTPVdxE2CWogoteOOD6 +55DBdH7sNQg/5g8tbS/m1ditW79aR0/ebKtlaJUI4JA22FlISuhUIqQyfw94vhRV +h2Q/5eIJ+JTSS/FZTvqBw1vMF/A/M2YW8fFmrD0JVA8+Gr8oOBNY6JJjlujj0wgb +hvgP7gooZtwHG7UwIjIakEJHvOR4Lu1McTLAcjGApYtV+wg7JBquvEjk86nlffaT +VrmCLQ8L1sUIQum60yQsMg80elRlzx2WbWkgXg87GDrAWLgDtASNox1h6IkRNLX9 +1emVZXzIjYMeOIi+AwwaRX9bOCFn3mrQfymQtVAPGDsKF7L97SppspzhdBfwOYsh +Gz9L39p+Ty3YB7G5kDyTMn+4Dcu+I57JltjbekEt/BtwNRbATNLqaOttptfXTrPJ +LM5k7eB9i27Lo2eZYpYTpVeqnGXzBVBG4bhCm2guACTC9sAUe57N/4rrfzw8Wl7S +QKBSzni91rRZQBFIp1ixOpuJsu239mRk9igg1fLLxsxseXCLX2WFTd7qVMpK20s2 +Ls0+o2eHuKalx85JDe2zyyKaXPmWrRHEDtFrl8/Qnf6J0GX3xGbXQaJQOJJCzZPB +TgNTd22G+XlzpoTCEFi363Gldz2Kp0+uTOkDIJHZyF0uaZh4s4yjrpCW3pvUcrtm +jXE15XwxmleOjQ3s7IJ70wijEGq+9Ds7SZWO3xrIJN6OClV7Tgmz/BivhdXRcivw +HRKqL1rDMYT6i7lX63dIgVOWheSuOe8F0TNVj3RACf1eJPQd1/6PZubY5WIoT9RG +TbAwLOcoeoFstnPNwgALjU3iceATAKFOTeS/VjV7H4JSe1uC/1rG3qnW/8OXK3gG +r7JOMcY/dmYT8lSKxGPlL7Hu55syRkxBIw2cNuI1g8/DyP0Xz9FH77/n6rp7858R +BhQrtbLY5zMc0ke+C4i2QPbMxBzpp+yeQRi2ABy0QBpoxNZo2mblnpIGoXzv6k9p +EZIM8r5PN1rbv67mtlb5vsUR9Wkdj5yCIoTQ3mjlCK62EtXXCvLQPmzMfwrL5lYf +oubrIFGdieokimSiWh1+zZSvvVTLe2mqJMDl4593r+I/qPnKlLqjpltsv2Ah74yM +RbptEFZMtcRU7jLGxAlkJx+eDFoaY2NmutN4ozvZLIwdF8hCIipPaRdMGP8qoQM+ +h7nNctf3m35pPrb0X9nJePyyICCv+Il65eX7FmaGKUbk0IW1Z+Mezfh2RQ47XeRV +MqKBdB0VACBP3IMvxevXqZ5SM3FNjMR2C2qA54y5RKpfzrWdXlHlTsbQ21/IQ48n +psKk+8wc/kNFFOP/WNV5EoVcsX/5qRW9QDXPT5evF19k9fZuJ1JQ3g== +-----END RSA PRIVATE KEY----- diff --git a/dohclient/dohclient.go b/dohclient/dohclient.go new file mode 100644 index 0000000..0bff084 --- /dev/null +++ b/dohclient/dohclient.go @@ -0,0 +1,27 @@ +package main +// special:注意doh的网址格式问题,和域名最后的一个点"."的格式问题 +import ( + "encoding/base64" + "fmt" + "github.com/miekg/dns" + "io/ioutil" + "net/http" + "os" +) + +func main() { + query := dns.Msg{} + query.SetQuestion("www.taobao.com.", dns.TypeA) + msg, _ := query.Pack() + b64 := base64.RawURLEncoding.EncodeToString(msg) + resp, err := http.Get("https://example.com/dns-query?dns=" + b64) + if err != nil { + fmt.Printf("Send query error, err:%v\n", err) + os.Exit(1) + } + defer resp.Body.Close() + bodyBytes, _ := ioutil.ReadAll(resp.Body) + response := dns.Msg{} + response.Unpack(bodyBytes) + fmt.Printf("Dns answer is :%v\n", response.String()) +} diff --git a/dohclient/dohclient.py b/dohclient/dohclient.py new file mode 100644 index 0000000..26e4cb2 --- /dev/null +++ b/dohclient/dohclient.py @@ -0,0 +1,18 @@ +import dns.message +import requests +import base64 +import json + +doh_url = "https://example.com/dns-query" +domain = "alibaba.com" +rr = "A" +result = [] + +message = dns.message.make_query(domain, rr) +dns_req = base64.b64encode(message.to_wire()).decode("UTF8").rstrip("=") +r = requests.get(doh_url + "?dns=" + dns_req, + headers={"Content-type": "application/dns-message"}) +for answer in dns.message.from_wire(r.content).answer: + dns = answer.to_text().split() + result.append({"Query": dns[0], "TTL": dns[1], "RR": dns[3], "Answer": dns[4]}) + print(json.dumps(result)) diff --git "a/dohclient/\344\275\277\347\224\250go\344\273\243\347\240\201\347\232\204\344\270\200\344\272\233\345\221\275\344\273\244.txt" "b/dohclient/\344\275\277\347\224\250go\344\273\243\347\240\201\347\232\204\344\270\200\344\272\233\345\221\275\344\273\244.txt" new file mode 100644 index 0000000..7c56ea0 --- /dev/null +++ "b/dohclient/\344\275\277\347\224\250go\344\273\243\347\240\201\347\232\204\344\270\200\344\272\233\345\221\275\344\273\244.txt" @@ -0,0 +1,4 @@ +go get xxx +go mod init +go mod tidy +go run dohclient.go \ No newline at end of file diff --git a/readme_1.md b/readme_1.md new file mode 100644 index 0000000..3f20fde --- /dev/null +++ b/readme_1.md @@ -0,0 +1,28 @@ +一、插件说明: +插件代码见./ag_201_coredns/plugin/dnsovertor +代码暂未使用git进行版本管理,上传的是初版代码,能实现基本功能,metrics和setup_test功能未经测试。 +本插件使Coredns通过Tor的socks5代理端口向xxxx.onion域名的暗网隐藏服务发送DOH请求。 +本插件应与Coredns原代码配合使用 + +插件功能:向暗网DNS服务转发DNS请求,向客户转发DNS响应;处理请求和响应时输出日志 +注意事项:暗网DNS地址xxxx.onion提供DNS隐藏服务。 + +二、使用方法: +1.在plugin.cfg末尾添加dnsovertor:dnsovertor。 +2.将dnsovertor文件夹放入Coredns源文件的plugin文件夹。 +以上两步已集成到ag_201_coredns文件夹中。 +3.通过源代码编译Coredns。 + + +三、代码说明: +dnsovertor.go和setup.go是dnsovertor插件的两个核心go文件,这两个文件就可以实现基本功能。 +dohclient.py,dohclient.go使用DoH查询域名alibaba.com. +(https://help.aliyun.com/document_detail/171664.html) + +四、匿名解析的测试例子(VPS已失效,流程类似): +匿名解析网关:8.210.118.152,在443端口提供了DOH服务(域名证书为www.runn77.tk),在53端口提供DNS服务 +对www.runn77.tk发起doh请求:python dohclient.py +对8.210.118.152发起dns请求:dig @8.210.118.152 baidu.com 或 nslookup baidu.com 8.210.118.152 + + + -- cgit v1.2.3