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:
parent
547f155465
commit
84ec780ffc
12 changed files with 272 additions and 0 deletions
|
@ -39,6 +39,7 @@ var Directives = []string{
|
|||
"auto",
|
||||
"secondary",
|
||||
"etcd",
|
||||
"loop",
|
||||
"forward",
|
||||
"proxy",
|
||||
"erratic",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
6
plugin/loop/OWNERS
Normal file
|
@ -0,0 +1,6 @@
|
|||
reviewers:
|
||||
- miekg
|
||||
- chrisohaver
|
||||
approvers:
|
||||
- miekg
|
||||
- chrisohaver
|
40
plugin/loop/README.md
Normal file
40
plugin/loop/README.md
Normal 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
5
plugin/loop/log_test.go
Normal 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
90
plugin/loop/loop.go
Normal 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
11
plugin/loop/loop_test.go
Normal 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
89
plugin/loop/setup.go
Normal 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
19
plugin/loop/setup_test.go
Normal 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")
|
||||
}
|
||||
}
|
|
@ -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 + ": " }
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
reviewers:
|
||||
- miekg
|
||||
- chrisohaver
|
||||
approvers:
|
||||
- miekg
|
||||
- chrisohaver
|
||||
|
|
Loading…
Add table
Reference in a new issue