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
|
.PHONY: test
|
||||||
test: deps
|
test: deps
|
||||||
go test $(TEST_VERBOSE) ./...
|
go test -race $(TEST_VERBOSE) ./...
|
||||||
|
|
||||||
.PHONY: testk8s
|
.PHONY: testk8s
|
||||||
testk8s: deps
|
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
|
.PHONY: coverage
|
||||||
coverage: deps
|
coverage: deps
|
||||||
|
|
|
@ -5,7 +5,8 @@ import (
|
||||||
// plug in the server
|
// plug in the server
|
||||||
_ "github.com/miekg/coredns/core/dnsserver"
|
_ "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/bind"
|
||||||
_ "github.com/miekg/coredns/middleware/cache"
|
_ "github.com/miekg/coredns/middleware/cache"
|
||||||
_ "github.com/miekg/coredns/middleware/chaos"
|
_ "github.com/miekg/coredns/middleware/chaos"
|
||||||
|
|
|
@ -89,6 +89,7 @@ var directives = []string{
|
||||||
|
|
||||||
"dnssec",
|
"dnssec",
|
||||||
"file",
|
"file",
|
||||||
|
"auto",
|
||||||
"secondary",
|
"secondary",
|
||||||
"etcd",
|
"etcd",
|
||||||
"kubernetes",
|
"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
|
// add handler for example.net
|
||||||
dns.HandleFunc("example.net.", func(w dns.ResponseWriter, r *dns.Msg) {
|
dns.HandleFunc("example.net.", func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
t.Logf("writing response for example.net.")
|
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
m.Answer = []dns.RR{test.A("example.net. 86400 IN A 93.184.216.34")}
|
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 maps zone names to a *Zone.
|
||||||
Zones struct {
|
Zones struct {
|
||||||
Z map[string]*Zone
|
Z map[string]*Zone // A map mapping zone (origin) to the Zone's data
|
||||||
Names []string
|
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")
|
return dns.RcodeServerFailure, errors.New("can only deal with ClassINET")
|
||||||
}
|
}
|
||||||
qname := state.Name()
|
qname := state.Name()
|
||||||
|
// TODO(miek): match the qname better in the map
|
||||||
zone := middleware.Zones(f.Zones.Names).Matches(qname)
|
zone := middleware.Zones(f.Zones.Names).Matches(qname)
|
||||||
if zone == "" {
|
if zone == "" {
|
||||||
if f.Next != nil {
|
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 {
|
if z == nil {
|
||||||
return dns.RcodeServerFailure, nil
|
return dns.RcodeServerFailure, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is only for when we are a secondary zones.
|
||||||
if r.Opcode == dns.OpcodeNotify {
|
if r.Opcode == dns.OpcodeNotify {
|
||||||
if z.isNotify(state) {
|
if z.isNotify(state) {
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/miekg/coredns/middleware/test"
|
"github.com/miekg/coredns/middleware/test"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ func TestZoneReload(t *testing.T) {
|
||||||
t.Fatalf("failed to parse zone: %s", err)
|
t.Fatalf("failed to parse zone: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
z.Reload(nil)
|
z.Reload()
|
||||||
|
|
||||||
if _, _, _, res := z.Lookup("miek.nl.", dns.TypeSOA, false); res != Success {
|
if _, _, _, res := z.Lookup("miek.nl.", dns.TypeSOA, false); res != Success {
|
||||||
t.Fatalf("failed to lookup, got %d", res)
|
t.Fatalf("failed to lookup, got %d", res)
|
||||||
|
|
|
@ -33,7 +33,7 @@ func setup(c *caddy.Controller) error {
|
||||||
if len(z.TransferTo) > 0 {
|
if len(z.TransferTo) > 0 {
|
||||||
z.Notify()
|
z.Notify()
|
||||||
}
|
}
|
||||||
z.Reload(nil)
|
z.Reload()
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@ -99,7 +99,7 @@ func fileParse(c *caddy.Controller) (Zones, error) {
|
||||||
case "no_reload":
|
case "no_reload":
|
||||||
noReload = true
|
noReload = true
|
||||||
}
|
}
|
||||||
// discard from, here, maybe check and show log when we do?
|
|
||||||
for _, origin := range origins {
|
for _, origin := range origins {
|
||||||
if t != nil {
|
if t != nil {
|
||||||
z[origin].TransferTo = append(z[origin].TransferTo, t...)
|
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...]'.
|
// 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) {
|
func TransferParse(c *caddy.Controller, secondary bool) (tos, froms []string, err error) {
|
||||||
what := c.Val()
|
what := c.Val()
|
||||||
if !c.NextArg() {
|
if !c.NextArg() {
|
||||||
|
|
|
@ -27,9 +27,9 @@ type Zone struct {
|
||||||
TransferFrom []string
|
TransferFrom []string
|
||||||
Expired *bool
|
Expired *bool
|
||||||
|
|
||||||
NoReload bool
|
NoReload bool
|
||||||
reloadMu sync.RWMutex
|
reloadMu sync.RWMutex
|
||||||
// TODO: shutdown watcher channel
|
ReloadShutdown chan bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apex contains the apex records of a zone: SOA, NS and their potential signatures.
|
// 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.
|
// NewZone returns a new zone.
|
||||||
func NewZone(name, file string) *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
|
*z.Expired = false
|
||||||
return z
|
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.
|
// 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 {
|
if z.NoReload {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -156,7 +162,7 @@ func (z *Zone) Reload(shutdown chan bool) error {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event := <-watcher.Events:
|
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)
|
reader, err := os.Open(z.file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed to open `%s' for `%s': %v", z.file, z.origin, err)
|
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)
|
log.Printf("[INFO] Successfully reloaded zone `%s'", z.origin)
|
||||||
z.Notify()
|
z.Notify()
|
||||||
}
|
}
|
||||||
case <-shutdown:
|
case <-z.ReloadShutdown:
|
||||||
watcher.Close()
|
watcher.Close()
|
||||||
return
|
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