diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go index e2eed2117..280c03144 100644 --- a/core/dnsserver/zdirectives.go +++ b/core/dnsserver/zdirectives.go @@ -39,6 +39,7 @@ var Directives = []string{ "auto", "secondary", "etcd", + "loop", "forward", "proxy", "erratic", diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go index 80a3edddf..397438663 100644 --- a/core/plugin/zplugin.go +++ b/core/plugin/zplugin.go @@ -23,6 +23,7 @@ import ( _ "github.com/coredns/coredns/plugin/kubernetes" _ "github.com/coredns/coredns/plugin/loadbalance" _ "github.com/coredns/coredns/plugin/log" + _ "github.com/coredns/coredns/plugin/loop" _ "github.com/coredns/coredns/plugin/metadata" _ "github.com/coredns/coredns/plugin/metrics" _ "github.com/coredns/coredns/plugin/nsid" diff --git a/plugin.cfg b/plugin.cfg index 646aee92d..6a8ad0ecd 100644 --- a/plugin.cfg +++ b/plugin.cfg @@ -48,6 +48,7 @@ file:file auto:auto secondary:secondary etcd:etcd +loop:loop forward:forward proxy:proxy erratic:erratic diff --git a/plugin/loop/OWNERS b/plugin/loop/OWNERS new file mode 100644 index 000000000..3a4ef23a1 --- /dev/null +++ b/plugin/loop/OWNERS @@ -0,0 +1,6 @@ +reviewers: + - miekg + - chrisohaver +approvers: + - miekg + - chrisohaver diff --git a/plugin/loop/README.md b/plugin/loop/README.md new file mode 100644 index 000000000..0b02a5158 --- /dev/null +++ b/plugin/loop/README.md @@ -0,0 +1,40 @@ +# loop + +## Name + +*loop* - detect forwarding loops and halt the server. + +## Description + +The *loop* plugin will send a random query to ourselves and will then keep track of how many times +we see it. If we see it more than twice, we assume CoreDNS is looping and we halt the process. + +The plugin will try to send the query for up to 30 seconds. This is done to give CoreDNS enough time +to start up. Once a query has been successfully sent *loop* disables itself to prevent a query of +death. + +The query send is `..zone` with type set to HINFO. + +## Syntax + +~~~ txt +loop +~~~ + +## Examples + +Start a server on the default port and load the *loop* and *forward* plugins. The *forward* plugin +forwards to it self. + +~~~ txt +. { + loop + forward . 127.0.0.1 +} +~~~ + +After CoreDNS has started it stops the process while logging: + +~~~ txt +plugin/loop: Seen "HINFO IN 5577006791947779410.8674665223082153551." more than twice, loop detected +~~~ diff --git a/plugin/loop/log_test.go b/plugin/loop/log_test.go new file mode 100644 index 000000000..882b5c846 --- /dev/null +++ b/plugin/loop/log_test.go @@ -0,0 +1,5 @@ +package loop + +import clog "github.com/coredns/coredns/plugin/pkg/log" + +func init() { clog.Discard() } diff --git a/plugin/loop/loop.go b/plugin/loop/loop.go new file mode 100644 index 000000000..56e039c9c --- /dev/null +++ b/plugin/loop/loop.go @@ -0,0 +1,90 @@ +package loop + +import ( + "context" + "sync" + + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +var log = clog.NewWithPlugin("loop") + +// Loop is a plugin that implements loop detection by sending a "random" query. +type Loop struct { + Next plugin.Handler + + zone string + qname string + + sync.RWMutex + i int + off bool +} + +// New returns a new initialized Loop. +func New(zone string) *Loop { return &Loop{zone: zone, qname: qname(zone)} } + +// ServeDNS implements the plugin.Handler interface. +func (l *Loop) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if r.Question[0].Qtype != dns.TypeHINFO { + return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r) + } + if l.disabled() { + return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r) + } + + state := request.Request{W: w, Req: r} + + zone := plugin.Zones([]string{l.zone}).Matches(state.Name()) + if zone == "" { + return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r) + } + + if state.Name() == l.qname { + l.inc() + } + + if l.seen() > 2 { + log.Fatalf("Seen \"HINFO IN %s\" more than twice, loop detected", l.qname) + } + + return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r) +} + +// Name implements the plugin.Handler interface. +func (l *Loop) Name() string { return "loop" } + +func (l *Loop) exchange(addr string) (*dns.Msg, error) { + m := new(dns.Msg) + m.SetQuestion(l.qname, dns.TypeHINFO) + + return dns.Exchange(m, addr) +} + +func (l *Loop) seen() int { + l.RLock() + defer l.RUnlock() + return l.i +} + +func (l *Loop) inc() { + l.Lock() + defer l.Unlock() + l.i++ +} + +func (l *Loop) setDisabled() { + l.Lock() + defer l.Unlock() + l.off = true +} + +func (l *Loop) disabled() bool { + l.RLock() + defer l.RUnlock() + return l.off +} diff --git a/plugin/loop/loop_test.go b/plugin/loop/loop_test.go new file mode 100644 index 000000000..e7a4b06bb --- /dev/null +++ b/plugin/loop/loop_test.go @@ -0,0 +1,11 @@ +package loop + +import "testing" + +func TestLoop(t *testing.T) { + l := New(".") + l.inc() + if l.seen() != 1 { + t.Errorf("Failed to inc loop, expected %d, got %d", 1, l.seen()) + } +} diff --git a/plugin/loop/setup.go b/plugin/loop/setup.go new file mode 100644 index 000000000..415a8db2f --- /dev/null +++ b/plugin/loop/setup.go @@ -0,0 +1,89 @@ +package loop + +import ( + "math/rand" + "net" + "strconv" + "time" + + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnsutil" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("loop", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + l, err := parse(c) + if err != nil { + return plugin.Error("loop", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + l.Next = next + return l + }) + + // Send query to ourselves and see if it end up with us again. + c.OnStartup(func() error { + // Another Go function, otherwise we block startup and can't send the packet. + go func() { + deadline := time.Now().Add(30 * time.Second) + conf := dnsserver.GetConfig(c) + + for time.Now().Before(deadline) { + lh := conf.ListenHosts[0] + addr := net.JoinHostPort(lh, conf.Port) + if _, err := l.exchange(addr); err != nil { + time.Sleep(1 * time.Second) + continue + } + + go func() { + time.Sleep(2 * time.Second) + l.setDisabled() + }() + } + l.setDisabled() + }() + return nil + }) + + return nil +} + +func parse(c *caddy.Controller) (*Loop, error) { + i := 0 + zone := "." + for c.Next() { + if i > 0 { + return nil, plugin.ErrOnce + } + i++ + if c.NextArg() { + return nil, c.ArgErr() + } + + if len(c.ServerBlockKeys) > 0 { + zone = plugin.Host(c.ServerBlockKeys[0]).Normalize() + } + } + return New(zone), nil +} + +// qname returns a random name. .. +func qname(zone string) string { + l1 := strconv.Itoa(r.Int()) + l2 := strconv.Itoa(r.Int()) + + return dnsutil.Join([]string{l1, l2, zone}) +} + +var r = rand.New(rand.NewSource(time.Now().UnixNano())) diff --git a/plugin/loop/setup_test.go b/plugin/loop/setup_test.go new file mode 100644 index 000000000..2a3c6846f --- /dev/null +++ b/plugin/loop/setup_test.go @@ -0,0 +1,19 @@ +package loop + +import ( + "testing" + + "github.com/mholt/caddy" +) + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", `loop`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `loop argument`) + if err := setup(c); err == nil { + t.Fatal("Expected errors, but got none") + } +} diff --git a/plugin/pkg/log/plugin.go b/plugin/pkg/log/plugin.go index 354c19d3f..1df302609 100644 --- a/plugin/pkg/log/plugin.go +++ b/plugin/pkg/log/plugin.go @@ -3,6 +3,7 @@ package log import ( "fmt" golog "log" + "os" ) // P is a logger that includes the plugin doing the logging. @@ -58,4 +59,10 @@ func (p P) Error(v ...interface{}) { p.log(err, v...) } // Errorf logs as log.Errorf. func (p P) Errorf(format string, v ...interface{}) { p.logf(err, format, v...) } +// Fatal logs as log.Fatal and calls os.Exit(1). +func (p P) Fatal(v ...interface{}) { p.log(fatal, v...); os.Exit(1) } + +// Fatalf logs as log.Fatalf and calls os.Exit(1). +func (p P) Fatalf(format string, v ...interface{}) { p.logf(fatal, format, v...); os.Exit(1) } + func pFormat(s string) string { return "plugin/" + s + ": " } diff --git a/plugin/whoami/OWNERS b/plugin/whoami/OWNERS index eee46f686..3a4ef23a1 100644 --- a/plugin/whoami/OWNERS +++ b/plugin/whoami/OWNERS @@ -1,4 +1,6 @@ reviewers: - miekg + - chrisohaver approvers: - miekg + - chrisohaver