diff --git a/Makefile b/Makefile index 0443b6f09..3824f2063 100644 --- a/Makefile +++ b/Makefile @@ -23,11 +23,11 @@ deps: .PHONY: test test: deps - go test $(TEST_VERBOSE) ./... + go test -race $(TEST_VERBOSE) ./... .PHONY: testk8s testk8s: deps - go test $(TEST_VERBOSE) -tags=k8s -run 'TestKubernetes' ./test ./middleware/kubernetes/... + go test -race $(TEST_VERBOSE) -tags=k8s -run 'TestKubernetes' ./test ./middleware/kubernetes/... .PHONY: coverage coverage: deps diff --git a/core/coredns.go b/core/coredns.go index 558b03efc..45dffcb37 100644 --- a/core/coredns.go +++ b/core/coredns.go @@ -5,7 +5,8 @@ import ( // plug in the server _ "github.com/miekg/coredns/core/dnsserver" - // plug in the standard directives + // plug in the standard directives (sorted) + _ "github.com/miekg/coredns/middleware/auto" _ "github.com/miekg/coredns/middleware/bind" _ "github.com/miekg/coredns/middleware/cache" _ "github.com/miekg/coredns/middleware/chaos" diff --git a/core/dnsserver/directives.go b/core/dnsserver/directives.go index 5898f8365..b73c0463e 100644 --- a/core/dnsserver/directives.go +++ b/core/dnsserver/directives.go @@ -89,6 +89,7 @@ var directives = []string{ "dnssec", "file", + "auto", "secondary", "etcd", "kubernetes", diff --git a/middleware/auto/README.md b/middleware/auto/README.md new file mode 100644 index 000000000..94a606708 --- /dev/null +++ b/middleware/auto/README.md @@ -0,0 +1,61 @@ +# auto + +*auto* enables serving zone data from an RFC 1035-style master file which is automatically picked +up from disk. + +The *auto* middleware is used for an "old-style" DNS server. It serves from a preloaded file that exists +on disk. If the zone file contains signatures (i.e. is signed, i.e. DNSSEC) correct DNSSEC answers +are returned. Only NSEC is supported! If you use this setup *you* are responsible for resigning the +zonefile. New zones or changed zone are automatically picked up from disk. + +## Syntax + +~~~ +auto [ZONES...] { + directory DIR [REGEXP ORIGIN_TEMPLATE [TIMEOUT]] +} +~~~ + +**ZONES** zones it should be authoritative for. If empty, the zones from the configuration block +are used. + +* `directory` loads zones from the speficied **DIR**. If a file name matches **REGEXP** it will be + used to extract the origin. **ORIGIN_TEMPLATE** will be used as a template for the origin. Strings + like `{}` are replaced with the respective matches in the file name, i.e. `{1}` is the + first match, `{2}` is the second, etc.. The default is: `db\.(.*) {1}` e.g. from a file with the + name `db.example.com`, the extracted origin will be `example.com`. **TIMEOUT** specifies how often + CoreDNS should scan the directory, the default is every 60 seconds. This value is in seconds. + The minimum value is 1 second. + +All directives from the *file* middleware are supported. Note that *auto* will load all zones found, +even though the directive might only receive queries for a specific zone. I.e: + +~~~ +auto example.org { + directory /etc/coredns/zones +} +~~~ +Will happily pick up a zone for `example.COM`, except it will never be queried, because the *auto* +directive only is authoritative for `example.ORG`. + +## Examples + +Load `org` domains from `/etc/coredns/zones/org` and allow transfers to the internet, but send +notifies to 10.240.1.1 + +~~~ +auto org { + directory /etc/coredns/zones/org + transfer to * + transfer to 10.240.1.1 +} +~~~ + +Load `org` domains from `/etc/coredns/zones/org` and looks for file names as `www.db.example.org`, +where `example.org` is the origin. Scan every 45 seconds. + +~~~ +auto org { + directory /etc/coredns/zones/org www\.db\.(.*) {1} 45 +} +~~~ diff --git a/middleware/auto/auto.go b/middleware/auto/auto.go new file mode 100644 index 000000000..7721c194e --- /dev/null +++ b/middleware/auto/auto.go @@ -0,0 +1,99 @@ +// Package auto implements an on-the-fly loading file backend. +package auto + +import ( + "errors" + "regexp" + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/file" + "github.com/miekg/coredns/request" + + "github.com/miekg/dns" + "golang.org/x/net/context" +) + +type ( + // Auto holds the zones and the loader configuration for automatically loading zones. + Auto struct { + Next middleware.Handler + *Zones + + loader + } + + loader struct { + directory string + template string + re *regexp.Regexp + + // In the future this should be something like ZoneMeta that contains all this stuff. + transferTo []string + noReload bool + + duration time.Duration + } +) + +// ServeDNS implements the middleware.Handle interface. +func (a Auto) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + if state.QClass() != dns.ClassINET { + return dns.RcodeServerFailure, errors.New("can only deal with ClassINET") + } + qname := state.Name() + + // TODO(miek): match the qname better in the map + + // Precheck with the origins, i.e. are we allowed to looks here. + zone := middleware.Zones(a.Zones.Origins()).Matches(qname) + if zone == "" { + if a.Next != nil { + return a.Next.ServeDNS(ctx, w, r) + } + return dns.RcodeServerFailure, errors.New("no next middleware found") + } + + // Now the real zone. + zone = middleware.Zones(a.Zones.Names()).Matches(qname) + + a.Zones.RLock() + z, ok := a.Zones.Z[zone] + a.Zones.RUnlock() + + if !ok { + return a.Next.ServeDNS(ctx, w, r) + } + if z == nil { + return dns.RcodeServerFailure, nil + } + + if state.QType() == dns.TypeAXFR || state.QType() == dns.TypeIXFR { + xfr := file.Xfr{Zone: z} + return xfr.ServeDNS(ctx, w, r) + } + + answer, ns, extra, result := z.Lookup(qname, state.QType(), state.Do()) + + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true + m.Answer, m.Ns, m.Extra = answer, ns, extra + + switch result { + case file.Success: + case file.NoData: + case file.NameError: + m.Rcode = dns.RcodeNameError + case file.Delegation: + m.Authoritative = false + case file.ServerFailure: + return dns.RcodeServerFailure, nil + } + + state.SizeAndDo(m) + m, _ = state.Scrub(m) + w.WriteMsg(m) + return dns.RcodeSuccess, nil +} diff --git a/middleware/auto/regexp.go b/middleware/auto/regexp.go new file mode 100644 index 000000000..fa424ec7e --- /dev/null +++ b/middleware/auto/regexp.go @@ -0,0 +1,20 @@ +package auto + +// rewriteToExpand rewrites our template string to one that we can give to regexp.ExpandString. This basically +// involves prefixing any '{' with a '$'. +func rewriteToExpand(s string) string { + // Pretty dumb at the moment, every { will get a $ prefixed. + // Also wasteful as we build the string with +=. This is OKish + // as we do this during config parsing. + + copy := "" + + for _, c := range s { + if c == '{' { + copy += "$" + } + copy += string(c) + } + + return copy +} diff --git a/middleware/auto/regexp_test.go b/middleware/auto/regexp_test.go new file mode 100644 index 000000000..17c35eb90 --- /dev/null +++ b/middleware/auto/regexp_test.go @@ -0,0 +1,20 @@ +package auto + +import "testing" + +func TestRewriteToExpand(t *testing.T) { + tests := []struct { + in string + expected string + }{ + {in: "", expected: ""}, + {in: "{1}", expected: "${1}"}, + {in: "{1", expected: "${1"}, + } + for i, tc := range tests { + got := rewriteToExpand(tc.in) + if got != tc.expected { + t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expected, got) + } + } +} diff --git a/middleware/auto/setup.go b/middleware/auto/setup.go new file mode 100644 index 000000000..8c56f90a0 --- /dev/null +++ b/middleware/auto/setup.go @@ -0,0 +1,150 @@ +package auto + +import ( + "log" + "os" + "path" + "regexp" + "strconv" + "time" + + "github.com/miekg/coredns/core/dnsserver" + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/file" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("auto", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + a, err := autoParse(c) + if err != nil { + return middleware.Error("auto", err) + } + + walkChan := make(chan bool) + + c.OnStartup(func() error { + err := a.Zones.Walk(a.loader) + if err != nil { + return err + } + + go func() { + ticker := time.NewTicker(a.loader.duration) + for { + select { + case <-walkChan: + return + case <-ticker.C: + a.Zones.Walk(a.loader) + } + } + }() + return nil + }) + + c.OnShutdown(func() error { + close(walkChan) + return nil + }) + + dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler { + a.Next = next + return a + }) + + return nil +} + +func autoParse(c *caddy.Controller) (Auto, error) { + var a = Auto{ + loader: loader{template: "${1}", re: regexp.MustCompile(`db\.(.*)`), duration: time.Duration(60 * time.Second)}, + Zones: &Zones{}, + } + + config := dnsserver.GetConfig(c) + + for c.Next() { + if c.Val() == "auto" { + // auto [ZONES...] + a.Zones.origins = make([]string, len(c.ServerBlockKeys)) + copy(a.Zones.origins, c.ServerBlockKeys) + + args := c.RemainingArgs() + if len(args) > 0 { + a.Zones.origins = args + } + for i := range a.Zones.origins { + a.Zones.origins[i] = middleware.Host(a.Zones.origins[i]).Normalize() + } + + for c.NextBlock() { + switch c.Val() { + case "directory": // directory DIR [REGEXP [TEMPLATE] [DURATION]] + if !c.NextArg() { + return a, c.ArgErr() + } + a.loader.directory = c.Val() + if !path.IsAbs(a.loader.directory) && config.Root != "" { + a.loader.directory = path.Join(config.Root, a.loader.directory) + } + _, err := os.Stat(a.loader.directory) + if err != nil { + if os.IsNotExist(err) { + log.Printf("[WARNING] Directory does not exist: %s", a.loader.directory) + } else { + return a, c.Errf("Unable to access root path '%s': %v", a.loader.directory, err) + } + } + + // regexp + if c.NextArg() { + a.loader.re, err = regexp.Compile(c.Val()) + if err != nil { + return a, err + } + if a.loader.re.NumSubexp() == 0 { + return a, c.Errf("Need at least one sub expression") + } + } + + // template + if c.NextArg() { + a.loader.template = rewriteToExpand(c.Val()) + } + + // template + if c.NextArg() { + i, err := strconv.Atoi(c.Val()) + if err != nil { + return a, err + } + if i < 1 { + i = 1 + } + a.loader.duration = time.Duration(i) * time.Second + } + + case "no_reload": + a.loader.noReload = true + + default: + t, _, e := file.TransferParse(c, false) + if e != nil { + return a, e + } + a.loader.transferTo = t + } + } + + } + } + return a, nil +} diff --git a/middleware/auto/setup_test.go b/middleware/auto/setup_test.go new file mode 100644 index 000000000..15790ead6 --- /dev/null +++ b/middleware/auto/setup_test.go @@ -0,0 +1,81 @@ +package auto + +import ( + "testing" + + "github.com/mholt/caddy" +) + +func TestAutoParse(t *testing.T) { + tests := []struct { + inputFileRules string + shouldErr bool + expectedDirectory string + expectedTempl string + expectedRe string + expectedTo string + }{ + { + `auto example.org { + directory /tmp + transfer to 127.0.0.1 + }`, + false, "/tmp", "${1}", `db\.(.*)`, "127.0.0.1:53", + }, + { + `auto { + directory /tmp + }`, + false, "/tmp", "${1}", `db\.(.*)`, "", + }, + { + `auto { + directory /tmp (.*) bliep + }`, + false, "/tmp", "bliep", `(.*)`, "", + }, + // errors + { + `auto example.org { + directory + }`, + true, "", "${1}", `db\.(.*)`, "", + }, + { + `auto example.org { + directory /tmp * {1} + }`, + true, "", "${1}", ``, "", + }, + { + `auto example.org { + directory /tmp .* {1} + }`, + true, "", "${1}", ``, "", + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputFileRules) + a, err := autoParse(c) + + if err == nil && test.shouldErr { + t.Fatalf("Test %d expected errors, but got no error", i) + } else if err != nil && !test.shouldErr { + t.Fatalf("Test %d expected no errors, but got '%v'", i, err) + } else if !test.shouldErr { + if a.loader.directory != test.expectedDirectory { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedDirectory, a.loader.directory) + } + if a.loader.template != test.expectedTempl { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedTempl, a.loader.template) + } + if a.loader.re.String() != test.expectedRe { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedRe, a.loader.re) + } + if test.expectedTo != "" && a.loader.transferTo[0] != test.expectedTo { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedTo, a.loader.transferTo[0]) + } + } + } +} diff --git a/middleware/auto/walk.go b/middleware/auto/walk.go new file mode 100644 index 000000000..4259d7f17 --- /dev/null +++ b/middleware/auto/walk.go @@ -0,0 +1,96 @@ +package auto + +import ( + "log" + "os" + "path" + "path/filepath" + "regexp" + + "github.com/miekg/coredns/middleware/file" + + "github.com/miekg/dns" +) + +// Walk will recursively walk of the file under l.directory and adds the one that match l.re. +func (z *Zones) Walk(l loader) error { + + // TODO(miek): should add something so that we don't stomp on each other. + + toDelete := make(map[string]bool) + for _, n := range z.Names() { + toDelete[n] = true + } + + filepath.Walk(l.directory, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + + match, origin := matches(l.re, info.Name(), l.template) + if !match { + return nil + } + + if _, ok := z.Z[origin]; ok { + // we already have this zone + toDelete[origin] = false + return nil + } + + reader, err := os.Open(path) + if err != nil { + log.Printf("[WARNING] Opening %s failed: %s", path, err) + return nil + } + + zo, err := file.Parse(reader, origin, path) + if err != nil { + // Parse barfs warning by itself... + return nil + } + + zo.NoReload = l.noReload + zo.TransferTo = l.transferTo + + z.Insert(zo, origin) + + zo.Notify() + + log.Printf("[INFO] Inserting zone `%s' from: %s", origin, path) + + toDelete[origin] = false + + return nil + }) + + for origin, ok := range toDelete { + if !ok { + continue + } + z.Delete(origin) + log.Printf("[INFO] Deleting zone `%s'", origin) + } + + return nil +} + +// matches matches re to filename, if is is a match, the subexpression will be used to expand +// template to an origin. When match is true that origin is returned. Origin is fully qualified. +func matches(re *regexp.Regexp, filename, template string) (match bool, origin string) { + base := path.Base(filename) + + matches := re.FindStringSubmatchIndex(base) + if matches == nil { + return false, "" + } + + by := re.ExpandString(nil, template, base, matches) + if by == nil { + return false, "" + } + + origin = dns.Fqdn(string(by)) + + return true, origin +} diff --git a/middleware/auto/walk_test.go b/middleware/auto/walk_test.go new file mode 100644 index 000000000..cc420d5b6 --- /dev/null +++ b/middleware/auto/walk_test.go @@ -0,0 +1,72 @@ +package auto + +import ( + "io/ioutil" + "log" + "os" + "path" + "regexp" + "testing" +) + +var dbFiles = []string{"db.example.org", "aa.example.org"} + +const zoneContent = `; testzone +@ IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082534 7200 3600 1209600 3600 + NS a.iana-servers.net. + NS b.iana-servers.net. + +www IN A 127.0.0.1 +` + +func TestWalk(t *testing.T) { + log.SetOutput(ioutil.Discard) + + tempdir, err := createFiles() + if err != nil { + if tempdir != "" { + os.RemoveAll(tempdir) + } + t.Fatal(err) + } + defer os.RemoveAll(tempdir) + + ldr := loader{ + directory: tempdir, + re: regexp.MustCompile(`db\.(.*)`), + template: `${1}`, + } + + z := &Zones{} + + z.Walk(ldr) + + // db.example.org and db.example.com should be here (created in createFiles) + for _, name := range []string{"example.com.", "example.org."} { + if _, ok := z.Z[name]; !ok { + t.Errorf("%s should have been added", name) + } + } +} + +func createFiles() (string, error) { + dir, err := ioutil.TempDir(os.TempDir(), "coredns") + if err != nil { + return dir, err + } + + for _, name := range dbFiles { + if err := ioutil.WriteFile(path.Join(dir, name), []byte(zoneContent), 0644); err != nil { + return dir, err + } + } + // symlinks + if err = os.Symlink(path.Join(dir, "db.example.org"), path.Join(dir, "db.example.com")); err != nil { + return dir, err + } + if err = os.Symlink(path.Join(dir, "db.example.org"), path.Join(dir, "aa.example.com")); err != nil { + return dir, err + } + + return dir, nil +} diff --git a/middleware/auto/watcher_test.go b/middleware/auto/watcher_test.go new file mode 100644 index 000000000..751c78c0d --- /dev/null +++ b/middleware/auto/watcher_test.go @@ -0,0 +1,48 @@ +package auto + +import ( + "io/ioutil" + "log" + "os" + "path" + "regexp" + "testing" +) + +func TestWatcher(t *testing.T) { + log.SetOutput(ioutil.Discard) + + tempdir, err := createFiles() + if err != nil { + if tempdir != "" { + os.RemoveAll(tempdir) + } + t.Fatal(err) + } + defer os.RemoveAll(tempdir) + + ldr := loader{ + directory: tempdir, + re: regexp.MustCompile(`db\.(.*)`), + template: `${1}`, + } + + z := &Zones{} + + z.Walk(ldr) + + // example.org and example.com should exist + if x := len(z.Z["example.org."].All()); x != 4 { + t.Fatalf("expected 4 RRs, got %d", x) + } + if x := len(z.Z["example.com."].All()); x != 4 { + t.Fatalf("expected 4 RRs, got %d", x) + } + + // Now remove one file, rescan and see if it's gone. + if err := os.Remove(path.Join(tempdir, "db.example.com")); err != nil { + t.Fatal(err) + } + + z.Walk(ldr) +} diff --git a/middleware/auto/zone.go b/middleware/auto/zone.go new file mode 100644 index 000000000..4c950b908 --- /dev/null +++ b/middleware/auto/zone.go @@ -0,0 +1,76 @@ +// Package auto implements a on-the-fly loading file backend. +package auto + +import ( + "sync" + + "github.com/miekg/coredns/middleware/file" +) + +// Zones maps zone names to a *Zone. This keep track of what we zones we have loaded at +// any one time. +type Zones struct { + Z map[string]*file.Zone // A map mapping zone (origin) to the Zone's data. + names []string // All the keys from the map Z as a string slice. + + origins []string // Any origins from the server block. + + sync.RWMutex +} + +// Names returns the names from z. +func (z *Zones) Names() []string { + z.RLock() + n := z.names + z.RUnlock() + return n +} + +// Origins returns the origins from z. +func (z *Zones) Origins() []string { + // doesn't need locking, because there aren't multiple Go routines accessing it. + return z.origins +} + +// Zones returns a zone with origin name from z, nil when not found. +func (z *Zones) Zones(name string) *file.Zone { + z.RLock() + zo := z.Z[name] + z.RUnlock() + return zo +} + +// Insert inserts a new zone into z. If zo.NoReload is false, the +// reload goroutine is started. +func (z *Zones) Insert(zo *file.Zone, name string) { + z.Lock() + + if z.Z == nil { + z.Z = make(map[string]*file.Zone) + } + + z.Z[name] = zo + z.names = append(z.names, name) + + zo.Reload() + + z.Unlock() +} + +// Delete removes the zone named name from z. It also stop the the zone's reload goroutine. +func (z *Zones) Delete(name string) { + z.Lock() + + if zo, ok := z.Z[name]; ok && !zo.NoReload { + zo.ReloadShutdown <- true + } + + delete(z.Z, name) + + // just regenerate Names (might be bad if you have a lot of zones...) + z.names = []string{} + for n := range z.Z { + z.names = append(z.names, n) + } + z.Unlock() +} diff --git a/middleware/etcd/stub_test.go b/middleware/etcd/stub_test.go index 93ea80ebd..c1ba5ee5c 100644 --- a/middleware/etcd/stub_test.go +++ b/middleware/etcd/stub_test.go @@ -22,7 +22,6 @@ func fakeStubServerExampleNet(t *testing.T) (*dns.Server, string) { } // add handler for example.net dns.HandleFunc("example.net.", func(w dns.ResponseWriter, r *dns.Msg) { - t.Logf("writing response for example.net.") m := new(dns.Msg) m.SetReply(r) m.Answer = []dns.RR{test.A("example.net. 86400 IN A 93.184.216.34")} diff --git a/middleware/file/file.go b/middleware/file/file.go index b1136c7db..90d15af79 100644 --- a/middleware/file/file.go +++ b/middleware/file/file.go @@ -22,8 +22,8 @@ type ( // Zones maps zone names to a *Zone. Zones struct { - Z map[string]*Zone - Names []string + Z map[string]*Zone // A map mapping zone (origin) to the Zone's data + Names []string // All the keys from the map Z as a string slice. } ) @@ -35,6 +35,7 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i return dns.RcodeServerFailure, errors.New("can only deal with ClassINET") } qname := state.Name() + // TODO(miek): match the qname better in the map zone := middleware.Zones(f.Zones.Names).Matches(qname) if zone == "" { if f.Next != nil { @@ -49,6 +50,8 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i if z == nil { return dns.RcodeServerFailure, nil } + + // This is only for when we are a secondary zones. if r.Opcode == dns.OpcodeNotify { if z.isNotify(state) { m := new(dns.Msg) diff --git a/middleware/file/reload_test.go b/middleware/file/reload_test.go index 9dcafc8a2..c46dc3e20 100644 --- a/middleware/file/reload_test.go +++ b/middleware/file/reload_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/miekg/coredns/middleware/test" + "github.com/miekg/dns" ) @@ -28,7 +29,7 @@ func TestZoneReload(t *testing.T) { t.Fatalf("failed to parse zone: %s", err) } - z.Reload(nil) + z.Reload() if _, _, _, res := z.Lookup("miek.nl.", dns.TypeSOA, false); res != Success { t.Fatalf("failed to lookup, got %d", res) diff --git a/middleware/file/setup.go b/middleware/file/setup.go index a73fa50b1..b0946ed4b 100644 --- a/middleware/file/setup.go +++ b/middleware/file/setup.go @@ -33,7 +33,7 @@ func setup(c *caddy.Controller) error { if len(z.TransferTo) > 0 { z.Notify() } - z.Reload(nil) + z.Reload() }) return nil }) @@ -99,7 +99,7 @@ func fileParse(c *caddy.Controller) (Zones, error) { case "no_reload": noReload = true } - // discard from, here, maybe check and show log when we do? + for _, origin := range origins { if t != nil { z[origin].TransferTo = append(z[origin].TransferTo, t...) @@ -113,8 +113,6 @@ func fileParse(c *caddy.Controller) (Zones, error) { } // TransferParse parses transfer statements: 'transfer to [address...]'. -// Exported so secondary can use this as well. For the `file` middleware transfer from does -// not make sense; make this an error. func TransferParse(c *caddy.Controller, secondary bool) (tos, froms []string, err error) { what := c.Val() if !c.NextArg() { diff --git a/middleware/file/zone.go b/middleware/file/zone.go index abb837499..b503aa5d2 100644 --- a/middleware/file/zone.go +++ b/middleware/file/zone.go @@ -27,9 +27,9 @@ type Zone struct { TransferFrom []string Expired *bool - NoReload bool - reloadMu sync.RWMutex - // TODO: shutdown watcher channel + NoReload bool + reloadMu sync.RWMutex + ReloadShutdown chan bool } // Apex contains the apex records of a zone: SOA, NS and their potential signatures. @@ -42,7 +42,13 @@ type Apex struct { // NewZone returns a new zone. func NewZone(name, file string) *Zone { - z := &Zone{origin: dns.Fqdn(name), file: path.Clean(file), Tree: &tree.Tree{}, Expired: new(bool)} + z := &Zone{ + origin: dns.Fqdn(name), + file: path.Clean(file), + Tree: &tree.Tree{}, + Expired: new(bool), + ReloadShutdown: make(chan bool), + } *z.Expired = false return z } @@ -138,7 +144,7 @@ func (z *Zone) All() []dns.RR { } // Reload reloads a zone when it is changed on disk. If z.NoRoload is true, no reloading will be done. -func (z *Zone) Reload(shutdown chan bool) error { +func (z *Zone) Reload() error { if z.NoReload { return nil } @@ -156,7 +162,7 @@ func (z *Zone) Reload(shutdown chan bool) error { for { select { case event := <-watcher.Events: - if path.Clean(event.Name) == z.file { + if event.Op == fsnotify.Write && path.Clean(event.Name) == z.file { reader, err := os.Open(z.file) if err != nil { log.Printf("[ERROR] Failed to open `%s' for `%s': %v", z.file, z.origin, err) @@ -176,7 +182,7 @@ func (z *Zone) Reload(shutdown chan bool) error { log.Printf("[INFO] Successfully reloaded zone `%s'", z.origin) z.Notify() } - case <-shutdown: + case <-z.ReloadShutdown: watcher.Close() return } diff --git a/test/auto_test.go b/test/auto_test.go new file mode 100644 index 000000000..525e58df3 --- /dev/null +++ b/test/auto_test.go @@ -0,0 +1,88 @@ +package test + +import ( + "io/ioutil" + "log" + "os" + "path" + "testing" + "time" + + "github.com/miekg/coredns/middleware/proxy" + "github.com/miekg/coredns/middleware/test" + "github.com/miekg/coredns/request" + + "github.com/miekg/dns" +) + +func TestAuto(t *testing.T) { + tmpdir, err := ioutil.TempDir(os.TempDir(), "coredns") + if err != nil { + t.Fatal(err) + } + + corefile := `org:0 { + auto { + directory ` + tmpdir + ` db\.(.*) {1} 1 + } + } +` + + i, err := CoreDNSServer(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + + udp, _ := CoreDNSServerPorts(i, 0) + if udp == "" { + t.Fatalf("Could not get UDP listening port") + } + defer i.Stop() + + log.SetOutput(ioutil.Discard) + + p := proxy.New([]string{udp}) + state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} + + resp, err := p.Lookup(state, "www.example.org.", dns.TypeA) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + if resp.Rcode != dns.RcodeServerFailure { + t.Fatalf("Expected reply to be a SERVFAIL, got %d", resp.Rcode) + } + + // Write db.example.org to get example.org. + if err = ioutil.WriteFile(path.Join(tmpdir, "db.example.org"), []byte(zoneContent), 0644); err != nil { + t.Fatal(err) + } + + time.Sleep(1100 * time.Millisecond) // wait for it to be picked up + resp, err = p.Lookup(state, "www.example.org.", dns.TypeA) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + if len(resp.Answer) != 1 { + t.Fatalf("Expected 1 RR in the answer section, got %d", len(resp.Answer)) + } + + // Remove db.example.org again. + os.Remove(path.Join(tmpdir, "db.example.org")) + + time.Sleep(1100 * time.Millisecond) // wait for it to be picked up + resp, err = p.Lookup(state, "www.example.org.", dns.TypeA) + if err != nil { + t.Fatal("Expected to receive reply, but didn't") + } + if resp.Rcode != dns.RcodeServerFailure { + t.Fatalf("Expected reply to be a SERVFAIL, got %d", resp.Rcode) + } +} + +const zoneContent = `; testzone +@ IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082534 7200 3600 1209600 3600 + NS a.iana-servers.net. + NS b.iana-servers.net. + +www IN A 127.0.0.1 +`