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:
parent
20e16491ec
commit
c961acbb6e
8 changed files with 207 additions and 21 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"})
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue