From c961acbb6e9e06279d3dca077ba47d8a6170da20 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Tue, 5 Apr 2016 10:53:23 +0100 Subject: [PATCH] Add complete secondary support Respond to notifies and allow a secondary to follow the SOA parameters to update a zone from a primary. Also sprinkle it with logging. Also extend monitoring to include qtype in more metrics. --- core/setup/file.go | 14 +++- core/setup/secondary.go | 16 +++- middleware/file/file.go | 22 +++++- middleware/file/notify.go | 30 +++++++- middleware/file/secondary.go | 123 +++++++++++++++++++++++++++++-- middleware/file/zone.go | 9 +++ middleware/prometheus/handler.go | 6 +- middleware/prometheus/metrics.go | 8 +- 8 files changed, 207 insertions(+), 21 deletions(-) diff --git a/core/setup/file.go b/core/setup/file.go index f501cf518..edcddd30c 100644 --- a/core/setup/file.go +++ b/core/setup/file.go @@ -14,6 +14,16 @@ func File(c *Controller) (middleware.Middleware, error) { return nil, err } + // Add startup functions to notify the master. + for _, n := range zones.Names { + if len(zones.Z[n].TransferTo) > 0 { + c.Startup = append(c.Startup, func() error { + zones.Z[n].Notify() + return err + }) + } + } + return func(next middleware.Handler) middleware.Handler { return file.File{Next: next, Zones: zones} }, nil @@ -58,7 +68,9 @@ func fileParse(c *Controller) (file.Zones, error) { } // discard from, here, maybe check and show log when we do? for _, origin := range origins { - z[origin].TransferTo = append(z[origin].TransferTo, t) + if t != "" { + z[origin].TransferTo = append(z[origin].TransferTo, t) + } } } } diff --git a/core/setup/secondary.go b/core/setup/secondary.go index c7b5cc8ce..2f620c43e 100644 --- a/core/setup/secondary.go +++ b/core/setup/secondary.go @@ -13,13 +13,19 @@ func Secondary(c *Controller) (middleware.Middleware, error) { return nil, err } - // Setup retrieve the zone. + // Add startup functions to retrieve the zone and keep it up to date. for _, n := range zones.Names { if len(zones.Z[n].TransferFrom) > 0 { c.Startup = append(c.Startup, func() error { err := zones.Z[n].TransferIn() return err }) + c.Startup = append(c.Startup, func() error { + go func() { + zones.Z[n].Update() + }() + return nil + }) } } @@ -52,8 +58,12 @@ func secondaryParse(c *Controller) (file.Zones, error) { return file.Zones{}, e } for _, origin := range origins { - z[origin].TransferTo = append(z[origin].TransferTo, t) - z[origin].TransferFrom = append(z[origin].TransferFrom, f) + if t != "" { + z[origin].TransferTo = append(z[origin].TransferTo, t) + } + if f != "" { + z[origin].TransferFrom = append(z[origin].TransferFrom, f) + } } } } diff --git a/middleware/file/file.go b/middleware/file/file.go index a647f9d12..473b55574 100644 --- a/middleware/file/file.go +++ b/middleware/file/file.go @@ -28,7 +28,6 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i if state.QClass() != dns.ClassINET { return dns.RcodeServerFailure, fmt.Errorf("file: can only deal with ClassINET") } - qname := state.Name() zone := middleware.Zones(f.Zones.Names).Matches(qname) if zone == "" { @@ -41,7 +40,26 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i if z == nil { return dns.RcodeServerFailure, nil } + if r.Opcode == dns.OpcodeNotify { + if z.isNotify(state) { + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true + w.WriteMsg(m) + + if ok, _ := z.shouldTransfer(); ok { + log.Printf("[INFO] Valid notify from %s for %s: initiating transfer", state.IP(), zone) + z.TransferIn() + } + + return dns.RcodeSuccess, nil + } + log.Printf("[INFO] Dropping notify from %s for %s", state.IP(), zone) + return dns.RcodeSuccess, nil + } + if z.Expired != nil && *z.Expired { + log.Printf("[ERROR] Zone %s is expired", zone) return dns.RcodeServerFailure, nil } @@ -81,7 +99,7 @@ func Parse(f io.Reader, origin, fileName string) (*Zone, error) { z := NewZone(origin) for x := range tokens { if x.Error != nil { - log.Printf("[ERROR] failed to parse %s: %v", origin, x.Error) + log.Printf("[ERROR] Failed to parse %s: %v", origin, x.Error) return nil, x.Error } if x.RR.Header().Rrtype == dns.TypeSOA { diff --git a/middleware/file/notify.go b/middleware/file/notify.go index 6667fb05d..a88ca9192 100644 --- a/middleware/file/notify.go +++ b/middleware/file/notify.go @@ -9,7 +9,26 @@ import ( "github.com/miekg/dns" ) -// Notify will send notifies to all configured IP addresses. +// isNotify checks if state is a notify message and if so, will *also* check if it +// is from one of the configured masters. If not it will not be a valid notify +// message. If the zone z is not a secondary zone the message will also be ignored. +func (z *Zone) isNotify(state middleware.State) bool { + if state.Req.Opcode != dns.OpcodeNotify { + return false + } + if len(z.TransferFrom) == 0 { + return false + } + remote := middleware.Addr(state.IP()).Normalize() + for _, from := range z.TransferFrom { + if from == remote { + return true + } + } + return false +} + +// Notify will send notifies to all configured TransferTo IP addresses. func (z *Zone) Notify() { go notify(z.name, z.TransferTo) } @@ -23,6 +42,10 @@ func notify(zone string, to []string) error { c := new(dns.Client) for _, t := range to { + // TODO(miek): these ACLs thingies not to be formalized. + if t == "*" { + continue + } if err := notifyAddr(c, m, t); err != nil { log.Printf("[ERROR] " + err.Error()) } else { @@ -35,7 +58,10 @@ func notify(zone string, to []string) error { func notifyAddr(c *dns.Client, m *dns.Msg, s string) error { for i := 0; i < 3; i++ { ret, err := middleware.Exchange(c, m, s) - if err == nil && ret.Rcode == dns.RcodeSuccess || ret.Rcode == dns.RcodeNotImplemented { + if err != nil { + continue + } + if ret.Rcode == dns.RcodeSuccess || ret.Rcode == dns.RcodeNotImplemented { return nil } } diff --git a/middleware/file/secondary.go b/middleware/file/secondary.go index 53e66cebe..4c39e805c 100644 --- a/middleware/file/secondary.go +++ b/middleware/file/secondary.go @@ -2,6 +2,9 @@ package file import ( "log" + "time" + + "github.com/miekg/coredns/middleware" "github.com/miekg/dns" ) @@ -11,16 +14,18 @@ func (z *Zone) TransferIn() error { if len(z.TransferFrom) == 0 { return nil } - t := new(dns.Transfer) m := new(dns.Msg) m.SetAxfr(z.name) + z1 := z.Copy() var Err error + Transfer: for _, tr := range z.TransferFrom { + t := new(dns.Transfer) c, err := t.In(m, tr) if err != nil { - log.Printf("[ERROR] Failed to setup transfer %s with %s: %v", z.name, z.TransferFrom[0], err) + log.Printf("[ERROR] Failed to setup transfer %s with %s: %v", z.name, tr, err) Err = err continue Transfer } @@ -32,21 +37,127 @@ Transfer: } for _, rr := range env.RR { if rr.Header().Rrtype == dns.TypeSOA { - z.SOA = rr.(*dns.SOA) + z1.SOA = rr.(*dns.SOA) continue } if rr.Header().Rrtype == dns.TypeRRSIG { if x, ok := rr.(*dns.RRSIG); ok && x.TypeCovered == dns.TypeSOA { - z.SIG = append(z.SIG, x) + z1.SIG = append(z1.SIG, x) } } - z.Insert(rr) + z1.Insert(rr) } } + Err = nil + break } if Err != nil { log.Printf("[ERROR] Failed to transfer %s", z.name) + return nil } + + z.Tree = z1.Tree + *z.Expired = false + log.Printf("[INFO] Transfered: %s", z.name) + return nil +} + +// shouldTransfer checks the primaries of zone, retrieves the SOA record, checks the current serial +// and the remote serial and will return true if the remote one is higher than the locally configured one. +func (z *Zone) shouldTransfer() (bool, error) { + c := new(dns.Client) + c.Net = "tcp" // do this query over TCP to minimize spoofing + m := new(dns.Msg) + m.SetQuestion(z.name, dns.TypeSOA) + + var Err error + serial := -1 + + for _, tr := range z.TransferFrom { + Err = nil + ret, err := middleware.Exchange(c, m, tr) + if err != nil || ret.Rcode != dns.RcodeSuccess { + Err = err + continue + } + for _, a := range ret.Answer { + if a.Header().Rrtype == dns.TypeSOA { + serial = int(a.(*dns.SOA).Serial) + } + } + } + if serial == -1 { + return false, Err + } + return less(z.SOA.Serial, uint32(serial)), Err +} + +// less return true of a is smaller than b when taking RFC 1982 serial arithmetic into account. +func less(a, b uint32) bool { + // TODO(miek): implement! + return a < b +} + +// Update updates the secondary zone according to its SOA. It will run for the life time of the server +// and uses the SOA parameters. Every refresh it will check for a new SOA number. If that fails (for all +// server) it wil retry every retry interval. If the zone failed to transfer before the expire, the zone +// will be marked expired. +func (z *Zone) Update() error { + // TODO(miek): if SOA changes we need to redo this with possible different timer values. + // TODO(miek): yeah... + for z.SOA == nil { + time.Sleep(1 * time.Second) + } + + refresh := time.Second * time.Duration(z.SOA.Refresh) + retry := time.Second * time.Duration(z.SOA.Retry) + expire := time.Second * time.Duration(z.SOA.Expire) + retryActive := false + + // TODO(miek): check max as well? + if refresh < time.Hour { + refresh = time.Hour + } + if retry < time.Hour { + retry = time.Hour + } + + refreshTicker := time.NewTicker(refresh) + retryTicker := time.NewTicker(retry) + expireTicker := time.NewTicker(expire) + + for { + select { + case <-expireTicker.C: + if !retryActive { + break + } + // TODO(miek): should actually keep track of last succesfull transfer + *z.Expired = true + + case <-retryTicker.C: + if !retryActive { + break + } + ok, err := z.shouldTransfer() + if err != nil && ok { + log.Printf("[INFO] Refreshing zone: %s: initiating transfer", z.name) + z.TransferIn() + retryActive = false + } + + case <-refreshTicker.C: + ok, err := z.shouldTransfer() + retryActive = err != nil + if err != nil && ok { + log.Printf("[INFO] Refreshing zone: %s: initiating transfer", z.name) + z.TransferIn() + } + } + } + + refreshTicker.Stop() + retryTicker.Stop() + expireTicker.Stop() return nil - return Err // ignore errors for now. TODO(miek) } diff --git a/middleware/file/zone.go b/middleware/file/zone.go index 3d291ba33..57d0cd4a0 100644 --- a/middleware/file/zone.go +++ b/middleware/file/zone.go @@ -25,6 +25,15 @@ func NewZone(name string) *Zone { return z } +// Copy copies a zone *without* copying the zone's content. It is not a deep copy. +func (z *Zone) Copy() *Zone { + z1 := NewZone(z.name) + z1.TransferTo = z.TransferTo + z1.TransferFrom = z.TransferFrom + z1.Expired = z.Expired + return z1 +} + // Insert inserts r into z. func (z *Zone) Insert(r dns.RR) { z.Tree.Insert(r) } diff --git a/middleware/prometheus/handler.go b/middleware/prometheus/handler.go index a0cfcc872..3c07cd942 100644 --- a/middleware/prometheus/handler.go +++ b/middleware/prometheus/handler.go @@ -25,9 +25,9 @@ func (m *Metrics) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg status, err := m.Next.ServeDNS(ctx, rw, r) requestCount.WithLabelValues(zone, qtype).Inc() - requestDuration.WithLabelValues(zone).Observe(float64(time.Since(rw.Start()) / time.Second)) - responseSize.WithLabelValues(zone).Observe(float64(rw.Size())) - responseRcode.WithLabelValues(zone, strconv.Itoa(rw.Rcode())).Inc() + requestDuration.WithLabelValues(zone, qtype).Observe(float64(time.Since(rw.Start()) / time.Second)) + responseSize.WithLabelValues(zone, qtype).Observe(float64(rw.Size())) + responseRcode.WithLabelValues(zone, strconv.Itoa(rw.Rcode()), qtype).Inc() return status, err } diff --git a/middleware/prometheus/metrics.go b/middleware/prometheus/metrics.go index d6c4249e0..f030a6fed 100644 --- a/middleware/prometheus/metrics.go +++ b/middleware/prometheus/metrics.go @@ -53,7 +53,7 @@ func define(subsystem string) { Namespace: namespace, Subsystem: subsystem, Name: "request_count_total", - Help: "Counter of DNS requests made per zone and type.", + Help: "Counter of DNS requests made per zone and type and opcode.", }, []string{"zone", "qtype"}) requestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ @@ -61,7 +61,7 @@ func define(subsystem string) { Subsystem: subsystem, Name: "request_duration_seconds", Help: "Histogram of the time (in seconds) each request took.", - }, []string{"zone"}) + }, []string{"zone", "qtype"}) responseSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: namespace, @@ -69,12 +69,12 @@ func define(subsystem string) { Name: "response_size_bytes", Help: "Size of the returns response in bytes.", Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3}, - }, []string{"zone"}) + }, []string{"zone", "qtype"}) responseRcode = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: namespace, Subsystem: subsystem, Name: "response_rcode_count_total", Help: "Counter of response status codes.", - }, []string{"zone", "rcode"}) + }, []string{"zone", "rcode", "qtype"}) }