From e56d206542c901a48b28c4501fe5805e9e9e1a10 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Mon, 28 Mar 2016 12:08:05 +0100 Subject: [PATCH] Support outgoing zone transfers These can be enabled by adding "transfer out" to the Corefile. Without it no AXFR is allowed. For now only AXFR and no IXFR. No TSIG and no ACLs. --- core/setup/file.go | 22 +++++++++++-- middleware/file/file.go | 11 +++---- middleware/file/file.md | 21 +++---------- middleware/file/notify.go | 8 +++++ middleware/file/tree/all.go | 21 +++++++++++++ middleware/file/tree/tree.go | 2 ++ middleware/file/xfr.go | 61 ++++++++++++++++++++++++++++++++++++ middleware/file/xfr_test.go | 34 ++++++++++++++++++++ middleware/file/zone.go | 38 ++++++++++++++++++++-- middleware/recorder.go | 4 ++- 10 files changed, 195 insertions(+), 27 deletions(-) create mode 100644 middleware/file/notify.go create mode 100644 middleware/file/tree/all.go create mode 100644 middleware/file/xfr.go create mode 100644 middleware/file/xfr_test.go diff --git a/core/setup/file.go b/core/setup/file.go index 858b784c2..7128b77aa 100644 --- a/core/setup/file.go +++ b/core/setup/file.go @@ -32,7 +32,6 @@ func fileParse(c *Controller) (file.Zones, error) { origin := c.ServerBlockHosts[c.ServerBlockHostIndex] if c.NextArg() { - c.Next() origin = c.Val() } // normalize this origin @@ -42,12 +41,31 @@ func fileParse(c *Controller) (file.Zones, error) { if err != nil { return file.Zones{}, err } - zone, err := file.Parse(reader, origin, fileName) if err == nil { 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 + } + } } } return file.Zones{Z: z, Names: names}, nil diff --git a/middleware/file/file.go b/middleware/file/file.go index 5e4e0b357..237515a57 100644 --- a/middleware/file/file.go +++ b/middleware/file/file.go @@ -1,10 +1,5 @@ package file -// TODO(miek): the zone's implementation is basically non-existent -// we return a list and when searching for an answer we iterate -// over the list. This must be moved to a tree-like structure and -// have some fluff for DNSSEC (and be memory efficient). - import ( "io" "log" @@ -19,7 +14,6 @@ type ( File struct { Next middleware.Handler Zones Zones - // Maybe a list of all zones as well, as a []string? } Zones struct { @@ -40,6 +34,11 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i return f.Next.ServeDNS(ctx, w, r) } + if state.Proto() != "udp" && state.QType() == dns.TypeAXFR { + xfr := Xfr{z} + return xfr.ServeDNS(ctx, w, r) + } + rrs, extra, result := z.Lookup(qname, state.QType(), state.Do()) m := new(dns.Msg) diff --git a/middleware/file/file.md b/middleware/file/file.md index 2e23b0332..bd9a24d1e 100644 --- a/middleware/file/file.md +++ b/middleware/file/file.md @@ -15,26 +15,15 @@ file dbfile [zones...] * `zones` zones it should be authoritative for. If empty the zones from the configuration block are used. -If you want to `round robin` A and AAAA responses look at the `loadbalance` middleware. +If you want to round robin A and AAAA responses look at the `loadbalance` middleware. ~~~ -file { - db - masters [...masters...] +file dbfile [zones... ] { + transfer in|out } ~~~ - - - - -* `path` /skydns -* `endpoint` endpoints... -* `stubzones` +* `transfer` enable zone transfers, for now only `transfer out` does something. It enables outgoing + zone transfers when defined. ## Examples - -dnssec { - file blaat, transparant allow already signed responses - ksk bliep.dsdsk -} diff --git a/middleware/file/notify.go b/middleware/file/notify.go new file mode 100644 index 000000000..cb61cc050 --- /dev/null +++ b/middleware/file/notify.go @@ -0,0 +1,8 @@ +package file + +// Notify sends notifies to the configured remotes. It will try up to three times +// before giving up on a specific remote. +func Notify(remotes []string) error { + return nil + +} diff --git a/middleware/file/tree/all.go b/middleware/file/tree/all.go new file mode 100644 index 000000000..f621e3465 --- /dev/null +++ b/middleware/file/tree/all.go @@ -0,0 +1,21 @@ +package tree + +// All traverses tree and returns all elements +func (t *Tree) All() []*Elem { + if t.Root == nil { + return nil + } + found := t.Root.all(nil) + return found +} + +func (n *Node) all(found []*Elem) []*Elem { + if n.Left != nil { + found = n.Left.all(found) + } + found = append(found, n.Elem) + if n.Right != nil { + found = n.Right.all(found) + } + return found +} diff --git a/middleware/file/tree/tree.go b/middleware/file/tree/tree.go index db57c2092..234060eba 100644 --- a/middleware/file/tree/tree.go +++ b/middleware/file/tree/tree.go @@ -528,6 +528,8 @@ func (n *Node) floor(rr dns.RR) *Node { return n } +// TODO(successor, predecessor) + // Ceil returns the smallest value equal to or greater than the query q according to q.Compare(). func (t *Tree) Ceil(rr dns.RR) *Elem { if t.Root == nil { diff --git a/middleware/file/xfr.go b/middleware/file/xfr.go new file mode 100644 index 000000000..297de2fc5 --- /dev/null +++ b/middleware/file/xfr.go @@ -0,0 +1,61 @@ +package file + +import ( + "fmt" + + "github.com/miekg/coredns/middleware" + + "github.com/miekg/dns" + "golang.org/x/net/context" +) + +type ( + Xfr struct { + *Zone + } +) + +// Serve an AXFR (or maybe later an IXFR) as well. +func (x Xfr) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := middleware.State{W: w, Req: r} + if !x.TransferAllowed(state) { + return dns.RcodeServerFailure, nil + } + if state.QType() != dns.TypeAXFR { + return 0, fmt.Errorf("file: xfr called with non xfr type: %d", state.QType()) + } + if state.Proto() == "udp" { + return 0, fmt.Errorf("file: xfr called with udp") + } + + records := x.All() + if len(records) == 0 { + return dns.RcodeServerFailure, nil + } + + ch := make(chan *dns.Envelope) + defer close(ch) + tr := new(dns.Transfer) + go tr.Out(w, r, ch) + + j, l := 0, 0 + records = append(records, records[0]) + for i, r := range records { + l += dns.Len(r) + if l > transferLength { + ch <- &dns.Envelope{RR: records[j:i]} + l = 0 + j = i + } + } + if j < len(records) { + ch <- &dns.Envelope{RR: records[j:]} + } + + w.Hijack() + // w.Close() // Client closes connection + return dns.RcodeSuccess, nil +} + +//const transferLength = 10e3 // Start a new envelop after message reaches this size. +const transferLength = 100 // Start a new envelop after message reaches this size. diff --git a/middleware/file/xfr_test.go b/middleware/file/xfr_test.go new file mode 100644 index 000000000..07caaf1d9 --- /dev/null +++ b/middleware/file/xfr_test.go @@ -0,0 +1,34 @@ +package file + +import ( + "fmt" + "strings" +) + +func ExampleZone_All() { + zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin") + if err != nil { + return + } + records := zone.All() + for _, r := range records { + fmt.Printf("%+v\n", r) + } + // Output + // xfr_test.go:15: miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400 + // xfr_test.go:15: www.miek.nl. 1800 IN CNAME a.miek.nl. + // xfr_test.go:15: miek.nl. 1800 IN NS linode.atoom.net. + // xfr_test.go:15: miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl. + // xfr_test.go:15: miek.nl. 1800 IN NS omval.tednet.nl. + // xfr_test.go:15: miek.nl. 1800 IN NS ext.ns.whyscream.net. + // xfr_test.go:15: miek.nl. 1800 IN MX 1 aspmx.l.google.com. + // xfr_test.go:15: miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com. + // xfr_test.go:15: miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com. + // xfr_test.go:15: miek.nl. 1800 IN MX 10 aspmx2.googlemail.com. + // xfr_test.go:15: miek.nl. 1800 IN MX 10 aspmx3.googlemail.com. + // xfr_test.go:15: miek.nl. 1800 IN A 139.162.196.78 + // xfr_test.go:15: miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 + // xfr_test.go:15: archive.miek.nl. 1800 IN CNAME a.miek.nl. + // xfr_test.go:15: a.miek.nl. 1800 IN A 139.162.196.78 + // xfr_test.go:15: a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 +} diff --git a/middleware/file/zone.go b/middleware/file/zone.go index 57eb8d997..bac420669 100644 --- a/middleware/file/zone.go +++ b/middleware/file/zone.go @@ -1,20 +1,29 @@ package file import ( + "github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware/file/tree" "github.com/miekg/dns" ) +type Transfer struct { + Out bool + In bool + // more later +} + type Zone struct { SOA *dns.SOA - SIG []*dns.RRSIG + SIG []dns.RR name string *tree.Tree + Masters []string + Transfer *Transfer } func NewZone(name string) *Zone { - return &Zone{name: dns.Fqdn(name), Tree: &tree.Tree{}} + return &Zone{name: dns.Fqdn(name), Tree: &tree.Tree{}, Transfer: &Transfer{}} } func (z *Zone) Insert(r dns.RR) { @@ -24,3 +33,28 @@ func (z *Zone) Insert(r dns.RR) { func (z *Zone) Delete(r dns.RR) { z.Tree.Delete(r) } + +// It the transfer request allowed. +func (z *Zone) TransferAllowed(state middleware.State) bool { + if z.Transfer == nil { + return false + } + return z.Transfer.Out +} + +// All returns all records from the zone, the first record will be the SOA record, +// otionally followed by all RRSIG(SOA)s. +func (z *Zone) All() []dns.RR { + records := []dns.RR{} + allNodes := z.Tree.All() + for _, a := range allNodes { + records = append(records, a.All()...) + } + + if len(z.SIG) > 0 { + records = append(z.SIG, records...) + } + return append([]dns.RR{z.SOA}, records...) +} + +// Apex function? diff --git a/middleware/recorder.go b/middleware/recorder.go index 19a15463e..6b926e48d 100644 --- a/middleware/recorder.go +++ b/middleware/recorder.go @@ -37,7 +37,9 @@ func NewResponseRecorder(w dns.ResponseWriter) *ResponseRecorder { // underlying ResponseWriter's WriteMsg method. func (r *ResponseRecorder) WriteMsg(res *dns.Msg) error { r.rcode = res.Rcode - r.size = res.Len() + // We may get called multiple times (axfr for instance). + // Save the last message, but add the sizes. + r.size += res.Len() r.msg = res return r.ResponseWriter.WriteMsg(res) }