diff --git a/core/directives.go b/core/directives.go index 14f52ced8..c6b7a4b4a 100644 --- a/core/directives.go +++ b/core/directives.go @@ -59,6 +59,7 @@ var directiveOrder = []directive{ {"errors", setup.Errors}, {"file", setup.File}, + {"secondary", setup.Secondary}, {"etcd", setup.Etcd}, {"proxy", setup.Proxy}, } diff --git a/core/setup/file.go b/core/setup/file.go index aca9f8f9f..dc0d61e18 100644 --- a/core/setup/file.go +++ b/core/setup/file.go @@ -36,9 +36,9 @@ func fileParse(c *Controller) (file.Zones, error) { if c.NextArg() { origin = c.Val() } - // normalize this origin - origin = middleware.Host(origin).Standard() + origin = middleware.Host(origin).Normalize() + // TODO(miek): we should allow more. Issue #54. reader, err := os.Open(fileName) if err != nil { return file.Zones{}, err @@ -48,27 +48,40 @@ func fileParse(c *Controller) (file.Zones, error) { z[origin] = zone } names = append(names, origin) - if c.NextBlock() { - what := c.Val() - if !c.NextArg() { - return file.Zones{}, c.ArgErr() - } - value := c.Val() - var err error - switch what { - case "transfer": - if value == "out" { - z[origin].Transfer.Out = true - } - if value == "in" { - z[origin].Transfer.In = true - } - } - if err != nil { - return file.Zones{}, err + + for c.NextBlock() { + t, _, e := parseTransfer(c) + if e != nil { + return file.Zones{}, e } + // discard from, here, maybe check and show log when we do? + z[origin].TransferTo = append(z[origin].TransferTo, t) } } } return file.Zones{Z: z, Names: names}, nil } + +// transfer to [address] +func parseTransfer(c *Controller) (to, from string, err error) { + what := c.Val() + if !c.NextArg() { + return "", "", c.ArgErr() + } + value := c.Val() + switch what { + case "transfer": + if !c.NextArg() { + return "", "", c.ArgErr() + } + if value == "to" { + to = c.Val() + to = middleware.Addr(to).Normalize() + } + if value == "from" { + from = c.Val() + from = middleware.Addr(from).Normalize() + } + } + return +} diff --git a/core/setup/secondary.go b/core/setup/secondary.go new file mode 100644 index 000000000..3282f92b1 --- /dev/null +++ b/core/setup/secondary.go @@ -0,0 +1,59 @@ +package setup + +import ( + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/file" + "github.com/miekg/coredns/middleware/secondary" +) + +// Secondary sets up the secondary middleware. +func Secondary(c *Controller) (middleware.Middleware, error) { + zones, err := secondaryParse(c) + if err != nil { + return nil, err + } + + // Setup retrieve the zone. + 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 + }) + } + } + + return func(next middleware.Handler) middleware.Handler { + return secondary.Secondary{file.File{Next: next, Zones: zones}} + }, nil + +} + +func secondaryParse(c *Controller) (file.Zones, error) { + z := make(map[string]*file.Zone) + names := []string{} + for c.Next() { + if c.Val() == "secondary" { + // secondary [origin] + origin := c.ServerBlockHosts[c.ServerBlockHostIndex] + if c.NextArg() { + origin = c.Val() + } + // TODO(miek): we should allow more. Issue #54. + origin = middleware.Host(origin).Normalize() + + z[origin] = file.NewZone(origin) + names = append(names, origin) + + for c.NextBlock() { + t, f, e := parseTransfer(c) + if e != nil { + return file.Zones{}, e + } + z[origin].TransferTo = append(z[origin].TransferTo, t) + z[origin].TransferFrom = append(z[origin].TransferFrom, f) + } + } + } + return file.Zones{Z: z, Names: names}, nil +} diff --git a/middleware/file/README.md b/middleware/file/README.md index 407c1119c..36b76a1aa 100644 --- a/middleware/file/README.md +++ b/middleware/file/README.md @@ -1,9 +1,9 @@ # file -`file` enabled reading zone data from a RFC-1035 styled file. +`file` enables serving zone data from a RFC-1035 styled file. -The etcd middleware makes extensive use of the proxy middleware to forward and query -other servers in the network. +The file middleware is used for "old-style" DNS server. It serves from a preloaded file that exists +on disk. ## Syntax @@ -17,15 +17,26 @@ file dbfile [zones...] If you want to round robin A and AAAA responses look at the `loadbalance` middleware. -TSIG key configuration is TODO; directive format will change. +TSIG key configuration is TODO; directive format for transfer will probably be extended with +TSIG key information, something like `transfer out [address] key [name] [base64]` ~~~ file dbfile [zones... ] { transfer out [address...] + transfer to [address] } ~~~ -* `transfer` enable zone transfers, for now only `transfer out` does something. It enables outgoing - zone transfers when defined. +* `transfer` enables zone transfers. It may be specified multiples times. *To* or *from* signals + the direction. Address must be denoted in CIDR notation (127.0.0.1/32 etc.). The special + wildcard "*" means: the entire internet. ## Examples + +Load the `miek.nl` zone from `miek.nl.signed` and allow transfers to the internet. + +~~~ +file miek.nl.signed miek.nl { + transfer to * +} +~~~ diff --git a/middleware/file/file.go b/middleware/file/file.go index c005b41cc..caf0c5fbc 100644 --- a/middleware/file/file.go +++ b/middleware/file/file.go @@ -33,6 +33,12 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i if !ok { return f.Next.ServeDNS(ctx, w, r) } + if z == nil { + return dns.RcodeServerFailure, nil + } + if z.Expired != nil && *z.Expired { + return dns.RcodeServerFailure, nil + } if state.Proto() != "udp" && state.QType() == dns.TypeAXFR { xfr := Xfr{z} diff --git a/middleware/file/lookup_test.go b/middleware/file/lookup_test.go index df65d0150..11ec5bab0 100644 --- a/middleware/file/lookup_test.go +++ b/middleware/file/lookup_test.go @@ -107,6 +107,15 @@ func TestLookup(t *testing.T) { } } +func TestLookupNil(t *testing.T) { + fm := File{Next: coretest.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: nil}, Names: []string{testzone}}} + ctx := context.TODO() + + m := dnsTestCases[0].Msg() + rec := middleware.NewResponseRecorder(&middleware.TestResponseWriter{}) + fm.ServeDNS(ctx, rec, m) +} + func BenchmarkLookup(b *testing.B) { zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin") if err != nil { diff --git a/middleware/file/notify.go b/middleware/file/notify.go index cc8f493e4..bbdafe022 100644 --- a/middleware/file/notify.go +++ b/middleware/file/notify.go @@ -10,31 +10,31 @@ import ( // Notify will send notifies to all configured IP addresses. func (z *Zone) Notify() { - go notify(z.name, z.Peers) + go notify(z.name, z.TransferTo) } -// notify sends notifies to the configured remotes. It will try up to three times -// before giving up on a specific remote. We will sequentially loop through the remotes +// notify sends notifies to the configured remote servers. It will try up to three times +// before giving up on a specific remote. We will sequentially loop through "to" // until they all have replied (or have 3 failed attempts). -func notify(zone string, remotes []string) error { +func notify(zone string, to []string) error { m := new(dns.Msg) m.SetNotify(zone) c := new(dns.Client) // TODO(miek): error handling? Run this in a goroutine? - for _, remote := range remotes { - notifyRemote(c, m, middleware.Addr(remote).Standard()) + for _, t := range to { + notifyAddr(c, m, t) } return nil } -func notifyRemote(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++ { ret, err := middleware.Exchange(c, m, s) if err == nil && ret.Rcode == dns.RcodeSuccess || ret.Rcode == dns.RcodeNotImplemented { return nil } - // timeout? mean don't want it. should stop sending as well + // timeout? mean don't want it. should stop sending as well? } return fmt.Errorf("failed to send notify for zone '%s' to '%s'", m.Question[0].Name, s) } diff --git a/middleware/file/secondary.go b/middleware/file/secondary.go new file mode 100644 index 000000000..95c063a9b --- /dev/null +++ b/middleware/file/secondary.go @@ -0,0 +1,63 @@ +package file + +import ( + "log" + + "github.com/miekg/dns" +) + +// TransferIn retrieves the zone from the masters, parses it and sets it live. +func (z *Zone) TransferIn() error { + if len(z.TransferFrom) == 0 { + return nil + } + t := new(dns.Transfer) + m := new(dns.Msg) + m.SetAxfr(z.name) + /* + t.TsigSecret = map[string]string{"axfr.": "so6ZGir4GPAqINNh9U5c3A=="} + m.SetTsig("axfr.", dns.HmacMD5, 300, time.Now().Unix()) + */ + + var Err error +Transfer: + for _, tr := range z.TransferFrom { + 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) + Err = err + continue Transfer + } + for env := range c { + if env.Error != nil { + log.Printf("[ERROR] failed to parse transfer %s: %v", z.name, env.Error) + Err = env.Error + continue Transfer + } + for _, rr := range env.RR { + if rr.Header().Rrtype == dns.TypeSOA { + z.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) + } + } + z.Insert(rr) + } + } + } + return Err +} + +/* + + 28800 ; refresh (8 hours) + 7200 ; retry (2 hours) + 604800 ; expire (1 week) + 3600 ; minimum (1 hour) +// Check SOA +// Just check every refresh hours, if fail set to retry until succeeds +// expire is need: to give SERVFAIL. +*/ diff --git a/middleware/file/xfr.go b/middleware/file/xfr.go index 297de2fc5..5ef205f6e 100644 --- a/middleware/file/xfr.go +++ b/middleware/file/xfr.go @@ -22,7 +22,7 @@ func (x Xfr) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (in return dns.RcodeServerFailure, nil } if state.QType() != dns.TypeAXFR { - return 0, fmt.Errorf("file: xfr called with non xfr type: %d", state.QType()) + return 0, fmt.Errorf("file: xfr called with non transfer type: %d", state.QType()) } if state.Proto() == "udp" { return 0, fmt.Errorf("file: xfr called with udp") @@ -39,7 +39,7 @@ func (x Xfr) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (in go tr.Out(w, r, ch) j, l := 0, 0 - records = append(records, records[0]) + records = append(records, records[0]) // add closing SOA to the end for i, r := range records { l += dns.Len(r) if l > transferLength { diff --git a/middleware/file/zone.go b/middleware/file/zone.go index 8c56b6bf9..c3567b45f 100644 --- a/middleware/file/zone.go +++ b/middleware/file/zone.go @@ -7,23 +7,22 @@ import ( "github.com/miekg/dns" ) -type Transfer struct { - Out bool - In bool -} - type Zone struct { SOA *dns.SOA SIG []dns.RR name string *tree.Tree - Peers []string - Transfer *Transfer + + TransferTo []string + TransferFrom []string + Expired *bool } // NewZone returns a new zone. func NewZone(name string) *Zone { - return &Zone{name: dns.Fqdn(name), Tree: &tree.Tree{}, Transfer: &Transfer{}} + z := &Zone{name: dns.Fqdn(name), Tree: &tree.Tree{}, Expired: new(bool)} + *z.Expired = false + return z } // Insert inserts r into z. @@ -32,12 +31,14 @@ func (z *Zone) Insert(r dns.RR) { z.Tree.Insert(r) } // Delete deletes r from z. func (z *Zone) Delete(r dns.RR) { z.Tree.Delete(r) } -// It the transfer request allowed. +// TransferAllowed checks if incoming request for transferring the zone is allowed according to the ACLs. func (z *Zone) TransferAllowed(state middleware.State) bool { - if z.Transfer == nil { - return false + for _, t := range z.TransferTo { + if t == "*" { + return true + } } - return z.Transfer.Out + return false } // All returns all records from the zone, the first record will be the SOA record, @@ -54,5 +55,3 @@ func (z *Zone) All() []dns.RR { } return append([]dns.RR{z.SOA}, records...) } - -// Apex function? diff --git a/middleware/secondary/README.md b/middleware/secondary/README.md new file mode 100644 index 000000000..c2b1366e7 --- /dev/null +++ b/middleware/secondary/README.md @@ -0,0 +1,34 @@ +# secondary + +`secondary` enables serving a zone retrieved from a primary server. + +## Syntax + +~~~ +secondary [zones...] +~~~ + +* `zones` zones it should be authoritative for. If empty, the zones from the configuration block + are used. Not that with an remote address to *get* the zone the above is not that useful. + +A working syntax would be: + +~~~ +secondary [zones...] { + transfer from address + [transfer to address] +} +~~~ + +* `transfer from` tell from which address to fetch the zone. It can be specified multiple time, + if one does not work another will be tried. +* `transfer to` can be enabled to allow this secondary zone to be transfered again. + +## Examples + +~~~ +secondary [zones...] { + transfer from 10.0.1.1 + transfer from 10.1.2.1 +} +~~~ diff --git a/middleware/secondary/secondary.go b/middleware/secondary/secondary.go new file mode 100644 index 000000000..60006b69c --- /dev/null +++ b/middleware/secondary/secondary.go @@ -0,0 +1,7 @@ +package secondary + +import "github.com/miekg/coredns/middleware/file" + +type Secondary struct { + file.File +}