Add secondary support
Allow specifying a primary server and retrieve the zone's content. Add tests and an Expired bool to zone struct, to stop server zones that are expired. The zone is retrieved on Startup, no updates of changed content are done. We also don't respond to notifies yet.
This commit is contained in:
parent
7fb959470e
commit
f58f1e4285
12 changed files with 252 additions and 50 deletions
|
@ -59,6 +59,7 @@ var directiveOrder = []directive{
|
|||
{"errors", setup.Errors},
|
||||
|
||||
{"file", setup.File},
|
||||
{"secondary", setup.Secondary},
|
||||
{"etcd", setup.Etcd},
|
||||
{"proxy", setup.Proxy},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
59
core/setup/secondary.go
Normal file
59
core/setup/secondary.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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 *
|
||||
}
|
||||
~~~
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
63
middleware/file/secondary.go
Normal file
63
middleware/file/secondary.go
Normal file
|
@ -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.
|
||||
*/
|
|
@ -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 {
|
||||
|
|
|
@ -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?
|
||||
|
|
34
middleware/secondary/README.md
Normal file
34
middleware/secondary/README.md
Normal file
|
@ -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
|
||||
}
|
||||
~~~
|
7
middleware/secondary/secondary.go
Normal file
7
middleware/secondary/secondary.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package secondary
|
||||
|
||||
import "github.com/miekg/coredns/middleware/file"
|
||||
|
||||
type Secondary struct {
|
||||
file.File
|
||||
}
|
Loading…
Add table
Reference in a new issue