aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/setup/etcd.go108
-rw-r--r--middleware/etcd/etcd.go3
-rw-r--r--middleware/etcd/etcd.md16
-rw-r--r--middleware/etcd/handler.go16
-rw-r--r--middleware/etcd/stub.go124
-rw-r--r--middleware/etcd/stub_handler.go78
-rw-r--r--middleware/etcd/stub_test.go145
-rw-r--r--middleware/proxy/lookup.go9
8 files changed, 401 insertions, 98 deletions
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/<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.
-// 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