mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-31 02:03:20 -04:00 
			
		
		
		
	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:
		| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user