aboutsummaryrefslogtreecommitdiff
path: root/plugin
diff options
context:
space:
mode:
Diffstat (limited to 'plugin')
-rw-r--r--plugin/auto/README.md68
-rw-r--r--plugin/auto/auto.go96
-rw-r--r--plugin/auto/regexp.go20
-rw-r--r--plugin/auto/regexp_test.go20
-rw-r--r--plugin/auto/setup.go172
-rw-r--r--plugin/auto/setup_test.go125
-rw-r--r--plugin/auto/walk.go109
-rw-r--r--plugin/auto/walk_test.go94
-rw-r--r--plugin/auto/watcher_test.go58
-rw-r--r--plugin/auto/zone.go76
-rw-r--r--plugin/autopath/README.md45
-rw-r--r--plugin/autopath/autopath.go152
-rw-r--r--plugin/autopath/autopath_test.go166
-rw-r--r--plugin/autopath/cname.go25
-rw-r--r--plugin/autopath/setup.go93
-rw-r--r--plugin/autopath/setup_test.go77
-rw-r--r--plugin/backend.go32
-rw-r--r--plugin/backend_lookup.go410
-rw-r--r--plugin/bind/README.md22
-rw-r--r--plugin/bind/bind.go11
-rw-r--r--plugin/bind/bind_test.go30
-rw-r--r--plugin/bind/setup.go24
-rw-r--r--plugin/cache/README.md68
-rw-r--r--plugin/cache/cache.go167
-rw-r--r--plugin/cache/cache_test.go251
-rw-r--r--plugin/cache/freq/freq.go55
-rw-r--r--plugin/cache/freq/freq_test.go36
-rw-r--r--plugin/cache/handler.go119
-rw-r--r--plugin/cache/item.go116
-rw-r--r--plugin/cache/prefech_test.go54
-rw-r--r--plugin/cache/setup.go170
-rw-r--r--plugin/cache/setup_test.go94
-rw-r--r--plugin/chaos/README.md46
-rw-r--r--plugin/chaos/chaos.go62
-rw-r--r--plugin/chaos/chaos_test.go80
-rw-r--r--plugin/chaos/setup.go55
-rw-r--r--plugin/chaos/setup_test.go54
-rw-r--r--plugin/debug/README.md20
-rw-r--r--plugin/debug/debug.go28
-rw-r--r--plugin/debug/debug_test.go49
-rw-r--r--plugin/dnssec/README.md88
-rw-r--r--plugin/dnssec/black_lies.go24
-rw-r--r--plugin/dnssec/black_lies_test.go49
-rw-r--r--plugin/dnssec/cache.go22
-rw-r--r--plugin/dnssec/cache_test.go34
-rw-r--r--plugin/dnssec/dnskey.go72
-rw-r--r--plugin/dnssec/dnssec.go135
-rw-r--r--plugin/dnssec/dnssec_test.go219
-rw-r--r--plugin/dnssec/handler.go82
-rw-r--r--plugin/dnssec/handler_test.go155
-rw-r--r--plugin/dnssec/responsewriter.go49
-rw-r--r--plugin/dnssec/rrsig.go53
-rw-r--r--plugin/dnssec/setup.go128
-rw-r--r--plugin/dnssec/setup_test.go120
-rw-r--r--plugin/dnstap/README.md61
-rw-r--r--plugin/dnstap/handler.go79
-rw-r--r--plugin/dnstap/handler_test.go65
-rw-r--r--plugin/dnstap/msg/msg.go168
-rw-r--r--plugin/dnstap/msg/msg_test.go42
-rw-r--r--plugin/dnstap/msg/wrapper.go26
-rw-r--r--plugin/dnstap/out/socket.go86
-rw-r--r--plugin/dnstap/out/socket_test.go94
-rw-r--r--plugin/dnstap/out/tcp.go59
-rw-r--r--plugin/dnstap/out/tcp_test.go66
-rw-r--r--plugin/dnstap/setup.go98
-rw-r--r--plugin/dnstap/setup_test.go34
-rw-r--r--plugin/dnstap/taprw/writer.go73
-rw-r--r--plugin/dnstap/taprw/writer_test.go82
-rw-r--r--plugin/dnstap/test/helpers.go80
-rw-r--r--plugin/erratic/README.md76
-rw-r--r--plugin/erratic/autopath.go8
-rw-r--r--plugin/erratic/erratic.go95
-rw-r--r--plugin/erratic/erratic_test.go79
-rw-r--r--plugin/erratic/setup.go117
-rw-r--r--plugin/erratic/setup_test.go103
-rw-r--r--plugin/errors/README.md22
-rw-r--r--plugin/errors/errors.go79
-rw-r--r--plugin/errors/errors_test.go73
-rw-r--r--plugin/errors/setup.go55
-rw-r--r--plugin/errors/setup_test.go45
-rw-r--r--plugin/etcd/README.md109
-rw-r--r--plugin/etcd/cname_test.go79
-rw-r--r--plugin/etcd/etcd.go188
-rw-r--r--plugin/etcd/group_test.go74
-rw-r--r--plugin/etcd/handler.go97
-rw-r--r--plugin/etcd/lookup_test.go273
-rw-r--r--plugin/etcd/msg/path.go48
-rw-r--r--plugin/etcd/msg/path_test.go12
-rw-r--r--plugin/etcd/msg/service.go203
-rw-r--r--plugin/etcd/msg/service_test.go125
-rw-r--r--plugin/etcd/msg/type.go33
-rw-r--r--plugin/etcd/msg/type_test.go31
-rw-r--r--plugin/etcd/multi_test.go59
-rw-r--r--plugin/etcd/other_test.go150
-rw-r--r--plugin/etcd/setup.go144
-rw-r--r--plugin/etcd/setup_test.go64
-rw-r--r--plugin/etcd/stub.go82
-rw-r--r--plugin/etcd/stub_handler.go86
-rw-r--r--plugin/etcd/stub_test.go88
-rw-r--r--plugin/federation/README.md43
-rw-r--r--plugin/federation/federation.go141
-rw-r--r--plugin/federation/federation_test.go81
-rw-r--r--plugin/federation/kubernetes_api_test.go111
-rw-r--r--plugin/federation/setup.go89
-rw-r--r--plugin/federation/setup_test.go65
-rw-r--r--plugin/file/README.md55
-rw-r--r--plugin/file/closest.go24
-rw-r--r--plugin/file/closest_test.go38
-rw-r--r--plugin/file/cname_test.go124
-rw-r--r--plugin/file/delegation_test.go207
-rw-r--r--plugin/file/dname.go44
-rw-r--r--plugin/file/dname_test.go300
-rw-r--r--plugin/file/dnssec_test.go358
-rw-r--r--plugin/file/dnssex_test.go145
-rw-r--r--plugin/file/ds_test.go75
-rw-r--r--plugin/file/ent_test.go159
-rw-r--r--plugin/file/example_org.go113
-rw-r--r--plugin/file/file.go138
-rw-r--r--plugin/file/file_test.go31
-rw-r--r--plugin/file/glue_test.go253
-rw-r--r--plugin/file/include_test.go32
-rw-r--r--plugin/file/lookup.go467
-rw-r--r--plugin/file/lookup_test.go194
-rw-r--r--plugin/file/notify.go82
-rw-r--r--plugin/file/nsec3_test.go28
-rw-r--r--plugin/file/reload.go72
-rw-r--r--plugin/file/reload_test.go82
-rw-r--r--plugin/file/secondary.go199
-rw-r--r--plugin/file/secondary_test.go168
-rw-r--r--plugin/file/setup.go171
-rw-r--r--plugin/file/setup_test.go77
-rw-r--r--plugin/file/tree/all.go48
-rw-r--r--plugin/file/tree/elem.go136
-rw-r--r--plugin/file/tree/less.go59
-rw-r--r--plugin/file/tree/less_test.go81
-rw-r--r--plugin/file/tree/print.go62
-rw-r--r--plugin/file/tree/tree.go455
-rw-r--r--plugin/file/wildcard.go13
-rw-r--r--plugin/file/wildcard_test.go289
-rw-r--r--plugin/file/xfr.go62
-rw-r--r--plugin/file/xfr_test.go34
-rw-r--r--plugin/file/zone.go190
-rw-r--r--plugin/file/zone_test.go30
-rw-r--r--plugin/health/README.md23
-rw-r--r--plugin/health/health.go69
-rw-r--r--plugin/health/health_test.go47
-rw-r--r--plugin/health/healther.go42
-rw-r--r--plugin/health/setup.go73
-rw-r--r--plugin/health/setup_test.go35
-rw-r--r--plugin/hosts/README.md45
-rw-r--r--plugin/hosts/hosts.go136
-rw-r--r--plugin/hosts/hosts_test.go75
-rw-r--r--plugin/hosts/hostsfile.go193
-rw-r--r--plugin/hosts/hostsfile_test.go239
-rw-r--r--plugin/hosts/setup.go88
-rw-r--r--plugin/hosts/setup_test.go86
-rw-r--r--plugin/kubernetes/DEV-README.md43
-rw-r--r--plugin/kubernetes/README.md167
-rw-r--r--plugin/kubernetes/apiproxy.go76
-rw-r--r--plugin/kubernetes/autopath.go53
-rw-r--r--plugin/kubernetes/controller.go399
-rw-r--r--plugin/kubernetes/federation.go45
-rw-r--r--plugin/kubernetes/handler.go86
-rw-r--r--plugin/kubernetes/handler_pod_disabled_test.go61
-rw-r--r--plugin/kubernetes/handler_pod_insecure_test.go59
-rw-r--r--plugin/kubernetes/handler_pod_verified_test.go59
-rw-r--r--plugin/kubernetes/handler_test.go347
-rw-r--r--plugin/kubernetes/kubernetes.go457
-rw-r--r--plugin/kubernetes/kubernetes_apex_test.go68
-rw-r--r--plugin/kubernetes/kubernetes_test.go242
-rw-r--r--plugin/kubernetes/local.go40
-rw-r--r--plugin/kubernetes/ns.go65
-rw-r--r--plugin/kubernetes/ns_test.go69
-rw-r--r--plugin/kubernetes/parse.go112
-rw-r--r--plugin/kubernetes/parse_test.go56
-rw-r--r--plugin/kubernetes/reverse.go55
-rw-r--r--plugin/kubernetes/reverse_test.go125
-rw-r--r--plugin/kubernetes/setup.go208
-rw-r--r--plugin/kubernetes/setup_reverse_test.go35
-rw-r--r--plugin/kubernetes/setup_test.go473
-rw-r--r--plugin/kubernetes/setup_ttl_test.go45
-rw-r--r--plugin/loadbalance/README.md22
-rw-r--r--plugin/loadbalance/handler.go23
-rw-r--r--plugin/loadbalance/loadbalance.go87
-rw-r--r--plugin/loadbalance/loadbalance_test.go168
-rw-r--r--plugin/loadbalance/setup.go26
-rw-r--r--plugin/log/README.md102
-rw-r--r--plugin/log/log.go91
-rw-r--r--plugin/log/log_test.go101
-rw-r--r--plugin/log/setup.go116
-rw-r--r--plugin/log/setup_test.go130
-rw-r--r--plugin/metrics/README.md53
-rw-r--r--plugin/metrics/handler.go34
-rw-r--r--plugin/metrics/metrics.go101
-rw-r--r--plugin/metrics/metrics_test.go83
-rw-r--r--plugin/metrics/setup.go100
-rw-r--r--plugin/metrics/setup_test.go42
-rw-r--r--plugin/metrics/test/scrape.go225
-rw-r--r--plugin/metrics/vars/report.go62
-rw-r--r--plugin/metrics/vars/vars.go69
-rw-r--r--plugin/normalize.go137
-rw-r--r--plugin/normalize_test.go84
-rw-r--r--plugin/pkg/cache/cache.go129
-rw-r--r--plugin/pkg/cache/cache_test.go31
-rw-r--r--plugin/pkg/cache/shard_test.go60
-rw-r--r--plugin/pkg/dnsrecorder/recorder.go58
-rw-r--r--plugin/pkg/dnsrecorder/recorder_test.go28
-rw-r--r--plugin/pkg/dnsutil/cname.go15
-rw-r--r--plugin/pkg/dnsutil/cname_test.go55
-rw-r--r--plugin/pkg/dnsutil/dedup.go12
-rw-r--r--plugin/pkg/dnsutil/doc.go2
-rw-r--r--plugin/pkg/dnsutil/host.go82
-rw-r--r--plugin/pkg/dnsutil/host_test.go85
-rw-r--r--plugin/pkg/dnsutil/join.go19
-rw-r--r--plugin/pkg/dnsutil/join_test.go20
-rw-r--r--plugin/pkg/dnsutil/reverse.go68
-rw-r--r--plugin/pkg/dnsutil/reverse_test.go51
-rw-r--r--plugin/pkg/dnsutil/zone.go20
-rw-r--r--plugin/pkg/dnsutil/zone_test.go39
-rw-r--r--plugin/pkg/edns/edns.go46
-rw-r--r--plugin/pkg/edns/edns_test.go37
-rw-r--r--plugin/pkg/healthcheck/healthcheck.go243
-rw-r--r--plugin/pkg/healthcheck/policy.go120
-rw-r--r--plugin/pkg/healthcheck/policy_test.go143
-rw-r--r--plugin/pkg/nonwriter/nonwriter.go23
-rw-r--r--plugin/pkg/nonwriter/nonwriter_test.go19
-rw-r--r--plugin/pkg/rcode/rcode.go16
-rw-r--r--plugin/pkg/rcode/rcode_test.go29
-rw-r--r--plugin/pkg/replacer/replacer.go161
-rw-r--r--plugin/pkg/replacer/replacer_test.go61
-rw-r--r--plugin/pkg/response/classify.go61
-rw-r--r--plugin/pkg/response/typify.go146
-rw-r--r--plugin/pkg/response/typify_test.go84
-rw-r--r--plugin/pkg/singleflight/singleflight.go64
-rw-r--r--plugin/pkg/singleflight/singleflight_test.go85
-rw-r--r--plugin/pkg/tls/tls.go128
-rw-r--r--plugin/pkg/tls/tls_test.go101
-rw-r--r--plugin/pkg/trace/trace.go12
-rw-r--r--plugin/plugin.go102
-rw-r--r--plugin/plugin_test.go1
-rw-r--r--plugin/pprof/README.md41
-rw-r--r--plugin/pprof/pprof.go49
-rw-r--r--plugin/pprof/setup.go53
-rw-r--r--plugin/pprof/setup_test.go34
-rw-r--r--plugin/proxy/README.md175
-rw-r--r--plugin/proxy/dns.go106
-rw-r--r--plugin/proxy/dnstap_test.go57
-rw-r--r--plugin/proxy/exchanger.go22
-rw-r--r--plugin/proxy/google.go244
-rw-r--r--plugin/proxy/google_rr.go89
-rw-r--r--plugin/proxy/google_test.go5
-rw-r--r--plugin/proxy/grpc.go96
-rw-r--r--plugin/proxy/grpc_test.go71
-rw-r--r--plugin/proxy/lookup.go132
-rw-r--r--plugin/proxy/metrics.go30
-rw-r--r--plugin/proxy/proxy.go195
-rw-r--r--plugin/proxy/proxy_test.go87
-rw-r--r--plugin/proxy/response.go21
-rw-r--r--plugin/proxy/setup.go46
-rw-r--r--plugin/proxy/upstream.go234
-rw-r--r--plugin/proxy/upstream_test.go324
-rw-r--r--plugin/reverse/README.md86
-rw-r--r--plugin/reverse/network.go87
-rw-r--r--plugin/reverse/network_test.go135
-rw-r--r--plugin/reverse/reverse.go107
-rw-r--r--plugin/reverse/reverse_test.go71
-rw-r--r--plugin/reverse/setup.go147
-rw-r--r--plugin/reverse/setup_test.go195
-rw-r--r--plugin/rewrite/README.md91
-rw-r--r--plugin/rewrite/class.go35
-rw-r--r--plugin/rewrite/condition.go132
-rw-r--r--plugin/rewrite/condition_test.go102
-rw-r--r--plugin/rewrite/edns0.go425
-rw-r--r--plugin/rewrite/name.go24
-rw-r--r--plugin/rewrite/reverter.go39
-rw-r--r--plugin/rewrite/rewrite.go86
-rw-r--r--plugin/rewrite/rewrite_test.go532
-rw-r--r--plugin/rewrite/setup.go42
-rw-r--r--plugin/rewrite/setup_test.go25
-rw-r--r--plugin/rewrite/testdata/testdir/empty0
-rw-r--r--plugin/rewrite/testdata/testfile1
-rw-r--r--plugin/rewrite/type.go37
-rw-r--r--plugin/root/README.md22
-rw-r--r--plugin/root/root.go43
-rw-r--r--plugin/root/root_test.go107
-rw-r--r--plugin/secondary/README.md54
-rw-r--r--plugin/secondary/secondary.go10
-rw-r--r--plugin/secondary/setup.go108
-rw-r--r--plugin/secondary/setup_test.go65
-rw-r--r--plugin/test/doc.go2
-rw-r--r--plugin/test/file.go107
-rw-r--r--plugin/test/file_test.go11
-rw-r--r--plugin/test/helpers.go348
-rw-r--r--plugin/test/responsewriter.go61
-rw-r--r--plugin/test/server.go52
-rw-r--r--plugin/tls/README.md52
-rw-r--r--plugin/tls/tls.go37
-rw-r--r--plugin/tls/tls_test.go44
-rw-r--r--plugin/trace/README.md73
-rw-r--r--plugin/trace/setup.go113
-rw-r--r--plugin/trace/setup_test.go60
-rw-r--r--plugin/trace/trace.go84
-rw-r--r--plugin/trace/trace_test.go33
-rw-r--r--plugin/whoami/README.md44
-rw-r--r--plugin/whoami/setup.go28
-rw-r--r--plugin/whoami/setup_test.go19
-rw-r--r--plugin/whoami/whoami.go57
-rw-r--r--plugin/whoami/whoami_test.go56
308 files changed, 29222 insertions, 0 deletions
diff --git a/plugin/auto/README.md b/plugin/auto/README.md
new file mode 100644
index 000000000..7cbc4fced
--- /dev/null
+++ b/plugin/auto/README.md
@@ -0,0 +1,68 @@
+# auto
+
+*auto* enables serving zone data from an RFC 1035-style master file which is automatically picked
+up from disk.
+
+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. DNSSEC) correct DNSSEC answers
+are returned. Only NSEC is supported! If you use this setup *you* are responsible for resigning the
+zonefile. New zones or changed zone are automatically picked up from disk.
+
+## Syntax
+
+~~~
+auto [ZONES...] {
+ directory DIR [REGEXP ORIGIN_TEMPLATE [TIMEOUT]]
+ no_reload
+ upstream ADDRESS...
+}
+~~~
+
+**ZONES** zones it should be authoritative for. If empty, the zones from the configuration block
+are used.
+
+* `directory` loads zones from the speficied **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 `{<number>}` are replaced with the respective matches in the file name, i.e. `{1}` is the
+ first match, `{2}` is the second, etc.. The default is: `db\.(.*) {1}` e.g. from a file with the
+ name `db.example.com`, the extracted origin will be `example.com`. **TIMEOUT** specifies how often
+ CoreDNS should scan the directory, the default is every 60 seconds. This value is in seconds.
+ The minimum value is 1 second.
+* `no_reload` by default CoreDNS will reload a zone from disk whenever it detects a change to the
+ file. This option disables that behavior.
+* `upstream` defines upstream resolvers to be used resolve external names found (think CNAMEs)
+ pointing to external names. **ADDRESS** can be an IP address, and IP:port or a string pointing to
+ a file that is structured as /etc/resolv.conf.
+
+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:
+
+~~~
+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
+
+~~~
+auto org {
+ directory /etc/coredns/zones/org
+ transfer to *
+ transfer 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.
+
+~~~
+auto org {
+ directory /etc/coredns/zones/org www\.db\.(.*) {1} 45
+}
+~~~
diff --git a/plugin/auto/auto.go b/plugin/auto/auto.go
new file mode 100644
index 000000000..e9cab1950
--- /dev/null
+++ b/plugin/auto/auto.go
@@ -0,0 +1,96 @@
+// Package auto implements an on-the-fly loading file backend.
+package auto
+
+import (
+ "regexp"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/file"
+ "github.com/coredns/coredns/plugin/metrics"
+ "github.com/coredns/coredns/plugin/proxy"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+type (
+ // Auto holds the zones and the loader configuration for automatically loading zones.
+ Auto struct {
+ Next plugin.Handler
+ *Zones
+
+ metrics *metrics.Metrics
+ loader
+ }
+
+ loader struct {
+ directory string
+ template string
+ re *regexp.Regexp
+
+ // In the future this should be something like ZoneMeta that contains all this stuff.
+ transferTo []string
+ noReload bool
+ proxy proxy.Proxy // Proxy for looking up names during the resolution process
+
+ duration time.Duration
+ }
+)
+
+// ServeDNS implements the plugin.Handle 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()
+
+ // TODO(miek): match the qname better in the map
+
+ // Precheck with the origins, i.e. are we allowed to looks 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)
+
+ a.Zones.RLock()
+ z, ok := a.Zones.Z[zone]
+ a.Zones.RUnlock()
+
+ if !ok || z == nil {
+ return dns.RcodeServerFailure, nil
+ }
+
+ if state.QType() == dns.TypeAXFR || state.QType() == dns.TypeIXFR {
+ xfr := file.Xfr{Zone: z}
+ return xfr.ServeDNS(ctx, w, r)
+ }
+
+ answer, ns, extra, result := z.Lookup(state, qname)
+
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Authoritative, m.RecursionAvailable, m.Compress = true, true, 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:
+ return dns.RcodeServerFailure, nil
+ }
+
+ state.SizeAndDo(m)
+ m, _ = state.Scrub(m)
+ w.WriteMsg(m)
+ return dns.RcodeSuccess, nil
+}
+
+// Name implements the Handler interface.
+func (a Auto) Name() string { return "auto" }
diff --git a/plugin/auto/regexp.go b/plugin/auto/regexp.go
new file mode 100644
index 000000000..fa424ec7e
--- /dev/null
+++ b/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/plugin/auto/regexp_test.go b/plugin/auto/regexp_test.go
new file mode 100644
index 000000000..17c35eb90
--- /dev/null
+++ b/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/plugin/auto/setup.go b/plugin/auto/setup.go
new file mode 100644
index 000000000..75966f8a0
--- /dev/null
+++ b/plugin/auto/setup.go
@@ -0,0 +1,172 @@
+package auto
+
+import (
+ "log"
+ "os"
+ "path"
+ "regexp"
+ "strconv"
+ "time"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/file"
+ "github.com/coredns/coredns/plugin/metrics"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/proxy"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("auto", caddy.Plugin{
+ ServerType: "dns",
+ Action: 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 {
+ return nil
+ }
+ (&a).metrics = m.(*metrics.Metrics)
+ return nil
+ })
+
+ walkChan := make(chan bool)
+
+ c.OnStartup(func() error {
+ err := a.Walk()
+ if err != nil {
+ return err
+ }
+
+ go func() {
+ ticker := time.NewTicker(a.loader.duration)
+ 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) {
+ var a = Auto{
+ loader: loader{template: "${1}", re: regexp.MustCompile(`db\.(.*)`), duration: 60 * time.Second},
+ Zones: &Zones{},
+ }
+
+ config := dnsserver.GetConfig(c)
+
+ for c.Next() {
+ // auto [ZONES...]
+ a.Zones.origins = make([]string, len(c.ServerBlockKeys))
+ copy(a.Zones.origins, c.ServerBlockKeys)
+
+ args := c.RemainingArgs()
+ if len(args) > 0 {
+ a.Zones.origins = args
+ }
+ for i := range a.Zones.origins {
+ a.Zones.origins[i] = plugin.Host(a.Zones.origins[i]).Normalize()
+ }
+
+ for c.NextBlock() {
+ switch c.Val() {
+ case "directory": // directory DIR [REGEXP [TEMPLATE] [DURATION]]
+ if !c.NextArg() {
+ return a, c.ArgErr()
+ }
+ a.loader.directory = c.Val()
+ if !path.IsAbs(a.loader.directory) && config.Root != "" {
+ a.loader.directory = path.Join(config.Root, a.loader.directory)
+ }
+ _, err := os.Stat(a.loader.directory)
+ if err != nil {
+ if os.IsNotExist(err) {
+ log.Printf("[WARNING] 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
+ 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")
+ }
+ }
+
+ // template
+ if c.NextArg() {
+ a.loader.template = rewriteToExpand(c.Val())
+ }
+
+ // duration
+ if c.NextArg() {
+ i, err := strconv.Atoi(c.Val())
+ if err != nil {
+ return a, err
+ }
+ if i < 1 {
+ i = 1
+ }
+ a.loader.duration = time.Duration(i) * time.Second
+ }
+
+ case "no_reload":
+ a.loader.noReload = true
+
+ case "upstream":
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ return a, c.ArgErr()
+ }
+ ups, err := dnsutil.ParseHostPortOrFile(args...)
+ if err != nil {
+ return a, err
+ }
+ a.loader.proxy = proxy.NewLookup(ups)
+
+ default:
+ t, _, e := file.TransferParse(c, false)
+ if e != nil {
+ return a, e
+ }
+ if t != nil {
+ a.loader.transferTo = append(a.loader.transferTo, t...)
+ }
+ }
+ }
+ }
+ return a, nil
+}
diff --git a/plugin/auto/setup_test.go b/plugin/auto/setup_test.go
new file mode 100644
index 000000000..9754551d2
--- /dev/null
+++ b/plugin/auto/setup_test.go
@@ -0,0 +1,125 @@
+package auto
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestAutoParse(t *testing.T) {
+ tests := []struct {
+ inputFileRules string
+ shouldErr bool
+ expectedDirectory string
+ expectedTempl string
+ expectedRe string
+ expectedTo []string
+ }{
+ {
+ `auto example.org {
+ directory /tmp
+ transfer to 127.0.0.1
+ }`,
+ false, "/tmp", "${1}", `db\.(.*)`, []string{"127.0.0.1:53"},
+ },
+ {
+ `auto 10.0.0.0/24 {
+ directory /tmp
+ }`,
+ false, "/tmp", "${1}", `db\.(.*)`, nil,
+ },
+ {
+ `auto {
+ directory /tmp
+ no_reload
+ }`,
+ false, "/tmp", "${1}", `db\.(.*)`, nil,
+ },
+ {
+ `auto {
+ directory /tmp (.*) bliep
+ }`,
+ false, "/tmp", "bliep", `(.*)`, nil,
+ },
+ {
+ `auto {
+ directory /tmp (.*) bliep 10
+ }`,
+ false, "/tmp", "bliep", `(.*)`, nil,
+ },
+ {
+ `auto {
+ directory /tmp (.*) bliep
+ transfer to 127.0.0.1
+ transfer to 127.0.0.2
+ upstream 8.8.8.8
+ }`,
+ false, "/tmp", "bliep", `(.*)`, []string{"127.0.0.1:53", "127.0.0.2:53"},
+ },
+ // errors
+ {
+ `auto example.org {
+ directory
+ }`,
+ true, "", "${1}", `db\.(.*)`, nil,
+ },
+ {
+ `auto example.org {
+ directory /tmp * {1}
+ }`,
+ true, "", "${1}", ``, nil,
+ },
+ {
+ `auto example.org {
+ directory /tmp * {1} aa
+ }`,
+ true, "", "${1}", ``, nil,
+ },
+ {
+ `auto example.org {
+ directory /tmp .* {1}
+ }`,
+ true, "", "${1}", ``, nil,
+ },
+ {
+ `auto example.org {
+ directory /tmp .* {1}
+ }`,
+ true, "", "${1}", ``, nil,
+ },
+ {
+ `auto example.org {
+ directory /tmp .* {1}
+ }`,
+ true, "", "${1}", ``, nil,
+ },
+ }
+
+ 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 test.expectedTo != nil {
+ for j, got := range a.loader.transferTo {
+ if got != test.expectedTo[j] {
+ t.Fatalf("Test %d expected %v, got %v", i, test.expectedTo[j], got)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/plugin/auto/walk.go b/plugin/auto/walk.go
new file mode 100644
index 000000000..a98f2318e
--- /dev/null
+++ b/plugin/auto/walk.go
@@ -0,0 +1,109 @@
+package auto
+
+import (
+ "log"
+ "os"
+ "path"
+ "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, err 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 _, ok := a.Zones.Z[origin]; ok {
+ // we already have this zone
+ toDelete[origin] = false
+ return nil
+ }
+
+ reader, err := os.Open(path)
+ if err != nil {
+ log.Printf("[WARNING] 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.Printf("[WARNING] Parse zone `%s': %v", origin, err)
+ return nil
+ }
+
+ zo.NoReload = a.loader.noReload
+ zo.Proxy = a.loader.proxy
+ zo.TransferTo = a.loader.transferTo
+
+ a.Zones.Add(zo, origin)
+
+ if a.metrics != nil {
+ a.metrics.AddZone(origin)
+ }
+
+ zo.Notify()
+
+ log.Printf("[INFO] 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.Printf("[INFO] Deleting zone `%s'", origin)
+ }
+
+ return nil
+}
+
+// matches matches re to filename, if is 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 := path.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/plugin/auto/walk_test.go b/plugin/auto/walk_test.go
new file mode 100644
index 000000000..29b9dbb55
--- /dev/null
+++ b/plugin/auto/walk_test.go
@@ -0,0 +1,94 @@
+package auto
+
+import (
+ "io/ioutil"
+ "log"
+ "os"
+ "path"
+ "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) {
+ log.SetOutput(ioutil.Discard)
+
+ 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) {
+ log.SetOutput(ioutil.Discard)
+
+ 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 := ioutil.TempDir(os.TempDir(), "coredns")
+ if err != nil {
+ return dir, err
+ }
+
+ for _, name := range dbFiles {
+ if err := ioutil.WriteFile(path.Join(dir, name), []byte(zoneContent), 0644); err != nil {
+ return dir, err
+ }
+ }
+ // symlinks
+ if err = os.Symlink(path.Join(dir, "db.example.org"), path.Join(dir, "db.example.com")); err != nil {
+ return dir, err
+ }
+ if err = os.Symlink(path.Join(dir, "db.example.org"), path.Join(dir, "aa.example.com")); err != nil {
+ return dir, err
+ }
+
+ return dir, nil
+}
diff --git a/plugin/auto/watcher_test.go b/plugin/auto/watcher_test.go
new file mode 100644
index 000000000..329d8dc85
--- /dev/null
+++ b/plugin/auto/watcher_test.go
@@ -0,0 +1,58 @@
+package auto
+
+import (
+ "io/ioutil"
+ "log"
+ "os"
+ "path"
+ "regexp"
+ "testing"
+)
+
+func TestWatcher(t *testing.T) {
+ log.SetOutput(ioutil.Discard)
+
+ 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
+ if x := len(a.Zones.Z["example.org."].All()); x != 4 {
+ t.Fatalf("Expected 4 RRs, got %d", x)
+ }
+ if x := len(a.Zones.Z["example.com."].All()); x != 4 {
+ t.Fatalf("Expected 4 RRs, got %d", x)
+ }
+
+ // Now remove one file, rescan and see if it's gone.
+ if err := os.Remove(path.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.")
+ }
+}
diff --git a/plugin/auto/zone.go b/plugin/auto/zone.go
new file mode 100644
index 000000000..e46f04e33
--- /dev/null
+++ b/plugin/auto/zone.go
@@ -0,0 +1,76 @@
+// Package auto implements a on-the-fly loading file backend.
+package auto
+
+import (
+ "sync"
+
+ "github.com/coredns/coredns/plugin/file"
+)
+
+// Zones maps zone names to a *Zone. This keep track of what we 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 zo.NoReload is false, the
+// reload goroutine is started.
+func (z *Zones) Add(zo *file.Zone, name string) {
+ 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()
+
+ z.Unlock()
+}
+
+// Remove removes the zone named name from z. It also stop the the zone's reload goroutine.
+func (z *Zones) Remove(name string) {
+ z.Lock()
+
+ if zo, ok := z.Z[name]; ok && !zo.NoReload {
+ zo.ReloadShutdown <- true
+ }
+
+ 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/plugin/autopath/README.md b/plugin/autopath/README.md
new file mode 100644
index 000000000..02b4390fc
--- /dev/null
+++ b/plugin/autopath/README.md
@@ -0,0 +1,45 @@
+# autopath
+
+The *autopath* plugin allows CoreDNS to perform server side search path completion.
+If it sees a query that matches the first element of the configured search path, *autopath* will
+follow the chain of search path elements and returns 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.
+
+## 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.
+
+Currently the following set of plugin has implemented *autopath*:
+
+* *kubernetes*
+* *erratic*
+
+## Examples
+
+~~~
+autopath my-resolv.conf
+~~~
+
+Use `my-resolv.conf` as the file to get the search path from. This file only needs so have one line:
+`search domain1 domain2 ...`
+
+~~~
+autopath @kubernetes
+~~~
+
+Use the search path dynamically retrieved from the kubernetes plugin.
+
+## Bugs
+
+When the *cache* plugin is enabled it is possible for pods in different namespaces to get the
+same answer.
diff --git a/plugin/autopath/autopath.go b/plugin/autopath/autopath.go
new file mode 100644
index 000000000..5c804a040
--- /dev/null
+++ b/plugin/autopath/autopath.go
@@ -0,0 +1,152 @@
+/*
+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 establish 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 (iff '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.
+
+Midldeware 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 Middleware ) AutoPath(state request.Request) []string {
+ return []string{"first", "second", "last", ""}
+}
+*/
+package autopath
+
+import (
+ "log"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/pkg/nonwriter"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// 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
+
+// AutoPath perform 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 {
+ log.Printf("[WARNING] No search path available for autopath")
+ 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]) // TODO(miek): we loose the original case of the query here.
+ 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)
+ 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/plugin/autopath/autopath_test.go b/plugin/autopath/autopath_test.go
new file mode 100644
index 000000000..a00bbf0a6
--- /dev/null
+++ b/plugin/autopath/autopath_test.go
@@ -0,0 +1,166 @@
+package autopath
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := ap.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", err)
+ continue
+ }
+
+ // No sorting here as we want to check if the CNAME sits *before* the
+ // test of the answer.
+ resp := rec.Msg
+
+ if !test.Header(t, tc, resp) {
+ t.Logf("%v\n", resp)
+ continue
+ }
+ if !test.Section(t, tc, test.Answer, resp.Answer) {
+ t.Logf("%v\n", resp)
+ }
+ if !test.Section(t, tc, test.Ns, resp.Ns) {
+ t.Logf("%v\n", resp)
+ }
+ if !test.Section(t, tc, test.Extra, resp.Extra) {
+ t.Logf("%v\n", resp)
+ }
+ }
+}
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ rcode, err := ap.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", 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/plugin/autopath/cname.go b/plugin/autopath/cname.go
new file mode 100644
index 000000000..3b2c60f4e
--- /dev/null
+++ b/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/plugin/autopath/setup.go b/plugin/autopath/setup.go
new file mode 100644
index 000000000..c83912a63
--- /dev/null
+++ b/plugin/autopath/setup.go
@@ -0,0 +1,93 @@
+package autopath
+
+import (
+ "fmt"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/erratic"
+ "github.com/coredns/coredns/plugin/kubernetes"
+
+ "github.com/mholt/caddy"
+ "github.com/miekg/dns"
+)
+
+func init() {
+ caddy.RegisterPlugin("autopath", caddy.Plugin{
+ ServerType: "dns",
+ Action: 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.(*kubernetes.Kubernetes); ok {
+ ap.searchFunc = x.AutoPath
+ }
+ if x, ok := m.(*erratic.Erratic); ok {
+ ap.searchFunc = x.AutoPath
+ }
+ return nil
+ })
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ ap.Next = next
+ return ap
+ })
+
+ return nil
+}
+
+// allowedMiddleware has a list of plugin that can be used by autopath.
+var allowedMiddleware = map[string]bool{
+ "@kubernetes": true,
+ "@erratic": true,
+}
+
+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 resolv[0] == '@' {
+ _, ok := allowedMiddleware[resolv]
+ if ok {
+ 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, "") // sentinal value as demanded.
+ }
+ ap.Zones = zoneAndresolv[:len(zoneAndresolv)-1]
+ if len(ap.Zones) == 0 {
+ ap.Zones = make([]string, len(c.ServerBlockKeys))
+ copy(ap.Zones, c.ServerBlockKeys)
+ }
+ for i, str := range ap.Zones {
+ ap.Zones[i] = plugin.Host(str).Normalize()
+ }
+ }
+ return ap, mw, nil
+}
diff --git a/plugin/autopath/setup_test.go b/plugin/autopath/setup_test.go
new file mode 100644
index 000000000..3e13aa74f
--- /dev/null
+++ b/plugin/autopath/setup_test.go
@@ -0,0 +1,77 @@
+package autopath
+
+import (
+ "os"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/mholt/caddy"
+)
+
+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"},
+ }
+
+ 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, Middleware 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/plugin/backend.go b/plugin/backend.go
new file mode 100644
index 000000000..b520ce390
--- /dev/null
+++ b/plugin/backend.go
@@ -0,0 +1,32 @@
+package plugin
+
+import (
+ "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 definition. Exact indicates
+ // on exact much are that we are allowed to recurs.
+ Services(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(state request.Request, exact bool, opt Options) ([]msg.Service, error)
+
+ // Lookup is used to find records else where.
+ Lookup(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(state request.Request, exact bool) ([]msg.Service, error)
+
+ // IsNameError return true if err indicated a record not found condition
+ IsNameError(err error) bool
+}
+
+// Options are extra options that can be specified for a lookup.
+type Options struct{}
diff --git a/plugin/backend_lookup.go b/plugin/backend_lookup.go
new file mode 100644
index 000000000..f04b397b2
--- /dev/null
+++ b/plugin/backend_lookup.go
@@ -0,0 +1,410 @@
+package plugin
+
+import (
+ "fmt"
+ "math"
+ "net"
+ "time"
+
+ "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(b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, err error) {
+ services, err := b.Services(state, false, opt)
+ if err != nil {
+ return nil, err
+ }
+
+ 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
+ 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
+ }
+
+ state1 := state.NewWithQuestion(serv.Host, state.QType())
+ nextRecords, err := A(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...)
+ }
+ continue
+ }
+ // This means we can not complete the CNAME, try to look else where.
+ target := newRecord.Target
+ if dns.IsSubDomain(zone, target) {
+ // We should already have found it
+ continue
+ }
+ // Lookup
+ m1, e1 := b.Lookup(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.TypeA:
+ records = append(records, serv.NewA(state.QName(), ip))
+
+ case dns.TypeAAAA:
+ // nodata?
+ }
+ }
+ return records, nil
+}
+
+// AAAA returns AAAA records from Backend or an error.
+func AAAA(b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, err error) {
+ services, err := b.Services(state, false, opt)
+ if err != nil {
+ return nil, err
+ }
+
+ 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
+ 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
+ }
+
+ state1 := state.NewWithQuestion(serv.Host, state.QType())
+ nextRecords, err := AAAA(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...)
+ }
+ continue
+ }
+ // This means we can not complete the CNAME, try to look else where.
+ target := newRecord.Target
+ if dns.IsSubDomain(zone, target) {
+ // We should already have found it
+ continue
+ }
+ m1, e1 := b.Lookup(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
+ // both here again
+
+ case dns.TypeA:
+ // nada?
+
+ case dns.TypeAAAA:
+ records = append(records, serv.NewAAAA(state.QName(), ip))
+ }
+ }
+ return records, 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(b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, err error) {
+ services, err := b.Services(state, false, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Looping twice to get the right weight vs priority
+ 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
+ }
+ lookup := make(map[string]bool)
+ for _, serv := range services {
+ w1 := 100.0 / float64(w[serv.Priority])
+ if serv.Weight == 0 {
+ w1 *= 100
+ } else {
+ w1 *= float64(serv.Weight)
+ }
+ weight := uint16(math.Floor(w1))
+
+ 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] = true
+
+ if !dns.IsSubDomain(zone, srv.Target) {
+ m1, e1 := b.Lookup(state, srv.Target, dns.TypeA)
+ if e1 == nil {
+ extra = append(extra, m1.Answer...)
+ }
+
+ m1, e1 = b.Lookup(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(b, zone, state1, nil, opt)
+ if e1 == nil {
+ extra = append(extra, addr...)
+ }
+ // IPv6 lookups here as well? AAAA(zone, state1, nil).
+
+ case dns.TypeA, dns.TypeAAAA:
+ serv.Host = msg.Domain(serv.Key)
+ srv := serv.NewSRV(state.QName(), weight)
+
+ records = append(records, srv)
+ 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(b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, err error) {
+ services, err := b.Services(state, false, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ lookup := make(map[string]bool)
+ 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] = true
+
+ if !dns.IsSubDomain(zone, mx.Mx) {
+ m1, e1 := b.Lookup(state, mx.Mx, dns.TypeA)
+ if e1 == nil {
+ extra = append(extra, m1.Answer...)
+ }
+
+ m1, e1 = b.Lookup(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(b, zone, state1, nil, opt)
+ if e1 == nil {
+ extra = append(extra, addr...)
+ }
+ // e.AAAA as well
+
+ case dns.TypeA, dns.TypeAAAA:
+ serv.Host = msg.Domain(serv.Key)
+ records = append(records, serv.NewMX(state.QName()))
+ 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(b ServiceBackend, zone string, state request.Request, opt Options) (records []dns.RR, err error) {
+ services, err := b.Services(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(b ServiceBackend, zone string, state request.Request, opt Options) (records []dns.RR, err error) {
+ services, err := b.Services(state, false, opt)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, serv := range services {
+ if serv.Text == "" {
+ continue
+ }
+ records = append(records, serv.NewTXT(state.QName()))
+ }
+ return records, nil
+}
+
+// PTR returns the PTR records from the backend, only services that have a domain name as host are included.
+func PTR(b ServiceBackend, zone string, state request.Request, opt Options) (records []dns.RR, err error) {
+ services, err := b.Reverse(state, true, opt)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, serv := range services {
+ if ip := net.ParseIP(serv.Host); ip == nil {
+ records = append(records, serv.NewPTR(state.QName(), serv.Host))
+ }
+ }
+ return records, nil
+}
+
+// NS returns NS records from the backend
+func NS(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.<zone>. Fake our lookup.
+ // only a tad bit fishy...
+ old := state.QName()
+
+ state.Clear()
+ state.Req.Question[0].Name = "ns.dns." + zone
+ services, err := b.Services(state, false, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+ // ... and reset
+ state.Req.Question[0].Name = old
+
+ 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)
+ records = append(records, serv.NewNS(state.QName()))
+ extra = append(extra, newAddress(serv, serv.Host, ip, what))
+ }
+ }
+ return records, extra, nil
+}
+
+// SOA returns a SOA record from the backend.
+func SOA(b ServiceBackend, zone string, state request.Request, opt Options) ([]dns.RR, error) {
+ header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: 300, Class: dns.ClassINET}
+
+ Mbox := hostmaster + "."
+ Ns := "ns.dns."
+ if zone[0] != '.' {
+ Mbox += zone
+ Ns += zone
+ }
+
+ soa := &dns.SOA{Hdr: header,
+ Mbox: Mbox,
+ Ns: Ns,
+ Serial: uint32(time.Now().Unix()),
+ Refresh: 7200,
+ Retry: 1800,
+ Expire: 86400,
+ Minttl: minTTL,
+ }
+ return []dns.RR{soa}, nil
+}
+
+// BackendError writes an error response to the client.
+func BackendError(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, m.RecursionAvailable, m.Compress = true, true, true
+ m.Ns, _ = SOA(b, zone, state, opt)
+
+ state.SizeAndDo(m)
+ 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}
+}
+
+const (
+ minTTL = 60
+ hostmaster = "hostmaster"
+)
diff --git a/plugin/bind/README.md b/plugin/bind/README.md
new file mode 100644
index 000000000..57b3c1e18
--- /dev/null
+++ b/plugin/bind/README.md
@@ -0,0 +1,22 @@
+# bind
+
+*bind* overrides the host to which the server should bind.
+
+Normally, the listener binds to the wildcard host. However, you may force the listener to bind to
+another IP instead. This directive accepts only an address, not a port.
+
+## Syntax
+
+~~~ txt
+bind ADDRESS
+~~~
+
+**ADDRESS** is the IP address to bind to.
+
+## Examples
+
+To make your socket accessible only to that machine, bind to IP 127.0.0.1 (localhost):
+
+~~~ txt
+bind 127.0.0.1
+~~~
diff --git a/plugin/bind/bind.go b/plugin/bind/bind.go
new file mode 100644
index 000000000..bd3c32b51
--- /dev/null
+++ b/plugin/bind/bind.go
@@ -0,0 +1,11 @@
+// Package bind allows binding to a specific interface instead of bind to all of them.
+package bind
+
+import "github.com/mholt/caddy"
+
+func init() {
+ caddy.RegisterPlugin("bind", caddy.Plugin{
+ ServerType: "dns",
+ Action: setupBind,
+ })
+}
diff --git a/plugin/bind/bind_test.go b/plugin/bind/bind_test.go
new file mode 100644
index 000000000..11556f0bd
--- /dev/null
+++ b/plugin/bind/bind_test.go
@@ -0,0 +1,30 @@
+package bind
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/core/dnsserver"
+
+ "github.com/mholt/caddy"
+)
+
+func TestSetupBind(t *testing.T) {
+ c := caddy.NewTestController("dns", `bind 1.2.3.4`)
+ err := setupBind(c)
+ if err != nil {
+ t.Fatalf("Expected no errors, but got: %v", err)
+ }
+
+ cfg := dnsserver.GetConfig(c)
+ if got, want := cfg.ListenHost, "1.2.3.4"; got != want {
+ t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got)
+ }
+}
+
+func TestBindAddress(t *testing.T) {
+ c := caddy.NewTestController("dns", `bind 1.2.3.bla`)
+ err := setupBind(c)
+ if err == nil {
+ t.Fatalf("Expected errors, but got none")
+ }
+}
diff --git a/plugin/bind/setup.go b/plugin/bind/setup.go
new file mode 100644
index 000000000..796377841
--- /dev/null
+++ b/plugin/bind/setup.go
@@ -0,0 +1,24 @@
+package bind
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func setupBind(c *caddy.Controller) error {
+ config := dnsserver.GetConfig(c)
+ for c.Next() {
+ if !c.Args(&config.ListenHost) {
+ return plugin.Error("bind", c.ArgErr())
+ }
+ }
+ if net.ParseIP(config.ListenHost) == nil {
+ return plugin.Error("bind", fmt.Errorf("not a valid IP address: %s", config.ListenHost))
+ }
+ return nil
+}
diff --git a/plugin/cache/README.md b/plugin/cache/README.md
new file mode 100644
index 000000000..6477fe891
--- /dev/null
+++ b/plugin/cache/README.md
@@ -0,0 +1,68 @@
+# cache
+
+*cache* enables a frontend cache. It will cache all records except zone transfers and metadata records.
+
+## 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 the record 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).
+For the negative cache, the SOA's MinTTL value is used. A cache can contain up to 10,000 items by
+default. A TTL of zero is not allowed.
+
+If you want more control:
+
+~~~ txt
+cache [TTL] [ZONES...] {
+ success CAPACITY [TTL]
+ denial CAPACITY [TTL]
+ prefetch AMOUNT [[DURATION] [PERCENTAGE%]]
+}
+~~~
+
+* **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.
+* `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.
+ 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 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%`. Values should be in the range `[10%, 90%]`. Note the percent sign is
+ mandatory. **PERCENTAGE** is treated as an `int`.
+
+The minimum TTL allowed on resource records is 5 seconds.
+
+## Metrics
+
+If monitoring is enabled (via the *prometheus* directive) then the following metrics are exported:
+
+* coredns_cache_size{type} - Total elements in the cache by cache type.
+* coredns_cache_capacity{type} - Total capacity of the cache by cache type.
+* coredns_cache_hits_total{type} - Counter of cache hits by cache type.
+* coredns_cache_misses_total - Counter of cache misses.
+
+Cache types are either "denial" or "success".
+
+## Examples
+
+Enable caching for all zones, but cap everything to a TTL of 10 seconds:
+
+~~~
+cache 10
+~~~
+
+Proxy to Google Public DNS and only cache responses for example.org (or below).
+
+~~~
+proxy . 8.8.8.8:53
+cache example.org
+~~~
diff --git a/plugin/cache/cache.go b/plugin/cache/cache.go
new file mode 100644
index 000000000..b37e527cf
--- /dev/null
+++ b/plugin/cache/cache.go
@@ -0,0 +1,167 @@
+// Package cache implements a cache.
+package cache
+
+import (
+ "encoding/binary"
+ "hash/fnv"
+ "log"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/cache"
+ "github.com/coredns/coredns/plugin/pkg/response"
+
+ "github.com/miekg/dns"
+)
+
+// Cache is 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
+
+ ncache *cache.Cache
+ ncap int
+ nttl time.Duration
+
+ pcache *cache.Cache
+ pcap int
+ pttl time.Duration
+
+ // Prefetch.
+ prefetch int
+ duration time.Duration
+ percentage int
+}
+
+// Return 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.
+func key(m *dns.Msg, t response.Type, do bool) int {
+ // We don't store truncated responses.
+ if m.Truncated {
+ return -1
+ }
+ // Nor errors or Meta or Update
+ if t == response.OtherError || t == response.Meta || t == response.Update {
+ return -1
+ }
+
+ return int(hash(m.Question[0].Name, m.Question[0].Qtype, do))
+}
+
+var one = []byte("1")
+var zero = []byte("0")
+
+func hash(qname string, qtype uint16, do bool) uint32 {
+ h := fnv.New32()
+
+ if do {
+ h.Write(one)
+ } else {
+ h.Write(zero)
+ }
+
+ b := make([]byte, 2)
+ binary.BigEndian.PutUint16(b, qtype)
+ h.Write(b)
+
+ for i := range qname {
+ c := qname[i]
+ if c >= 'A' && c <= 'Z' {
+ c += 'a' - 'A'
+ }
+ h.Write([]byte{c})
+ }
+
+ return h.Sum32()
+}
+
+// ResponseWriter is a response writer that caches the reply message.
+type ResponseWriter struct {
+ dns.ResponseWriter
+ *Cache
+
+ prefetch bool // When true write nothing back to the client.
+}
+
+// WriteMsg implements the dns.ResponseWriter interface.
+func (w *ResponseWriter) WriteMsg(res *dns.Msg) error {
+ do := false
+ mt, opt := response.Typify(res, time.Now().UTC())
+ if opt != nil {
+ do = opt.Do()
+ }
+
+ // key returns empty string for anything we don't want to cache.
+ key := key(res, mt, do)
+
+ duration := w.pttl
+ if mt == response.NameError || mt == response.NoData {
+ duration = w.nttl
+ }
+
+ msgTTL := minMsgTTL(res, mt)
+ if msgTTL < duration {
+ duration = msgTTL
+ }
+
+ if key != -1 {
+ w.set(res, key, mt, duration)
+
+ cacheSize.WithLabelValues(Success).Set(float64(w.pcache.Len()))
+ cacheSize.WithLabelValues(Denial).Set(float64(w.ncache.Len()))
+ }
+
+ if w.prefetch {
+ return nil
+ }
+
+ return w.ResponseWriter.WriteMsg(res)
+}
+
+func (w *ResponseWriter) set(m *dns.Msg, key int, mt response.Type, duration time.Duration) {
+ if key == -1 {
+ log.Printf("[ERROR] Caching called with empty cache key")
+ return
+ }
+
+ switch mt {
+ case response.NoError, response.Delegation:
+ i := newItem(m, duration)
+ w.pcache.Add(uint32(key), i)
+
+ case response.NameError, response.NoData:
+ i := newItem(m, duration)
+ w.ncache.Add(uint32(key), i)
+
+ case response.OtherError:
+ // don't cache these
+ default:
+ log.Printf("[WARNING] Caching called with unknown classification: %d", mt)
+ }
+}
+
+// Write implements the dns.ResponseWriter interface.
+func (w *ResponseWriter) Write(buf []byte) (int, error) {
+ log.Printf("[WARNING] Caching called with Write: not caching reply")
+ if w.prefetch {
+ return 0, nil
+ }
+ n, err := w.ResponseWriter.Write(buf)
+ return n, err
+}
+
+const (
+ maxTTL = 1 * time.Hour
+ maxNTTL = 30 * time.Minute
+
+ minTTL = 5 // seconds
+
+ 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/plugin/cache/cache_test.go b/plugin/cache/cache_test.go
new file mode 100644
index 000000000..ad23f4d5a
--- /dev/null
+++ b/plugin/cache/cache_test.go
@@ -0,0 +1,251 @@
+package cache
+
+import (
+ "io/ioutil"
+ "log"
+ "testing"
+ "time"
+
+ "golang.org/x/net/context"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/cache"
+ "github.com/coredns/coredns/plugin/pkg/response"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+)
+
+type cacheTestCase struct {
+ test.Case
+ in test.Case
+ AuthenticatedData bool
+ Authoritative bool
+ RecursionAvailable bool
+ Truncated bool
+ shouldCache bool
+}
+
+var cacheTestCases = []cacheTestCase{
+ {
+ RecursionAvailable: true, AuthenticatedData: true, Authoritative: 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, Authoritative: 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,
+ },
+ {
+ 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, Authoritative: 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, Authoritative: 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: false,
+ },
+ {
+ RecursionAvailable: true, Authoritative: 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,
+ },
+}
+
+func cacheMsg(m *dns.Msg, tc cacheTestCase) *dns.Msg {
+ m.RecursionAvailable = tc.RecursionAvailable
+ m.AuthenticatedData = tc.AuthenticatedData
+ m.Authoritative = tc.Authoritative
+ 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 := &Cache{Zones: []string{"."}, pcap: defaultCap, ncap: defaultCap, pttl: ttl, nttl: ttl}
+ c.pcache = cache.New(c.pcap)
+ c.ncache = cache.New(c.ncap)
+
+ crr := &ResponseWriter{ResponseWriter: nil, Cache: c}
+ return c, crr
+}
+
+func TestCache(t *testing.T) {
+ now, _ := time.Parse(time.UnixDate, "Fri Apr 21 10:51:21 BST 2017")
+ utc := now.UTC()
+
+ c, crr := newTestCache(maxTTL)
+
+ log.SetOutput(ioutil.Discard)
+
+ for _, tc := range cacheTestCases {
+ m := tc.in.Msg()
+ m = cacheMsg(m, tc)
+ do := tc.in.Do
+
+ mt, _ := response.Typify(m, utc)
+ k := key(m, mt, do)
+
+ crr.set(m, k, mt, c.pttl)
+
+ name := plugin.Name(m.Question[0].Name).Normalize()
+ qtype := m.Question[0].Qtype
+
+ i, _ := c.get(time.Now().UTC(), name, qtype, do)
+ ok := i != nil
+
+ if ok != tc.shouldCache {
+ t.Errorf("cached message that should not have been cached: %s", name)
+ continue
+ }
+
+ if ok {
+ resp := i.toMsg(m)
+
+ if !test.Header(t, tc.Case, resp) {
+ t.Logf("%v\n", resp)
+ continue
+ }
+
+ if !test.Section(t, tc.Case, test.Answer, resp.Answer) {
+ t.Logf("%v\n", resp)
+ }
+ if !test.Section(t, tc.Case, test.Ns, resp.Ns) {
+ t.Logf("%v\n", resp)
+
+ }
+ if !test.Section(t, tc.Case, test.Extra, resp.Extra) {
+ t.Logf("%v\n", resp)
+ }
+ }
+ }
+}
+
+func BenchmarkCacheResponse(b *testing.B) {
+ c := &Cache{Zones: []string{"."}, pcap: defaultCap, ncap: defaultCap, pttl: maxTTL, nttl: maxTTL}
+ c.pcache = cache.New(c.pcap)
+ c.ncache = cache.New(c.ncap)
+ c.prefetch = 1
+ c.duration = 1 * time.Second
+ 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.RunParallel(func(pb *testing.PB) {
+ i := 0
+ for pb.Next() {
+ req := reqs[i]
+ c.ServeDNS(ctx, &test.ResponseWriter{}, req)
+ i++
+ i = i % 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
+ })
+}
diff --git a/plugin/cache/freq/freq.go b/plugin/cache/freq/freq.go
new file mode 100644
index 000000000..f545f222e
--- /dev/null
+++ b/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/plugin/cache/freq/freq_test.go b/plugin/cache/freq/freq_test.go
new file mode 100644
index 000000000..740194c86
--- /dev/null
+++ b/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/plugin/cache/handler.go b/plugin/cache/handler.go
new file mode 100644
index 000000000..ebd87d659
--- /dev/null
+++ b/plugin/cache/handler.go
@@ -0,0 +1,119 @@
+package cache
+
+import (
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "github.com/prometheus/client_golang/prometheus"
+ "golang.org/x/net/context"
+)
+
+// ServeDNS implements the plugin.Handler interface.
+func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ state := request.Request{W: w, Req: r}
+
+ qname := state.Name()
+ qtype := state.QType()
+ zone := plugin.Zones(c.Zones).Matches(qname)
+ if zone == "" {
+ return c.Next.ServeDNS(ctx, w, r)
+ }
+
+ do := state.Do() // TODO(): might need more from OPT record? Like the actual bufsize?
+
+ now := time.Now().UTC()
+
+ i, ttl := c.get(now, qname, qtype, do)
+ if i != nil && ttl > 0 {
+ resp := i.toMsg(r)
+
+ state.SizeAndDo(resp)
+ resp, _ = state.Scrub(resp)
+ w.WriteMsg(resp)
+
+ i.Freq.Update(c.duration, now)
+
+ pct := 100
+ if i.origTTL != 0 { // you'll never know
+ pct = int(float64(ttl) / float64(i.origTTL) * 100)
+ }
+
+ if c.prefetch > 0 && i.Freq.Hits() > c.prefetch && pct < c.percentage {
+ // 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.
+ prr := &ResponseWriter{ResponseWriter: w, Cache: c, prefetch: true}
+ plugin.NextOrFailure(c.Name(), c.Next, ctx, prr, r)
+
+ if i1, _ := c.get(now, qname, qtype, do); i1 != nil {
+ i1.Freq.Reset(now, i.Freq.Hits())
+ }
+ }
+
+ return dns.RcodeSuccess, nil
+ }
+
+ crr := &ResponseWriter{ResponseWriter: w, Cache: c}
+ return plugin.NextOrFailure(c.Name(), c.Next, ctx, crr, r)
+}
+
+// Name implements the Handler interface.
+func (c *Cache) Name() string { return "cache" }
+
+func (c *Cache) get(now time.Time, qname string, qtype uint16, do bool) (*item, int) {
+ k := hash(qname, qtype, do)
+
+ if i, ok := c.ncache.Get(k); ok {
+ cacheHits.WithLabelValues(Denial).Inc()
+ return i.(*item), i.(*item).ttl(now)
+ }
+
+ if i, ok := c.pcache.Get(k); ok {
+ cacheHits.WithLabelValues(Success).Inc()
+ return i.(*item), i.(*item).ttl(now)
+ }
+ cacheMisses.Inc()
+ return nil, 0
+}
+
+var (
+ cacheSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "size",
+ Help: "The number of elements in the cache.",
+ }, []string{"type"})
+
+ cacheCapacity = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "capacity",
+ Help: "The cache's capacity.",
+ }, []string{"type"})
+
+ cacheHits = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "hits_total",
+ Help: "The count of cache hits.",
+ }, []string{"type"})
+
+ cacheMisses = prometheus.NewCounter(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "misses_total",
+ Help: "The count of cache misses.",
+ })
+)
+
+const subsystem = "cache"
+
+func init() {
+ prometheus.MustRegister(cacheSize)
+ prometheus.MustRegister(cacheCapacity)
+ prometheus.MustRegister(cacheHits)
+ prometheus.MustRegister(cacheMisses)
+}
diff --git a/plugin/cache/item.go b/plugin/cache/item.go
new file mode 100644
index 000000000..2c215617b
--- /dev/null
+++ b/plugin/cache/item.go
@@ -0,0 +1,116 @@
+package cache
+
+import (
+ "time"
+
+ "github.com/coredns/coredns/plugin/cache/freq"
+ "github.com/coredns/coredns/plugin/pkg/response"
+ "github.com/miekg/dns"
+)
+
+type item struct {
+ Rcode int
+ Authoritative bool
+ AuthenticatedData bool
+ RecursionAvailable bool
+ Answer []dns.RR
+ Ns []dns.RR
+ Extra []dns.RR
+
+ origTTL uint32
+ stored time.Time
+
+ *freq.Freq
+}
+
+func newItem(m *dns.Msg, d time.Duration) *item {
+ i := new(item)
+ i.Rcode = m.Rcode
+ i.Authoritative = m.Authoritative
+ 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 record 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 = time.Now().UTC()
+
+ i.Freq = new(freq.Freq)
+
+ return i
+}
+
+// toMsg turns i into a message, it tailors the reply to m.
+// The Authoritative bit is always set to 0, because the answer is from the cache.
+func (i *item) toMsg(m *dns.Msg) *dns.Msg {
+ m1 := new(dns.Msg)
+ m1.SetReply(m)
+
+ m1.Authoritative = false
+ m1.AuthenticatedData = i.AuthenticatedData
+ m1.RecursionAvailable = i.RecursionAvailable
+ m1.Rcode = i.Rcode
+ m1.Compress = true
+
+ 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(time.Now()))
+ if ttl < minTTL {
+ ttl = minTTL
+ }
+
+ for j, r := range i.Answer {
+ m1.Answer[j] = dns.Copy(r)
+ m1.Answer[j].Header().Ttl = ttl
+ }
+ for j, r := range i.Ns {
+ m1.Ns[j] = dns.Copy(r)
+ m1.Ns[j].Header().Ttl = ttl
+ }
+ for j, r := range i.Extra {
+ m1.Extra[j] = dns.Copy(r)
+ if m1.Extra[j].Header().Rrtype != dns.TypeOPT {
+ m1.Extra[j].Header().Ttl = ttl
+ }
+ }
+ return m1
+}
+
+func (i *item) ttl(now time.Time) int {
+ ttl := int(i.origTTL) - int(now.UTC().Sub(i.stored).Seconds())
+ return ttl
+}
+
+func minMsgTTL(m *dns.Msg, mt response.Type) time.Duration {
+ if mt != response.NoError && mt != response.NameError && mt != response.NoData {
+ return 0
+ }
+
+ minTTL := maxTTL
+ for _, r := range append(m.Answer, m.Ns...) {
+ switch mt {
+ case response.NameError, response.NoData:
+ if r.Header().Rrtype == dns.TypeSOA {
+ return time.Duration(r.(*dns.SOA).Minttl) * time.Second
+ }
+ case response.NoError, response.Delegation:
+ if r.Header().Ttl < uint32(minTTL.Seconds()) {
+ minTTL = time.Duration(r.Header().Ttl) * time.Second
+ }
+ }
+ }
+ return minTTL
+}
diff --git a/plugin/cache/prefech_test.go b/plugin/cache/prefech_test.go
new file mode 100644
index 000000000..0e9d84da2
--- /dev/null
+++ b/plugin/cache/prefech_test.go
@@ -0,0 +1,54 @@
+package cache
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/cache"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+
+ "github.com/coredns/coredns/plugin/test"
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+var p = false
+
+func TestPrefetch(t *testing.T) {
+ c := &Cache{Zones: []string{"."}, pcap: defaultCap, ncap: defaultCap, pttl: maxTTL, nttl: maxTTL}
+ c.pcache = cache.New(c.pcap)
+ c.ncache = cache.New(c.ncap)
+ c.prefetch = 1
+ c.duration = 1 * time.Second
+ c.Next = PrefetchHandler(t, dns.RcodeSuccess, nil)
+
+ ctx := context.TODO()
+
+ req := new(dns.Msg)
+ req.SetQuestion("lowttl.example.org.", dns.TypeA)
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+
+ c.ServeDNS(ctx, rec, req)
+ p = true // prefetch should be true for the 2nd fetch
+ c.ServeDNS(ctx, rec, req)
+}
+
+func PrefetchHandler(t *testing.T, rcode int, err error) plugin.Handler {
+ return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ m := new(dns.Msg)
+ m.SetQuestion("lowttl.example.org.", dns.TypeA)
+ m.Response = true
+ m.RecursionAvailable = true
+ m.Answer = append(m.Answer, test.A("lowttl.example.org. 80 IN A 127.0.0.53"))
+ if p != w.(*ResponseWriter).prefetch {
+ err = fmt.Errorf("cache prefetch not equal to p: got %t, want %t", p, w.(*ResponseWriter).prefetch)
+ t.Fatal(err)
+ }
+
+ w.WriteMsg(m)
+ return rcode, err
+ })
+}
diff --git a/plugin/cache/setup.go b/plugin/cache/setup.go
new file mode 100644
index 000000000..d8ef9a8d7
--- /dev/null
+++ b/plugin/cache/setup.go
@@ -0,0 +1,170 @@
+package cache
+
+import (
+ "fmt"
+ "strconv"
+ "time"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/cache"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("cache", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ ca, err := cacheParse(c)
+ if err != nil {
+ return plugin.Error("cache", err)
+ }
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ ca.Next = next
+ return ca
+ })
+
+ // Export the capacity for the metrics. This only happens once, because this is a re-load change only.
+ cacheCapacity.WithLabelValues(Success).Set(float64(ca.pcap))
+ cacheCapacity.WithLabelValues(Denial).Set(float64(ca.ncap))
+
+ return nil
+}
+
+func cacheParse(c *caddy.Controller) (*Cache, error) {
+
+ ca := &Cache{pcap: defaultCap, ncap: defaultCap, pttl: maxTTL, nttl: maxNTTL, prefetch: 0, duration: 1 * time.Minute}
+
+ for c.Next() {
+ // cache [ttl] [zones..]
+ origins := make([]string, len(c.ServerBlockKeys))
+ copy(origins, c.ServerBlockKeys)
+ args := c.RemainingArgs()
+
+ if len(args) > 0 {
+ // first args may be just a number, then it is the ttl, if not it is a zone
+ ttl, err := strconv.Atoi(args[0])
+ 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:]
+ }
+ if len(args) > 0 {
+ copy(origins, args)
+ }
+ }
+
+ // 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
+ }
+ 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
+ }
+ 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
+
+ ca.duration = 1 * time.Minute
+ ca.percentage = 10
+ 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
+ }
+
+ default:
+ return nil, c.ArgErr()
+ }
+ }
+
+ for i := range origins {
+ origins[i] = plugin.Host(origins[i]).Normalize()
+ }
+
+ ca.Zones = origins
+
+ ca.pcache = cache.New(ca.pcap)
+ ca.ncache = cache.New(ca.ncap)
+
+ return ca, nil
+ }
+
+ return nil, nil
+}
diff --git a/plugin/cache/setup_test.go b/plugin/cache/setup_test.go
new file mode 100644
index 000000000..afc2ecc13
--- /dev/null
+++ b/plugin/cache/setup_test.go
@@ -0,0 +1,94 @@
+package cache
+
+import (
+ "testing"
+ "time"
+
+ "github.com/mholt/caddy"
+)
+
+func TestSetup(t *testing.T) {
+ tests := []struct {
+ input string
+ shouldErr bool
+ expectedNcap int
+ expectedPcap int
+ expectedNttl time.Duration
+ expectedPttl time.Duration
+ expectedPrefetch int
+ }{
+ {`cache`, false, defaultCap, defaultCap, maxNTTL, maxTTL, 0},
+ {`cache {}`, false, defaultCap, defaultCap, maxNTTL, maxTTL, 0},
+ {`cache example.nl {
+ success 10
+ }`, false, defaultCap, 10, maxNTTL, maxTTL, 0},
+ {`cache example.nl {
+ success 10
+ denial 10 15
+ }`, false, 10, 10, 15 * time.Second, maxTTL, 0},
+ {`cache 25 example.nl {
+ success 10
+ denial 10 15
+ }`, false, 10, 10, 15 * time.Second, 25 * time.Second, 0},
+ {`cache aaa example.nl`, false, defaultCap, defaultCap, maxNTTL, maxTTL, 0},
+ {`cache {
+ prefetch 10
+ }`, false, defaultCap, defaultCap, maxNTTL, maxTTL, 10},
+
+ // fails
+ {`cache example.nl {
+ success
+ denial 10 15
+ }`, true, defaultCap, defaultCap, maxTTL, maxTTL, 0},
+ {`cache example.nl {
+ success 15
+ denial aaa
+ }`, true, defaultCap, defaultCap, maxTTL, maxTTL, 0},
+ {`cache example.nl {
+ positive 15
+ negative aaa
+ }`, true, defaultCap, defaultCap, maxTTL, maxTTL, 0},
+ {`cache 0 example.nl`, true, defaultCap, defaultCap, maxTTL, maxTTL, 0},
+ {`cache -1 example.nl`, true, defaultCap, defaultCap, maxTTL, maxTTL, 0},
+ {`cache 1 example.nl {
+ positive 0
+ }`, true, defaultCap, defaultCap, maxTTL, maxTTL, 0},
+ {`cache 1 example.nl {
+ positive 0
+ prefetch -1
+ }`, true, defaultCap, defaultCap, maxTTL, maxTTL, 0},
+ {`cache 1 example.nl {
+ prefetch 0 blurp
+ }`, true, defaultCap, defaultCap, maxTTL, maxTTL, 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.pttl != test.expectedPttl {
+ t.Errorf("Test %v: Expected pttl %v but found: %v", i, test.expectedPttl, ca.pttl)
+ }
+ if ca.prefetch != test.expectedPrefetch {
+ t.Errorf("Test %v: Expected prefetch %v but found: %v", i, test.expectedPrefetch, ca.prefetch)
+ }
+ }
+}
diff --git a/plugin/chaos/README.md b/plugin/chaos/README.md
new file mode 100644
index 000000000..4c43590e5
--- /dev/null
+++ b/plugin/chaos/README.md
@@ -0,0 +1,46 @@
+# chaos
+
+The *chaos* plugin allows CoreDNS to respond to TXT queries in the CH class.
+
+This is useful for retrieving version or author information from the server.
+
+## Syntax
+
+~~~
+chaos [VERSION] [AUTHORS...]
+~~~
+
+* **VERSION** is the version to return. Defaults to `CoreDNS-<version>`, if not set.
+* **AUTHORS** is what authors to return. No default.
+
+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/plugin/chaos/chaos.go b/plugin/chaos/chaos.go
new file mode 100644
index 000000000..c9811fbd0
--- /dev/null
+++ b/plugin/chaos/chaos.go
@@ -0,0 +1,62 @@
+// Package chaos implements a plugin that answer to 'CH version.bind TXT' type queries.
+package chaos
+
+import (
+ "os"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Chaos allows CoreDNS to reply to CH TXT queries and return author or
+// version information.
+type Chaos struct {
+ Next plugin.Handler
+ Version string
+ Authors map[string]bool
+}
+
+// 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 c.Next.ServeDNS(ctx, w, r)
+ case "authors.bind.":
+ for a := range c.Authors {
+ m.Answer = append(m.Answer, &dns.TXT{Hdr: hdr, Txt: []string{trim(a)}})
+ }
+ case "version.bind.", "version.server.":
+ m.Answer = []dns.RR{&dns.TXT{Hdr: hdr, Txt: []string{trim(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)}}}
+ }
+ state.SizeAndDo(m)
+ w.WriteMsg(m)
+ return 0, nil
+}
+
+// Name implements the Handler interface.
+func (c Chaos) Name() string { return "chaos" }
+
+func trim(s string) string {
+ if len(s) < 256 {
+ return s
+ }
+ return s[:255]
+}
diff --git a/plugin/chaos/chaos_test.go b/plugin/chaos/chaos_test.go
new file mode 100644
index 000000000..332d90381
--- /dev/null
+++ b/plugin/chaos/chaos_test.go
@@ -0,0 +1,80 @@
+package chaos
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestChaos(t *testing.T) {
+ em := Chaos{
+ Version: version,
+ Authors: map[string]bool{"Miek Gieben": true},
+ }
+
+ 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 := dnsrecorder.New(&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/plugin/chaos/setup.go b/plugin/chaos/setup.go
new file mode 100644
index 000000000..2064f4eae
--- /dev/null
+++ b/plugin/chaos/setup.go
@@ -0,0 +1,55 @@
+package chaos
+
+import (
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("chaos", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+
+}
+
+func setup(c *caddy.Controller) error {
+ version, authors, err := chaosParse(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 chaosParse(c *caddy.Controller) (string, map[string]bool, error) {
+ // Set here so we pick up AppName and AppVersion that get set in coremain's init().
+ chaosVersion = caddy.AppName + "-" + caddy.AppVersion
+
+ version := ""
+ authors := make(map[string]bool)
+
+ for c.Next() {
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ return chaosVersion, nil, nil
+ }
+ if len(args) == 1 {
+ return args[0], nil, nil
+ }
+ version = args[0]
+ for _, a := range args[1:] {
+ authors[a] = true
+ }
+ return version, authors, nil
+ }
+ return version, authors, nil
+}
+
+var chaosVersion string
diff --git a/plugin/chaos/setup_test.go b/plugin/chaos/setup_test.go
new file mode 100644
index 000000000..6f3c13fb3
--- /dev/null
+++ b/plugin/chaos/setup_test.go
@@ -0,0 +1,54 @@
+package chaos
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/mholt/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 map).
+ 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 := chaosParse(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("Chaos not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedVersion, version)
+ }
+ if !test.shouldErr && authors != nil {
+ if _, ok := authors[test.expectedAuthor]; !ok {
+ t.Errorf("Chaos not correctly set for input %s. Expected: '%s', actual: '%s'", test.input, test.expectedAuthor, "Miek Gieben")
+ }
+ }
+ }
+}
diff --git a/plugin/debug/README.md b/plugin/debug/README.md
new file mode 100644
index 000000000..2598a1900
--- /dev/null
+++ b/plugin/debug/README.md
@@ -0,0 +1,20 @@
+# debug
+
+*debug* disables the automatic recovery upon a CoreDNS crash so that you'll get a nice stack trace.
+
+Note that the *errors* plugin (if loaded) will also set a `recover` negating this setting.
+The main use of *debug* is to help testing.
+
+## Syntax
+
+~~~ txt
+debug
+~~~
+
+## Examples
+
+Disable CoreDNS' ability to recover from crashes:
+
+~~~ txt
+debug
+~~~
diff --git a/plugin/debug/debug.go b/plugin/debug/debug.go
new file mode 100644
index 000000000..d69ce0e55
--- /dev/null
+++ b/plugin/debug/debug.go
@@ -0,0 +1,28 @@
+package debug
+
+import (
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("debug", caddy.Plugin{
+ ServerType: "dns",
+ Action: 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/plugin/debug/debug_test.go b/plugin/debug/debug_test.go
new file mode 100644
index 000000000..a4802fee5
--- /dev/null
+++ b/plugin/debug/debug_test.go
@@ -0,0 +1,49 @@
+package debug
+
+import (
+ "io/ioutil"
+ "log"
+ "testing"
+
+ "github.com/coredns/coredns/core/dnsserver"
+
+ "github.com/mholt/caddy"
+)
+
+func TestDebug(t *testing.T) {
+ log.SetOutput(ioutil.Discard)
+
+ 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/plugin/dnssec/README.md b/plugin/dnssec/README.md
new file mode 100644
index 000000000..e087f6c9a
--- /dev/null
+++ b/plugin/dnssec/README.md
@@ -0,0 +1,88 @@
+# dnssec
+
+*dnssec* enables on-the-fly DNSSEC signing of served data.
+
+## Syntax
+
+~~~
+dnssec [ZONES... ] {
+ key file KEY...
+ cache_capacity CAPACITY
+}
+~~~
+
+The specified key is used for all signing operations. The DNSSEC signing will treat this key 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.
+
+If multiple *dnssec* plugins are specified in the same zone, the last one specified will be
+used ( see [bugs](#bugs) ).
+
+* `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 <zonename>`. A key created for zone *A* can be safely used for zone *B*. The name of the
+ key file can be specified as 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 capacity is 10000.
+
+## Metrics
+
+If monitoring is enabled (via the *prometheus* directive) then the following metrics are exported:
+
+* coredns_dnssec_cache_size{type} - total elements in the cache, type is "signature".
+* coredns_dnssec_cache_capacity{type} - total capacity of the cache, type is "signature".
+* coredns_dnssec_cache_hits_total - Counter of cache hits.
+* coredns_dnssec_cache_misses_total - Counter of cache misses.
+
+## Examples
+
+Sign responses for `example.org` with the key "Kexample.org.+013+45330.key".
+
+~~~
+example.org:53 {
+ dnssec {
+ key file /etc/coredns/Kexample.org.+013+45330
+ }
+ whoami
+}
+~~~
+
+Sign responses for a kubernetes zone with the key "Kcluster.local+013+45129.key".
+
+~~~
+cluster.local:53 {
+ kubernetes cluster.local
+ dnssec cluster.local {
+ key file /etc/coredns/Kcluster.local+013+45129
+ }
+}
+~~~
+
+## Bugs
+
+Multiple *dnssec* plugins inside one server stanza will silently overwrite earlier ones, here
+`example.local` will overwrite the one for `cluster.local`.
+
+~~~
+.:53 {
+ kubernetes cluster.local
+ dnssec cluster.local {
+ key file /etc/coredns/cluster.local
+ }
+ dnssec example.local {
+ key file /etc/coredns/example.local
+ }
+ whoami
+}
+~~~
diff --git a/plugin/dnssec/black_lies.go b/plugin/dnssec/black_lies.go
new file mode 100644
index 000000000..527b2fc3e
--- /dev/null
+++ b/plugin/dnssec/black_lies.go
@@ -0,0 +1,24 @@
+package dnssec
+
+import "github.com/miekg/dns"
+
+// nsec returns an NSEC useful for NXDOMAIN respsones.
+// 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(name, zone string, ttl, incep, expir uint32) ([]dns.RR, error) {
+ nsec := &dns.NSEC{}
+ nsec.Hdr = dns.RR_Header{Name: name, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeNSEC}
+ nsec.NextDomain = "\\000." + name
+ nsec.TypeBitMap = []uint16{dns.TypeRRSIG, dns.TypeNSEC}
+
+ sigs, err := d.sign([]dns.RR{nsec}, zone, ttl, incep, expir)
+ if err != nil {
+ return nil, err
+ }
+
+ return append(sigs, nsec), nil
+}
diff --git a/plugin/dnssec/black_lies_test.go b/plugin/dnssec/black_lies_test.go
new file mode 100644
index 000000000..80c2ce484
--- /dev/null
+++ b/plugin/dnssec/black_lies_test.go
@@ -0,0 +1,49 @@
+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}
+ m = d.Sign(state, "miek.nl.", time.Now().UTC())
+ if !section(m.Ns, 2) {
+ t.Errorf("authority section should have 2 sig")
+ }
+ 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 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")},
+ }
+}
diff --git a/plugin/dnssec/cache.go b/plugin/dnssec/cache.go
new file mode 100644
index 000000000..ea95b73b4
--- /dev/null
+++ b/plugin/dnssec/cache.go
@@ -0,0 +1,22 @@
+package dnssec
+
+import (
+ "hash/fnv"
+
+ "github.com/miekg/dns"
+)
+
+// hash serializes the RRset and return a signature cache key.
+func hash(rrs []dns.RR) uint32 {
+ h := fnv.New32()
+ buf := make([]byte, 256)
+ for _, r := range rrs {
+ off, err := dns.PackRR(r, buf, 0, nil, false)
+ if err == nil {
+ h.Write(buf[:off])
+ }
+ }
+
+ i := h.Sum32()
+ return i
+}
diff --git a/plugin/dnssec/cache_test.go b/plugin/dnssec/cache_test.go
new file mode 100644
index 000000000..b978df244
--- /dev/null
+++ b/plugin/dnssec/cache_test.go
@@ -0,0 +1,34 @@
+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}
+ k := hash(m.Answer) // calculate *before* we add the sig
+ d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, nil, c)
+ d.Sign(state, "miek.nl.", time.Now().UTC())
+
+ _, ok := d.get(k)
+ if !ok {
+ t.Errorf("signature was not added to the cache")
+ }
+}
diff --git a/plugin/dnssec/dnskey.go b/plugin/dnssec/dnskey.go
new file mode 100644
index 000000000..ce787ab54
--- /dev/null
+++ b/plugin/dnssec/dnskey.go
@@ -0,0 +1,72 @@
+package dnssec
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "errors"
+ "os"
+ "time"
+
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// DNSKEY holds a DNSSEC public and private key used for on-the-fly signing.
+type DNSKEY struct {
+ K *dns.DNSKEY
+ s crypto.Signer
+ keytag 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(pubFile)
+ if e != nil {
+ return nil, e
+ }
+ k, e := dns.ReadRR(f, pubFile)
+ if e != nil {
+ return nil, e
+ }
+
+ f, e = os.Open(privFile)
+ if e != nil {
+ return nil, e
+ }
+ p, e := k.(*dns.DNSKEY).ReadPrivateKey(f, privFile)
+ if e != nil {
+ return nil, e
+ }
+
+ if v, ok := p.(*rsa.PrivateKey); ok {
+ return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil
+ }
+ if v, ok := p.(*ecdsa.PrivateKey); ok {
+ return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil
+ }
+ return &DNSKEY{k.(*dns.DNSKEY), nil, 0}, errors.New("no known? 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) *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); err == nil {
+ m.Answer = append(m.Answer, sigs...)
+ }
+ return m
+}
diff --git a/plugin/dnssec/dnssec.go b/plugin/dnssec/dnssec.go
new file mode 100644
index 000000000..84de05c86
--- /dev/null
+++ b/plugin/dnssec/dnssec.go
@@ -0,0 +1,135 @@
+// 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
+ inflight *singleflight.Group
+ cache *cache.Cache
+}
+
+// New returns a new Dnssec.
+func New(zones []string, keys []*DNSKEY, next plugin.Handler, c *cache.Cache) Dnssec {
+ return Dnssec{Next: next,
+ zones: zones,
+ keys: keys,
+ 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. Signatures
+// creates 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, zone string, now time.Time) *dns.Msg {
+ req := state.Req
+
+ mt, _ := response.Typify(req, time.Now().UTC()) // TODO(miek): need opt record here?
+ if mt == response.Delegation {
+ // TODO(miek): uh, signing DS record?!?!
+ return req
+ }
+
+ incep, expir := incepExpir(now)
+
+ if mt == response.NameError {
+ 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, zone, ttl, incep, expir); err == nil {
+ req.Ns = append(req.Ns, sigs...)
+ }
+ if sigs, err := d.nsec(state.Name(), zone, ttl, incep, expir); 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, zone, ttl, incep, expir); 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, zone, ttl, incep, expir); 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, zone, ttl, incep, expir); err == nil {
+ req.Extra = append(sigs, req.Extra...) // prepend to leave OPT alone
+ }
+ }
+ return req
+}
+
+func (d Dnssec) sign(rrs []dns.RR, signerName string, ttl, incep, expir uint32) ([]dns.RR, error) {
+ k := hash(rrs)
+ sgs, ok := d.get(k)
+ if ok {
+ return sgs, nil
+ }
+
+ sigs, err := d.inflight.Do(k, func() (interface{}, error) {
+ sigs := make([]dns.RR, len(d.keys))
+ var e error
+ for i, k := range d.keys {
+ sig := k.newRRSIG(signerName, ttl, incep, expir)
+ e = sig.Sign(k.s, rrs)
+ sigs[i] = sig
+ }
+ d.set(k, sigs)
+ return sigs, e
+ })
+ return sigs.([]dns.RR), err
+}
+
+func (d Dnssec) set(key uint32, sigs []dns.RR) {
+ d.cache.Add(key, sigs)
+}
+
+func (d Dnssec) get(key uint32) ([]dns.RR, bool) {
+ if s, ok := d.cache.Get(key); ok {
+ cacheHits.Inc()
+ return s.([]dns.RR), true
+ }
+ cacheMisses.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
+ defaultCap = 10000 // default capacity of the cache.
+)
diff --git a/plugin/dnssec/dnssec_test.go b/plugin/dnssec/dnssec_test.go
new file mode 100644
index 000000000..83ce70beb
--- /dev/null
+++ b/plugin/dnssec/dnssec_test.go
@@ -0,0 +1,219 @@
+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}
+
+ m = d.Sign(state, "miek.nl.", time.Now().UTC())
+ if !section(m.Answer, 1) {
+ t.Errorf("answer section should have 1 sig")
+ }
+ if !section(m.Ns, 1) {
+ t.Errorf("authority section should have 1 sig")
+ }
+}
+
+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}
+ m = d.Sign(state, "miek.nl.", time.Now().UTC())
+ if !section(m.Answer, 2) {
+ t.Errorf("answer section should have 1 sig")
+ }
+ if !section(m.Ns, 2) {
+ t.Errorf("authority section should have 1 sig")
+ }
+}
+
+// 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}
+ c := cache.New(defaultCap)
+ d := New([]string{"example.org."}, []*DNSKEY{key}, nil, c)
+ m = d.Sign(state, "example.org.", time.Now().UTC())
+ if !section(m.Answer, 1) {
+ t.Errorf("answer section should have 1 sig")
+ t.Logf("%+v\n", m)
+ }
+ if !section(m.Ns, 1) {
+ t.Errorf("authority section should have 1 sig")
+ 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}
+ m = d.Sign(state, "miek.nl.", time.Now().UTC())
+ if !section(m.Answer, 1) {
+ t.Errorf("answer section should have 1 sig")
+ }
+}
+
+func TestZoneSigningDelegation(t *testing.T) {
+ d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
+ defer rm1()
+ defer rm2()
+
+ m := testDelegationMsg()
+ state := request.Request{Req: m}
+ m = d.Sign(state, "miek.nl.", time.Now().UTC())
+ if !section(m.Ns, 0) {
+ t.Errorf("authority section should have 0 sig")
+ t.Logf("%v\n", m)
+ }
+ if !section(m.Extra, 0) {
+ t.Errorf("answer section should have 0 sig")
+ t.Logf("%v\n", m)
+ }
+}
+
+func TestSigningDname(t *testing.T) {
+ d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
+ defer rm1()
+ defer rm2()
+
+ m := testMsgDname()
+ state := request.Request{Req: m}
+ // We sign *everything* we see, also the synthesized CNAME.
+ m = d.Sign(state, "miek.nl.", time.Now().UTC())
+ if !section(m.Answer, 3) {
+ t.Errorf("answer section should have 3 sig")
+ }
+}
+
+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 testDelegationMsg() *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 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 newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) {
+ k, rm1, rm2 := newKey(t)
+ c := cache.New(defaultCap)
+ d := New(zones, []*DNSKEY{k}, 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/plugin/dnssec/handler.go b/plugin/dnssec/handler.go
new file mode 100644
index 000000000..6fa2dd042
--- /dev/null
+++ b/plugin/dnssec/handler.go
@@ -0,0 +1,82 @@
+package dnssec
+
+import (
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "github.com/prometheus/client_golang/prometheus"
+ "golang.org/x/net/context"
+)
+
+// 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)
+ }
+
+ // 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)
+ resp.Authoritative = true
+ state.SizeAndDo(resp)
+ w.WriteMsg(resp)
+ return dns.RcodeSuccess, nil
+ }
+ }
+ }
+
+ drr := &ResponseWriter{w, d}
+ return plugin.NextOrFailure(d.Name(), d.Next, ctx, drr, r)
+}
+
+var (
+ cacheSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "cache_size",
+ Help: "The number of elements in the dnssec cache.",
+ }, []string{"type"})
+
+ cacheCapacity = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "cache_capacity",
+ Help: "The dnssec cache's capacity.",
+ }, []string{"type"})
+
+ cacheHits = prometheus.NewCounter(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "cache_hits_total",
+ Help: "The count of cache hits.",
+ })
+
+ cacheMisses = prometheus.NewCounter(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "cache_misses_total",
+ Help: "The count of cache misses.",
+ })
+)
+
+// Name implements the Handler interface.
+func (d Dnssec) Name() string { return "dnssec" }
+
+const subsystem = "dnssec"
+
+func init() {
+ prometheus.MustRegister(cacheSize)
+ prometheus.MustRegister(cacheCapacity)
+ prometheus.MustRegister(cacheHits)
+ prometheus.MustRegister(cacheMisses)
+}
diff --git a/plugin/dnssec/handler_test.go b/plugin/dnssec/handler_test.go
new file mode 100644
index 000000000..2202a9ffe
--- /dev/null
+++ b/plugin/dnssec/handler_test.go
@@ -0,0 +1,155 @@
+package dnssec
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/file"
+ "github.com/coredns/coredns/plugin/pkg/cache"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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)},
+ },
+}
+
+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=="),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ 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=="),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ Qname: "www.example.org.", Qtype: dns.TypeAAAA, Do: true,
+ Rcode: dns.RcodeServerFailure,
+ // Extra: []dns.RR{test.OPT(4096, true)}, // test.ErrorHandler is a simple handler that does not do EDNS.
+ },
+}
+
+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}, fm, c)
+ ctx := context.TODO()
+
+ for _, tc := range dnsTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := dh.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+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}, test.ErrorHandler(), c)
+ ctx := context.TODO()
+
+ for _, tc := range dnssecTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := dh.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ if !resp.Authoritative {
+ t.Errorf("Authoritative Answer should be true, got false")
+ }
+
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+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/plugin/dnssec/responsewriter.go b/plugin/dnssec/responsewriter.go
new file mode 100644
index 000000000..793cbcdd0
--- /dev/null
+++ b/plugin/dnssec/responsewriter.go
@@ -0,0 +1,49 @@
+package dnssec
+
+import (
+ "log"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// ResponseWriter sign the response on the fly.
+type ResponseWriter struct {
+ dns.ResponseWriter
+ d Dnssec
+}
+
+// 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}
+
+ qname := state.Name()
+ zone := plugin.Zones(d.d.zones).Matches(qname)
+ if zone == "" {
+ return d.ResponseWriter.WriteMsg(res)
+ }
+
+ if state.Do() {
+ res = d.d.Sign(state, zone, time.Now().UTC())
+
+ cacheSize.WithLabelValues("signature").Set(float64(d.d.cache.Len()))
+ }
+ state.SizeAndDo(res)
+
+ return d.ResponseWriter.WriteMsg(res)
+}
+
+// Write implements the dns.ResponseWriter interface.
+func (d *ResponseWriter) Write(buf []byte) (int, error) {
+ log.Printf("[WARNING] Dnssec called with Write: not signing reply")
+ n, err := d.ResponseWriter.Write(buf)
+ return n, err
+}
+
+// Hijack implements the dns.ResponseWriter interface.
+func (d *ResponseWriter) Hijack() { d.ResponseWriter.Hijack() }
diff --git a/plugin/dnssec/rrsig.go b/plugin/dnssec/rrsig.go
new file mode 100644
index 000000000..c68413622
--- /dev/null
+++ b/plugin/dnssec/rrsig.go
@@ -0,0 +1,53 @@
+package dnssec
+
+import "github.com/miekg/dns"
+
+// newRRSIG return 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.keytag
+ 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/plugin/dnssec/setup.go b/plugin/dnssec/setup.go
new file mode 100644
index 000000000..2f5c21d97
--- /dev/null
+++ b/plugin/dnssec/setup.go
@@ -0,0 +1,128 @@
+package dnssec
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/cache"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("dnssec", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ zones, keys, capacity, err := dnssecParse(c)
+ if err != nil {
+ return plugin.Error("dnssec", err)
+ }
+
+ ca := cache.New(capacity)
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ return New(zones, keys, next, ca)
+ })
+
+ // Export the capacity for the metrics. This only happens once, because this is a re-load change only.
+ cacheCapacity.WithLabelValues("signature").Set(float64(capacity))
+
+ return nil
+}
+
+func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, error) {
+ zones := []string{}
+
+ keys := []*DNSKEY{}
+
+ capacity := defaultCap
+ for c.Next() {
+ // dnssec [zones...]
+ zones = make([]string, len(c.ServerBlockKeys))
+ copy(zones, c.ServerBlockKeys)
+ args := c.RemainingArgs()
+ if len(args) > 0 {
+ zones = args
+ }
+
+ for c.NextBlock() {
+ switch c.Val() {
+ case "key":
+ k, e := keyParse(c)
+ if e != nil {
+ return nil, nil, 0, e
+ }
+ keys = append(keys, k...)
+ case "cache_capacity":
+ if !c.NextArg() {
+ return nil, nil, 0, c.ArgErr()
+ }
+ value := c.Val()
+ cacheCap, err := strconv.Atoi(value)
+ if err != nil {
+ return nil, nil, 0, err
+ }
+ capacity = cacheCap
+ }
+
+ }
+ }
+ for i := range zones {
+ zones[i] = plugin.Host(zones[i]).Normalize()
+ }
+
+ // 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, fmt.Errorf("key %s (keyid: %d) can not sign any of the zones", string(kname), k.keytag)
+ }
+ }
+
+ return zones, keys, capacity, nil
+}
+
+func keyParse(c *caddy.Controller) ([]*DNSKEY, error) {
+ keys := []*DNSKEY{}
+
+ 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]
+ }
+ k, err := ParseKeyFile(base+".key", base+".private")
+ if err != nil {
+ return nil, err
+ }
+ keys = append(keys, k)
+ }
+ }
+ return keys, nil
+}
diff --git a/plugin/dnssec/setup_test.go b/plugin/dnssec/setup_test.go
new file mode 100644
index 000000000..99a71279d
--- /dev/null
+++ b/plugin/dnssec/setup_test.go
@@ -0,0 +1,120 @@
+package dnssec
+
+import (
+ "io/ioutil"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestSetupDnssec(t *testing.T) {
+ if err := ioutil.WriteFile("Kcluster.local.key", []byte(keypub), 0644); err != nil {
+ t.Fatalf("Failed to write pub key file: %s", err)
+ }
+ defer func() { os.Remove("Kcluster.local.key") }()
+ if err := ioutil.WriteFile("Kcluster.local.private", []byte(keypriv), 0644); err != nil {
+ t.Fatalf("Failed to write private key file: %s", err)
+ }
+ defer func() { os.Remove("Kcluster.local.private") }()
+
+ tests := []struct {
+ input string
+ shouldErr bool
+ expectedZones []string
+ expectedKeys []string
+ expectedCapacity int
+ expectedErrContent string
+ }{
+ {`dnssec`, false, nil, nil, defaultCap, ""},
+ {`dnssec example.org`, false, []string{"example.org."}, nil, defaultCap, ""},
+ {`dnssec 10.0.0.0/8`, false, []string{"10.in-addr.arpa."}, nil, defaultCap, ""},
+ {
+ `dnssec example.org {
+ cache_capacity 100
+ }`, false, []string{"example.org."}, nil, 100, "",
+ },
+ {
+ `dnssec cluster.local {
+ key file Kcluster.local
+ }`, false, []string{"cluster.local."}, nil, defaultCap, "",
+ },
+ {
+ `dnssec example.org cluster.local {
+ key file Kcluster.local
+ }`, false, []string{"example.org.", "cluster.local."}, nil, defaultCap, "",
+ },
+ // fails
+ {
+ `dnssec example.org {
+ key file Kcluster.local
+ }`, true, []string{"example.org."}, nil, defaultCap, "can not sign any",
+ },
+ {
+ `dnssec example.org {
+ key
+ }`, true, []string{"example.org."}, nil, defaultCap, "argument count",
+ },
+ {
+ `dnssec example.org {
+ key file
+ }`, true, []string{"example.org."}, nil, defaultCap, "argument count",
+ },
+ }
+
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.input)
+ zones, keys, capacity, 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 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
+`
diff --git a/plugin/dnstap/README.md b/plugin/dnstap/README.md
new file mode 100644
index 000000000..d91c9422c
--- /dev/null
+++ b/plugin/dnstap/README.md
@@ -0,0 +1,61 @@
+# dnstap
+
+*dnstap* enables logging to dnstap, a flexible, structured binary log format for DNS software: http://dnstap.info.
+
+There is a buffer, expect at least 13 requests before the server sends its dnstap messages to the socket.
+
+## Syntax
+
+~~~ txt
+dnstap SOCKET [full]
+~~~
+
+* **SOCKET** is the socket path supplied to the dnstap command line tool.
+* `full` to include the wire-format DNS message.
+
+## 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
+~~~
+
+## Dnstap command line tool
+
+~~~ sh
+go get github.com/dnstap/golang-dnstap
+cd $GOPATH/src/github.com/dnstap/golang-dnstap/dnstap
+go build
+./dnstap
+~~~
+
+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
+~~~
diff --git a/plugin/dnstap/handler.go b/plugin/dnstap/handler.go
new file mode 100644
index 000000000..b20bb2ad9
--- /dev/null
+++ b/plugin/dnstap/handler.go
@@ -0,0 +1,79 @@
+package dnstap
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/dnstap/msg"
+ "github.com/coredns/coredns/plugin/dnstap/taprw"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Dnstap is the dnstap handler.
+type Dnstap struct {
+ Next plugin.Handler
+ Out io.Writer
+ Pack bool
+}
+
+type (
+ // Tapper is implemented by the Context passed by the dnstap handler.
+ Tapper interface {
+ TapMessage(*tap.Message) error
+ TapBuilder() msg.Builder
+ }
+ tapContext struct {
+ context.Context
+ Dnstap
+ }
+)
+
+// TapperFromContext will return a Tapper if the dnstap plugin is enabled.
+func TapperFromContext(ctx context.Context) (t Tapper) {
+ t, _ = ctx.(Tapper)
+ return
+}
+
+func tapMessageTo(w io.Writer, m *tap.Message) error {
+ frame, err := msg.Marshal(m)
+ if err != nil {
+ return fmt.Errorf("marshal: %s", err)
+ }
+ _, err = w.Write(frame)
+ return err
+}
+
+// TapMessage implements Tapper.
+func (h Dnstap) TapMessage(m *tap.Message) error {
+ return tapMessageTo(h.Out, m)
+}
+
+// TapBuilder implements Tapper.
+func (h Dnstap) TapBuilder() msg.Builder {
+ return msg.Builder{Full: h.Pack}
+}
+
+// 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 := &taprw.ResponseWriter{ResponseWriter: w, Tapper: &h, Query: r}
+ rw.QueryEpoch()
+
+ code, err := plugin.NextOrFailure(h.Name(), h.Next, tapContext{ctx, h}, rw, r)
+ if err != nil {
+ // ignore dnstap errors
+ return code, err
+ }
+
+ if err := rw.DnstapError(); err != nil {
+ return code, plugin.Error("dnstap", err)
+ }
+
+ return code, nil
+}
+
+// Name returns dnstap.
+func (h Dnstap) Name() string { return "dnstap" }
diff --git a/plugin/dnstap/handler_test.go b/plugin/dnstap/handler_test.go
new file mode 100644
index 000000000..54509de82
--- /dev/null
+++ b/plugin/dnstap/handler_test.go
@@ -0,0 +1,65 @@
+package dnstap
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/dnstap/test"
+ mwtest "github.com/coredns/coredns/plugin/test"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/golang/protobuf/proto"
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func testCase(t *testing.T, tapq, tapr *tap.Message, q, r *dns.Msg) {
+ w := writer{}
+ w.queue = append(w.queue, tapq, tapr)
+ h := Dnstap{
+ Next: mwtest.HandlerFunc(func(_ context.Context,
+ w dns.ResponseWriter, _ *dns.Msg) (int, error) {
+
+ return 0, w.WriteMsg(r)
+ }),
+ Out: &w,
+ Pack: false,
+ }
+ _, err := h.ServeDNS(context.TODO(), &mwtest.ResponseWriter{}, q)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+type writer struct {
+ queue []*tap.Message
+}
+
+func (w *writer) Write(b []byte) (int, error) {
+ e := tap.Dnstap{}
+ if err := proto.Unmarshal(b, &e); err != nil {
+ return 0, err
+ }
+ if len(w.queue) == 0 {
+ return 0, errors.New("message not expected")
+ }
+ if !test.MsgEqual(w.queue[0], e.Message) {
+ return 0, fmt.Errorf("want: %v, have: %v", w.queue[0], e.Message)
+ }
+ w.queue = w.queue[1:]
+ return len(b), nil
+}
+
+func TestDnstap(t *testing.T) {
+ q := mwtest.Case{Qname: "example.org", Qtype: dns.TypeA}.Msg()
+ r := mwtest.Case{
+ Qname: "example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ mwtest.A("example.org. 3600 IN A 10.0.0.1"),
+ },
+ }.Msg()
+ tapq := test.TestingData().ToClientQuery()
+ tapr := test.TestingData().ToClientResponse()
+ testCase(t, tapq, tapr, q, r)
+}
diff --git a/plugin/dnstap/msg/msg.go b/plugin/dnstap/msg/msg.go
new file mode 100644
index 000000000..07930929e
--- /dev/null
+++ b/plugin/dnstap/msg/msg.go
@@ -0,0 +1,168 @@
+// Package msg helps to build a dnstap Message.
+package msg
+
+import (
+ "errors"
+ "net"
+ "strconv"
+ "time"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+)
+
+// Builder helps to build Data by being aware of the dnstap plugin configuration.
+type Builder struct {
+ Full bool
+ Data
+}
+
+// AddrMsg parses the info of net.Addr and dns.Msg.
+func (b *Builder) AddrMsg(a net.Addr, m *dns.Msg) (err error) {
+ err = b.RemoteAddr(a)
+ if err != nil {
+ return
+ }
+ return b.Msg(m)
+}
+
+// Msg parses the info of dns.Msg.
+func (b *Builder) Msg(m *dns.Msg) (err error) {
+ if b.Full {
+ err = b.Pack(m)
+ }
+ return
+}
+
+// Data helps to build a dnstap Message.
+// It can be transformed into the actual Message using this package.
+type Data struct {
+ Packed []byte
+ SocketProto tap.SocketProtocol
+ SocketFam tap.SocketFamily
+ Address []byte
+ Port uint32
+ TimeSec uint64
+}
+
+// HostPort decodes into Data any string returned by dnsutil.ParseHostPortOrFile.
+func (d *Data) HostPort(addr string) error {
+ ip, port, err := net.SplitHostPort(addr)
+ if err != nil {
+ return err
+ }
+ p, err := strconv.ParseUint(port, 10, 32)
+ if err != nil {
+ return err
+ }
+ d.Port = uint32(p)
+
+ if ip := net.ParseIP(ip); ip != nil {
+ d.Address = []byte(ip)
+ if ip := ip.To4(); ip != nil {
+ d.SocketFam = tap.SocketFamily_INET
+ } else {
+ d.SocketFam = tap.SocketFamily_INET6
+ }
+ return nil
+ }
+ return errors.New("not an ip address")
+}
+
+// RemoteAddr parses the information about the remote address into Data.
+func (d *Data) RemoteAddr(remote net.Addr) error {
+ switch addr := remote.(type) {
+ case *net.TCPAddr:
+ d.Address = addr.IP
+ d.Port = uint32(addr.Port)
+ d.SocketProto = tap.SocketProtocol_TCP
+ case *net.UDPAddr:
+ d.Address = addr.IP
+ d.Port = uint32(addr.Port)
+ d.SocketProto = tap.SocketProtocol_UDP
+ default:
+ return errors.New("unknown remote address type")
+ }
+
+ if a := net.IP(d.Address); a.To4() != nil {
+ d.SocketFam = tap.SocketFamily_INET
+ } else {
+ d.SocketFam = tap.SocketFamily_INET6
+ }
+
+ return nil
+}
+
+// Pack encodes the DNS message into Data.
+func (d *Data) Pack(m *dns.Msg) error {
+ packed, err := m.Pack()
+ if err != nil {
+ return err
+ }
+ d.Packed = packed
+ return nil
+}
+
+// Epoch returns the epoch time in seconds.
+func Epoch() uint64 {
+ return uint64(time.Now().Unix())
+}
+
+// Epoch sets the dnstap message epoch.
+func (d *Data) Epoch() {
+ d.TimeSec = Epoch()
+}
+
+// ToClientResponse transforms Data into a client response message.
+func (d *Data) ToClientResponse() *tap.Message {
+ t := tap.Message_CLIENT_RESPONSE
+ return &tap.Message{
+ Type: &t,
+ SocketFamily: &d.SocketFam,
+ SocketProtocol: &d.SocketProto,
+ ResponseTimeSec: &d.TimeSec,
+ ResponseMessage: d.Packed,
+ QueryAddress: d.Address,
+ QueryPort: &d.Port,
+ }
+}
+
+// ToClientQuery transforms Data into a client query message.
+func (d *Data) ToClientQuery() *tap.Message {
+ t := tap.Message_CLIENT_QUERY
+ return &tap.Message{
+ Type: &t,
+ SocketFamily: &d.SocketFam,
+ SocketProtocol: &d.SocketProto,
+ QueryTimeSec: &d.TimeSec,
+ QueryMessage: d.Packed,
+ QueryAddress: d.Address,
+ QueryPort: &d.Port,
+ }
+}
+
+// ToOutsideQuery transforms the data into a forwarder or resolver query message.
+func (d *Data) ToOutsideQuery(t tap.Message_Type) *tap.Message {
+ return &tap.Message{
+ Type: &t,
+ SocketFamily: &d.SocketFam,
+ SocketProtocol: &d.SocketProto,
+ QueryTimeSec: &d.TimeSec,
+ QueryMessage: d.Packed,
+ ResponseAddress: d.Address,
+ ResponsePort: &d.Port,
+ }
+}
+
+// ToOutsideResponse transforms the data into a forwarder or resolver response message.
+func (d *Data) ToOutsideResponse(t tap.Message_Type) *tap.Message {
+ return &tap.Message{
+ Type: &t,
+ SocketFamily: &d.SocketFam,
+ SocketProtocol: &d.SocketProto,
+ ResponseTimeSec: &d.TimeSec,
+ ResponseMessage: d.Packed,
+ ResponseAddress: d.Address,
+ ResponsePort: &d.Port,
+ }
+}
diff --git a/plugin/dnstap/msg/msg_test.go b/plugin/dnstap/msg/msg_test.go
new file mode 100644
index 000000000..649659a80
--- /dev/null
+++ b/plugin/dnstap/msg/msg_test.go
@@ -0,0 +1,42 @@
+package msg
+
+import (
+ "net"
+ "reflect"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/test"
+ "github.com/coredns/coredns/request"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+)
+
+func testRequest(t *testing.T, expected Data, r request.Request) {
+ d := Data{}
+ if err := d.RemoteAddr(r.W.RemoteAddr()); err != nil {
+ t.Fail()
+ return
+ }
+ if d.SocketProto != expected.SocketProto ||
+ d.SocketFam != expected.SocketFam ||
+ !reflect.DeepEqual(d.Address, expected.Address) ||
+ d.Port != expected.Port {
+ t.Fatalf("expected: %v, have: %v", expected, d)
+ return
+ }
+}
+func TestRequest(t *testing.T) {
+ testRequest(t, Data{
+ SocketProto: tap.SocketProtocol_UDP,
+ SocketFam: tap.SocketFamily_INET,
+ Address: net.ParseIP("10.240.0.1"),
+ Port: 40212,
+ }, testingRequest())
+}
+func testingRequest() 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/plugin/dnstap/msg/wrapper.go b/plugin/dnstap/msg/wrapper.go
new file mode 100644
index 000000000..a74c604d8
--- /dev/null
+++ b/plugin/dnstap/msg/wrapper.go
@@ -0,0 +1,26 @@
+package msg
+
+import (
+ "fmt"
+
+ lib "github.com/dnstap/golang-dnstap"
+ "github.com/golang/protobuf/proto"
+)
+
+func wrap(m *lib.Message) *lib.Dnstap {
+ t := lib.Dnstap_MESSAGE
+ return &lib.Dnstap{
+ Type: &t,
+ Message: m,
+ }
+}
+
+// Marshal encodes the message to a binary dnstap payload.
+func Marshal(m *lib.Message) (data []byte, err error) {
+ data, err = proto.Marshal(wrap(m))
+ if err != nil {
+ err = fmt.Errorf("proto: %s", err)
+ return
+ }
+ return
+}
diff --git a/plugin/dnstap/out/socket.go b/plugin/dnstap/out/socket.go
new file mode 100644
index 000000000..520dcf1d8
--- /dev/null
+++ b/plugin/dnstap/out/socket.go
@@ -0,0 +1,86 @@
+package out
+
+import (
+ "fmt"
+ "net"
+
+ fs "github.com/farsightsec/golang-framestream"
+)
+
+// Socket is a Frame Streams encoder over a UNIX socket.
+type Socket struct {
+ path string
+ enc *fs.Encoder
+ conn net.Conn
+ err error
+}
+
+func openSocket(s *Socket) error {
+ conn, err := net.Dial("unix", s.path)
+ if err != nil {
+ return err
+ }
+ s.conn = conn
+
+ enc, err := fs.NewEncoder(conn, &fs.EncoderOptions{
+ ContentType: []byte("protobuf:dnstap.Dnstap"),
+ Bidirectional: true,
+ })
+ if err != nil {
+ return err
+ }
+ s.enc = enc
+
+ s.err = nil
+ return nil
+}
+
+// NewSocket will always return a new Socket.
+// err if nothing is listening to it, it will attempt to reconnect on the next Write.
+func NewSocket(path string) (s *Socket, err error) {
+ s = &Socket{path: path}
+ if err = openSocket(s); err != nil {
+ err = fmt.Errorf("open socket: %s", err)
+ s.err = err
+ return
+ }
+ return
+}
+
+// Write a single Frame Streams frame.
+func (s *Socket) Write(frame []byte) (int, error) {
+ if s.err != nil {
+ // is the dnstap tool listening?
+ if err := openSocket(s); err != nil {
+ return 0, fmt.Errorf("open socket: %s", err)
+ }
+ }
+ n, err := s.enc.Write(frame)
+ if err != nil {
+ // the dnstap command line tool is down
+ s.conn.Close()
+ s.err = err
+ return 0, err
+ }
+ return n, nil
+
+}
+
+// Close the socket and flush the remaining frames.
+func (s *Socket) Close() error {
+ if s.err != nil {
+ // nothing to close
+ return nil
+ }
+
+ defer s.conn.Close()
+
+ if err := s.enc.Flush(); err != nil {
+ return fmt.Errorf("flush: %s", err)
+ }
+ if err := s.enc.Close(); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/plugin/dnstap/out/socket_test.go b/plugin/dnstap/out/socket_test.go
new file mode 100644
index 000000000..050a38d36
--- /dev/null
+++ b/plugin/dnstap/out/socket_test.go
@@ -0,0 +1,94 @@
+package out
+
+import (
+ "net"
+ "testing"
+
+ fs "github.com/farsightsec/golang-framestream"
+)
+
+func acceptOne(t *testing.T, l net.Listener) {
+ server, err := l.Accept()
+ if err != nil {
+ t.Fatalf("server accept: %s", err)
+ return
+ }
+
+ dec, err := fs.NewDecoder(server, &fs.DecoderOptions{
+ ContentType: []byte("protobuf:dnstap.Dnstap"),
+ Bidirectional: true,
+ })
+ if err != nil {
+ t.Fatalf("server decoder: %s", err)
+ return
+ }
+
+ if _, err := dec.Decode(); err != nil {
+ t.Errorf("server decode: %s", err)
+ }
+
+ if err := server.Close(); err != nil {
+ t.Error(err)
+ }
+}
+func sendOne(socket *Socket) error {
+ if _, err := socket.Write([]byte("frame")); err != nil {
+ return err
+ }
+ if err := socket.enc.Flush(); err != nil {
+ // Would happen during Write in real life.
+ socket.conn.Close()
+ socket.err = err
+ return err
+ }
+ return nil
+}
+func TestSocket(t *testing.T) {
+ socket, err := NewSocket("dnstap.sock")
+ if err == nil {
+ t.Fatal("new socket: not listening but no error")
+ return
+ }
+
+ if err := sendOne(socket); err == nil {
+ t.Fatal("not listening but no error")
+ return
+ }
+
+ l, err := net.Listen("unix", "dnstap.sock")
+ if err != nil {
+ t.Fatal(err)
+ return
+ }
+
+ wait := make(chan bool)
+ go func() {
+ acceptOne(t, l)
+ wait <- true
+ }()
+
+ if err := sendOne(socket); err != nil {
+ t.Fatalf("send one: %s", err)
+ return
+ }
+
+ <-wait
+ if err := sendOne(socket); err == nil {
+ panic("must fail")
+ }
+
+ go func() {
+ acceptOne(t, l)
+ wait <- true
+ }()
+
+ if err := sendOne(socket); err != nil {
+ t.Fatalf("send one: %s", err)
+ return
+ }
+
+ <-wait
+ if err := l.Close(); err != nil {
+ t.Error(err)
+ }
+}
diff --git a/plugin/dnstap/out/tcp.go b/plugin/dnstap/out/tcp.go
new file mode 100644
index 000000000..8d2c25270
--- /dev/null
+++ b/plugin/dnstap/out/tcp.go
@@ -0,0 +1,59 @@
+package out
+
+import (
+ "net"
+ "time"
+
+ fs "github.com/farsightsec/golang-framestream"
+)
+
+// TCP is a Frame Streams encoder over TCP.
+type TCP struct {
+ address string
+ frames [][]byte
+}
+
+// NewTCP returns a TCP writer.
+func NewTCP(address string) *TCP {
+ s := &TCP{address: address}
+ s.frames = make([][]byte, 0, 13) // 13 messages buffer
+ return s
+}
+
+// Write a single Frame Streams frame.
+func (s *TCP) Write(frame []byte) (n int, err error) {
+ s.frames = append(s.frames, frame)
+ if len(s.frames) == cap(s.frames) {
+ return len(frame), s.Flush()
+ }
+ return len(frame), nil
+}
+
+// Flush the remaining frames.
+func (s *TCP) Flush() error {
+ defer func() {
+ s.frames = s.frames[0:]
+ }()
+ c, err := net.DialTimeout("tcp", s.address, time.Second)
+ if err != nil {
+ return err
+ }
+ enc, err := fs.NewEncoder(c, &fs.EncoderOptions{
+ ContentType: []byte("protobuf:dnstap.Dnstap"),
+ Bidirectional: true,
+ })
+ if err != nil {
+ return err
+ }
+ for _, frame := range s.frames {
+ if _, err = enc.Write(frame); err != nil {
+ return err
+ }
+ }
+ return enc.Flush()
+}
+
+// Close is an alias to Flush to satisfy io.WriteCloser similarly to type Socket.
+func (s *TCP) Close() error {
+ return s.Flush()
+}
diff --git a/plugin/dnstap/out/tcp_test.go b/plugin/dnstap/out/tcp_test.go
new file mode 100644
index 000000000..113603cd4
--- /dev/null
+++ b/plugin/dnstap/out/tcp_test.go
@@ -0,0 +1,66 @@
+package out
+
+import (
+ "net"
+ "testing"
+)
+
+func sendOneTCP(tcp *TCP) error {
+ if _, err := tcp.Write([]byte("frame")); err != nil {
+ return err
+ }
+ if err := tcp.Flush(); err != nil {
+ return err
+ }
+ return nil
+}
+func TestTCP(t *testing.T) {
+ tcp := NewTCP("localhost:14000")
+
+ if err := sendOneTCP(tcp); err == nil {
+ t.Fatal("Not listening but no error.")
+ return
+ }
+
+ l, err := net.Listen("tcp", "localhost:14000")
+ if err != nil {
+ t.Fatal(err)
+ return
+ }
+
+ wait := make(chan bool)
+ go func() {
+ acceptOne(t, l)
+ wait <- true
+ }()
+
+ if err := sendOneTCP(tcp); err != nil {
+ t.Fatalf("send one: %s", err)
+ return
+ }
+
+ <-wait
+
+ // TODO: When the server isn't responding according to the framestream protocol
+ // the thread is blocked.
+ /*
+ if err := sendOneTCP(tcp); err == nil {
+ panic("must fail")
+ }
+ */
+
+ go func() {
+ acceptOne(t, l)
+ wait <- true
+ }()
+
+ if err := sendOneTCP(tcp); err != nil {
+ t.Fatalf("send one: %s", err)
+ return
+ }
+
+ <-wait
+ if err := l.Close(); err != nil {
+ t.Error(err)
+ }
+}
diff --git a/plugin/dnstap/setup.go b/plugin/dnstap/setup.go
new file mode 100644
index 000000000..a57873470
--- /dev/null
+++ b/plugin/dnstap/setup.go
@@ -0,0 +1,98 @@
+package dnstap
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "strings"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/dnstap/out"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+
+ "github.com/mholt/caddy"
+ "github.com/mholt/caddy/caddyfile"
+)
+
+func init() {
+ caddy.RegisterPlugin("dnstap", caddy.Plugin{
+ ServerType: "dns",
+ Action: wrapSetup,
+ })
+}
+
+func wrapSetup(c *caddy.Controller) error {
+ if err := setup(c); err != nil {
+ return plugin.Error("dnstap", err)
+ }
+ return nil
+}
+
+type config struct {
+ target string
+ socket bool
+ full bool
+}
+
+func parseConfig(d *caddyfile.Dispenser) (c config, err error) {
+ d.Next() // directive name
+
+ if !d.Args(&c.target) {
+ return c, d.ArgErr()
+ }
+
+ if strings.HasPrefix(c.target, "tcp://") {
+ // remote IP endpoint
+ servers, err := dnsutil.ParseHostPortOrFile(c.target[6:])
+ if err != nil {
+ return c, d.ArgErr()
+ }
+ c.target = servers[0]
+ } else {
+ // default to UNIX socket
+ if strings.HasPrefix(c.target, "unix://") {
+ c.target = c.target[7:]
+ }
+ c.socket = true
+ }
+
+ c.full = d.NextArg() && d.Val() == "full"
+
+ return
+}
+
+func setup(c *caddy.Controller) error {
+ conf, err := parseConfig(&c.Dispenser)
+ if err != nil {
+ return err
+ }
+
+ dnstap := Dnstap{Pack: conf.full}
+
+ var o io.WriteCloser
+ if conf.socket {
+ o, err = out.NewSocket(conf.target)
+ if err != nil {
+ log.Printf("[WARN] Can't connect to %s at the moment: %s", conf.target, err)
+ }
+ } else {
+ o = out.NewTCP(conf.target)
+ }
+ dnstap.Out = o
+
+ c.OnShutdown(func() error {
+ if err := o.Close(); err != nil {
+ return fmt.Errorf("output: %s", err)
+ }
+ return nil
+ })
+
+ dnsserver.GetConfig(c).AddPlugin(
+ func(next plugin.Handler) plugin.Handler {
+ dnstap.Next = next
+ return dnstap
+ })
+
+ return nil
+}
diff --git a/plugin/dnstap/setup_test.go b/plugin/dnstap/setup_test.go
new file mode 100644
index 000000000..fc1dc98e0
--- /dev/null
+++ b/plugin/dnstap/setup_test.go
@@ -0,0 +1,34 @@
+package dnstap
+
+import (
+ "github.com/mholt/caddy"
+ "testing"
+)
+
+func TestConfig(t *testing.T) {
+ tests := []struct {
+ file string
+ path string
+ full bool
+ socket bool
+ fail bool
+ }{
+ {"dnstap dnstap.sock full", "dnstap.sock", true, true, false},
+ {"dnstap unix://dnstap.sock", "dnstap.sock", false, true, false},
+ {"dnstap tcp://127.0.0.1:6000", "127.0.0.1:6000", false, false, false},
+ {"dnstap", "fail", false, true, true},
+ }
+ for _, c := range tests {
+ cad := caddy.NewTestController("dns", c.file)
+ conf, err := parseConfig(&cad.Dispenser)
+ if c.fail {
+ if err == nil {
+ t.Errorf("%s: %s", c.file, err)
+ }
+ } else if err != nil || conf.target != c.path ||
+ conf.full != c.full || conf.socket != c.socket {
+
+ t.Errorf("expected: %+v\nhave: %+v\nerror: %s\n", c, conf, err)
+ }
+ }
+}
diff --git a/plugin/dnstap/taprw/writer.go b/plugin/dnstap/taprw/writer.go
new file mode 100644
index 000000000..ae9965411
--- /dev/null
+++ b/plugin/dnstap/taprw/writer.go
@@ -0,0 +1,73 @@
+// Package taprw takes a query and intercepts the response.
+// It will log both after the response is written.
+package taprw
+
+import (
+ "fmt"
+
+ "github.com/coredns/coredns/plugin/dnstap/msg"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+)
+
+// Tapper is what ResponseWriter needs to log to dnstap.
+type Tapper interface {
+ TapMessage(m *tap.Message) error
+ TapBuilder() msg.Builder
+}
+
+// ResponseWriter captures the client response and logs the query to dnstap.
+// Single request use.
+type ResponseWriter struct {
+ queryEpoch uint64
+ Query *dns.Msg
+ dns.ResponseWriter
+ Tapper
+ err error
+}
+
+// DnstapError check if a dnstap error occurred during Write and returns it.
+func (w ResponseWriter) DnstapError() error {
+ return w.err
+}
+
+// QueryEpoch sets the query epoch as reported by dnstap.
+func (w *ResponseWriter) QueryEpoch() {
+ w.queryEpoch = msg.Epoch()
+}
+
+// WriteMsg writes back the response to the client and THEN works on logging the request
+// and response to dnstap.
+// Dnstap errors are to be checked by DnstapError.
+func (w *ResponseWriter) WriteMsg(resp *dns.Msg) (writeErr error) {
+ writeErr = w.ResponseWriter.WriteMsg(resp)
+ writeEpoch := msg.Epoch()
+
+ b := w.TapBuilder()
+ b.TimeSec = w.queryEpoch
+ if err := func() (err error) {
+ err = b.AddrMsg(w.ResponseWriter.RemoteAddr(), w.Query)
+ if err != nil {
+ return
+ }
+ return w.TapMessage(b.ToClientQuery())
+ }(); err != nil {
+ w.err = fmt.Errorf("client query: %s", err)
+ // don't forget to call DnstapError later
+ }
+
+ if writeErr == nil {
+ if err := func() (err error) {
+ b.TimeSec = writeEpoch
+ if err = b.Msg(resp); err != nil {
+ return
+ }
+ return w.TapMessage(b.ToClientResponse())
+ }(); err != nil {
+ w.err = fmt.Errorf("client response: %s", err)
+ }
+ }
+
+ return
+}
diff --git a/plugin/dnstap/taprw/writer_test.go b/plugin/dnstap/taprw/writer_test.go
new file mode 100644
index 000000000..6969dc515
--- /dev/null
+++ b/plugin/dnstap/taprw/writer_test.go
@@ -0,0 +1,82 @@
+package taprw
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/dnstap/msg"
+ "github.com/coredns/coredns/plugin/dnstap/test"
+ mwtest "github.com/coredns/coredns/plugin/test"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+)
+
+type TapFailer struct {
+}
+
+func (TapFailer) TapMessage(*tap.Message) error {
+ return errors.New("failed")
+}
+func (TapFailer) TapBuilder() msg.Builder {
+ return msg.Builder{Full: true}
+}
+
+func TestDnstapError(t *testing.T) {
+ rw := ResponseWriter{
+ Query: new(dns.Msg),
+ ResponseWriter: &mwtest.ResponseWriter{},
+ Tapper: TapFailer{},
+ }
+ if err := rw.WriteMsg(new(dns.Msg)); err != nil {
+ t.Errorf("dnstap error during Write: %s", err)
+ }
+ if rw.DnstapError() == nil {
+ t.Fatal("no dnstap error")
+ }
+}
+
+func testingMsg() (m *dns.Msg) {
+ m = new(dns.Msg)
+ m.SetQuestion("example.com.", dns.TypeA)
+ m.SetEdns0(4097, true)
+ return
+}
+
+func TestClientQueryResponse(t *testing.T) {
+ trapper := test.TrapTapper{Full: true}
+ m := testingMsg()
+ rw := ResponseWriter{
+ Query: m,
+ Tapper: &trapper,
+ ResponseWriter: &mwtest.ResponseWriter{},
+ }
+ d := test.TestingData()
+
+ // will the wire-format msg be reported?
+ bin, err := m.Pack()
+ if err != nil {
+ t.Fatal(err)
+ return
+ }
+ d.Packed = bin
+
+ if err := rw.WriteMsg(m); err != nil {
+ t.Fatal(err)
+ return
+ }
+ if l := len(trapper.Trap); l != 2 {
+ t.Fatalf("%d msg trapped", l)
+ return
+ }
+ want := d.ToClientQuery()
+ have := trapper.Trap[0]
+ if !test.MsgEqual(want, have) {
+ t.Fatalf("query: want: %v\nhave: %v", want, have)
+ }
+ want = d.ToClientResponse()
+ have = trapper.Trap[1]
+ if !test.MsgEqual(want, have) {
+ t.Fatalf("response: want: %v\nhave: %v", want, have)
+ }
+}
diff --git a/plugin/dnstap/test/helpers.go b/plugin/dnstap/test/helpers.go
new file mode 100644
index 000000000..8c5809725
--- /dev/null
+++ b/plugin/dnstap/test/helpers.go
@@ -0,0 +1,80 @@
+package test
+
+import (
+ "net"
+ "reflect"
+
+ "github.com/coredns/coredns/plugin/dnstap/msg"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "golang.org/x/net/context"
+)
+
+// Context is a message trap.
+type Context struct {
+ context.Context
+ TrapTapper
+}
+
+// TestingData returns the Data matching coredns/test.ResponseWriter.
+func TestingData() (d *msg.Data) {
+ d = &msg.Data{
+ SocketFam: tap.SocketFamily_INET,
+ SocketProto: tap.SocketProtocol_UDP,
+ Address: net.ParseIP("10.240.0.1"),
+ Port: 40212,
+ }
+ return
+}
+
+type comp struct {
+ Type *tap.Message_Type
+ SF *tap.SocketFamily
+ SP *tap.SocketProtocol
+ QA []byte
+ RA []byte
+ QP *uint32
+ RP *uint32
+ QTSec bool
+ RTSec bool
+ RM []byte
+ QM []byte
+}
+
+func toComp(m *tap.Message) comp {
+ return comp{
+ Type: m.Type,
+ SF: m.SocketFamily,
+ SP: m.SocketProtocol,
+ QA: m.QueryAddress,
+ RA: m.ResponseAddress,
+ QP: m.QueryPort,
+ RP: m.ResponsePort,
+ QTSec: m.QueryTimeSec != nil,
+ RTSec: m.ResponseTimeSec != nil,
+ RM: m.ResponseMessage,
+ QM: m.QueryMessage,
+ }
+}
+
+// MsgEqual compares two dnstap messages ignoring timestamps.
+func MsgEqual(a, b *tap.Message) bool {
+ return reflect.DeepEqual(toComp(a), toComp(b))
+}
+
+// TrapTapper traps messages.
+type TrapTapper struct {
+ Trap []*tap.Message
+ Full bool
+}
+
+// TapMessage adds the message to the trap.
+func (t *TrapTapper) TapMessage(m *tap.Message) error {
+ t.Trap = append(t.Trap, m)
+ return nil
+}
+
+// TapBuilder returns a test msg.Builder.
+func (t *TrapTapper) TapBuilder() msg.Builder {
+ return msg.Builder{Full: t.Full}
+}
diff --git a/plugin/erratic/README.md b/plugin/erratic/README.md
new file mode 100644
index 000000000..a41faaca9
--- /dev/null
+++ b/plugin/erratic/README.md
@@ -0,0 +1,76 @@
+# erratic
+
+*erratic* is a plugin useful for testing client behavior. It 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. The reply for A will return 192.0.2.53 (see RFC 5737), for AAAA it returns
+2001:DB8::53 (see RFC 3849).
+
+*erratic* can also be used in conjunction with the *autopath* plugin. This is mostly to aid in
+ testing.
+
+## 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.
+
+## Examples
+
+~~~ txt
+.:53 {
+ erratic {
+ drop 3
+ }
+}
+~~~
+
+Or even shorter if the defaults suits you. Note this only drops queries, it does not delay them.
+
+~~~ txt
+. {
+ erratic
+}
+~~~
+
+Delay 1 in 3 queries for 50ms
+
+~~~ txt
+. {
+ erratic {
+ delay 3 50ms
+ }
+}
+~~~
+
+Delay 1 in 3 and truncate 1 in 5.
+
+~~~ txt
+. {
+ erratic {
+ delay 3 5ms
+ truncate 5
+ }
+}
+~~~
+
+Drop every second query.
+
+~~~ txt
+. {
+ erratic {
+ drop 2
+ truncate 2
+ }
+}
+~~~
diff --git a/plugin/erratic/autopath.go b/plugin/erratic/autopath.go
new file mode 100644
index 000000000..0e29fffe5
--- /dev/null
+++ b/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/plugin/erratic/erratic.go b/plugin/erratic/erratic.go
new file mode 100644
index 000000000..5b8cd30c9
--- /dev/null
+++ b/plugin/erratic/erratic.go
@@ -0,0 +1,95 @@
+// Package erratic implements a plugin that returns erratic answers (delayed, dropped).
+package erratic
+
+import (
+ "sync/atomic"
+ "time"
+
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Erratic is a plugin that returns erratic repsonses to each client.
+type Erratic struct {
+ drop uint64
+
+ delay uint64
+ duration time.Duration
+
+ truncate uint64
+
+ q uint64 // counter of queries
+}
+
+// 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.Compress = true
+ 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)
+ case dns.TypeAAAA:
+ rr := *(rrAAAA.(*dns.AAAA))
+ rr.Header().Name = state.QName()
+ m.Answer = append(m.Answer, &rr)
+ default:
+ if !drop {
+ 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)
+ }
+
+ state.SizeAndDo(m)
+ 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/plugin/erratic/erratic_test.go b/plugin/erratic/erratic_test.go
new file mode 100644
index 000000000..7a1a420da
--- /dev/null
+++ b/plugin/erratic/erratic_test.go
@@ -0,0 +1,79 @@
+package erratic
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestErraticDrop(t *testing.T) {
+ e := &Erratic{drop: 2} // 50% drops
+
+ tests := []struct {
+ expectedCode int
+ expectedErr error
+ drop bool
+ }{
+ {expectedCode: dns.RcodeSuccess, expectedErr: nil, drop: true},
+ {expectedCode: dns.RcodeSuccess, expectedErr: nil, drop: false},
+ }
+
+ ctx := context.TODO()
+
+ for i, tc := range tests {
+ req := new(dns.Msg)
+ req.SetQuestion("example.org.", dns.TypeA)
+
+ rec := dnsrecorder.New(&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 := dnsrecorder.New(&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)
+ }
+ }
+}
diff --git a/plugin/erratic/setup.go b/plugin/erratic/setup.go
new file mode 100644
index 000000000..b0a56927d
--- /dev/null
+++ b/plugin/erratic/setup.go
@@ -0,0 +1,117 @@
+package erratic
+
+import (
+ "fmt"
+ "strconv"
+ "time"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("erratic", caddy.Plugin{
+ ServerType: "dns",
+ Action: setupErratic,
+ })
+}
+
+func setupErratic(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)
+ 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/plugin/erratic/setup_test.go b/plugin/erratic/setup_test.go
new file mode 100644
index 000000000..759845f7a
--- /dev/null
+++ b/plugin/erratic/setup_test.go
@@ -0,0 +1,103 @@
+package erratic
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestSetupErratic(t *testing.T) {
+ c := caddy.NewTestController("dns", `erratic {
+ drop
+ }`)
+ if err := setupErratic(c); err != nil {
+ t.Fatalf("Test 1, expected no errors, but got: %q", err)
+ }
+
+ c = caddy.NewTestController("dns", `erratic`)
+ if err := setupErratic(c); err != nil {
+ t.Fatalf("Test 2, expected no errors, but got: %q", err)
+ }
+
+ c = caddy.NewTestController("dns", `erratic {
+ drop -1
+ }`)
+ if err := setupErratic(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/plugin/errors/README.md b/plugin/errors/README.md
new file mode 100644
index 000000000..21b8f4848
--- /dev/null
+++ b/plugin/errors/README.md
@@ -0,0 +1,22 @@
+# errors
+
+*errors* enables error logging.
+
+Any errors encountered during the query processing will be printed to standard output.
+
+## Syntax
+
+~~~
+errors
+~~~
+
+## Examples
+
+Use the *whoami* to respond to queries and Log errors to standard output.
+
+~~~ corefile
+. {
+ whoami
+ errors
+}
+~~~
diff --git a/plugin/errors/errors.go b/plugin/errors/errors.go
new file mode 100644
index 000000000..a313f2e0d
--- /dev/null
+++ b/plugin/errors/errors.go
@@ -0,0 +1,79 @@
+// Package errors implements an HTTP error handling plugin.
+package errors
+
+import (
+ "fmt"
+ "log"
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// errorHandler handles DNS errors (and errors from other plugin).
+type errorHandler struct {
+ Next plugin.Handler
+ LogFile string
+ Log *log.Logger
+}
+
+// ServeDNS implements the plugin.Handler interface.
+func (h errorHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ defer h.recovery(ctx, w, r)
+
+ rcode, err := plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
+
+ if err != nil {
+ state := request.Request{W: w, Req: r}
+ errMsg := fmt.Sprintf("%s [ERROR %d %s %s] %v", time.Now().Format(timeFormat), rcode, state.Name(), state.Type(), err)
+
+ h.Log.Println(errMsg)
+ }
+
+ return rcode, err
+}
+
+func (h errorHandler) Name() string { return "errors" }
+
+func (h errorHandler) recovery(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
+ rec := recover()
+ if rec == nil {
+ return
+ }
+
+ // Obtain source of panic
+ // From: https://gist.github.com/swdunlop/9629168
+ var name, file string // function name, file name
+ var line int
+ var pc [16]uintptr
+ n := runtime.Callers(3, pc[:])
+ for _, pc := range pc[:n] {
+ fn := runtime.FuncForPC(pc)
+ if fn == nil {
+ continue
+ }
+ file, line = fn.FileLine(pc)
+ name = fn.Name()
+ if !strings.HasPrefix(name, "runtime.") {
+ break
+ }
+ }
+
+ // Trim file path
+ delim := "/coredns/"
+ pkgPathPos := strings.Index(file, delim)
+ if pkgPathPos > -1 && len(file) > pkgPathPos+len(delim) {
+ file = file[pkgPathPos+len(delim):]
+ }
+
+ panicMsg := fmt.Sprintf("%s [PANIC %s %s] %s:%d - %v", time.Now().Format(timeFormat), r.Question[0].Name, dns.Type(r.Question[0].Qtype), file, line, rec)
+ // Currently we don't use the function name, since file:line is more conventional
+ h.Log.Printf(panicMsg)
+}
+
+const timeFormat = "02/Jan/2006:15:04:05 -0700"
diff --git a/plugin/errors/errors_test.go b/plugin/errors/errors_test.go
new file mode 100644
index 000000000..039562a56
--- /dev/null
+++ b/plugin/errors/errors_test.go
@@ -0,0 +1,73 @@
+package errors
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "log"
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestErrors(t *testing.T) {
+ buf := bytes.Buffer{}
+ em := errorHandler{Log: log.New(&buf, "", 0)}
+
+ 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("[ERROR %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 := dnsrecorder.New(&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 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/plugin/errors/setup.go b/plugin/errors/setup.go
new file mode 100644
index 000000000..19bdcdb80
--- /dev/null
+++ b/plugin/errors/setup.go
@@ -0,0 +1,55 @@
+package errors
+
+import (
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("errors", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ handler, err := errorsParse(c)
+ if err != nil {
+ return plugin.Error("errors", err)
+ }
+
+ handler.Log = log.New(os.Stdout, "", 0)
+
+ 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 := errorHandler{}
+
+ for c.Next() {
+ args := c.RemainingArgs()
+ switch len(args) {
+ case 0:
+ handler.LogFile = "stdout"
+ case 1:
+ if args[0] != "stdout" {
+ return handler, fmt.Errorf("invalid log file: %s", args[0])
+ }
+ handler.LogFile = args[0]
+ default:
+ return handler, c.ArgErr()
+ }
+ }
+ return handler, nil
+}
diff --git a/plugin/errors/setup_test.go b/plugin/errors/setup_test.go
new file mode 100644
index 000000000..bae85da32
--- /dev/null
+++ b/plugin/errors/setup_test.go
@@ -0,0 +1,45 @@
+package errors
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestErrorsParse(t *testing.T) {
+ tests := []struct {
+ inputErrorsRules string
+ shouldErr bool
+ expectedErrorHandler errorHandler
+ }{
+ {`errors`, false, errorHandler{
+ LogFile: "stdout",
+ }},
+ {`errors stdout`, false, errorHandler{
+ LogFile: "stdout",
+ }},
+ {`errors errors.txt`, true, errorHandler{
+ LogFile: "",
+ }},
+ {`errors visible`, true, errorHandler{
+ LogFile: "",
+ }},
+ {`errors { log visible }`, true, errorHandler{
+ LogFile: "stdout",
+ }},
+ }
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.inputErrorsRules)
+ actualErrorsRule, 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)
+ }
+ if actualErrorsRule.LogFile != test.expectedErrorHandler.LogFile {
+ t.Errorf("Test %d expected LogFile to be %s, but got %s",
+ i, test.expectedErrorHandler.LogFile, actualErrorsRule.LogFile)
+ }
+ }
+}
diff --git a/plugin/etcd/README.md b/plugin/etcd/README.md
new file mode 100644
index 000000000..f65c193f1
--- /dev/null
+++ b/plugin/etcd/README.md
@@ -0,0 +1,109 @@
+# etcd
+
+*etcd* enables reading zone data from an etcd instance. The data in etcd 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 should also work just like SkyDNS.
+
+The etcd plugin makes extensive use of the proxy plugin to forward and query other servers
+in the network.
+
+## Syntax
+
+~~~
+etcd [ZONES...]
+~~~
+
+* **ZONES** zones etcd should be authoritative for.
+
+The path will default to `/skydns` the local etcd proxy (http://localhost:2379).
+If no zones are specified the block's zone will be used as the zone.
+
+If you want to `round robin` A and AAAA responses look at the `loadbalance` plugin.
+
+~~~
+etcd [ZONES...] {
+ stubzones
+ fallthrough
+ path PATH
+ endpoint ENDPOINT...
+ upstream ADDRESS...
+ tls CERT KEY CACERT
+}
+~~~
+
+* `stubzones` enables the stub zones feature. The stubzone is *only* done in the etcd tree located
+ under the *first* zone specified.
+* `fallthrough` If zone matches but no record can be generated, pass request to the next plugin.
+* **PATH** the path inside etcd. Defaults to "/skydns".
+* **ENDPOINT** the etcd endpoints. Defaults to "http://localhost:2397".
+* `upstream` upstream resolvers to be used resolve external names found in etcd (think CNAMEs)
+ pointing to external names. If you want CoreDNS to act as a proxy for clients, you'll need to add
+ the proxy plugin. **ADDRESS** can be an IP address, and IP:port or a string pointing to a file
+ that is structured as /etc/resolv.conf.
+* `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
+
+## Examples
+
+This is the default SkyDNS setup, with everying specified in full:
+
+~~~
+.:53 {
+ etcd skydns.local {
+ stubzones
+ path /skydns
+ endpoint http://localhost:2379
+ upstream 8.8.8.8:53 8.8.4.4:53
+ }
+ prometheus
+ cache 160 skydns.local
+ loadbalance
+ proxy . 8.8.8.8:53 8.8.4.4:53
+}
+~~~
+
+Or a setup where we use `/etc/resolv.conf` as the basis for the proxy and the upstream
+when resolving external pointing CNAMEs.
+
+~~~
+.:53 {
+ etcd skydns.local {
+ path /skydns
+ upstream /etc/resolv.conf
+ }
+ cache 160 skydns.local
+ proxy . /etc/resolv.conf
+}
+~~~
+
+
+### 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. (The fun starts with IPv6 reverse zones
+in the ip6.arpa domain.) Showing a snippet of a Corefile:
+
+~~~
+ etcd skydns.local 0.0.10.in-addr.arpa {
+ stubzones
+ ...
+~~~
+
+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.
+
+~~~
+% curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/arpa/in-addr/10/0/0/127 \
+ -d value='{"host":"reverse.skydns.local."}'
+~~~
+
+Querying with dig:
+
+~~~
+% dig @localhost -x 10.0.0.127 +short
+reverse.atoom.net.
+~~~
diff --git a/plugin/etcd/cname_test.go b/plugin/etcd/cname_test.go
new file mode 100644
index 000000000..33291094b
--- /dev/null
+++ b/plugin/etcd/cname_test.go
@@ -0,0 +1,79 @@
+// +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/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+)
+
+// Check the ordering of returned cname.
+func TestCnameLookup(t *testing.T) {
+ etc := newEtcdMiddleware()
+
+ for _, serv := range servicesCname {
+ set(t, etc, serv.Key, 0, serv)
+ defer delete(t, etc, serv.Key)
+ }
+ for _, tc := range dnsTestCasesCname {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := etc.ServeDNS(ctxt, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ if !test.Header(t, tc, resp) {
+ t.Logf("%v\n", resp)
+ continue
+ }
+ if !test.Section(t, tc, test.Answer, resp.Answer) {
+ t.Logf("%v\n", resp)
+ }
+ if !test.Section(t, tc, test.Ns, resp.Ns) {
+ t.Logf("%v\n", resp)
+ }
+ if !test.Section(t, tc, test.Extra, resp.Extra) {
+ t.Logf("%v\n", resp)
+ }
+ }
+}
+
+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."},
+}
+
+var dnsTestCasesCname = []test.Case{
+ {
+ 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"),
+ },
+ },
+}
diff --git a/plugin/etcd/etcd.go b/plugin/etcd/etcd.go
new file mode 100644
index 000000000..862be065b
--- /dev/null
+++ b/plugin/etcd/etcd.go
@@ -0,0 +1,188 @@
+// Package etcd provides the etcd backend plugin.
+package etcd
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/etcd/msg"
+ "github.com/coredns/coredns/plugin/pkg/cache"
+ "github.com/coredns/coredns/plugin/pkg/singleflight"
+ "github.com/coredns/coredns/plugin/proxy"
+ "github.com/coredns/coredns/request"
+
+ etcdc "github.com/coreos/etcd/client"
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Etcd is a plugin talks to an etcd cluster.
+type Etcd struct {
+ Next plugin.Handler
+ Fallthrough bool
+ Zones []string
+ PathPrefix string
+ Proxy proxy.Proxy // Proxy for looking up names during the resolution process
+ Client etcdc.KeysAPI
+ Ctx context.Context
+ Inflight *singleflight.Group
+ Stubmap *map[string]proxy.Proxy // list of proxies for stub resolving.
+
+ endpoints []string // Stored here as well, to aid in testing.
+}
+
+// Services implements the ServiceBackend interface.
+func (e *Etcd) Services(state request.Request, exact bool, opt plugin.Options) (services []msg.Service, err error) {
+ services, err = e.Records(state, exact)
+ if err != nil {
+ return
+ }
+
+ services = msg.Group(services)
+ return
+}
+
+// Reverse implements the ServiceBackend interface.
+func (e *Etcd) Reverse(state request.Request, exact bool, opt plugin.Options) (services []msg.Service, err error) {
+ return e.Services(state, exact, opt)
+}
+
+// Lookup implements the ServiceBackend interface.
+func (e *Etcd) Lookup(state request.Request, name string, typ uint16) (*dns.Msg, error) {
+ return e.Proxy.Lookup(state, name, typ)
+}
+
+// IsNameError implements the ServiceBackend interface.
+func (e *Etcd) IsNameError(err error) bool {
+ if ee, ok := err.(etcdc.Error); ok && ee.Code == etcdc.ErrorCodeKeyNotFound {
+ return true
+ }
+ return false
+}
+
+// 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(state request.Request, exact bool) ([]msg.Service, error) {
+ name := state.Name()
+
+ path, star := msg.PathWithWildcard(name, e.PathPrefix)
+ r, err := e.get(path, true)
+ if err != nil {
+ return nil, err
+ }
+ segments := strings.Split(msg.Path(name, e.PathPrefix), "/")
+ switch {
+ case exact && r.Node.Dir:
+ return nil, nil
+ case r.Node.Dir:
+ return e.loopNodes(r.Node.Nodes, segments, star, nil)
+ default:
+ return e.loopNodes([]*etcdc.Node{r.Node}, segments, false, nil)
+ }
+}
+
+// get is a wrapper for client.Get that uses SingleInflight to suppress multiple outstanding queries.
+func (e *Etcd) get(path string, recursive bool) (*etcdc.Response, error) {
+
+ hash := cache.Hash([]byte(path))
+
+ resp, err := e.Inflight.Do(hash, func() (interface{}, error) {
+ ctx, cancel := context.WithTimeout(e.Ctx, etcdTimeout)
+ defer cancel()
+ r, e := e.Client.Get(ctx, path, &etcdc.GetOptions{Sort: false, Recursive: recursive})
+ if e != nil {
+ return nil, e
+ }
+ return r, e
+ })
+ if err != nil {
+ return nil, err
+ }
+ return resp.(*etcdc.Response), err
+}
+
+// skydns/local/skydns/east/staging/web
+// skydns/local/skydns/west/production/web
+//
+// skydns/local/skydns/*/*/web
+// skydns/local/skydns/*/web
+
+// loopNodes recursively loops through the nodes and returns all the values. The nodes' keyname
+// will be match against any wildcards when star is true.
+func (e *Etcd) loopNodes(ns []*etcdc.Node, nameParts []string, star bool, bx map[msg.Service]bool) (sx []msg.Service, err error) {
+ if bx == nil {
+ bx = make(map[msg.Service]bool)
+ }
+Nodes:
+ for _, n := range ns {
+ if n.Dir {
+ nodes, err := e.loopNodes(n.Nodes, nameParts, star, bx)
+ if err != nil {
+ return nil, err
+ }
+ sx = append(sx, nodes...)
+ continue
+ }
+ if star {
+ keyParts := strings.Split(n.Key, "/")
+ 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([]byte(n.Value), serv); err != nil {
+ return nil, fmt.Errorf("%s: %s", n.Key, err.Error())
+ }
+ b := msg.Service{Host: serv.Host, Port: serv.Port, Priority: serv.Priority, Weight: serv.Weight, Text: serv.Text, Key: n.Key}
+ if _, ok := bx[b]; ok {
+ continue
+ }
+ bx[b] = true
+
+ serv.Key = n.Key
+ serv.TTL = e.TTL(n, serv)
+ if serv.Priority == 0 {
+ serv.Priority = priority
+ }
+ 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(node *etcdc.Node, serv *msg.Service) uint32 {
+ etcdTTL := uint32(node.TTL)
+
+ 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
+}
+
+const (
+ priority = 10 // default priority when nothing is set
+ ttl = 300 // default ttl when nothing is set
+ etcdTimeout = 5 * time.Second
+)
diff --git a/plugin/etcd/group_test.go b/plugin/etcd/group_test.go
new file mode 100644
index 000000000..d7f6172c7
--- /dev/null
+++ b/plugin/etcd/group_test.go
@@ -0,0 +1,74 @@
+// +build etcd
+
+package etcd
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/etcd/msg"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+)
+
+func TestGroupLookup(t *testing.T) {
+ etc := newEtcdMiddleware()
+
+ 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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := etc.ServeDNS(ctxt, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", err)
+ continue
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+// 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"},
+}
+
+var dnsTestCasesGroup = []test.Case{
+ // Groups
+ {
+ // hits the group 'g1' and only includes those 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"),
+ },
+ },
+}
diff --git a/plugin/etcd/handler.go b/plugin/etcd/handler.go
new file mode 100644
index 000000000..49f15343d
--- /dev/null
+++ b/plugin/etcd/handler.go
@@ -0,0 +1,97 @@
+package etcd
+
+import (
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// 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}
+
+ name := state.Name()
+
+ // We need to check stubzones first, because we may get a request for a zone we
+ // are not auth. for *but* do have a stubzone forward for. If we do the stubzone
+ // handler will handle the request.
+ if e.Stubmap != nil && len(*e.Stubmap) > 0 {
+ for zone := range *e.Stubmap {
+ if plugin.Name(zone).Matches(name) {
+ stub := Stub{Etcd: e, Zone: zone}
+ return stub.ServeDNS(ctx, w, 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
+ err error
+ )
+ switch state.Type() {
+ case "A":
+ records, err = plugin.A(e, zone, state, nil, opt)
+ case "AAAA":
+ records, err = plugin.AAAA(e, zone, state, nil, opt)
+ case "TXT":
+ records, err = plugin.TXT(e, zone, state, opt)
+ case "CNAME":
+ records, err = plugin.CNAME(e, zone, state, opt)
+ case "PTR":
+ records, err = plugin.PTR(e, zone, state, opt)
+ case "MX":
+ records, extra, err = plugin.MX(e, zone, state, opt)
+ case "SRV":
+ records, extra, err = plugin.SRV(e, zone, state, opt)
+ case "SOA":
+ records, err = plugin.SOA(e, zone, state, opt)
+ case "NS":
+ if state.Name() == zone {
+ records, extra, err = plugin.NS(e, zone, state, opt)
+ break
+ }
+ fallthrough
+ default:
+ // Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN
+ _, err = plugin.A(e, zone, state, nil, opt)
+ }
+
+ if e.IsNameError(err) {
+ if e.Fallthrough {
+ 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(e, zone, dns.RcodeNameError, state, nil /* err */, opt)
+ }
+ if err != nil {
+ return plugin.BackendError(e, zone, dns.RcodeServerFailure, state, err, opt)
+ }
+
+ if len(records) == 0 {
+ return plugin.BackendError(e, zone, dns.RcodeSuccess, state, err, opt)
+ }
+
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
+ m.Answer = append(m.Answer, records...)
+ m.Extra = append(m.Extra, extra...)
+
+ m = dnsutil.Dedup(m)
+ state.SizeAndDo(m)
+ m, _ = state.Scrub(m)
+ w.WriteMsg(m)
+ return dns.RcodeSuccess, nil
+}
+
+// Name implements the Handler interface.
+func (e *Etcd) Name() string { return "etcd" }
diff --git a/plugin/etcd/lookup_test.go b/plugin/etcd/lookup_test.go
new file mode 100644
index 000000000..1f52d5993
--- /dev/null
+++ b/plugin/etcd/lookup_test.go
@@ -0,0 +1,273 @@
+// +build etcd
+
+package etcd
+
+import (
+ "context"
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/coredns/coredns/plugin/etcd/msg"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/pkg/singleflight"
+ "github.com/coredns/coredns/plugin/pkg/tls"
+ "github.com/coredns/coredns/plugin/proxy"
+ "github.com/coredns/coredns/plugin/test"
+
+ etcdc "github.com/coreos/etcd/client"
+ "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."},
+ // 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."},
+ // 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."},
+ // 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. 300 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")},
+ },
+ // CNAME (unresolvable internal name)
+ {
+ Qname: "cname.prod.region1.skydns.test.", Qtype: dns.TypeA,
+ Ns: []dns.RR{test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")},
+ },
+ // 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. 300 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. 300 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. 300 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. 300 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. 300 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."),
+ },
+ 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"),
+ },
+ },
+ // NS Record Test
+ {
+ Qname: "a.skydns.test.", Qtype: dns.TypeNS, Rcode: dns.RcodeNameError,
+ Ns: []dns.RR{test.SOA("skydns.test. 300 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"),
+ },
+ },
+ {
+ Qname: "skydns_extra.test.", Qtype: dns.TypeSOA,
+ Answer: []dns.RR{test.SOA("skydns_extra.test. 300 IN SOA ns.dns.skydns_extra.test. hostmaster.skydns_extra.test. 1460498836 14400 3600 604800 60")},
+ },
+ // 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 newEtcdMiddleware() *Etcd {
+ ctxt = context.TODO()
+
+ endpoints := []string{"http://localhost:2379"}
+ tlsc, _ := tls.NewTLSConfigFromArgs()
+ client, _ := newEtcdClient(endpoints, tlsc)
+
+ return &Etcd{
+ Proxy: proxy.NewLookup([]string{"8.8.8.8:53"}),
+ PathPrefix: "skydns",
+ Ctx: context.Background(),
+ Inflight: &singleflight.Group{},
+ Zones: []string{"skydns.test.", "skydns_extra.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.Set(ctxt, path, string(b), &etcdc.SetOptions{TTL: ttl})
+}
+
+func delete(t *testing.T, e *Etcd, k string) {
+ path, _ := msg.PathWithWildcard(k, e.PathPrefix)
+ e.Client.Delete(ctxt, path, &etcdc.DeleteOptions{Recursive: false})
+}
+
+func TestLookup(t *testing.T) {
+ etc := newEtcdMiddleware()
+ for _, serv := range services {
+ set(t, etc, serv.Key, 0, serv)
+ defer delete(t, etc, serv.Key)
+ }
+
+ for _, tc := range dnsTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ etc.ServeDNS(ctxt, rec, m)
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+var ctxt context.Context
diff --git a/plugin/etcd/msg/path.go b/plugin/etcd/msg/path.go
new file mode 100644
index 000000000..2abdec0fe
--- /dev/null
+++ b/plugin/etcd/msg/path.go
@@ -0,0 +1,48 @@
+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, "/")
+ // 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 ascts as Path, but if a name contains wildcards (* or any), the name will be
+// chopped of before the (first) wildcard, and we do a highler evel 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/plugin/etcd/msg/path_test.go b/plugin/etcd/msg/path_test.go
new file mode 100644
index 000000000..a9ec59713
--- /dev/null
+++ b/plugin/etcd/msg/path_test.go
@@ -0,0 +1,12 @@
+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)
+ }
+ }
+}
diff --git a/plugin/etcd/msg/service.go b/plugin/etcd/msg/service.go
new file mode 100644
index 000000000..9250cb634
--- /dev/null
+++ b/plugin/etcd/msg/service.go
@@ -0,0 +1,203 @@
+// Package msg defines the Service structure which is used for service discovery.
+package msg
+
+import (
+ "fmt"
+ "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:"-"`
+}
+
+// RR returns an RR representation of s. It is in a condensed form to minimize space
+// when this is returned in a DNS message.
+// The RR will look like:
+// 1.rails.production.east.skydns.local. 300 CH TXT "service1.example.com:8080(10,0,,false)[0,]"
+// etcd Key Ttl Host:Port < see below >
+// between parens: (Priority, Weight, Text (only first 200 bytes!), Mail)
+// between blockquotes: [TargetStrip,Group]
+// If the record is synthesised by CoreDNS (i.e. no lookup in etcd happened):
+//
+// TODO(miek): what to put here?
+//
+func (s *Service) RR() *dns.TXT {
+ l := len(s.Text)
+ if l > 200 {
+ l = 200
+ }
+ t := new(dns.TXT)
+ t.Hdr.Class = dns.ClassCHAOS
+ t.Hdr.Ttl = s.TTL
+ t.Hdr.Rrtype = dns.TypeTXT
+ t.Hdr.Name = Domain(s.Key)
+
+ t.Txt = make([]string, 1)
+ t.Txt[0] = fmt.Sprintf("%s:%d(%d,%d,%s,%t)[%d,%s]",
+ s.Host, s.Port,
+ s.Priority, s.Weight, s.Text[:l], s.Mail,
+ s.TargetStrip, s.Group)
+ return t
+}
+
+// NewSRV returns a new SRV record based on the Service.
+func (s *Service) NewSRV(name string, weight uint16) *dns.SRV {
+ host := targetStrip(dns.Fqdn(s.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: dns.Fqdn(host)}
+}
+
+// NewMX returns a new MX record based on the Service.
+func (s *Service) NewMX(name string) *dns.MX {
+ host := targetStrip(dns.Fqdn(s.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 := targetStrip(dns.Fqdn(s.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 {
+ if targetStrip == 0 {
+ return name
+ }
+
+ offset, end := 0, false
+ for i := 0; i < targetStrip; i++ {
+ offset, end = dns.NextLabel(name, offset)
+ }
+ if end {
+ // We overshot the name, use the orignal one.
+ offset = 0
+ }
+ name = name[offset:]
+ return name
+}
diff --git a/plugin/etcd/msg/service_test.go b/plugin/etcd/msg/service_test.go
new file mode 100644
index 000000000..0c19ba95b
--- /dev/null
+++ b/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 fith 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/plugin/etcd/msg/type.go b/plugin/etcd/msg/type.go
new file mode 100644
index 000000000..7f3bfdbb9
--- /dev/null
+++ b/plugin/etcd/msg/type.go
@@ -0,0 +1,33 @@
+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:
+ return dns.TypeCNAME, 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/plugin/etcd/msg/type_test.go b/plugin/etcd/msg/type_test.go
new file mode 100644
index 000000000..bad1eead0
--- /dev/null
+++ b/plugin/etcd/msg/type_test.go
@@ -0,0 +1,31 @@
+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/plugin/etcd/multi_test.go b/plugin/etcd/multi_test.go
new file mode 100644
index 000000000..56b5af265
--- /dev/null
+++ b/plugin/etcd/multi_test.go
@@ -0,0 +1,59 @@
+// +build etcd
+
+package etcd
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/etcd/msg"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+)
+
+func TestMultiLookup(t *testing.T) {
+ etc := newEtcdMiddleware()
+ etc.Zones = []string{"skydns.test.", "miek.nl."}
+ etc.Fallthrough = true
+ 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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := etc.ServeDNS(ctxt, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+// 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/plugin/etcd/other_test.go b/plugin/etcd/other_test.go
new file mode 100644
index 000000000..d28c33537
--- /dev/null
+++ b/plugin/etcd/other_test.go
@@ -0,0 +1,150 @@
+// +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/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+)
+
+func TestOtherLookup(t *testing.T) {
+ etc := newEtcdMiddleware()
+
+ 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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := etc.ServeDNS(ctxt, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", err)
+ continue
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+// 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. 300 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))),
+ },
+ },
+ // Large txt greater than 512 (UDP)
+ {
+ Qname: "large600.skydns.test.", Qtype: dns.TypeTXT,
+ Answer: []dns.RR{
+ test.TXT(fmt.Sprintf("large600.skydns.test. 300 IN TXT \"%s\"", strings.Repeat("0", 600))),
+ },
+ },
+ // Large txt greater than 1500 (typical Ethernet)
+ {
+ Qname: "large2000.skydns.test.", Qtype: dns.TypeTXT,
+ Answer: []dns.RR{
+ test.TXT(fmt.Sprintf("large2000.skydns.test. 300 IN TXT \"%s\"", strings.Repeat("0", 2000))),
+ },
+ },
+ // 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/plugin/etcd/setup.go b/plugin/etcd/setup.go
new file mode 100644
index 000000000..415feb2ef
--- /dev/null
+++ b/plugin/etcd/setup.go
@@ -0,0 +1,144 @@
+package etcd
+
+import (
+ "crypto/tls"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/pkg/singleflight"
+ mwtls "github.com/coredns/coredns/plugin/pkg/tls"
+ "github.com/coredns/coredns/plugin/proxy"
+
+ etcdc "github.com/coreos/etcd/client"
+ "github.com/mholt/caddy"
+ "golang.org/x/net/context"
+)
+
+func init() {
+ caddy.RegisterPlugin("etcd", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ e, stubzones, err := etcdParse(c)
+ if err != nil {
+ return plugin.Error("etcd", err)
+ }
+
+ if stubzones {
+ c.OnStartup(func() error {
+ e.UpdateStubZones()
+ return nil
+ })
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ e.Next = next
+ return e
+ })
+
+ return nil
+}
+
+func etcdParse(c *caddy.Controller) (*Etcd, bool, error) {
+ stub := make(map[string]proxy.Proxy)
+ etc := Etcd{
+ // Don't default to a proxy for lookups.
+ // Proxy: proxy.NewLookup([]string{"8.8.8.8:53", "8.8.4.4:53"}),
+ PathPrefix: "skydns",
+ Ctx: context.Background(),
+ Inflight: &singleflight.Group{},
+ Stubmap: &stub,
+ }
+ var (
+ tlsConfig *tls.Config
+ err error
+ endpoints = []string{defaultEndpoint}
+ stubzones = false
+ )
+ for c.Next() {
+ etc.Zones = c.RemainingArgs()
+ if len(etc.Zones) == 0 {
+ etc.Zones = make([]string, len(c.ServerBlockKeys))
+ copy(etc.Zones, c.ServerBlockKeys)
+ }
+ for i, str := range etc.Zones {
+ etc.Zones[i] = plugin.Host(str).Normalize()
+ }
+
+ if c.NextBlock() {
+ for {
+ switch c.Val() {
+ case "stubzones":
+ stubzones = true
+ case "fallthrough":
+ etc.Fallthrough = true
+ case "debug":
+ /* it is a noop now */
+ case "path":
+ if !c.NextArg() {
+ return &Etcd{}, false, c.ArgErr()
+ }
+ etc.PathPrefix = c.Val()
+ case "endpoint":
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ return &Etcd{}, false, c.ArgErr()
+ }
+ endpoints = args
+ case "upstream":
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ return &Etcd{}, false, c.ArgErr()
+ }
+ ups, err := dnsutil.ParseHostPortOrFile(args...)
+ if err != nil {
+ return &Etcd{}, false, err
+ }
+ etc.Proxy = proxy.NewLookup(ups)
+ case "tls": // cert key cacertfile
+ args := c.RemainingArgs()
+ tlsConfig, err = mwtls.NewTLSConfigFromArgs(args...)
+ if err != nil {
+ return &Etcd{}, false, err
+ }
+ default:
+ if c.Val() != "}" {
+ return &Etcd{}, false, c.Errf("unknown property '%s'", c.Val())
+ }
+ }
+
+ if !c.Next() {
+ break
+ }
+ }
+
+ }
+ client, err := newEtcdClient(endpoints, tlsConfig)
+ if err != nil {
+ return &Etcd{}, false, err
+ }
+ etc.Client = client
+ etc.endpoints = endpoints
+
+ return &etc, stubzones, nil
+ }
+ return &Etcd{}, false, nil
+}
+
+func newEtcdClient(endpoints []string, cc *tls.Config) (etcdc.KeysAPI, error) {
+ etcdCfg := etcdc.Config{
+ Endpoints: endpoints,
+ Transport: mwtls.NewHTTPSTransport(cc),
+ }
+ cli, err := etcdc.New(etcdCfg)
+ if err != nil {
+ return nil, err
+ }
+ return etcdc.NewKeysAPI(cli), nil
+}
+
+const defaultEndpoint = "http://localhost:2379"
diff --git a/plugin/etcd/setup_test.go b/plugin/etcd/setup_test.go
new file mode 100644
index 000000000..833e2ba4c
--- /dev/null
+++ b/plugin/etcd/setup_test.go
@@ -0,0 +1,64 @@
+package etcd
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/mholt/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.
+ }{
+ // positive
+ {
+ `etcd`, false, "skydns", "http://localhost:2379", "",
+ },
+ {
+ `etcd skydns.local {
+ endpoint localhost:300
+}
+`, false, "skydns", "localhost:300", "",
+ },
+ // negative
+ {
+ `etcd {
+ endpoints localhost:300
+}
+`, true, "", "", "unknown property 'endpoints'",
+ },
+ }
+
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.input)
+ etcd, _ /*stubzones*/, 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, 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 && etcd.endpoints[0] != test.expectedEndpoint { // only checks the first
+ t.Errorf("Etcd not correctly set for input %s. Expected: '%s', actual: '%s'", test.input, test.expectedEndpoint, etcd.endpoints[0])
+ }
+ }
+}
diff --git a/plugin/etcd/stub.go b/plugin/etcd/stub.go
new file mode 100644
index 000000000..d7b9d5036
--- /dev/null
+++ b/plugin/etcd/stub.go
@@ -0,0 +1,82 @@
+package etcd
+
+import (
+ "log"
+ "net"
+ "strconv"
+ "time"
+
+ "github.com/coredns/coredns/plugin/etcd/msg"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/proxy"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// UpdateStubZones checks etcd for an update on the stubzones.
+func (e *Etcd) UpdateStubZones() {
+ go func() {
+ for {
+ e.updateStubZones()
+ time.Sleep(15 * time.Second)
+ }
+ }()
+}
+
+// Look in .../dns/stub/<zone>/xx for msg.Services. Loop through them
+// extract <zone> and add them as forwarders (ip:port-combos) for
+// the stub zones. Only numeric (i.e. IP address) hosts are used.
+// Only the first zone configured on e is used for the lookup.
+func (e *Etcd) updateStubZones() {
+ zone := e.Zones[0]
+
+ fakeState := request.Request{W: nil, Req: new(dns.Msg)}
+ fakeState.Req.SetQuestion(stubDomain+"."+zone, dns.TypeA)
+
+ services, err := e.Records(fakeState, false)
+ if err != nil {
+ return
+ }
+
+ stubmap := make(map[string]proxy.Proxy)
+ // track the nameservers on a per domain basis, but allow a list on the domain.
+ nameservers := map[string][]string{}
+
+Services:
+ for _, serv := range services {
+ if serv.Port == 0 {
+ serv.Port = 53
+ }
+ ip := net.ParseIP(serv.Host)
+ if ip == nil {
+ log.Printf("[WARNING] Non IP address stub nameserver: %s", serv.Host)
+ continue
+ }
+
+ domain := msg.Domain(serv.Key)
+ labels := dns.SplitDomainName(domain)
+
+ // If the remaining name equals any of the zones we have, we ignore it.
+ for _, z := range e.Zones {
+ // Chop of left most label, because that is used as the nameserver place holder
+ // and drop the right most labels that belong to zone.
+ // We must *also* chop of dns.stub. which means cutting two more labels.
+ domain = dnsutil.Join(labels[1 : len(labels)-dns.CountLabel(z)-2])
+ if domain == z {
+ log.Printf("[WARNING] Skipping nameserver for domain we are authoritative for: %s", domain)
+ continue Services
+ }
+ }
+ nameservers[domain] = append(nameservers[domain], net.JoinHostPort(serv.Host, strconv.Itoa(serv.Port)))
+ }
+
+ for domain, nss := range nameservers {
+ stubmap[domain] = proxy.NewLookup(nss)
+ }
+ // atomic swap (at least that's what we hope it is)
+ if len(stubmap) > 0 {
+ e.Stubmap = &stubmap
+ }
+ return
+}
diff --git a/plugin/etcd/stub_handler.go b/plugin/etcd/stub_handler.go
new file mode 100644
index 000000000..6f4a49950
--- /dev/null
+++ b/plugin/etcd/stub_handler.go
@@ -0,0 +1,86 @@
+package etcd
+
+import (
+ "errors"
+ "log"
+
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Stub wraps an Etcd. We have this type so that it can have a ServeDNS method.
+type Stub struct {
+ *Etcd
+ Zone string // for what zone (and thus what nameservers are we called)
+}
+
+// ServeDNS implements the plugin.Handler interface.
+func (s Stub) ServeDNS(ctx context.Context, w dns.ResponseWriter, req *dns.Msg) (int, error) {
+ if hasStubEdns0(req) {
+ log.Printf("[WARNING] Forwarding cycle detected, refusing msg: %s", req.Question[0].Name)
+ return dns.RcodeRefused, errors.New("stub forward cycle")
+ }
+ req = addStubEdns0(req)
+ proxy, ok := (*s.Etcd.Stubmap)[s.Zone]
+ if !ok { // somebody made a mistake..
+ return dns.RcodeServerFailure, nil
+ }
+
+ state := request.Request{W: w, Req: req}
+ m, e := proxy.Forward(state)
+ if e != nil {
+ return dns.RcodeServerFailure, e
+ }
+ m.RecursionAvailable, m.Compress = true, true
+ state.SizeAndDo(m)
+ w.WriteMsg(m)
+ return dns.RcodeSuccess, nil
+}
+
+// hasStubEdns0 checks if the message is carrying our special edns0 zero option.
+func hasStubEdns0(m *dns.Msg) bool {
+ option := m.IsEdns0()
+ if option == nil {
+ return false
+ }
+ for _, o := range option.Option {
+ if o.Option() == ednsStubCode && len(o.(*dns.EDNS0_LOCAL).Data) == 1 &&
+ o.(*dns.EDNS0_LOCAL).Data[0] == 1 {
+ return true
+ }
+ }
+ return false
+}
+
+// addStubEdns0 adds our special option to the message's OPT record.
+func addStubEdns0(m *dns.Msg) *dns.Msg {
+ option := m.IsEdns0()
+ // Add a custom EDNS0 option to the packet, so we can detect loops when 2 stubs are forwarding to each other.
+ if option != nil {
+ option.Option = append(option.Option, &dns.EDNS0_LOCAL{Code: ednsStubCode, Data: []byte{1}})
+ return m
+ }
+
+ m.Extra = append(m.Extra, ednsStub)
+ return m
+}
+
+const (
+ ednsStubCode = dns.EDNS0LOCALSTART + 10
+ stubDomain = "stub.dns"
+)
+
+var ednsStub = func() *dns.OPT {
+ o := new(dns.OPT)
+ o.Hdr.Name = "."
+ o.Hdr.Rrtype = dns.TypeOPT
+ o.SetUDPSize(4096)
+
+ e := new(dns.EDNS0_LOCAL)
+ e.Code = ednsStubCode
+ e.Data = []byte{1}
+ o.Option = append(o.Option, e)
+ return o
+}()
diff --git a/plugin/etcd/stub_test.go b/plugin/etcd/stub_test.go
new file mode 100644
index 000000000..56fd481b7
--- /dev/null
+++ b/plugin/etcd/stub_test.go
@@ -0,0 +1,88 @@
+// +build etcd
+
+package etcd
+
+import (
+ "net"
+ "strconv"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/etcd/msg"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+)
+
+func fakeStubServerExampleNet(t *testing.T) (*dns.Server, string) {
+ server, addr, err := test.UDPServer("127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("failed to create a UDP server: %s", err)
+ }
+ // add handler for example.net
+ dns.HandleFunc("example.net.", func(w dns.ResponseWriter, r *dns.Msg) {
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Answer = []dns.RR{test.A("example.net. 86400 IN A 93.184.216.34")}
+ w.WriteMsg(m)
+ })
+
+ return server, addr
+}
+
+func TestStubLookup(t *testing.T) {
+ server, addr := fakeStubServerExampleNet(t)
+ defer server.Shutdown()
+
+ host, p, _ := net.SplitHostPort(addr)
+ port, _ := strconv.Atoi(p)
+ exampleNetStub := &msg.Service{Host: host, Port: port, Key: "a.example.net.stub.dns.skydns.test."}
+ servicesStub = append(servicesStub, exampleNetStub)
+
+ etc := newEtcdMiddleware()
+
+ for _, serv := range servicesStub {
+ set(t, etc, serv.Key, 0, serv)
+ defer delete(t, etc, serv.Key)
+ }
+
+ etc.updateStubZones()
+
+ for _, tc := range dnsTestCasesStub {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := etc.ServeDNS(ctxt, rec, m)
+ if err != nil && m.Question[0].Name == "example.org." {
+ // This is OK, we expect this backend to *not* work.
+ continue
+ }
+ if err != nil {
+ t.Errorf("expected no error, got %v for %s\n", err, m.Question[0].Name)
+ }
+ resp := rec.Msg
+ if resp == nil {
+ // etcd not running?
+ continue
+ }
+
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+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."},
+}
+
+var dnsTestCasesStub = []test.Case{
+ {
+ Qname: "example.org.", Qtype: dns.TypeA, Rcode: dns.RcodeServerFailure,
+ },
+ {
+ Qname: "example.net.", Qtype: dns.TypeA,
+ Answer: []dns.RR{test.A("example.net. 86400 IN A 93.184.216.34")},
+ Extra: []dns.RR{test.OPT(4096, false)}, // This will have an EDNS0 section, because *we* added our local stub forward to detect loops.
+ },
+}
diff --git a/plugin/federation/README.md b/plugin/federation/README.md
new file mode 100644
index 000000000..fb3d44e8c
--- /dev/null
+++ b/plugin/federation/README.md
@@ -0,0 +1,43 @@
+# federation
+
+The *federation* plugin enables
+[federated](https://kubernetes.io/docs/tasks/federation/federation-service-discovery/) queries to be
+resolved via the kubernetes plugin.
+
+Enabling *federation* without also having *kubernetes* is a noop.
+
+## Syntax
+
+~~~
+federation [ZONES...] {
+ NAME DOMAIN
+~~~
+
+* Each **NAME** and **DOMAIN** defines federation membership. One entry for each. A duplicate
+ **NAME** will silently overwrite any previous value.
+
+## Examples
+
+Here we handle all service requests in the `prod` and `stage` federations.
+
+~~~ txt
+. {
+ kubernetes cluster.local
+ federation cluster.local {
+ prod prod.feddomain.com
+ staging staging.feddomain.com
+ }
+}
+~~~
+
+Or slightly shorter:
+
+~~~ txt
+cluster.local {
+ kubernetes
+ federation {
+ prod prod.feddomain.com
+ staging staging.feddomain.com
+ }
+}
+~~~
diff --git a/plugin/federation/federation.go b/plugin/federation/federation.go
new file mode 100644
index 000000000..c94e8f819
--- /dev/null
+++ b/plugin/federation/federation.go
@@ -0,0 +1,141 @@
+/*
+Package federation implements kubernetes federation. It checks if the qname matches
+a possible federation. If this is the case and the captured answer is an NXDOMAIN,
+federation is performed. If this is not the case the original answer is returned.
+
+The federation label is always the 2nd to last once the zone is chopped of. For
+instance "nginx.mynamespace.myfederation.svc.example.com" has "myfederation" as
+the federation label. For federation to work we do a normal k8s lookup
+*without* that label, if that comes back with NXDOMAIN or NODATA(??) we create
+a federation record and return that.
+
+Federation is only useful in conjunction with the kubernetes plugin, without it is a noop.
+*/
+package federation
+
+import (
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/etcd/msg"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/pkg/nonwriter"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Federation contains the name to zone mapping used for federation in kubernetes.
+type Federation struct {
+ f map[string]string
+ zones []string
+
+ Next plugin.Handler
+ Federations Func
+}
+
+// Func needs to be implemented by any plugin that implements
+// federation. Right now this is only the kubernetes plugin.
+type Func func(state request.Request, fname, fzone string) (msg.Service, error)
+
+// New returns a new federation.
+func New() *Federation {
+ return &Federation{f: make(map[string]string)}
+}
+
+// ServeDNS implements the plugin.Handle interface.
+func (f *Federation) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ if f.Federations == nil {
+ return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
+ }
+
+ state := request.Request{W: w, Req: r}
+ zone := plugin.Zones(f.zones).Matches(state.Name())
+ if zone == "" {
+ return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
+ }
+
+ state.Zone = zone
+
+ // Remove the federation label from the qname to see if something exists.
+ without, label := f.isNameFederation(state.Name(), state.Zone)
+ if without == "" {
+ return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
+ }
+
+ qname := r.Question[0].Name
+ r.Question[0].Name = without
+ state.Clear()
+
+ // Start the next plugin, but with a nowriter, capture the result, if NXDOMAIN
+ // perform federation, otherwise just write the result.
+ nw := nonwriter.New(w)
+ ret, err := plugin.NextOrFailure(f.Name(), f.Next, ctx, nw, r)
+
+ if !plugin.ClientWrite(ret) {
+ // something went wrong
+ r.Question[0].Name = qname
+ return ret, err
+ }
+
+ if m := nw.Msg; m.Rcode != dns.RcodeNameError {
+ // If positive answer we need to substitute the original qname in the answer.
+ m.Question[0].Name = qname
+ for _, a := range m.Answer {
+ a.Header().Name = qname
+ }
+
+ state.SizeAndDo(m)
+ m, _ = state.Scrub(m)
+ w.WriteMsg(m)
+
+ return dns.RcodeSuccess, nil
+ }
+
+ // Still here, we've seen NXDOMAIN and need to perform federation.
+ service, err := f.Federations(state, label, f.f[label]) // state references Req which has updated qname
+ if err != nil {
+ r.Question[0].Name = qname
+ return dns.RcodeServerFailure, err
+ }
+
+ r.Question[0].Name = qname
+
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
+
+ m.Answer = []dns.RR{service.NewCNAME(state.QName(), service.Host)}
+
+ state.SizeAndDo(m)
+ m, _ = state.Scrub(m)
+ w.WriteMsg(m)
+
+ return dns.RcodeSuccess, nil
+}
+
+// Name implements the plugin.Handle interface.
+func (f *Federation) Name() string { return "federation" }
+
+// IsNameFederation checks the qname to see if it is a potential federation. The federation
+// label is always the 2nd to last once the zone is chopped of. For instance
+// "nginx.mynamespace.myfederation.svc.example.com" has "myfederation" as the federation label.
+// IsNameFederation returns a new qname with the federation label and the label itself or two
+// empty strings if there wasn't a hit.
+func (f *Federation) isNameFederation(name, zone string) (string, string) {
+ base, _ := dnsutil.TrimZone(name, zone)
+
+ // TODO(miek): dns.PrevLabel is better for memory, or dns.Split.
+ labels := dns.SplitDomainName(base)
+ ll := len(labels)
+ if ll < 2 {
+ return "", ""
+ }
+
+ fed := labels[ll-2]
+
+ if _, ok := f.f[fed]; ok {
+ without := dnsutil.Join(labels[:ll-2]) + labels[ll-1] + "." + zone
+ return without, fed
+ }
+ return "", ""
+}
diff --git a/plugin/federation/federation_test.go b/plugin/federation/federation_test.go
new file mode 100644
index 000000000..920f1a340
--- /dev/null
+++ b/plugin/federation/federation_test.go
@@ -0,0 +1,81 @@
+package federation
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/kubernetes"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestIsNameFederation(t *testing.T) {
+ tests := []struct {
+ fed string
+ qname string
+ expectedZone string
+ }{
+ {"prod", "nginx.mynamespace.prod.svc.example.com.", "nginx.mynamespace.svc.example.com."},
+ {"prod", "nginx.mynamespace.staging.svc.example.com.", ""},
+ {"prod", "nginx.mynamespace.example.com.", ""},
+ {"prod", "example.com.", ""},
+ {"prod", "com.", ""},
+ }
+
+ fed := New()
+ for i, tc := range tests {
+ fed.f[tc.fed] = "test-name"
+ if x, _ := fed.isNameFederation(tc.qname, "example.com."); x != tc.expectedZone {
+ t.Errorf("Test %d, failed to get zone, expected %s, got %s", i, tc.expectedZone, x)
+ }
+ }
+}
+
+func TestFederationKubernetes(t *testing.T) {
+ tests := []test.Case{
+ {
+ // service exists so we return the IP address associated with it.
+ Qname: "svc1.testns.prod.svc.cluster.local.", Qtype: dns.TypeA,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{
+ test.A("svc1.testns.prod.svc.cluster.local. 303 IN A 10.0.0.1"),
+ },
+ },
+ {
+ // service does not exist, do the federation dance.
+ Qname: "svc0.testns.prod.svc.cluster.local.", Qtype: dns.TypeA,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{
+ test.CNAME("svc0.testns.prod.svc.cluster.local. 303 IN CNAME svc0.testns.prod.svc.fd-az.fd-r.federal.example."),
+ },
+ },
+ }
+
+ k := kubernetes.New([]string{"cluster.local."})
+ k.APIConn = &APIConnFederationTest{}
+
+ fed := New()
+ fed.zones = []string{"cluster.local."}
+ fed.Federations = k.Federations
+ fed.Next = k
+ fed.f = map[string]string{
+ "prod": "federal.example.",
+ }
+
+ ctx := context.TODO()
+ for i, tc := range tests {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fed.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Test %d, expected no error, got %v\n", i, err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
diff --git a/plugin/federation/kubernetes_api_test.go b/plugin/federation/kubernetes_api_test.go
new file mode 100644
index 000000000..48a03666e
--- /dev/null
+++ b/plugin/federation/kubernetes_api_test.go
@@ -0,0 +1,111 @@
+package federation
+
+import (
+ "github.com/coredns/coredns/plugin/kubernetes"
+
+ "k8s.io/client-go/1.5/pkg/api"
+)
+
+type APIConnFederationTest struct{}
+
+func (APIConnFederationTest) Run() { return }
+func (APIConnFederationTest) Stop() error { return nil }
+
+func (APIConnFederationTest) PodIndex(string) []interface{} {
+ a := make([]interface{}, 1)
+ a[0] = &api.Pod{
+ ObjectMeta: api.ObjectMeta{
+ Namespace: "podns",
+ },
+ Status: api.PodStatus{
+ PodIP: "10.240.0.1", // Remote IP set in test.ResponseWriter
+ },
+ }
+ return a
+}
+
+func (APIConnFederationTest) ServiceList() []*api.Service {
+ svcs := []*api.Service{
+ {
+ ObjectMeta: api.ObjectMeta{
+ Name: "svc1",
+ Namespace: "testns",
+ },
+ Spec: api.ServiceSpec{
+ ClusterIP: "10.0.0.1",
+ Ports: []api.ServicePort{{
+ Name: "http",
+ Protocol: "tcp",
+ Port: 80,
+ }},
+ },
+ },
+ {
+ ObjectMeta: api.ObjectMeta{
+ Name: "hdls1",
+ Namespace: "testns",
+ },
+ Spec: api.ServiceSpec{
+ ClusterIP: api.ClusterIPNone,
+ },
+ },
+ {
+ ObjectMeta: api.ObjectMeta{
+ Name: "external",
+ Namespace: "testns",
+ },
+ Spec: api.ServiceSpec{
+ ExternalName: "ext.interwebs.test",
+ Ports: []api.ServicePort{{
+ Name: "http",
+ Protocol: "tcp",
+ Port: 80,
+ }},
+ },
+ },
+ }
+ return svcs
+
+}
+
+func (APIConnFederationTest) EndpointsList() api.EndpointsList {
+ return api.EndpointsList{
+ Items: []api.Endpoints{
+ {
+ Subsets: []api.EndpointSubset{
+ {
+ Addresses: []api.EndpointAddress{
+ {
+ IP: "172.0.0.1",
+ Hostname: "ep1a",
+ },
+ },
+ Ports: []api.EndpointPort{
+ {
+ Port: 80,
+ Protocol: "tcp",
+ Name: "http",
+ },
+ },
+ },
+ },
+ ObjectMeta: api.ObjectMeta{
+ Name: "svc1",
+ Namespace: "testns",
+ },
+ },
+ },
+ }
+}
+
+func (APIConnFederationTest) GetNodeByName(name string) (api.Node, error) {
+ return api.Node{
+ ObjectMeta: api.ObjectMeta{
+ Name: "test.node.foo.bar",
+ Labels: map[string]string{
+ kubernetes.LabelRegion: "fd-r",
+ kubernetes.LabelZone: "fd-az",
+ },
+ },
+ }, nil
+}
diff --git a/plugin/federation/setup.go b/plugin/federation/setup.go
new file mode 100644
index 000000000..72514fe8f
--- /dev/null
+++ b/plugin/federation/setup.go
@@ -0,0 +1,89 @@
+package federation
+
+import (
+ "fmt"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/kubernetes"
+ "github.com/miekg/dns"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("federation", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ fed, err := federationParse(c)
+ if err != nil {
+ return plugin.Error("federation", err)
+ }
+
+ // Do this in OnStartup, so all plugin has been initialized.
+ c.OnStartup(func() error {
+ m := dnsserver.GetConfig(c).Handler("kubernetes")
+ if m == nil {
+ return nil
+ }
+ if x, ok := m.(*kubernetes.Kubernetes); ok {
+ fed.Federations = x.Federations
+ }
+ return nil
+ })
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ fed.Next = next
+ return fed
+ })
+
+ return nil
+}
+
+func federationParse(c *caddy.Controller) (*Federation, error) {
+ fed := New()
+
+ for c.Next() {
+ // federation [zones..]
+ zones := c.RemainingArgs()
+ origins := []string{}
+ if len(zones) > 0 {
+ origins = make([]string, len(zones))
+ copy(origins, zones)
+ } else {
+ origins = make([]string, len(c.ServerBlockKeys))
+ copy(origins, c.ServerBlockKeys)
+ }
+
+ for c.NextBlock() {
+ x := c.Val()
+ switch x {
+ default:
+ args := c.RemainingArgs()
+ if x := len(args); x != 1 {
+ return fed, fmt.Errorf("need two arguments for federation, got %d", x)
+ }
+
+ fed.f[x] = dns.Fqdn(args[0])
+ }
+ }
+
+ for i := range origins {
+ origins[i] = plugin.Host(origins[i]).Normalize()
+ }
+
+ fed.zones = origins
+
+ if len(fed.f) == 0 {
+ return fed, fmt.Errorf("at least one name to zone federation expected")
+ }
+
+ return fed, nil
+ }
+
+ return fed, nil
+}
diff --git a/plugin/federation/setup_test.go b/plugin/federation/setup_test.go
new file mode 100644
index 000000000..e85b01772
--- /dev/null
+++ b/plugin/federation/setup_test.go
@@ -0,0 +1,65 @@
+package federation
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestSetup(t *testing.T) {
+ tests := []struct {
+ input string
+ shouldErr bool
+ expectedLen int
+ expectedNameZone []string // contains only entry for now
+ }{
+ // ok
+ {`federation {
+ prod prod.example.org
+ }`, false, 1, []string{"prod", "prod.example.org."}},
+
+ {`federation {
+ staging staging.example.org
+ prod prod.example.org
+ }`, false, 2, []string{"prod", "prod.example.org."}},
+ {`federation {
+ staging staging.example.org
+ prod prod.example.org
+ }`, false, 2, []string{"staging", "staging.example.org."}},
+ {`federation example.com {
+ staging staging.example.org
+ prod prod.example.org
+ }`, false, 2, []string{"staging", "staging.example.org."}},
+ // errors
+ {`federation {
+ }`, true, 0, []string{}},
+ {`federation {
+ staging
+ }`, true, 0, []string{}},
+ }
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.input)
+ fed, err := federationParse(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 x := len(fed.f); x != test.expectedLen {
+ t.Errorf("Test %v: Expected map length of %d, got: %d", i, test.expectedLen, x)
+ }
+ if x, ok := fed.f[test.expectedNameZone[0]]; !ok {
+ t.Errorf("Test %v: Expected name for %s, got nothing", i, test.expectedNameZone[0])
+ } else {
+ if x != test.expectedNameZone[1] {
+ t.Errorf("Test %v: Expected zone: %s, got %s", i, test.expectedNameZone[1], x)
+ }
+ }
+ }
+}
diff --git a/plugin/file/README.md b/plugin/file/README.md
new file mode 100644
index 000000000..d7e1590b4
--- /dev/null
+++ b/plugin/file/README.md
@@ -0,0 +1,55 @@
+# file
+
+*file* enables serving zone data from an RFC 1035-style master file.
+
+The file 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. DNSSEC) correct DNSSEC answers
+are returned. Only NSEC is supported! If you use this setup *you* are responsible for resigning the
+zonefile.
+
+## Syntax
+
+~~~
+file DBFILE [ZONES...]
+~~~
+
+* **DBFILE** the database file to read and parse. If the path is relative the path from the *root*
+ directive 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.
+
+TSIG key configuration is TODO; directive format for transfer will probably be extended with
+TSIG key information, something like `transfer out [ADDRESS...] key [NAME[:ALG]] [BASE64]`
+
+~~~
+file DBFILE [ZONES... ] {
+ transfer to ADDRESS...
+ no_reload
+ upstream ADDRESS...
+}
+~~~
+
+* `transfer` enables zone transfers. It may be specified multiples times. `To` or `from` signals
+ the direction. **ADDRESS** must be denoted in CIDR notation (127.0.0.1/32 etc.) or just as plain
+ addresses. The special wildcard `*` means: the entire internet (only valid for 'transfer to').
+ When an address is specified a notify message will be send whenever the zone is reloaded.
+* `no_reload` by default CoreDNS will reload a zone from disk whenever it detects a change to the
+ file. This option disables that behavior.
+* `upstream` defines upstream resolvers to be used resolve external names found (think CNAMEs)
+ pointing to external names. This is only really useful when CoreDNS is configured as a proxy, for
+ normal authoritative serving you don't need *or* want to use this. **ADDRESS** can be an IP
+ address, and IP:port or a string pointing to a file that is structured as /etc/resolv.conf.
+
+## Examples
+
+Load the `example.org` zone from `example.org.signed` and allow transfers to the internet, but send
+notifies to 10.240.1.1
+
+~~~
+file example.org.signed example.org {
+ transfer to *
+ transfer to 10.240.1.1
+}
+~~~
diff --git a/plugin/file/closest.go b/plugin/file/closest.go
new file mode 100644
index 000000000..64652af83
--- /dev/null
+++ b/plugin/file/closest.go
@@ -0,0 +1,24 @@
+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/plugin/file/closest_test.go b/plugin/file/closest_test.go
new file mode 100644
index 000000000..b37495493
--- /dev/null
+++ b/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/plugin/file/cname_test.go b/plugin/file/cname_test.go
new file mode 100644
index 000000000..1178a7512
--- /dev/null
+++ b/plugin/file/cname_test.go
@@ -0,0 +1,124 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/proxy"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestLookupCNAMEChain(t *testing.T) {
+ name := "example.org."
+ zone, err := Parse(strings.NewReader(dbExampleCNAME), name, "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{name: zone}, Names: []string{name}}}
+ ctx := context.TODO()
+
+ for _, tc := range cnameTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+var cnameTestCases = []test.Case{
+ {
+ Qname: "a.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.A("a.example.org. 1800 IN A 127.0.0.1"),
+ },
+ },
+ {
+ Qname: "www3.example.org.", Qtype: dns.TypeCNAME,
+ Answer: []dns.RR{
+ test.CNAME("www3.example.org. 1800 IN CNAME www2.example.org."),
+ },
+ },
+ {
+ Qname: "dangling.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.CNAME("dangling.example.org. 1800 IN CNAME foo.example.org."),
+ },
+ },
+ {
+ Qname: "www3.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.A("a.example.org. 1800 IN A 127.0.0.1"),
+ test.CNAME("www.example.org. 1800 IN CNAME a.example.org."),
+ test.CNAME("www1.example.org. 1800 IN CNAME www.example.org."),
+ test.CNAME("www2.example.org. 1800 IN CNAME www1.example.org."),
+ test.CNAME("www3.example.org. 1800 IN CNAME www2.example.org."),
+ },
+ },
+}
+
+func TestLookupCNAMEExternal(t *testing.T) {
+ name := "example.org."
+ zone, err := Parse(strings.NewReader(dbExampleCNAME), name, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expected no error when reading zone, got %q", err)
+ }
+ zone.Proxy = proxy.NewLookup([]string{"8.8.8.8:53"}) // TODO(miek): point to local instance
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{name: zone}, Names: []string{name}}}
+ ctx := context.TODO()
+
+ for _, tc := range exernalTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+var exernalTestCases = []test.Case{
+ {
+ Qname: "external.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.CNAME("external.example.org. 1800 CNAME www.example.net."),
+ // magic 303 TTL that says: don't check TTL.
+ test.A("www.example.net. 303 IN A 93.184.216.34"),
+ },
+ },
+}
+
+const dbExampleCNAME = `
+$TTL 30M
+$ORIGIN example.org.
+@ IN SOA linode.atoom.net. miek.miek.nl. (
+ 1282630057 ; Serial
+ 4H ; Refresh
+ 1H ; Retry
+ 7D ; Expire
+ 4H ) ; Negative Cache TTL
+
+a IN A 127.0.0.1
+www3 IN CNAME www2
+www2 IN CNAME www1
+www1 IN CNAME www
+www IN CNAME a
+dangling IN CNAME foo
+external IN CNAME www.example.net.`
diff --git a/plugin/file/delegation_test.go b/plugin/file/delegation_test.go
new file mode 100644
index 000000000..1ad9804f4
--- /dev/null
+++ b/plugin/file/delegation_test.go
@@ -0,0 +1,207 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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,
+ Answer: []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: "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.OPT(4096, true),
+ 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,
+ Answer: []dns.RR{
+ test.NS("delegated.example.org. 1800 IN NS a.delegated.example.org."),
+ test.NS("delegated.example.org. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ },
+ Extra: []dns.RR{
+ test.OPT(4096, true),
+ 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.OPT(4096, true),
+ 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.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.OPT(4096, true),
+ 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"),
+ },
+ },
+}
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %q\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+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/plugin/file/dname.go b/plugin/file/dname.go
new file mode 100644
index 000000000..f552bfdfd
--- /dev/null
+++ b/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/plugin/file/dname_test.go b/plugin/file/dname_test.go
new file mode 100644
index 000000000..92e33dde7
--- /dev/null
+++ b/plugin/file/dname_test.go
@@ -0,0 +1,300 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// 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 = "<no match>"
+ }
+
+ 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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", 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="),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ 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="),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+}
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", 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/plugin/file/dnssec_test.go b/plugin/file/dnssec_test.go
new file mode 100644
index 000000000..17b122c7e
--- /dev/null
+++ b/plugin/file/dnssec_test.go
@@ -0,0 +1,358 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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,
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ 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,
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ 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="),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ 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,
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ 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,
+ Extra: []dns.RR{
+ test.OPT(4096, true),
+ },
+ },
+ {
+ // 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"),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ 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"),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ 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"),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ 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"),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+}
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+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 := dnsrecorder.New(&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/plugin/file/dnssex_test.go b/plugin/file/dnssex_test.go
new file mode 100644
index 000000000..d9a0a4568
--- /dev/null
+++ b/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/plugin/file/ds_test.go b/plugin/file/ds_test.go
new file mode 100644
index 000000000..e1087a81d
--- /dev/null
+++ b/plugin/file/ds_test.go
@@ -0,0 +1,75 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
diff --git a/plugin/file/ent_test.go b/plugin/file/ent_test.go
new file mode 100644
index 000000000..6f4f1db6c
--- /dev/null
+++ b/plugin/file/ent_test.go
@@ -0,0 +1,159 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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"),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+}
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+// fdjfdjkf
+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/plugin/file/example_org.go b/plugin/file/example_org.go
new file mode 100644
index 000000000..eba18e0e4
--- /dev/null
+++ b/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/plugin/file/file.go b/plugin/file/file.go
new file mode 100644
index 000000000..89c2df90a
--- /dev/null
+++ b/plugin/file/file.go
@@ -0,0 +1,138 @@
+// Package file implements a file backend.
+package file
+
+import (
+ "fmt"
+ "io"
+ "log"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+type (
+ // File is the plugin that reads zone data from disk.
+ File struct {
+ Next plugin.Handler
+ Zones Zones
+ }
+
+ // 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
+ }
+
+ // 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, m.RecursionAvailable, m.Compress = true, true, true
+ state.SizeAndDo(m)
+ w.WriteMsg(m)
+
+ log.Printf("[INFO] Notify from %s for %s: checking transfer", state.IP(), zone)
+ ok, err := z.shouldTransfer()
+ if ok {
+ z.TransferIn()
+ } else {
+ log.Printf("[INFO] Notify from %s for %s: no serial increase seen", state.IP(), zone)
+ }
+ if err != nil {
+ log.Printf("[WARNING] Notify from %s for %s: failed primary check: %s", state.IP(), zone, err)
+ }
+ return dns.RcodeSuccess, nil
+ }
+ log.Printf("[INFO] Dropping notify from %s for %s", state.IP(), zone)
+ return dns.RcodeSuccess, nil
+ }
+
+ if z.Expired != nil && *z.Expired {
+ log.Printf("[ERROR] Zone %s is expired", zone)
+ return dns.RcodeServerFailure, nil
+ }
+
+ if state.QType() == dns.TypeAXFR || state.QType() == dns.TypeIXFR {
+ xfr := Xfr{z}
+ return xfr.ServeDNS(ctx, w, r)
+ }
+
+ answer, ns, extra, result := z.Lookup(state, qname)
+
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Authoritative, m.RecursionAvailable, m.Compress = true, true, 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:
+ return dns.RcodeServerFailure, nil
+ }
+
+ state.SizeAndDo(m)
+ m, _ = state.Scrub(m)
+ w.WriteMsg(m)
+ return dns.RcodeSuccess, nil
+}
+
+// Name implements the Handler interface.
+func (f File) Name() string { return "file" }
+
+// 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) {
+ tokens := dns.ParseZone(f, dns.Fqdn(origin), fileName)
+ z := NewZone(origin, fileName)
+ seenSOA := false
+ for x := range tokens {
+ if x.Error != nil {
+ return nil, x.Error
+ }
+
+ if !seenSOA && serial >= 0 {
+ if s, ok := x.RR.(*dns.SOA); ok {
+ if s.Serial == uint32(serial) { // same zone
+ return nil, fmt.Errorf("no change in serial: %d", serial)
+ }
+ seenSOA = true
+ }
+ }
+
+ if err := z.Insert(x.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/plugin/file/file_test.go b/plugin/file/file_test.go
new file mode 100644
index 000000000..02668785b
--- /dev/null
+++ b/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/plugin/file/glue_test.go b/plugin/file/glue_test.go
new file mode 100644
index 000000000..3880953c2
--- /dev/null
+++ b/plugin/file/glue_test.go
@@ -0,0 +1,253 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// 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),
+ 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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+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/plugin/file/include_test.go b/plugin/file/include_test.go
new file mode 100644
index 000000000..fad91df5c
--- /dev/null
+++ b/plugin/file/include_test.go
@@ -0,0 +1,32 @@
+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/plugin/file/lookup.go b/plugin/file/lookup.go
new file mode 100644
index 000000000..cf2f06841
--- /dev/null
+++ b/plugin/file/lookup.go
@@ -0,0 +1,467 @@
+package file
+
+import (
+ "github.com/coredns/coredns/plugin/file/tree"
+ "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(state request.Request, qname string) ([]dns.RR, []dns.RR, []dns.RR, Result) {
+
+ qtype := state.QType()
+ do := state.Do()
+
+ if !z.NoReload {
+ z.reloadMu.RLock()
+ }
+ defer func() {
+ if !z.NoReload {
+ z.reloadMu.RUnlock()
+ }
+ }()
+
+ // 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.
+ soa := z.Apex.SOA
+ if soa == nil {
+ return nil, nil, nil, ServerFailure
+ }
+
+ if qtype == dns.TypeSOA {
+ return z.soa(do), z.ns(do), nil, Success
+ }
+ if qtype == dns.TypeNS && qname == z.origin {
+ nsrrs := z.ns(do)
+ glue := z.Glue(nsrrs, do)
+ return nsrrs, nil, glue, Success
+ }
+
+ var (
+ found, shot bool
+ parts string
+ i int
+ elem, wildElem *tree.Elem
+ )
+
+ // 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 resposne.
+ //
+ // 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 = z.Tree.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 := z.Tree.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.Types(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 {
+ answer, ns, extra, rcode := z.searchCNAME(state, elem, []dns.RR{cname})
+
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(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.Types(dns.TypeNS); nsrrs != nil {
+ glue := z.Glue(nsrrs, do)
+ // If qtype == NS, we should returns success to put RRs in answer.
+ if qtype == dns.TypeNS {
+ return nsrrs, nil, glue, Success
+ }
+
+ if do {
+ dss := z.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.Types(dns.TypeCNAME); len(rrs) > 0 && qtype != dns.TypeCNAME {
+ return z.searchCNAME(state, elem, rrs)
+ }
+
+ rrs := elem.Types(qtype, qname)
+
+ // NODATA
+ if len(rrs) == 0 {
+ ret := z.soa(do)
+ if do {
+ nsec := z.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 baliwick -
+ // if so add IP addresses to the additional section.
+ additional := additionalProcessing(z, rrs, do)
+
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, qtype)
+ rrs = append(rrs, sigs...)
+ }
+
+ return rrs, z.ns(do), additional, Success
+
+ }
+
+ // Haven't found the original name.
+
+ // Found wildcard.
+ if wildElem != nil {
+ auth := z.ns(do)
+
+ if rrs := wildElem.Types(dns.TypeCNAME, qname); len(rrs) > 0 {
+ return z.searchCNAME(state, wildElem, rrs)
+ }
+
+ rrs := wildElem.Types(qtype, qname)
+
+ // NODATA response.
+ if len(rrs) == 0 {
+ ret := z.soa(do)
+ if do {
+ nsec := z.typeFromElem(wildElem, dns.TypeNSEC, do)
+ ret = append(ret, nsec...)
+ }
+ return nil, ret, nil, Success
+ }
+
+ if do {
+ // An NSEC is needed to say no longer name exists under this wildcard.
+ if deny, found := z.Tree.Prev(qname); found {
+ nsec := z.typeFromElem(deny, dns.TypeNSEC, do)
+ auth = append(auth, nsec...)
+ }
+
+ sigs := wildElem.Types(dns.TypeRRSIG, qname)
+ sigs = signatureForSubType(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 := z.Tree.Next(qname); found {
+ if dns.IsSubDomain(qname, x.Name()) {
+ rcode = Success
+ }
+ }
+
+ ret := z.soa(do)
+ if do {
+ deny, _ := z.Tree.Prev(qname) // TODO(miek): *found* was not used here.
+ nsec := z.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 := z.Tree.Prev(wildcard); found {
+ // Only add this nsec if it is different than the one already added
+ if ss.Name() != deny.Name() {
+ nsec := z.typeFromElem(ss, dns.TypeNSEC, do)
+ ret = append(ret, nsec...)
+ }
+ }
+ }
+
+ }
+Out:
+ return nil, ret, nil, rcode
+}
+
+// Return type tp from e and add signatures (if they exists) and do is true.
+func (z *Zone) typeFromElem(elem *tree.Elem, tp uint16, do bool) []dns.RR {
+ rrs := elem.Types(tp)
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, tp)
+ if len(sigs) > 0 {
+ rrs = append(rrs, sigs...)
+ }
+ }
+ return rrs
+}
+
+func (z *Zone) soa(do bool) []dns.RR {
+ if do {
+ ret := append([]dns.RR{z.Apex.SOA}, z.Apex.SIGSOA...)
+ return ret
+ }
+ return []dns.RR{z.Apex.SOA}
+}
+
+func (z *Zone) ns(do bool) []dns.RR {
+ if do {
+ ret := append(z.Apex.NS, z.Apex.SIGNS...)
+ return ret
+ }
+ return z.Apex.NS
+}
+
+// TODO(miek): should be better named, like aditionalProcessing?
+func (z *Zone) searchCNAME(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.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, dns.TypeCNAME)
+ if len(sigs) > 0 {
+ rrs = append(rrs, sigs...)
+ }
+ }
+
+ targetName := rrs[0].(*dns.CNAME).Target
+ elem, _ = z.Tree.Search(targetName)
+ if elem == nil {
+ if !dns.IsSubDomain(z.origin, targetName) {
+ rrs = append(rrs, z.externalLookup(state, targetName, qtype)...)
+ }
+ return rrs, z.ns(do), nil, Success
+ }
+
+ i := 0
+
+Redo:
+ cname := elem.Types(dns.TypeCNAME)
+ if len(cname) > 0 {
+ rrs = append(rrs, cname...)
+
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, dns.TypeCNAME)
+ if len(sigs) > 0 {
+ rrs = append(rrs, sigs...)
+ }
+ }
+ targetName := cname[0].(*dns.CNAME).Target
+ elem, _ = z.Tree.Search(targetName)
+ if elem == nil {
+ if !dns.IsSubDomain(z.origin, targetName) {
+ if !dns.IsSubDomain(z.origin, targetName) {
+ rrs = append(rrs, z.externalLookup(state, targetName, qtype)...)
+ }
+ }
+ return rrs, z.ns(do), nil, Success
+ }
+
+ i++
+ if i > maxChain {
+ return rrs, z.ns(do), nil, Success
+ }
+
+ goto Redo
+ }
+
+ targets := cnameForType(elem.All(), qtype)
+ if len(targets) > 0 {
+ rrs = append(rrs, targets...)
+
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, qtype)
+ if len(sigs) > 0 {
+ rrs = append(rrs, sigs...)
+ }
+ }
+ }
+
+ return rrs, z.ns(do), nil, Success
+}
+
+func cnameForType(targets []dns.RR, origQtype uint16) []dns.RR {
+ ret := []dns.RR{}
+ for _, target := range targets {
+ if target.Header().Rrtype == origQtype {
+ ret = append(ret, target)
+ }
+ }
+ return ret
+}
+
+func (z *Zone) externalLookup(state request.Request, target string, qtype uint16) []dns.RR {
+ m, e := z.Proxy.Lookup(state, target, qtype)
+ if e != nil {
+ // TODO(miek): debugMsg for this as well? Log?
+ return nil
+ }
+ return m.Answer
+}
+
+// signatureForSubType range through the signature and return the correct ones for the subtype.
+func signatureForSubType(rrs []dns.RR, subtype uint16) []dns.RR {
+ sigs := []dns.RR{}
+ for _, sig := range rrs {
+ if s, ok := sig.(*dns.RRSIG); ok {
+ if s.TypeCovered == subtype {
+ sigs = append(sigs, s)
+ }
+ }
+ }
+ return sigs
+}
+
+// Glue returns any potential glue records for nsrrs.
+func (z *Zone) 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, z.searchGlue(ns.Ns, do)...)
+ }
+ }
+ return glue
+}
+
+// searchGlue looks up A and AAAA for name.
+func (z *Zone) searchGlue(name string, do bool) []dns.RR {
+ glue := []dns.RR{}
+
+ // A
+ if elem, found := z.Tree.Search(name); found {
+ glue = append(glue, elem.Types(dns.TypeA)...)
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, dns.TypeA)
+ glue = append(glue, sigs...)
+ }
+ }
+
+ // AAAA
+ if elem, found := z.Tree.Search(name); found {
+ glue = append(glue, elem.Types(dns.TypeAAAA)...)
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, dns.TypeAAAA)
+ glue = append(glue, sigs...)
+ }
+ }
+ return glue
+}
+
+// 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 additionalProcessing(z *Zone, 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 !dns.IsSubDomain(z.origin, name) {
+ continue
+ }
+
+ elem, _ := z.Tree.Search(name)
+ if elem == nil {
+ continue
+ }
+
+ sigs := elem.Types(dns.TypeRRSIG)
+ for _, addr := range []uint16{dns.TypeA, dns.TypeAAAA} {
+ if a := elem.Types(addr); a != nil {
+ extra = append(extra, a...)
+ if do {
+ sig := signatureForSubType(sigs, addr)
+ extra = append(extra, sig...)
+ }
+ }
+ }
+ }
+
+ return extra
+}
+
+const maxChain = 8
diff --git a/plugin/file/lookup_test.go b/plugin/file/lookup_test.go
new file mode 100644
index 000000000..8fd93fd8e
--- /dev/null
+++ b/plugin/file/lookup_test.go
@@ -0,0 +1,194 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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,
+ },
+}
+
+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("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 dnsTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ fm.ServeDNS(ctx, rec, m)
+}
+
+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 := dnsrecorder.New(&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
+
+srv IN SRV 10 10 8080 a.miek.nl.
+mx IN MX 10 a.miek.nl.`
diff --git a/plugin/file/notify.go b/plugin/file/notify.go
new file mode 100644
index 000000000..68850e0d3
--- /dev/null
+++ b/plugin/file/notify.go
@@ -0,0 +1,82 @@
+package file
+
+import (
+ "fmt"
+ "log"
+ "net"
+
+ "github.com/coredns/coredns/plugin/pkg/rcode"
+ "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
+}
+
+// Notify will send notifies to all configured TransferTo IP addresses.
+func (z *Zone) Notify() {
+ go notify(z.origin, z.TransferTo)
+}
+
+// notify sends notifies to the configured remote servers. It will try up to three times
+// before giving up on a specific remote. We will sequentially loop through "to"
+// until they all have replied (or have 3 failed attempts).
+func notify(zone string, to []string) error {
+ m := new(dns.Msg)
+ m.SetNotify(zone)
+ c := new(dns.Client)
+
+ for _, t := range to {
+ if t == "*" {
+ continue
+ }
+ if err := notifyAddr(c, m, t); err != nil {
+ log.Printf("[ERROR] " + err.Error())
+ } else {
+ log.Printf("[INFO] Sent notify for zone %q to %q", zone, t)
+ }
+ }
+ return nil
+}
+
+func notifyAddr(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/plugin/file/nsec3_test.go b/plugin/file/nsec3_test.go
new file mode 100644
index 000000000..6611056cb
--- /dev/null
+++ b/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/plugin/file/reload.go b/plugin/file/reload.go
new file mode 100644
index 000000000..18e949a94
--- /dev/null
+++ b/plugin/file/reload.go
@@ -0,0 +1,72 @@
+package file
+
+import (
+ "log"
+ "os"
+ "path"
+
+ "github.com/fsnotify/fsnotify"
+)
+
+// Reload reloads a zone when it is changed on disk. If z.NoRoload is true, no reloading will be done.
+func (z *Zone) Reload() error {
+ if z.NoReload {
+ return nil
+ }
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ return err
+ }
+ err = watcher.Add(path.Dir(z.file))
+ if err != nil {
+ return err
+ }
+
+ go func() {
+ // TODO(miek): needs to be killed on reload.
+ for {
+ select {
+ case event := <-watcher.Events:
+ if path.Clean(event.Name) == z.file {
+
+ reader, err := os.Open(z.file)
+ if err != nil {
+ log.Printf("[ERROR] Failed to open `%s' for `%s': %v", z.file, z.origin, err)
+ continue
+ }
+
+ serial := z.SOASerialIfDefined()
+ zone, err := Parse(reader, z.origin, z.file, serial)
+ if err != nil {
+ log.Printf("[WARNING] Parsing zone `%s': %v", z.origin, err)
+ continue
+ }
+
+ // copy elements we need
+ z.reloadMu.Lock()
+ z.Apex = zone.Apex
+ z.Tree = zone.Tree
+ z.reloadMu.Unlock()
+
+ log.Printf("[INFO] Successfully reloaded zone `%s'", z.origin)
+ z.Notify()
+ }
+ case <-z.ReloadShutdown:
+ watcher.Close()
+ 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.reloadMu.Lock()
+ defer z.reloadMu.Unlock()
+ if z.Apex.SOA != nil {
+ return int64(z.Apex.SOA.Serial)
+ }
+ return -1
+}
diff --git a/plugin/file/reload_test.go b/plugin/file/reload_test.go
new file mode 100644
index 000000000..601c426d3
--- /dev/null
+++ b/plugin/file/reload_test.go
@@ -0,0 +1,82 @@
+package file
+
+import (
+ "io/ioutil"
+ "log"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/coredns/coredns/plugin/test"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+func TestZoneReload(t *testing.T) {
+ log.SetOutput(ioutil.Discard)
+
+ 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.Reload()
+
+ r := new(dns.Msg)
+ r.SetQuestion("miek.nl", dns.TypeSOA)
+ state := request.Request{W: &test.ResponseWriter{}, Req: r}
+ if _, _, _, res := z.Lookup(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(state, "miek.nl."); res != Success {
+ t.Fatalf("failed to lookup, got %d", res)
+ }
+
+ if len(z.All()) != 5 {
+ t.Fatalf("expected 5 RRs, got %d", len(z.All()))
+ }
+ if err := ioutil.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(1 * time.Second)
+
+ if len(z.All()) != 3 {
+ t.Fatalf("expected 3 RRs, got %d", len(z.All()))
+ }
+}
+
+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/plugin/file/secondary.go b/plugin/file/secondary.go
new file mode 100644
index 000000000..a37d62442
--- /dev/null
+++ b/plugin/file/secondary.go
@@ -0,0 +1,199 @@
+package file
+
+import (
+ "log"
+ "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.Copy()
+ 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.Printf("[ERROR] 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.Printf("[ERROR] 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.Printf("[ERROR] 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.Tree = z1.Tree
+ z.Apex = z1.Apex
+ *z.Expired = false
+ log.Printf("[INFO] 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 return 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 wil 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)
+
+ if refresh < time.Hour {
+ refresh = time.Hour
+ }
+ if retry < time.Hour {
+ retry = time.Hour
+ }
+ if refresh > 24*time.Hour {
+ refresh = 24 * time.Hour
+ }
+ if retry > 12*time.Hour {
+ retry = 12 * time.Hour
+ }
+
+ 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 && ok {
+ if err := z.TransferIn(); err != nil {
+ // transfer failed, leave retryActive true
+ break
+ }
+ retryActive = false
+ // transfer OK, possible new SOA, stop timers and redo
+ refreshTicker.Stop()
+ retryTicker.Stop()
+ expireTicker.Stop()
+ goto Restart
+ }
+
+ case <-refreshTicker.C:
+
+ time.Sleep(jitter(5000)) // 5s randomize
+
+ ok, err := z.shouldTransfer()
+ retryActive = err != nil
+ if err != nil && ok {
+ if err := z.TransferIn(); err != nil {
+ // transfer failed
+ retryActive = true
+ break
+ }
+ retryActive = false
+ // transfer OK, possible new SOA, stop timers and redo
+ 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/plugin/file/secondary_test.go b/plugin/file/secondary_test.go
new file mode 100644
index 000000000..8f2c2e15f
--- /dev/null
+++ b/plugin/file/secondary_test.go
@@ -0,0 +1,168 @@
+package file
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/test"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// TODO(miek): should test notifies as well, ie start test server (a real coredns one)...
+// setup other test server that sends notify, see if CoreDNS comes calling for a zone
+// tranfer
+
+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}
+ log.SetOutput(ioutil.Discard)
+
+ dns.HandleFunc(testZone, soa.Handler)
+ defer dns.HandleRemove(testZone)
+
+ s, addrstr, err := test.TCPServer("127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("unable to run test server: %v", err)
+ }
+ defer s.Shutdown()
+
+ z := new(Zone)
+ z.origin = testZone
+ z.TransferFrom = []string{addrstr}
+
+ // 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}
+ log.SetOutput(ioutil.Discard)
+
+ dns.HandleFunc(testZone, soa.Handler)
+ defer dns.HandleRemove(testZone)
+
+ s, addrstr, err := test.TCPServer("127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("unable to run test server: %v", err)
+ }
+ defer s.Shutdown()
+
+ z := new(Zone)
+ z.Expired = new(bool)
+ z.origin = testZone
+ z.TransferFrom = []string{addrstr}
+
+ err = z.TransferIn()
+ if 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.Expired = new(bool)
+ 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 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/plugin/file/setup.go b/plugin/file/setup.go
new file mode 100644
index 000000000..bf0523c54
--- /dev/null
+++ b/plugin/file/setup.go
@@ -0,0 +1,171 @@
+package file
+
+import (
+ "fmt"
+ "os"
+ "path"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/proxy"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("file", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ zones, err := fileParse(c)
+ if err != nil {
+ return plugin.Error("file", err)
+ }
+
+ // Add startup functions to notify the master(s).
+ for _, n := range zones.Names {
+ z := zones.Z[n]
+ c.OnStartup(func() error {
+ z.StartupOnce.Do(func() {
+ if len(z.TransferTo) > 0 {
+ z.Notify()
+ }
+ z.Reload()
+ })
+ return nil
+ })
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ return File{Next: next, Zones: zones}
+ })
+
+ return nil
+}
+
+func fileParse(c *caddy.Controller) (Zones, error) {
+ z := make(map[string]*Zone)
+ names := []string{}
+ origins := []string{}
+
+ config := dnsserver.GetConfig(c)
+
+ for c.Next() {
+ // file db.file [zones...]
+ if !c.NextArg() {
+ return Zones{}, c.ArgErr()
+ }
+ fileName := c.Val()
+
+ origins = make([]string, len(c.ServerBlockKeys))
+ copy(origins, c.ServerBlockKeys)
+ args := c.RemainingArgs()
+ if len(args) > 0 {
+ origins = args
+ }
+
+ if !path.IsAbs(fileName) && config.Root != "" {
+ fileName = path.Join(config.Root, fileName)
+ }
+
+ reader, err := os.Open(fileName)
+ if err != nil {
+ // bail out
+ return Zones{}, err
+ }
+
+ for i := range origins {
+ origins[i] = plugin.Host(origins[i]).Normalize()
+ zone, err := Parse(reader, origins[i], fileName, 0)
+ if err == nil {
+ z[origins[i]] = zone
+ } else {
+ return Zones{}, err
+ }
+ names = append(names, origins[i])
+ }
+
+ noReload := false
+ prxy := proxy.Proxy{}
+ t := []string{}
+ var e error
+
+ for c.NextBlock() {
+ switch c.Val() {
+ case "transfer":
+ t, _, e = TransferParse(c, false)
+ if e != nil {
+ return Zones{}, e
+ }
+
+ case "no_reload":
+ noReload = true
+
+ case "upstream":
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ return Zones{}, c.ArgErr()
+ }
+ ups, err := dnsutil.ParseHostPortOrFile(args...)
+ if err != nil {
+ return Zones{}, err
+ }
+ prxy = proxy.NewLookup(ups)
+ default:
+ return Zones{}, c.Errf("unknown property '%s'", c.Val())
+ }
+
+ for _, origin := range origins {
+ if t != nil {
+ z[origin].TransferTo = append(z[origin].TransferTo, t...)
+ }
+ z[origin].NoReload = noReload
+ z[origin].Proxy = prxy
+ }
+ }
+ }
+ return Zones{Z: z, Names: names}, nil
+}
+
+// TransferParse parses transfer statements: 'transfer to [address...]'.
+func TransferParse(c *caddy.Controller, secondary bool) (tos, froms []string, err error) {
+ if !c.NextArg() {
+ return nil, nil, c.ArgErr()
+ }
+ value := c.Val()
+ switch value {
+ case "to":
+ tos = c.RemainingArgs()
+ for i := range tos {
+ if tos[i] != "*" {
+ normalized, err := dnsutil.ParseHostPort(tos[i], "53")
+ if err != nil {
+ return nil, nil, err
+ }
+ tos[i] = normalized
+ }
+ }
+
+ case "from":
+ if !secondary {
+ return nil, nil, fmt.Errorf("can't use `transfer from` when not being a secondary")
+ }
+ froms = c.RemainingArgs()
+ for i := range froms {
+ if froms[i] != "*" {
+ normalized, err := dnsutil.ParseHostPort(froms[i], "53")
+ if err != nil {
+ return nil, nil, err
+ }
+ froms[i] = normalized
+ } else {
+ return nil, nil, fmt.Errorf("can't use '*' in transfer from")
+ }
+ }
+ }
+ return
+}
diff --git a/plugin/file/setup_test.go b/plugin/file/setup_test.go
new file mode 100644
index 000000000..62e8476f6
--- /dev/null
+++ b/plugin/file/setup_test.go
@@ -0,0 +1,77 @@
+package file
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/mholt/caddy"
+)
+
+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 {
+ transfer from 127.0.0.1
+ }`,
+ true,
+ Zones{},
+ },
+ {
+ `file`,
+ true,
+ 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."}},
+ },
+ }
+
+ 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])
+ }
+ }
+ }
+ }
+}
diff --git a/plugin/file/tree/all.go b/plugin/file/tree/all.go
new file mode 100644
index 000000000..fd806365f
--- /dev/null
+++ b/plugin/file/tree/all.go
@@ -0,0 +1,48 @@
+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
+}
+
+// Do performs fn on all values stored in the tree. A boolean is returned indicating whether the
+// Do traversal was interrupted by an Operation returning true. If fn alters stored values' sort
+// relationships, future tree operation behaviors are undefined.
+func (t *Tree) Do(fn func(e *Elem) bool) bool {
+ if t.Root == nil {
+ return false
+ }
+ return t.Root.do(fn)
+}
+
+func (n *Node) do(fn func(e *Elem) bool) (done bool) {
+ if n.Left != nil {
+ done = n.Left.do(fn)
+ if done {
+ return
+ }
+ }
+ done = fn(n.Elem)
+ if done {
+ return
+ }
+ if n.Right != nil {
+ done = n.Right.do(fn)
+ }
+ return
+}
diff --git a/plugin/file/tree/elem.go b/plugin/file/tree/elem.go
new file mode 100644
index 000000000..6317cc912
--- /dev/null
+++ b/plugin/file/tree/elem.go
@@ -0,0 +1,136 @@
+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 RRs with type qtype from e. If qname is given (only the
+// first one is used), the RR are copied and the owner is replaced with qname[0].
+func (e *Elem) Types(qtype uint16, qname ...string) []dns.RR {
+ rrs := e.m[qtype]
+
+ if rrs != nil && len(qname) > 0 {
+ copied := make([]dns.RR, len(rrs))
+ for i := range rrs {
+ copied[i] = dns.Copy(rrs[i])
+ copied[i].Header().Name = qname[0]
+ }
+ return copied
+ }
+ return rrs
+}
+
+// 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 this is a noop.
+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
+ }
+ for _, er := range rrs {
+ if equalRdata(er, rr) {
+ return
+ }
+ }
+
+ rrs = append(rrs, rr)
+ e.m[t] = rrs
+}
+
+// Delete removes rr from e. When e is empty after the removal the returned bool is true.
+func (e *Elem) Delete(rr dns.RR) (empty bool) {
+ if e.m == nil {
+ return true
+ }
+
+ t := rr.Header().Rrtype
+ rrs, ok := e.m[t]
+ if !ok {
+ return
+ }
+
+ for i, er := range rrs {
+ if equalRdata(er, rr) {
+ rrs = removeFromSlice(rrs, i)
+ e.m[t] = rrs
+ empty = len(rrs) == 0
+ if empty {
+ delete(e.m, t)
+ }
+ return
+ }
+ }
+ return
+}
+
+// Less is a tree helper function that calls less.
+func Less(a *Elem, name string) int { return less(name, a.Name()) }
+
+// Assuming the same type and name this will check if the rdata is equal as well.
+func equalRdata(a, b dns.RR) bool {
+ switch x := a.(type) {
+ // TODO(miek): more types, i.e. all types. + tests for this.
+ case *dns.A:
+ return x.A.Equal(b.(*dns.A).A)
+ case *dns.AAAA:
+ return x.AAAA.Equal(b.(*dns.AAAA).AAAA)
+ case *dns.MX:
+ if x.Mx == b.(*dns.MX).Mx && x.Preference == b.(*dns.MX).Preference {
+ return true
+ }
+ }
+ return false
+}
+
+// removeFromSlice removes index i from the slice.
+func removeFromSlice(rrs []dns.RR, i int) []dns.RR {
+ if i >= len(rrs) {
+ return rrs
+ }
+ rrs = append(rrs[:i], rrs[i+1:]...)
+ return rrs
+}
diff --git a/plugin/file/tree/less.go b/plugin/file/tree/less.go
new file mode 100644
index 000000000..3b8340088
--- /dev/null
+++ b/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 http://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/plugin/file/tree/less_test.go b/plugin/file/tree/less_test.go
new file mode 100644
index 000000000..ed021b66f
--- /dev/null
+++ b/plugin/file/tree/less_test.go
@@ -0,0 +1,81 @@
+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\n", 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/plugin/file/tree/print.go b/plugin/file/tree/print.go
new file mode 100644
index 000000000..bd86ef690
--- /dev/null
+++ b/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("<nil>")
+ }
+ 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/plugin/file/tree/tree.go b/plugin/file/tree/tree.go
new file mode 100644
index 000000000..ed33c09a4
--- /dev/null
+++ b/plugin/file/tree/tree.go
@@ -0,0 +1,455 @@
+// 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 rr from the tree, is the node turns empty, that node is deleted with DeleteNode.
+func (t *Tree) Delete(rr dns.RR) {
+ if t.Root == nil {
+ return
+ }
+
+ el, _ := t.Search(rr.Header().Name)
+ if el == nil {
+ t.deleteNode(rr)
+ return
+ }
+ // Delete from this element.
+ empty := el.Delete(rr)
+ if empty {
+ t.deleteNode(rr)
+ return
+ }
+}
+
+// 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/plugin/file/wildcard.go b/plugin/file/wildcard.go
new file mode 100644
index 000000000..9526cb53f
--- /dev/null
+++ b/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/plugin/file/wildcard_test.go b/plugin/file/wildcard_test.go
new file mode 100644
index 000000000..038d37a43
--- /dev/null
+++ b/plugin/file/wildcard_test.go
@@ -0,0 +1,289 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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...),
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ 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...),
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ // 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`),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+}
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+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 := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+const exampleOrg = `; example.org test file
+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
+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
+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/plugin/file/xfr.go b/plugin/file/xfr.go
new file mode 100644
index 000000000..4a03779ed
--- /dev/null
+++ b/plugin/file/xfr.go
@@ -0,0 +1,62 @@
+package file
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Xfr serves up an AXFR.
+type Xfr struct {
+ *Zone
+}
+
+// ServeDNS implements the plugin.Handler interface.
+func (x Xfr) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ state := request.Request{W: w, Req: r}
+ if !x.TransferAllowed(state) {
+ return dns.RcodeServerFailure, nil
+ }
+ if state.QType() != dns.TypeAXFR && state.QType() != dns.TypeIXFR {
+ return 0, plugin.Error(x.Name(), fmt.Errorf("xfr called with non transfer type: %d", state.QType()))
+ }
+
+ records := x.All()
+ if len(records) == 0 {
+ return dns.RcodeServerFailure, nil
+ }
+
+ ch := make(chan *dns.Envelope)
+ defer close(ch)
+ tr := new(dns.Transfer)
+ go tr.Out(w, r, ch)
+
+ j, l := 0, 0
+ records = append(records, records[0]) // add closing SOA to the end
+ log.Printf("[INFO] Outgoing transfer of %d records of zone %s to %s started", len(records), x.origin, state.IP())
+ for i, r := range records {
+ l += dns.Len(r)
+ if l > transferLength {
+ ch <- &dns.Envelope{RR: records[j:i]}
+ l = 0
+ j = i
+ }
+ }
+ if j < len(records) {
+ ch <- &dns.Envelope{RR: records[j:]}
+ }
+
+ w.Hijack()
+ // w.Close() // Client closes connection
+ return dns.RcodeSuccess, nil
+}
+
+// Name implements the plugin.Hander interface.
+func (x Xfr) Name() string { return "xfr" }
+
+const transferLength = 1000 // Start a new envelop after message reaches this size in bytes. Intentionally small to test multi envelope parsing.
diff --git a/plugin/file/xfr_test.go b/plugin/file/xfr_test.go
new file mode 100644
index 000000000..69ad68e64
--- /dev/null
+++ b/plugin/file/xfr_test.go
@@ -0,0 +1,34 @@
+package file
+
+import (
+ "fmt"
+ "strings"
+)
+
+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
+}
diff --git a/plugin/file/zone.go b/plugin/file/zone.go
new file mode 100644
index 000000000..1cef9dc3a
--- /dev/null
+++ b/plugin/file/zone.go
@@ -0,0 +1,190 @@
+package file
+
+import (
+ "fmt"
+ "net"
+ "path"
+ "strings"
+ "sync"
+
+ "github.com/coredns/coredns/plugin/file/tree"
+ "github.com/coredns/coredns/plugin/proxy"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// Zone defines a structure that contains all data related to a DNS zone.
+type Zone struct {
+ origin string
+ origLen int
+ file string
+ *tree.Tree
+ Apex Apex
+
+ TransferTo []string
+ StartupOnce sync.Once
+ TransferFrom []string
+ Expired *bool
+
+ NoReload bool
+ reloadMu sync.RWMutex
+ ReloadShutdown chan bool
+ Proxy proxy.Proxy // Proxy for looking up 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 {
+ z := &Zone{
+ origin: dns.Fqdn(name),
+ origLen: dns.CountLabel(dns.Fqdn(name)),
+ file: path.Clean(file),
+ Tree: &tree.Tree{},
+ Expired: new(bool),
+ ReloadShutdown: make(chan bool),
+ }
+ *z.Expired = false
+
+ return z
+}
+
+// Copy copies a zone.
+func (z *Zone) Copy() *Zone {
+ z1 := NewZone(z.origin, z.file)
+ z1.TransferTo = z.TransferTo
+ z1.TransferFrom = z.TransferFrom
+ z1.Expired = z.Expired
+
+ z1.Apex = z.Apex
+ 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
+}
+
+// Delete deletes r from z.
+func (z *Zone) Delete(r dns.RR) { z.Tree.Delete(r) }
+
+// TransferAllowed checks if incoming request for transferring the zone is allowed according to the ACLs.
+func (z *Zone) TransferAllowed(state request.Request) bool {
+ for _, t := range z.TransferTo {
+ if t == "*" {
+ return true
+ }
+ // If remote IP matches we accept.
+ remote := state.IP()
+ to, _, err := net.SplitHostPort(t)
+ if err != nil {
+ continue
+ }
+ if to == remote {
+ return true
+ }
+ }
+ // TODO(miek): future matching against IP/CIDR notations
+ return false
+}
+
+// All returns all records from the zone, the first record will be the SOA record,
+// otionally followed by all RRSIG(SOA)s.
+func (z *Zone) All() []dns.RR {
+ if !z.NoReload {
+ z.reloadMu.RLock()
+ defer z.reloadMu.RUnlock()
+ }
+
+ records := []dns.RR{}
+ allNodes := z.Tree.All()
+ for _, a := range allNodes {
+ records = append(records, a.All()...)
+ }
+
+ if len(z.Apex.SIGNS) > 0 {
+ records = append(z.Apex.SIGNS, records...)
+ }
+ records = append(z.Apex.NS, records...)
+
+ if len(z.Apex.SIGSOA) > 0 {
+ records = append(z.Apex.SIGSOA, records...)
+ }
+ return append([]dns.RR{z.Apex.SOA}, records...)
+}
+
+// Print prints the zone's tree to stdout.
+func (z *Zone) Print() {
+ z.Tree.Print()
+}
+
+// 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
+ shot := false
+ 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/plugin/file/zone_test.go b/plugin/file/zone_test.go
new file mode 100644
index 000000000..c9ff174db
--- /dev/null
+++ b/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\n", i, tc.expected, got)
+ }
+ if shot != tc.shot {
+ t.Errorf("Test %d: expected shot to be %t, got %t\n", i, tc.shot, shot)
+ }
+ }
+}
diff --git a/plugin/health/README.md b/plugin/health/README.md
new file mode 100644
index 000000000..59aed3b81
--- /dev/null
+++ b/plugin/health/README.md
@@ -0,0 +1,23 @@
+# health
+
+This module enables a simple health check endpoint. By default it will listen on port 8080.
+
+## 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 CoreDNS is healthy. It returns
+a 503. *health* periodically (1s) polls plugin that exports health information. If any of the
+plugin signals that it is unhealthy, the server will go unhealthy too. Each plugin that
+supports health checks has a section "Health" in their README.
+
+## Examples
+
+Run another health endpoint on http://localhost:8091.
+
+~~~
+health localhost:8091
+~~~
diff --git a/plugin/health/health.go b/plugin/health/health.go
new file mode 100644
index 000000000..0a256c963
--- /dev/null
+++ b/plugin/health/health.go
@@ -0,0 +1,69 @@
+// Package health implements an HTTP handler that responds to health checks.
+package health
+
+import (
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "sync"
+)
+
+var once sync.Once
+
+type health struct {
+ Addr string
+
+ ln net.Listener
+ mux *http.ServeMux
+
+ // A slice of Healthers that the health plugin will poll every second for their health status.
+ h []Healther
+ sync.RWMutex
+ ok bool // ok is the global boolean indicating an all healthy plugin stack
+}
+
+func (h *health) Startup() error {
+ if h.Addr == "" {
+ h.Addr = defAddr
+ }
+
+ once.Do(func() {
+ ln, err := net.Listen("tcp", h.Addr)
+ if err != nil {
+ log.Printf("[ERROR] Failed to start health handler: %s", err)
+ return
+ }
+
+ h.ln = ln
+
+ h.mux = http.NewServeMux()
+
+ h.mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+ if h.Ok() {
+ w.WriteHeader(http.StatusOK)
+ io.WriteString(w, ok)
+ return
+ }
+ w.WriteHeader(http.StatusServiceUnavailable)
+ })
+
+ go func() {
+ http.Serve(h.ln, h.mux)
+ }()
+ })
+ return nil
+}
+
+func (h *health) Shutdown() error {
+ if h.ln != nil {
+ return h.ln.Close()
+ }
+ return nil
+}
+
+const (
+ ok = "OK"
+ defAddr = ":8080"
+ path = "/health"
+)
diff --git a/plugin/health/health_test.go b/plugin/health/health_test.go
new file mode 100644
index 000000000..f05f53073
--- /dev/null
+++ b/plugin/health/health_test.go
@@ -0,0 +1,47 @@
+package health
+
+// TODO(miek): enable again if plugin gets health check.
+/*
+func TestHealth(t *testing.T) {
+ h := health{Addr: ":0"}
+ h.h = append(h.h, &erratic.Erratic{})
+
+ if err := h.Startup(); err != nil {
+ t.Fatalf("Unable to startup the health server: %v", err)
+ }
+ defer h.Shutdown()
+
+ // Reconstruct the http address based on the port allocated by operating system.
+ address := fmt.Sprintf("http://%s%s", h.ln.Addr().String(), path)
+
+ // Norhing set should be unhealthy
+ 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 '503', got '%d'", response.StatusCode)
+ }
+ response.Body.Close()
+
+ // Make healthy
+ h.Poll()
+
+ 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 := ioutil.ReadAll(response.Body)
+ if err != nil {
+ t.Fatalf("Unable to get response body from %s: %v", address, err)
+ }
+ response.Body.Close()
+
+ if string(content) != ok {
+ t.Errorf("Invalid response body: expecting 'OK', got '%s'", string(content))
+ }
+}
+*/
diff --git a/plugin/health/healther.go b/plugin/health/healther.go
new file mode 100644
index 000000000..ad0261dfb
--- /dev/null
+++ b/plugin/health/healther.go
@@ -0,0 +1,42 @@
+package health
+
+// Healther interface needs to be implemented by each plugin willing to
+// provide healthhceck information to the health plugin. As a second step
+// the plugin needs to registered against the health plugin, by addding
+// it to healthers map. Note this method should return quickly, i.e. just
+// checking a boolean status, as it is called every second from the health
+// plugin.
+type Healther interface {
+ // Health returns a boolean indicating the health status of a plugin.
+ // False indicates unhealthy.
+ Health() bool
+}
+
+// Ok returns the global health status of all plugin configured in this server.
+func (h *health) Ok() bool {
+ h.RLock()
+ defer h.RUnlock()
+ return h.ok
+}
+
+// SetOk sets the global health status of all plugin configured in this server.
+func (h *health) SetOk(ok bool) {
+ h.Lock()
+ defer h.Unlock()
+ h.ok = ok
+}
+
+// poll polls all healthers and sets the global state.
+func (h *health) poll() {
+ for _, m := range h.h {
+ if !m.Health() {
+ h.SetOk(false)
+ return
+ }
+ }
+ h.SetOk(true)
+}
+
+// Middleware that implements the Healther interface.
+// TODO(miek): none yet.
+var healthers = map[string]bool{}
diff --git a/plugin/health/setup.go b/plugin/health/setup.go
new file mode 100644
index 000000000..2fec9baa7
--- /dev/null
+++ b/plugin/health/setup.go
@@ -0,0 +1,73 @@
+package health
+
+import (
+ "net"
+ "time"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("health", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ addr, err := healthParse(c)
+ if err != nil {
+ return plugin.Error("health", err)
+ }
+
+ h := &health{Addr: addr}
+
+ c.OnStartup(func() error {
+ for he := range healthers {
+ m := dnsserver.GetConfig(c).Handler(he)
+ if x, ok := m.(Healther); ok {
+ h.h = append(h.h, x)
+ }
+ }
+ return nil
+ })
+
+ c.OnStartup(func() error {
+ h.poll()
+ go func() {
+ for {
+ <-time.After(1 * time.Second)
+ h.poll()
+ }
+ }()
+ return nil
+ })
+
+ c.OnStartup(h.Startup)
+ c.OnFinalShutdown(h.Shutdown)
+
+ // Don't do AddMiddleware, as health is not *really* a plugin just a separate webserver running.
+ return nil
+}
+
+func healthParse(c *caddy.Controller) (string, error) {
+ addr := ""
+ for c.Next() {
+ 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/plugin/health/setup_test.go b/plugin/health/setup_test.go
new file mode 100644
index 000000000..87f4fc5fd
--- /dev/null
+++ b/plugin/health/setup_test.go
@@ -0,0 +1,35 @@
+package health
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestSetupHealth(t *testing.T) {
+ tests := []struct {
+ input string
+ shouldErr bool
+ }{
+ {`health`, false},
+ {`health localhost:1234`, false},
+ {`health bla:a`, false},
+ {`health bla`, true},
+ {`health bla bla`, true},
+ }
+
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.input)
+ _, err := healthParse(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)
+ }
+ }
+ }
+}
diff --git a/plugin/hosts/README.md b/plugin/hosts/README.md
new file mode 100644
index 000000000..60c738077
--- /dev/null
+++ b/plugin/hosts/README.md
@@ -0,0 +1,45 @@
+# hosts
+
+*hosts* enables serving zone data from a `/etc/hosts` style file.
+
+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.
+
+## Syntax
+
+~~~
+hosts [FILE [ZONES...]] {
+ fallthrough
+}
+~~~
+
+* **FILE** the hosts file to read and parse. If the path is relative the path from the *root*
+ directive will be prepended to it. Defaults to /etc/hosts if omitted
+* **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block
+ are used.
+* `fallthrough` If zone matches and no record can be generated, pass request to the next plugin.
+
+## Examples
+
+Load `/etc/hosts` file.
+
+~~~
+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
+}
+~~~
diff --git a/plugin/hosts/hosts.go b/plugin/hosts/hosts.go
new file mode 100644
index 000000000..09dedbb64
--- /dev/null
+++ b/plugin/hosts/hosts.go
@@ -0,0 +1,136 @@
+package hosts
+
+import (
+ "net"
+
+ "golang.org/x/net/context"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/request"
+ "github.com/miekg/dns"
+)
+
+// Hosts is the plugin handler
+type Hosts struct {
+ Next plugin.Handler
+ *Hostsfile
+
+ Fallthrough bool
+}
+
+// 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.Type() != "PTR" {
+ // 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, names)
+ case dns.TypeA:
+ ips := h.LookupStaticHostV4(qname)
+ answers = a(qname, ips)
+ case dns.TypeAAAA:
+ ips := h.LookupStaticHostV6(qname)
+ answers = aaaa(qname, ips)
+ }
+
+ if len(answers) == 0 {
+ if h.Fallthrough {
+ return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
+ }
+ if !h.otherRecordsExist(state.QType(), qname) {
+ return dns.RcodeNameError, nil
+ }
+ }
+
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
+ m.Answer = answers
+
+ state.SizeAndDo(m)
+ m, _ = state.Scrub(m)
+ w.WriteMsg(m)
+ return dns.RcodeSuccess, nil
+}
+
+func (h Hosts) otherRecordsExist(qtype uint16, qname string) bool {
+ switch qtype {
+ case dns.TypeA:
+ if len(h.LookupStaticHostV6(qname)) > 0 {
+ return true
+ }
+ case dns.TypeAAAA:
+ if len(h.LookupStaticHostV4(qname)) > 0 {
+ return true
+ }
+ default:
+ 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, ips []net.IP) []dns.RR {
+ answers := []dns.RR{}
+ for _, ip := range ips {
+ r := new(dns.A)
+ r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeA,
+ Class: dns.ClassINET, Ttl: 3600}
+ r.A = ip
+ answers = append(answers, r)
+ }
+ return answers
+}
+
+// aaaa takes a slice of net.IPs and returns a slice of AAAA RRs.
+func aaaa(zone string, ips []net.IP) []dns.RR {
+ answers := []dns.RR{}
+ for _, ip := range ips {
+ r := new(dns.AAAA)
+ r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeAAAA,
+ Class: dns.ClassINET, Ttl: 3600}
+ r.AAAA = ip
+ answers = append(answers, 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, names []string) []dns.RR {
+ answers := []dns.RR{}
+ for _, n := range names {
+ r := new(dns.PTR)
+ r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypePTR,
+ Class: dns.ClassINET, Ttl: 3600}
+ r.Ptr = dns.Fqdn(n)
+ answers = append(answers, r)
+ }
+ return answers
+}
diff --git a/plugin/hosts/hosts_test.go b/plugin/hosts/hosts_test.go
new file mode 100644
index 000000000..68b91b8c2
--- /dev/null
+++ b/plugin/hosts/hosts_test.go
@@ -0,0 +1,75 @@
+package hosts
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestLookupA(t *testing.T) {
+ h := Hosts{Next: test.ErrorHandler(), Hostsfile: &Hostsfile{expire: time.Now().Add(1 * time.Hour), Origins: []string{"."}}}
+ h.Parse(strings.NewReader(hostsExample))
+
+ ctx := context.TODO()
+
+ for _, tc := range hostsTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := h.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+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: "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: "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{},
+ },
+}
+
+const hostsExample = `
+127.0.0.1 localhost localhost.domain
+::1 localhost localhost.domain
+10.0.0.1 example.org`
diff --git a/plugin/hosts/hostsfile.go b/plugin/hosts/hostsfile.go
new file mode 100644
index 000000000..91e828099
--- /dev/null
+++ b/plugin/hosts/hostsfile.go
@@ -0,0 +1,193 @@
+// 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"
+)
+
+const cacheMaxAge = 5 * time.Second
+
+func parseLiteralIP(addr string) net.IP {
+ if i := strings.Index(addr, "%"); i >= 0 {
+ // discard ipv6 zone
+ addr = addr[0:i]
+ }
+
+ return net.ParseIP(addr)
+}
+
+func absDomainName(b string) string {
+ return plugin.Name(b).Normalize()
+}
+
+// Hostsfile contains known host entries.
+type Hostsfile struct {
+ sync.Mutex
+
+ // list of zones we are authoritive for
+ Origins []string
+
+ // Key for the list of literal IP addresses must be a host
+ // name. It would be part of DNS labels, a FQDN or an absolute
+ // FQDN.
+ // For now the key is converted to lower case for convenience.
+ byNameV4 map[string][]net.IP
+ byNameV6 map[string][]net.IP
+
+ // Key for the list of host names must be a literal IP address
+ // including IPv6 address with zone identifier.
+ // We don't support old-classful IP address notation.
+ byAddr map[string][]string
+
+ expire time.Time
+ path string
+ mtime time.Time
+ size int64
+}
+
+// ReadHosts determines if the cached data needs to be updated based on the size and modification time of the hostsfile.
+func (h *Hostsfile) ReadHosts() {
+ now := time.Now()
+
+ if now.Before(h.expire) && len(h.byAddr) > 0 {
+ return
+ }
+ stat, err := os.Stat(h.path)
+ if err == nil && h.mtime.Equal(stat.ModTime()) && h.size == stat.Size() {
+ h.expire = now.Add(cacheMaxAge)
+ return
+ }
+
+ var file *os.File
+ if file, _ = os.Open(h.path); file == nil {
+ return
+ }
+ defer file.Close()
+
+ h.Parse(file)
+
+ // Update the data cache.
+ h.expire = now.Add(cacheMaxAge)
+ h.mtime = stat.ModTime()
+ h.size = stat.Size()
+}
+
+// Parse reads the hostsfile and populates the byName and byAddr maps.
+func (h *Hostsfile) Parse(file io.Reader) {
+ hsv4 := make(map[string][]net.IP)
+ hsv6 := make(map[string][]net.IP)
+ is := make(map[string][]string)
+
+ scanner := bufio.NewScanner(file)
+ 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 := parseLiteralIP(string(f[0]))
+ if addr == nil {
+ continue
+ }
+ ver := ipVersion(string(f[0]))
+ for i := 1; i < len(f); i++ {
+ name := absDomainName(string(f[i]))
+ if plugin.Zones(h.Origins).Matches(name) == "" {
+ // name is not in Origins
+ continue
+ }
+ switch ver {
+ case 4:
+ hsv4[name] = append(hsv4[name], addr)
+ case 6:
+ hsv6[name] = append(hsv6[name], addr)
+ default:
+ continue
+ }
+ is[addr.String()] = append(is[addr.String()], name)
+ }
+ }
+ h.byNameV4 = hsv4
+ h.byNameV6 = hsv6
+ h.byAddr = is
+}
+
+// ipVersion returns what IP version was used textually
+func ipVersion(s string) int {
+ for i := 0; i < len(s); i++ {
+ switch s[i] {
+ case '.':
+ return 4
+ case ':':
+ return 6
+ }
+ }
+ return 0
+}
+
+// LookupStaticHostV4 looks up the IPv4 addresses for the given host from the hosts file.
+func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP {
+ h.Lock()
+ defer h.Unlock()
+ h.ReadHosts()
+ if len(h.byNameV4) != 0 {
+ if ips, ok := h.byNameV4[absDomainName(host)]; ok {
+ ipsCp := make([]net.IP, len(ips))
+ copy(ipsCp, ips)
+ return ipsCp
+ }
+ }
+ return nil
+}
+
+// LookupStaticHostV6 looks up the IPv6 addresses for the given host from the hosts file.
+func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP {
+ h.Lock()
+ defer h.Unlock()
+ h.ReadHosts()
+ if len(h.byNameV6) != 0 {
+ if ips, ok := h.byNameV6[absDomainName(host)]; ok {
+ ipsCp := make([]net.IP, len(ips))
+ copy(ipsCp, ips)
+ return ipsCp
+ }
+ }
+ return nil
+}
+
+// LookupStaticAddr looks up the hosts for the given address from the hosts file.
+func (h *Hostsfile) LookupStaticAddr(addr string) []string {
+ h.Lock()
+ defer h.Unlock()
+ h.ReadHosts()
+ addr = parseLiteralIP(addr).String()
+ if addr == "" {
+ return nil
+ }
+ if len(h.byAddr) != 0 {
+ if hosts, ok := h.byAddr[addr]; ok {
+ hostsCp := make([]string, len(hosts))
+ copy(hostsCp, hosts)
+ return hostsCp
+ }
+ }
+ return nil
+}
diff --git a/plugin/hosts/hostsfile_test.go b/plugin/hosts/hostsfile_test.go
new file mode 100644
index 000000000..65841fa42
--- /dev/null
+++ b/plugin/hosts/hostsfile_test.go
@@ -0,0 +1,239 @@
+// 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"
+ "time"
+)
+
+func testHostsfile(file string) *Hostsfile {
+ h := &Hostsfile{expire: time.Now().Add(1 * time.Hour), Origins: []string{"."}}
+ 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.
+ #
+ # The literal IPv4 address parser in the net package is a relaxed
+ # one. It may accept a literal IPv4 address in dotted-decimal notation
+ # with leading zeros such as "001.2.003.4".
+
+ # internet address and host name
+ 127.0.0.1 localhost # inline comment separated by tab
+ 127.000.000.002 localhost # inline comment separated by space
+
+ # internet address, host name and aliases
+ 127.000.000.003 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, absDomainName(ent.in), 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] = absDomainName(ent.out[i])
+ }
+ 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/plugin/hosts/setup.go b/plugin/hosts/setup.go
new file mode 100644
index 000000000..c7c0c728a
--- /dev/null
+++ b/plugin/hosts/setup.go
@@ -0,0 +1,88 @@
+package hosts
+
+import (
+ "log"
+ "os"
+ "path"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("hosts", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ h, err := hostsParse(c)
+ if err != nil {
+ return plugin.Error("hosts", err)
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ h.Next = next
+ return h
+ })
+
+ return nil
+}
+
+func hostsParse(c *caddy.Controller) (Hosts, error) {
+ var h = Hosts{
+ Hostsfile: &Hostsfile{path: "/etc/hosts"},
+ }
+ defer h.ReadHosts()
+
+ config := dnsserver.GetConfig(c)
+
+ for c.Next() {
+ args := c.RemainingArgs()
+ if len(args) >= 1 {
+ h.path = args[0]
+ args = args[1:]
+
+ if !path.IsAbs(h.path) && config.Root != "" {
+ h.path = path.Join(config.Root, h.path)
+ }
+ _, err := os.Stat(h.path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ log.Printf("[WARNING] File does not exist: %s", h.path)
+ } else {
+ return h, c.Errf("unable to access hosts file '%s': %v", h.path, err)
+ }
+ }
+ }
+
+ origins := make([]string, len(c.ServerBlockKeys))
+ copy(origins, c.ServerBlockKeys)
+ if len(args) > 0 {
+ origins = args
+ }
+
+ for i := range origins {
+ origins[i] = plugin.Host(origins[i]).Normalize()
+ }
+ h.Origins = origins
+
+ for c.NextBlock() {
+ switch c.Val() {
+ case "fallthrough":
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ h.Fallthrough = true
+ continue
+ }
+ return h, c.ArgErr()
+ default:
+ return h, c.Errf("unknown property '%s'", c.Val())
+ }
+ }
+ }
+ return h, nil
+}
diff --git a/plugin/hosts/setup_test.go b/plugin/hosts/setup_test.go
new file mode 100644
index 000000000..a4c95b1c6
--- /dev/null
+++ b/plugin/hosts/setup_test.go
@@ -0,0 +1,86 @@
+package hosts
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestHostsParse(t *testing.T) {
+ tests := []struct {
+ inputFileRules string
+ shouldErr bool
+ expectedPath string
+ expectedOrigins []string
+ expectedFallthrough bool
+ }{
+ {
+ `hosts
+`,
+ false, "/etc/hosts", nil, false,
+ },
+ {
+ `hosts /tmp`,
+ false, "/tmp", nil, false,
+ },
+ {
+ `hosts /etc/hosts miek.nl.`,
+ false, "/etc/hosts", []string{"miek.nl."}, false,
+ },
+ {
+ `hosts /etc/hosts miek.nl. pun.gent.`,
+ false, "/etc/hosts", []string{"miek.nl.", "pun.gent."}, false,
+ },
+ {
+ `hosts {
+ fallthrough
+ }`,
+ false, "/etc/hosts", nil, true,
+ },
+ {
+ `hosts /tmp {
+ fallthrough
+ }`,
+ false, "/tmp", nil, true,
+ },
+ {
+ `hosts /etc/hosts miek.nl. {
+ fallthrough
+ }`,
+ false, "/etc/hosts", []string{"miek.nl."}, true,
+ },
+ {
+ `hosts /etc/hosts miek.nl 10.0.0.9/8 {
+ fallthrough
+ }`,
+ false, "/etc/hosts", []string{"miek.nl.", "10.in-addr.arpa."}, true,
+ },
+ }
+
+ 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.Fallthrough != test.expectedFallthrough {
+ t.Fatalf("Test %d expected fallthrough of %v, got %v", i, test.expectedFallthrough, h.Fallthrough)
+ }
+ 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])
+ }
+ }
+ }
+ }
+}
diff --git a/plugin/kubernetes/DEV-README.md b/plugin/kubernetes/DEV-README.md
new file mode 100644
index 000000000..4f652b578
--- /dev/null
+++ b/plugin/kubernetes/DEV-README.md
@@ -0,0 +1,43 @@
+# Basic Setup for Development and Testing
+
+## Launch Kubernetes
+
+To run the tests, you'll need a private, live Kubernetes cluster. If you don't have one,
+you can try out [minikube](https://github.com/kubernetes/minikube), which is
+also available via Homebrew for OS X users.
+
+## Configure Test Data
+
+The test data is all in [this manifest](https://github.com/coredns/coredns/blob/master/.travis/kubernetes/dns-test.yaml)
+and you can load it with `kubectl apply -f`. It will create a couple namespaces and some services.
+For the tests to pass, you should not create anything else in the cluster.
+
+## Proxy the API Server
+
+Assuming your Kuberentes API server isn't running on http://localhost:8080, you will need to proxy from that
+port to your cluster. You can do this with `kubectl proxy --port 8080`.
+
+## Run CoreDNS Kubernetes Tests
+
+Now you can run the tests locally, for example:
+
+~~~
+$ cd $GOPATH/src/github.com/coredns/coredns/test
+$ go test -v -tags k8s
+~~~
+
+# Implementation Notes/Ideas
+
+* Additional features:
+ * Implement IP selection and ordering (internal/external). Related to
+ wildcards and SkyDNS use of CNAMES.
+ * Expose arbitrary kubernetes repository data as TXT records?
+* DNS Correctness
+ * Do we need to generate synthetic zone records for namespaces?
+ * Do we need to generate synthetic zone records for the skydns synthetic zones?
+* Test cases
+ * Test with CoreDNS caching. CoreDNS caching for DNS response is working
+ using the `cache` directive. Tested working using 20s cache timeout
+ and A-record queries. Automate testing with cache in place.
+ * Automate CoreDNS performance tests. Initially for zone files, and for
+ pre-loaded k8s API cache. With and without CoreDNS response caching.
diff --git a/plugin/kubernetes/README.md b/plugin/kubernetes/README.md
new file mode 100644
index 000000000..387f1cf75
--- /dev/null
+++ b/plugin/kubernetes/README.md
@@ -0,0 +1,167 @@
+# kubernetes
+
+The *kubernetes* plugin enables the reading zone data from a Kubernetes cluster. It 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 of 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](http://blog.kubernetes.io/2017/04/configuring-private-dns-zones-upstream-nameservers-kubernetes.html)
+are implemented via the *proxy* plugin.
+
+## Syntax
+
+~~~
+kubernetes [ZONES...]
+~~~
+
+With only the directive 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...] {
+ resyncperiod DURATION
+ endpoint URL
+ tls CERT KEY CACERT
+ namespaces NAMESPACE...
+ labels EXPRESSION
+ pods POD-MODE
+ upstream ADDRESS...
+ ttl TTL
+ fallthrough
+}
+```
+* `resyncperiod` specifies the Kubernetes data API **DURATION** period.
+* `endpoint` specifies the **URL** for a remove k8s API endpoint.
+ If omitted, it will connect to k8s in-cluster using the cluster service account.
+ 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.
+* `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).
+* `namespaces` **NAMESPACE [NAMESPACE...]**, exposed only the k8s namespaces listed.
+ If this option is omitted all namespaces are exposed
+* `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](http://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 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.
+
+* `upstream` **ADDRESS [ADDRESS...]** defines the upstream resolvers used for resolving services
+ that point to external hosts (External Services). **ADDRESS** can be an ip, an ip:port, or a path
+ to a file structured like resolv.conf.
+* `ttl` allows you to set a custom TTL for responses. The default (and allowed minimum) is to use
+ 5 seconds, the maximum is capped at 3600 seconds.
+* `fallthrough` If a query for a record in the cluster zone 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.
+
+## Examples
+
+Handle all queries in the `cluster.local` zone. Connect to Kubernetes in-cluster.
+Also handle all `PTR` requests for `10.0.0.0/16` . Verify the existence of pods when answering pod
+requests. Resolve upstream records against `10.102.3.10`. Note we show the entire server block
+here:
+
+~~~ txt
+10.0.0.0/16 cluster.local {
+ kubernetes {
+ pods verified
+ upstream 10.102.3.10:53
+ }
+}
+~~~
+
+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
+}
+~~~
+
+Here we use the *proxy* plugin to implement stubDomains that forwards `example.org` and
+`example.com` to another nameserver.
+
+~~~ txt
+cluster.local {
+ kubernetes {
+ endpoint https://k8s-endpoint:8443
+ tls cert key cacert
+ }
+}
+example.org {
+ proxy . 8.8.8.8:53
+}
+example.com {
+ proxy . 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.
+
+ cluster.local {
+ autopath @kubernetes
+ kubernetes {
+ pods verified
+ }
+ }
+
+## Federation
+
+The *kubernetes* plugin can be used in conjunction with the *federation* plugin. Using this
+feature enables serving federated domains from the kubernetes clusters.
+
+ cluster.local {
+ federation {
+ fallthrough
+ prod prod.example.org
+ staging staging.example.org
+
+ }
+ kubernetes
+ }
+
+
+## Wildcards
+
+Some query labels accept a wildcard value to match any value. If a label is a valid wildcard (\*,
+or the word "any"), then that label will match all values. The labels that accept wildcards are:
+
+ * _service_ in an `A` record request: _service_.namespace.svc.zone.
+ * e.g. `*.ns.svc.myzone.local`
+ * _namespace_ in an `A` record request: service._namespace_.svc.zone.
+ * e.g. `nginx.*.svc.myzone.local`
+ * _port and/or protocol_ in an `SRV` request: __port_.__protocol_.service.namespace.svc.zone.
+ * e.g. `_http.*.service.ns.svc.`
+ * multiple wild cards are allowed in a single query.
+ * e.g. `A` Request `*.*.svc.zone.` or `SRV` request `*.*.*.*.svc.zone.`
diff --git a/plugin/kubernetes/apiproxy.go b/plugin/kubernetes/apiproxy.go
new file mode 100644
index 000000000..3e185f898
--- /dev/null
+++ b/plugin/kubernetes/apiproxy.go
@@ -0,0 +1,76 @@
+package kubernetes
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "net/http"
+
+ "github.com/coredns/coredns/plugin/pkg/healthcheck"
+)
+
+type proxyHandler struct {
+ healthcheck.HealthCheck
+}
+
+type apiProxy struct {
+ http.Server
+ listener net.Listener
+ handler proxyHandler
+}
+
+func (p *proxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ upstream := p.Select()
+ network := "tcp"
+ if upstream.Network != "" {
+ network = upstream.Network
+ }
+ address := upstream.Name
+ d, err := net.Dial(network, address)
+ if err != nil {
+ log.Printf("[ERROR] Unable to establish connection to upstream %s://%s: %s", network, address, err)
+ http.Error(w, fmt.Sprintf("Unable to establish connection to upstream %s://%s: %s", network, address, err), 500)
+ return
+ }
+ hj, ok := w.(http.Hijacker)
+ if !ok {
+ log.Printf("[ERROR] Unable to establish connection: no hijacker")
+ http.Error(w, "Unable to establish connection: no hijacker", 500)
+ return
+ }
+ nc, _, err := hj.Hijack()
+ if err != nil {
+ log.Printf("[ERROR] Unable to hijack connection: %s", err)
+ http.Error(w, fmt.Sprintf("Unable to hijack connection: %s", err), 500)
+ return
+ }
+ defer nc.Close()
+ defer d.Close()
+
+ err = r.Write(d)
+ if err != nil {
+ log.Printf("[ERROR] Unable to copy connection to upstream %s://%s: %s", network, address, err)
+ http.Error(w, fmt.Sprintf("Unable to copy connection to upstream %s://%s: %s", network, address, err), 500)
+ return
+ }
+
+ errChan := make(chan error, 2)
+ cp := func(dst io.Writer, src io.Reader) {
+ _, err := io.Copy(dst, src)
+ errChan <- err
+ }
+ go cp(d, nc)
+ go cp(nc, d)
+ <-errChan
+}
+
+func (p *apiProxy) Run() {
+ p.handler.Start()
+ p.Serve(p.listener)
+}
+
+func (p *apiProxy) Stop() {
+ p.handler.Stop()
+ p.listener.Close()
+}
diff --git a/plugin/kubernetes/autopath.go b/plugin/kubernetes/autopath.go
new file mode 100644
index 000000000..f758869f1
--- /dev/null
+++ b/plugin/kubernetes/autopath.go
@@ -0,0 +1,53 @@
+package kubernetes
+
+import (
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "k8s.io/client-go/1.5/pkg/api"
+)
+
+// 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 authoriative for and thus if we want autopath.
+ zone := plugin.Zones(k.Zones).Matches(state.Name())
+ if zone == "" {
+ 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, "") // sentinal
+ return search
+}
+
+// podWithIP return the api.Pod for source IP ip. It returns nil if nothing can be found.
+func (k *Kubernetes) podWithIP(ip string) (p *api.Pod) {
+ objList := k.APIConn.PodIndex(ip)
+ for _, o := range objList {
+ p, ok := o.(*api.Pod)
+ if !ok {
+ return nil
+ }
+ return p
+ }
+ return nil
+}
diff --git a/plugin/kubernetes/controller.go b/plugin/kubernetes/controller.go
new file mode 100644
index 000000000..b809264e1
--- /dev/null
+++ b/plugin/kubernetes/controller.go
@@ -0,0 +1,399 @@
+package kubernetes
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "sync"
+ "time"
+
+ "k8s.io/client-go/1.5/kubernetes"
+ "k8s.io/client-go/1.5/pkg/api"
+ unversionedapi "k8s.io/client-go/1.5/pkg/api/unversioned"
+ "k8s.io/client-go/1.5/pkg/api/v1"
+ "k8s.io/client-go/1.5/pkg/labels"
+ "k8s.io/client-go/1.5/pkg/runtime"
+ "k8s.io/client-go/1.5/pkg/watch"
+ "k8s.io/client-go/1.5/tools/cache"
+)
+
+var (
+ namespace = api.NamespaceAll
+)
+
+// storeToNamespaceLister makes a Store that lists Namespaces.
+type storeToNamespaceLister struct {
+ cache.Store
+}
+
+const podIPIndex = "PodIP"
+
+// List lists all Namespaces in the store.
+func (s *storeToNamespaceLister) List() (ns api.NamespaceList, err error) {
+ for _, m := range s.Store.List() {
+ ns.Items = append(ns.Items, *(m.(*api.Namespace)))
+ }
+ return ns, nil
+}
+
+type dnsController interface {
+ ServiceList() []*api.Service
+ PodIndex(string) []interface{}
+ EndpointsList() api.EndpointsList
+
+ GetNodeByName(string) (api.Node, error)
+
+ Run()
+ Stop() error
+}
+
+type dnsControl struct {
+ client *kubernetes.Clientset
+
+ selector *labels.Selector
+
+ svcController *cache.Controller
+ podController *cache.Controller
+ nsController *cache.Controller
+ epController *cache.Controller
+
+ svcLister cache.StoreToServiceLister
+ podLister cache.StoreToPodLister
+ nsLister storeToNamespaceLister
+ epLister cache.StoreToEndpointsLister
+
+ // 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{}
+}
+
+type dnsControlOpts struct {
+ initPodCache bool
+ resyncPeriod time.Duration
+ // Label handling.
+ labelSelector *unversionedapi.LabelSelector
+ selector *labels.Selector
+}
+
+// newDNSController creates a controller for CoreDNS.
+func newdnsController(kubeClient *kubernetes.Clientset, opts dnsControlOpts) *dnsControl {
+ dns := dnsControl{
+ client: kubeClient,
+ selector: opts.selector,
+ stopCh: make(chan struct{}),
+ }
+
+ dns.svcLister.Indexer, dns.svcController = cache.NewIndexerInformer(
+ &cache.ListWatch{
+ ListFunc: serviceListFunc(dns.client, namespace, dns.selector),
+ WatchFunc: serviceWatchFunc(dns.client, namespace, dns.selector),
+ },
+ &api.Service{},
+ opts.resyncPeriod,
+ cache.ResourceEventHandlerFuncs{},
+ cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
+
+ if opts.initPodCache {
+ dns.podLister.Indexer, dns.podController = cache.NewIndexerInformer(
+ &cache.ListWatch{
+ ListFunc: podListFunc(dns.client, namespace, dns.selector),
+ WatchFunc: podWatchFunc(dns.client, namespace, dns.selector),
+ },
+ &api.Pod{}, // TODO replace with a lighter-weight custom struct
+ opts.resyncPeriod,
+ cache.ResourceEventHandlerFuncs{},
+ cache.Indexers{podIPIndex: podIPIndexFunc})
+ }
+
+ dns.nsLister.Store, dns.nsController = cache.NewInformer(
+ &cache.ListWatch{
+ ListFunc: namespaceListFunc(dns.client, dns.selector),
+ WatchFunc: namespaceWatchFunc(dns.client, dns.selector),
+ },
+ &api.Namespace{},
+ opts.resyncPeriod,
+ cache.ResourceEventHandlerFuncs{})
+
+ dns.epLister.Store, dns.epController = cache.NewInformer(
+ &cache.ListWatch{
+ ListFunc: endpointsListFunc(dns.client, namespace, dns.selector),
+ WatchFunc: endpointsWatchFunc(dns.client, namespace, dns.selector),
+ },
+ &api.Endpoints{},
+ opts.resyncPeriod,
+ cache.ResourceEventHandlerFuncs{})
+
+ return &dns
+}
+
+func podIPIndexFunc(obj interface{}) ([]string, error) {
+ p, ok := obj.(*api.Pod)
+ if !ok {
+ return nil, errors.New("obj was not an *api.Pod")
+ }
+ return []string{p.Status.PodIP}, nil
+}
+
+func serviceListFunc(c *kubernetes.Clientset, ns string, s *labels.Selector) func(api.ListOptions) (runtime.Object, error) {
+ return func(opts api.ListOptions) (runtime.Object, error) {
+ if s != nil {
+ opts.LabelSelector = *s
+ }
+ listV1, err := c.Core().Services(ns).List(opts)
+
+ if err != nil {
+ return nil, err
+ }
+ var listAPI api.ServiceList
+ err = v1.Convert_v1_ServiceList_To_api_ServiceList(listV1, &listAPI, nil)
+ if err != nil {
+ return nil, err
+ }
+ return &listAPI, err
+ }
+}
+
+func podListFunc(c *kubernetes.Clientset, ns string, s *labels.Selector) func(api.ListOptions) (runtime.Object, error) {
+ return func(opts api.ListOptions) (runtime.Object, error) {
+ if s != nil {
+ opts.LabelSelector = *s
+ }
+ listV1, err := c.Core().Pods(ns).List(opts)
+
+ if err != nil {
+ return nil, err
+ }
+ var listAPI api.PodList
+ err = v1.Convert_v1_PodList_To_api_PodList(listV1, &listAPI, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return &listAPI, err
+ }
+}
+
+func v1ToAPIFilter(in watch.Event) (out watch.Event, keep bool) {
+ if in.Type == watch.Error {
+ return in, true
+ }
+
+ switch v1Obj := in.Object.(type) {
+ case *v1.Service:
+ var apiObj api.Service
+ err := v1.Convert_v1_Service_To_api_Service(v1Obj, &apiObj, nil)
+ if err != nil {
+ log.Printf("[ERROR] Could not convert v1.Service: %s", err)
+ return in, true
+ }
+ return watch.Event{Type: in.Type, Object: &apiObj}, true
+ case *v1.Pod:
+ var apiObj api.Pod
+ err := v1.Convert_v1_Pod_To_api_Pod(v1Obj, &apiObj, nil)
+ if err != nil {
+ log.Printf("[ERROR] Could not convert v1.Pod: %s", err)
+ return in, true
+ }
+ return watch.Event{Type: in.Type, Object: &apiObj}, true
+ case *v1.Namespace:
+ var apiObj api.Namespace
+ err := v1.Convert_v1_Namespace_To_api_Namespace(v1Obj, &apiObj, nil)
+ if err != nil {
+ log.Printf("[ERROR] Could not convert v1.Namespace: %s", err)
+ return in, true
+ }
+ return watch.Event{Type: in.Type, Object: &apiObj}, true
+ case *v1.Endpoints:
+ var apiObj api.Endpoints
+ err := v1.Convert_v1_Endpoints_To_api_Endpoints(v1Obj, &apiObj, nil)
+ if err != nil {
+ log.Printf("[ERROR] Could not convert v1.Endpoint: %s", err)
+ return in, true
+ }
+ return watch.Event{Type: in.Type, Object: &apiObj}, true
+ }
+
+ log.Printf("[WARN] Unhandled v1 type in event: %v", in)
+ return in, true
+}
+
+func serviceWatchFunc(c *kubernetes.Clientset, ns string, s *labels.Selector) func(options api.ListOptions) (watch.Interface, error) {
+ return func(options api.ListOptions) (watch.Interface, error) {
+ if s != nil {
+ options.LabelSelector = *s
+ }
+ w, err := c.Core().Services(ns).Watch(options)
+ if err != nil {
+ return nil, err
+ }
+ return watch.Filter(w, v1ToAPIFilter), nil
+ }
+}
+
+func podWatchFunc(c *kubernetes.Clientset, ns string, s *labels.Selector) func(options api.ListOptions) (watch.Interface, error) {
+ return func(options api.ListOptions) (watch.Interface, error) {
+ if s != nil {
+ options.LabelSelector = *s
+ }
+ w, err := c.Core().Pods(ns).Watch(options)
+
+ if err != nil {
+ return nil, err
+ }
+ return watch.Filter(w, v1ToAPIFilter), nil
+ }
+}
+
+func namespaceListFunc(c *kubernetes.Clientset, s *labels.Selector) func(api.ListOptions) (runtime.Object, error) {
+ return func(opts api.ListOptions) (runtime.Object, error) {
+ if s != nil {
+ opts.LabelSelector = *s
+ }
+ listV1, err := c.Core().Namespaces().List(opts)
+ if err != nil {
+ return nil, err
+ }
+ var listAPI api.NamespaceList
+ err = v1.Convert_v1_NamespaceList_To_api_NamespaceList(listV1, &listAPI, nil)
+ if err != nil {
+ return nil, err
+ }
+ return &listAPI, err
+ }
+}
+
+func namespaceWatchFunc(c *kubernetes.Clientset, s *labels.Selector) func(options api.ListOptions) (watch.Interface, error) {
+ return func(options api.ListOptions) (watch.Interface, error) {
+ if s != nil {
+ options.LabelSelector = *s
+ }
+ w, err := c.Core().Namespaces().Watch(options)
+ if err != nil {
+ return nil, err
+ }
+ return watch.Filter(w, v1ToAPIFilter), nil
+ }
+}
+
+func endpointsListFunc(c *kubernetes.Clientset, ns string, s *labels.Selector) func(api.ListOptions) (runtime.Object, error) {
+ return func(opts api.ListOptions) (runtime.Object, error) {
+ if s != nil {
+ opts.LabelSelector = *s
+ }
+ listV1, err := c.Core().Endpoints(ns).List(opts)
+
+ if err != nil {
+ return nil, err
+ }
+ var listAPI api.EndpointsList
+ err = v1.Convert_v1_EndpointsList_To_api_EndpointsList(listV1, &listAPI, nil)
+ if err != nil {
+ return nil, err
+ }
+ return &listAPI, err
+ }
+}
+
+func endpointsWatchFunc(c *kubernetes.Clientset, ns string, s *labels.Selector) func(options api.ListOptions) (watch.Interface, error) {
+ return func(options api.ListOptions) (watch.Interface, error) {
+ if s != nil {
+ options.LabelSelector = *s
+ }
+ w, err := c.Core().Endpoints(ns).Watch(options)
+ if err != nil {
+ return nil, err
+ }
+ return watch.Filter(w, v1ToAPIFilter), nil
+ }
+}
+
+func (dns *dnsControl) controllersInSync() bool {
+ hs := dns.svcController.HasSynced() &&
+ dns.nsController.HasSynced() &&
+ dns.epController.HasSynced()
+
+ if dns.podController != nil {
+ hs = hs && dns.podController.HasSynced()
+ }
+
+ return hs
+}
+
+// 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)
+ go dns.nsController.Run(dns.stopCh)
+ go dns.epController.Run(dns.stopCh)
+ if dns.podController != nil {
+ go dns.podController.Run(dns.stopCh)
+ }
+ <-dns.stopCh
+}
+
+func (dns *dnsControl) NamespaceList() *api.NamespaceList {
+ nsList, err := dns.nsLister.List()
+ if err != nil {
+ return &api.NamespaceList{}
+ }
+
+ return &nsList
+}
+
+func (dns *dnsControl) ServiceList() []*api.Service {
+ svcs, err := dns.svcLister.List(labels.Everything())
+ if err != nil {
+ return []*api.Service{}
+ }
+
+ return svcs
+}
+
+func (dns *dnsControl) PodIndex(ip string) []interface{} {
+ pods, err := dns.podLister.Indexer.ByIndex(podIPIndex, ip)
+ if err != nil {
+ return nil
+ }
+
+ return pods
+}
+
+func (dns *dnsControl) EndpointsList() api.EndpointsList {
+ epl, err := dns.epLister.List()
+ if err != nil {
+ return api.EndpointsList{}
+ }
+
+ return epl
+}
+
+func (dns *dnsControl) GetNodeByName(name string) (api.Node, error) {
+ v1node, err := dns.client.Core().Nodes().Get(name)
+ if err != nil {
+ return api.Node{}, err
+ }
+ var apinode api.Node
+ err = v1.Convert_v1_Node_To_api_Node(v1node, &apinode, nil)
+ if err != nil {
+ return api.Node{}, err
+ }
+ return apinode, nil
+}
diff --git a/plugin/kubernetes/federation.go b/plugin/kubernetes/federation.go
new file mode 100644
index 000000000..df6ae948b
--- /dev/null
+++ b/plugin/kubernetes/federation.go
@@ -0,0 +1,45 @@
+package kubernetes
+
+import (
+ "github.com/coredns/coredns/plugin/etcd/msg"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/request"
+)
+
+// The federation node.Labels keys used.
+const (
+ // TODO: Do not hardcode these labels. Pull them out of the API instead.
+ //
+ // We can get them via ....
+ // import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ // metav1.LabelZoneFailureDomain
+ // metav1.LabelZoneRegion
+ //
+ // But importing above breaks coredns with flag collision of 'log_dir'
+
+ LabelZone = "failure-domain.beta.kubernetes.io/zone"
+ LabelRegion = "failure-domain.beta.kubernetes.io/region"
+)
+
+// Federations is used from the federations plugin to return the service that should be
+// returned as a CNAME for federation(s) to work.
+func (k *Kubernetes) Federations(state request.Request, fname, fzone string) (msg.Service, error) {
+ nodeName := k.localNodeName()
+ node, err := k.APIConn.GetNodeByName(nodeName)
+ if err != nil {
+ return msg.Service{}, err
+ }
+ r, err := parseRequest(state)
+ if err != nil {
+ return msg.Service{}, err
+ }
+
+ lz := node.Labels[LabelZone]
+ lr := node.Labels[LabelRegion]
+
+ if r.endpoint == "" {
+ return msg.Service{Host: dnsutil.Join([]string{r.service, r.namespace, fname, r.podOrSvc, lz, lr, fzone})}, nil
+ }
+
+ return msg.Service{Host: dnsutil.Join([]string{r.endpoint, r.service, r.namespace, fname, r.podOrSvc, lz, lr, fzone})}, nil
+}
diff --git a/plugin/kubernetes/handler.go b/plugin/kubernetes/handler.go
new file mode 100644
index 000000000..9dc435111
--- /dev/null
+++ b/plugin/kubernetes/handler.go
@@ -0,0 +1,86 @@
+package kubernetes
+
+import (
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// 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}
+
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
+
+ zone := plugin.Zones(k.Zones).Matches(state.Name())
+ if zone == "" {
+ return plugin.NextOrFailure(k.Name(), k.Next, ctx, w, r)
+ }
+
+ state.Zone = zone
+
+ var (
+ records []dns.RR
+ extra []dns.RR
+ err error
+ )
+
+ switch state.Type() {
+ case "A":
+ records, err = plugin.A(&k, zone, state, nil, plugin.Options{})
+ case "AAAA":
+ records, err = plugin.AAAA(&k, zone, state, nil, plugin.Options{})
+ case "TXT":
+ records, err = plugin.TXT(&k, zone, state, plugin.Options{})
+ case "CNAME":
+ records, err = plugin.CNAME(&k, zone, state, plugin.Options{})
+ case "PTR":
+ records, err = plugin.PTR(&k, zone, state, plugin.Options{})
+ case "MX":
+ records, extra, err = plugin.MX(&k, zone, state, plugin.Options{})
+ case "SRV":
+ records, extra, err = plugin.SRV(&k, zone, state, plugin.Options{})
+ case "SOA":
+ records, err = plugin.SOA(&k, zone, state, plugin.Options{})
+ case "NS":
+ if state.Name() == zone {
+ records, extra, err = plugin.NS(&k, zone, state, plugin.Options{})
+ break
+ }
+ fallthrough
+ default:
+ // Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN
+ _, err = plugin.A(&k, zone, state, nil, plugin.Options{})
+ }
+
+ if k.IsNameError(err) {
+ if k.Fallthrough {
+ return plugin.NextOrFailure(k.Name(), k.Next, ctx, w, r)
+ }
+ return plugin.BackendError(&k, zone, dns.RcodeNameError, state, nil /* err */, plugin.Options{})
+ }
+ if err != nil {
+ return dns.RcodeServerFailure, err
+ }
+
+ if len(records) == 0 {
+ return plugin.BackendError(&k, zone, dns.RcodeSuccess, state, nil, plugin.Options{})
+ }
+
+ m.Answer = append(m.Answer, records...)
+ m.Extra = append(m.Extra, extra...)
+
+ m = dnsutil.Dedup(m)
+ state.SizeAndDo(m)
+ m, _ = state.Scrub(m)
+ w.WriteMsg(m)
+ return dns.RcodeSuccess, nil
+}
+
+// Name implements the Handler interface.
+func (k Kubernetes) Name() string { return "kubernetes" }
diff --git a/plugin/kubernetes/handler_pod_disabled_test.go b/plugin/kubernetes/handler_pod_disabled_test.go
new file mode 100644
index 000000000..4c6e15710
--- /dev/null
+++ b/plugin/kubernetes/handler_pod_disabled_test.go
@@ -0,0 +1,61 @@
+package kubernetes
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+var podModeDisabledCases = []test.Case{
+ {
+ Qname: "10-240-0-1.podns.pod.cluster.local.", Qtype: dns.TypeA,
+ Rcode: dns.RcodeNameError,
+ Error: errPodsDisabled,
+ Ns: []dns.RR{
+ test.SOA("cluster.local. 300 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 60"),
+ },
+ },
+ {
+ Qname: "172-0-0-2.podns.pod.cluster.local.", Qtype: dns.TypeA,
+ Rcode: dns.RcodeNameError,
+ Error: errPodsDisabled,
+ Ns: []dns.RR{
+ test.SOA("cluster.local. 300 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 60"),
+ },
+ },
+}
+
+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 := dnsrecorder.New(&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)
+ }
+
+ test.SortAndCheck(t, resp, tc)
+ }
+}
diff --git a/plugin/kubernetes/handler_pod_insecure_test.go b/plugin/kubernetes/handler_pod_insecure_test.go
new file mode 100644
index 000000000..b2df8a504
--- /dev/null
+++ b/plugin/kubernetes/handler_pod_insecure_test.go
@@ -0,0 +1,59 @@
+package kubernetes
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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. 0 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. 0 IN A 172.0.0.2"),
+ },
+ },
+}
+
+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 := dnsrecorder.New(&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)
+ }
+
+ test.SortAndCheck(t, resp, tc)
+ }
+}
diff --git a/plugin/kubernetes/handler_pod_verified_test.go b/plugin/kubernetes/handler_pod_verified_test.go
new file mode 100644
index 000000000..ea585cc6a
--- /dev/null
+++ b/plugin/kubernetes/handler_pod_verified_test.go
@@ -0,0 +1,59 @@
+package kubernetes
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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. 0 IN A 10.240.0.1"),
+ },
+ },
+ {
+ Qname: "172-0-0-2.podns.pod.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. 1499347823 7200 1800 86400 60"),
+ },
+ },
+}
+
+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 := dnsrecorder.New(&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)
+ }
+
+ test.SortAndCheck(t, resp, tc)
+ }
+}
diff --git a/plugin/kubernetes/handler_test.go b/plugin/kubernetes/handler_test.go
new file mode 100644
index 000000000..5413f5b4c
--- /dev/null
+++ b/plugin/kubernetes/handler_test.go
@@ -0,0 +1,347 @@
+package kubernetes
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+ "k8s.io/client-go/1.5/pkg/api"
+)
+
+var dnsTestCases = []test.Case{
+ // 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"),
+ },
+ },
+ // A Service (wildcard)
+ {
+ Qname: "svc1.*.svc.cluster.local.", Qtype: dns.TypeA,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{
+ test.A("svc1.*.svc.cluster.local. 5 IN A 10.0.0.1"),
+ },
+ },
+ {
+ Qname: "svc1.testns.svc.cluster.local.", Qtype: dns.TypeSRV,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{test.SRV("svc1.testns.svc.cluster.local. 303 IN SRV 0 100 80 svc1.testns.svc.cluster.local.")},
+ Extra: []dns.RR{test.A("svc1.testns.svc.cluster.local. 303 IN A 10.0.0.1")},
+ },
+ // SRV Service (wildcard)
+ {
+ Qname: "svc1.*.svc.cluster.local.", Qtype: dns.TypeSRV,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{test.SRV("svc1.*.svc.cluster.local. 303 IN SRV 0 100 80 svc1.testns.svc.cluster.local.")},
+ Extra: []dns.RR{test.A("svc1.testns.svc.cluster.local. 303 IN A 10.0.0.1")},
+ },
+ // SRV Service (wildcards)
+ {
+ Qname: "*.any.svc1.*.svc.cluster.local.", Qtype: dns.TypeSRV,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{test.SRV("*.any.svc1.*.svc.cluster.local. 303 IN SRV 0 100 80 svc1.testns.svc.cluster.local.")},
+ Extra: []dns.RR{test.A("svc1.testns.svc.cluster.local. 303 IN A 10.0.0.1")},
+ },
+ // A Service (wildcards)
+ {
+ Qname: "*.any.svc1.*.svc.cluster.local.", Qtype: dns.TypeA,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{
+ test.A("*.any.svc1.*.svc.cluster.local. 303 IN A 10.0.0.1"),
+ },
+ },
+ // SRV Service Not udp/tcp
+ {
+ Qname: "*._not-udp-or-tcp.svc1.testns.svc.cluster.local.", Qtype: dns.TypeSRV,
+ Rcode: dns.RcodeNameError,
+ Ns: []dns.RR{
+ test.SOA("cluster.local. 300 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 60"),
+ },
+ },
+ // 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. 303 IN SRV 0 100 80 svc1.testns.svc.cluster.local."),
+ },
+ Extra: []dns.RR{
+ test.A("svc1.testns.svc.cluster.local. 303 IN A 10.0.0.1"),
+ },
+ },
+ // A Service (Headless)
+ {
+ Qname: "hdls1.testns.svc.cluster.local.", Qtype: dns.TypeA,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{
+ test.A("hdls1.testns.svc.cluster.local. 303 IN A 172.0.0.2"),
+ test.A("hdls1.testns.svc.cluster.local. 303 IN A 172.0.0.3"),
+ },
+ },
+ // SRV Service (Headless)
+ {
+ 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. 303 IN SRV 0 50 80 172-0-0-2.hdls1.testns.svc.cluster.local."),
+ test.SRV("_http._tcp.hdls1.testns.svc.cluster.local. 303 IN SRV 0 50 80 172-0-0-3.hdls1.testns.svc.cluster.local."),
+ },
+ Extra: []dns.RR{
+ test.A("172-0-0-2.hdls1.testns.svc.cluster.local. 303 IN A 172.0.0.2"),
+ test.A("172-0-0-3.hdls1.testns.svc.cluster.local. 303 IN A 172.0.0.3"),
+ },
+ },
+ // CNAME External
+ {
+ Qname: "external.testns.svc.cluster.local.", Qtype: dns.TypeCNAME,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{
+ test.CNAME("external.testns.svc.cluster.local. 303 IN CNAME ext.interwebs.test."),
+ },
+ },
+ // AAAA Service (existing service)
+ {
+ Qname: "svc1.testns.svc.cluster.local.", Qtype: dns.TypeAAAA,
+ Rcode: dns.RcodeSuccess,
+ Ns: []dns.RR{
+ test.SOA("cluster.local. 300 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 60"),
+ },
+ },
+ // AAAA Service (non-existing service)
+ {
+ Qname: "svc0.testns.svc.cluster.local.", Qtype: dns.TypeAAAA,
+ Rcode: dns.RcodeNameError,
+ Ns: []dns.RR{
+ test.SOA("cluster.local. 300 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 60"),
+ },
+ },
+ // A Service (non-existing service)
+ {
+ Qname: "svc0.testns.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. 1499347823 7200 1800 86400 60"),
+ },
+ },
+ // TXT Schema
+ {
+ Qname: "dns-version.cluster.local.", Qtype: dns.TypeTXT,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{
+ test.TXT("dns-version.cluster.local 28800 IN TXT 1.0.1"),
+ },
+ },
+ // A Service (Headless) does not exist
+ {
+ Qname: "bogusendpoint.hdls1.testns.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. 1499347823 7200 1800 86400 60"),
+ },
+ },
+ // A Service does not exist
+ {
+ Qname: "bogusendpoint.svc0.testns.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. 1499347823 7200 1800 86400 60"),
+ },
+ },
+}
+
+func TestServeDNS(t *testing.T) {
+
+ k := New([]string{"cluster.local."})
+ k.APIConn = &APIConnServeTest{}
+ k.Next = test.NextHandler(dns.RcodeSuccess, nil)
+ ctx := context.TODO()
+
+ for i, tc := range dnsTestCases {
+ r := tc.Msg()
+
+ w := dnsrecorder.New(&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(t, resp)
+
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+type APIConnServeTest struct{}
+
+func (APIConnServeTest) Run() { return }
+func (APIConnServeTest) Stop() error { return nil }
+
+func (APIConnServeTest) PodIndex(string) []interface{} {
+ a := make([]interface{}, 1)
+ a[0] = &api.Pod{
+ ObjectMeta: api.ObjectMeta{
+ Namespace: "podns",
+ },
+ Status: api.PodStatus{
+ PodIP: "10.240.0.1", // Remote IP set in test.ResponseWriter
+ },
+ }
+ return a
+}
+
+func (APIConnServeTest) ServiceList() []*api.Service {
+ svcs := []*api.Service{
+ {
+ ObjectMeta: api.ObjectMeta{
+ Name: "svc1",
+ Namespace: "testns",
+ },
+ Spec: api.ServiceSpec{
+ ClusterIP: "10.0.0.1",
+ Ports: []api.ServicePort{{
+ Name: "http",
+ Protocol: "tcp",
+ Port: 80,
+ }},
+ },
+ },
+ {
+ ObjectMeta: api.ObjectMeta{
+ Name: "hdls1",
+ Namespace: "testns",
+ },
+ Spec: api.ServiceSpec{
+ ClusterIP: api.ClusterIPNone,
+ },
+ },
+ {
+ ObjectMeta: api.ObjectMeta{
+ Name: "external",
+ Namespace: "testns",
+ },
+ Spec: api.ServiceSpec{
+ ExternalName: "ext.interwebs.test",
+ Ports: []api.ServicePort{{
+ Name: "http",
+ Protocol: "tcp",
+ Port: 80,
+ }},
+ },
+ },
+ }
+ return svcs
+
+}
+
+func (APIConnServeTest) EndpointsList() api.EndpointsList {
+ n := "test.node.foo.bar"
+
+ return api.EndpointsList{
+ Items: []api.Endpoints{
+ {
+ Subsets: []api.EndpointSubset{
+ {
+ Addresses: []api.EndpointAddress{
+ {
+ IP: "172.0.0.1",
+ Hostname: "ep1a",
+ },
+ },
+ Ports: []api.EndpointPort{
+ {
+ Port: 80,
+ Protocol: "tcp",
+ Name: "http",
+ },
+ },
+ },
+ },
+ ObjectMeta: api.ObjectMeta{
+ Name: "svc1",
+ Namespace: "testns",
+ },
+ },
+ {
+ Subsets: []api.EndpointSubset{
+ {
+ Addresses: []api.EndpointAddress{
+ {
+ IP: "172.0.0.2",
+ },
+ },
+ Ports: []api.EndpointPort{
+ {
+ Port: 80,
+ Protocol: "tcp",
+ Name: "http",
+ },
+ },
+ },
+ },
+ ObjectMeta: api.ObjectMeta{
+ Name: "hdls1",
+ Namespace: "testns",
+ },
+ },
+ {
+ Subsets: []api.EndpointSubset{
+ {
+ Addresses: []api.EndpointAddress{
+ {
+ IP: "172.0.0.3",
+ },
+ },
+ Ports: []api.EndpointPort{
+ {
+ Port: 80,
+ Protocol: "tcp",
+ Name: "http",
+ },
+ },
+ },
+ },
+ ObjectMeta: api.ObjectMeta{
+ Name: "hdls1",
+ Namespace: "testns",
+ },
+ },
+ {
+ Subsets: []api.EndpointSubset{
+ {
+ Addresses: []api.EndpointAddress{
+ {
+ IP: "10.9.8.7",
+ NodeName: &n,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (APIConnServeTest) GetNodeByName(name string) (api.Node, error) {
+ return api.Node{
+ ObjectMeta: api.ObjectMeta{
+ Name: "test.node.foo.bar",
+ },
+ }, nil
+}
diff --git a/plugin/kubernetes/kubernetes.go b/plugin/kubernetes/kubernetes.go
new file mode 100644
index 000000000..90fcd6182
--- /dev/null
+++ b/plugin/kubernetes/kubernetes.go
@@ -0,0 +1,457 @@
+// Package kubernetes provides the kubernetes backend.
+package kubernetes
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/etcd/msg"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/pkg/healthcheck"
+ "github.com/coredns/coredns/plugin/proxy"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "k8s.io/client-go/1.5/kubernetes"
+ "k8s.io/client-go/1.5/pkg/api"
+ unversionedapi "k8s.io/client-go/1.5/pkg/api/unversioned"
+ "k8s.io/client-go/1.5/pkg/labels"
+ "k8s.io/client-go/1.5/rest"
+ "k8s.io/client-go/1.5/tools/clientcmd"
+ clientcmdapi "k8s.io/client-go/1.5/tools/clientcmd/api"
+)
+
+// Kubernetes implements a plugin that connects to a Kubernetes cluster.
+type Kubernetes struct {
+ Next plugin.Handler
+ Zones []string
+ Proxy proxy.Proxy // Proxy for looking up names during the resolution process
+ APIServerList []string
+ APIProxy *apiProxy
+ APICertAuth string
+ APIClientCert string
+ APIClientKey string
+ APIConn dnsController
+ Namespaces map[string]bool
+ podMode string
+ Fallthrough bool
+ ttl uint32
+
+ primaryZoneIndex int
+ interfaceAddrsFunc func() net.IP
+ autoPathSearch []string // Local search path from /etc/resolv.conf. Needed for autopath.
+}
+
+// New returns a intialized 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]bool)
+ k.interfaceAddrsFunc = func() net.IP { return net.ParseIP("127.0.0.1") }
+ k.podMode = podModeDisabled
+ k.Proxy = proxy.Proxy{}
+ 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 verfying they exist
+ podModeInsecure = "insecure"
+ // DNSSchemaVersion is the schema version: https://github.com/kubernetes/dns/blob/master/docs/specification.md
+ DNSSchemaVersion = "1.0.1"
+)
+
+var (
+ errNoItems = errors.New("no items found")
+ errNsNotExposed = errors.New("namespace is not exposed")
+ errInvalidRequest = errors.New("invalid query name")
+ errAPIBadPodType = errors.New("expected type *api.Pod")
+ errPodsDisabled = errors.New("pod records disabled")
+)
+
+// Services implements the ServiceBackend interface.
+func (k *Kubernetes) Services(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, fmt.Errorf("kubernetes: TXT query can only be for dns-version: %s", state.QName())
+ }
+ 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 equal the zone, see ServeDNS in handler.go.
+ ns := k.nsAddr()
+ svc := msg.Service{Host: ns.A.String(), Key: msg.Path(state.QName(), "coredns")}
+ return []msg.Service{svc}, nil
+ }
+
+ if state.QType() == dns.TypeA && isDefaultNS(state.Name(), state.Zone) {
+ // If this is an A request for "ns.dns", respond with a "fake" record for coredns.
+ // SOA records always use this hardcoded name
+ ns := k.nsAddr()
+ svc := msg.Service{Host: ns.A.String(), Key: msg.Path(state.QName(), "coredns")}
+ return []msg.Service{svc}, nil
+ }
+
+ s, e := k.Records(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(state request.Request, name string, typ uint16) (*dns.Msg, error) {
+ return k.Proxy.Lookup(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) {
+ loadingRules := &clientcmd.ClientConfigLoadingRules{}
+ overrides := &clientcmd.ConfigOverrides{}
+ clusterinfo := clientcmdapi.Cluster{}
+ authinfo := clientcmdapi.AuthInfo{}
+
+ if len(k.APIServerList) == 0 {
+ cc, err := rest.InClusterConfig()
+ if err != nil {
+ return nil, err
+ }
+ return cc, err
+ }
+
+ endpoint := k.APIServerList[0]
+ if len(k.APIServerList) > 1 {
+ // Use a random port for api proxy, will get the value later through listener.Addr()
+ listener, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ return nil, fmt.Errorf("failed to create kubernetes api proxy: %v", err)
+ }
+ k.APIProxy = &apiProxy{
+ listener: listener,
+ handler: proxyHandler{
+ HealthCheck: healthcheck.HealthCheck{
+ FailTimeout: 3 * time.Second,
+ MaxFails: 1,
+ Future: 10 * time.Second,
+ Path: "/",
+ Interval: 5 * time.Second,
+ },
+ },
+ }
+ k.APIProxy.handler.Hosts = make([]*healthcheck.UpstreamHost, len(k.APIServerList))
+ for i, entry := range k.APIServerList {
+
+ uh := &healthcheck.UpstreamHost{
+ Name: strings.TrimPrefix(entry, "http://"),
+
+ CheckDown: func(upstream *proxyHandler) healthcheck.UpstreamHostDownFunc {
+ return func(uh *healthcheck.UpstreamHost) bool {
+
+ down := false
+
+ uh.CheckMu.Lock()
+ until := uh.OkUntil
+ uh.CheckMu.Unlock()
+
+ if !until.IsZero() && time.Now().After(until) {
+ down = true
+ }
+
+ fails := atomic.LoadInt32(&uh.Fails)
+ if fails >= upstream.MaxFails && upstream.MaxFails != 0 {
+ down = true
+ }
+ return down
+ }
+ }(&k.APIProxy.handler),
+ }
+
+ k.APIProxy.handler.Hosts[i] = uh
+ }
+ k.APIProxy.Handler = &k.APIProxy.handler
+
+ // Find the random port used for api proxy
+ endpoint = fmt.Sprintf("http://%s", listener.Addr())
+ }
+ clusterinfo.Server = endpoint
+
+ 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)
+
+ return clientConfig.ClientConfig()
+}
+
+// initKubeCache initializes a new Kubernetes cache.
+func (k *Kubernetes) initKubeCache(opts dnsControlOpts) (err error) {
+
+ config, err := k.getClientConfig()
+ if err != nil {
+ return err
+ }
+
+ kubeClient, err := kubernetes.NewForConfig(config)
+ if err != nil {
+ return fmt.Errorf("failed to create kubernetes notification controller: %q", err)
+ }
+
+ if opts.labelSelector != nil {
+ var selector labels.Selector
+ selector, err = unversionedapi.LabelSelectorAsSelector(opts.labelSelector)
+ if err != nil {
+ return fmt.Errorf("unable to create Selector for LabelSelector '%s': %q", opts.labelSelector, err)
+ }
+ opts.selector = &selector
+ }
+
+ opts.initPodCache = k.podMode == podModeVerified
+
+ k.APIConn = newdnsController(kubeClient, opts)
+
+ return err
+}
+
+// Records looks up services in kubernetes.
+func (k *Kubernetes) Records(state request.Request, exact bool) ([]msg.Service, error) {
+ r, e := parseRequest(state)
+ if e != nil {
+ return nil, e
+ }
+
+ if !wildcard(r.namespace) && !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 api.EndpointAddress) string {
+ if addr.Hostname != "" {
+ return strings.ToLower(addr.Hostname)
+ }
+ if strings.Contains(addr.IP, ".") {
+ return strings.Replace(addr.IP, ".", "-", -1)
+ }
+ if strings.Contains(addr.IP, ":") {
+ return strings.ToLower(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, errPodsDisabled
+ }
+
+ namespace := r.namespace
+ podname := r.service
+ zonePath := msg.Path(zone, "coredns")
+ ip := ""
+ err = errNoItems
+
+ if strings.Count(podname, "-") == 3 && !strings.Contains(podname, "--") {
+ ip = strings.Replace(podname, "-", ".", -1)
+ } else {
+ ip = strings.Replace(podname, "-", ":", -1)
+ }
+
+ if k.podMode == podModeInsecure {
+ return []msg.Service{{Key: strings.Join([]string{zonePath, Pod, namespace, podname}, "/"), Host: ip}}, nil
+ }
+
+ // PodModeVerified
+ objList := k.APIConn.PodIndex(ip)
+
+ for _, o := range objList {
+ p, ok := o.(*api.Pod)
+ if !ok {
+ return nil, errAPIBadPodType
+ }
+ // If namespace has a wildcard, filter results against Corefile namespace list.
+ if wildcard(namespace) && !k.namespaceExposed(p.Namespace) {
+ continue
+ }
+ // check for matching ip and namespace
+ if ip == p.Status.PodIP && match(namespace, p.Namespace) {
+ s := msg.Service{Key: strings.Join([]string{zonePath, Pod, namespace, podname}, "/"), Host: ip}
+ 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) {
+ serviceList := k.APIConn.ServiceList()
+ zonePath := msg.Path(zone, "coredns")
+ err = errNoItems // Set to errNoItems to signal really nothing found, gets reset when name is matched.
+
+ for _, svc := range serviceList {
+ if !(match(r.namespace, svc.Namespace) && match(r.service, svc.Name)) {
+ continue
+ }
+
+ // If namespace has a wildcard, filter results against Corefile namespace list.
+ // (Namespaces without a wildcard were filtered before the call to this function.)
+ if wildcard(r.namespace) && !k.namespaceExposed(svc.Namespace) {
+ continue
+ }
+
+ // Endpoint query or headless service
+ if svc.Spec.ClusterIP == api.ClusterIPNone || r.endpoint != "" {
+ endpointsList := k.APIConn.EndpointsList()
+ for _, ep := range endpointsList.Items {
+ if ep.ObjectMeta.Name != svc.Name || ep.ObjectMeta.Namespace != svc.Namespace {
+ 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)) {
+ continue
+ }
+ }
+
+ for _, p := range eps.Ports {
+ if !(match(r.port, p.Name) && match(r.protocol, string(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)}, "/")
+
+ err = nil
+
+ services = append(services, s)
+ }
+ }
+ }
+ }
+ continue
+ }
+
+ // External service
+ if svc.Spec.ExternalName != "" {
+ s := msg.Service{Key: strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/"), Host: svc.Spec.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
+ }
+ }
+
+ // ClusterIP service
+ for _, p := range svc.Spec.Ports {
+ if !(match(r.port, p.Name) && match(r.protocol, string(p.Protocol))) {
+ continue
+ }
+
+ err = nil
+
+ s := msg.Service{Host: svc.Spec.ClusterIP, 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
+}
+
+// match checks if a and b are equal taking wildcards into account.
+func match(a, b string) bool {
+ if wildcard(a) {
+ return true
+ }
+ if wildcard(b) {
+ return true
+ }
+ return strings.EqualFold(a, b)
+}
+
+// wildcard checks whether s contains a wildcard value defined as "*" or "any".
+func wildcard(s string) bool {
+ return s == "*" || s == "any"
+}
+
+// namespaceExposed returns true when the namespace is exposed.
+func (k *Kubernetes) namespaceExposed(namespace string) bool {
+ _, ok := k.Namespaces[namespace]
+ if len(k.Namespaces) > 0 && !ok {
+ return false
+ }
+ return true
+}
+
+const (
+ // 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
+)
diff --git a/plugin/kubernetes/kubernetes_apex_test.go b/plugin/kubernetes/kubernetes_apex_test.go
new file mode 100644
index 000000000..41b70b883
--- /dev/null
+++ b/plugin/kubernetes/kubernetes_apex_test.go
@@ -0,0 +1,68 @@
+package kubernetes
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+var kubeApexCases = [](test.Case){
+ {
+ Qname: "cluster.local.", Qtype: dns.TypeSOA,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{
+ test.SOA("cluster.local. 303 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 60"),
+ },
+ },
+ {
+ Qname: "cluster.local.", Qtype: dns.TypeHINFO,
+ Rcode: dns.RcodeSuccess,
+ Ns: []dns.RR{
+ test.SOA("cluster.local. 303 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1499347823 7200 1800 86400 60"),
+ },
+ },
+ {
+ Qname: "cluster.local.", Qtype: dns.TypeNS,
+ Rcode: dns.RcodeSuccess,
+ Answer: []dns.RR{
+ test.NS("cluster.local. 303 IN NS ns.dns.cluster.local."),
+ },
+ Extra: []dns.RR{
+ test.A("ns.dns.cluster.local. 303 IN A 127.0.0.1"),
+ },
+ },
+}
+
+func TestServeDNSApex(t *testing.T) {
+
+ k := New([]string{"cluster.local."})
+ k.APIConn = &APIConnServeTest{}
+ k.Next = test.NextHandler(dns.RcodeSuccess, nil)
+ ctx := context.TODO()
+
+ for i, tc := range kubeApexCases {
+ r := tc.Msg()
+
+ w := dnsrecorder.New(&test.ResponseWriter{})
+
+ _, err := k.ServeDNS(ctx, w, r)
+ if err != tc.Error {
+ t.Errorf("Test %d, expected no error, got %v\n", 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)
+ }
+
+ test.SortAndCheck(t, resp, tc)
+ }
+}
diff --git a/plugin/kubernetes/kubernetes_test.go b/plugin/kubernetes/kubernetes_test.go
new file mode 100644
index 000000000..f347f10fc
--- /dev/null
+++ b/plugin/kubernetes/kubernetes_test.go
@@ -0,0 +1,242 @@
+package kubernetes
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "k8s.io/client-go/1.5/pkg/api"
+)
+
+func TestWildcard(t *testing.T) {
+ var tests = []struct {
+ s string
+ expected bool
+ }{
+ {"mynamespace", false},
+ {"*", true},
+ {"any", true},
+ {"my*space", false},
+ {"*space", false},
+ {"myname*", false},
+ }
+
+ for _, te := range tests {
+ got := wildcard(te.s)
+ if got != te.expected {
+ t.Errorf("Expected Wildcard result '%v' for example '%v', got '%v'.", te.expected, te.s, got)
+ }
+ }
+}
+
+func TestEndpointHostname(t *testing.T) {
+ var tests = []struct {
+ ip string
+ hostname string
+ expected string
+ }{
+ {"10.11.12.13", "", "10-11-12-13"},
+ {"10.11.12.13", "epname", "epname"},
+ }
+ for _, test := range tests {
+ result := endpointHostname(api.EndpointAddress{IP: test.ip, Hostname: test.hostname})
+ 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) Run() { return }
+func (APIConnServiceTest) Stop() error { return nil }
+func (APIConnServiceTest) PodIndex(string) []interface{} { return nil }
+
+func (APIConnServiceTest) ServiceList() []*api.Service {
+ svcs := []*api.Service{
+ {
+ ObjectMeta: api.ObjectMeta{
+ Name: "svc1",
+ Namespace: "testns",
+ },
+ Spec: api.ServiceSpec{
+ ClusterIP: "10.0.0.1",
+ Ports: []api.ServicePort{{
+ Name: "http",
+ Protocol: "tcp",
+ Port: 80,
+ }},
+ },
+ },
+ {
+ ObjectMeta: api.ObjectMeta{
+ Name: "hdls1",
+ Namespace: "testns",
+ },
+ Spec: api.ServiceSpec{
+ ClusterIP: api.ClusterIPNone,
+ },
+ },
+ {
+ ObjectMeta: api.ObjectMeta{
+ Name: "external",
+ Namespace: "testns",
+ },
+ Spec: api.ServiceSpec{
+ ExternalName: "coredns.io",
+ Ports: []api.ServicePort{{
+ Name: "http",
+ Protocol: "tcp",
+ Port: 80,
+ }},
+ },
+ },
+ }
+ return svcs
+}
+
+func (APIConnServiceTest) EndpointsList() api.EndpointsList {
+ n := "test.node.foo.bar"
+
+ return api.EndpointsList{
+ Items: []api.Endpoints{
+ {
+ Subsets: []api.EndpointSubset{
+ {
+ Addresses: []api.EndpointAddress{
+ {
+ IP: "172.0.0.1",
+ Hostname: "ep1a",
+ },
+ },
+ Ports: []api.EndpointPort{
+ {
+ Port: 80,
+ Protocol: "tcp",
+ Name: "http",
+ },
+ },
+ },
+ },
+ ObjectMeta: api.ObjectMeta{
+ Name: "svc1",
+ Namespace: "testns",
+ },
+ },
+ {
+ Subsets: []api.EndpointSubset{
+ {
+ Addresses: []api.EndpointAddress{
+ {
+ IP: "172.0.0.2",
+ },
+ },
+ Ports: []api.EndpointPort{
+ {
+ Port: 80,
+ Protocol: "tcp",
+ Name: "http",
+ },
+ },
+ },
+ },
+ ObjectMeta: api.ObjectMeta{
+ Name: "hdls1",
+ Namespace: "testns",
+ },
+ },
+ {
+ Subsets: []api.EndpointSubset{
+ {
+ Addresses: []api.EndpointAddress{
+ {
+ IP: "172.0.0.3",
+ },
+ },
+ Ports: []api.EndpointPort{
+ {
+ Port: 80,
+ Protocol: "tcp",
+ Name: "http",
+ },
+ },
+ },
+ },
+ ObjectMeta: api.ObjectMeta{
+ Name: "hdls1",
+ Namespace: "testns",
+ },
+ },
+ {
+ Subsets: []api.EndpointSubset{
+ {
+ Addresses: []api.EndpointAddress{
+ {
+ IP: "10.9.8.7",
+ NodeName: &n,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (APIConnServiceTest) GetNodeByName(name string) (api.Node, error) {
+ return api.Node{
+ ObjectMeta: api.ObjectMeta{
+ Name: "test.node.foo.bar",
+ },
+ }, 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"}},
+
+ // External Services
+ {qname: "external.testns.svc.interwebs.test.", qtype: dns.TypeCNAME, answer: svcAns{host: "coredns.io", key: "/coredns/test/interwebs/svc/testns/external"}},
+ }
+
+ 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(state, false, plugin.Options{})
+ if e != nil {
+ t.Errorf("Test %d: got error '%v'", i, e)
+ continue
+ }
+ if len(svcs) != 1 {
+ t.Errorf("Test %d, expected expected 1 answer, got %v", i, len(svcs))
+ continue
+ }
+
+ if test.answer.host != svcs[0].Host {
+ t.Errorf("Test %d, expected host '%v', got '%v'", i, test.answer.host, svcs[0].Host)
+ }
+ if test.answer.key != svcs[0].Key {
+ t.Errorf("Test %d, expected key '%v', got '%v'", i, test.answer.key, svcs[0].Key)
+ }
+ }
+}
diff --git a/plugin/kubernetes/local.go b/plugin/kubernetes/local.go
new file mode 100644
index 000000000..e5b7f1e0f
--- /dev/null
+++ b/plugin/kubernetes/local.go
@@ -0,0 +1,40 @@
+package kubernetes
+
+import "net"
+
+func localPodIP() net.IP {
+ addrs, err := net.InterfaceAddrs()
+ if err != nil {
+ return nil
+ }
+
+ for _, addr := range addrs {
+ ip, _, _ := net.ParseCIDR(addr.String())
+ ip = ip.To4()
+ if ip == nil || ip.IsLoopback() {
+ continue
+ }
+ return ip
+ }
+ return nil
+}
+
+func (k *Kubernetes) localNodeName() string {
+ localIP := k.interfaceAddrsFunc()
+ if localIP == nil {
+ return ""
+ }
+
+ // Find endpoint matching localIP
+ endpointsList := k.APIConn.EndpointsList()
+ for _, ep := range endpointsList.Items {
+ for _, eps := range ep.Subsets {
+ for _, addr := range eps.Addresses {
+ if localIP.Equal(net.ParseIP(addr.IP)) {
+ return *addr.NodeName
+ }
+ }
+ }
+ }
+ return ""
+}
diff --git a/plugin/kubernetes/ns.go b/plugin/kubernetes/ns.go
new file mode 100644
index 000000000..4cacc382f
--- /dev/null
+++ b/plugin/kubernetes/ns.go
@@ -0,0 +1,65 @@
+package kubernetes
+
+import (
+ "net"
+ "strings"
+
+ "github.com/miekg/dns"
+ "k8s.io/client-go/1.5/pkg/api"
+)
+
+func isDefaultNS(name, zone string) bool {
+ return strings.Index(name, defaultNSName) == 0 && strings.Index(name, zone) == len(defaultNSName)
+}
+
+func (k *Kubernetes) nsAddr() *dns.A {
+ var (
+ svcName string
+ svcNamespace string
+ )
+
+ rr := new(dns.A)
+ localIP := k.interfaceAddrsFunc()
+ endpointsList := k.APIConn.EndpointsList()
+
+ rr.A = localIP
+
+FindEndpoint:
+ for _, ep := range endpointsList.Items {
+ for _, eps := range ep.Subsets {
+ for _, addr := range eps.Addresses {
+ if localIP.Equal(net.ParseIP(addr.IP)) {
+ svcNamespace = ep.ObjectMeta.Namespace
+ svcName = ep.ObjectMeta.Name
+ break FindEndpoint
+ }
+ }
+ }
+ }
+
+ if len(svcName) == 0 {
+ rr.Hdr.Name = defaultNSName
+ rr.A = localIP
+ return rr
+ }
+ // Find service to get ClusterIP
+ serviceList := k.APIConn.ServiceList()
+
+FindService:
+ for _, svc := range serviceList {
+ if svcName == svc.Name && svcNamespace == svc.Namespace {
+ if svc.Spec.ClusterIP == api.ClusterIPNone {
+ rr.A = localIP
+ } else {
+ rr.A = net.ParseIP(svc.Spec.ClusterIP)
+ }
+ break FindService
+ }
+ }
+
+ rr.Hdr.Name = strings.Join([]string{svcName, svcNamespace, "svc."}, ".")
+
+ return rr
+}
+
+const defaultNSName = "ns.dns."
diff --git a/plugin/kubernetes/ns_test.go b/plugin/kubernetes/ns_test.go
new file mode 100644
index 000000000..8e9e80c71
--- /dev/null
+++ b/plugin/kubernetes/ns_test.go
@@ -0,0 +1,69 @@
+package kubernetes
+
+import (
+ "testing"
+
+ "k8s.io/client-go/1.5/pkg/api"
+)
+
+type APIConnTest struct{}
+
+func (APIConnTest) Run() { return }
+func (APIConnTest) Stop() error { return nil }
+func (APIConnTest) PodIndex(string) []interface{} { return nil }
+
+func (APIConnTest) ServiceList() []*api.Service {
+ svc := api.Service{
+ ObjectMeta: api.ObjectMeta{
+ Name: "dns-service",
+ Namespace: "kube-system",
+ },
+ Spec: api.ServiceSpec{
+ ClusterIP: "10.0.0.111",
+ },
+ }
+
+ return []*api.Service{&svc}
+
+}
+
+func (APIConnTest) EndpointsList() api.EndpointsList {
+ return api.EndpointsList{
+ Items: []api.Endpoints{
+ {
+ Subsets: []api.EndpointSubset{
+ {
+ Addresses: []api.EndpointAddress{
+ {
+ IP: "127.0.0.1",
+ },
+ },
+ },
+ },
+ ObjectMeta: api.ObjectMeta{
+ Name: "dns-service",
+ Namespace: "kube-system",
+ },
+ },
+ },
+ }
+}
+
+func (APIConnTest) GetNodeByName(name string) (api.Node, error) { return api.Node{}, nil }
+
+func TestNsAddr(t *testing.T) {
+
+ k := New([]string{"inter.webs.test."})
+ k.APIConn = &APIConnTest{}
+
+ cdr := k.nsAddr()
+ expected := "10.0.0.111"
+
+ if cdr.A.String() != expected {
+ t.Errorf("Expected A to be %q, got %q", expected, cdr.A.String())
+ }
+ expected = "dns-service.kube-system.svc."
+ if cdr.Hdr.Name != expected {
+ t.Errorf("Expected Hdr.Name to be %q, got %q", expected, cdr.Hdr.Name)
+ }
+}
diff --git a/plugin/kubernetes/parse.go b/plugin/kubernetes/parse.go
new file mode 100644
index 000000000..a66e77699
--- /dev/null
+++ b/plugin/kubernetes/parse.go
@@ -0,0 +1,112 @@
+package kubernetes
+
+import (
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/request"
+
+ "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(state request.Request) (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
+ //
+ // Federations are handled in the federation plugin. And aren't parsed here.
+
+ base, _ := dnsutil.TrimZone(state.Name(), state.Zone)
+ segs := dns.SplitDomainName(base)
+
+ r.port = "*"
+ r.protocol = "*"
+ r.service = "*"
+ r.namespace = "*"
+ // r.endpoint is the odd one out, we need to know if it has been set or not. If it is
+ // empty we should skip the endpoint check in k.get(). Hence we cannot set if to "*".
+
+ // start at the right and fill out recordRequest with the bits we find, so we look for
+ // pod|svc.namespace.service and then either
+ // * endpoint
+ // *_protocol._port
+
+ 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 ambiquity 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 s[0] != '_' {
+ return s
+ }
+ return s[1:]
+}
+
+// String return 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/plugin/kubernetes/parse_test.go b/plugin/kubernetes/parse_test.go
new file mode 100644
index 000000000..06d5a2aaa
--- /dev/null
+++ b/plugin/kubernetes/parse_test.go
@@ -0,0 +1,56 @@
+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.test.", "http.tcp..webs.mynamespace.svc"},
+ // wildcard acceptance
+ {"*.any.*.any.svc.inter.webs.test.", "*.any..*.any.svc"},
+ // A request of endpoint
+ {"1-2-3-4.webs.mynamespace.svc.inter.webs.test.", "*.*.1-2-3-4.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)
+ 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 (stringyfied) 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); e == nil {
+ t.Errorf("Test %d: expected error from %s, got none", i, query)
+ }
+ }
+}
+
+const zone = "intern.webs.tests."
diff --git a/plugin/kubernetes/reverse.go b/plugin/kubernetes/reverse.go
new file mode 100644
index 000000000..0143b721a
--- /dev/null
+++ b/plugin/kubernetes/reverse.go
@@ -0,0 +1,55 @@
+package kubernetes
+
+import (
+ "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(state request.Request, exact bool, opt plugin.Options) ([]msg.Service, error) {
+
+ ip := dnsutil.ExtractAddressFromReverse(state.Name())
+ if ip == "" {
+ return nil, nil
+ }
+
+ records := k.serviceRecordForIP(ip, state.Name())
+ 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
+ svcList := k.APIConn.ServiceList()
+
+ for _, service := range svcList {
+ if (len(k.Namespaces) > 0) && !k.namespaceExposed(service.Namespace) {
+ continue
+ }
+ if service.Spec.ClusterIP == ip {
+ domain := strings.Join([]string{service.Name, service.Namespace, Svc, k.primaryZone()}, ".")
+ return []msg.Service{{Host: domain}}
+ }
+ }
+ // If no cluster ips match, search endpoints
+ epList := k.APIConn.EndpointsList()
+ for _, ep := range epList.Items {
+ if (len(k.Namespaces) > 0) && !k.namespaceExposed(ep.ObjectMeta.Namespace) {
+ continue
+ }
+ for _, eps := range ep.Subsets {
+ for _, addr := range eps.Addresses {
+ if addr.IP == ip {
+ domain := strings.Join([]string{endpointHostname(addr), ep.ObjectMeta.Name, ep.ObjectMeta.Namespace, Svc, k.primaryZone()}, ".")
+ return []msg.Service{{Host: domain}}
+ }
+ }
+ }
+ }
+ return nil
+}
diff --git a/plugin/kubernetes/reverse_test.go b/plugin/kubernetes/reverse_test.go
new file mode 100644
index 000000000..aa9d09585
--- /dev/null
+++ b/plugin/kubernetes/reverse_test.go
@@ -0,0 +1,125 @@
+package kubernetes
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+ "k8s.io/client-go/1.5/pkg/api"
+)
+
+type APIConnReverseTest struct{}
+
+func (APIConnReverseTest) Run() { return }
+func (APIConnReverseTest) Stop() error { return nil }
+func (APIConnReverseTest) PodIndex(string) []interface{} { return nil }
+
+func (APIConnReverseTest) ServiceList() []*api.Service {
+ svcs := []*api.Service{
+ {
+ ObjectMeta: api.ObjectMeta{
+ Name: "svc1",
+ Namespace: "testns",
+ },
+ Spec: api.ServiceSpec{
+ ClusterIP: "192.168.1.100",
+ Ports: []api.ServicePort{{
+ Name: "http",
+ Protocol: "tcp",
+ Port: 80,
+ }},
+ },
+ },
+ }
+ return svcs
+}
+
+func (APIConnReverseTest) EndpointsList() api.EndpointsList {
+ return api.EndpointsList{
+ Items: []api.Endpoints{
+ {
+ Subsets: []api.EndpointSubset{
+ {
+ Addresses: []api.EndpointAddress{
+ {
+ IP: "10.0.0.100",
+ Hostname: "ep1a",
+ },
+ },
+ Ports: []api.EndpointPort{
+ {
+ Port: 80,
+ Protocol: "tcp",
+ Name: "http",
+ },
+ },
+ },
+ },
+ ObjectMeta: api.ObjectMeta{
+ Name: "svc1",
+ Namespace: "testns",
+ },
+ },
+ },
+ }
+}
+
+func (APIConnReverseTest) GetNodeByName(name string) (api.Node, error) {
+ return api.Node{
+ ObjectMeta: api.ObjectMeta{
+ Name: "test.node.foo.bar",
+ },
+ }, nil
+}
+
+func TestReverse(t *testing.T) {
+
+ k := New([]string{"cluster.local.", "0.10.in-addr.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. 303 IN PTR ep1a.svc1.testns.svc.cluster.local."),
+ },
+ },
+ {
+ Qname: "101.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
+ Rcode: dns.RcodeSuccess,
+ Ns: []dns.RR{
+ test.SOA("0.10.in-addr.arpa. 300 IN SOA ns.dns.0.10.in-addr.arpa. hostmaster.0.10.in-addr.arpa. 1502782828 7200 1800 86400 60"),
+ },
+ },
+ {
+ Qname: "example.org.cluster.local.", Qtype: dns.TypePTR,
+ Rcode: dns.RcodeSuccess,
+ Ns: []dns.RR{
+ test.SOA("cluster.local. 300 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1502989566 7200 1800 86400 60"),
+ },
+ },
+ }
+
+ ctx := context.TODO()
+ for i, tc := range tests {
+ r := tc.Msg()
+
+ w := dnsrecorder.New(&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)
+ }
+ test.SortAndCheck(t, resp, tc)
+ }
+}
diff --git a/plugin/kubernetes/setup.go b/plugin/kubernetes/setup.go
new file mode 100644
index 000000000..e60239d42
--- /dev/null
+++ b/plugin/kubernetes/setup.go
@@ -0,0 +1,208 @@
+package kubernetes
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/proxy"
+ "github.com/miekg/dns"
+
+ "github.com/mholt/caddy"
+ unversionedapi "k8s.io/client-go/1.5/pkg/api/unversioned"
+)
+
+func init() {
+ caddy.RegisterPlugin("kubernetes", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ kubernetes, initOpts, err := kubernetesParse(c)
+ if err != nil {
+ return plugin.Error("kubernetes", err)
+ }
+
+ err = kubernetes.initKubeCache(initOpts)
+ if err != nil {
+ return plugin.Error("kubernetes", err)
+ }
+
+ // Register KubeCache start and stop functions with Caddy
+ c.OnStartup(func() error {
+ go kubernetes.APIConn.Run()
+ if kubernetes.APIProxy != nil {
+ go kubernetes.APIProxy.Run()
+ }
+ return nil
+ })
+
+ c.OnShutdown(func() error {
+ if kubernetes.APIProxy != nil {
+ kubernetes.APIProxy.Stop()
+ }
+ return kubernetes.APIConn.Stop()
+ })
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ kubernetes.Next = next
+ return kubernetes
+ })
+
+ return nil
+}
+
+func kubernetesParse(c *caddy.Controller) (*Kubernetes, dnsControlOpts, error) {
+ k8s := New([]string{""})
+ k8s.interfaceAddrsFunc = localPodIP
+ k8s.autoPathSearch = searchFromResolvConf()
+
+ opts := dnsControlOpts{
+ resyncPeriod: defaultResyncPeriod,
+ }
+
+ for c.Next() {
+ zones := c.RemainingArgs()
+
+ if len(zones) != 0 {
+ k8s.Zones = zones
+ for i := 0; i < len(k8s.Zones); i++ {
+ k8s.Zones[i] = plugin.Host(k8s.Zones[i]).Normalize()
+ }
+ } else {
+ k8s.Zones = make([]string, len(c.ServerBlockKeys))
+ for i := 0; i < len(c.ServerBlockKeys); i++ {
+ k8s.Zones[i] = plugin.Host(c.ServerBlockKeys[i]).Normalize()
+ }
+ }
+
+ k8s.primaryZoneIndex = -1
+ for i, z := range k8s.Zones {
+ if strings.HasSuffix(z, "in-addr.arpa.") || strings.HasSuffix(z, "ip6.arpa.") {
+ continue
+ }
+ k8s.primaryZoneIndex = i
+ break
+ }
+
+ if k8s.primaryZoneIndex == -1 {
+ return nil, opts, errors.New("non-reverse zone name must be used")
+ }
+
+ for c.NextBlock() {
+ switch c.Val() {
+ case "pods":
+ args := c.RemainingArgs()
+ if len(args) == 1 {
+ switch args[0] {
+ case podModeDisabled, podModeInsecure, podModeVerified:
+ k8s.podMode = args[0]
+ default:
+ return nil, opts, fmt.Errorf("wrong value for pods: %s, must be one of: disabled, verified, insecure", args[0])
+ }
+ continue
+ }
+ return nil, opts, c.ArgErr()
+ case "namespaces":
+ args := c.RemainingArgs()
+ if len(args) > 0 {
+ for _, a := range args {
+ k8s.Namespaces[a] = true
+ }
+ continue
+ }
+ return nil, opts, c.ArgErr()
+ case "endpoint":
+ args := c.RemainingArgs()
+ if len(args) > 0 {
+ for _, endpoint := range strings.Split(args[0], ",") {
+ k8s.APIServerList = append(k8s.APIServerList, strings.TrimSpace(endpoint))
+ }
+ continue
+ }
+ return nil, opts, 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, opts, c.ArgErr()
+ case "resyncperiod":
+ args := c.RemainingArgs()
+ if len(args) > 0 {
+ rp, err := time.ParseDuration(args[0])
+ if err != nil {
+ return nil, opts, fmt.Errorf("unable to parse resync duration value: '%v': %v", args[0], err)
+ }
+ opts.resyncPeriod = rp
+ continue
+ }
+ return nil, opts, c.ArgErr()
+ case "labels":
+ args := c.RemainingArgs()
+ if len(args) > 0 {
+ labelSelectorString := strings.Join(args, " ")
+ ls, err := unversionedapi.ParseToLabelSelector(labelSelectorString)
+ if err != nil {
+ return nil, opts, fmt.Errorf("unable to parse label selector value: '%v': %v", labelSelectorString, err)
+ }
+ opts.labelSelector = ls
+ continue
+ }
+ return nil, opts, c.ArgErr()
+ case "fallthrough":
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ k8s.Fallthrough = true
+ continue
+ }
+ return nil, opts, c.ArgErr()
+ case "upstream":
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ return nil, opts, c.ArgErr()
+ }
+ ups, err := dnsutil.ParseHostPortOrFile(args...)
+ if err != nil {
+ return nil, opts, err
+ }
+ k8s.Proxy = proxy.NewLookup(ups)
+ case "ttl":
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ return nil, opts, c.ArgErr()
+ }
+ t, err := strconv.Atoi(args[0])
+ if err != nil {
+ return nil, opts, err
+ }
+ if t < 5 || t > 3600 {
+ return nil, opts, c.Errf("ttl must be in range [5, 3600]: %d", t)
+ }
+ k8s.ttl = uint32(t)
+ default:
+ return nil, opts, c.Errf("unknown property '%s'", c.Val())
+ }
+ }
+ }
+ return k8s, opts, nil
+}
+
+func searchFromResolvConf() []string {
+ rc, err := dns.ClientConfigFromFile("/etc/resolv.conf")
+ if err != nil {
+ return nil
+ }
+ plugin.Zones(rc.Search).Normalize()
+ return rc.Search
+}
+
+const defaultResyncPeriod = 5 * time.Minute
diff --git a/plugin/kubernetes/setup_reverse_test.go b/plugin/kubernetes/setup_reverse_test.go
new file mode 100644
index 000000000..ed51a7410
--- /dev/null
+++ b/plugin/kubernetes/setup_reverse_test.go
@@ -0,0 +1,35 @@
+package kubernetes
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestKubernetesParseReverseZone(t *testing.T) {
+ tests := []struct {
+ input string // Corefile data as string
+ expectedZones []string // expected count of defined zones.
+ }{
+ {`kubernetes coredns.local 10.0.0.0/16`, []string{"coredns.local.", "0.10.in-addr.arpa."}},
+ {`kubernetes coredns.local 10.0.0.0/17`, []string{"coredns.local.", "10.0.0.0/17."}},
+ }
+
+ for i, tc := range tests {
+ c := caddy.NewTestController("dns", tc.input)
+ k, _, err := kubernetesParse(c)
+ if err != nil {
+ t.Fatalf("Test %d: Expected no error, got %q", i, err)
+ }
+
+ zl := len(k.Zones)
+ if zl != len(tc.expectedZones) {
+ t.Errorf("Test %d: Expected kubernetes to be initialized with %d zones, found %d zones", i, len(tc.expectedZones), zl)
+ }
+ for i, z := range tc.expectedZones {
+ if k.Zones[i] != z {
+ t.Errorf("Test %d: Expected zones to be %q, got %q", i, z, k.Zones[i])
+ }
+ }
+ }
+}
diff --git a/plugin/kubernetes/setup_test.go b/plugin/kubernetes/setup_test.go
new file mode 100644
index 000000000..2fdc38a9c
--- /dev/null
+++ b/plugin/kubernetes/setup_test.go
@@ -0,0 +1,473 @@
+package kubernetes
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/mholt/caddy"
+ "k8s.io/client-go/1.5/pkg/api/unversioned"
+)
+
+func TestKubernetesParse(t *testing.T) {
+ tests := []struct {
+ input string // Corefile data as string
+ shouldErr bool // true if test case is exected 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.
+ expectedResyncPeriod time.Duration // expected resync period value
+ expectedLabelSelector string // expected label selector value
+ expectedPodMode string
+ expectedFallthrough bool
+ expectedUpstreams []string
+ }{
+ // positive
+ {
+ `kubernetes coredns.local`,
+ false,
+ "",
+ 1,
+ 0,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local test.local`,
+ false,
+ "",
+ 2,
+ 0,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+}`,
+ false,
+ "",
+ 1,
+ 0,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ endpoint http://localhost:9090
+}`,
+ false,
+ "",
+ 1,
+ 0,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ namespaces demo
+}`,
+ false,
+ "",
+ 1,
+ 1,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ namespaces demo test
+}`,
+ false,
+ "",
+ 1,
+ 2,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ resyncperiod 30s
+}`,
+ false,
+ "",
+ 1,
+ 0,
+ 30 * time.Second,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ resyncperiod 15m
+}`,
+ false,
+ "",
+ 1,
+ 0,
+ 15 * time.Minute,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ labels environment=prod
+}`,
+ false,
+ "",
+ 1,
+ 0,
+ defaultResyncPeriod,
+ "environment=prod",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ labels environment in (production, staging, qa),application=nginx
+}`,
+ false,
+ "",
+ 1,
+ 0,
+ defaultResyncPeriod,
+ "application=nginx,environment in (production,qa,staging)",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local test.local {
+ resyncperiod 15m
+ endpoint http://localhost:8080
+ namespaces demo test
+ labels environment in (production, staging, qa),application=nginx
+ fallthrough
+}`,
+ false,
+ "",
+ 2,
+ 2,
+ 15 * time.Minute,
+ "application=nginx,environment in (production,qa,staging)",
+ podModeDisabled,
+ true,
+ nil,
+ },
+ // negative
+ {
+ `kubernetes coredns.local {
+ endpoint
+}`,
+ true,
+ "rong argument count or unexpected line ending",
+ -1,
+ -1,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ namespaces
+}`,
+ true,
+ "rong argument count or unexpected line ending",
+ -1,
+ -1,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ resyncperiod
+}`,
+ true,
+ "rong argument count or unexpected line ending",
+ -1,
+ 0,
+ 0 * time.Minute,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ resyncperiod 15
+}`,
+ true,
+ "unable to parse resync duration value",
+ -1,
+ 0,
+ 0 * time.Second,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ resyncperiod abc
+}`,
+ true,
+ "unable to parse resync duration value",
+ -1,
+ 0,
+ 0 * time.Second,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ labels
+}`,
+ true,
+ "rong argument count or unexpected line ending",
+ -1,
+ 0,
+ 0 * time.Second,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ {
+ `kubernetes coredns.local {
+ labels environment in (production, qa
+}`,
+ true,
+ "unable to parse label selector",
+ -1,
+ 0,
+ 0 * time.Second,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ // pods disabled
+ {
+ `kubernetes coredns.local {
+ pods disabled
+}`,
+ false,
+ "",
+ 1,
+ 0,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ // pods insecure
+ {
+ `kubernetes coredns.local {
+ pods insecure
+}`,
+ false,
+ "",
+ 1,
+ 0,
+ defaultResyncPeriod,
+ "",
+ podModeInsecure,
+ false,
+ nil,
+ },
+ // pods verified
+ {
+ `kubernetes coredns.local {
+ pods verified
+}`,
+ false,
+ "",
+ 1,
+ 0,
+ defaultResyncPeriod,
+ "",
+ podModeVerified,
+ false,
+ nil,
+ },
+ // pods invalid
+ {
+ `kubernetes coredns.local {
+ pods giant_seed
+}`,
+ true,
+ "rong value for pods",
+ -1,
+ 0,
+ defaultResyncPeriod,
+ "",
+ podModeVerified,
+ false,
+ nil,
+ },
+ // fallthrough invalid
+ {
+ `kubernetes coredns.local {
+ fallthrough junk
+}`,
+ true,
+ "rong argument count",
+ -1,
+ 0,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ // Valid upstream
+ {
+ `kubernetes coredns.local {
+ upstream 13.14.15.16:53
+}`,
+ false,
+ "",
+ 1,
+ 0,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ []string{"13.14.15.16:53"},
+ },
+ // Invalid upstream
+ {
+ `kubernetes coredns.local {
+ upstream 13.14.15.16orange
+}`,
+ true,
+ "not an IP address or file: \"13.14.15.16orange\"",
+ -1,
+ 0,
+ defaultResyncPeriod,
+ "",
+ podModeDisabled,
+ false,
+ nil,
+ },
+ }
+
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.input)
+ k8sController, opts, 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)
+ }
+
+ // ResyncPeriod
+ foundResyncPeriod := opts.resyncPeriod
+ if foundResyncPeriod != test.expectedResyncPeriod {
+ t.Errorf("Test %d: Expected kubernetes controller to be initialized with resync period '%s'. Instead found period '%s' for input '%s'", i, test.expectedResyncPeriod, foundResyncPeriod, test.input)
+ }
+
+ // Labels
+ if opts.labelSelector != nil {
+ foundLabelSelectorString := unversioned.FormatLabelSelector(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
+ foundFallthrough := k8sController.Fallthrough
+ if foundFallthrough != 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, foundFallthrough, test.input)
+ }
+ // upstream
+ foundUpstreams := k8sController.Proxy.Upstreams
+ if test.expectedUpstreams == nil {
+ if foundUpstreams != nil {
+ t.Errorf("Test %d: Expected kubernetes controller to not be initialized with upstreams for input '%s'", i, test.input)
+ }
+ } else {
+ if foundUpstreams == nil {
+ t.Errorf("Test %d: Expected kubernetes controller to be initialized with upstreams for input '%s'", i, test.input)
+ } else {
+ if len(*foundUpstreams) != len(test.expectedUpstreams) {
+ t.Errorf("Test %d: Expected kubernetes controller to be initialized with %d upstreams. Instead found %d upstreams for input '%s'", i, len(test.expectedUpstreams), len(*foundUpstreams), test.input)
+ }
+ for j, want := range test.expectedUpstreams {
+ got := (*foundUpstreams)[j].Select().Name
+ if got != want {
+ t.Errorf("Test %d: Expected kubernetes controller to be initialized with upstream '%s'. Instead found upstream '%s' for input '%s'", i, want, got, test.input)
+ }
+ }
+
+ }
+ }
+ }
+}
diff --git a/plugin/kubernetes/setup_ttl_test.go b/plugin/kubernetes/setup_ttl_test.go
new file mode 100644
index 000000000..d58f91576
--- /dev/null
+++ b/plugin/kubernetes/setup_ttl_test.go
@@ -0,0 +1,45 @@
+package kubernetes
+
+import (
+ "testing"
+
+ "github.com/mholt/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/plugin/loadbalance/README.md b/plugin/loadbalance/README.md
new file mode 100644
index 000000000..1cce54ebf
--- /dev/null
+++ b/plugin/loadbalance/README.md
@@ -0,0 +1,22 @@
+# loadbalance
+
+*loadbalance* acts as a round-robin DNS loadbalancer by randomizing the order of A and AAAA records
+ in the answer.
+
+ See [Wikipedia](https://en.wikipedia.org/wiki/Round-robin_DNS) about the pros and cons on 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 is "round_robin"
+
+## Examples
+
+~~~
+loadbalance round_robin
+~~~
diff --git a/plugin/loadbalance/handler.go b/plugin/loadbalance/handler.go
new file mode 100644
index 000000000..da4cf1549
--- /dev/null
+++ b/plugin/loadbalance/handler.go
@@ -0,0 +1,23 @@
+// Package loadbalance is plugin for rewriting responses to do "load balancing"
+package loadbalance
+
+import (
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// RoundRobin is 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/plugin/loadbalance/loadbalance.go b/plugin/loadbalance/loadbalance.go
new file mode 100644
index 000000000..7df0b31c6
--- /dev/null
+++ b/plugin/loadbalance/loadbalance.go
@@ -0,0 +1,87 @@
+// Package loadbalance shuffles A and AAAA records.
+package loadbalance
+
+import (
+ "log"
+
+ "github.com/miekg/dns"
+)
+
+// RoundRobinResponseWriter is a response writer that shuffles A and AAAA 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)
+ }
+
+ 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*(int(dns.Id())%4+1); j++ {
+ q := int(dns.Id()) % l
+ p := int(dns.Id()) % l
+ if q == p {
+ p = (p + 1) % l
+ }
+ records[q], records[p] = records[p], records[q]
+ }
+ }
+}
+
+// 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.Printf("[WARNING] RoundRobin called with Write: no shuffling records")
+ n, err := r.ResponseWriter.Write(buf)
+ return n, err
+}
+
+// Hijack implements the dns.ResponseWriter interface.
+func (r *RoundRobinResponseWriter) Hijack() {
+ r.ResponseWriter.Hijack()
+ return
+}
diff --git a/plugin/loadbalance/loadbalance_test.go b/plugin/loadbalance/loadbalance_test.go
new file mode 100644
index 000000000..bde92b543
--- /dev/null
+++ b/plugin/loadbalance/loadbalance_test.go
@@ -0,0 +1,168 @@
+package loadbalance
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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 := dnsrecorder.New(&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 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/plugin/loadbalance/setup.go b/plugin/loadbalance/setup.go
new file mode 100644
index 000000000..c2d90958e
--- /dev/null
+++ b/plugin/loadbalance/setup.go
@@ -0,0 +1,26 @@
+package loadbalance
+
+import (
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("loadbalance", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ for c.Next() {
+ // TODO(miek): block and option parsing
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ return RoundRobin{Next: next}
+ })
+
+ return nil
+}
diff --git a/plugin/log/README.md b/plugin/log/README.md
new file mode 100644
index 000000000..223888ccc
--- /dev/null
+++ b/plugin/log/README.md
@@ -0,0 +1,102 @@
+# log
+
+*log* enables query logging to standard output.
+
+## Syntax
+
+~~~ txt
+log
+~~~
+
+* With no arguments, a query log entry is written to *stdout* in the common log format for all requests
+
+~~~ txt
+log FILE
+~~~
+
+* **FILE** is the log file to create (or append to). The *only* valid name for **FILE** is *stdout*.
+
+~~~ txt
+log [NAME] FILE [FORMAT]
+~~~
+
+* `NAME` is the name to match in order to be logged
+* `FILE` is the log file (again only *stdout* is allowed here).
+* `FORMAT` is the log format to use (default is Common Log Format)
+
+You can further specify the class of responses that get logged:
+
+~~~ txt
+log [NAME] FILE [FORMAT] {
+ class [success|denial|error|all]
+}
+~~~
+
+Here `success` `denial` and `error` denotes the class of responses that should be logged. The
+classes have the following meaning:
+
+* `success`: successful response
+* `denial`: either NXDOMAIN or NODATA (name exists, type does not)
+* `error`: SERVFAIL, NOTIMP, REFUSED, etc. Anything that indicates the remote server is not willing to
+ resolve the request.
+* `all`: the default - nothing is specified.
+
+If no class is specified, it defaults to *all*.
+
+## Log File
+
+The "log file" can only be *stdout*. CoreDNS expects another service to pick up this output and deal
+with it, i.e. journald when using systemd or Docker's logging capabilities.
+
+## 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)
+* `{when}`: time of the query
+* `{remote}`: client's IP address
+* `{size}`: request size in bytes
+* `{port}`: client's port
+* `{duration}`: response duration
+* `{rcode}`: response RCODE
+* `{rsize}`: response size
+* `{>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
+
+The default Common Log Format is:
+
+~~~ txt
+`{remote} - [{when}] "{type} {class} {name} {proto} {size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {duration}`
+~~~
+
+## Examples
+
+Log all requests to stdout
+
+~~~
+log stdout
+~~~
+
+Custom log format, for all zones (`.`)
+
+~~~
+log . stdout "{proto} Request: {name} {type} {>id}"
+~~~
+
+Only log denials for example.org (and below to a file)
+
+~~~
+log example.org stdout {
+ class denial
+}
+~~~
diff --git a/plugin/log/log.go b/plugin/log/log.go
new file mode 100644
index 000000000..52af79d35
--- /dev/null
+++ b/plugin/log/log.go
@@ -0,0 +1,91 @@
+// Package log implements basic but useful request (access) logging plugin.
+package log
+
+import (
+ "log"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/metrics/vars"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/pkg/rcode"
+ "github.com/coredns/coredns/plugin/pkg/replacer"
+ "github.com/coredns/coredns/plugin/pkg/response"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Logger is a basic request logging plugin.
+type Logger struct {
+ Next plugin.Handler
+ Rules []Rule
+ ErrorFunc func(dns.ResponseWriter, *dns.Msg, int) // failover error handler
+}
+
+// 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}
+ for _, rule := range l.Rules {
+ if !plugin.Name(rule.NameScope).Matches(state.Name()) {
+ continue
+ }
+
+ rrw := dnsrecorder.New(w)
+ rc, err := plugin.NextOrFailure(l.Name(), l.Next, ctx, rrw, r)
+
+ if rc > 0 {
+ // There was an error up the chain, but no response has been written yet.
+ // The error must be handled here so the log entry will record the response size.
+ if l.ErrorFunc != nil {
+ l.ErrorFunc(rrw, r, rc)
+ } else {
+ answer := new(dns.Msg)
+ answer.SetRcode(r, rc)
+ state.SizeAndDo(answer)
+
+ vars.Report(state, vars.Dropped, rcode.ToString(rc), answer.Len(), time.Now())
+
+ w.WriteMsg(answer)
+ }
+ rc = 0
+ }
+
+ tpe, _ := response.Typify(rrw.Msg, time.Now().UTC())
+ class := response.Classify(tpe)
+ if rule.Class == response.All || rule.Class == class {
+ rep := replacer.New(r, rrw, CommonLogEmptyValue)
+ rule.Log.Println(rep.Replace(rule.Format))
+ }
+
+ 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 response.Class
+ OutputFile string
+ Format string
+ Log *log.Logger
+}
+
+const (
+ // DefaultLogFilename is the default output name. This is the only supported value.
+ DefaultLogFilename = "stdout"
+ // CommonLogFormat is the common log format.
+ CommonLogFormat = `{remote} ` + CommonLogEmptyValue + ` [{when}] "{type} {class} {name} {proto} {size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {duration}`
+ // CommonLogEmptyValue is the common empty log value.
+ CommonLogEmptyValue = "-"
+ // CombinedLogFormat is the combined log format.
+ CombinedLogFormat = CommonLogFormat + ` "{>opcode}"`
+ // DefaultLogFormat is the default log format.
+ DefaultLogFormat = CommonLogFormat
+)
diff --git a/plugin/log/log_test.go b/plugin/log/log_test.go
new file mode 100644
index 000000000..ee1201a13
--- /dev/null
+++ b/plugin/log/log_test.go
@@ -0,0 +1,101 @@
+package log
+
+import (
+ "bytes"
+ "log"
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/pkg/response"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestLoggedStatus(t *testing.T) {
+ var f bytes.Buffer
+ rule := Rule{
+ NameScope: ".",
+ Format: DefaultLogFormat,
+ Log: log.New(&f, "", 0),
+ }
+
+ logger := Logger{
+ Rules: []Rule{rule},
+ Next: test.ErrorHandler(),
+ }
+
+ ctx := context.TODO()
+ r := new(dns.Msg)
+ r.SetQuestion("example.org.", dns.TypeA)
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+
+ rcode, _ := logger.ServeDNS(ctx, rec, r)
+ if rcode != 0 {
+ t.Errorf("Expected rcode to be 0 - 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) {
+ var f bytes.Buffer
+ rule := Rule{
+ NameScope: ".",
+ Format: DefaultLogFormat,
+ Log: log.New(&f, "", 0),
+ Class: response.Denial,
+ }
+
+ logger := Logger{
+ Rules: []Rule{rule},
+ Next: test.ErrorHandler(),
+ }
+
+ ctx := context.TODO()
+ r := new(dns.Msg)
+ r.SetQuestion("example.org.", dns.TypeA)
+
+ rec := dnsrecorder.New(&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) {
+ var f bytes.Buffer
+ rule := Rule{
+ NameScope: ".",
+ Format: DefaultLogFormat,
+ Log: log.New(&f, "", 0),
+ Class: response.Error,
+ }
+
+ logger := Logger{
+ Rules: []Rule{rule},
+ Next: test.ErrorHandler(),
+ }
+
+ ctx := context.TODO()
+ r := new(dns.Msg)
+ r.SetQuestion("example.org.", dns.TypeA)
+
+ rec := dnsrecorder.New(&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)
+ }
+}
diff --git a/plugin/log/setup.go b/plugin/log/setup.go
new file mode 100644
index 000000000..673962f10
--- /dev/null
+++ b/plugin/log/setup.go
@@ -0,0 +1,116 @@
+package log
+
+import (
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/response"
+
+ "github.com/mholt/caddy"
+ "github.com/miekg/dns"
+)
+
+func init() {
+ caddy.RegisterPlugin("log", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ rules, err := logParse(c)
+ if err != nil {
+ return plugin.Error("log", err)
+ }
+
+ // Open the log files for writing when the server starts
+ c.OnStartup(func() error {
+ for i := 0; i < len(rules); i++ {
+ // We only support stdout
+ writer := os.Stdout
+ if rules[i].OutputFile != "stdout" {
+ return plugin.Error("log", fmt.Errorf("invalid log file: %s", rules[i].OutputFile))
+ }
+
+ rules[i].Log = log.New(writer, "", 0)
+ }
+
+ return nil
+ })
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ return Logger{Next: next, Rules: rules, ErrorFunc: dnsserver.DefaultErrorFunc}
+ })
+
+ return nil
+}
+
+func logParse(c *caddy.Controller) ([]Rule, error) {
+ var rules []Rule
+
+ for c.Next() {
+ args := c.RemainingArgs()
+
+ if len(args) == 0 {
+ // Nothing specified; use defaults
+ rules = append(rules, Rule{
+ NameScope: ".",
+ OutputFile: DefaultLogFilename,
+ Format: DefaultLogFormat,
+ })
+ } else if len(args) == 1 {
+ // Only an output file specified.
+ rules = append(rules, Rule{
+ NameScope: ".",
+ OutputFile: args[0],
+ Format: DefaultLogFormat,
+ })
+ } else {
+ // Name scope, output file, and maybe a format specified
+
+ format := DefaultLogFormat
+
+ if len(args) > 2 {
+ switch args[2] {
+ case "{common}":
+ format = CommonLogFormat
+ case "{combined}":
+ format = CombinedLogFormat
+ default:
+ format = args[2]
+ }
+ }
+
+ rules = append(rules, Rule{
+ NameScope: dns.Fqdn(args[0]),
+ OutputFile: args[1],
+ Format: format,
+ })
+ }
+
+ // Class refinements in an extra block.
+ for c.NextBlock() {
+ switch c.Val() {
+ // class followed by all, denial, error or success.
+ case "class":
+ classes := c.RemainingArgs()
+ if len(classes) == 0 {
+ return nil, c.ArgErr()
+ }
+ cls, err := response.ClassFromString(classes[0])
+ if err != nil {
+ return nil, err
+ }
+ // update class and the last added Rule (bit icky)
+ rules[len(rules)-1].Class = cls
+ default:
+ return nil, c.ArgErr()
+ }
+ }
+ }
+
+ return rules, nil
+}
diff --git a/plugin/log/setup_test.go b/plugin/log/setup_test.go
new file mode 100644
index 000000000..161f674be
--- /dev/null
+++ b/plugin/log/setup_test.go
@@ -0,0 +1,130 @@
+package log
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/response"
+
+ "github.com/mholt/caddy"
+)
+
+func TestLogParse(t *testing.T) {
+ tests := []struct {
+ inputLogRules string
+ shouldErr bool
+ expectedLogRules []Rule
+ }{
+ {`log`, false, []Rule{{
+ NameScope: ".",
+ OutputFile: DefaultLogFilename,
+ Format: DefaultLogFormat,
+ }}},
+ {`log log.txt`, false, []Rule{{
+ NameScope: ".",
+ OutputFile: "log.txt",
+ Format: DefaultLogFormat,
+ }}},
+ {`log example.org log.txt`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "log.txt",
+ Format: DefaultLogFormat,
+ }}},
+ {`log example.org. stdout`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "stdout",
+ Format: DefaultLogFormat,
+ }}},
+ {`log example.org log.txt {common}`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "log.txt",
+ Format: CommonLogFormat,
+ }}},
+ {`log example.org accesslog.txt {combined}`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "accesslog.txt",
+ Format: CombinedLogFormat,
+ }}},
+ {`log example.org. log.txt
+ log example.net accesslog.txt {combined}`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "log.txt",
+ Format: DefaultLogFormat,
+ }, {
+ NameScope: "example.net.",
+ OutputFile: "accesslog.txt",
+ Format: CombinedLogFormat,
+ }}},
+ {`log example.org stdout {host}
+ log example.org log.txt {when}`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "stdout",
+ Format: "{host}",
+ }, {
+ NameScope: "example.org.",
+ OutputFile: "log.txt",
+ Format: "{when}",
+ }}},
+
+ {`log example.org log.txt {
+ class all
+ }`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "log.txt",
+ Format: CommonLogFormat,
+ Class: response.All,
+ }}},
+ {`log example.org log.txt {
+ class denial
+ }`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "log.txt",
+ Format: CommonLogFormat,
+ Class: response.Denial,
+ }}},
+ {`log {
+ class denial
+ }`, false, []Rule{{
+ NameScope: ".",
+ OutputFile: DefaultLogFilename,
+ Format: CommonLogFormat,
+ Class: response.Denial,
+ }}},
+ }
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.inputLogRules)
+ actualLogRules, err := logParse(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)
+ }
+ 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 to be %s , but got %s",
+ i, j, test.expectedLogRules[j].NameScope, actualLogRule.NameScope)
+ }
+
+ if actualLogRule.OutputFile != test.expectedLogRules[j].OutputFile {
+ t.Errorf("Test %d expected %dth LogRule OutputFile to be %s , but got %s",
+ i, j, test.expectedLogRules[j].OutputFile, actualLogRule.OutputFile)
+ }
+
+ if actualLogRule.Format != test.expectedLogRules[j].Format {
+ t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s",
+ i, j, test.expectedLogRules[j].Format, actualLogRule.Format)
+ }
+
+ if actualLogRule.Class != test.expectedLogRules[j].Class {
+ t.Errorf("Test %d expected %dth LogRule Class to be %s , but got %s",
+ i, j, test.expectedLogRules[j].Class, actualLogRule.Class)
+ }
+ }
+ }
+
+}
diff --git a/plugin/metrics/README.md b/plugin/metrics/README.md
new file mode 100644
index 000000000..ead1f7e75
--- /dev/null
+++ b/plugin/metrics/README.md
@@ -0,0 +1,53 @@
+# prometheus
+
+This module enables prometheus metrics for CoreDNS.
+
+The default location for the metrics is `localhost:9153`. The metrics path is fixed to `/metrics`.
+The following metrics are exported:
+
+* coredns_dns_request_count_total{zone, proto, family}
+* coredns_dns_request_duration_milliseconds{zone}
+* coredns_dns_request_size_bytes{zone, proto}
+* coredns_dns_request_do_count_total{zone}
+* coredns_dns_request_type_count_total{zone, type}
+* coredns_dns_response_size_bytes{zone, proto}
+* coredns_dns_response_rcode_count_total{zone, rcode}
+
+Each counter has a label `zone` which is the zonename used for the request/response.
+
+Extra labels used are:
+
+* `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, IXFR, AXFR and ANY) and "other" which lumps together all
+ other types.
+* The `response_rcode_count_total` has an extra label `rcode` which holds the rcode of the response.
+
+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).
+
+
+## Syntax
+
+~~~
+prometheus [ADDRESS]
+~~~
+
+For each zone that you want to see metrics for.
+
+It optionally takes an address to which the metrics are exported; the default
+is `localhost:9153`. The metrics path is fixed to `/metrics`.
+
+## Examples
+
+Use an alternative address:
+
+~~~
+prometheus localhost:9253
+~~~
+
+# Bugs
+
+When reloading, we keep the handler running, meaning that any changes to the handler's address
+aren't picked up. You'll need to restart CoreDNS for that to happen.
diff --git a/plugin/metrics/handler.go b/plugin/metrics/handler.go
new file mode 100644
index 000000000..bc9a6ec47
--- /dev/null
+++ b/plugin/metrics/handler.go
@@ -0,0 +1,34 @@
+package metrics
+
+import (
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/metrics/vars"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/pkg/rcode"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// 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 := dnsrecorder.New(w)
+ status, err := plugin.NextOrFailure(m.Name(), m.Next, ctx, rw, r)
+
+ vars.Report(state, zone, rcode.ToString(rw.Rcode), rw.Len, rw.Start)
+
+ return status, err
+}
+
+// Name implements the Handler interface.
+func (m *Metrics) Name() string { return "prometheus" }
diff --git a/plugin/metrics/metrics.go b/plugin/metrics/metrics.go
new file mode 100644
index 000000000..0dabcdf96
--- /dev/null
+++ b/plugin/metrics/metrics.go
@@ -0,0 +1,101 @@
+// Package metrics implement a handler and plugin that provides Prometheus metrics.
+package metrics
+
+import (
+ "log"
+ "net"
+ "net/http"
+ "sync"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/metrics/vars"
+
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+func init() {
+ prometheus.MustRegister(vars.RequestCount)
+ prometheus.MustRegister(vars.RequestDuration)
+ prometheus.MustRegister(vars.RequestSize)
+ prometheus.MustRegister(vars.RequestDo)
+ prometheus.MustRegister(vars.RequestType)
+
+ prometheus.MustRegister(vars.ResponseSize)
+ prometheus.MustRegister(vars.ResponseRcode)
+}
+
+// Metrics holds the prometheus configuration. The metrics' path is fixed to be /metrics
+type Metrics struct {
+ Next plugin.Handler
+ Addr string
+ ln net.Listener
+ mux *http.ServeMux
+
+ zoneNames []string
+ zoneMap map[string]bool
+ zoneMu sync.RWMutex
+}
+
+// AddZone adds zone z to m.
+func (m *Metrics) AddZone(z string) {
+ m.zoneMu.Lock()
+ m.zoneMap[z] = true
+ 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 := net.Listen("tcp", m.Addr)
+ if err != nil {
+ log.Printf("[ERROR] Failed to start metrics handler: %s", err)
+ return err
+ }
+
+ m.ln = ln
+ ListenAddr = m.ln.Addr().String()
+
+ m.mux = http.NewServeMux()
+ m.mux.Handle("/metrics", prometheus.Handler())
+
+ go func() {
+ http.Serve(m.ln, m.mux)
+ }()
+ return nil
+}
+
+// OnShutdown tears down the metrics on shutdown and restart.
+func (m *Metrics) OnShutdown() error {
+ if m.ln != nil {
+ return m.ln.Close()
+ }
+ return nil
+}
+
+func keys(m map[string]bool) []string {
+ sx := []string{}
+ for k := range m {
+ sx = append(sx, k)
+ }
+ return sx
+}
+
+// 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
diff --git a/plugin/metrics/metrics_test.go b/plugin/metrics/metrics_test.go
new file mode 100644
index 000000000..f5a17607c
--- /dev/null
+++ b/plugin/metrics/metrics_test.go
@@ -0,0 +1,83 @@
+package metrics
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin"
+ mtest "github.com/coredns/coredns/plugin/metrics/test"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestMetrics(t *testing.T) {
+ met := &Metrics{Addr: "localhost:0", zoneMap: make(map[string]bool)}
+ if err := met.OnStartup(); err != nil {
+ t.Fatalf("Failed to start metrics handler: %s", err)
+ }
+ defer met.OnShutdown()
+
+ 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_request_count_total",
+ expectedValue: "1",
+ },
+ {
+ next: test.NextHandler(dns.RcodeSuccess, nil),
+ qname: "example.org",
+ metric: "coredns_dns_request_count_total",
+ expectedValue: "2",
+ },
+ {
+ next: test.NextHandler(dns.RcodeSuccess, nil),
+ qname: "example.org",
+ metric: "coredns_dns_request_type_count_total",
+ expectedValue: "3",
+ },
+ {
+ next: test.NextHandler(dns.RcodeSuccess, nil),
+ qname: "example.org",
+ metric: "coredns_dns_response_rcode_count_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(dns.Fqdn(tc.qname), tc.qtype)
+ met.Next = tc.next
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := met.ServeDNS(ctx, rec, req)
+ if err != nil {
+ t.Fatalf("Test %d: Expected no error, but got %s", i, err)
+ }
+
+ result := mtest.Scrape(t, "http://"+ListenAddr+"/metrics")
+
+ if tc.expectedValue != "" {
+ got, _ := mtest.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/plugin/metrics/setup.go b/plugin/metrics/setup.go
new file mode 100644
index 000000000..eecfac62c
--- /dev/null
+++ b/plugin/metrics/setup.go
@@ -0,0 +1,100 @@
+package metrics
+
+import (
+ "net"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("prometheus", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+
+ uniqAddr = addrs{a: make(map[string]int)}
+}
+
+func setup(c *caddy.Controller) error {
+ m, err := prometheusParse(c)
+ if err != nil {
+ return plugin.Error("prometheus", err)
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ m.Next = next
+ return m
+ })
+
+ for a, v := range uniqAddr.a {
+ if v == todo {
+ // During restarts we will keep this handler running, BUG.
+ c.OncePerServerBlock(m.OnStartup)
+ }
+ uniqAddr.a[a] = done
+ }
+ c.OnFinalShutdown(m.OnShutdown)
+
+ return nil
+}
+
+func prometheusParse(c *caddy.Controller) (*Metrics, error) {
+ var (
+ met = &Metrics{Addr: addr, zoneMap: make(map[string]bool)}
+ err error
+ )
+
+ defer func() {
+ uniqAddr.SetAddress(met.Addr)
+ }()
+
+ for c.Next() {
+ if len(met.ZoneNames()) > 0 {
+ return met, c.Err("can only have one metrics module per server")
+ }
+
+ for _, z := range c.ServerBlockKeys {
+ met.AddZone(plugin.Host(z).Normalize())
+ }
+ 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, err
+}
+
+var uniqAddr addrs
+
+// Keep track on which addrs we listen, so we only start one listener.
+type addrs struct {
+ a map[string]int
+}
+
+func (a *addrs) SetAddress(addr string) {
+ // If already there and set to done, we've already started this listener.
+ if a.a[addr] == done {
+ return
+ }
+ a.a[addr] = todo
+}
+
+// Addr is the address the where the metrics are exported by default.
+const addr = "localhost:9153"
+
+const (
+ todo = 1
+ done = 2
+)
diff --git a/plugin/metrics/setup_test.go b/plugin/metrics/setup_test.go
new file mode 100644
index 000000000..73555427e
--- /dev/null
+++ b/plugin/metrics/setup_test.go
@@ -0,0 +1,42 @@
+package metrics
+
+import (
+ "testing"
+
+ "github.com/mholt/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 := prometheusParse(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/plugin/metrics/test/scrape.go b/plugin/metrics/test/scrape.go
new file mode 100644
index 000000000..a21c0061d
--- /dev/null
+++ b/plugin/metrics/test/scrape.go
@@ -0,0 +1,225 @@
+// 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"
+ "testing"
+
+ "github.com/matttproud/golang_protobuf_extensions/pbutil"
+ "github.com/prometheus/common/expfmt"
+
+ dto "github.com/prometheus/client_model/go"
+)
+
+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(t *testing.T, 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
+}
+
+// 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/plugin/metrics/vars/report.go b/plugin/metrics/vars/report.go
new file mode 100644
index 000000000..5d8f2ba64
--- /dev/null
+++ b/plugin/metrics/vars/report.go
@@ -0,0 +1,62 @@
+package vars
+
+import (
+ "time"
+
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// Report reports the metrics data associcated with request.
+func Report(req request.Request, zone, rcode string, size int, start time.Time) {
+ // Proto and Family.
+ net := req.Proto()
+ fam := "1"
+ if req.Family() == 2 {
+ fam = "2"
+ }
+
+ typ := req.QType()
+
+ RequestCount.WithLabelValues(zone, net, fam).Inc()
+ RequestDuration.WithLabelValues(zone).Observe(float64(time.Since(start) / time.Millisecond))
+
+ if req.Do() {
+ RequestDo.WithLabelValues(zone).Inc()
+ }
+
+ if _, known := monitorType[typ]; known {
+ RequestType.WithLabelValues(zone, dns.Type(typ).String()).Inc()
+ } else {
+ RequestType.WithLabelValues(zone, other).Inc()
+ }
+
+ ResponseSize.WithLabelValues(zone, net).Observe(float64(size))
+ RequestSize.WithLabelValues(zone, net).Observe(float64(req.Len()))
+
+ ResponseRcode.WithLabelValues(zone, rcode).Inc()
+}
+
+var monitorType = map[uint16]bool{
+ dns.TypeAAAA: true,
+ dns.TypeA: true,
+ dns.TypeCNAME: true,
+ dns.TypeDNSKEY: true,
+ dns.TypeDS: true,
+ dns.TypeMX: true,
+ dns.TypeNSEC3: true,
+ dns.TypeNSEC: true,
+ dns.TypeNS: true,
+ dns.TypePTR: true,
+ dns.TypeRRSIG: true,
+ dns.TypeSOA: true,
+ dns.TypeSRV: true,
+ dns.TypeTXT: true,
+ // Meta Qtypes
+ dns.TypeIXFR: true,
+ dns.TypeAXFR: true,
+ dns.TypeANY: true,
+}
+
+const other = "other"
diff --git a/plugin/metrics/vars/vars.go b/plugin/metrics/vars/vars.go
new file mode 100644
index 000000000..826f9ebed
--- /dev/null
+++ b/plugin/metrics/vars/vars.go
@@ -0,0 +1,69 @@
+package vars
+
+import (
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+// Request* and Response* are the prometheus counters and gauges we are using for exporting metrics.
+var (
+ RequestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "request_count_total",
+ Help: "Counter of DNS requests made per zone, protocol and family.",
+ }, []string{"zone", "proto", "family"})
+
+ RequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "request_duration_milliseconds",
+ Buckets: append(prometheus.DefBuckets, []float64{50, 100, 200, 500, 1000, 2000, 3000, 4000, 5000, 10000}...),
+ Help: "Histogram of the time (in milliseconds) each request took.",
+ }, []string{"zone"})
+
+ RequestSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "request_size_bytes",
+ Help: "Size of the EDNS0 UDP buffer in bytes (64K for TCP).",
+ Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3},
+ }, []string{"zone", "proto"})
+
+ RequestDo = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "request_do_count_total",
+ Help: "Counter of DNS requests with DO bit set per zone.",
+ }, []string{"zone"})
+
+ RequestType = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "request_type_count_total",
+ Help: "Counter of DNS requests per type, per zone.",
+ }, []string{"zone", "type"})
+
+ ResponseSize = prometheus.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{"zone", "proto"})
+
+ ResponseRcode = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: subsystem,
+ Name: "response_rcode_count_total",
+ Help: "Counter of response status codes.",
+ }, []string{"zone", "rcode"})
+)
+
+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/plugin/normalize.go b/plugin/normalize.go
new file mode 100644
index 000000000..75b9c53c8
--- /dev/null
+++ b/plugin/normalize.go
@@ -0,0 +1,137 @@
+package plugin
+
+import (
+ "fmt"
+ "net"
+ "strconv"
+ "strings"
+
+ "github.com/miekg/dns"
+)
+
+// See core/dnsserver/address.go - we should unify these two impls.
+
+// Zones respresents a lists of zone names.
+type Zones []string
+
+// Matches checks is qname is a subdomain of any of the zones in z. The match
+// will return the most specific zones that matches other. 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.
+func (h Host) Normalize() string {
+
+ s := string(h)
+
+ switch {
+ case strings.HasPrefix(s, TransportTLS+"://"):
+ s = s[len(TransportTLS+"://"):]
+ case strings.HasPrefix(s, TransportDNS+"://"):
+ s = s[len(TransportDNS+"://"):]
+ case strings.HasPrefix(s, TransportGRPC+"://"):
+ s = s[len(TransportGRPC+"://"):]
+ }
+
+ // The error can be ignore here, because this function is called after the corefile
+ // has already been vetted.
+ host, _, _ := SplitHostPort(s)
+ return Name(host).Normalize()
+}
+
+// SplitHostPort splits s up in a host and port portion, taking reverse address notation into account.
+// String the string s should *not* be prefixed with any protocols, i.e. dns://
+func SplitHostPort(s string) (host, 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 its a number.
+ host = s
+
+ colon := strings.LastIndex(s, ":")
+ if colon == len(s)-1 {
+ return "", "", 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)
+ host = s[:colon]
+ }
+ }
+
+ // TODO(miek): this should take escaping into account.
+ if len(host) > 255 {
+ return "", "", fmt.Errorf("specified zone is too long: %d > 255", len(host))
+ }
+
+ _, d := dns.IsDomainName(host)
+ if !d {
+ return "", "", fmt.Errorf("zone is not a valid domain name: %s", host)
+ }
+
+ // Check if it parses as a reverse zone, if so we use that. Must be fully
+ // specified IP and mask and mask % 8 = 0.
+ ip, net, err := net.ParseCIDR(host)
+ if err == nil {
+ if rev, e := dns.ReverseAddr(ip.String()); e == nil {
+ ones, bits := net.Mask.Size()
+ if (bits-ones)%8 == 0 {
+ offset, end := 0, false
+ for i := 0; i < (bits-ones)/8; i++ {
+ offset, end = dns.NextLabel(rev, offset)
+ if end {
+ break
+ }
+ }
+ host = rev[offset:]
+ }
+ }
+ }
+ return host, port, nil
+}
+
+// Duplicated from core/dnsserver/address.go !
+const (
+ TransportDNS = "dns"
+ TransportTLS = "tls"
+ TransportGRPC = "grpc"
+)
diff --git a/plugin/normalize_test.go b/plugin/normalize_test.go
new file mode 100644
index 000000000..3eb9c5231
--- /dev/null
+++ b/plugin/normalize_test.go
@@ -0,0 +1,84 @@
+package plugin
+
+import "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\n", 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\n", expected, actual)
+ }
+ }
+}
+
+func TestHostNormalize(t *testing.T) {
+ hosts := []string{".:53", ".", "example.org:53", "example.org.", "example.org.:53", "example.org.",
+ "10.0.0.0/8:53", "10.in-addr.arpa.", "10.0.0.0/9", "10.0.0.0/9.",
+ "dns://example.org", "example.org."}
+
+ for i := 0; i < len(hosts); i += 2 {
+ ts := hosts[i]
+ expected := hosts[i+1]
+ actual := Host(ts).Normalize()
+ if expected != actual {
+ t.Errorf("Expected %v, got %v\n", expected, actual)
+ }
+ }
+}
diff --git a/plugin/pkg/cache/cache.go b/plugin/pkg/cache/cache.go
new file mode 100644
index 000000000..56cae2180
--- /dev/null
+++ b/plugin/pkg/cache/cache.go
@@ -0,0 +1,129 @@
+// 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) uint32 {
+ h := fnv.New32()
+ h.Write(what)
+ return h.Sum32()
+}
+
+// Cache is cache.
+type Cache struct {
+ shards [shardSize]*shard
+}
+
+// shard is a cache with random eviction.
+type shard struct {
+ items map[uint32]interface{}
+ size int
+
+ sync.RWMutex
+}
+
+// New returns a new cache.
+func New(size int) *Cache {
+ ssize := size / shardSize
+ if ssize < 512 {
+ ssize = 512
+ }
+
+ 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.
+func (c *Cache) Add(key uint32, el interface{}) {
+ shard := key & (shardSize - 1)
+ c.shards[shard].Add(key, el)
+}
+
+// Get looks up element index under key.
+func (c *Cache) Get(key uint32) (interface{}, bool) {
+ shard := key & (shardSize - 1)
+ return c.shards[shard].Get(key)
+}
+
+// Remove removes the element indexed with key.
+func (c *Cache) Remove(key uint32) {
+ 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
+}
+
+// newShard returns a new shard with size.
+func newShard(size int) *shard { return &shard{items: make(map[uint32]interface{}), size: size} }
+
+// Add adds element indexed by key into the cache. Any existing element is overwritten
+func (s *shard) Add(key uint32, el interface{}) {
+ l := s.Len()
+ if l+1 > s.size {
+ s.Evict()
+ }
+
+ s.Lock()
+ s.items[key] = el
+ s.Unlock()
+}
+
+// Remove removes the element indexed by key from the cache.
+func (s *shard) Remove(key uint32) {
+ s.Lock()
+ delete(s.items, key)
+ s.Unlock()
+}
+
+// Evict removes a random element from the cache.
+func (s *shard) Evict() {
+ s.Lock()
+ defer s.Unlock()
+
+ key := -1
+ for k := range s.items {
+ key = int(k)
+ break
+ }
+ if key == -1 {
+ // empty cache
+ return
+ }
+ delete(s.items, uint32(key))
+}
+
+// Get looks up the element indexed under key.
+func (s *shard) Get(key uint32) (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
+}
+
+const shardSize = 256
diff --git a/plugin/pkg/cache/cache_test.go b/plugin/pkg/cache/cache_test.go
new file mode 100644
index 000000000..2c92bf438
--- /dev/null
+++ b/plugin/pkg/cache/cache_test.go
@@ -0,0 +1,31 @@
+package cache
+
+import "testing"
+
+func TestCacheAddAndGet(t *testing.T) {
+ c := New(4)
+ c.Add(1, 1)
+
+ if _, found := c.Get(1); !found {
+ t.Fatal("Failed to find inserted record")
+ }
+}
+
+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)
+ }
+}
diff --git a/plugin/pkg/cache/shard_test.go b/plugin/pkg/cache/shard_test.go
new file mode 100644
index 000000000..26675cee1
--- /dev/null
+++ b/plugin/pkg/cache/shard_test.go
@@ -0,0 +1,60 @@
+package cache
+
+import "testing"
+
+func TestShardAddAndGet(t *testing.T) {
+ s := newShard(4)
+ s.Add(1, 1)
+
+ if _, found := s.Get(1); !found {
+ t.Fatal("Failed to find inserted record")
+ }
+}
+
+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)
+ }
+}
diff --git a/plugin/pkg/dnsrecorder/recorder.go b/plugin/pkg/dnsrecorder/recorder.go
new file mode 100644
index 000000000..3ca5f00d0
--- /dev/null
+++ b/plugin/pkg/dnsrecorder/recorder.go
@@ -0,0 +1,58 @@
+// Package dnsrecorder allows you to record a DNS response when it is send to the client.
+package dnsrecorder
+
+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
+}
+
+// New 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 New(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
+}
+
+// Hijack implements dns.Hijacker. It simply wraps the underlying
+// ResponseWriter's Hijack method if there is one, or returns an error.
+func (r *Recorder) Hijack() { r.ResponseWriter.Hijack(); return }
diff --git a/plugin/pkg/dnsrecorder/recorder_test.go b/plugin/pkg/dnsrecorder/recorder_test.go
new file mode 100644
index 000000000..c9c2f6ce4
--- /dev/null
+++ b/plugin/pkg/dnsrecorder/recorder_test.go
@@ -0,0 +1,28 @@
+package dnsrecorder
+
+/*
+func TestNewResponseRecorder(t *testing.T) {
+ w := httptest.NewRecorder()
+ recordRequest := NewResponseRecorder(w)
+ if !(recordRequest.ResponseWriter == w) {
+ t.Fatalf("Expected Response writer in the Recording to be same as the one sent\n")
+ }
+ if recordRequest.status != http.StatusOK {
+ t.Fatalf("Expected recorded status to be http.StatusOK (%d) , but found %d\n ", http.StatusOK, recordRequest.status)
+ }
+}
+
+func TestWrite(t *testing.T) {
+ w := httptest.NewRecorder()
+ responseTestString := "test"
+ recordRequest := NewResponseRecorder(w)
+ buf := []byte(responseTestString)
+ recordRequest.Write(buf)
+ if recordRequest.size != len(buf) {
+ t.Fatalf("Expected the bytes written counter to be %d, but instead found %d\n", len(buf), recordRequest.size)
+ }
+ if w.Body.String() != responseTestString {
+ t.Fatalf("Expected Response Body to be %s , but found %s\n", responseTestString, w.Body.String())
+ }
+}
+*/
diff --git a/plugin/pkg/dnsutil/cname.go b/plugin/pkg/dnsutil/cname.go
new file mode 100644
index 000000000..281e03218
--- /dev/null
+++ b/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/plugin/pkg/dnsutil/cname_test.go b/plugin/pkg/dnsutil/cname_test.go
new file mode 100644
index 000000000..5fb8d3029
--- /dev/null
+++ b/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/plugin/pkg/dnsutil/dedup.go b/plugin/pkg/dnsutil/dedup.go
new file mode 100644
index 000000000..dae656a01
--- /dev/null
+++ b/plugin/pkg/dnsutil/dedup.go
@@ -0,0 +1,12 @@
+package dnsutil
+
+import "github.com/miekg/dns"
+
+// Dedup de-duplicates a message.
+func Dedup(m *dns.Msg) *dns.Msg {
+ // TODO(miek): expensive!
+ m.Answer = dns.Dedup(m.Answer, nil)
+ m.Ns = dns.Dedup(m.Ns, nil)
+ m.Extra = dns.Dedup(m.Extra, nil)
+ return m
+}
diff --git a/plugin/pkg/dnsutil/doc.go b/plugin/pkg/dnsutil/doc.go
new file mode 100644
index 000000000..75d1e8c7a
--- /dev/null
+++ b/plugin/pkg/dnsutil/doc.go
@@ -0,0 +1,2 @@
+// Package dnsutil contains DNS related helper functions.
+package dnsutil
diff --git a/plugin/pkg/dnsutil/host.go b/plugin/pkg/dnsutil/host.go
new file mode 100644
index 000000000..aaab586e8
--- /dev/null
+++ b/plugin/pkg/dnsutil/host.go
@@ -0,0 +1,82 @@
+package dnsutil
+
+import (
+ "fmt"
+ "net"
+ "os"
+
+ "github.com/miekg/dns"
+)
+
+// ParseHostPortOrFile parses the strings in s, each string can either be a address,
+// address:port or a filename. The address part is checked and the filename case a
+// resolv.conf like file is parsed and the nameserver found are returned.
+func ParseHostPortOrFile(s ...string) ([]string, error) {
+ var servers []string
+ for _, host := range s {
+ addr, _, err := net.SplitHostPort(host)
+ if err != nil {
+ // Parse didn't work, it is not a addr:port combo
+ if net.ParseIP(host) == nil {
+ // Not an IP address.
+ ss, err := tryFile(host)
+ if err == nil {
+ servers = append(servers, ss...)
+ continue
+ }
+ return servers, fmt.Errorf("not an IP address or file: %q", host)
+ }
+ ss := net.JoinHostPort(host, "53")
+ servers = append(servers, ss)
+ continue
+ }
+
+ if net.ParseIP(addr) == nil {
+ // No an IP address.
+ 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, host)
+ }
+ 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
+}
+
+// ParseHostPort 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 ParseHostPort(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/plugin/pkg/dnsutil/host_test.go b/plugin/pkg/dnsutil/host_test.go
new file mode 100644
index 000000000..cc55f4570
--- /dev/null
+++ b/plugin/pkg/dnsutil/host_test.go
@@ -0,0 +1,85 @@
+package dnsutil
+
+import (
+ "io/ioutil"
+ "os"
+ "testing"
+)
+
+func TestParseHostPortOrFile(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,
+ },
+ }
+
+ err := ioutil.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 := ParseHostPortOrFile(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 := ParseHostPort(tc.in, "53")
+ 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/plugin/pkg/dnsutil/join.go b/plugin/pkg/dnsutil/join.go
new file mode 100644
index 000000000..515bf3dad
--- /dev/null
+++ b/plugin/pkg/dnsutil/join.go
@@ -0,0 +1,19 @@
+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] == "." {
+ s := strings.Join(labels[:ll-1], ".")
+ return dns.Fqdn(s)
+ }
+ s := strings.Join(labels, ".")
+ return dns.Fqdn(s)
+}
diff --git a/plugin/pkg/dnsutil/join_test.go b/plugin/pkg/dnsutil/join_test.go
new file mode 100644
index 000000000..26eeb5897
--- /dev/null
+++ b/plugin/pkg/dnsutil/join_test.go
@@ -0,0 +1,20 @@
+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{"."}, "."},
+ }
+
+ 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/plugin/pkg/dnsutil/reverse.go b/plugin/pkg/dnsutil/reverse.go
new file mode 100644
index 000000000..daf9cc600
--- /dev/null
+++ b/plugin/pkg/dnsutil/reverse.go
@@ -0,0 +1,68 @@
+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
+// failes the empty string is returned.
+func ExtractAddressFromReverse(reverseName string) string {
+ search := ""
+
+ f := reverse
+
+ switch {
+ case strings.HasSuffix(reverseName, v4arpaSuffix):
+ search = strings.TrimSuffix(reverseName, v4arpaSuffix)
+ case strings.HasSuffix(reverseName, v6arpaSuffix):
+ search = strings.TrimSuffix(reverseName, v6arpaSuffix)
+ f = reverse6
+ default:
+ return ""
+ }
+
+ // Reverse the segments and then combine them.
+ return f(strings.Split(search, "."))
+}
+
+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 (
+ // v4arpaSuffix is the reverse tree suffix for v4 IP addresses.
+ v4arpaSuffix = ".in-addr.arpa."
+ // v6arpaSuffix is the reverse tree suffix for v6 IP addresses.
+ v6arpaSuffix = ".ip6.arpa."
+)
diff --git a/plugin/pkg/dnsutil/reverse_test.go b/plugin/pkg/dnsutil/reverse_test.go
new file mode 100644
index 000000000..25bd897ac
--- /dev/null
+++ b/plugin/pkg/dnsutil/reverse_test.go
@@ -0,0 +1,51 @@
+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)
+ }
+ }
+}
diff --git a/plugin/pkg/dnsutil/zone.go b/plugin/pkg/dnsutil/zone.go
new file mode 100644
index 000000000..579fef1ba
--- /dev/null
+++ b/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/plugin/pkg/dnsutil/zone_test.go b/plugin/pkg/dnsutil/zone_test.go
new file mode 100644
index 000000000..81cd1adad
--- /dev/null
+++ b/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/plugin/pkg/edns/edns.go b/plugin/pkg/edns/edns.go
new file mode 100644
index 000000000..3f0ea5e16
--- /dev/null
+++ b/plugin/pkg/edns/edns.go
@@ -0,0 +1,46 @@
+// Package edns provides function useful for adding/inspecting OPT records to/in messages.
+package edns
+
+import (
+ "errors"
+
+ "github.com/miekg/dns"
+)
+
+// 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)
+ 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 int) int {
+ if proto == "tcp" {
+ return dns.MaxMsgSize
+ }
+ if size < dns.MinMsgSize {
+ return dns.MinMsgSize
+ }
+ return size
+}
diff --git a/plugin/pkg/edns/edns_test.go b/plugin/pkg/edns/edns_test.go
new file mode 100644
index 000000000..89ac6d2ec
--- /dev/null
+++ b/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/plugin/pkg/healthcheck/healthcheck.go b/plugin/pkg/healthcheck/healthcheck.go
new file mode 100644
index 000000000..18f09087c
--- /dev/null
+++ b/plugin/pkg/healthcheck/healthcheck.go
@@ -0,0 +1,243 @@
+package healthcheck
+
+import (
+ "io"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "net/url"
+ "sync"
+ "sync/atomic"
+ "time"
+)
+
+// UpstreamHostDownFunc can be used to customize how Down behaves.
+type UpstreamHostDownFunc func(*UpstreamHost) bool
+
+// UpstreamHost represents a single proxy upstream
+type UpstreamHost struct {
+ Conns int64 // must be first field to be 64-bit aligned on 32-bit systems
+ Name string // IP address (and port) of this upstream host
+ Network string // Network (tcp, unix, etc) of the host, default "" is "tcp"
+ Fails int32
+ FailTimeout time.Duration
+ OkUntil time.Time
+ CheckDown UpstreamHostDownFunc
+ CheckURL string
+ WithoutPathPrefix string
+ Checking bool
+ CheckMu sync.Mutex
+}
+
+// Down checks whether the upstream host is down or not.
+// Down will try to use uh.CheckDown first, and will fall
+// back to some default criteria if necessary.
+func (uh *UpstreamHost) Down() bool {
+ if uh.CheckDown == nil {
+ // Default settings
+ fails := atomic.LoadInt32(&uh.Fails)
+ after := false
+
+ uh.CheckMu.Lock()
+ until := uh.OkUntil
+ uh.CheckMu.Unlock()
+
+ if !until.IsZero() && time.Now().After(until) {
+ after = true
+ }
+
+ return after || fails > 0
+ }
+ return uh.CheckDown(uh)
+}
+
+// HostPool is a collection of UpstreamHosts.
+type HostPool []*UpstreamHost
+
+// HealthCheck is used for performing healthcheck
+// on a collection of upstream hosts and select
+// one based on the policy.
+type HealthCheck struct {
+ wg sync.WaitGroup // Used to wait for running goroutines to stop.
+ stop chan struct{} // Signals running goroutines to stop.
+ Hosts HostPool
+ Policy Policy
+ Spray Policy
+ FailTimeout time.Duration
+ MaxFails int32
+ Future time.Duration
+ Path string
+ Port string
+ Interval time.Duration
+}
+
+// Start starts the healthcheck
+func (u *HealthCheck) Start() {
+ u.stop = make(chan struct{})
+ if u.Path != "" {
+ u.wg.Add(1)
+ go func() {
+ defer u.wg.Done()
+ u.healthCheckWorker(u.stop)
+ }()
+ }
+}
+
+// Stop sends a signal to all goroutines started by this staticUpstream to exit
+// and waits for them to finish before returning.
+func (u *HealthCheck) Stop() error {
+ close(u.stop)
+ u.wg.Wait()
+ return nil
+}
+
+// This was moved into a thread so that each host could throw a health
+// check at the same time. The reason for this is that if we are checking
+// 3 hosts, and the first one is gone, and we spend minutes timing out to
+// fail it, we would not have been doing any other health checks in that
+// time. So we now have a per-host lock and a threaded health check.
+//
+// We use the Checking bool to avoid concurrent checks against the same
+// host; if one is taking a long time, the next one will find a check in
+// progress and simply return before trying.
+//
+// We are carefully avoiding having the mutex locked while we check,
+// otherwise checks will back up, potentially a lot of them if a host is
+// absent for a long time. This arrangement makes checks quickly see if
+// they are the only one running and abort otherwise.
+func healthCheckURL(nextTs time.Time, host *UpstreamHost) {
+
+ // lock for our bool check. We don't just defer the unlock because
+ // we don't want the lock held while http.Get runs
+ host.CheckMu.Lock()
+
+ // are we mid check? Don't run another one
+ if host.Checking {
+ host.CheckMu.Unlock()
+ return
+ }
+
+ host.Checking = true
+ host.CheckMu.Unlock()
+
+ //log.Printf("[DEBUG] Healthchecking %s, nextTs is %s\n", url, nextTs.Local())
+
+ // fetch that url. This has been moved into a go func because
+ // when the remote host is not merely not serving, but actually
+ // absent, then tcp syn timeouts can be very long, and so one
+ // fetch could last several check intervals
+ if r, err := http.Get(host.CheckURL); err == nil {
+ io.Copy(ioutil.Discard, r.Body)
+ r.Body.Close()
+
+ if r.StatusCode < 200 || r.StatusCode >= 400 {
+ log.Printf("[WARNING] Host %s health check returned HTTP code %d\n",
+ host.Name, r.StatusCode)
+ nextTs = time.Unix(0, 0)
+ }
+ } else {
+ log.Printf("[WARNING] Host %s health check probe failed: %v\n", host.Name, err)
+ nextTs = time.Unix(0, 0)
+ }
+
+ host.CheckMu.Lock()
+ host.Checking = false
+ host.OkUntil = nextTs
+ host.CheckMu.Unlock()
+}
+
+func (u *HealthCheck) healthCheck() {
+ for _, host := range u.Hosts {
+
+ if host.CheckURL == "" {
+ var hostName, checkPort string
+
+ // The DNS server might be an HTTP server. If so, extract its name.
+ ret, err := url.Parse(host.Name)
+ if err == nil && len(ret.Host) > 0 {
+ hostName = ret.Host
+ } else {
+ hostName = host.Name
+ }
+
+ // Extract the port number from the parsed server name.
+ checkHostName, checkPort, err := net.SplitHostPort(hostName)
+ if err != nil {
+ checkHostName = hostName
+ }
+
+ if u.Port != "" {
+ checkPort = u.Port
+ }
+
+ host.CheckURL = "http://" + net.JoinHostPort(checkHostName, checkPort) + u.Path
+ }
+
+ // calculate this before the get
+ nextTs := time.Now().Add(u.Future)
+
+ // locks/bools should prevent requests backing up
+ go healthCheckURL(nextTs, host)
+ }
+}
+
+func (u *HealthCheck) healthCheckWorker(stop chan struct{}) {
+ ticker := time.NewTicker(u.Interval)
+ u.healthCheck()
+ for {
+ select {
+ case <-ticker.C:
+ u.healthCheck()
+ case <-stop:
+ ticker.Stop()
+ return
+ }
+ }
+}
+
+// Select selects an upstream host based on the policy
+// and the healthcheck result.
+func (u *HealthCheck) Select() *UpstreamHost {
+ pool := u.Hosts
+ if len(pool) == 1 {
+ if pool[0].Down() && u.Spray == nil {
+ return nil
+ }
+ return pool[0]
+ }
+ allDown := true
+ for _, host := range pool {
+ if !host.Down() {
+ allDown = false
+ break
+ }
+ }
+ if allDown {
+ if u.Spray == nil {
+ return nil
+ }
+ return u.Spray.Select(pool)
+ }
+
+ if u.Policy == nil {
+ h := (&Random{}).Select(pool)
+ if h != nil {
+ return h
+ }
+ if h == nil && u.Spray == nil {
+ return nil
+ }
+ return u.Spray.Select(pool)
+ }
+
+ h := u.Policy.Select(pool)
+ if h != nil {
+ return h
+ }
+
+ if u.Spray == nil {
+ return nil
+ }
+ return u.Spray.Select(pool)
+}
diff --git a/plugin/pkg/healthcheck/policy.go b/plugin/pkg/healthcheck/policy.go
new file mode 100644
index 000000000..6a828fc4d
--- /dev/null
+++ b/plugin/pkg/healthcheck/policy.go
@@ -0,0 +1,120 @@
+package healthcheck
+
+import (
+ "log"
+ "math/rand"
+ "sync/atomic"
+)
+
+var (
+ // SupportedPolicies is the collection of policies registered
+ SupportedPolicies = make(map[string]func() Policy)
+)
+
+// RegisterPolicy adds a custom policy to the proxy.
+func RegisterPolicy(name string, policy func() Policy) {
+ SupportedPolicies[name] = policy
+}
+
+// Policy decides how a host will be selected from a pool. When all hosts are unhealthy, it is assumed the
+// healthchecking failed. In this case each policy will *randomly* return a host from the pool to prevent
+// no traffic to go through at all.
+type Policy interface {
+ Select(pool HostPool) *UpstreamHost
+}
+
+func init() {
+ RegisterPolicy("random", func() Policy { return &Random{} })
+ RegisterPolicy("least_conn", func() Policy { return &LeastConn{} })
+ RegisterPolicy("round_robin", func() Policy { return &RoundRobin{} })
+}
+
+// Random is a policy that selects up hosts from a pool at random.
+type Random struct{}
+
+// Select selects an up host at random from the specified pool.
+func (r *Random) Select(pool HostPool) *UpstreamHost {
+ // instead of just generating a random index
+ // this is done to prevent selecting a down host
+ var randHost *UpstreamHost
+ count := 0
+ for _, host := range pool {
+ if host.Down() {
+ continue
+ }
+ count++
+ if count == 1 {
+ randHost = host
+ } else {
+ r := rand.Int() % count
+ if r == (count - 1) {
+ randHost = host
+ }
+ }
+ }
+ return randHost
+}
+
+// Spray is a policy that selects a host from a pool at random. This should be used as a last ditch
+// attempt to get a host when all hosts are reporting unhealthy.
+type Spray struct{}
+
+// Select selects an up host at random from the specified pool.
+func (r *Spray) Select(pool HostPool) *UpstreamHost {
+ rnd := rand.Int() % len(pool)
+ randHost := pool[rnd]
+ log.Printf("[WARNING] All hosts reported as down, spraying to target: %s", randHost.Name)
+ return randHost
+}
+
+// LeastConn is a policy that selects the host with the least connections.
+type LeastConn struct{}
+
+// Select selects the up host with the least number of connections in the
+// pool. If more than one host has the same least number of connections,
+// one of the hosts is chosen at random.
+func (r *LeastConn) Select(pool HostPool) *UpstreamHost {
+ var bestHost *UpstreamHost
+ count := 0
+ leastConn := int64(1<<63 - 1)
+ for _, host := range pool {
+ if host.Down() {
+ continue
+ }
+ hostConns := host.Conns
+ if hostConns < leastConn {
+ bestHost = host
+ leastConn = hostConns
+ count = 1
+ } else if hostConns == leastConn {
+ // randomly select host among hosts with least connections
+ count++
+ if count == 1 {
+ bestHost = host
+ } else {
+ r := rand.Int() % count
+ if r == (count - 1) {
+ bestHost = host
+ }
+ }
+ }
+ }
+ return bestHost
+}
+
+// RoundRobin is a policy that selects hosts based on round robin ordering.
+type RoundRobin struct {
+ Robin uint32
+}
+
+// Select selects an up host from the pool using a round robin ordering scheme.
+func (r *RoundRobin) Select(pool HostPool) *UpstreamHost {
+ poolLen := uint32(len(pool))
+ selection := atomic.AddUint32(&r.Robin, 1) % poolLen
+ host := pool[selection]
+ // if the currently selected host is down, just ffwd to up host
+ for i := uint32(1); host.Down() && i < poolLen; i++ {
+ host = pool[(selection+i)%poolLen]
+ }
+ return host
+}
diff --git a/plugin/pkg/healthcheck/policy_test.go b/plugin/pkg/healthcheck/policy_test.go
new file mode 100644
index 000000000..4c667952c
--- /dev/null
+++ b/plugin/pkg/healthcheck/policy_test.go
@@ -0,0 +1,143 @@
+package healthcheck
+
+import (
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+ "time"
+)
+
+var workableServer *httptest.Server
+
+func TestMain(m *testing.M) {
+ workableServer = httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ // do nothing
+ }))
+ r := m.Run()
+ workableServer.Close()
+ os.Exit(r)
+}
+
+type customPolicy struct{}
+
+func (r *customPolicy) Select(pool HostPool) *UpstreamHost {
+ return pool[0]
+}
+
+func testPool() HostPool {
+ pool := []*UpstreamHost{
+ {
+ Name: workableServer.URL, // this should resolve (healthcheck test)
+ },
+ {
+ Name: "http://shouldnot.resolve", // this shouldn't
+ },
+ {
+ Name: "http://C",
+ },
+ }
+ return HostPool(pool)
+}
+
+func TestRegisterPolicy(t *testing.T) {
+ name := "custom"
+ customPolicy := &customPolicy{}
+ RegisterPolicy(name, func() Policy { return customPolicy })
+ if _, ok := SupportedPolicies[name]; !ok {
+ t.Error("Expected supportedPolicies to have a custom policy.")
+ }
+
+}
+
+// TODO(miek): Disabled for now, we should get out of the habit of using
+// realtime in these tests .
+func testHealthCheck(t *testing.T) {
+ log.SetOutput(ioutil.Discard)
+
+ u := &HealthCheck{
+ Hosts: testPool(),
+ FailTimeout: 10 * time.Second,
+ Future: 60 * time.Second,
+ MaxFails: 1,
+ }
+
+ u.healthCheck()
+ // sleep a bit, it's async now
+ time.Sleep(time.Duration(2 * time.Second))
+
+ if u.Hosts[0].Down() {
+ t.Error("Expected first host in testpool to not fail healthcheck.")
+ }
+ if !u.Hosts[1].Down() {
+ t.Error("Expected second host in testpool to fail healthcheck.")
+ }
+}
+
+func TestSelect(t *testing.T) {
+ u := &HealthCheck{
+ Hosts: testPool()[:3],
+ FailTimeout: 10 * time.Second,
+ Future: 60 * time.Second,
+ MaxFails: 1,
+ }
+ u.Hosts[0].OkUntil = time.Unix(0, 0)
+ u.Hosts[1].OkUntil = time.Unix(0, 0)
+ u.Hosts[2].OkUntil = time.Unix(0, 0)
+ if h := u.Select(); h != nil {
+ t.Error("Expected select to return nil as all host are down")
+ }
+ u.Hosts[2].OkUntil = time.Time{}
+ if h := u.Select(); h == nil {
+ t.Error("Expected select to not return nil")
+ }
+}
+
+func TestRoundRobinPolicy(t *testing.T) {
+ pool := testPool()
+ rrPolicy := &RoundRobin{}
+ h := rrPolicy.Select(pool)
+ // First selected host is 1, because counter starts at 0
+ // and increments before host is selected
+ if h != pool[1] {
+ t.Error("Expected first round robin host to be second host in the pool.")
+ }
+ h = rrPolicy.Select(pool)
+ if h != pool[2] {
+ t.Error("Expected second round robin host to be third host in the pool.")
+ }
+ // mark host as down
+ pool[0].OkUntil = time.Unix(0, 0)
+ h = rrPolicy.Select(pool)
+ if h != pool[1] {
+ t.Error("Expected third round robin host to be first host in the pool.")
+ }
+}
+
+func TestLeastConnPolicy(t *testing.T) {
+ pool := testPool()
+ lcPolicy := &LeastConn{}
+ pool[0].Conns = 10
+ pool[1].Conns = 10
+ h := lcPolicy.Select(pool)
+ if h != pool[2] {
+ t.Error("Expected least connection host to be third host.")
+ }
+ pool[2].Conns = 100
+ h = lcPolicy.Select(pool)
+ if h != pool[0] && h != pool[1] {
+ t.Error("Expected least connection host to be first or second host.")
+ }
+}
+
+func TestCustomPolicy(t *testing.T) {
+ pool := testPool()
+ customPolicy := &customPolicy{}
+ h := customPolicy.Select(pool)
+ if h != pool[0] {
+ t.Error("Expected custom policy host to be the first host.")
+ }
+}
diff --git a/plugin/pkg/nonwriter/nonwriter.go b/plugin/pkg/nonwriter/nonwriter.go
new file mode 100644
index 000000000..7819a320f
--- /dev/null
+++ b/plugin/pkg/nonwriter/nonwriter.go
@@ -0,0 +1,23 @@
+// 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
+}
+
+func (w *Writer) Write(buf []byte) (int, error) { return len(buf), nil }
diff --git a/plugin/pkg/nonwriter/nonwriter_test.go b/plugin/pkg/nonwriter/nonwriter_test.go
new file mode 100644
index 000000000..d8433af55
--- /dev/null
+++ b/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/plugin/pkg/rcode/rcode.go b/plugin/pkg/rcode/rcode.go
new file mode 100644
index 000000000..32863f0b2
--- /dev/null
+++ b/plugin/pkg/rcode/rcode.go
@@ -0,0 +1,16 @@
+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/plugin/pkg/rcode/rcode_test.go b/plugin/pkg/rcode/rcode_test.go
new file mode 100644
index 000000000..bfca32f1d
--- /dev/null
+++ b/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/plugin/pkg/replacer/replacer.go b/plugin/pkg/replacer/replacer.go
new file mode 100644
index 000000000..fc98e5d29
--- /dev/null
+++ b/plugin/pkg/replacer/replacer.go
@@ -0,0 +1,161 @@
+package replacer
+
+import (
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// Replacer is a type which can replace placeholder
+// substrings in a string with actual values from a
+// dns.Msg and responseRecorder. Always use
+// NewReplacer to get one of these.
+type Replacer interface {
+ Replace(string) string
+ Set(key, value string)
+}
+
+type replacer struct {
+ replacements map[string]string
+ emptyValue string
+}
+
+// New makes a new replacer based on r and rr.
+// Do not create a new replacer until r and rr have all
+// the needed values, because this function copies those
+// values into the replacer. rr may be nil if it is not
+// available. emptyValue should be the string that is used
+// in place of empty string (can still be empty string).
+func New(r *dns.Msg, rr *dnsrecorder.Recorder, emptyValue string) Replacer {
+ req := request.Request{W: rr, Req: r}
+ rep := replacer{
+ replacements: map[string]string{
+ "{type}": req.Type(),
+ "{name}": req.Name(),
+ "{class}": req.Class(),
+ "{proto}": req.Proto(),
+ "{when}": func() string {
+ return time.Now().Format(timeFormat)
+ }(),
+ "{size}": strconv.Itoa(req.Len()),
+ "{remote}": req.IP(),
+ "{port}": req.Port(),
+ },
+ emptyValue: emptyValue,
+ }
+ if rr != nil {
+ rcode := dns.RcodeToString[rr.Rcode]
+ if rcode == "" {
+ rcode = strconv.Itoa(rr.Rcode)
+ }
+ rep.replacements["{rcode}"] = rcode
+ rep.replacements["{rsize}"] = strconv.Itoa(rr.Len)
+ rep.replacements["{duration}"] = time.Since(rr.Start).String()
+ if rr.Msg != nil {
+ rep.replacements[headerReplacer+"rflags}"] = flagsToString(rr.Msg.MsgHdr)
+ }
+ }
+
+ // Header placeholders (case-insensitive)
+ rep.replacements[headerReplacer+"id}"] = strconv.Itoa(int(r.Id))
+ rep.replacements[headerReplacer+"opcode}"] = strconv.Itoa(r.Opcode)
+ rep.replacements[headerReplacer+"do}"] = boolToString(req.Do())
+ rep.replacements[headerReplacer+"bufsize}"] = strconv.Itoa(req.Size())
+
+ return rep
+}
+
+// Replace performs a replacement of values on s and returns
+// the string with the replaced values.
+func (r replacer) Replace(s string) string {
+ // Header replacements - these are case-insensitive, so we can't just use strings.Replace()
+ for strings.Contains(s, headerReplacer) {
+ idxStart := strings.Index(s, headerReplacer)
+ endOffset := idxStart + len(headerReplacer)
+ idxEnd := strings.Index(s[endOffset:], "}")
+ if idxEnd > -1 {
+ placeholder := strings.ToLower(s[idxStart : endOffset+idxEnd+1])
+ replacement := r.replacements[placeholder]
+ if replacement == "" {
+ replacement = r.emptyValue
+ }
+ s = s[:idxStart] + replacement + s[endOffset+idxEnd+1:]
+ } else {
+ break
+ }
+ }
+
+ // Regular replacements - these are easier because they're case-sensitive
+ for placeholder, replacement := range r.replacements {
+ if replacement == "" {
+ replacement = r.emptyValue
+ }
+ s = strings.Replace(s, placeholder, replacement, -1)
+ }
+
+ return s
+}
+
+// Set sets key to value in the replacements map.
+func (r replacer) Set(key, value string) {
+ r.replacements["{"+key+"}"] = value
+}
+
+func boolToString(b bool) string {
+ if b {
+ return "true"
+ }
+ return "false"
+}
+
+// flagsToString checks all header flags and returns those
+// that are set as a string separated with commas
+func flagsToString(h dns.MsgHdr) string {
+ flags := make([]string, 7)
+ i := 0
+
+ if h.Response {
+ flags[i] = "qr"
+ i++
+ }
+
+ if h.Authoritative {
+ flags[i] = "aa"
+ i++
+ }
+ if h.Truncated {
+ flags[i] = "tc"
+ i++
+ }
+ if h.RecursionDesired {
+ flags[i] = "rd"
+ i++
+ }
+ if h.RecursionAvailable {
+ flags[i] = "ra"
+ i++
+ }
+ if h.Zero {
+ flags[i] = "z"
+ i++
+ }
+ if h.AuthenticatedData {
+ flags[i] = "ad"
+ i++
+ }
+ if h.CheckingDisabled {
+ flags[i] = "cd"
+ i++
+ }
+ return strings.Join(flags[:i], ",")
+}
+
+const (
+ timeFormat = "02/Jan/2006:15:04:05 -0700"
+ headerReplacer = "{>"
+)
diff --git a/plugin/pkg/replacer/replacer_test.go b/plugin/pkg/replacer/replacer_test.go
new file mode 100644
index 000000000..95c3bbd52
--- /dev/null
+++ b/plugin/pkg/replacer/replacer_test.go
@@ -0,0 +1,61 @@
+package replacer
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+)
+
+func TestNewReplacer(t *testing.T) {
+ w := dnsrecorder.New(&test.ResponseWriter{})
+
+ r := new(dns.Msg)
+ r.SetQuestion("example.org.", dns.TypeHINFO)
+ r.MsgHdr.AuthenticatedData = true
+
+ replaceValues := New(r, w, "")
+
+ switch v := replaceValues.(type) {
+ case replacer:
+
+ if v.replacements["{type}"] != "HINFO" {
+ t.Errorf("Expected type to be HINFO, got %q", v.replacements["{type}"])
+ }
+ if v.replacements["{name}"] != "example.org." {
+ t.Errorf("Expected request name to be example.org., got %q", v.replacements["{name}"])
+ }
+ if v.replacements["{size}"] != "29" { // size of request
+ t.Errorf("Expected size to be 29, got %q", v.replacements["{size}"])
+ }
+
+ default:
+ t.Fatal("Return Value from New Replacer expected pass type assertion into a replacer type\n")
+ }
+}
+
+func TestSet(t *testing.T) {
+ w := dnsrecorder.New(&test.ResponseWriter{})
+
+ r := new(dns.Msg)
+ r.SetQuestion("example.org.", dns.TypeHINFO)
+ r.MsgHdr.AuthenticatedData = true
+
+ repl := New(r, w, "")
+
+ repl.Set("name", "coredns.io.")
+ repl.Set("type", "A")
+ repl.Set("size", "20")
+
+ if repl.Replace("This name is {name}") != "This name is coredns.io." {
+ t.Error("Expected name replacement failed")
+ }
+ if repl.Replace("This type is {type}") != "This type is A" {
+ t.Error("Expected type replacement failed")
+ }
+ if repl.Replace("The request size is {size}") != "The request size is 20" {
+ t.Error("Expected size replacement failed")
+ }
+}
diff --git a/plugin/pkg/response/classify.go b/plugin/pkg/response/classify.go
new file mode 100644
index 000000000..2e705cb0b
--- /dev/null
+++ b/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/plugin/pkg/response/typify.go b/plugin/pkg/response/typify.go
new file mode 100644
index 000000000..7cfaab497
--- /dev/null
+++ b/plugin/pkg/response/typify.go
@@ -0,0 +1,146 @@
+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
+ // 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",
+ 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++
+ }
+ }
+
+ // Check length of different sections, and drop stuff that is just to large? TODO(miek).
+
+ if soa && m.Rcode == dns.RcodeSuccess {
+ return NoData, opt
+ }
+ if soa && m.Rcode == dns.RcodeNameError {
+ return NameError, 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/plugin/pkg/response/typify_test.go b/plugin/pkg/response/typify_test.go
new file mode 100644
index 000000000..faeaf3579
--- /dev/null
+++ b/plugin/pkg/response/typify_test.go
@@ -0,0 +1,84 @@
+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 = addOpt(m)
+ if mt, _ := Typify(m, utc); mt != OtherError {
+ t.Errorf("message is wrongly typified, expected 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
+}
+
+func addOpt(m *dns.Msg) *dns.Msg {
+ m.Extra = append(m.Extra, test.OPT(4096, true))
+ return m
+}
diff --git a/plugin/pkg/singleflight/singleflight.go b/plugin/pkg/singleflight/singleflight.go
new file mode 100644
index 000000000..365e3ef58
--- /dev/null
+++ b/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[uint32]*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 uint32, fn func() (interface{}, error)) (interface{}, error) {
+ g.mu.Lock()
+ if g.m == nil {
+ g.m = make(map[uint32]*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/plugin/pkg/singleflight/singleflight_test.go b/plugin/pkg/singleflight/singleflight_test.go
new file mode 100644
index 000000000..d1d406e0b
--- /dev/null
+++ b/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/plugin/pkg/tls/tls.go b/plugin/pkg/tls/tls.go
new file mode 100644
index 000000000..6fc10dd8e
--- /dev/null
+++ b/plugin/pkg/tls/tls.go
@@ -0,0 +1,128 @@
+package tls
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "time"
+)
+
+// 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
+ }
+
+ return &tls.Config{Certificates: []tls.Certificate{cert}, RootCAs: roots}, 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
+ }
+
+ return &tls.Config{RootCAs: roots}, nil
+}
+
+func loadRoots(caPath string) (*x509.CertPool, error) {
+ if caPath == "" {
+ return nil, nil
+ }
+
+ roots := x509.NewCertPool()
+ pem, err := ioutil.ReadFile(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 {
+ // this seems like a bad idea but was here in the previous version
+ if cc != nil {
+ cc.InsecureSkipVerify = true
+ }
+
+ 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/plugin/pkg/tls/tls_test.go b/plugin/pkg/tls/tls_test.go
new file mode 100644
index 000000000..8c88bfcc4
--- /dev/null
+++ b/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("Certificateis 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/plugin/pkg/trace/trace.go b/plugin/pkg/trace/trace.go
new file mode 100644
index 000000000..35a8ddabd
--- /dev/null
+++ b/plugin/pkg/trace/trace.go
@@ -0,0 +1,12 @@
+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/plugin/plugin.go b/plugin/plugin.go
new file mode 100644
index 000000000..0c4d7f604
--- /dev/null
+++ b/plugin/plugin.go
@@ -0,0 +1,102 @@
+// Package plugin provides some types and functions common among plugin.
+package plugin
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/miekg/dns"
+ ot "github.com/opentracing/opentracing-go"
+ "golang.org/x/net/context"
+)
+
+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. If the status code is not one of the following:
+ //
+ // * SERVFAIL (dns.RcodeServerFailure)
+ //
+ // * REFUSED (dns.RecodeRefused)
+ //
+ // * FORMERR (dns.RcodeFormatError)
+ //
+ // * NOTIMP (dns.RcodeNotImplemented)
+ //
+ // CoreDNS assumes *no* reply has yet been written. 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 nill, otherwise it will return, a ServerFailure
+// and a nil error.
+func NextOrFailure(name string, next Handler, ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ 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 adhire 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"
diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go
new file mode 100644
index 000000000..b0736c3a0
--- /dev/null
+++ b/plugin/plugin_test.go
@@ -0,0 +1 @@
+package plugin
diff --git a/plugin/pprof/README.md b/plugin/pprof/README.md
new file mode 100644
index 000000000..06a36e442
--- /dev/null
+++ b/plugin/pprof/README.md
@@ -0,0 +1,41 @@
+# pprof
+
+*pprof* publishes runtime profiling data at endpoints under /debug/pprof.
+
+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 site, consider restricting access or enabling it only temporarily.
+
+For more information, please see [Go's pprof
+documentation](https://golang.org/pkg/net/http/pprof/) and read
+[Profiling Go Programs](https://blog.golang.org/profiling-go-programs).
+
+## Syntax
+
+~~~
+pprof [ADDRESS]
+~~~
+
+If not specified, ADDRESS defaults to localhost:6053.
+
+## Examples
+
+Enable pprof endpoints:
+
+~~~
+pprof
+~~~
+
+Listen on an alternate address:
+
+~~~
+pprof 10.9.8.7:6060
+~~~
+
+Listen on an all addresses on port 6060:
+
+~~~
+pprof :6060
+~~~
diff --git a/plugin/pprof/pprof.go b/plugin/pprof/pprof.go
new file mode 100644
index 000000000..020776ecf
--- /dev/null
+++ b/plugin/pprof/pprof.go
@@ -0,0 +1,49 @@
+// Package pprof implement a debug endpoint for getting profiles using the
+// go pprof tooling.
+package pprof
+
+import (
+ "log"
+ "net"
+ "net/http"
+ pp "net/http/pprof"
+)
+
+type handler struct {
+ addr string
+ ln net.Listener
+ mux *http.ServeMux
+}
+
+func (h *handler) Startup() error {
+ ln, err := net.Listen("tcp", h.addr)
+ if err != nil {
+ log.Printf("[ERROR] Failed to start pprof handler: %s", err)
+ return err
+ }
+
+ h.ln = ln
+
+ h.mux = http.NewServeMux()
+ 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)
+
+ 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/plugin/pprof/setup.go b/plugin/pprof/setup.go
new file mode 100644
index 000000000..22b82e94b
--- /dev/null
+++ b/plugin/pprof/setup.go
@@ -0,0 +1,53 @@
+package pprof
+
+import (
+ "net"
+ "sync"
+
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+const defaultAddr = "localhost:6053"
+
+func init() {
+ caddy.RegisterPlugin("pprof", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ found := false
+ h := &handler{addr: defaultAddr}
+ for c.Next() {
+ if found {
+ return plugin.Error("pprof", c.Err("pprof can only be specified once"))
+ }
+ args := c.RemainingArgs()
+ if len(args) == 1 {
+ h.addr = args[0]
+ _, _, e := net.SplitHostPort(h.addr)
+ if e != nil {
+ return e
+ }
+ }
+ if len(args) > 1 {
+ return plugin.Error("pprof", c.ArgErr())
+ }
+ if c.NextBlock() {
+ return plugin.Error("pprof", c.ArgErr())
+ }
+ found = true
+ }
+
+ pprofOnce.Do(func() {
+ c.OnStartup(h.Startup)
+ c.OnShutdown(h.Shutdown)
+ })
+
+ return nil
+}
+
+var pprofOnce sync.Once
diff --git a/plugin/pprof/setup_test.go b/plugin/pprof/setup_test.go
new file mode 100644
index 000000000..eaa4cb37e
--- /dev/null
+++ b/plugin/pprof/setup_test.go
@@ -0,0 +1,34 @@
+package pprof
+
+import (
+ "testing"
+
+ "github.com/mholt/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 {}`, true},
+ {`pprof /foo`, true},
+ {`pprof {
+ a b
+ }`, 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/plugin/proxy/README.md b/plugin/proxy/README.md
new file mode 100644
index 000000000..3cccf05ee
--- /dev/null
+++ b/plugin/proxy/README.md
@@ -0,0 +1,175 @@
+# proxy
+
+*proxy* facilitates both a basic reverse proxy and a robust load balancer.
+
+The proxy has support for multiple backends. The load balancing features include multiple policies,
+health checks, and failovers. If all hosts fail their health check the proxy plugin will fail
+back to randomly selecting a target and sending packets to it.
+
+## Syntax
+
+In its most basic form, a simple reverse proxy uses this syntax:
+
+~~~
+proxy FROM TO
+~~~
+
+* **FROM** is the base domain to match for the request to be proxied.
+* **TO** is the destination endpoint to proxy to.
+
+However, advanced features including load balancing can be utilized with an expanded syntax:
+
+~~~
+proxy FROM TO... {
+ policy random|least_conn|round_robin
+ fail_timeout DURATION
+ max_fails INTEGER
+ health_check PATH:PORT [DURATION]
+ except IGNORED_NAMES...
+ spray
+ protocol [dns [force_tcp]|https_google [bootstrap ADDRESS...]|grpc [insecure|CACERT|KEY CERT|KEY CERT CACERT]]
+}
+~~~
+
+* **FROM** is the name to match for the request to be proxied.
+* **TO** is the destination endpoint to proxy to. At least one is required, but multiple may be
+ specified. **TO** may be an IP:Port pair, or may reference a file in resolv.conf format
+* `policy` is the load balancing policy to use; applies only with multiple backends. May be one of
+ random, least_conn, or round_robin. Default is random.
+* `fail_timeout` specifies how long to consider a backend as down after it has failed. While it is
+ down, requests will not be routed to that backend. A backend is "down" if CoreDNS fails to
+ communicate with it. The default value is 10 seconds ("10s").
+* `max_fails` is the number of failures within fail_timeout that are needed before considering
+ a backend to be down. If 0, the backend will never be marked as down. Default is 1.
+* `health_check` will check path (on port) on each backend. If a backend returns a status code of
+ 200-399, then that backend is marked healthy for double the healthcheck duration. If it doesn't,
+ it is marked as unhealthy and no requests are routed to it. If this option is not provided then
+ health checks are disabled. The default duration is 30 seconds ("30s").
+* **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.
+* `spray` when all backends are unhealthy, randomly pick one to send the traffic to. (This is
+ a failsafe.)
+* `protocol` specifies what protocol to use to speak to an upstream, `dns` (the default) is plain
+ old DNS, and `https_google` uses `https://dns.google.com` and speaks a JSON DNS dialect. Note when
+ using this **TO** will be ignored. The `grpc` option will talk to a server that has implemented
+ the [DnsService](https://github.com/coredns/coredns/pb/dns.proto).
+ An out-of-tree plugin that implements the server side of this can be found at
+ [here](https://github.com/infobloxopen/coredns-grpc).
+
+## Policies
+
+There are three load-balancing policies available:
+* `random` (default) - Randomly select a backend
+* `least_conn` - Select the backend with the fewest active connections
+* `round_robin` - Select the backend in round-robin fashion
+
+All polices implement randomly spraying packets to backend hosts when *no healthy* hosts are
+available. This is to preeempt the case where the healthchecking (as a mechanism) fails.
+
+## Upstream Protocols
+
+Currently `protocol` supports `dns` (i.e., standard DNS over UDP/TCP) and `https_google` (JSON
+payload over HTTPS). Note that with `https_google` the entire transport is encrypted. Only *you* and
+*Google* can see your DNS activity.
+
+* `dns`: uses the standard DNS exchange. You can pass `force_tcp` to make sure that the proxied connection is performed
+ over TCP, regardless of the inbound request's protocol.
+* `https_google`: bootstrap **ADDRESS...** is used to (re-)resolve `dns.google.com` to an address to
+ connect to. This happens every 300s. If not specified the default is used: 8.8.8.8:53/8.8.4.4:53.
+ Note that **TO** is *ignored* when `https_google` is used, as its upstream is defined as
+ `dns.google.com`.
+
+ Debug queries are enabled by default and currently there is no way to turn them off. When CoreDNS
+ receives a debug query (i.e. the name is prefixed with `o-o.debug.`) a TXT record with Comment
+ from `dns.google.com` is added. Note this is not always set.
+* `grpc`: options are used to control how the TLS connection is made to the gRPC server.
+ * None - No client authentication is used, and the system CAs are used to verify the server certificate.
+ * `insecure` - TLS is not used, the connection is made in plaintext (not good in production).
+ * **CACERT** - No client authentication is used, and the file **CACERT** is used to verify the server certificate.
+ * **KEY** **CERT** - Client authentication is used with the specified key/cert pair. The server
+ certificate is verified with the system CAs.
+ * **KEY** **CERT** **CACERT** - Client authentication is used with the specified key/cert pair. The
+ server certificate is verified using the **CACERT** file.
+
+ An out-of-tree plugin that implements the server side of this can be found at
+ [here](https://github.com/infobloxopen/coredns-grpc).
+
+## Metrics
+
+If monitoring is enabled (via the *prometheus* directive) then the following metric is exported:
+
+* coredns_proxy_request_count_total{proto, proxy_proto, from}
+
+Where `proxy_proto` is the protocol used (`dns`, `grpc`, or `https_google`) and `from` is **FROM**
+specified in the config, `proto` is the protocol used by the incoming query ("tcp" or "udp").
+
+## Examples
+
+Proxy all requests within example.org. to a backend system:
+
+~~~
+proxy example.org 127.0.0.1:9005
+~~~
+
+Load-balance all requests between three backends (using random policy):
+
+~~~
+proxy . 10.0.0.10:53 10.0.0.11:1053 10.0.0.12
+~~~
+
+Same as above, but round-robin style:
+
+~~~
+proxy . 10.0.0.10:53 10.0.0.11:1053 10.0.0.12 {
+ policy round_robin
+}
+~~~
+
+With health checks and proxy headers to pass hostname, IP, and scheme upstream:
+
+~~~
+proxy . 10.0.0.11:53 10.0.0.11:53 10.0.0.12:53 {
+ policy round_robin
+ health_check /health:8080
+}
+~~~
+
+Proxy everything except requests to miek.nl or example.org
+
+~~~
+proxy . 10.0.0.10:1234 {
+ except miek.nl example.org
+}
+~~~
+
+Proxy everything except example.org using the host resolv.conf nameservers:
+
+~~~
+proxy . /etc/resolv.conf {
+ except miek.nl example.org
+}
+~~~
+
+Proxy all requests within example.org to Google's dns.google.com.
+
+~~~
+proxy example.org 1.2.3.4:53 {
+ protocol https_google
+}
+~~~
+
+Proxy everything with HTTPS to `dns.google.com`, except `example.org`. Then have another proxy in
+another stanza that uses plain DNS to resolve names under `example.org`.
+
+~~~
+. {
+ proxy . 1.2.3.4:53 {
+ except example.org
+ protocol https_google
+ }
+}
+
+example.org {
+ proxy . 8.8.8.8:53
+}
+~~~
diff --git a/plugin/proxy/dns.go b/plugin/proxy/dns.go
new file mode 100644
index 000000000..4d8038422
--- /dev/null
+++ b/plugin/proxy/dns.go
@@ -0,0 +1,106 @@
+package proxy
+
+import (
+ "context"
+ "net"
+ "time"
+
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+type dnsEx struct {
+ Timeout time.Duration
+ Options
+}
+
+// Options define the options understood by dns.Exchange.
+type Options struct {
+ ForceTCP bool // If true use TCP for upstream no matter what
+}
+
+func newDNSEx() *dnsEx {
+ return newDNSExWithOption(Options{})
+}
+
+func newDNSExWithOption(opt Options) *dnsEx {
+ return &dnsEx{Timeout: defaultTimeout * time.Second, Options: opt}
+}
+
+func (d *dnsEx) Transport() string {
+ if d.Options.ForceTCP {
+ return "tcp"
+ }
+
+ // The protocol will be determined by `state.Proto()` during Exchange.
+ return ""
+}
+func (d *dnsEx) Protocol() string { return "dns" }
+func (d *dnsEx) OnShutdown(p *Proxy) error { return nil }
+func (d *dnsEx) OnStartup(p *Proxy) error { return nil }
+
+// Exchange implements the Exchanger interface.
+func (d *dnsEx) Exchange(ctx context.Context, addr string, state request.Request) (*dns.Msg, error) {
+ proto := state.Proto()
+ if d.Options.ForceTCP {
+ proto = "tcp"
+ }
+ co, err := net.DialTimeout(proto, addr, d.Timeout)
+ if err != nil {
+ return nil, err
+ }
+
+ reply, _, err := d.ExchangeConn(state.Req, co)
+
+ co.Close()
+
+ if reply != nil && reply.Truncated {
+ // Suppress proxy error for truncated responses
+ err = nil
+ }
+
+ if err != nil {
+ return nil, err
+ }
+ // Make sure it fits in the DNS response.
+ reply, _ = state.Scrub(reply)
+ reply.Compress = true
+ reply.Id = state.Req.Id
+
+ return reply, nil
+}
+
+func (d *dnsEx) ExchangeConn(m *dns.Msg, co net.Conn) (*dns.Msg, time.Duration, error) {
+ start := time.Now()
+ r, err := exchange(m, co)
+ rtt := time.Since(start)
+
+ return r, rtt, err
+}
+
+func exchange(m *dns.Msg, co net.Conn) (*dns.Msg, error) {
+ opt := m.IsEdns0()
+
+ udpsize := uint16(dns.MinMsgSize)
+ // If EDNS0 is used use that for size.
+ if opt != nil && opt.UDPSize() >= dns.MinMsgSize {
+ udpsize = opt.UDPSize()
+ }
+
+ dnsco := &dns.Conn{Conn: co, UDPSize: udpsize}
+
+ writeDeadline := time.Now().Add(defaultTimeout)
+ dnsco.SetWriteDeadline(writeDeadline)
+ dnsco.WriteMsg(m)
+
+ readDeadline := time.Now().Add(defaultTimeout)
+ co.SetReadDeadline(readDeadline)
+ r, err := dnsco.ReadMsg()
+
+ dnsco.Close()
+ if r == nil {
+ return nil, err
+ }
+ return r, err
+}
diff --git a/plugin/proxy/dnstap_test.go b/plugin/proxy/dnstap_test.go
new file mode 100644
index 000000000..05169a1ca
--- /dev/null
+++ b/plugin/proxy/dnstap_test.go
@@ -0,0 +1,57 @@
+package proxy
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/dnstap/msg"
+ "github.com/coredns/coredns/plugin/dnstap/test"
+ mwtest "github.com/coredns/coredns/plugin/test"
+ "github.com/coredns/coredns/request"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func testCase(t *testing.T, ex Exchanger, q, r *dns.Msg, datq, datr *msg.Data) {
+ tapq := datq.ToOutsideQuery(tap.Message_FORWARDER_QUERY)
+ tapr := datr.ToOutsideResponse(tap.Message_FORWARDER_RESPONSE)
+ ctx := test.Context{}
+ err := toDnstap(&ctx, "10.240.0.1:40212", ex,
+ request.Request{W: &mwtest.ResponseWriter{}, Req: q}, r, 0, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(ctx.Trap) != 2 {
+ t.Fatalf("messages: %d", len(ctx.Trap))
+ }
+ if !test.MsgEqual(ctx.Trap[0], tapq) {
+ t.Errorf("want: %v\nhave: %v", tapq, ctx.Trap[0])
+ }
+ if !test.MsgEqual(ctx.Trap[1], tapr) {
+ t.Errorf("want: %v\nhave: %v", tapr, ctx.Trap[1])
+ }
+}
+
+func TestDnstap(t *testing.T) {
+ q := mwtest.Case{Qname: "example.org", Qtype: dns.TypeA}.Msg()
+ r := mwtest.Case{
+ Qname: "example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ mwtest.A("example.org. 3600 IN A 10.0.0.1"),
+ },
+ }.Msg()
+ tapq, tapr := test.TestingData(), test.TestingData()
+ testCase(t, newDNSEx(), q, r, tapq, tapr)
+ tapq.SocketProto = tap.SocketProtocol_TCP
+ tapr.SocketProto = tap.SocketProtocol_TCP
+ testCase(t, newDNSExWithOption(Options{ForceTCP: true}), q, r, tapq, tapr)
+ testCase(t, newGoogle("", []string{"8.8.8.8:53", "8.8.4.4:53"}), q, r, tapq, tapr)
+}
+
+func TestNoDnstap(t *testing.T) {
+ err := toDnstap(context.TODO(), "", nil, request.Request{}, nil, 0, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/plugin/proxy/exchanger.go b/plugin/proxy/exchanger.go
new file mode 100644
index 000000000..b98a687e7
--- /dev/null
+++ b/plugin/proxy/exchanger.go
@@ -0,0 +1,22 @@
+package proxy
+
+import (
+ "context"
+
+ "github.com/coredns/coredns/request"
+ "github.com/miekg/dns"
+)
+
+// Exchanger is an interface that specifies a type implementing a DNS resolver that
+// can use whatever transport it likes.
+type Exchanger interface {
+ Exchange(ctx context.Context, addr string, state request.Request) (*dns.Msg, error)
+ Protocol() string
+
+ // Transport returns the only transport protocol used by this Exchanger or "".
+ // If the return value is "", Exchange must use `state.Proto()`.
+ Transport() string
+
+ OnStartup(*Proxy) error
+ OnShutdown(*Proxy) error
+}
diff --git a/plugin/proxy/google.go b/plugin/proxy/google.go
new file mode 100644
index 000000000..ecc5e6dfd
--- /dev/null
+++ b/plugin/proxy/google.go
@@ -0,0 +1,244 @@
+package proxy
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "net/url"
+ "sync/atomic"
+ "time"
+
+ "github.com/coredns/coredns/plugin/pkg/healthcheck"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+type google struct {
+ client *http.Client
+
+ endpoint string // Name to resolve via 'bootstrapProxy'
+
+ bootstrapProxy Proxy
+ quit chan bool
+}
+
+func newGoogle(endpoint string, bootstrap []string) *google {
+ if endpoint == "" {
+ endpoint = ghost
+ }
+ tls := &tls.Config{ServerName: endpoint}
+ client := &http.Client{
+ Timeout: time.Second * defaultTimeout,
+ Transport: &http.Transport{TLSClientConfig: tls},
+ }
+
+ boot := NewLookup(bootstrap)
+
+ return &google{client: client, endpoint: dns.Fqdn(endpoint), bootstrapProxy: boot, quit: make(chan bool)}
+}
+
+func (g *google) Exchange(ctx context.Context, addr string, state request.Request) (*dns.Msg, error) {
+ v := url.Values{}
+
+ v.Set("name", state.Name())
+ v.Set("type", fmt.Sprintf("%d", state.QType()))
+
+ buf, backendErr := g.exchangeJSON(addr, v.Encode())
+
+ if backendErr == nil {
+ gm := new(googleMsg)
+ if err := json.Unmarshal(buf, gm); err != nil {
+ return nil, err
+ }
+
+ m, err := toMsg(gm)
+ if err != nil {
+ return nil, err
+ }
+
+ m.Id = state.Req.Id
+ return m, nil
+ }
+
+ log.Printf("[WARNING] Failed to connect to HTTPS backend %q: %s", g.endpoint, backendErr)
+ return nil, backendErr
+}
+
+func (g *google) exchangeJSON(addr, json string) ([]byte, error) {
+ url := "https://" + addr + "/resolve?" + json
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Host = g.endpoint // TODO(miek): works with the extra dot at the end?
+
+ resp, err := g.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ buf, err := ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode != 200 {
+ return nil, fmt.Errorf("failed to get 200 status code, got %d", resp.StatusCode)
+ }
+
+ return buf, nil
+}
+
+func (g *google) Transport() string { return "tcp" }
+func (g *google) Protocol() string { return "https_google" }
+
+func (g *google) OnShutdown(p *Proxy) error {
+ g.quit <- true
+ return nil
+}
+
+func (g *google) OnStartup(p *Proxy) error {
+ // We fake a state because normally the proxy is called after we already got a incoming query.
+ // This is a non-edns0, udp request to g.endpoint.
+ req := new(dns.Msg)
+ req.SetQuestion(g.endpoint, dns.TypeA)
+ state := request.Request{W: new(fakeBootWriter), Req: req}
+
+ if len(*p.Upstreams) == 0 {
+ return fmt.Errorf("no upstreams defined")
+ }
+
+ oldUpstream := (*p.Upstreams)[0]
+
+ log.Printf("[INFO] Bootstrapping A records %q", g.endpoint)
+
+ new, err := g.bootstrapProxy.Lookup(state, g.endpoint, dns.TypeA)
+ if err != nil {
+ log.Printf("[WARNING] Failed to bootstrap A records %q: %s", g.endpoint, err)
+ } else {
+ addrs, err1 := extractAnswer(new)
+ if err1 != nil {
+ log.Printf("[WARNING] Failed to bootstrap A records %q: %s", g.endpoint, err1)
+ } else {
+
+ up := newUpstream(addrs, oldUpstream.(*staticUpstream))
+ p.Upstreams = &[]Upstream{up}
+
+ log.Printf("[INFO] Bootstrapping A records %q found: %v", g.endpoint, addrs)
+ }
+ }
+
+ go func() {
+ tick := time.NewTicker(120 * time.Second)
+
+ for {
+ select {
+ case <-tick.C:
+
+ log.Printf("[INFO] Resolving A records %q", g.endpoint)
+
+ new, err := g.bootstrapProxy.Lookup(state, g.endpoint, dns.TypeA)
+ if err != nil {
+ log.Printf("[WARNING] Failed to resolve A records %q: %s", g.endpoint, err)
+ continue
+ }
+
+ addrs, err1 := extractAnswer(new)
+ if err1 != nil {
+ log.Printf("[WARNING] Failed to resolve A records %q: %s", g.endpoint, err1)
+ continue
+ }
+
+ up := newUpstream(addrs, oldUpstream.(*staticUpstream))
+ p.Upstreams = &[]Upstream{up}
+
+ log.Printf("[INFO] Resolving A records %q found: %v", g.endpoint, addrs)
+
+ case <-g.quit:
+ return
+ }
+ }
+ }()
+
+ return nil
+}
+
+func extractAnswer(m *dns.Msg) ([]string, error) {
+ if len(m.Answer) == 0 {
+ return nil, fmt.Errorf("no answer section in response")
+ }
+ ret := []string{}
+ for _, an := range m.Answer {
+ if a, ok := an.(*dns.A); ok {
+ ret = append(ret, net.JoinHostPort(a.A.String(), "443"))
+ }
+ }
+ if len(ret) > 0 {
+ return ret, nil
+ }
+
+ return nil, fmt.Errorf("no address records in answer section")
+}
+
+// newUpstream returns an upstream initialized with hosts.
+func newUpstream(hosts []string, old *staticUpstream) Upstream {
+ upstream := &staticUpstream{
+ from: old.from,
+ HealthCheck: healthcheck.HealthCheck{
+ FailTimeout: 10 * time.Second,
+ MaxFails: 3,
+ Future: 60 * time.Second,
+ },
+ ex: old.ex,
+ WithoutPathPrefix: old.WithoutPathPrefix,
+ IgnoredSubDomains: old.IgnoredSubDomains,
+ }
+
+ upstream.Hosts = make([]*healthcheck.UpstreamHost, len(hosts))
+ for i, h := range hosts {
+ uh := &healthcheck.UpstreamHost{
+ Name: h,
+ Conns: 0,
+ Fails: 0,
+ FailTimeout: upstream.FailTimeout,
+
+ CheckDown: func(upstream *staticUpstream) healthcheck.UpstreamHostDownFunc {
+ return func(uh *healthcheck.UpstreamHost) bool {
+
+ down := false
+
+ uh.CheckMu.Lock()
+ until := uh.OkUntil
+ uh.CheckMu.Unlock()
+
+ if !until.IsZero() && time.Now().After(until) {
+ down = true
+ }
+
+ fails := atomic.LoadInt32(&uh.Fails)
+ if fails >= upstream.MaxFails && upstream.MaxFails != 0 {
+ down = true
+ }
+ return down
+ }
+ }(upstream),
+ WithoutPathPrefix: upstream.WithoutPathPrefix,
+ }
+
+ upstream.Hosts[i] = uh
+ }
+ return upstream
+}
+
+const (
+ // Default endpoint for this service.
+ ghost = "dns.google.com."
+)
diff --git a/plugin/proxy/google_rr.go b/plugin/proxy/google_rr.go
new file mode 100644
index 000000000..3b9233b7b
--- /dev/null
+++ b/plugin/proxy/google_rr.go
@@ -0,0 +1,89 @@
+package proxy
+
+import (
+ "fmt"
+
+ "github.com/miekg/dns"
+)
+
+// toMsg converts a googleMsg into the dns message.
+func toMsg(g *googleMsg) (*dns.Msg, error) {
+ m := new(dns.Msg)
+ m.Response = true
+ m.Rcode = g.Status
+ m.Truncated = g.TC
+ m.RecursionDesired = g.RD
+ m.RecursionAvailable = g.RA
+ m.AuthenticatedData = g.AD
+ m.CheckingDisabled = g.CD
+
+ m.Question = make([]dns.Question, 1)
+ m.Answer = make([]dns.RR, len(g.Answer))
+ m.Ns = make([]dns.RR, len(g.Authority))
+ m.Extra = make([]dns.RR, len(g.Additional))
+
+ m.Question[0] = dns.Question{Name: g.Question[0].Name, Qtype: g.Question[0].Type, Qclass: dns.ClassINET}
+
+ var err error
+ for i := 0; i < len(m.Answer); i++ {
+ m.Answer[i], err = toRR(g.Answer[i])
+ if err != nil {
+ return nil, err
+ }
+ }
+ for i := 0; i < len(m.Ns); i++ {
+ m.Ns[i], err = toRR(g.Authority[i])
+ if err != nil {
+ return nil, err
+ }
+ }
+ for i := 0; i < len(m.Extra); i++ {
+ m.Extra[i], err = toRR(g.Additional[i])
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return m, nil
+}
+
+// toRR transforms a "google" RR to a dns.RR.
+func toRR(g googleRR) (dns.RR, error) {
+ typ, ok := dns.TypeToString[g.Type]
+ if !ok {
+ return nil, fmt.Errorf("failed to convert type %q", g.Type)
+ }
+
+ str := fmt.Sprintf("%s %d %s %s", g.Name, g.TTL, typ, g.Data)
+ rr, err := dns.NewRR(str)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse %q: %s", str, err)
+ }
+ return rr, nil
+}
+
+// googleRR represents a dns.RR in another form.
+type googleRR struct {
+ Name string
+ Type uint16
+ TTL uint32
+ Data string
+}
+
+// googleMsg is a JSON representation of the dns.Msg.
+type googleMsg struct {
+ Status int
+ TC bool
+ RD bool
+ RA bool
+ AD bool
+ CD bool
+ Question []struct {
+ Name string
+ Type uint16
+ }
+ Answer []googleRR
+ Authority []googleRR
+ Additional []googleRR
+ Comment string
+}
diff --git a/plugin/proxy/google_test.go b/plugin/proxy/google_test.go
new file mode 100644
index 000000000..1ce591664
--- /dev/null
+++ b/plugin/proxy/google_test.go
@@ -0,0 +1,5 @@
+package proxy
+
+// TODO(miek):
+// Test cert failures - put those in SERVFAIL messages, but attach error code in TXT
+// Test connecting to a a bad host.
diff --git a/plugin/proxy/grpc.go b/plugin/proxy/grpc.go
new file mode 100644
index 000000000..f98fd2e91
--- /dev/null
+++ b/plugin/proxy/grpc.go
@@ -0,0 +1,96 @@
+package proxy
+
+import (
+ "context"
+ "crypto/tls"
+ "log"
+
+ "github.com/coredns/coredns/pb"
+ "github.com/coredns/coredns/plugin/pkg/trace"
+ "github.com/coredns/coredns/request"
+
+ "github.com/grpc-ecosystem/grpc-opentracing/go/otgrpc"
+ "github.com/miekg/dns"
+ opentracing "github.com/opentracing/opentracing-go"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials"
+)
+
+type grpcClient struct {
+ dialOpts []grpc.DialOption
+ clients map[string]pb.DnsServiceClient
+ conns []*grpc.ClientConn
+ upstream *staticUpstream
+}
+
+func newGrpcClient(tls *tls.Config, u *staticUpstream) *grpcClient {
+ g := &grpcClient{upstream: u}
+
+ if tls == nil {
+ g.dialOpts = append(g.dialOpts, grpc.WithInsecure())
+ } else {
+ g.dialOpts = append(g.dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(tls)))
+ }
+ g.clients = map[string]pb.DnsServiceClient{}
+
+ return g
+}
+
+func (g *grpcClient) Exchange(ctx context.Context, addr string, state request.Request) (*dns.Msg, error) {
+ msg, err := state.Req.Pack()
+ if err != nil {
+ return nil, err
+ }
+
+ reply, err := g.clients[addr].Query(ctx, &pb.DnsPacket{Msg: msg})
+ if err != nil {
+ return nil, err
+ }
+ d := new(dns.Msg)
+ err = d.Unpack(reply.Msg)
+ if err != nil {
+ return nil, err
+ }
+ return d, nil
+}
+
+func (g *grpcClient) Transport() string { return "tcp" }
+
+func (g *grpcClient) Protocol() string { return "grpc" }
+
+func (g *grpcClient) OnShutdown(p *Proxy) error {
+ g.clients = map[string]pb.DnsServiceClient{}
+ for i, conn := range g.conns {
+ err := conn.Close()
+ if err != nil {
+ log.Printf("[WARNING] Error closing connection %d: %s\n", i, err)
+ }
+ }
+ g.conns = []*grpc.ClientConn{}
+ return nil
+}
+
+func (g *grpcClient) OnStartup(p *Proxy) error {
+ dialOpts := g.dialOpts
+ if p.Trace != nil {
+ if t, ok := p.Trace.(trace.Trace); ok {
+ onlyIfParent := func(parentSpanCtx opentracing.SpanContext, method string, req, resp interface{}) bool {
+ return parentSpanCtx != nil
+ }
+ intercept := otgrpc.OpenTracingClientInterceptor(t.Tracer(), otgrpc.IncludingSpans(onlyIfParent))
+ dialOpts = append(dialOpts, grpc.WithUnaryInterceptor(intercept))
+ } else {
+ log.Printf("[WARNING] Wrong type for trace plugin reference: %s", p.Trace)
+ }
+ }
+ for _, host := range g.upstream.Hosts {
+ conn, err := grpc.Dial(host.Name, dialOpts...)
+ if err != nil {
+ log.Printf("[WARNING] Skipping gRPC host '%s' due to Dial error: %s\n", host.Name, err)
+ } else {
+ g.clients[host.Name] = pb.NewDnsServiceClient(conn)
+ g.conns = append(g.conns, conn)
+ }
+ }
+ return nil
+}
diff --git a/plugin/proxy/grpc_test.go b/plugin/proxy/grpc_test.go
new file mode 100644
index 000000000..52c5737d6
--- /dev/null
+++ b/plugin/proxy/grpc_test.go
@@ -0,0 +1,71 @@
+package proxy
+
+import (
+ "testing"
+ "time"
+
+ "github.com/coredns/coredns/plugin/pkg/healthcheck"
+
+ "google.golang.org/grpc/grpclog"
+)
+
+func pool() []*healthcheck.UpstreamHost {
+ return []*healthcheck.UpstreamHost{
+ {
+ Name: "localhost:10053",
+ },
+ {
+ Name: "localhost:10054",
+ },
+ }
+}
+
+func TestStartupShutdown(t *testing.T) {
+ grpclog.SetLogger(discard{})
+
+ upstream := &staticUpstream{
+ from: ".",
+ HealthCheck: healthcheck.HealthCheck{
+ Hosts: pool(),
+ FailTimeout: 10 * time.Second,
+ Future: 60 * time.Second,
+ MaxFails: 1,
+ },
+ }
+ g := newGrpcClient(nil, upstream)
+ upstream.ex = g
+
+ p := &Proxy{}
+ p.Upstreams = &[]Upstream{upstream}
+
+ err := g.OnStartup(p)
+ if err != nil {
+ t.Errorf("Error starting grpc client exchanger: %s", err)
+ return
+ }
+ if len(g.clients) != len(pool()) {
+ t.Errorf("Expected %d grpc clients but found %d", len(pool()), len(g.clients))
+ }
+
+ err = g.OnShutdown(p)
+ if err != nil {
+ t.Errorf("Error stopping grpc client exchanger: %s", err)
+ return
+ }
+ if len(g.clients) != 0 {
+ t.Errorf("Shutdown didn't remove clients, found %d", len(g.clients))
+ }
+ if len(g.conns) != 0 {
+ t.Errorf("Shutdown didn't remove conns, found %d", len(g.conns))
+ }
+}
+
+// discard is a Logger that outputs nothing.
+type discard struct{}
+
+func (d discard) Fatal(args ...interface{}) {}
+func (d discard) Fatalf(format string, args ...interface{}) {}
+func (d discard) Fatalln(args ...interface{}) {}
+func (d discard) Print(args ...interface{}) {}
+func (d discard) Printf(format string, args ...interface{}) {}
+func (d discard) Println(args ...interface{}) {}
diff --git a/plugin/proxy/lookup.go b/plugin/proxy/lookup.go
new file mode 100644
index 000000000..9be62edd5
--- /dev/null
+++ b/plugin/proxy/lookup.go
@@ -0,0 +1,132 @@
+package proxy
+
+// functions other plugin might want to use to do lookup in the same style as the proxy.
+
+import (
+ "context"
+ "fmt"
+ "sync/atomic"
+ "time"
+
+ "github.com/coredns/coredns/plugin/pkg/healthcheck"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// NewLookup create a new proxy with the hosts in host and a Random policy.
+func NewLookup(hosts []string) Proxy { return NewLookupWithOption(hosts, Options{}) }
+
+// NewLookupWithOption process creates a simple round robin forward with potentially forced proto for upstream.
+func NewLookupWithOption(hosts []string, opts Options) Proxy {
+ p := Proxy{Next: nil}
+
+ // TODO(miek): this needs to be unified with upstream.go's NewStaticUpstreams, caddy uses NewHost
+ // we should copy/make something similar.
+ upstream := &staticUpstream{
+ from: ".",
+ HealthCheck: healthcheck.HealthCheck{
+ FailTimeout: 10 * time.Second,
+ MaxFails: 3, // TODO(miek): disable error checking for simple lookups?
+ Future: 60 * time.Second,
+ },
+ ex: newDNSExWithOption(opts),
+ }
+ upstream.Hosts = make([]*healthcheck.UpstreamHost, len(hosts))
+
+ for i, host := range hosts {
+ uh := &healthcheck.UpstreamHost{
+ Name: host,
+ Conns: 0,
+ Fails: 0,
+ FailTimeout: upstream.FailTimeout,
+
+ CheckDown: func(upstream *staticUpstream) healthcheck.UpstreamHostDownFunc {
+ return func(uh *healthcheck.UpstreamHost) bool {
+
+ down := false
+
+ uh.CheckMu.Lock()
+ until := uh.OkUntil
+ uh.CheckMu.Unlock()
+
+ if !until.IsZero() && time.Now().After(until) {
+ down = true
+ }
+
+ fails := atomic.LoadInt32(&uh.Fails)
+ if fails >= upstream.MaxFails && upstream.MaxFails != 0 {
+ down = true
+ }
+ return down
+ }
+ }(upstream),
+ WithoutPathPrefix: upstream.WithoutPathPrefix,
+ }
+
+ upstream.Hosts[i] = uh
+ }
+ p.Upstreams = &[]Upstream{upstream}
+ return p
+}
+
+// Lookup will use name and type to forge a new message and will send that upstream. It will
+// set any EDNS0 options correctly so that downstream will be able to process the reply.
+func (p Proxy) Lookup(state request.Request, name string, typ uint16) (*dns.Msg, error) {
+ req := new(dns.Msg)
+ req.SetQuestion(name, typ)
+ state.SizeAndDo(req)
+
+ state2 := request.Request{W: state.W, Req: req}
+
+ return p.lookup(state2)
+}
+
+// Forward forward the request in state as-is. Unlike Lookup that adds EDNS0 suffix to the message.
+func (p Proxy) Forward(state request.Request) (*dns.Msg, error) {
+ return p.lookup(state)
+}
+
+func (p Proxy) lookup(state request.Request) (*dns.Msg, error) {
+ upstream := p.match(state)
+ if upstream == nil {
+ return nil, errInvalidDomain
+ }
+ for {
+ start := time.Now()
+ reply := new(dns.Msg)
+ var backendErr error
+
+ // Since Select() should give us "up" hosts, keep retrying
+ // hosts until timeout (or until we get a nil host).
+ for time.Since(start) < tryDuration {
+ host := upstream.Select()
+ if host == nil {
+ return nil, fmt.Errorf("%s: %s", errUnreachable, "no upstream host")
+ }
+
+ // duplicated from proxy.go, but with a twist, we don't write the
+ // reply back to the client, we return it and there is no monitoring.
+
+ atomic.AddInt64(&host.Conns, 1)
+
+ reply, backendErr = upstream.Exchanger().Exchange(context.TODO(), host.Name, state)
+
+ atomic.AddInt64(&host.Conns, -1)
+
+ if backendErr == nil {
+ return reply, nil
+ }
+ timeout := host.FailTimeout
+ if timeout == 0 {
+ timeout = 10 * time.Second
+ }
+ atomic.AddInt32(&host.Fails, 1)
+ go func(host *healthcheck.UpstreamHost, timeout time.Duration) {
+ time.Sleep(timeout)
+ atomic.AddInt32(&host.Fails, -1)
+ }(host, timeout)
+ }
+ return nil, fmt.Errorf("%s: %s", errUnreachable, backendErr)
+ }
+}
diff --git a/plugin/proxy/metrics.go b/plugin/proxy/metrics.go
new file mode 100644
index 000000000..893c26d6b
--- /dev/null
+++ b/plugin/proxy/metrics.go
@@ -0,0 +1,30 @@
+package proxy
+
+import (
+ "sync"
+
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+// Metrics the proxy plugin exports.
+var (
+ RequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: "proxy",
+ Name: "request_duration_milliseconds",
+ Buckets: append(prometheus.DefBuckets, []float64{50, 100, 200, 500, 1000, 2000, 3000, 4000, 5000, 10000}...),
+ Help: "Histogram of the time (in milliseconds) each request took.",
+ }, []string{"proto", "proxy_proto", "from"})
+)
+
+// OnStartupMetrics sets up the metrics on startup. This is done for all proxy protocols.
+func OnStartupMetrics() error {
+ metricsOnce.Do(func() {
+ prometheus.MustRegister(RequestDuration)
+ })
+ return nil
+}
+
+var metricsOnce sync.Once
diff --git a/plugin/proxy/proxy.go b/plugin/proxy/proxy.go
new file mode 100644
index 000000000..9d1e1906b
--- /dev/null
+++ b/plugin/proxy/proxy.go
@@ -0,0 +1,195 @@
+// Package proxy is plugin that proxies requests.
+package proxy
+
+import (
+ "errors"
+ "fmt"
+ "sync/atomic"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/dnstap"
+ "github.com/coredns/coredns/plugin/dnstap/msg"
+ "github.com/coredns/coredns/plugin/pkg/healthcheck"
+ "github.com/coredns/coredns/request"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+ ot "github.com/opentracing/opentracing-go"
+ "golang.org/x/net/context"
+)
+
+var (
+ errUnreachable = errors.New("unreachable backend")
+ errInvalidProtocol = errors.New("invalid protocol")
+ errInvalidDomain = errors.New("invalid path for proxy")
+)
+
+// Proxy represents a plugin instance that can proxy requests to another (DNS) server.
+type Proxy struct {
+ Next plugin.Handler
+
+ // Upstreams is a pointer to a slice, so we can update the upstream (used for Google)
+ // midway.
+
+ Upstreams *[]Upstream
+
+ // Trace is the Trace plugin, if it is installed
+ // This is used by the grpc exchanger to trace through the grpc calls
+ Trace plugin.Handler
+}
+
+// Upstream manages a pool of proxy upstream hosts. Select should return a
+// suitable upstream host, or nil if no such hosts are available.
+type Upstream interface {
+ // The domain name this upstream host should be routed on.
+ From() string
+ // Selects an upstream host to be routed to.
+ Select() *healthcheck.UpstreamHost
+ // Checks if subpdomain is not an ignored.
+ IsAllowedDomain(string) bool
+ // Exchanger returns the exchanger to be used for this upstream.
+ Exchanger() Exchanger
+ // Stops the upstream from proxying requests to shutdown goroutines cleanly.
+ Stop() error
+}
+
+// tryDuration is how long to try upstream hosts; failures result in
+// immediate retries until this duration ends or we get a nil host.
+var tryDuration = 60 * time.Second
+
+// ServeDNS satisfies the plugin.Handler interface.
+func (p Proxy) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ var span, child ot.Span
+ span = ot.SpanFromContext(ctx)
+ state := request.Request{W: w, Req: r}
+
+ upstream := p.match(state)
+ if upstream == nil {
+ return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
+ }
+
+ for {
+ start := time.Now()
+ reply := new(dns.Msg)
+ var backendErr error
+
+ // Since Select() should give us "up" hosts, keep retrying
+ // hosts until timeout (or until we get a nil host).
+ for time.Since(start) < tryDuration {
+ host := upstream.Select()
+ if host == nil {
+
+ RequestDuration.WithLabelValues(state.Proto(), upstream.Exchanger().Protocol(), upstream.From()).Observe(float64(time.Since(start) / time.Millisecond))
+
+ return dns.RcodeServerFailure, fmt.Errorf("%s: %s", errUnreachable, "no upstream host")
+ }
+
+ if span != nil {
+ child = span.Tracer().StartSpan("exchange", ot.ChildOf(span.Context()))
+ ctx = ot.ContextWithSpan(ctx, child)
+ }
+
+ atomic.AddInt64(&host.Conns, 1)
+ queryEpoch := msg.Epoch()
+
+ reply, backendErr = upstream.Exchanger().Exchange(ctx, host.Name, state)
+
+ respEpoch := msg.Epoch()
+ atomic.AddInt64(&host.Conns, -1)
+
+ if child != nil {
+ child.Finish()
+ }
+
+ taperr := toDnstap(ctx, host.Name, upstream.Exchanger(), state, reply, queryEpoch, respEpoch)
+
+ if backendErr == nil {
+ w.WriteMsg(reply)
+
+ RequestDuration.WithLabelValues(state.Proto(), upstream.Exchanger().Protocol(), upstream.From()).Observe(float64(time.Since(start) / time.Millisecond))
+
+ return 0, taperr
+ }
+
+ timeout := host.FailTimeout
+ if timeout == 0 {
+ timeout = 10 * time.Second
+ }
+ atomic.AddInt32(&host.Fails, 1)
+ go func(host *healthcheck.UpstreamHost, timeout time.Duration) {
+ time.Sleep(timeout)
+ atomic.AddInt32(&host.Fails, -1)
+ }(host, timeout)
+ }
+
+ RequestDuration.WithLabelValues(state.Proto(), upstream.Exchanger().Protocol(), upstream.From()).Observe(float64(time.Since(start) / time.Millisecond))
+
+ return dns.RcodeServerFailure, fmt.Errorf("%s: %s", errUnreachable, backendErr)
+ }
+}
+
+func (p Proxy) match(state request.Request) (u Upstream) {
+ if p.Upstreams == nil {
+ return nil
+ }
+
+ longestMatch := 0
+ for _, upstream := range *p.Upstreams {
+ from := upstream.From()
+
+ if !plugin.Name(from).Matches(state.Name()) || !upstream.IsAllowedDomain(state.Name()) {
+ continue
+ }
+
+ if lf := len(from); lf > longestMatch {
+ longestMatch = lf
+ u = upstream
+ }
+ }
+ return u
+
+}
+
+// Name implements the Handler interface.
+func (p Proxy) Name() string { return "proxy" }
+
+// defaultTimeout is the default networking timeout for DNS requests.
+const defaultTimeout = 5 * time.Second
+
+func toDnstap(ctx context.Context, host string, ex Exchanger, state request.Request, reply *dns.Msg, queryEpoch, respEpoch uint64) (err error) {
+ if tapper := dnstap.TapperFromContext(ctx); tapper != nil {
+ // Query
+ b := tapper.TapBuilder()
+ b.TimeSec = queryEpoch
+ if err = b.HostPort(host); err != nil {
+ return
+ }
+ t := ex.Transport()
+ if t == "" {
+ t = state.Proto()
+ }
+ if t == "tcp" {
+ b.SocketProto = tap.SocketProtocol_TCP
+ } else {
+ b.SocketProto = tap.SocketProtocol_UDP
+ }
+ if err = b.Msg(state.Req); err != nil {
+ return
+ }
+ err = tapper.TapMessage(b.ToOutsideQuery(tap.Message_FORWARDER_QUERY))
+ if err != nil {
+ return
+ }
+
+ // Response
+ if reply != nil {
+ b.TimeSec = respEpoch
+ if err = b.Msg(reply); err != nil {
+ return
+ }
+ err = tapper.TapMessage(b.ToOutsideResponse(tap.Message_FORWARDER_RESPONSE))
+ }
+ }
+ return
+}
diff --git a/plugin/proxy/proxy_test.go b/plugin/proxy/proxy_test.go
new file mode 100644
index 000000000..b0cb9c3cb
--- /dev/null
+++ b/plugin/proxy/proxy_test.go
@@ -0,0 +1,87 @@
+package proxy
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/mholt/caddy/caddyfile"
+)
+
+func TestStop(t *testing.T) {
+ config := "proxy . %s {\n health_check /healthcheck:%s %dms \n}"
+ tests := []struct {
+ name string
+ intervalInMilliseconds int
+ numHealthcheckIntervals int
+ }{
+ {
+ "No Healthchecks After Stop - 5ms, 1 intervals",
+ 5,
+ 1,
+ },
+ {
+ "No Healthchecks After Stop - 5ms, 2 intervals",
+ 5,
+ 2,
+ },
+ {
+ "No Healthchecks After Stop - 5ms, 3 intervals",
+ 5,
+ 3,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+
+ // Set up proxy.
+ var counter int64
+ backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ r.Body.Close()
+ atomic.AddInt64(&counter, 1)
+ }))
+
+ defer backend.Close()
+
+ port := backend.URL[17:] // Remove all crap up to the port
+ back := backend.URL[7:] // Remove http://
+ c := caddyfile.NewDispenser("Testfile", strings.NewReader(fmt.Sprintf(config, back, port, test.intervalInMilliseconds)))
+ upstreams, err := NewStaticUpstreams(&c)
+ if err != nil {
+ t.Error("Expected no error. Got:", err.Error())
+ }
+
+ // Give some time for healthchecks to hit the server.
+ time.Sleep(time.Duration(test.intervalInMilliseconds*test.numHealthcheckIntervals) * time.Millisecond)
+
+ for _, upstream := range upstreams {
+ if err := upstream.Stop(); err != nil {
+ t.Error("Expected no error stopping upstream. Got: ", err.Error())
+ }
+ }
+
+ counterValueAfterShutdown := atomic.LoadInt64(&counter)
+
+ // Give some time to see if healthchecks are still hitting the server.
+ time.Sleep(time.Duration(test.intervalInMilliseconds*test.numHealthcheckIntervals) * time.Millisecond)
+
+ if counterValueAfterShutdown == 0 {
+ t.Error("Expected healthchecks to hit test server. Got no healthchecks.")
+ }
+
+ // health checks are in a go routine now, so one may well occur after we shutdown,
+ // but we only ever expect one more
+ counterValueAfterWaiting := atomic.LoadInt64(&counter)
+ if counterValueAfterWaiting > (counterValueAfterShutdown + 1) {
+ t.Errorf("Expected no more healthchecks after shutdown. Got: %d healthchecks after shutdown", counterValueAfterWaiting-counterValueAfterShutdown)
+ }
+
+ })
+
+ }
+}
diff --git a/plugin/proxy/response.go b/plugin/proxy/response.go
new file mode 100644
index 000000000..2ad553c41
--- /dev/null
+++ b/plugin/proxy/response.go
@@ -0,0 +1,21 @@
+package proxy
+
+import (
+ "net"
+
+ "github.com/miekg/dns"
+)
+
+type fakeBootWriter struct {
+ dns.ResponseWriter
+}
+
+func (w *fakeBootWriter) LocalAddr() net.Addr {
+ local := net.ParseIP("127.0.0.1")
+ return &net.UDPAddr{IP: local, Port: 53} // Port is not used here
+}
+
+func (w *fakeBootWriter) RemoteAddr() net.Addr {
+ remote := net.ParseIP("8.8.8.8")
+ return &net.UDPAddr{IP: remote, Port: 53} // Port is not used here
+}
diff --git a/plugin/proxy/setup.go b/plugin/proxy/setup.go
new file mode 100644
index 000000000..bbe65c35d
--- /dev/null
+++ b/plugin/proxy/setup.go
@@ -0,0 +1,46 @@
+package proxy
+
+import (
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("proxy", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ upstreams, err := NewStaticUpstreams(&c.Dispenser)
+ if err != nil {
+ return plugin.Error("proxy", err)
+ }
+
+ t := dnsserver.GetConfig(c).Handler("trace")
+ P := &Proxy{Trace: t}
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ P.Next = next
+ P.Upstreams = &upstreams
+ return P
+ })
+
+ c.OnStartup(OnStartupMetrics)
+
+ for i := range upstreams {
+ u := upstreams[i]
+ c.OnStartup(func() error {
+ return u.Exchanger().OnStartup(P)
+ })
+ c.OnShutdown(func() error {
+ return u.Exchanger().OnShutdown(P)
+ })
+ // Register shutdown handlers.
+ c.OnShutdown(u.Stop)
+ }
+
+ return nil
+}
diff --git a/plugin/proxy/upstream.go b/plugin/proxy/upstream.go
new file mode 100644
index 000000000..b60b6ff58
--- /dev/null
+++ b/plugin/proxy/upstream.go
@@ -0,0 +1,234 @@
+package proxy
+
+import (
+ "fmt"
+ "net"
+ "strconv"
+ "sync/atomic"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/pkg/healthcheck"
+ "github.com/coredns/coredns/plugin/pkg/tls"
+ "github.com/mholt/caddy/caddyfile"
+ "github.com/miekg/dns"
+)
+
+type staticUpstream struct {
+ from string
+
+ healthcheck.HealthCheck
+
+ WithoutPathPrefix string
+ IgnoredSubDomains []string
+ ex Exchanger
+}
+
+// NewStaticUpstreams parses the configuration input and sets up
+// static upstreams for the proxy plugin.
+func NewStaticUpstreams(c *caddyfile.Dispenser) ([]Upstream, error) {
+ var upstreams []Upstream
+ for c.Next() {
+ upstream := &staticUpstream{
+ from: ".",
+ HealthCheck: healthcheck.HealthCheck{
+ FailTimeout: 10 * time.Second,
+ MaxFails: 1,
+ Future: 60 * time.Second,
+ },
+ ex: newDNSEx(),
+ }
+
+ if !c.Args(&upstream.from) {
+ return upstreams, c.ArgErr()
+ }
+ upstream.from = plugin.Host(upstream.from).Normalize()
+
+ to := c.RemainingArgs()
+ if len(to) == 0 {
+ return upstreams, c.ArgErr()
+ }
+
+ // process the host list, substituting in any nameservers in files
+ toHosts, err := dnsutil.ParseHostPortOrFile(to...)
+ if err != nil {
+ return upstreams, err
+ }
+
+ for c.NextBlock() {
+ if err := parseBlock(c, upstream); err != nil {
+ return upstreams, err
+ }
+ }
+
+ upstream.Hosts = make([]*healthcheck.UpstreamHost, len(toHosts))
+ for i, host := range toHosts {
+ uh := &healthcheck.UpstreamHost{
+ Name: host,
+ Conns: 0,
+ Fails: 0,
+ FailTimeout: upstream.FailTimeout,
+
+ CheckDown: func(upstream *staticUpstream) healthcheck.UpstreamHostDownFunc {
+ return func(uh *healthcheck.UpstreamHost) bool {
+
+ down := false
+
+ uh.CheckMu.Lock()
+ until := uh.OkUntil
+ uh.CheckMu.Unlock()
+
+ if !until.IsZero() && time.Now().After(until) {
+ down = true
+ }
+
+ fails := atomic.LoadInt32(&uh.Fails)
+ if fails >= upstream.MaxFails && upstream.MaxFails != 0 {
+ down = true
+ }
+ return down
+ }
+ }(upstream),
+ WithoutPathPrefix: upstream.WithoutPathPrefix,
+ }
+
+ upstream.Hosts[i] = uh
+ }
+ upstream.Start()
+
+ upstreams = append(upstreams, upstream)
+ }
+ return upstreams, nil
+}
+
+func (u *staticUpstream) From() string {
+ return u.from
+}
+
+func parseBlock(c *caddyfile.Dispenser, u *staticUpstream) error {
+ switch c.Val() {
+ case "policy":
+ if !c.NextArg() {
+ return c.ArgErr()
+ }
+ policyCreateFunc, ok := healthcheck.SupportedPolicies[c.Val()]
+ if !ok {
+ return c.ArgErr()
+ }
+ u.Policy = policyCreateFunc()
+ case "fail_timeout":
+ if !c.NextArg() {
+ return c.ArgErr()
+ }
+ dur, err := time.ParseDuration(c.Val())
+ if err != nil {
+ return err
+ }
+ u.FailTimeout = dur
+ case "max_fails":
+ if !c.NextArg() {
+ return c.ArgErr()
+ }
+ n, err := strconv.Atoi(c.Val())
+ if err != nil {
+ return err
+ }
+ u.MaxFails = int32(n)
+ case "health_check":
+ if !c.NextArg() {
+ return c.ArgErr()
+ }
+ var err error
+ u.HealthCheck.Path, u.HealthCheck.Port, err = net.SplitHostPort(c.Val())
+ if err != nil {
+ return err
+ }
+ u.HealthCheck.Interval = 30 * time.Second
+ if c.NextArg() {
+ dur, err := time.ParseDuration(c.Val())
+ if err != nil {
+ return err
+ }
+ u.HealthCheck.Interval = dur
+ u.Future = 2 * dur
+
+ // set a minimum of 3 seconds
+ if u.Future < (3 * time.Second) {
+ u.Future = 3 * time.Second
+ }
+ }
+ case "without":
+ if !c.NextArg() {
+ return c.ArgErr()
+ }
+ u.WithoutPathPrefix = c.Val()
+ case "except":
+ ignoredDomains := c.RemainingArgs()
+ if len(ignoredDomains) == 0 {
+ return c.ArgErr()
+ }
+ for i := 0; i < len(ignoredDomains); i++ {
+ ignoredDomains[i] = plugin.Host(ignoredDomains[i]).Normalize()
+ }
+ u.IgnoredSubDomains = ignoredDomains
+ case "spray":
+ u.Spray = &healthcheck.Spray{}
+ case "protocol":
+ encArgs := c.RemainingArgs()
+ if len(encArgs) == 0 {
+ return c.ArgErr()
+ }
+ switch encArgs[0] {
+ case "dns":
+ if len(encArgs) > 1 {
+ if encArgs[1] == "force_tcp" {
+ opts := Options{ForceTCP: true}
+ u.ex = newDNSExWithOption(opts)
+ } else {
+ return fmt.Errorf("only force_tcp allowed as parameter to dns")
+ }
+ } else {
+ u.ex = newDNSEx()
+ }
+ case "https_google":
+ boot := []string{"8.8.8.8:53", "8.8.4.4:53"}
+ if len(encArgs) > 2 && encArgs[1] == "bootstrap" {
+ boot = encArgs[2:]
+ }
+
+ u.ex = newGoogle("", boot) // "" for default in google.go
+ case "grpc":
+ if len(encArgs) == 2 && encArgs[1] == "insecure" {
+ u.ex = newGrpcClient(nil, u)
+ return nil
+ }
+ tls, err := tls.NewTLSConfigFromArgs(encArgs[1:]...)
+ if err != nil {
+ return err
+ }
+ u.ex = newGrpcClient(tls, u)
+ default:
+ return fmt.Errorf("%s: %s", errInvalidProtocol, encArgs[0])
+ }
+
+ default:
+ return c.Errf("unknown property '%s'", c.Val())
+ }
+ return nil
+}
+
+func (u *staticUpstream) IsAllowedDomain(name string) bool {
+ if dns.Name(name) == dns.Name(u.From()) {
+ return true
+ }
+
+ for _, ignoredSubDomain := range u.IgnoredSubDomains {
+ if plugin.Name(ignoredSubDomain).Matches(name) {
+ return false
+ }
+ }
+ return true
+}
+
+func (u *staticUpstream) Exchanger() Exchanger { return u.ex }
diff --git a/plugin/proxy/upstream_test.go b/plugin/proxy/upstream_test.go
new file mode 100644
index 000000000..42d50cac3
--- /dev/null
+++ b/plugin/proxy/upstream_test.go
@@ -0,0 +1,324 @@
+package proxy
+
+import (
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/mholt/caddy"
+)
+
+func TestAllowedDomain(t *testing.T) {
+ upstream := &staticUpstream{
+ from: "miek.nl.",
+ IgnoredSubDomains: []string{"download.miek.nl.", "static.miek.nl."}, // closing dot mandatory
+ }
+ tests := []struct {
+ name string
+ expected bool
+ }{
+ {"miek.nl.", true},
+ {"download.miek.nl.", false},
+ {"static.miek.nl.", false},
+ {"blaat.miek.nl.", true},
+ }
+
+ for i, test := range tests {
+ isAllowed := upstream.IsAllowedDomain(test.name)
+ if test.expected != isAllowed {
+ t.Errorf("Test %d: expected %v found %v for %s", i+1, test.expected, isAllowed, test.name)
+ }
+ }
+}
+
+func TestProxyParse(t *testing.T) {
+ rmFunc, cert, key, ca := getPEMFiles(t)
+ defer rmFunc()
+
+ grpc1 := "proxy . 8.8.8.8:53 {\n protocol grpc " + ca + "\n}"
+ grpc2 := "proxy . 8.8.8.8:53 {\n protocol grpc " + cert + " " + key + "\n}"
+ grpc3 := "proxy . 8.8.8.8:53 {\n protocol grpc " + cert + " " + key + " " + ca + "\n}"
+ grpc4 := "proxy . 8.8.8.8:53 {\n protocol grpc " + key + "\n}"
+
+ tests := []struct {
+ inputUpstreams string
+ shouldErr bool
+ }{
+ {
+ `proxy . 8.8.8.8:53`,
+ false,
+ },
+ {
+ `proxy 10.0.0.0/24 8.8.8.8:53`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ policy round_robin
+}`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ fail_timeout 5s
+}`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ max_fails 10
+}`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ health_check /health:8080
+}`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ without without
+}`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ except miek.nl example.org 10.0.0.0/24
+}`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ spray
+}`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ error_option
+}`,
+ true,
+ },
+ {
+ `
+proxy . some_bogus_filename`,
+ true,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ protocol dns
+}`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ protocol grpc
+}`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ protocol grpc insecure
+}`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ protocol dns force_tcp
+}`,
+ false,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ protocol grpc a b c d
+}`,
+ true,
+ },
+ {
+ grpc1,
+ false,
+ },
+ {
+ grpc2,
+ false,
+ },
+ {
+ grpc3,
+ false,
+ },
+ {
+ grpc4,
+ true,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ protocol foobar
+}`,
+ true,
+ },
+ {
+ `proxy`,
+ true,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ protocol foobar
+}`,
+ true,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ policy
+}`,
+ true,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ fail_timeout
+}`,
+ true,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ fail_timeout junky
+}`,
+ true,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ health_check
+}`,
+ true,
+ },
+ {
+ `
+proxy . 8.8.8.8:53 {
+ protocol dns force
+}`,
+ true,
+ },
+ }
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.inputUpstreams)
+ _, err := NewStaticUpstreams(&c.Dispenser)
+ if (err != nil) != test.shouldErr {
+ t.Errorf("Test %d expected no error, got %v for %s", i+1, err, test.inputUpstreams)
+ }
+ }
+}
+
+func TestResolvParse(t *testing.T) {
+ tests := []struct {
+ inputUpstreams string
+ filedata string
+ shouldErr bool
+ expected []string
+ }{
+ {
+ `
+proxy . FILE
+`,
+ `
+nameserver 1.2.3.4
+nameserver 4.3.2.1
+`,
+ false,
+ []string{"1.2.3.4:53", "4.3.2.1:53"},
+ },
+ {
+ `
+proxy example.com 1.1.1.1:5000
+proxy . FILE
+proxy example.org 2.2.2.2:1234
+`,
+ `
+nameserver 1.2.3.4
+`,
+ false,
+ []string{"1.1.1.1:5000", "1.2.3.4:53", "2.2.2.2:1234"},
+ },
+ {
+ `
+proxy example.com 1.1.1.1:5000
+proxy . FILE
+proxy example.org 2.2.2.2:1234
+`,
+ `
+junky resolve.conf
+`,
+ false,
+ []string{"1.1.1.1:5000", "2.2.2.2:1234"},
+ },
+ }
+ for i, tc := range tests {
+
+ path, rm, err := test.TempFile(".", tc.filedata)
+ if err != nil {
+ t.Fatalf("Test %d could not creat temp file %v", i, err)
+ }
+ defer rm()
+
+ config := strings.Replace(tc.inputUpstreams, "FILE", path, -1)
+ c := caddy.NewTestController("dns", config)
+ upstreams, err := NewStaticUpstreams(&c.Dispenser)
+ if (err != nil) != tc.shouldErr {
+ t.Errorf("Test %d expected no error, got %v", i+1, err)
+ }
+ var hosts []string
+ for _, u := range upstreams {
+ for _, h := range u.(*staticUpstream).Hosts {
+ hosts = append(hosts, h.Name)
+ }
+ }
+ if !tc.shouldErr {
+ if len(hosts) != len(tc.expected) {
+ t.Errorf("Test %d expected %d hosts got %d", i+1, len(tc.expected), len(upstreams))
+ } else {
+ ok := true
+ for i, v := range tc.expected {
+ if v != hosts[i] {
+ ok = false
+ }
+ }
+ if !ok {
+ t.Errorf("Test %d expected %v got %v", i+1, tc.expected, upstreams)
+ }
+ }
+ }
+ }
+}
+
+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
+}
diff --git a/plugin/reverse/README.md b/plugin/reverse/README.md
new file mode 100644
index 000000000..63a3a968c
--- /dev/null
+++ b/plugin/reverse/README.md
@@ -0,0 +1,86 @@
+# reverse
+
+The *reverse* plugin allows CoreDNS to respond dynamically to a PTR request and the related A/AAAA request.
+
+## Syntax
+
+~~~
+reverse NETWORK... {
+ hostname TEMPLATE
+ [ttl TTL]
+ [fallthrough]
+ [wildcard]
+~~~
+
+* **NETWORK** one or more CIDR formatted networks to respond on.
+* `hostname` injects the IP and zone to a template for the hostname. Defaults to "ip-{IP}.{zone[1]}". See below for template.
+* `ttl` defaults to 60
+* `fallthrough` if zone matches and no record can be generated, pass request to the next plugin.
+* `wildcard` allows matches to catch all subdomains as well.
+
+### Template Syntax
+
+The template for the hostname is used for generating the PTR for a reverse lookup and matching the
+forward lookup back to an IP.
+
+#### `{ip}`
+
+The `{ip}` symbol is **required** to make reverse work.
+For IPv4 lookups the IP is directly extracted
+With IPv6 lookups the ":" is removed, and any zero ranged are expanded, e.g.,
+"ffff::ffff" results in "ffff000000000000000000000000ffff"
+
+#### `{zone[i]}`
+
+The `{zone[i]}` symbol is **optional** and can be replaced by a fixed (zone) string.
+The zone will be matched by the zones listed in *this* configuration stanza.
+`i` needs to be replaced with the index of the configured listener zones, starting with 1.
+
+## Examples
+
+~~~ txt
+arpa compute.internal {
+ # proxy unmatched requests
+ proxy . 8.8.8.8
+
+ # answer requests for IPs in this network
+ # PTR 1.0.32.10.in-addr.arpa. 3600 ip-10.0.32.1.compute.internal.
+ # A ip-10.0.32.1.compute.internal. 3600 10.0.32.1
+ # v6 is also possible
+ # 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.0.0.0.0.1.0.d.f.ip6.arpa. 3600 ip-fd010000000000000000000000000001.compute.internal.
+ # AAAA ip-fd010000000000000000000000000001.compute.internal. 3600 fd01::1
+ reverse 10.32.0.0/16 fd01::/16 {
+ # template of the ip injection to hostname, zone resolved to compute.internal.
+ hostname ip-{ip}.{zone[2]}
+
+ ttl 3600
+
+ # Forward unanswered or unmatched requests to proxy
+ # without this flag, requesting A/AAAA records on compute.internal. will end here.
+ fallthrough
+ }
+}
+~~~
+
+
+~~~ txt
+32.10.in-addr.arpa.arpa arpa.company.org {
+
+ reverse 10.32.0.0/16 {
+ # template of the ip injection to hostname, zone resolved to arpa.company.org.
+ hostname "ip-{ip}.v4.{zone[2]}"
+
+ ttl 3600
+
+ # fallthrough is not required, v4.arpa.company.org. will be only answered here
+ }
+
+ # cidr closer to the ip wins, so we can overwrite the "default"
+ reverse 10.32.2.0/24 {
+ # its also possible to set fix domain suffix
+ hostname ip-{ip}.fix.arpa.company.org.
+
+ ttl 3600
+ }
+}
+~~~
diff --git a/plugin/reverse/network.go b/plugin/reverse/network.go
new file mode 100644
index 000000000..80d533382
--- /dev/null
+++ b/plugin/reverse/network.go
@@ -0,0 +1,87 @@
+package reverse
+
+import (
+ "bytes"
+ "net"
+ "regexp"
+ "strings"
+)
+
+type network struct {
+ IPnet *net.IPNet
+ Zone string // forward lookup zone
+ Template string
+ TTL uint32
+ RegexMatchIP *regexp.Regexp
+}
+
+// TODO: we might want to get rid of these regexes.
+const hexDigit = "0123456789abcdef"
+const templateNameIP = "{ip}"
+const regexMatchV4 = "((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))"
+const regexMatchV6 = "([0-9a-fA-F]{32})"
+
+// hostnameToIP converts the hostname back to an ip, based on the template
+// returns nil if there is no IP found.
+func (network *network) hostnameToIP(rname string) net.IP {
+ var matchedIP net.IP
+
+ match := network.RegexMatchIP.FindStringSubmatch(rname)
+ if len(match) != 2 {
+ return nil
+ }
+
+ if network.IPnet.IP.To4() != nil {
+ matchedIP = net.ParseIP(match[1])
+ } else {
+ // TODO: can probably just allocate a []byte and use that.
+ var buf bytes.Buffer
+ // convert back to an valid ipv6 string with colons
+ for i := 0; i < 8*4; i += 4 {
+ buf.WriteString(match[1][i : i+4])
+ if i < 28 {
+ buf.WriteString(":")
+ }
+ }
+ matchedIP = net.ParseIP(buf.String())
+ }
+
+ // No valid ip or it does not belong to this network
+ if matchedIP == nil || !network.IPnet.Contains(matchedIP) {
+ return nil
+ }
+
+ return matchedIP
+}
+
+// ipToHostname converts an IP to an DNS compatible hostname and injects it into the template.domain.
+func (network *network) ipToHostname(ip net.IP) (name string) {
+ if ipv4 := ip.To4(); ipv4 != nil {
+ // replace . to -
+ name = ipv4.String()
+ } else {
+ // assume v6
+ // ensure zeros are present in string
+ buf := make([]byte, 0, len(ip)*4)
+ for i := 0; i < len(ip); i++ {
+ v := ip[i]
+ buf = append(buf, hexDigit[v>>4])
+ buf = append(buf, hexDigit[v&0xF])
+ }
+ name = string(buf)
+ }
+ // inject the converted ip into the fqdn template
+ return strings.Replace(network.Template, templateNameIP, name, 1)
+}
+
+type networks []network
+
+func (n networks) Len() int { return len(n) }
+func (n networks) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
+
+// cidr closer to the ip wins (by netmask)
+func (n networks) Less(i, j int) bool {
+ isize, _ := n[i].IPnet.Mask.Size()
+ jsize, _ := n[j].IPnet.Mask.Size()
+ return isize > jsize
+}
diff --git a/plugin/reverse/network_test.go b/plugin/reverse/network_test.go
new file mode 100644
index 000000000..a826707e5
--- /dev/null
+++ b/plugin/reverse/network_test.go
@@ -0,0 +1,135 @@
+package reverse
+
+import (
+ "net"
+ "reflect"
+ "regexp"
+ "testing"
+)
+
+// Test converting from hostname to IP and back again to hostname
+func TestNetworkConversion(t *testing.T) {
+
+ _, net4, _ := net.ParseCIDR("10.1.1.0/24")
+ _, net6, _ := net.ParseCIDR("fd01::/64")
+
+ regexIP4, _ := regexp.Compile("^dns-" + regexMatchV4 + "\\.domain\\.internal\\.$")
+ regexIP6, _ := regexp.Compile("^dns-" + regexMatchV6 + "\\.domain\\.internal\\.$")
+
+ tests := []struct {
+ network network
+ resultHost string
+ resultIP net.IP
+ }{
+ {
+ network{
+ IPnet: net4,
+ Template: "dns-{ip}.domain.internal.",
+ RegexMatchIP: regexIP4,
+ },
+ "dns-10.1.1.23.domain.internal.",
+ net.ParseIP("10.1.1.23"),
+ },
+ {
+ network{
+ IPnet: net6,
+ Template: "dns-{ip}.domain.internal.",
+ RegexMatchIP: regexIP6,
+ },
+ "dns-fd01000000000000000000000000a32f.domain.internal.",
+ net.ParseIP("fd01::a32f"),
+ },
+ }
+
+ for i, test := range tests {
+ resultIP := test.network.hostnameToIP(test.resultHost)
+ if !reflect.DeepEqual(test.resultIP, resultIP) {
+ t.Fatalf("Test %d expected %v, got %v", i, test.resultIP, resultIP)
+ }
+
+ resultHost := test.network.ipToHostname(test.resultIP)
+ if !reflect.DeepEqual(test.resultHost, resultHost) {
+ t.Fatalf("Test %d expected %v, got %v", i, test.resultHost, resultHost)
+ }
+ }
+}
+
+func TestNetworkHostnameToIP(t *testing.T) {
+
+ _, net4, _ := net.ParseCIDR("10.1.1.0/24")
+ _, net6, _ := net.ParseCIDR("fd01::/64")
+
+ regexIP4, _ := regexp.Compile("^dns-" + regexMatchV4 + "\\.domain\\.internal\\.$")
+ regexIP6, _ := regexp.Compile("^dns-" + regexMatchV6 + "\\.domain\\.internal\\.$")
+
+ // Test regex does NOT match
+ // All this test should return nil
+ testsNil := []struct {
+ network network
+ hostname string
+ }{
+ {
+ network{
+ IPnet: net4,
+ RegexMatchIP: regexIP4,
+ },
+ // domain does not match
+ "dns-10.1.1.23.domain.internals.",
+ },
+ {
+ network{
+ IPnet: net4,
+ RegexMatchIP: regexIP4,
+ },
+ // IP does match / contain in subnet
+ "dns-200.1.1.23.domain.internals.",
+ },
+ {
+ network{
+ IPnet: net4,
+ RegexMatchIP: regexIP4,
+ },
+ // template does not match
+ "dns-10.1.1.23-x.domain.internal.",
+ },
+ {
+ network{
+ IPnet: net4,
+ RegexMatchIP: regexIP4,
+ },
+ // template does not match
+ "IP-dns-10.1.1.23.domain.internal.",
+ },
+ {
+ network{
+ IPnet: net6,
+ RegexMatchIP: regexIP6,
+ },
+ // template does not match
+ "dnx-fd01000000000000000000000000a32f.domain.internal.",
+ },
+ {
+ network{
+ IPnet: net6,
+ RegexMatchIP: regexIP6,
+ },
+ // no valid v6 (missing one 0, only 31 chars)
+ "dns-fd0100000000000000000000000a32f.domain.internal.",
+ },
+ {
+ network{
+ IPnet: net6,
+ RegexMatchIP: regexIP6,
+ },
+ // IP does match / contain in subnet
+ "dns-ed01000000000000000000000000a32f.domain.internal.",
+ },
+ }
+
+ for i, test := range testsNil {
+ resultIP := test.network.hostnameToIP(test.hostname)
+ if resultIP != nil {
+ t.Fatalf("Test %d expected nil, got %v", i, resultIP)
+ }
+ }
+}
diff --git a/plugin/reverse/reverse.go b/plugin/reverse/reverse.go
new file mode 100644
index 000000000..7d7681867
--- /dev/null
+++ b/plugin/reverse/reverse.go
@@ -0,0 +1,107 @@
+package reverse
+
+import (
+ "net"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Reverse provides dynamic reverse DNS and the related forward RR.
+type Reverse struct {
+ Next plugin.Handler
+ Networks networks
+ Fallthrough bool
+}
+
+// ServeDNS implements the plugin.Handler interface.
+func (re Reverse) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ var rr dns.RR
+
+ state := request.Request{W: w, Req: r}
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
+
+ switch state.QType() {
+ case dns.TypePTR:
+ address := dnsutil.ExtractAddressFromReverse(state.Name())
+
+ if address == "" {
+ // Not an reverse lookup, but can still be an pointer for an domain
+ break
+ }
+
+ ip := net.ParseIP(address)
+ // loop through the configured networks
+ for _, n := range re.Networks {
+ if n.IPnet.Contains(ip) {
+ rr = &dns.PTR{
+ Hdr: dns.RR_Header{Name: state.QName(), Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: n.TTL},
+ Ptr: n.ipToHostname(ip),
+ }
+ break
+ }
+ }
+
+ case dns.TypeA:
+ for _, n := range re.Networks {
+ if dns.IsSubDomain(n.Zone, state.Name()) {
+
+ // skip if requesting an v4 address and network is not v4
+ if n.IPnet.IP.To4() == nil {
+ continue
+ }
+
+ result := n.hostnameToIP(state.Name())
+ if result != nil {
+ rr = &dns.A{
+ Hdr: dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: n.TTL},
+ A: result,
+ }
+ break
+ }
+ }
+ }
+
+ case dns.TypeAAAA:
+ for _, n := range re.Networks {
+ if dns.IsSubDomain(n.Zone, state.Name()) {
+
+ // Do not use To16 which tries to make v4 in v6
+ if n.IPnet.IP.To4() != nil {
+ continue
+ }
+
+ result := n.hostnameToIP(state.Name())
+ if result != nil {
+ rr = &dns.AAAA{
+ Hdr: dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: n.TTL},
+ AAAA: result,
+ }
+ break
+ }
+ }
+ }
+
+ }
+
+ if rr != nil {
+ m.Answer = append(m.Answer, rr)
+ state.SizeAndDo(m)
+ w.WriteMsg(m)
+ return dns.RcodeSuccess, nil
+ }
+
+ if re.Fallthrough {
+ return plugin.NextOrFailure(re.Name(), re.Next, ctx, w, r)
+ }
+ return dns.RcodeServerFailure, nil
+}
+
+// Name implements the Handler interface.
+func (re Reverse) Name() string { return "reverse" }
diff --git a/plugin/reverse/reverse_test.go b/plugin/reverse/reverse_test.go
new file mode 100644
index 000000000..c7a7fea6c
--- /dev/null
+++ b/plugin/reverse/reverse_test.go
@@ -0,0 +1,71 @@
+package reverse
+
+import (
+ "net"
+ "regexp"
+ "testing"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestReverse(t *testing.T) {
+ _, net4, _ := net.ParseCIDR("10.1.1.0/24")
+ regexIP4, _ := regexp.Compile("^.*ip-" + regexMatchV4 + "\\.example\\.org\\.$")
+
+ em := Reverse{
+ Networks: networks{network{
+ IPnet: net4,
+ Zone: "example.org",
+ Template: "ip-{ip}.example.org.",
+ RegexMatchIP: regexIP4,
+ }},
+ Fallthrough: false,
+ }
+
+ tests := []struct {
+ next plugin.Handler
+ qname string
+ qtype uint16
+ expectedCode int
+ expectedReply string
+ expectedErr error
+ }{
+ {
+ next: test.NextHandler(dns.RcodeSuccess, nil),
+ qname: "test.ip-10.1.1.2.example.org",
+ expectedCode: dns.RcodeSuccess,
+ expectedReply: "10.1.1.2",
+ expectedErr: nil,
+ },
+ }
+
+ ctx := context.TODO()
+
+ for i, tr := range tests {
+ req := new(dns.Msg)
+
+ tr.qtype = dns.TypeA
+ req.SetQuestion(dns.Fqdn(tr.qname), tr.qtype)
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ code, err := em.ServeDNS(ctx, rec, req)
+
+ if err != tr.expectedErr {
+ t.Errorf("Test %d: Expected error %v, but got %v", i, tr.expectedErr, err)
+ }
+ if code != int(tr.expectedCode) {
+ t.Errorf("Test %d: Expected status code %d, but got %d", i, tr.expectedCode, code)
+ }
+ if tr.expectedReply != "" {
+ answer := rec.Msg.Answer[0].(*dns.A).A.String()
+ if answer != tr.expectedReply {
+ t.Errorf("Test %d: Expected answer %s, but got %s", i, tr.expectedReply, answer)
+ }
+ }
+ }
+}
diff --git a/plugin/reverse/setup.go b/plugin/reverse/setup.go
new file mode 100644
index 000000000..26e21eea9
--- /dev/null
+++ b/plugin/reverse/setup.go
@@ -0,0 +1,147 @@
+package reverse
+
+import (
+ "net"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("reverse", caddy.Plugin{
+ ServerType: "dns",
+ Action: setupReverse,
+ })
+}
+
+func setupReverse(c *caddy.Controller) error {
+ networks, fallThrough, err := reverseParse(c)
+ if err != nil {
+ return plugin.Error("reverse", err)
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ return Reverse{Next: next, Networks: networks, Fallthrough: fallThrough}
+ })
+
+ return nil
+}
+
+func reverseParse(c *caddy.Controller) (nets networks, fall bool, err error) {
+ zones := make([]string, len(c.ServerBlockKeys))
+ wildcard := false
+
+ // We copy from the serverblock, these contains Hosts.
+ for i, str := range c.ServerBlockKeys {
+ zones[i] = plugin.Host(str).Normalize()
+ }
+
+ for c.Next() {
+ var cidrs []*net.IPNet
+
+ // parse all networks
+ for _, cidr := range c.RemainingArgs() {
+ if cidr == "{" {
+ break
+ }
+ _, ipnet, err := net.ParseCIDR(cidr)
+ if err != nil {
+ return nil, false, c.Errf("network needs to be CIDR formatted: %q\n", cidr)
+ }
+ cidrs = append(cidrs, ipnet)
+ }
+ if len(cidrs) == 0 {
+ return nil, false, c.ArgErr()
+ }
+
+ // set defaults
+ var (
+ template = "ip-" + templateNameIP + ".{zone[1]}"
+ ttl = 60
+ )
+ for c.NextBlock() {
+ switch c.Val() {
+ case "hostname":
+ if !c.NextArg() {
+ return nil, false, c.ArgErr()
+ }
+ template = c.Val()
+
+ case "ttl":
+ if !c.NextArg() {
+ return nil, false, c.ArgErr()
+ }
+ ttl, err = strconv.Atoi(c.Val())
+ if err != nil {
+ return nil, false, err
+ }
+
+ case "wildcard":
+ wildcard = true
+
+ case "fallthrough":
+ fall = true
+
+ default:
+ return nil, false, c.ArgErr()
+ }
+ }
+
+ // prepare template
+ // replace {zone[index]} by the listen zone/domain of this config block
+ for i, zone := range zones {
+ // TODO: we should be smarter about actually replacing this. This works, but silently allows "zone[-1]"
+ // for instance.
+ template = strings.Replace(template, "{zone["+strconv.Itoa(i+1)+"]}", zone, 1)
+ }
+ if !strings.HasSuffix(template, ".") {
+ template += "."
+ }
+
+ // extract zone from template
+ templateZone := strings.SplitAfterN(template, ".", 2)
+ if len(templateZone) != 2 || templateZone[1] == "" {
+ return nil, false, c.Errf("cannot find domain in template '%v'", template)
+ }
+
+ // Create for each configured network in this stanza
+ for _, ipnet := range cidrs {
+ // precompile regex for hostname to ip matching
+ regexIP := regexMatchV4
+ if ipnet.IP.To4() == nil {
+ regexIP = regexMatchV6
+ }
+ prefix := "^"
+ if wildcard {
+ prefix += ".*"
+ }
+ regex, err := regexp.Compile(
+ prefix + strings.Replace( // inject ip regex into template
+ regexp.QuoteMeta(template), // escape dots
+ regexp.QuoteMeta(templateNameIP),
+ regexIP,
+ 1) + "$")
+ if err != nil {
+ return nil, false, err
+ }
+
+ nets = append(nets, network{
+ IPnet: ipnet,
+ Zone: templateZone[1],
+ Template: template,
+ RegexMatchIP: regex,
+ TTL: uint32(ttl),
+ })
+ }
+ }
+
+ // sort by cidr
+ sort.Sort(nets)
+ return nets, fall, nil
+}
diff --git a/plugin/reverse/setup_test.go b/plugin/reverse/setup_test.go
new file mode 100644
index 000000000..5b4c04e82
--- /dev/null
+++ b/plugin/reverse/setup_test.go
@@ -0,0 +1,195 @@
+package reverse
+
+import (
+ "net"
+ "reflect"
+ "regexp"
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestSetupParse(t *testing.T) {
+
+ _, net4, _ := net.ParseCIDR("10.1.1.0/24")
+ _, net6, _ := net.ParseCIDR("fd01::/64")
+
+ regexIP4wildcard, _ := regexp.Compile("^.*ip-" + regexMatchV4 + "\\.domain\\.com\\.$")
+ regexIP6, _ := regexp.Compile("^ip-" + regexMatchV6 + "\\.domain\\.com\\.$")
+ regexIpv4dynamic, _ := regexp.Compile("^dynamic-" + regexMatchV4 + "-intern\\.dynamic\\.domain\\.com\\.$")
+ regexIpv6dynamic, _ := regexp.Compile("^dynamic-" + regexMatchV6 + "-intern\\.dynamic\\.domain\\.com\\.$")
+ regexIpv4vpndynamic, _ := regexp.Compile("^dynamic-" + regexMatchV4 + "-vpn\\.dynamic\\.domain\\.com\\.$")
+
+ serverBlockKeys := []string{"domain.com.:8053", "dynamic.domain.com.:8053"}
+
+ tests := []struct {
+ inputFileRules string
+ shouldErr bool
+ networks networks
+ }{
+ {
+ // with defaults
+ `reverse fd01::/64`,
+ false,
+ networks{network{
+ IPnet: net6,
+ Template: "ip-{ip}.domain.com.",
+ Zone: "domain.com.",
+ TTL: 60,
+ RegexMatchIP: regexIP6,
+ }},
+ },
+ {
+ `reverse`,
+ true,
+ networks{},
+ },
+ {
+ //no cidr
+ `reverse 10.1.1.1`,
+ true,
+ networks{},
+ },
+ {
+ //no cidr
+ `reverse 10.1.1.0/16 fd00::`,
+ true,
+ networks{},
+ },
+ {
+ // invalid key
+ `reverse 10.1.1.0/24 {
+ notavailable
+ }`,
+ true,
+ networks{},
+ },
+ {
+ // no domain suffix
+ `reverse 10.1.1.0/24 {
+ hostname ip-{ip}.
+ }`,
+ true,
+ networks{},
+ },
+ {
+ // hostname requires an second arg
+ `reverse 10.1.1.0/24 {
+ hostname
+ }`,
+ true,
+ networks{},
+ },
+ {
+ // template breaks regex compile
+ `reverse 10.1.1.0/24 {
+ hostname ip-{[-x
+ }`,
+ true,
+ networks{},
+ },
+ {
+ // ttl requires an (u)int
+ `reverse 10.1.1.0/24 {
+ ttl string
+ }`,
+ true,
+ networks{},
+ },
+ {
+ `reverse fd01::/64 {
+ hostname dynamic-{ip}-intern.{zone[2]}
+ ttl 50
+ }
+ reverse 10.1.1.0/24 {
+ hostname dynamic-{ip}-vpn.{zone[2]}
+ fallthrough
+ }`,
+ false,
+ networks{network{
+ IPnet: net6,
+ Template: "dynamic-{ip}-intern.dynamic.domain.com.",
+ Zone: "dynamic.domain.com.",
+ TTL: 50,
+ RegexMatchIP: regexIpv6dynamic,
+ }, network{
+ IPnet: net4,
+ Template: "dynamic-{ip}-vpn.dynamic.domain.com.",
+ Zone: "dynamic.domain.com.",
+ TTL: 60,
+ RegexMatchIP: regexIpv4vpndynamic,
+ }},
+ },
+ {
+ // multiple networks in one stanza
+ `reverse fd01::/64 10.1.1.0/24 {
+ hostname dynamic-{ip}-intern.{zone[2]}
+ ttl 50
+ fallthrough
+ }`,
+ false,
+ networks{network{
+ IPnet: net6,
+ Template: "dynamic-{ip}-intern.dynamic.domain.com.",
+ Zone: "dynamic.domain.com.",
+ TTL: 50,
+ RegexMatchIP: regexIpv6dynamic,
+ }, network{
+ IPnet: net4,
+ Template: "dynamic-{ip}-intern.dynamic.domain.com.",
+ Zone: "dynamic.domain.com.",
+ TTL: 50,
+ RegexMatchIP: regexIpv4dynamic,
+ }},
+ },
+ {
+ // fix domain in template
+ `reverse fd01::/64 {
+ hostname dynamic-{ip}-intern.dynamic.domain.com
+ ttl 300
+ fallthrough
+ }`,
+ false,
+ networks{network{
+ IPnet: net6,
+ Template: "dynamic-{ip}-intern.dynamic.domain.com.",
+ Zone: "dynamic.domain.com.",
+ TTL: 300,
+ RegexMatchIP: regexIpv6dynamic,
+ }},
+ },
+ {
+ `reverse 10.1.1.0/24 {
+ hostname ip-{ip}.{zone[1]}
+ ttl 50
+ wildcard
+ fallthrough
+ }`,
+ false,
+ networks{network{
+ IPnet: net4,
+ Template: "ip-{ip}.domain.com.",
+ Zone: "domain.com.",
+ TTL: 50,
+ RegexMatchIP: regexIP4wildcard,
+ }},
+ },
+ }
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.inputFileRules)
+ c.ServerBlockKeys = serverBlockKeys
+ networks, _, err := reverseParse(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 j, n := range networks {
+ reflect.DeepEqual(test.networks[j], n)
+ if !reflect.DeepEqual(test.networks[j], n) {
+ t.Fatalf("Test %d/%d expected %v, got %v", i, j, test.networks[j], n)
+ }
+ }
+ }
+}
diff --git a/plugin/rewrite/README.md b/plugin/rewrite/README.md
new file mode 100644
index 000000000..63334d09c
--- /dev/null
+++ b/plugin/rewrite/README.md
@@ -0,0 +1,91 @@
+# rewrite
+
+*rewrite* performs internal message rewriting.
+
+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
+
+~~~
+rewrite FIELD FROM TO
+~~~
+
+* **FIELD** is (`type`, `class`, `name`, ...)
+* **FROM** is the exact name of type to match
+* **TO** is the destination name or type to rewrite to
+
+When the FIELD is `type` and FROM is (`A`, `MX`, etc.), the type of the message will be rewritten;
+e.g., to rewrite ANY queries to HINFO, use `rewrite type ANY HINFO`.
+
+When the FIELD is `class` and FROM is (`IN`, `CH`, or `HS`) the class of the message will be
+rewritten; e.g., to rewrite CH queries to IN use `rewrite class CH IN`.
+
+When the FIELD is `name` the query name in the message is rewritten; this
+needs to be a full match of the name, e.g., `rewrite name miek.nl example.org`.
+
+When the FIELD is `edns0` an EDNS0 option can be appended to the request as described below.
+
+If you specify multiple rules and an incoming query matches on multiple (simple) rules, only
+the first rewrite is applied.
+
+## EDNS0 Options
+
+Using FIELD edns0, you can set, append, or replace specific EDNS0 options on the request.
+
+* `replace` will modify any matching (what that means may vary based on EDNS0 type) option with the specified option
+* `append` will add the option regardless of what options already exist
+* `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 can be treated as hex if it starts with `0x`. Example:
+
+~~~
+rewrite edns0 local set 0xffee 0x61626364
+~~~
+
+rewrites the first local option with code 0xffee, setting the data to "abcd". Equivalent:
+
+~~~
+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}
+
+Example:
+
+~~~
+rewrite edns0 local set 0xffee {client_ip}
+~~~
+
+### `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 has source IP as IPv4, the first 24 bits in the IP will be the network subnet.
+* If the query has source IP as IPv6, the first 56 bits in the IP will be the network subnet.
+
diff --git a/plugin/rewrite/class.go b/plugin/rewrite/class.go
new file mode 100644
index 000000000..8cc7d26b7
--- /dev/null
+++ b/plugin/rewrite/class.go
@@ -0,0 +1,35 @@
+package rewrite
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/miekg/dns"
+)
+
+type classRule struct {
+ fromClass, toClass uint16
+}
+
+func newClassRule(fromS, toS string) (Rule, error) {
+ var from, to uint16
+ var ok bool
+ if from, ok = dns.StringToClass[strings.ToUpper(fromS)]; !ok {
+ return nil, fmt.Errorf("invalid class %q", strings.ToUpper(fromS))
+ }
+ if to, ok = dns.StringToClass[strings.ToUpper(toS)]; !ok {
+ return nil, fmt.Errorf("invalid class %q", strings.ToUpper(toS))
+ }
+ return &classRule{fromClass: from, toClass: to}, nil
+}
+
+// Rewrite rewrites the the current request.
+func (rule *classRule) Rewrite(w dns.ResponseWriter, r *dns.Msg) Result {
+ if rule.fromClass > 0 && rule.toClass > 0 {
+ if r.Question[0].Qclass == rule.fromClass {
+ r.Question[0].Qclass = rule.toClass
+ return RewriteDone
+ }
+ }
+ return RewriteIgnored
+}
diff --git a/plugin/rewrite/condition.go b/plugin/rewrite/condition.go
new file mode 100644
index 000000000..2f20d71aa
--- /dev/null
+++ b/plugin/rewrite/condition.go
@@ -0,0 +1,132 @@
+package rewrite
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/coredns/coredns/plugin/pkg/replacer"
+
+ "github.com/miekg/dns"
+)
+
+// Operators
+const (
+ Is = "is"
+ Not = "not"
+ Has = "has"
+ NotHas = "not_has"
+ StartsWith = "starts_with"
+ EndsWith = "ends_with"
+ Match = "match"
+ NotMatch = "not_match"
+)
+
+func operatorError(operator string) error {
+ return fmt.Errorf("invalid operator %v", operator)
+}
+
+func newReplacer(r *dns.Msg) replacer.Replacer {
+ return replacer.New(r, nil, "")
+}
+
+// condition is a rewrite condition.
+type condition func(string, string) bool
+
+var conditions = map[string]condition{
+ Is: isFunc,
+ Not: notFunc,
+ Has: hasFunc,
+ NotHas: notHasFunc,
+ StartsWith: startsWithFunc,
+ EndsWith: endsWithFunc,
+ Match: matchFunc,
+ NotMatch: notMatchFunc,
+}
+
+// isFunc is condition for Is operator.
+// It checks for equality.
+func isFunc(a, b string) bool {
+ return a == b
+}
+
+// notFunc is condition for Not operator.
+// It checks for inequality.
+func notFunc(a, b string) bool {
+ return a != b
+}
+
+// hasFunc is condition for Has operator.
+// It checks if b is a substring of a.
+func hasFunc(a, b string) bool {
+ return strings.Contains(a, b)
+}
+
+// notHasFunc is condition for NotHas operator.
+// It checks if b is not a substring of a.
+func notHasFunc(a, b string) bool {
+ return !strings.Contains(a, b)
+}
+
+// startsWithFunc is condition for StartsWith operator.
+// It checks if b is a prefix of a.
+func startsWithFunc(a, b string) bool {
+ return strings.HasPrefix(a, b)
+}
+
+// endsWithFunc is condition for EndsWith operator.
+// It checks if b is a suffix of a.
+func endsWithFunc(a, b string) bool {
+ // TODO(miek): IsSubDomain
+ return strings.HasSuffix(a, b)
+}
+
+// matchFunc is condition for Match operator.
+// It does regexp matching of a against pattern in b
+// and returns if they match.
+func matchFunc(a, b string) bool {
+ matched, _ := regexp.MatchString(b, a)
+ return matched
+}
+
+// notMatchFunc is condition for NotMatch operator.
+// It does regexp matching of a against pattern in b
+// and returns if they do not match.
+func notMatchFunc(a, b string) bool {
+ matched, _ := regexp.MatchString(b, a)
+ return !matched
+}
+
+// If is statement for a rewrite condition.
+type If struct {
+ A string
+ Operator string
+ B string
+}
+
+// True returns true if the condition is true and false otherwise.
+// If r is not nil, it replaces placeholders before comparison.
+func (i If) True(r *dns.Msg) bool {
+ if c, ok := conditions[i.Operator]; ok {
+ a, b := i.A, i.B
+ if r != nil {
+ replacer := newReplacer(r)
+ a = replacer.Replace(i.A)
+ b = replacer.Replace(i.B)
+ }
+ return c(a, b)
+ }
+ return false
+}
+
+// NewIf creates a new If condition.
+func NewIf(a, operator, b string) (If, error) {
+ if _, ok := conditions[operator]; !ok {
+ return If{}, operatorError(operator)
+ }
+ return If{
+ A: a,
+ Operator: operator,
+ B: b,
+ }, nil
+}
diff --git a/plugin/rewrite/condition_test.go b/plugin/rewrite/condition_test.go
new file mode 100644
index 000000000..91004f9d7
--- /dev/null
+++ b/plugin/rewrite/condition_test.go
@@ -0,0 +1,102 @@
+package rewrite
+
+/*
+func TestConditions(t *testing.T) {
+ tests := []struct {
+ condition string
+ isTrue bool
+ }{
+ {"a is b", false},
+ {"a is a", true},
+ {"a not b", true},
+ {"a not a", false},
+ {"a has a", true},
+ {"a has b", false},
+ {"ba has b", true},
+ {"bab has b", true},
+ {"bab has bb", false},
+ {"a not_has a", false},
+ {"a not_has b", true},
+ {"ba not_has b", false},
+ {"bab not_has b", false},
+ {"bab not_has bb", true},
+ {"bab starts_with bb", false},
+ {"bab starts_with ba", true},
+ {"bab starts_with bab", true},
+ {"bab ends_with bb", false},
+ {"bab ends_with bab", true},
+ {"bab ends_with ab", true},
+ {"a match *", false},
+ {"a match a", true},
+ {"a match .*", true},
+ {"a match a.*", true},
+ {"a match b.*", false},
+ {"ba match b.*", true},
+ {"ba match b[a-z]", true},
+ {"b0 match b[a-z]", false},
+ {"b0a match b[a-z]", false},
+ {"b0a match b[a-z]+", false},
+ {"b0a match b[a-z0-9]+", true},
+ {"a not_match *", true},
+ {"a not_match a", false},
+ {"a not_match .*", false},
+ {"a not_match a.*", false},
+ {"a not_match b.*", true},
+ {"ba not_match b.*", false},
+ {"ba not_match b[a-z]", false},
+ {"b0 not_match b[a-z]", true},
+ {"b0a not_match b[a-z]", true},
+ {"b0a not_match b[a-z]+", true},
+ {"b0a not_match b[a-z0-9]+", false},
+ }
+
+ for i, test := range tests {
+ str := strings.Fields(test.condition)
+ ifCond, err := NewIf(str[0], str[1], str[2])
+ if err != nil {
+ t.Error(err)
+ }
+ isTrue := ifCond.True(nil)
+ if isTrue != test.isTrue {
+ t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue)
+ }
+ }
+
+ invalidOperators := []string{"ss", "and", "if"}
+ for _, op := range invalidOperators {
+ _, err := NewIf("a", op, "b")
+ if err == nil {
+ t.Errorf("Invalid operator %v used, expected error.", op)
+ }
+ }
+
+ replaceTests := []struct {
+ url string
+ condition string
+ isTrue bool
+ }{
+ {"/home", "{uri} match /home", true},
+ {"/hom", "{uri} match /home", false},
+ {"/hom", "{uri} starts_with /home", false},
+ {"/hom", "{uri} starts_with /h", true},
+ {"/home/.hiddenfile", `{uri} match \/\.(.*)`, true},
+ {"/home/.hiddendir/afile", `{uri} match \/\.(.*)`, true},
+ }
+
+ for i, test := range replaceTests {
+ r, err := http.NewRequest("GET", test.url, nil)
+ if err != nil {
+ t.Error(err)
+ }
+ str := strings.Fields(test.condition)
+ ifCond, err := NewIf(str[0], str[1], str[2])
+ if err != nil {
+ t.Error(err)
+ }
+ isTrue := ifCond.True(r)
+ if isTrue != test.isTrue {
+ t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue)
+ }
+ }
+}
+*/
diff --git a/plugin/rewrite/edns0.go b/plugin/rewrite/edns0.go
new file mode 100644
index 000000000..d8b6f4128
--- /dev/null
+++ b/plugin/rewrite/edns0.go
@@ -0,0 +1,425 @@
+// Package rewrite is plugin for rewriting requests internally to something different.
+package rewrite
+
+import (
+ "encoding/binary"
+ "encoding/hex"
+ "fmt"
+ "net"
+ "strconv"
+ "strings"
+
+ "github.com/coredns/coredns/request"
+ "github.com/miekg/dns"
+)
+
+// edns0LocalRule is a rewrite rule for EDNS0_LOCAL options
+type edns0LocalRule struct {
+ action string
+ code uint16
+ data []byte
+}
+
+// edns0VariableRule is a rewrite rule for EDNS0_LOCAL options with variable
+type edns0VariableRule struct {
+ action string
+ code uint16
+ variable string
+}
+
+// ends0NsidRule is a rewrite rule for EDNS0_NSID options
+type edns0NsidRule struct {
+ 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, true)
+ o = r.IsEdns0()
+ }
+ return o
+}
+
+// Rewrite will alter the request EDNS0 NSID option
+func (rule *edns0NsidRule) Rewrite(w dns.ResponseWriter, r *dns.Msg) Result {
+ result := RewriteIgnored
+ o := setupEdns0Opt(r)
+ found := false
+Option:
+ for _, s := range o.Option {
+ switch e := s.(type) {
+ case *dns.EDNS0_NSID:
+ if rule.action == Replace || rule.action == Set {
+ e.Nsid = "" // make sure it is empty for request
+ result = RewriteDone
+ }
+ found = true
+ break Option
+ }
+ }
+
+ // add option if not found
+ if !found && (rule.action == Append || rule.action == Set) {
+ o.SetDo()
+ o.Option = append(o.Option, &dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""})
+ result = RewriteDone
+ }
+
+ return result
+}
+
+// Rewrite will alter the request EDNS0 local options
+func (rule *edns0LocalRule) Rewrite(w dns.ResponseWriter, r *dns.Msg) Result {
+ result := RewriteIgnored
+ o := setupEdns0Opt(r)
+ found := false
+ for _, s := range o.Option {
+ switch e := s.(type) {
+ case *dns.EDNS0_LOCAL:
+ if rule.code == e.Code {
+ if rule.action == Replace || rule.action == Set {
+ e.Data = rule.data
+ result = RewriteDone
+ }
+ found = true
+ break
+ }
+ }
+ }
+
+ // add option if not found
+ if !found && (rule.action == Append || rule.action == Set) {
+ o.SetDo()
+ var opt dns.EDNS0_LOCAL
+ opt.Code = rule.code
+ opt.Data = rule.data
+ o.Option = append(o.Option, &opt)
+ result = RewriteDone
+ }
+
+ return result
+}
+
+// newEdns0Rule creates an EDNS0 rule of the appropriate type based on the args
+func newEdns0Rule(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(action, args[2], args[3])
+ }
+ return newEdns0LocalRule(action, args[2], args[3])
+ case "nsid":
+ if len(args) != 2 {
+ return nil, fmt.Errorf("EDNS0 NSID rules do not accept args")
+ }
+ return &edns0NsidRule{action: action}, nil
+ case "subnet":
+ if len(args) != 4 {
+ return nil, fmt.Errorf("EDNS0 subnet rules require exactly three args")
+ }
+ return newEdns0SubnetRule(action, args[2], args[3])
+ default:
+ return nil, fmt.Errorf("invalid rule type %q", ruleType)
+ }
+}
+
+func newEdns0LocalRule(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
+ }
+ }
+ return &edns0LocalRule{action: action, code: uint16(c), data: decoded}, nil
+}
+
+// newEdns0VariableRule creates an EDNS0 rule that handles variable substitution
+func newEdns0VariableRule(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)
+ }
+ return &edns0VariableRule{action: action, code: uint16(c), variable: variable}, nil
+}
+
+// ipToWire writes IP address to wire/binary format, 4 or 16 bytes depends on IPV4 or IPV6.
+func (rule *edns0VariableRule) 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 (rule *edns0VariableRule) 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 (rule *edns0VariableRule) portToWire(portStr string) ([]byte, error) {
+
+ port, err := strconv.ParseUint(portStr, 10, 16)
+ if err != nil {
+ return nil, err
+ }
+ return rule.uint16ToWire(uint16(port)), nil
+}
+
+// Family returns the family of the transport, 1 for IPv4 and 2 for IPv6.
+func (rule *edns0VariableRule) family(ip net.Addr) int {
+ var a net.IP
+ if i, ok := ip.(*net.UDPAddr); ok {
+ a = i.IP
+ }
+ if i, ok := ip.(*net.TCPAddr); ok {
+ a = i.IP
+ }
+ if a.To4() != nil {
+ return 1
+ }
+ return 2
+}
+
+// ruleData returns the data specified by the variable
+func (rule *edns0VariableRule) ruleData(w dns.ResponseWriter, r *dns.Msg) ([]byte, error) {
+
+ req := request.Request{W: w, Req: r}
+ switch rule.variable {
+ case queryName:
+ //Query name is written as ascii string
+ return []byte(req.QName()), nil
+
+ case queryType:
+ return rule.uint16ToWire(req.QType()), nil
+
+ case clientIP:
+ return rule.ipToWire(req.Family(), req.IP())
+
+ case clientPort:
+ return rule.portToWire(req.Port())
+
+ case protocol:
+ // Proto is written as ascii string
+ return []byte(req.Proto()), nil
+
+ case serverIP:
+ ip, _, err := net.SplitHostPort(w.LocalAddr().String())
+ if err != nil {
+ ip = w.RemoteAddr().String()
+ }
+ return rule.ipToWire(rule.family(w.RemoteAddr()), ip)
+
+ case serverPort:
+ _, port, err := net.SplitHostPort(w.LocalAddr().String())
+ if err != nil {
+ port = "0"
+ }
+ return rule.portToWire(port)
+ }
+
+ 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(w dns.ResponseWriter, r *dns.Msg) Result {
+ result := RewriteIgnored
+
+ data, err := rule.ruleData(w, r)
+ if err != nil || data == nil {
+ return result
+ }
+
+ o := setupEdns0Opt(r)
+ found := false
+ for _, s := range o.Option {
+ switch e := s.(type) {
+ case *dns.EDNS0_LOCAL:
+ if rule.code == e.Code {
+ if rule.action == Replace || rule.action == Set {
+ e.Data = data
+ result = RewriteDone
+ }
+ found = true
+ break
+ }
+ }
+ }
+
+ // add option if not found
+ if !found && (rule.action == Append || rule.action == Set) {
+ o.SetDo()
+ var opt dns.EDNS0_LOCAL
+ opt.Code = rule.code
+ opt.Data = data
+ o.Option = append(o.Option, &opt)
+ result = RewriteDone
+ }
+
+ return result
+}
+
+func isValidVariable(variable string) bool {
+ switch variable {
+ case
+ queryName,
+ queryType,
+ clientIP,
+ clientPort,
+ protocol,
+ serverIP,
+ serverPort:
+ return true
+ }
+ return false
+}
+
+// ends0SubnetRule is a rewrite rule for EDNS0 subnet options
+type edns0SubnetRule struct {
+ v4BitMaskLen uint8
+ v6BitMaskLen uint8
+ action string
+}
+
+func newEdns0SubnetRule(action, v4BitMaskLen, v6BitMaskLen string) (*edns0SubnetRule, error) {
+ v4Len, err := strconv.ParseUint(v4BitMaskLen, 0, 16)
+ if err != nil {
+ return nil, err
+ }
+ // Validate V4 length
+ if v4Len > maxV4BitMaskLen {
+ 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 > maxV6BitMaskLen {
+ return nil, fmt.Errorf("invalid IPv6 bit mask length %d", v6Len)
+ }
+
+ return &edns0SubnetRule{action: action,
+ v4BitMaskLen: uint8(v4Len), v6BitMaskLen: uint8(v6Len)}, nil
+}
+
+// fillEcsData sets the subnet data into the ecs option
+func (rule *edns0SubnetRule) fillEcsData(w dns.ResponseWriter, r *dns.Msg,
+ ecs *dns.EDNS0_SUBNET) error {
+
+ req := request.Request{W: w, Req: r}
+ family := req.Family()
+ if (family != 1) && (family != 2) {
+ return fmt.Errorf("unable to fill data for EDNS0 subnet due to invalid IP family")
+ }
+
+ ecs.DraftOption = false
+ ecs.Family = uint16(family)
+ ecs.SourceScope = 0
+
+ ipAddr := req.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(w dns.ResponseWriter, r *dns.Msg) Result {
+ result := RewriteIgnored
+ o := setupEdns0Opt(r)
+ found := false
+ for _, s := range o.Option {
+ switch e := s.(type) {
+ case *dns.EDNS0_SUBNET:
+ if rule.action == Replace || rule.action == Set {
+ if rule.fillEcsData(w, r, e) == nil {
+ result = RewriteDone
+ }
+ }
+ found = true
+ break
+ }
+ }
+
+ // add option if not found
+ if !found && (rule.action == Append || rule.action == Set) {
+ o.SetDo()
+ opt := dns.EDNS0_SUBNET{Code: dns.EDNS0SUBNET}
+ if rule.fillEcsData(w, r, &opt) == nil {
+ o.Option = append(o.Option, &opt)
+ result = RewriteDone
+ }
+ }
+
+ return result
+}
+
+// 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}"
+)
+
+// Subnet maximum bit mask length
+const (
+ maxV4BitMaskLen = 32
+ maxV6BitMaskLen = 128
+)
diff --git a/plugin/rewrite/name.go b/plugin/rewrite/name.go
new file mode 100644
index 000000000..189133542
--- /dev/null
+++ b/plugin/rewrite/name.go
@@ -0,0 +1,24 @@
+package rewrite
+
+import (
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/miekg/dns"
+)
+
+type nameRule struct {
+ From, To string
+}
+
+func newNameRule(from, to string) (Rule, error) {
+ return &nameRule{plugin.Name(from).Normalize(), plugin.Name(to).Normalize()}, nil
+}
+
+// Rewrite rewrites the the current request.
+func (rule *nameRule) Rewrite(w dns.ResponseWriter, r *dns.Msg) Result {
+ if rule.From == r.Question[0].Name {
+ r.Question[0].Name = rule.To
+ return RewriteDone
+ }
+ return RewriteIgnored
+}
diff --git a/plugin/rewrite/reverter.go b/plugin/rewrite/reverter.go
new file mode 100644
index 000000000..400fb5fff
--- /dev/null
+++ b/plugin/rewrite/reverter.go
@@ -0,0 +1,39 @@
+package rewrite
+
+import "github.com/miekg/dns"
+
+// 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 miek.nl/HINFO/IN'
+type ResponseReverter struct {
+ dns.ResponseWriter
+ original dns.Question
+}
+
+// NewResponseReverter returns a pointer to a new ResponseReverter.
+func NewResponseReverter(w dns.ResponseWriter, r *dns.Msg) *ResponseReverter {
+ return &ResponseReverter{
+ ResponseWriter: w,
+ original: r.Question[0],
+ }
+}
+
+// WriteMsg records the status code and calls the
+// underlying ResponseWriter's WriteMsg method.
+func (r *ResponseReverter) WriteMsg(res *dns.Msg) error {
+ res.Question[0] = r.original
+ return r.ResponseWriter.WriteMsg(res)
+}
+
+// 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
+}
+
+// Hijack implements dns.Hijacker. It simply wraps the underlying
+// ResponseWriter's Hijack method if there is one, or returns an error.
+func (r *ResponseReverter) Hijack() {
+ r.ResponseWriter.Hijack()
+ return
+}
diff --git a/plugin/rewrite/rewrite.go b/plugin/rewrite/rewrite.go
new file mode 100644
index 000000000..d4931445c
--- /dev/null
+++ b/plugin/rewrite/rewrite.go
@@ -0,0 +1,86 @@
+package rewrite
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/miekg/dns"
+
+ "golang.org/x/net/context"
+)
+
+// 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
+ // RewriteStatus is returned when rewrite is not needed and status code should be set
+ // for the request.
+ RewriteStatus
+)
+
+// Rewrite is plugin to rewrite requests internally before being handled.
+type Rewrite struct {
+ Next plugin.Handler
+ Rules []Rule
+ noRevert bool
+}
+
+// ServeDNS implements the plugin.Handler interface.
+func (rw Rewrite) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ wr := NewResponseReverter(w, r)
+ for _, rule := range rw.Rules {
+ switch result := rule.Rewrite(w, r); result {
+ case RewriteDone:
+ if rw.noRevert {
+ return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, w, r)
+ }
+ return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, wr, r)
+ case RewriteIgnored:
+ break
+ case RewriteStatus:
+ // only valid for complex rules.
+ // if cRule, ok := rule.(*ComplexRule); ok && cRule.Status != 0 {
+ // return cRule.Status, nil
+ // }
+ }
+ }
+ return plugin.NextOrFailure(rw.Name(), rw.Next, ctx, w, 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(dns.ResponseWriter, *dns.Msg) Result
+}
+
+func newRule(args ...string) (Rule, error) {
+ if len(args) == 0 {
+ return nil, fmt.Errorf("no rule type specified for rewrite")
+ }
+
+ ruleType := strings.ToLower(args[0])
+ if ruleType != "edns0" && len(args) != 3 {
+ return nil, fmt.Errorf("%s rules must have exactly two arguments", ruleType)
+ }
+ switch ruleType {
+ case "name":
+ return newNameRule(args[1], args[2])
+ case "class":
+ return newClassRule(args[1], args[2])
+ case "type":
+ return newTypeRule(args[1], args[2])
+ case "edns0":
+ return newEdns0Rule(args[1:]...)
+ default:
+ return nil, fmt.Errorf("invalid rule type %q", args[0])
+ }
+}
diff --git a/plugin/rewrite/rewrite_test.go b/plugin/rewrite/rewrite_test.go
new file mode 100644
index 000000000..74a8594df
--- /dev/null
+++ b/plugin/rewrite/rewrite_test.go
@@ -0,0 +1,532 @@
+package rewrite
+
+import (
+ "bytes"
+ "reflect"
+ "testing"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func msgPrinter(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ 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(&nameRule{})},
+ {[]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{})},
+ }
+
+ 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 TestRewrite(t *testing.T) {
+ rules := []Rule{}
+ r, _ := newNameRule("from.nl.", "to.nl.")
+ rules = append(rules, r)
+ r, _ = newClassRule("CH", "IN")
+ rules = append(rules, r)
+ r, _ = newTypeRule("ANY", "HINFO")
+ rules = append(rules, r)
+
+ rw := Rewrite{
+ Next: plugin.HandlerFunc(msgPrinter),
+ Rules: rules,
+ noRevert: true,
+ }
+
+ 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},
+ // 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},
+ }
+
+ 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 := dnsrecorder.New(&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)
+ }
+ }
+}
+
+func TestRewriteEDNS0Local(t *testing.T) {
+ rw := Rewrite{
+ Next: plugin.HandlerFunc(msgPrinter),
+ noRevert: true,
+ }
+
+ tests := []struct {
+ fromOpts []dns.EDNS0
+ args []string
+ toOpts []dns.EDNS0
+ }{
+ {
+ []dns.EDNS0{},
+ []string{"local", "set", "0xffee", "0xabcdef"},
+ []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0xab, 0xcd, 0xef}}},
+ },
+ {
+ []dns.EDNS0{},
+ []string{"local", "append", "0xffee", "abcdefghijklmnop"},
+ []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("abcdefghijklmnop")}},
+ },
+ {
+ []dns.EDNS0{},
+ []string{"local", "replace", "0xffee", "abcdefghijklmnop"},
+ []dns.EDNS0{},
+ },
+ {
+ []dns.EDNS0{},
+ []string{"nsid", "set"},
+ []dns.EDNS0{&dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""}},
+ },
+ {
+ []dns.EDNS0{},
+ []string{"nsid", "append"},
+ []dns.EDNS0{&dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""}},
+ },
+ {
+ []dns.EDNS0{},
+ []string{"nsid", "replace"},
+ []dns.EDNS0{},
+ },
+ }
+
+ 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(tc.args...)
+ if err != nil {
+ t.Errorf("Error creating test rule: %s", err)
+ continue
+ }
+ rw.Rules = []Rule{r}
+
+ rec := dnsrecorder.New(&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 TestEdns0LocalMultiRule(t *testing.T) {
+ rules := []Rule{}
+ r, _ := newEdns0Rule("local", "replace", "0xffee", "abcdef")
+ rules = append(rules, r)
+ r, _ = newEdns0Rule("local", "set", "0xffee", "fedcba")
+ rules = append(rules, r)
+
+ rw := Rewrite{
+ Next: plugin.HandlerFunc(msgPrinter),
+ Rules: rules,
+ noRevert: true,
+ }
+
+ 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 := dnsrecorder.New(&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 !bytes.Equal(aa.Address, bb.Address) {
+ return false
+ }
+ if aa.DraftOption != bb.DraftOption {
+ return false
+ }
+ } else {
+ return false
+ }
+
+ default:
+ return false
+ }
+ }
+ return true
+}
+
+func TestRewriteEDNS0LocalVariable(t *testing.T) {
+ rw := Rewrite{
+ Next: plugin.HandlerFunc(msgPrinter),
+ noRevert: true,
+ }
+
+ // 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
+ }{
+ {
+ []dns.EDNS0{},
+ []string{"local", "set", "0xffee", "{qname}"},
+ []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("example.com.")}},
+ },
+ {
+ []dns.EDNS0{},
+ []string{"local", "set", "0xffee", "{qtype}"},
+ []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x00, 0x01}}},
+ },
+ {
+ []dns.EDNS0{},
+ []string{"local", "set", "0xffee", "{client_ip}"},
+ []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x0A, 0xF0, 0x00, 0x01}}},
+ },
+ {
+ []dns.EDNS0{},
+ []string{"local", "set", "0xffee", "{client_port}"},
+ []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x9D, 0x14}}},
+ },
+ {
+ []dns.EDNS0{},
+ []string{"local", "set", "0xffee", "{protocol}"},
+ []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("udp")}},
+ },
+ {
+ []dns.EDNS0{},
+ []string{"local", "set", "0xffee", "{server_ip}"},
+ []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x7F, 0x00, 0x00, 0x01}}},
+ },
+ {
+ []dns.EDNS0{},
+ []string{"local", "set", "0xffee", "{server_port}"},
+ []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0x00, 0x35}}},
+ },
+ }
+
+ 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(tc.args...)
+ if err != nil {
+ t.Errorf("Error creating test rule: %s", err)
+ continue
+ }
+ rw.Rules = []Rule{r}
+
+ rec := dnsrecorder.New(&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 TestRewriteEDNS0Subnet(t *testing.T) {
+ rw := Rewrite{
+ Next: plugin.HandlerFunc(msgPrinter),
+ noRevert: true,
+ }
+
+ tests := []struct {
+ writer dns.ResponseWriter
+ fromOpts []dns.EDNS0
+ args []string
+ toOpts []dns.EDNS0
+ }{
+ {
+ &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},
+ DraftOption: false}},
+ },
+ {
+ &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},
+ DraftOption: 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},
+ DraftOption: 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},
+ DraftOption: false}},
+ },
+ {
+ &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},
+ DraftOption: 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},
+ DraftOption: false}},
+ },
+ }
+
+ 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(tc.args...)
+ if err != nil {
+ t.Errorf("Error creating test rule: %s", err)
+ continue
+ }
+ rw.Rules = []Rule{r}
+ rec := dnsrecorder.New(tc.writer)
+ 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)
+ }
+ }
+}
diff --git a/plugin/rewrite/setup.go b/plugin/rewrite/setup.go
new file mode 100644
index 000000000..5954a3300
--- /dev/null
+++ b/plugin/rewrite/setup.go
@@ -0,0 +1,42 @@
+package rewrite
+
+import (
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("rewrite", caddy.Plugin{
+ ServerType: "dns",
+ Action: 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()
+ rule, err := newRule(args...)
+ if err != nil {
+ return nil, err
+ }
+ rules = append(rules, rule)
+ }
+ return rules, nil
+}
diff --git a/plugin/rewrite/setup_test.go b/plugin/rewrite/setup_test.go
new file mode 100644
index 000000000..67ef88e18
--- /dev/null
+++ b/plugin/rewrite/setup_test.go
@@ -0,0 +1,25 @@
+package rewrite
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestParse(t *testing.T) {
+ c := caddy.NewTestController("dns", `rewrite`)
+ _, err := rewriteParse(c)
+ if err == nil {
+ t.Errorf("Expected error but found nil for `rewrite`")
+ }
+ c = caddy.NewTestController("dns", `rewrite name`)
+ _, err = rewriteParse(c)
+ if err == nil {
+ t.Errorf("Expected error but found nil for `rewrite name`")
+ }
+ c = caddy.NewTestController("dns", `rewrite name a.com b.com`)
+ _, err = rewriteParse(c)
+ if err != nil {
+ t.Errorf("Expected success but found %s for `rewrite name a.com b.com`", err)
+ }
+}
diff --git a/plugin/rewrite/testdata/testdir/empty b/plugin/rewrite/testdata/testdir/empty
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/plugin/rewrite/testdata/testdir/empty
diff --git a/plugin/rewrite/testdata/testfile b/plugin/rewrite/testdata/testfile
new file mode 100644
index 000000000..7b4d68d70
--- /dev/null
+++ b/plugin/rewrite/testdata/testfile
@@ -0,0 +1 @@
+empty \ No newline at end of file
diff --git a/plugin/rewrite/type.go b/plugin/rewrite/type.go
new file mode 100644
index 000000000..ae3efcc5a
--- /dev/null
+++ b/plugin/rewrite/type.go
@@ -0,0 +1,37 @@
+// Package rewrite is plugin for rewriting requests internally to something different.
+package rewrite
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/miekg/dns"
+)
+
+// typeRule is a type rewrite rule.
+type typeRule struct {
+ fromType, toType uint16
+}
+
+func newTypeRule(fromS, toS string) (Rule, error) {
+ var from, to uint16
+ var ok bool
+ if from, ok = dns.StringToType[strings.ToUpper(fromS)]; !ok {
+ return nil, fmt.Errorf("invalid type %q", strings.ToUpper(fromS))
+ }
+ if to, ok = dns.StringToType[strings.ToUpper(toS)]; !ok {
+ return nil, fmt.Errorf("invalid type %q", strings.ToUpper(toS))
+ }
+ return &typeRule{fromType: from, toType: to}, nil
+}
+
+// Rewrite rewrites the the current request.
+func (rule *typeRule) Rewrite(w dns.ResponseWriter, r *dns.Msg) Result {
+ if rule.fromType > 0 && rule.toType > 0 {
+ if r.Question[0].Qtype == rule.fromType {
+ r.Question[0].Qtype = rule.toType
+ return RewriteDone
+ }
+ }
+ return RewriteIgnored
+}
diff --git a/plugin/root/README.md b/plugin/root/README.md
new file mode 100644
index 000000000..bd3fe33b3
--- /dev/null
+++ b/plugin/root/README.md
@@ -0,0 +1,22 @@
+# root
+
+*root* simply specifies the root of where CoreDNS finds (e.g.) zone files.
+
+The default root is the current working directory of CoreDNS. A relative root path is relative to
+the current working directory.
+
+## 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`:
+
+~~~ txt
+root /etc/coredns/zones
+~~~
diff --git a/plugin/root/root.go b/plugin/root/root.go
new file mode 100644
index 000000000..56fd42c01
--- /dev/null
+++ b/plugin/root/root.go
@@ -0,0 +1,43 @@
+package root
+
+import (
+ "log"
+ "os"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("root", caddy.Plugin{
+ ServerType: "dns",
+ Action: 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.Printf("[WARNING] 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/plugin/root/root_test.go b/plugin/root/root_test.go
new file mode 100644
index 000000000..ea0e53b5e
--- /dev/null
+++ b/plugin/root/root_test.go
@@ -0,0 +1,107 @@
+package root
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/core/dnsserver"
+
+ "github.com/mholt/caddy"
+)
+
+func TestRoot(t *testing.T) {
+ log.SetOutput(ioutil.Discard)
+
+ // 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 := ioutil.TempFile("", "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 returnes the path to the system temp directory. If it does not exists - 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/plugin/secondary/README.md b/plugin/secondary/README.md
new file mode 100644
index 000000000..d6cbe465a
--- /dev/null
+++ b/plugin/secondary/README.md
@@ -0,0 +1,54 @@
+# secondary
+
+*secondary* enables serving a zone retrieved from a primary server.
+
+## 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
+ transfer to ADDRESS
+ upstream 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.
+* `transfer to` can be enabled to allow this secondary zone to be transferred again.
+* `upstream` defines upstream resolvers to be used resolve external names found (think CNAMEs)
+ pointing to external names. This is only really useful when CoreDNS is configured as a proxy, for
+ normal authoritative serving you don't need *or* want to use this. **ADDRESS** can be an IP
+ address, and IP:port or a string pointing to a file that is structured as /etc/resolv.conf.
+
+## 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
+ transfer from 10.1.2.1
+ }
+}
+~~~
+
+Or re-export the retrieved zone to other secondaries.
+
+~~~ corefile
+. {
+ secondary example.net {
+ transfer from 10.1.2.1
+ transfer to *
+ }
+}
+~~~
diff --git a/plugin/secondary/secondary.go b/plugin/secondary/secondary.go
new file mode 100644
index 000000000..43934e80c
--- /dev/null
+++ b/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/plugin/secondary/setup.go b/plugin/secondary/setup.go
new file mode 100644
index 000000000..e2819197d
--- /dev/null
+++ b/plugin/secondary/setup.go
@@ -0,0 +1,108 @@
+package secondary
+
+import (
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/file"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/proxy"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("secondary", caddy.Plugin{
+ ServerType: "dns",
+ Action: 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 _, n := range zones.Names {
+ z := zones.Z[n]
+ if len(z.TransferFrom) > 0 {
+ c.OnStartup(func() error {
+ z.StartupOnce.Do(func() {
+ z.TransferIn()
+ go func() {
+ 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{}
+ origins := []string{}
+ prxy := proxy.Proxy{}
+ for c.Next() {
+
+ if c.Val() == "secondary" {
+ // secondary [origin]
+ origins = make([]string, len(c.ServerBlockKeys))
+ copy(origins, c.ServerBlockKeys)
+ args := c.RemainingArgs()
+ if len(args) > 0 {
+ origins = args
+ }
+ for i := range origins {
+ origins[i] = plugin.Host(origins[i]).Normalize()
+ z[origins[i]] = file.NewZone(origins[i], "stdin")
+ names = append(names, origins[i])
+ }
+
+ for c.NextBlock() {
+
+ t, f := []string{}, []string{}
+ var e error
+
+ switch c.Val() {
+ case "transfer":
+ t, f, e = file.TransferParse(c, true)
+ if e != nil {
+ return file.Zones{}, e
+ }
+ case "upstream":
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ return file.Zones{}, c.ArgErr()
+ }
+ ups, err := dnsutil.ParseHostPortOrFile(args...)
+ if err != nil {
+ return file.Zones{}, err
+ }
+ prxy = proxy.NewLookup(ups)
+ default:
+ return file.Zones{}, c.Errf("unknown property '%s'", c.Val())
+ }
+
+ for _, origin := range origins {
+ if t != nil {
+ z[origin].TransferTo = append(z[origin].TransferTo, t...)
+ }
+ if f != nil {
+ z[origin].TransferFrom = append(z[origin].TransferFrom, f...)
+ }
+ z[origin].Proxy = prxy
+ }
+ }
+ }
+ }
+ return file.Zones{Z: z, Names: names}, nil
+}
diff --git a/plugin/secondary/setup_test.go b/plugin/secondary/setup_test.go
new file mode 100644
index 000000000..bf2b203ad
--- /dev/null
+++ b/plugin/secondary/setup_test.go
@@ -0,0 +1,65 @@
+package secondary
+
+import (
+ "testing"
+
+ "github.com/mholt/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
+ transfer to 127.0.0.1
+ }`,
+ false,
+ "127.0.0.1:53",
+ nil,
+ },
+ {
+ `secondary example.org {
+ transfer from 127.0.0.1
+ transfer to 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 *iff* 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/plugin/test/doc.go b/plugin/test/doc.go
new file mode 100644
index 000000000..75281ed8b
--- /dev/null
+++ b/plugin/test/doc.go
@@ -0,0 +1,2 @@
+// Package test contains helper functions for writing plugin tests.
+package test
diff --git a/plugin/test/file.go b/plugin/test/file.go
new file mode 100644
index 000000000..f87300e55
--- /dev/null
+++ b/plugin/test/file.go
@@ -0,0 +1,107 @@
+package test
+
+import (
+ "io/ioutil"
+ "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 := ioutil.TempFile(dir, "go-test-tmpfile")
+ if err != nil {
+ return "", nil, err
+ }
+ if err := ioutil.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 := ioutil.TempDir(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 := ioutil.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 = ioutil.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 = ioutil.WriteFile(path, []byte(data), 0644); err != nil {
+ return "", nil, err
+ }
+
+ rmFunc := func() { os.RemoveAll(tempDir) }
+ return tempDir, rmFunc, nil
+}
diff --git a/plugin/test/file_test.go b/plugin/test/file_test.go
new file mode 100644
index 000000000..ed86a8260
--- /dev/null
+++ b/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/plugin/test/helpers.go b/plugin/test/helpers.go
new file mode 100644
index 000000000..065cf8935
--- /dev/null
+++ b/plugin/test/helpers.go
@@ -0,0 +1,348 @@
+package test
+
+import (
+ "sort"
+ "testing"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+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
+ 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) }
+
+// 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) }
+
+// 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 test if the header in resp matches the header as defined in tc.
+func Header(t *testing.T, tc Case, resp *dns.Msg) bool {
+ if resp.Rcode != tc.Rcode {
+ t.Errorf("rcode is %q, expected %q", dns.RcodeToString[resp.Rcode], dns.RcodeToString[tc.Rcode])
+ return false
+ }
+
+ if len(resp.Answer) != len(tc.Answer) {
+ t.Errorf("answer for %q contained %d results, %d expected", tc.Qname, len(resp.Answer), len(tc.Answer))
+ return false
+ }
+ if len(resp.Ns) != len(tc.Ns) {
+ t.Errorf("authority for %q contained %d results, %d expected", tc.Qname, len(resp.Ns), len(tc.Ns))
+ return false
+ }
+ if len(resp.Extra) != len(tc.Extra) {
+ t.Errorf("additional for %q contained %d results, %d expected", tc.Qname, len(resp.Extra), len(tc.Extra))
+ return false
+ }
+ return true
+}
+
+// Section tests if the the section in tc matches rr.
+func Section(t *testing.T, tc Case, sec sect, rr []dns.RR) bool {
+ 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 {
+ t.Errorf("rr %d should have a Header Name of %q, but has %q", i, section[i].Header().Name, a.Header().Name)
+ return false
+ }
+ // 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
+ t.Errorf("rr %d should have a Header TTL of %d, but has %d", i, section[i].Header().Ttl, a.Header().Ttl)
+ return false
+ }
+ }
+ if a.Header().Rrtype != section[i].Header().Rrtype {
+ t.Errorf("rr %d should have a header rr type of %d, but has %d", i, section[i].Header().Rrtype, a.Header().Rrtype)
+ return false
+ }
+
+ switch x := a.(type) {
+ case *dns.SRV:
+ if x.Priority != section[i].(*dns.SRV).Priority {
+ t.Errorf("rr %d should have a Priority of %d, but has %d", i, section[i].(*dns.SRV).Priority, x.Priority)
+ return false
+ }
+ if x.Weight != section[i].(*dns.SRV).Weight {
+ t.Errorf("rr %d should have a Weight of %d, but has %d", i, section[i].(*dns.SRV).Weight, x.Weight)
+ return false
+ }
+ if x.Port != section[i].(*dns.SRV).Port {
+ t.Errorf("rr %d should have a Port of %d, but has %d", i, section[i].(*dns.SRV).Port, x.Port)
+ return false
+ }
+ if x.Target != section[i].(*dns.SRV).Target {
+ t.Errorf("rr %d should have a Target of %q, but has %q", i, section[i].(*dns.SRV).Target, x.Target)
+ return false
+ }
+ case *dns.RRSIG:
+ if x.TypeCovered != section[i].(*dns.RRSIG).TypeCovered {
+ t.Errorf("rr %d should have a TypeCovered of %d, but has %d", i, section[i].(*dns.RRSIG).TypeCovered, x.TypeCovered)
+ return false
+ }
+ if x.Labels != section[i].(*dns.RRSIG).Labels {
+ t.Errorf("rr %d should have a Labels of %d, but has %d", i, section[i].(*dns.RRSIG).Labels, x.Labels)
+ return false
+ }
+ if x.SignerName != section[i].(*dns.RRSIG).SignerName {
+ t.Errorf("rr %d should have a SignerName of %s, but has %s", i, section[i].(*dns.RRSIG).SignerName, x.SignerName)
+ return false
+ }
+ case *dns.NSEC:
+ if x.NextDomain != section[i].(*dns.NSEC).NextDomain {
+ t.Errorf("rr %d should have a NextDomain of %s, but has %s", i, section[i].(*dns.NSEC).NextDomain, x.NextDomain)
+ return false
+ }
+ // TypeBitMap
+ case *dns.A:
+ if x.A.String() != section[i].(*dns.A).A.String() {
+ t.Errorf("rr %d should have a Address of %q, but has %q", i, section[i].(*dns.A).A.String(), x.A.String())
+ return false
+ }
+ case *dns.AAAA:
+ if x.AAAA.String() != section[i].(*dns.AAAA).AAAA.String() {
+ t.Errorf("rr %d should have a Address of %q, but has %q", i, section[i].(*dns.AAAA).AAAA.String(), x.AAAA.String())
+ return false
+ }
+ case *dns.TXT:
+ for j, txt := range x.Txt {
+ if txt != section[i].(*dns.TXT).Txt[j] {
+ t.Errorf("rr %d should have a Txt of %q, but has %q", i, section[i].(*dns.TXT).Txt[j], txt)
+ return false
+ }
+ }
+ case *dns.SOA:
+ tt := section[i].(*dns.SOA)
+ if x.Ns != tt.Ns {
+ t.Errorf("SOA nameserver should be %q, but is %q", tt.Ns, x.Ns)
+ return false
+ }
+ case *dns.PTR:
+ tt := section[i].(*dns.PTR)
+ if x.Ptr != tt.Ptr {
+ t.Errorf("PTR ptr should be %q, but is %q", tt.Ptr, x.Ptr)
+ return false
+ }
+ case *dns.CNAME:
+ tt := section[i].(*dns.CNAME)
+ if x.Target != tt.Target {
+ t.Errorf("CNAME target should be %q, but is %q", tt.Target, x.Target)
+ return false
+ }
+ case *dns.MX:
+ tt := section[i].(*dns.MX)
+ if x.Mx != tt.Mx {
+ t.Errorf("MX Mx should be %q, but is %q", tt.Mx, x.Mx)
+ return false
+ }
+ if x.Preference != tt.Preference {
+ t.Errorf("MX Preference should be %q, but is %q", tt.Preference, x.Preference)
+ return false
+ }
+ case *dns.NS:
+ tt := section[i].(*dns.NS)
+ if x.Ns != tt.Ns {
+ t.Errorf("NS nameserver should be %q, but is %q", tt.Ns, x.Ns)
+ return false
+ }
+ case *dns.OPT:
+ tt := section[i].(*dns.OPT)
+ if x.UDPSize() != tt.UDPSize() {
+ t.Errorf("OPT UDPSize should be %d, but is %d", tt.UDPSize(), x.UDPSize())
+ return false
+ }
+ if x.Do() != tt.Do() {
+ t.Errorf("OPT DO should be %t, but is %t", tt.Do(), x.Do())
+ return false
+ }
+ }
+ }
+ return true
+}
+
+// CNAMEOrder makes sure that CNAMES do not appear after their target records
+func CNAMEOrder(t *testing.T, res *dns.Msg) {
+ 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
+ }
+ t.Errorf("CNAME found after target record\n")
+ t.Logf("%v\n", res)
+
+ }
+ }
+}
+
+// SortAndCheck sorts resp and the checks the header and three sections against the testcase in tc.
+func SortAndCheck(t *testing.T, resp *dns.Msg, tc Case) {
+ sort.Sort(RRSet(resp.Answer))
+ sort.Sort(RRSet(resp.Ns))
+ sort.Sort(RRSet(resp.Extra))
+
+ if !Header(t, tc, resp) {
+ t.Logf("%v\n", resp)
+ return
+ }
+
+ if !Section(t, tc, Answer, resp.Answer) {
+ t.Logf("%v\n", resp)
+ return
+ }
+ if !Section(t, tc, Ns, resp.Ns) {
+ t.Logf("%v\n", resp)
+ return
+
+ }
+ if !Section(t, tc, Extra, resp.Extra) {
+ t.Logf("%v\n", resp)
+ return
+ }
+ return
+}
+
+// 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/plugin/test/responsewriter.go b/plugin/test/responsewriter.go
new file mode 100644
index 000000000..79eaa00f3
--- /dev/null
+++ b/plugin/test/responsewriter.go
@@ -0,0 +1,61 @@
+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{}
+
+// LocalAddr returns the local address, always 127.0.0.1:53 (UDP).
+func (t *ResponseWriter) LocalAddr() net.Addr {
+ ip := net.ParseIP("127.0.0.1")
+ port := 53
+ return &net.UDPAddr{IP: ip, Port: port, Zone: ""}
+}
+
+// RemoteAddr returns the remote address, always 10.240.0.1:40212 (UDP).
+func (t *ResponseWriter) RemoteAddr() net.Addr {
+ ip := net.ParseIP("10.240.0.1")
+ port := 40212
+ return &net.UDPAddr{IP: ip, Port: port, Zone: ""}
+}
+
+// WriteMsg implement dns.ResponseWriter interface.
+func (t *ResponseWriter) WriteMsg(m *dns.Msg) error { return nil }
+
+// Write implement dns.ResponseWriter interface.
+func (t *ResponseWriter) Write(buf []byte) (int, error) { return len(buf), nil }
+
+// Close implement dns.ResponseWriter interface.
+func (t *ResponseWriter) Close() error { return nil }
+
+// TsigStatus implement dns.ResponseWriter interface.
+func (t *ResponseWriter) TsigStatus() error { return nil }
+
+// TsigTimersOnly implement dns.ResponseWriter interface.
+func (t *ResponseWriter) TsigTimersOnly(bool) { return }
+
+// Hijack implement dns.ResponseWriter interface.
+func (t *ResponseWriter) Hijack() { return }
+
+// RepsponseWrite6 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).
+func (t *ResponseWriter6) LocalAddr() net.Addr {
+ return &net.UDPAddr{IP: net.ParseIP("::1"), Port: 53, Zone: ""}
+}
+
+// RemoteAddr returns the remote address, always fe80::42:ff:feca:4c65 port 40212 (UDP).
+func (t *ResponseWriter6) RemoteAddr() net.Addr {
+ return &net.UDPAddr{IP: net.ParseIP("fe80::42:ff:feca:4c65"), Port: 40212, Zone: ""}
+}
diff --git a/plugin/test/server.go b/plugin/test/server.go
new file mode 100644
index 000000000..eb39c7a5b
--- /dev/null
+++ b/plugin/test/server.go
@@ -0,0 +1,52 @@
+package test
+
+import (
+ "net"
+ "sync"
+ "time"
+
+ "github.com/miekg/dns"
+)
+
+// TCPServer starts a DNS server with a TCP listener on laddr.
+func TCPServer(laddr string) (*dns.Server, string, error) {
+ l, err := net.Listen("tcp", laddr)
+ if err != nil {
+ return nil, "", err
+ }
+
+ server := &dns.Server{Listener: l, ReadTimeout: time.Hour, WriteTimeout: time.Hour}
+
+ waitLock := sync.Mutex{}
+ waitLock.Lock()
+ server.NotifyStartedFunc = func() { waitLock.Unlock() }
+
+ go func() {
+ server.ActivateAndServe()
+ l.Close()
+ }()
+
+ waitLock.Lock()
+ return server, l.Addr().String(), nil
+}
+
+// UDPServer starts a DNS server with an UDP listener on laddr.
+func UDPServer(laddr string) (*dns.Server, string, error) {
+ pc, err := net.ListenPacket("udp", laddr)
+ if err != nil {
+ return nil, "", err
+ }
+ server := &dns.Server{PacketConn: pc, ReadTimeout: time.Hour, WriteTimeout: time.Hour}
+
+ waitLock := sync.Mutex{}
+ waitLock.Lock()
+ server.NotifyStartedFunc = func() { waitLock.Unlock() }
+
+ go func() {
+ server.ActivateAndServe()
+ pc.Close()
+ }()
+
+ waitLock.Lock()
+ return server, pc.LocalAddr().String(), nil
+}
diff --git a/plugin/tls/README.md b/plugin/tls/README.md
new file mode 100644
index 000000000..d2a56f793
--- /dev/null
+++ b/plugin/tls/README.md
@@ -0,0 +1,52 @@
+# tls
+
+*tls* allows you to configure the server certificates for the TLS and gRPC servers.
+For other types of servers it is ignored.
+
+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 *proxy* plugin also support gRPC (`protocol gRPC`), meaning you can chain CoreDNS servers
+using this protocol.
+
+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` directive 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
+~~~
+
+## 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
+ proxy . /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
+ proxy . /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.
+
+## Also See
+
+RFC 7858 and https://grpc.io.
diff --git a/plugin/tls/tls.go b/plugin/tls/tls.go
new file mode 100644
index 000000000..e0958a9aa
--- /dev/null
+++ b/plugin/tls/tls.go
@@ -0,0 +1,37 @@
+package tls
+
+import (
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/tls"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("tls", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(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) != 3 {
+ return plugin.Error("tls", c.ArgErr())
+ }
+ tls, err := tls.NewTLSConfig(args[0], args[1], args[2])
+ if err != nil {
+ return plugin.Error("tls", err)
+ }
+ config.TLSConfig = tls
+ }
+ return nil
+}
diff --git a/plugin/tls/tls_test.go b/plugin/tls/tls_test.go
new file mode 100644
index 000000000..2374d772c
--- /dev/null
+++ b/plugin/tls/tls_test.go
@@ -0,0 +1,44 @@
+package tls
+
+import (
+ "io/ioutil"
+ "log"
+ "strings"
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestTLS(t *testing.T) {
+ log.SetOutput(ioutil.Discard)
+
+ 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
+ // negative
+ }
+
+ 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)
+ }
+ }
+ }
+}
diff --git a/plugin/trace/README.md b/plugin/trace/README.md
new file mode 100644
index 000000000..62e6d463d
--- /dev/null
+++ b/plugin/trace/README.md
@@ -0,0 +1,73 @@
+# trace
+
+This module enables OpenTracing-based tracing of DNS requests as they go through the
+plugin chain.
+
+## Syntax
+
+The simplest form is just:
+
+~~~
+trace [ENDPOINT-TYPE] [ENDPOINT]
+~~~
+
+* **ENDPOINT-TYPE** is the type of tracing destination. Currently only `zipkin` is supported
+ and that is what it defaults to.
+* **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
+}
+~~~
+
+* `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.
+
+## Zipkin
+You can run Zipkin on a Docker host like this:
+
+```
+docker run -d -p 9411:9411 openzipkin/zipkin
+```
+
+## Examples
+
+Use an alternative Zipkin address:
+
+~~~
+trace tracinghost:9253
+~~~
+
+or
+
+~~~
+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
+~~~
+
+Trace one query every 10000 queries, rename the service, and enable same span:
+
+~~~
+trace tracinghost:9411 {
+ every 10000
+ service dnsproxy
+ client_server
+}
+~~~
diff --git a/plugin/trace/setup.go b/plugin/trace/setup.go
new file mode 100644
index 000000000..5c6e473c3
--- /dev/null
+++ b/plugin/trace/setup.go
@@ -0,0 +1,113 @@
+package trace
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("trace", caddy.Plugin{
+ ServerType: "dns",
+ Action: 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{Endpoint: defEP, EndpointType: defEpType, every: 1, serviceName: defServiceName}
+ err error
+ )
+
+ cfg := dnsserver.GetConfig(c)
+ tr.ServiceEndpoint = cfg.ListenHost + ":" + cfg.Port
+ for c.Next() { // trace
+ var err error
+ args := c.RemainingArgs()
+ switch len(args) {
+ case 0:
+ tr.Endpoint, err = normalizeEndpoint(tr.EndpointType, defEP)
+ case 1:
+ tr.Endpoint, err = normalizeEndpoint(defEpType, args[0])
+ case 2:
+ tr.EndpointType = strings.ToLower(args[0])
+ tr.Endpoint, err = normalizeEndpoint(tr.EndpointType, 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
+ }
+ }
+ }
+ }
+ return tr, err
+}
+
+func normalizeEndpoint(epType, ep string) (string, error) {
+ switch epType {
+ case "zipkin":
+ if !strings.Contains(ep, "http") {
+ ep = "http://" + ep + "/api/v1/spans"
+ }
+ return ep, nil
+ default:
+ return "", fmt.Errorf("tracing endpoint type '%s' is not supported", epType)
+ }
+}
+
+const (
+ defEP = "localhost:9411"
+ defEpType = "zipkin"
+ defServiceName = "coredns"
+)
diff --git a/plugin/trace/setup_test.go b/plugin/trace/setup_test.go
new file mode 100644
index 000000000..3c12b76e4
--- /dev/null
+++ b/plugin/trace/setup_test.go
@@ -0,0 +1,60 @@
+package trace
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestTraceParse(t *testing.T) {
+ tests := []struct {
+ input string
+ shouldErr bool
+ endpoint string
+ every uint64
+ serviceName string
+ clientServer bool
+ }{
+ // oks
+ {`trace`, false, "http://localhost:9411/api/v1/spans", 1, `coredns`, false},
+ {`trace localhost:1234`, false, "http://localhost:1234/api/v1/spans", 1, `coredns`, false},
+ {`trace http://localhost:1234/somewhere/else`, false, "http://localhost:1234/somewhere/else", 1, `coredns`, false},
+ {`trace zipkin localhost:1234`, false, "http://localhost:1234/api/v1/spans", 1, `coredns`, false},
+ {`trace zipkin http://localhost:1234/somewhere/else`, false, "http://localhost:1234/somewhere/else", 1, `coredns`, false},
+ {"trace {\n every 100\n}", false, "http://localhost:9411/api/v1/spans", 100, `coredns`, false},
+ {"trace {\n every 100\n service foobar\nclient_server\n}", false, "http://localhost:9411/api/v1/spans", 100, `foobar`, true},
+ {"trace {\n every 2\n client_server true\n}", false, "http://localhost:9411/api/v1/spans", 2, `coredns`, true},
+ {"trace {\n client_server false\n}", false, "http://localhost:9411/api/v1/spans", 1, `coredns`, false},
+ // fails
+ {`trace footype localhost:4321`, true, "", 1, "", false},
+ {"trace {\n every 2\n client_server junk\n}", true, "", 1, "", false},
+ }
+ 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 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)
+ }
+ }
+}
diff --git a/plugin/trace/trace.go b/plugin/trace/trace.go
new file mode 100644
index 000000000..fa561945e
--- /dev/null
+++ b/plugin/trace/trace.go
@@ -0,0 +1,84 @@
+// Package trace implements OpenTracing-based tracing
+package trace
+
+import (
+ "fmt"
+ "sync"
+ "sync/atomic"
+
+ "github.com/coredns/coredns/plugin"
+ // Plugin the trace package.
+ _ "github.com/coredns/coredns/plugin/pkg/trace"
+
+ "github.com/miekg/dns"
+ ot "github.com/opentracing/opentracing-go"
+ zipkin "github.com/openzipkin/zipkin-go-opentracing"
+ "golang.org/x/net/context"
+)
+
+type trace struct {
+ Next plugin.Handler
+ ServiceEndpoint string
+ Endpoint string
+ EndpointType string
+ tracer ot.Tracer
+ serviceName string
+ clientServer bool
+ every uint64
+ count uint64
+ Once sync.Once
+}
+
+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()
+ default:
+ err = fmt.Errorf("unknown endpoint type: %s", t.EndpointType)
+ }
+ })
+ return err
+}
+
+func (t *trace) setupZipkin() error {
+
+ collector, err := zipkin.NewHTTPCollector(t.Endpoint)
+ if err != nil {
+ return err
+ }
+
+ recorder := zipkin.NewRecorder(collector, false, t.ServiceEndpoint, t.serviceName)
+ t.tracer, err = zipkin.NewTracer(recorder, zipkin.ClientServerSameSpan(t.clientServer))
+
+ 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
+ }
+ }
+ if span := ot.SpanFromContext(ctx); span == nil && trace {
+ span := t.Tracer().StartSpan("servedns")
+ defer span.Finish()
+ ctx = ot.ContextWithSpan(ctx, span)
+ }
+ return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
+}
diff --git a/plugin/trace/trace_test.go b/plugin/trace/trace_test.go
new file mode 100644
index 000000000..b006009c3
--- /dev/null
+++ b/plugin/trace/trace_test.go
@@ -0,0 +1,33 @@
+package trace
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+// createTestTrace creates a trace plugin to be used in tests
+func createTestTrace(config string) (*caddy.Controller, *trace, error) {
+ c := caddy.NewTestController("dns", config)
+ m, err := traceParse(c)
+ return c, m, err
+}
+
+func TestTrace(t *testing.T) {
+ _, m, err := createTestTrace(`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.Tracer() == nil {
+ t.Errorf("Error, no tracer created")
+ }
+}
diff --git a/plugin/whoami/README.md b/plugin/whoami/README.md
new file mode 100644
index 000000000..d16a93766
--- /dev/null
+++ b/plugin/whoami/README.md
@@ -0,0 +1,44 @@
+# whoami
+
+*whoami* returns your resolver's local IP address, port and transport. Your IP address is returned
+ 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
+._<transport>.qname. 0 IN SRV 0 0 <port> .
+~~~
+
+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.
+
+The *whoami* plugin will respond to every A or AAAA query, regardless of the query name.
+
+## Syntax
+
+~~~ txt
+whoami
+~~~
+
+## Examples
+
+Start a server on the default port and load the *whoami* plugin.
+
+~~~ corefile
+. {
+ 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
+~~~
diff --git a/plugin/whoami/setup.go b/plugin/whoami/setup.go
new file mode 100644
index 000000000..9797c8bbf
--- /dev/null
+++ b/plugin/whoami/setup.go
@@ -0,0 +1,28 @@
+package whoami
+
+import (
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("whoami", caddy.Plugin{
+ ServerType: "dns",
+ Action: setupWhoami,
+ })
+}
+
+func setupWhoami(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/plugin/whoami/setup_test.go b/plugin/whoami/setup_test.go
new file mode 100644
index 000000000..73db67d88
--- /dev/null
+++ b/plugin/whoami/setup_test.go
@@ -0,0 +1,19 @@
+package whoami
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestSetupWhoami(t *testing.T) {
+ c := caddy.NewTestController("dns", `whoami`)
+ if err := setupWhoami(c); err != nil {
+ t.Fatalf("Expected no errors, but got: %v", err)
+ }
+
+ c = caddy.NewTestController("dns", `whoami example.org`)
+ if err := setupWhoami(c); err == nil {
+ t.Fatalf("Expected errors, but got: %v", err)
+ }
+}
diff --git a/plugin/whoami/whoami.go b/plugin/whoami/whoami.go
new file mode 100644
index 000000000..9d22c43a8
--- /dev/null
+++ b/plugin/whoami/whoami.go
@@ -0,0 +1,57 @@
+// Package whoami implements a plugin that returns details about the resolving
+// querying it.
+package whoami
+
+import (
+ "net"
+ "strconv"
+
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// 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.Compress = true
+ 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()}
+ port, _ := strconv.Atoi(state.Port())
+ srv.Port = uint16(port)
+ srv.Target = "."
+
+ a.Extra = []dns.RR{rr, srv}
+
+ state.SizeAndDo(a)
+ w.WriteMsg(a)
+
+ return 0, nil
+}
+
+// Name implements the Handler interface.
+func (wh Whoami) Name() string { return "whoami" }
diff --git a/plugin/whoami/whoami_test.go b/plugin/whoami/whoami_test.go
new file mode 100644
index 000000000..c8e57f80c
--- /dev/null
+++ b/plugin/whoami/whoami_test.go
@@ -0,0 +1,56 @@
+package whoami
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestWhoami(t *testing.T) {
+ wh := Whoami{}
+
+ tests := []struct {
+ qname string
+ qtype uint16
+ 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,
+ },
+ }
+
+ ctx := context.TODO()
+
+ for i, tc := range tests {
+ req := new(dns.Msg)
+ req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype)
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ 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)
+ }
+ }
+ }
+ }
+}