aboutsummaryrefslogtreecommitdiff
path: root/plugin/etcd
diff options
context:
space:
mode:
Diffstat (limited to 'plugin/etcd')
-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
19 files changed, 1945 insertions, 0 deletions
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.
+ },
+}