diff --git a/core/setup/etcd.go b/core/setup/etcd.go index 1423e71aa..9bb227844 100644 --- a/core/setup/etcd.go +++ b/core/setup/etcd.go @@ -21,10 +21,16 @@ const defaultEndpoint = "http://127.0.0.1:2379" // Etcd sets up the etcd middleware. func Etcd(c *Controller) (middleware.Middleware, error) { - etcd, err := etcdParse(c) + etcd, stubzones, err := etcdParse(c) if err != nil { return nil, err } + if stubzones { + c.Startup = append(c.Startup, func() error { + etcd.UpdateStubZones() + return nil + }) + } return func(next middleware.Handler) middleware.Handler { etcd.Next = next @@ -32,31 +38,113 @@ func Etcd(c *Controller) (middleware.Middleware, error) { }, nil } -func etcdParse(c *Controller) (etcd.Etcd, error) { +func etcdParse(c *Controller) (etcd.Etcd, bool, error) { + stub := make(map[string]proxy.Proxy) etc := etcd.Etcd{ - // make stuff configurable Proxy: proxy.New([]string{"8.8.8.8:53"}), PathPrefix: "skydns", Ctx: context.Background(), Inflight: &singleflight.Group{}, + Stubmap: &stub, } + var ( + client etcdc.KeysAPI + tlsCertFile = "" + tlsKeyFile = "" + tlsCAcertFile = "" + endpoints = []string{defaultEndpoint} + stubzones = false + ) for c.Next() { if c.Val() == "etcd" { - // etcd [origin...] - client, err := newEtcdClient([]string{defaultEndpoint}, "", "", "") - if err != nil { - return etcd.Etcd{}, err - } etc.Client = client etc.Zones = c.RemainingArgs() if len(etc.Zones) == 0 { etc.Zones = c.ServerBlockHosts } middleware.Zones(etc.Zones).FullyQualify() - return etc, nil + if c.NextBlock() { + // TODO(miek): 2 switches? + switch c.Val() { + case "stubzones": + stubzones = true + case "path": + if !c.NextArg() { + return etcd.Etcd{}, false, c.ArgErr() + } + etc.PathPrefix = c.Val() + case "endpoint": + args := c.RemainingArgs() + if len(args) == 0 { + return etcd.Etcd{}, false, c.ArgErr() + } + endpoints = args + case "upstream": + args := c.RemainingArgs() + if len(args) == 0 { + return etcd.Etcd{}, false, c.ArgErr() + } + for i := 0; i < len(args); i++ { + h, p, e := net.SplitHostPort(args[i]) + if e != nil && p == "" { + args[i] = h + ":53" + } + } + endpoints = args + etc.Proxy = proxy.New(args) + case "tls": // cert key cacertfile + args := c.RemainingArgs() + if len(args) != 3 { + return etcd.Etcd{}, false, c.ArgErr() + } + tlsCertFile, tlsKeyFile, tlsCAcertFile = args[0], args[1], args[2] + } + for c.Next() { + switch c.Val() { + case "stubzones": + stubzones = true + case "path": + if !c.NextArg() { + return etcd.Etcd{}, false, c.ArgErr() + } + etc.PathPrefix = c.Val() + case "endpoint": + args := c.RemainingArgs() + if len(args) == 0 { + return etcd.Etcd{}, false, c.ArgErr() + } + endpoints = args + case "upstream": + args := c.RemainingArgs() + if len(args) == 0 { + return etcd.Etcd{}, false, c.ArgErr() + } + for i := 0; i < len(args); i++ { + h, p, e := net.SplitHostPort(args[i]) + if e != nil && p == "" { + args[i] = h + ":53" + } + } + endpoints = args + etc.Proxy = proxy.New(args) + case "tls": // cert key cacertfile + args := c.RemainingArgs() + if len(args) != 3 { + return etcd.Etcd{}, false, c.ArgErr() + } + tlsCertFile, tlsKeyFile, tlsCAcertFile = args[0], args[1], args[2] + } + } + } + client, err := newEtcdClient(endpoints, tlsCertFile, tlsKeyFile, tlsCAcertFile) + if err != nil { + return etcd.Etcd{}, false, err + } + etc.Client = client + return etc, stubzones, nil } } - return etcd.Etcd{}, nil + return etcd.Etcd{}, false, nil } func newEtcdClient(endpoints []string, tlsCert, tlsKey, tlsCACert string) (etcdc.KeysAPI, error) { diff --git a/middleware/etcd/etcd.go b/middleware/etcd/etcd.go index 84b884bcb..4eb55f7ed 100644 --- a/middleware/etcd/etcd.go +++ b/middleware/etcd/etcd.go @@ -18,11 +18,12 @@ import ( type Etcd struct { Next middleware.Handler 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 - PathPrefix string + Stubmap *map[string]proxy.Proxy // List of proxies for stub resolving. } // Records looks up records in etcd. If exact is true, it will lookup just diff --git a/middleware/etcd/etcd.md b/middleware/etcd/etcd.md index ea1abc81f..e5c1d3997 100644 --- a/middleware/etcd/etcd.md +++ b/middleware/etcd/etcd.md @@ -13,7 +13,7 @@ other servers in the network. etcd [zones...] ~~~ -* `zones` zones it should be authoritative for. +* `zones` zones etcd should be authoritative for. The will default to `/skydns` as the path and the local etcd proxy (http://127.0.0.1:2379). If no zones are specified the block's zone will be used as the zone. @@ -21,15 +21,19 @@ 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` middleware. ~~~ -etcd { +etcd [zones...] { + stubzones path /skydns endpoint endpoint... - stubzones + upstream address... + tls cert key cacert } ~~~ -* `path` /skydns -* `endpoint` endpoints... -* `stubzones` +* `stubzones` enable the stub zones feature. +* `path` the path inside etcd, defaults to "/skydns". +* `endpoint` the etcd endpoints, default to "http://localhost:2397". +* `upstream` upstream resolvers to be used resolve external names found in etcd. +* `tls` followed the cert, key and the CA's cert filenames. ## Examples diff --git a/middleware/etcd/handler.go b/middleware/etcd/handler.go index a687d54ad..bd5df5e13 100644 --- a/middleware/etcd/handler.go +++ b/middleware/etcd/handler.go @@ -1,6 +1,8 @@ package etcd import ( + "strings" + "github.com/miekg/coredns/middleware" "github.com/miekg/dns" @@ -9,6 +11,20 @@ import ( func (e Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { state := middleware.State{W: w, Req: r} + + // 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. + name := state.Name() + if len(*e.Stubmap) > 0 { + for zone, _ := range *e.Stubmap { + if strings.HasSuffix(name, zone) { + stub := Stub{Etcd: e, Zone: zone} + return stub.ServeDNS(ctx, w, r) + } + } + } + zone := middleware.Zones(e.Zones).Matches(state.Name()) if zone == "" { return e.Next.ServeDNS(ctx, w, r) diff --git a/middleware/etcd/stub.go b/middleware/etcd/stub.go index 4bc877fb2..849bafa5b 100644 --- a/middleware/etcd/stub.go +++ b/middleware/etcd/stub.go @@ -4,104 +4,68 @@ import ( "net" "strconv" "strings" + "time" + + "github.com/miekg/coredns/middleware/proxy" "github.com/miekg/dns" ) -// 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 +func (e Etcd) UpdateStubZones() { + go func() { + for { + e.updateStubZones() + time.Sleep(15 * time.Second) } - } - 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{ednsStubCode, []byte{1}}) - } else { - m.Extra = append(m.Extra, ednsStub) - } - return m + }() } // Look in .../dns/stub//xx for msg.Services. Loop through them // extract and add them as forwarders (ip:port-combos) for // the stub zones. Only numeric (i.e. IP address) hosts are used. -// TODO(miek): makes this Startup Function. -func (e Etcd) UpdateStubZones(zone string) error { - stubmap := make(map[string][]string) - - services, err := e.Records("stub.dns."+zone, false) - if err != nil { - return err - } - for _, serv := range services { - if serv.Port == 0 { - serv.Port = 53 - } - ip := net.ParseIP(serv.Host) - if ip == nil { - //logf("stub zone non-address %s seen for: %s", serv.Key, serv.Host) +func (e Etcd) updateStubZones() { + stubmap := make(map[string]proxy.Proxy) + for _, zone := range e.Zones { + services, err := e.Records(stubDomain+"."+zone, false) + if err != nil { continue } - domain := e.Domain(serv.Key) - labels := dns.SplitDomainName(domain) + // track the nameservers on a per domain basis, but allow a list on the domain. + nameservers := map[string][]string{} - // 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. - domain = dns.Fqdn(strings.Join(labels[1:len(labels)-dns.CountLabel(z)], ".")) - if domain == z { + for _, serv := range services { + if serv.Port == 0 { + serv.Port = 53 + } + ip := net.ParseIP(serv.Host) + if ip == nil { continue } - stubmap[domain] = append(stubmap[domain], net.JoinHostPort(serv.Host, strconv.Itoa(serv.Port))) + + domain := e.Domain(serv.Key) + labels := dns.SplitDomainName(domain) + // nameserver need to be tracked by domain and *then* added + + // 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. + domain = dns.Fqdn(strings.Join(labels[1:len(labels)-dns.CountLabel(z)], ".")) + if domain == z { + continue + } + nameservers[domain] = append(nameservers[domain], net.JoinHostPort(serv.Host, strconv.Itoa(serv.Port))) + } + } + for domain, nss := range nameservers { + stubmap[domain] = proxy.New(nss) } } - // TODO(miek): add to etcd structure and startup with a StartFunction - // e.stub = &stubmap - // stubmap contains proxy is best way forward... I think. - // TODO(miek): setup a proxy that forward to these - // StubProxy type? - return nil -} - -func ServeDNSStubForward(w dns.ResponseWriter, req *dns.Msg) *dns.Msg { - if !hasStubEdns0(req) { - return nil + // atomic swap (at least that's what we hope it is) + if len(stubmap) > 0 { + e.Stubmap = &stubmap } - req = addStubEdns0(req) - // proxy woxy - return nil + return } - -// ednsStub is the EDNS0 record we add to stub queries. Queries which have this record are -// not forwarded again. -var ednsStub = func() *dns.OPT { - o := new(dns.OPT) - o.Hdr.Name = "." - o.Hdr.Rrtype = dns.TypeOPT - - e := new(dns.EDNS0_LOCAL) - e.Code = ednsStubCode - e.Data = []byte{1} - o.Option = append(o.Option, e) - return o -}() - -const ednsStubCode = dns.EDNS0LOCALSTART + 10 diff --git a/middleware/etcd/stub_handler.go b/middleware/etcd/stub_handler.go new file mode 100644 index 000000000..9e8facf15 --- /dev/null +++ b/middleware/etcd/stub_handler.go @@ -0,0 +1,78 @@ +package etcd + +import ( + "github.com/miekg/coredns/middleware" + "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) +} + +func (s Stub) ServeDNS(ctx context.Context, w dns.ResponseWriter, req *dns.Msg) (int, error) { + if hasStubEdns0(req) { + // TODO(miek): actual error here + return dns.RcodeServerFailure, nil + } + req = addStubEdns0(req) + proxy, ok := (*s.Etcd.Stubmap)[s.Zone] + if !ok { // somebody made a mistake.. + return dns.RcodeServerFailure, nil + } + state := middleware.State{W: w, Req: req} + + m1, e1 := proxy.Forward(state) + if e1 != nil { + return dns.RcodeServerFailure, e1 + } + m1.RecursionAvailable, m1.Compress = true, true + state.W.WriteMsg(m1) + 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{ednsStubCode, []byte{1}}) + } else { + 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 + + e := new(dns.EDNS0_LOCAL) + e.Code = ednsStubCode + e.Data = []byte{1} + o.Option = append(o.Option, e) + return o +}() diff --git a/middleware/etcd/stub_test.go b/middleware/etcd/stub_test.go new file mode 100644 index 000000000..789809a50 --- /dev/null +++ b/middleware/etcd/stub_test.go @@ -0,0 +1,145 @@ +// +build etcd + +package etcd + +import "testing" + +func TestStubLookup(t *testing.T) { + // e.updateStubZones() +} + +/* +func TestDNSStubForward(t *testing.T) { + s := newTestServer(t, false) + defer s.Stop() + + c := new(dns.Client) + m := new(dns.Msg) + + stubEx := &msg.Service{ + // IP address of a.iana-servers.net. + Host: "199.43.132.53", Key: "a.example.com.stub.dns.skydns.test.", + } + stubBroken := &msg.Service{ + Host: "127.0.0.1", Port: 5454, Key: "b.example.org.stub.dns.skydns.test.", + } + stubLoop := &msg.Service{ + Host: "127.0.0.1", Port: Port, Key: "b.example.net.stub.dns.skydns.test.", + } + addService(t, s, stubEx.Key, 0, stubEx) + defer delService(t, s, stubEx.Key) + addService(t, s, stubBroken.Key, 0, stubBroken) + defer delService(t, s, stubBroken.Key) + addService(t, s, stubLoop.Key, 0, stubLoop) + defer delService(t, s, stubLoop.Key) + + s.UpdateStubZones() + + m.SetQuestion("www.example.com.", dns.TypeA) + resp, _, err := c.Exchange(m, "127.0.0.1:"+StrPort) + if err != nil { + // try twice + resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) + if err != nil { + t.Fatal(err) + } + } + if len(resp.Answer) == 0 || resp.Rcode != dns.RcodeSuccess { + t.Fatal("answer expected to have A records or rcode not equal to RcodeSuccess") + } + // The main diff. here is that we expect the AA bit to be set, because we directly + // queried the authoritative servers. + if resp.Authoritative != true { + t.Fatal("answer expected to have AA bit set") + } + + // This should fail. + m.SetQuestion("www.example.org.", dns.TypeA) + resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) + if len(resp.Answer) != 0 || resp.Rcode != dns.RcodeServerFailure { + t.Fatal("answer expected to fail for example.org") + } + + // This should really fail with a timeout. + m.SetQuestion("www.example.net.", dns.TypeA) + resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) + if err == nil { + t.Fatal("answer expected to fail for example.net") + } else { + t.Logf("succesfully failing %s", err) + } + + // Packet with EDNS0 + m.SetEdns0(4096, true) + resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) + if err == nil { + t.Fatal("answer expected to fail for example.net") + } else { + t.Logf("succesfully failing %s", err) + } + + // Now start another SkyDNS instance on a different port, + // add a stubservice for it and check if the forwarding is + // actually working. + oldStrPort := StrPort + + s1 := newTestServer(t, false) + defer s1.Stop() + s1.config.Domain = "skydns.com." + + // Add forwarding IP for internal.skydns.com. Use Port to point to server s. + stubForward := &msg.Service{ + Host: "127.0.0.1", Port: Port, Key: "b.internal.skydns.com.stub.dns.skydns.test.", + } + addService(t, s, stubForward.Key, 0, stubForward) + defer delService(t, s, stubForward.Key) + s.UpdateStubZones() + + // Add an answer for this in our "new" server. + stubReply := &msg.Service{ + Host: "127.1.1.1", Key: "www.internal.skydns.com.", + } + addService(t, s1, stubReply.Key, 0, stubReply) + defer delService(t, s1, stubReply.Key) + + m = new(dns.Msg) + m.SetQuestion("www.internal.skydns.com.", dns.TypeA) + resp, _, err = c.Exchange(m, "127.0.0.1:"+oldStrPort) + if err != nil { + t.Fatalf("failed to forward %s", err) + } + if resp.Answer[0].(*dns.A).A.String() != "127.1.1.1" { + t.Fatalf("failed to get correct reply") + } + + // Adding an in baliwick internal domain forward. + s2 := newTestServer(t, false) + defer s2.Stop() + s2.config.Domain = "internal.skydns.net." + + // Add forwarding IP for internal.skydns.net. Use Port to point to server s. + stubForward1 := &msg.Service{ + Host: "127.0.0.1", Port: Port, Key: "b.internal.skydns.net.stub.dns.skydns.test.", + } + addService(t, s, stubForward1.Key, 0, stubForward1) + defer delService(t, s, stubForward1.Key) + s.UpdateStubZones() + + // Add an answer for this in our "new" server. + stubReply1 := &msg.Service{ + Host: "127.10.10.10", Key: "www.internal.skydns.net.", + } + addService(t, s2, stubReply1.Key, 0, stubReply1) + defer delService(t, s2, stubReply1.Key) + + m = new(dns.Msg) + m.SetQuestion("www.internal.skydns.net.", dns.TypeA) + resp, _, err = c.Exchange(m, "127.0.0.1:"+oldStrPort) + if err != nil { + t.Fatalf("failed to forward %s", err) + } + if resp.Answer[0].(*dns.A).A.String() != "127.10.10.10" { + t.Fatalf("failed to get correct reply") + } +} +*/ diff --git a/middleware/proxy/lookup.go b/middleware/proxy/lookup.go index 564b662c5..7aa4824e7 100644 --- a/middleware/proxy/lookup.go +++ b/middleware/proxy/lookup.go @@ -52,6 +52,9 @@ func New(hosts []string) Proxy { return p } +// Lookup will use name and tpe 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. +// Lookup is not suitable for forwarding request. So Forward for that. func (p Proxy) Lookup(state middleware.State, name string, tpe uint16) (*dns.Msg, error) { req := new(dns.Msg) req.SetQuestion(name, tpe) @@ -62,13 +65,17 @@ func (p Proxy) Lookup(state middleware.State, name string, tpe uint16) (*dns.Msg return p.lookup(state, req) } +func (p Proxy) Forward(state middleware.State) (*dns.Msg, error) { + return p.lookup(state, state.Req) +} + func (p Proxy) lookup(state middleware.State, r *dns.Msg) (*dns.Msg, error) { var ( reply *dns.Msg err error ) for _, upstream := range p.Upstreams { - // allowed bla bla bla TODO(miek): fix full proxy spec from caddy + // allowed bla bla bla TODO(miek): fix full proxy spec from caddy? start := time.Now() // Since Select() should give us "up" hosts, keep retrying