aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile4
-rw-r--r--core/coredns.go3
-rw-r--r--core/dnsserver/directives.go1
-rw-r--r--middleware/auto/README.md61
-rw-r--r--middleware/auto/auto.go99
-rw-r--r--middleware/auto/regexp.go20
-rw-r--r--middleware/auto/regexp_test.go20
-rw-r--r--middleware/auto/setup.go150
-rw-r--r--middleware/auto/setup_test.go81
-rw-r--r--middleware/auto/walk.go96
-rw-r--r--middleware/auto/walk_test.go72
-rw-r--r--middleware/auto/watcher_test.go48
-rw-r--r--middleware/auto/zone.go76
-rw-r--r--middleware/etcd/stub_test.go1
-rw-r--r--middleware/file/file.go7
-rw-r--r--middleware/file/reload_test.go3
-rw-r--r--middleware/file/setup.go6
-rw-r--r--middleware/file/zone.go20
-rw-r--r--test/auto_test.go88
19 files changed, 838 insertions, 18 deletions
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 `{<number>}` 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
+`