Support SkyDNS' stubzones

This implements stubzones in the same way as SkyDNS. This
also works with multiple configured domains and has tests.
Also add more configuration parameters for TLS and path prefix and
enabling stubzones.  Run StubUpdates as a startup command to keep up to
date with the list in etcd.
This commit is contained in:
Miek Gieben 2016-03-25 20:26:42 +00:00
parent a832ab696a
commit ebef64280a
8 changed files with 403 additions and 98 deletions

View file

@ -21,10 +21,16 @@ const defaultEndpoint = "http://127.0.0.1:2379"
// Etcd sets up the etcd middleware. // Etcd sets up the etcd middleware.
func Etcd(c *Controller) (middleware.Middleware, error) { func Etcd(c *Controller) (middleware.Middleware, error) {
etcd, err := etcdParse(c) etcd, stubzones, err := etcdParse(c)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if stubzones {
c.Startup = append(c.Startup, func() error {
etcd.UpdateStubZones()
return nil
})
}
return func(next middleware.Handler) middleware.Handler { return func(next middleware.Handler) middleware.Handler {
etcd.Next = next etcd.Next = next
@ -32,31 +38,113 @@ func Etcd(c *Controller) (middleware.Middleware, error) {
}, nil }, 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{ etc := etcd.Etcd{
// make stuff configurable
Proxy: proxy.New([]string{"8.8.8.8:53"}), Proxy: proxy.New([]string{"8.8.8.8:53"}),
PathPrefix: "skydns", PathPrefix: "skydns",
Ctx: context.Background(), Ctx: context.Background(),
Inflight: &singleflight.Group{}, Inflight: &singleflight.Group{},
Stubmap: &stub,
} }
var (
client etcdc.KeysAPI
tlsCertFile = ""
tlsKeyFile = ""
tlsCAcertFile = ""
endpoints = []string{defaultEndpoint}
stubzones = false
)
for c.Next() { for c.Next() {
if c.Val() == "etcd" { if c.Val() == "etcd" {
// etcd [origin...]
client, err := newEtcdClient([]string{defaultEndpoint}, "", "", "")
if err != nil {
return etcd.Etcd{}, err
}
etc.Client = client etc.Client = client
etc.Zones = c.RemainingArgs() etc.Zones = c.RemainingArgs()
if len(etc.Zones) == 0 { if len(etc.Zones) == 0 {
etc.Zones = c.ServerBlockHosts etc.Zones = c.ServerBlockHosts
} }
middleware.Zones(etc.Zones).FullyQualify() 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"
} }
} }
return etcd.Etcd{}, nil 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{}, false, nil
} }
func newEtcdClient(endpoints []string, tlsCert, tlsKey, tlsCACert string) (etcdc.KeysAPI, error) { func newEtcdClient(endpoints []string, tlsCert, tlsKey, tlsCACert string) (etcdc.KeysAPI, error) {

View file

@ -18,11 +18,12 @@ import (
type Etcd struct { type Etcd struct {
Next middleware.Handler Next middleware.Handler
Zones []string Zones []string
PathPrefix string
Proxy proxy.Proxy // Proxy for looking up names during the resolution process Proxy proxy.Proxy // Proxy for looking up names during the resolution process
Client etcdc.KeysAPI Client etcdc.KeysAPI
Ctx context.Context Ctx context.Context
Inflight *singleflight.Group 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 // Records looks up records in etcd. If exact is true, it will lookup just

View file

@ -13,7 +13,7 @@ other servers in the network.
etcd [zones...] 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). 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. If no zones are specified the block's zone will be used as the zone.
@ -21,15 +21,21 @@ 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. If you want to `round robin` A and AAAA responses look at the `loadbalance` middleware.
~~~ ~~~
etcd { etcd [zones...] {
stubzones
path /skydns path /skydns
endpoint endpoint... endpoint endpoint...
stubzones upstream address...
tls cert key cacert
} }
~~~ ~~~
* `path` /skydns * `stubzones` enable the stub zones feature.
* `endpoint` endpoints... * `path` the path inside etcd, defaults to "/skydns".
* `stubzones` * `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
TODO: TLS params!
## Examples ## Examples

View file

@ -1,6 +1,8 @@
package etcd package etcd
import ( import (
"strings"
"github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware"
"github.com/miekg/dns" "github.com/miekg/dns"
@ -9,6 +11,20 @@ import (
func (e Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { func (e Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := middleware.State{W: w, Req: r} 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()) zone := middleware.Zones(e.Zones).Matches(state.Name())
if zone == "" { if zone == "" {
return e.Next.ServeDNS(ctx, w, r) return e.Next.ServeDNS(ctx, w, r)

View file

@ -4,62 +4,48 @@ import (
"net" "net"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/miekg/coredns/middleware/proxy"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// hasStubEdns0 checks if the message is carrying our special func (e Etcd) UpdateStubZones() {
// edns0 zero option. go func() {
func hasStubEdns0(m *dns.Msg) bool { for {
option := m.IsEdns0() e.updateStubZones()
if option == nil { time.Sleep(15 * time.Second)
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
} }
// Look in .../dns/stub/<zone>/xx for msg.Services. Loop through them // Look in .../dns/stub/<zone>/xx for msg.Services. Loop through them
// extract <zone> and add them as forwarders (ip:port-combos) for // extract <zone> and add them as forwarders (ip:port-combos) for
// the stub zones. Only numeric (i.e. IP address) hosts are used. // the stub zones. Only numeric (i.e. IP address) hosts are used.
// TODO(miek): makes this Startup Function. func (e Etcd) updateStubZones() {
func (e Etcd) UpdateStubZones(zone string) error { stubmap := make(map[string]proxy.Proxy)
stubmap := make(map[string][]string) for _, zone := range e.Zones {
services, err := e.Records(stubDomain+"."+zone, false)
services, err := e.Records("stub.dns."+zone, false)
if err != nil { if err != nil {
return err continue
} }
// track the nameservers on a per domain basis, but allow a list on the domain.
nameservers := map[string][]string{}
for _, serv := range services { for _, serv := range services {
if serv.Port == 0 { if serv.Port == 0 {
serv.Port = 53 serv.Port = 53
} }
ip := net.ParseIP(serv.Host) ip := net.ParseIP(serv.Host)
if ip == nil { if ip == nil {
//logf("stub zone non-address %s seen for: %s", serv.Key, serv.Host)
continue continue
} }
domain := e.Domain(serv.Key) domain := e.Domain(serv.Key)
labels := dns.SplitDomainName(domain) 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. // If the remaining name equals any of the zones we have, we ignore it.
for _, z := range e.Zones { for _, z := range e.Zones {
@ -69,39 +55,17 @@ func (e Etcd) UpdateStubZones(zone string) error {
if domain == z { if domain == z {
continue continue
} }
stubmap[domain] = append(stubmap[domain], net.JoinHostPort(serv.Host, strconv.Itoa(serv.Port))) 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 // atomic swap (at least that's what we hope it is)
// e.stub = &stubmap if len(stubmap) > 0 {
// stubmap contains proxy is best way forward... I think. e.Stubmap = &stubmap
// TODO(miek): setup a proxy that forward to these
// StubProxy type?
return nil
} }
return
func ServeDNSStubForward(w dns.ResponseWriter, req *dns.Msg) *dns.Msg {
if !hasStubEdns0(req) {
return nil
} }
req = addStubEdns0(req)
// proxy woxy
return nil
}
// 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

View file

@ -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
}()

View file

@ -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")
}
}
*/

View file

@ -52,6 +52,9 @@ func New(hosts []string) Proxy {
return p 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) { func (p Proxy) Lookup(state middleware.State, name string, tpe uint16) (*dns.Msg, error) {
req := new(dns.Msg) req := new(dns.Msg)
req.SetQuestion(name, tpe) 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) 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) { func (p Proxy) lookup(state middleware.State, r *dns.Msg) (*dns.Msg, error) {
var ( var (
reply *dns.Msg reply *dns.Msg
err error err error
) )
for _, upstream := range p.Upstreams { 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() start := time.Now()
// Since Select() should give us "up" hosts, keep retrying // Since Select() should give us "up" hosts, keep retrying