New plugin: loop (#1989)

* New plugin: loop

Add a plugin that detects loops. It does this by sending an unique query
to our selves. If we see the query more than twice we stop the process.
If there isn't a loop, the plugin disables it self and becomes a noop
plugin.

Signed-off-by: Miek Gieben <miek@miek.nl>
This commit is contained in:
Miek Gieben 2018-07-20 19:45:17 +01:00 committed by GitHub
parent 547f155465
commit 84ec780ffc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 272 additions and 0 deletions

View file

@ -39,6 +39,7 @@ var Directives = []string{
"auto",
"secondary",
"etcd",
"loop",
"forward",
"proxy",
"erratic",

View file

@ -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"

View file

@ -48,6 +48,7 @@ file:file
auto:auto
secondary:secondary
etcd:etcd
loop:loop
forward:forward
proxy:proxy
erratic:erratic

6
plugin/loop/OWNERS Normal file
View file

@ -0,0 +1,6 @@
reviewers:
- miekg
- chrisohaver
approvers:
- miekg
- chrisohaver

40
plugin/loop/README.md Normal file
View file

@ -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 `<random number>.<random number>.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
~~~

5
plugin/loop/log_test.go Normal file
View file

@ -0,0 +1,5 @@
package loop
import clog "github.com/coredns/coredns/plugin/pkg/log"
func init() { clog.Discard() }

90
plugin/loop/loop.go Normal file
View file

@ -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
}

11
plugin/loop/loop_test.go Normal file
View file

@ -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())
}
}

89
plugin/loop/setup.go Normal file
View file

@ -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. <rand.Int()>.<rand.Int().<zone>.
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()))

19
plugin/loop/setup_test.go Normal file
View file

@ -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")
}
}

View file

@ -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 + ": " }

View file

@ -1,4 +1,6 @@
reviewers:
- miekg
- chrisohaver
approvers:
- miekg
- chrisohaver