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.
This commit is contained in:
Miek Gieben 2016-04-05 10:53:23 +01:00
parent 20e16491ec
commit c961acbb6e
8 changed files with 207 additions and 21 deletions

View file

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

View file

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

View file

@ -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 {

View file

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

View file

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

View file

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

View file

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

View file

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