From 740178c83f1be226fa1faa62d0887857bc09bb22 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Mon, 21 Mar 2016 21:21:29 +0000 Subject: [PATCH] more stuff; insight: use proxy for upstream queries --- middleware/etcd/etcd.go | 3 + middleware/etcd/etcd.md | 3 - middleware/etcd/lookup.go | 130 +++++++++++++------------------------ middleware/exchange.go | 6 -- middleware/proxy/lookup.go | 105 ++++++++++++++++++++++++++++++ middleware/zone.go | 6 +- 6 files changed, 156 insertions(+), 97 deletions(-) create mode 100644 middleware/proxy/lookup.go diff --git a/middleware/etcd/etcd.go b/middleware/etcd/etcd.go index 3cc573e34..e1d5c3fe0 100644 --- a/middleware/etcd/etcd.go +++ b/middleware/etcd/etcd.go @@ -8,6 +8,7 @@ import ( "github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware/etcd/msg" "github.com/miekg/coredns/middleware/etcd/singleflight" + "github.com/miekg/coredns/middleware/proxy" etcdc "github.com/coreos/etcd/client" "golang.org/x/net/context" @@ -17,6 +18,7 @@ type ( Etcd struct { Next middleware.Handler Zones []string + Proxy proxy.Proxy client etcdc.KeysAPI ctx context.Context inflight *singleflight.Group @@ -28,6 +30,7 @@ func NewEtcd(client etcdc.KeysAPI, next middleware.Handler, zones []string) Etcd return Etcd{ Next: next, Zones: zones, + Proxy: proxy.New([]string{"8.8.8.8:53"}), client: client, ctx: context.Background(), inflight: &singleflight.Group{}, diff --git a/middleware/etcd/etcd.md b/middleware/etcd/etcd.md index e9147c1ff..937e65456 100644 --- a/middleware/etcd/etcd.md +++ b/middleware/etcd/etcd.md @@ -4,9 +4,6 @@ a [message](https://github.com/skynetservices/skydns/blob/2fcff74cdc9f9a7dd64189a447ef27ac354b725f/msg/service.go#L26) like [SkyDNS](https//github.com/skynetservices/skydns). -If you need replies to SOA and NS queries you should add a little zone after etcd directive that has -these resource records. - ## Syntax ~~~ diff --git a/middleware/etcd/lookup.go b/middleware/etcd/lookup.go index 800518d82..e7b684307 100644 --- a/middleware/etcd/lookup.go +++ b/middleware/etcd/lookup.go @@ -1,8 +1,18 @@ package etcd -/* -func (s *server) AddressRecords(q dns.Question, name string, previousRecords []dns.RR, state middleware.State) (records []dns.RR, err error) { - services, err := s.backend.Records(name, false) +import ( + "math" + "net" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/etcd/msg" + "github.com/miekg/dns" +) + +// need current zone argument. + +func (e Etcd) AddressRecords(zone string, state middleware.State, previousRecords []dns.RR) (records []dns.RR, err error) { + services, err := e.Records(state.Name(), false) if err != nil { return nil, err } @@ -14,24 +24,25 @@ func (s *server) AddressRecords(q dns.Question, name string, previousRecords []d switch { case ip == nil: // Try to resolve as CNAME if it's not an IP, but only if we don't create loops. - if q.Name == dns.Fqdn(serv.Host) { + // TODO(miek): lowercasing, use Match in middleware/ + if state.Name() == dns.Fqdn(serv.Host) { // x CNAME x is a direct loop, don't add those continue } - newRecord := serv.NewCNAME(q.Name, dns.Fqdn(serv.Host)) + newRecord := serv.NewCNAME(state.QName(), dns.Fqdn(serv.Host)) if len(previousRecords) > 7 { - logf("CNAME lookup limit of 8 exceeded for %s", newRecord) // don't add it, and just continue continue } - if s.isDuplicateCNAME(newRecord, previousRecords) { - logf("CNAME loop detected for record %s", newRecord) + if isDuplicateCNAME(newRecord, previousRecords) { continue } - nextRecords, err := s.AddressRecords(dns.Question{Name: dns.Fqdn(serv.Host), Qtype: q.Qtype, Qclass: q.Qclass}, - strings.ToLower(dns.Fqdn(serv.Host)), append(previousRecords, newRecord), state) + // Fucks up recursion, need to define this in the function + // are use another var + state.Req.Question[0] = dns.Question{Name: dns.Fqdn(serv.Host), Qtype: state.QType(), Qclass: state.QClass()} + nextRecords, err := e.AddressRecords(zone, state, append(previousRecords, newRecord)) if err == nil { // Only have we found something we should add the CNAME and the IP addresses. if len(nextRecords) > 0 { @@ -42,59 +53,31 @@ func (s *server) AddressRecords(q dns.Question, name string, previousRecords []d } // This means we can not complete the CNAME, try to look else where. target := newRecord.Target - if dns.IsSubDomain(s.config.Domain, target) { + if dns.IsSubDomain(zone, target) { // We should already have found it continue } - m1, e1 := s.Lookup(target, q.Qtype, bufsize, dnssec) + m1, e1 := e.Proxy.Lookup(state, target, state.QType()) if e1 != nil { - logf("incomplete CNAME chain: %s", e1) continue } // Len(m1.Answer) > 0 here is well? records = append(records, newRecord) records = append(records, m1.Answer...) continue - case ip.To4() != nil && (q.Qtype == dns.TypeA || both): - records = append(records, serv.NewA(q.Name, ip.To4())) - case ip.To4() == nil && (q.Qtype == dns.TypeAAAA || both): - records = append(records, serv.NewAAAA(q.Name, ip.To16())) + case ip.To4() != nil && (state.QType() == dns.TypeA): + records = append(records, serv.NewA(state.QName(), ip.To4())) + case ip.To4() == nil && (state.QType() == dns.TypeAAAA): + records = append(records, serv.NewAAAA(state.QName(), ip.To16())) } } return records, nil } -// NSRecords returns NS records from etcd. -func (s *server) NSRecords(q dns.Question, state middleware.State) (records []dns.RR, extra []dns.RR, err error) { - services, err := s.backend.Records(name, false) - if err != nil { - return nil, nil, err - } - - services = msg.Group(services) - - for _, serv := range services { - ip := net.ParseIP(serv.Host) - switch { - case ip == nil: - return nil, nil, fmt.Errorf("NS record must be an IP address") - case ip.To4() != nil: - serv.Host = msg.Domain(serv.Key) - records = append(records, serv.NewNS(q.Name, serv.Host)) - extra = append(extra, serv.NewA(serv.Host, ip.To4())) - case ip.To4() == nil: - serv.Host = msg.Domain(serv.Key) - records = append(records, serv.NewNS(q.Name, serv.Host)) - extra = append(extra, serv.NewAAAA(serv.Host, ip.To16())) - } - } - return records, extra, nil -} - // SRVRecords returns SRV records from etcd. -// If the Target is not a name but an IP address, a name is created. -func (s *server) SRVRecords(s middleware.State) (records []dns.RR, extra []dns.RR, err error) { - services, err := s.backend.Records(name, false) +// If the Target is not a name but an IP address, a name is created on the fly. +func (e Etcd) SRVRecords(zone string, state middleware.State) (records []dns.RR, extra []dns.RR, err error) { + services, err := e.Records(name, false) if err != nil { return nil, nil, err } @@ -126,7 +109,7 @@ func (s *server) SRVRecords(s middleware.State) (records []dns.RR, extra []dns.R ip := net.ParseIP(serv.Host) switch { case ip == nil: - srv := serv.NewSRV(q.Name, weight) + srv := serv.NewSRV(state.QName(), weight) records = append(records, srv) if _, ok := lookup[srv.Target]; ok { @@ -135,12 +118,12 @@ func (s *server) SRVRecords(s middleware.State) (records []dns.RR, extra []dns.R lookup[srv.Target] = true - if !dns.IsSubDomain(s.config.Domain, srv.Target) { - m1, e1 := s.Lookup(srv.Target, dns.TypeA, bufsize, dnssec) + if !dns.IsSubDomain(zone, srv.Target) { + m1, e1 := e.Proxy.Lookup(state, srv.Target, dns.TypeA) if e1 == nil { extra = append(extra, m1.Answer...) } - m1, e1 = s.Lookup(srv.Target, dns.TypeAAAA, bufsize, dnssec) + m1, e1 = e.Proxy.Lookup(state, srv.Target, dns.TypeAAAA) if e1 == nil { // If we have seen CNAME's we *assume* that they are already added. for _, a := range m1.Answer { @@ -154,7 +137,7 @@ func (s *server) SRVRecords(s middleware.State) (records []dns.RR, extra []dns.R // Internal name, we should have some info on them, either v4 or v6 // Clients expect a complete answer, because we are a recursor in their // view. - addr, e1 := s.AddressRecords(dns.Question{srv.Target, dns.ClassINET, dns.TypeA}, + addr, e1 := e.AddressRecords(dns.Question{srv.Target, dns.ClassINET, dns.TypeA}, srv.Target, nil, bufsize, dnssec, true) if e1 == nil { extra = append(extra, addr...) @@ -177,9 +160,9 @@ func (s *server) SRVRecords(s middleware.State) (records []dns.RR, extra []dns.R } // MXRecords returns MX records from etcd. -// If the Target is not a name but an IP address, a name is created. -func (s *server) MXRecords(q dns.Question, name string, s middleware.State) (records []dns.RR, extra []dns.RR, err error) { - services, err := s.backend.Records(name, false) +// If the Target is not a name but an IP address, a name is created on the fly. +func (e Etcd) MXRecords(zone string, state middleware.State) (records []dns.RR, extra []dns.RR, err error) { + services, err := e.Records(name, false) if err != nil { return nil, nil, err } @@ -201,11 +184,11 @@ func (s *server) MXRecords(q dns.Question, name string, s middleware.State) (rec lookup[mx.Mx] = true if !dns.IsSubDomain(s.config.Domain, mx.Mx) { - m1, e1 := s.Lookup(mx.Mx, dns.TypeA, bufsize, dnssec) + m1, e1 := e.Proxy.Lookup(state, mx.Mx, dns.TypeA) if e1 == nil { extra = append(extra, m1.Answer...) } - m1, e1 = s.Lookup(mx.Mx, dns.TypeAAAA, bufsize, dnssec) + m1, e1 = e.Proxy.Lookup(state, mx.Mx, dns.TypeAAAA) if e1 == nil { // If we have seen CNAME's we *assume* that they are already added. for _, a := range m1.Answer { @@ -235,8 +218,8 @@ func (s *server) MXRecords(q dns.Question, name string, s middleware.State) (rec return records, extra, nil } -func (s *server) CNAMERecords(q dns.Question, state middleware.State) (records []dns.RR, err error) { - services, err := s.backend.Records(name, true) +func (e Etcd) CNAMERecords(zone string, state middleware.State) (records []dns.RR, err error) { + services, err := e.Records(name, true) if err != nil { return nil, err } @@ -252,8 +235,8 @@ func (s *server) CNAMERecords(q dns.Question, state middleware.State) (records [ return records, nil } -func (s *server) TXTRecords(q dns.Question, state middleware.State) (records []dns.RR, err error) { - services, err := s.backend.Records(name, false) +func (e Etcd) TXTRecords(zone string, state middleware.State) (records []dns.RR, err error) { + services, err := e.Records(state.Name(), false) if err != nil { return nil, err } @@ -280,6 +263,7 @@ func isDuplicateCNAME(r *dns.CNAME, records []dns.RR) bool { return false } +/* // Move to state.go somehow? func (s *server) NameError(req *dns.Msg) *dns.Msg { m := new(dns.Msg) @@ -289,30 +273,6 @@ func (s *server) NameError(req *dns.Msg) *dns.Msg { return m } -// overflowOrTruncated writes back an error to the client if the message does not fit. -// It updates prometheus metrics. If something has been written to the client, true -// will be returned. -func (s *server) overflowOrTruncated(w dns.ResponseWriter, m *dns.Msg, bufsize int, sy metrics.System) bool { - switch isTCP(w) { - case true: - if _, overflow := Fit(m, dns.MaxMsgSize, true); overflow { - metrics.ReportErrorCount(m, sy) - msgFail := s.ServerFailure(m) - w.WriteMsg(msgFail) - return true - } - case false: - // Overflow with udp always results in TC. - Fit(m, bufsize, false) - metrics.ReportErrorCount(m, sy) - if m.Truncated { - w.WriteMsg(m) - return true - } - } - return false -} - // etcNameError return a NameError to the client if the error // returned from etcd has ErrorCode == 100. func isEtcdNameError(err error, s *server) bool { diff --git a/middleware/exchange.go b/middleware/exchange.go index f2e74ab77..783d06e26 100644 --- a/middleware/exchange.go +++ b/middleware/exchange.go @@ -2,13 +2,7 @@ package middleware import "github.com/miekg/dns" -// Exchang sends message m to the server. -// TODO(miek): optionally it can do retries of other silly stuff. func Exchange(c *dns.Client, m *dns.Msg, server string) (*dns.Msg, error) { r, _, err := c.Exchange(m, server) return r, err } - -// Lookup functions, ala -// LookupHost -// LookupCNAME diff --git a/middleware/proxy/lookup.go b/middleware/proxy/lookup.go new file mode 100644 index 000000000..475b068ca --- /dev/null +++ b/middleware/proxy/lookup.go @@ -0,0 +1,105 @@ +package proxy + +// function OTHER middleware might want to use to do lookup in the same +// style as the proxy. + +import ( + "net/http" + "sync/atomic" + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/dns" +) + +func New(hosts []string) Proxy { + p := Proxy{Next: nil, Client: Clients()} + + upstream := &staticUpstream{ + from: "", + proxyHeaders: make(http.Header), + Hosts: make([]*UpstreamHost, len(hosts)), + Policy: &Random{}, + FailTimeout: 10 * time.Second, + MaxFails: 1, + } + + for i, host := range hosts { + uh := &UpstreamHost{ + Name: host, + Conns: 0, + Fails: 0, + FailTimeout: upstream.FailTimeout, + Unhealthy: false, + ExtraHeaders: upstream.proxyHeaders, + CheckDown: func(upstream *staticUpstream) UpstreamHostDownFunc { + return func(uh *UpstreamHost) bool { + if uh.Unhealthy { + return true + } + if uh.Fails >= upstream.MaxFails && + upstream.MaxFails != 0 { + return true + } + return false + } + }(upstream), + WithoutPathPrefix: upstream.WithoutPathPrefix, + } + upstream.Hosts[i] = uh + } + p.Upstreams = []Upstream{upstream} + return p, nil +} + +func (p Proxy) Lookup(state middleware.State, name string, tpe uint16) (*dns.Msg, error) { + req := new(dns.Msg) + req.SetQuestion(name, tpe) + // TODO(miek): + // USE STATE FOR DNSSEC ETCD BUFSIZE BLA BLA + return p.lookup(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 + start := time.Now() + + // Since Select() should give us "up" hosts, keep retrying + // hosts until timeout (or until we get a nil host). + for time.Now().Sub(start) < tryDuration { + host := upstream.Select() + if host == nil { + return nil, errUnreachable + } + + atomic.AddInt64(&host.Conns, 1) + // tls+tcp ? + if state.Proto() == "tcp" { + reply, err = middleware.Exchange(p.Client.TCP, r, host.Name) + } else { + reply, err = middleware.Exchange(p.Client.UDP, r, host.Name) + } + atomic.AddInt64(&host.Conns, -1) + + if err == nil { + return reply, nil + } + timeout := host.FailTimeout + if timeout == 0 { + timeout = 10 * time.Second + } + atomic.AddInt32(&host.Fails, 1) + go func(host *UpstreamHost, timeout time.Duration) { + time.Sleep(timeout) + atomic.AddInt32(&host.Fails, -1) + }(host, timeout) + } + return nil, errUnreachable + } + return nil, errUnreachable +} diff --git a/middleware/zone.go b/middleware/zone.go index 6798bca8e..aa1171c28 100644 --- a/middleware/zone.go +++ b/middleware/zone.go @@ -4,12 +4,12 @@ import "strings" type Zones []string -// Matches checks to see if other matches p. -// The match will return the most specific zones -// that matches other. The empty string signals a not found +// Matches checks to see if other matches p. The match will return the most +// specific zones that matches other. The empty string signals a not found // condition. func (z Zones) Matches(qname string) string { zone := "" + // TODO(miek): use IsSubDomain here? for _, zname := range z { if strings.HasSuffix(qname, zname) { if len(zname) > len(zone) {