middleware/auto: add (#333)
Add auto-load middleware that automatically picks up zones. Every X seconds it will scan for new zones. Add tests and documentation. Make 'make test' use -race.
This commit is contained in:
parent
2eafe3ee94
commit
d536272201
19 changed files with 838 additions and 18 deletions
4
Makefile
4
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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -89,6 +89,7 @@ var directives = []string{
|
|||
|
||||
"dnssec",
|
||||
"file",
|
||||
"auto",
|
||||
"secondary",
|
||||
"etcd",
|
||||
"kubernetes",
|
||||
|
|
61
middleware/auto/README.md
Normal file
61
middleware/auto/README.md
Normal file
|
@ -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
|
||||
}
|
||||
~~~
|
99
middleware/auto/auto.go
Normal file
99
middleware/auto/auto.go
Normal file
|
@ -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
|
||||
}
|
20
middleware/auto/regexp.go
Normal file
20
middleware/auto/regexp.go
Normal file
|
@ -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
|
||||
}
|
20
middleware/auto/regexp_test.go
Normal file
20
middleware/auto/regexp_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
150
middleware/auto/setup.go
Normal file
150
middleware/auto/setup.go
Normal file
|
@ -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
|
||||
}
|
81
middleware/auto/setup_test.go
Normal file
81
middleware/auto/setup_test.go
Normal file
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
96
middleware/auto/walk.go
Normal file
96
middleware/auto/walk.go
Normal file
|
@ -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
|
||||
}
|
72
middleware/auto/walk_test.go
Normal file
72
middleware/auto/walk_test.go
Normal file
|
@ -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
|
||||
}
|
48
middleware/auto/watcher_test.go
Normal file
48
middleware/auto/watcher_test.go
Normal file
|
@ -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)
|
||||
}
|
76
middleware/auto/zone.go
Normal file
76
middleware/auto/zone.go
Normal file
|
@ -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()
|
||||
}
|
|
@ -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")}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
88
test/auto_test.go
Normal file
88
test/auto_test.go
Normal file
|
@ -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
|
||||
`
|
Loading…
Add table
Add a link
Reference in a new issue