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
|
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 func(next middleware.Handler) middleware.Handler {
|
||||||
return file.File{Next: next, Zones: zones}
|
return file.File{Next: next, Zones: zones}
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -58,7 +68,9 @@ func fileParse(c *Controller) (file.Zones, error) {
|
||||||
}
|
}
|
||||||
// discard from, here, maybe check and show log when we do?
|
// discard from, here, maybe check and show log when we do?
|
||||||
for _, origin := range origins {
|
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
|
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 {
|
for _, n := range zones.Names {
|
||||||
if len(zones.Z[n].TransferFrom) > 0 {
|
if len(zones.Z[n].TransferFrom) > 0 {
|
||||||
c.Startup = append(c.Startup, func() error {
|
c.Startup = append(c.Startup, func() error {
|
||||||
err := zones.Z[n].TransferIn()
|
err := zones.Z[n].TransferIn()
|
||||||
return err
|
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
|
return file.Zones{}, e
|
||||||
}
|
}
|
||||||
for _, origin := range origins {
|
for _, origin := range origins {
|
||||||
z[origin].TransferTo = append(z[origin].TransferTo, t)
|
if t != "" {
|
||||||
z[origin].TransferFrom = append(z[origin].TransferFrom, f)
|
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 {
|
if state.QClass() != dns.ClassINET {
|
||||||
return dns.RcodeServerFailure, fmt.Errorf("file: can only deal with ClassINET")
|
return dns.RcodeServerFailure, fmt.Errorf("file: can only deal with ClassINET")
|
||||||
}
|
}
|
||||||
|
|
||||||
qname := state.Name()
|
qname := state.Name()
|
||||||
zone := middleware.Zones(f.Zones.Names).Matches(qname)
|
zone := middleware.Zones(f.Zones.Names).Matches(qname)
|
||||||
if zone == "" {
|
if zone == "" {
|
||||||
|
@ -41,7 +40,26 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i
|
||||||
if z == nil {
|
if z == nil {
|
||||||
return dns.RcodeServerFailure, 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 {
|
if z.Expired != nil && *z.Expired {
|
||||||
|
log.Printf("[ERROR] Zone %s is expired", zone)
|
||||||
return dns.RcodeServerFailure, nil
|
return dns.RcodeServerFailure, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +99,7 @@ func Parse(f io.Reader, origin, fileName string) (*Zone, error) {
|
||||||
z := NewZone(origin)
|
z := NewZone(origin)
|
||||||
for x := range tokens {
|
for x := range tokens {
|
||||||
if x.Error != nil {
|
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
|
return nil, x.Error
|
||||||
}
|
}
|
||||||
if x.RR.Header().Rrtype == dns.TypeSOA {
|
if x.RR.Header().Rrtype == dns.TypeSOA {
|
||||||
|
|
|
@ -9,7 +9,26 @@ import (
|
||||||
"github.com/miekg/dns"
|
"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() {
|
func (z *Zone) Notify() {
|
||||||
go notify(z.name, z.TransferTo)
|
go notify(z.name, z.TransferTo)
|
||||||
}
|
}
|
||||||
|
@ -23,6 +42,10 @@ func notify(zone string, to []string) error {
|
||||||
c := new(dns.Client)
|
c := new(dns.Client)
|
||||||
|
|
||||||
for _, t := range to {
|
for _, t := range to {
|
||||||
|
// TODO(miek): these ACLs thingies not to be formalized.
|
||||||
|
if t == "*" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err := notifyAddr(c, m, t); err != nil {
|
if err := notifyAddr(c, m, t); err != nil {
|
||||||
log.Printf("[ERROR] " + err.Error())
|
log.Printf("[ERROR] " + err.Error())
|
||||||
} else {
|
} else {
|
||||||
|
@ -35,7 +58,10 @@ func notify(zone string, to []string) error {
|
||||||
func notifyAddr(c *dns.Client, m *dns.Msg, s string) error {
|
func notifyAddr(c *dns.Client, m *dns.Msg, s string) error {
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
ret, err := middleware.Exchange(c, m, s)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@ package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/coredns/middleware"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
@ -11,16 +14,18 @@ func (z *Zone) TransferIn() error {
|
||||||
if len(z.TransferFrom) == 0 {
|
if len(z.TransferFrom) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
t := new(dns.Transfer)
|
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetAxfr(z.name)
|
m.SetAxfr(z.name)
|
||||||
|
|
||||||
|
z1 := z.Copy()
|
||||||
var Err error
|
var Err error
|
||||||
|
|
||||||
Transfer:
|
Transfer:
|
||||||
for _, tr := range z.TransferFrom {
|
for _, tr := range z.TransferFrom {
|
||||||
|
t := new(dns.Transfer)
|
||||||
c, err := t.In(m, tr)
|
c, err := t.In(m, tr)
|
||||||
if err != nil {
|
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
|
Err = err
|
||||||
continue Transfer
|
continue Transfer
|
||||||
}
|
}
|
||||||
|
@ -32,21 +37,127 @@ Transfer:
|
||||||
}
|
}
|
||||||
for _, rr := range env.RR {
|
for _, rr := range env.RR {
|
||||||
if rr.Header().Rrtype == dns.TypeSOA {
|
if rr.Header().Rrtype == dns.TypeSOA {
|
||||||
z.SOA = rr.(*dns.SOA)
|
z1.SOA = rr.(*dns.SOA)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if rr.Header().Rrtype == dns.TypeRRSIG {
|
if rr.Header().Rrtype == dns.TypeRRSIG {
|
||||||
if x, ok := rr.(*dns.RRSIG); ok && x.TypeCovered == dns.TypeSOA {
|
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 {
|
if Err != nil {
|
||||||
log.Printf("[ERROR] Failed to transfer %s", z.name)
|
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 nil
|
||||||
return Err // ignore errors for now. TODO(miek)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,15 @@ func NewZone(name string) *Zone {
|
||||||
return z
|
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.
|
// Insert inserts r into z.
|
||||||
func (z *Zone) Insert(r dns.RR) { z.Tree.Insert(r) }
|
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)
|
status, err := m.Next.ServeDNS(ctx, rw, r)
|
||||||
|
|
||||||
requestCount.WithLabelValues(zone, qtype).Inc()
|
requestCount.WithLabelValues(zone, qtype).Inc()
|
||||||
requestDuration.WithLabelValues(zone).Observe(float64(time.Since(rw.Start()) / time.Second))
|
requestDuration.WithLabelValues(zone, qtype).Observe(float64(time.Since(rw.Start()) / time.Second))
|
||||||
responseSize.WithLabelValues(zone).Observe(float64(rw.Size()))
|
responseSize.WithLabelValues(zone, qtype).Observe(float64(rw.Size()))
|
||||||
responseRcode.WithLabelValues(zone, strconv.Itoa(rw.Rcode())).Inc()
|
responseRcode.WithLabelValues(zone, strconv.Itoa(rw.Rcode()), qtype).Inc()
|
||||||
|
|
||||||
return status, err
|
return status, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ func define(subsystem string) {
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Subsystem: subsystem,
|
Subsystem: subsystem,
|
||||||
Name: "request_count_total",
|
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"})
|
}, []string{"zone", "qtype"})
|
||||||
|
|
||||||
requestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
requestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
|
@ -61,7 +61,7 @@ func define(subsystem string) {
|
||||||
Subsystem: subsystem,
|
Subsystem: subsystem,
|
||||||
Name: "request_duration_seconds",
|
Name: "request_duration_seconds",
|
||||||
Help: "Histogram of the time (in seconds) each request took.",
|
Help: "Histogram of the time (in seconds) each request took.",
|
||||||
}, []string{"zone"})
|
}, []string{"zone", "qtype"})
|
||||||
|
|
||||||
responseSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
responseSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
|
@ -69,12 +69,12 @@ func define(subsystem string) {
|
||||||
Name: "response_size_bytes",
|
Name: "response_size_bytes",
|
||||||
Help: "Size of the returns response in 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},
|
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{
|
responseRcode = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Subsystem: subsystem,
|
Subsystem: subsystem,
|
||||||
Name: "response_rcode_count_total",
|
Name: "response_rcode_count_total",
|
||||||
Help: "Counter of response status codes.",
|
Help: "Counter of response status codes.",
|
||||||
}, []string{"zone", "rcode"})
|
}, []string{"zone", "rcode", "qtype"})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue