Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fallback plugin (DNS endpoints only) #1398

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/dnsserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ func (s *Server) Tracer() ot.Tracer {
}

// DefaultErrorFunc responds to an DNS request with an error.
func DefaultErrorFunc(w dns.ResponseWriter, r *dns.Msg, rc int) {
var DefaultErrorFunc = func(w dns.ResponseWriter, r *dns.Msg, rc int) {
state := request.Request{W: w, Req: r}

answer := new(dns.Msg)
Expand Down
1 change: 1 addition & 0 deletions core/dnsserver/zdirectives.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var Directives = []string{
"secondary",
"etcd",
"proxy",
"fallback",
"erratic",
"whoami",
"startup",
Expand Down
1 change: 1 addition & 0 deletions core/plugin/zplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
_ "github.com/coredns/coredns/plugin/erratic"
_ "github.com/coredns/coredns/plugin/errors"
_ "github.com/coredns/coredns/plugin/etcd"
_ "github.com/coredns/coredns/plugin/fallback"
_ "github.com/coredns/coredns/plugin/federation"
_ "github.com/coredns/coredns/plugin/file"
_ "github.com/coredns/coredns/plugin/health"
Expand Down
1 change: 1 addition & 0 deletions plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ auto:auto
secondary:secondary
etcd:etcd
proxy:proxy
fallback:fallback
erratic:erratic
whoami:whoami
startup:github.com/mholt/caddy/startupshutdown
Expand Down
32 changes: 32 additions & 0 deletions plugin/fallback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# fallback

## Name

*fallback* - send failed DNS queries to fallback endpoints.

## Description

This plugin sends failed DNS quieres (e.g., NXDOMAIN, SERVFAIL, etc)
to fallback endpoints.

## Syntax

~~~ txt
fallback [ZONE] {
on [FAILURE] [ENDPOINTS...]
}
~~~

* **ZONE** the name of the domain to be accessed.
* **FAILURE** the failure code of the DNS queries (NXDOMAIN, SERVFAIL, etc.).
* **ENDPOINTS** the fallback endpoints to send to.

## Examples

~~~ corefile
. {
fallback example.org {
on NXDOMAIN 10.10.10.10:53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe exemple with several failures cases - to show the full usage.

on NXDOMAIN
on SERVFAIL
on .. ???

is there a list of all events that can happen and we can use ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes : SERVFAIL, NXDOMAIN, ERROR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should NXDOMAIN be DENIAL ? or is there a NODATA ?

}
}
~~~
42 changes: 42 additions & 0 deletions plugin/fallback/fallback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package fallback

import (
"log"

"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/request"

"github.com/miekg/dns"
"golang.org/x/net/context"
)

type processor struct {
rcode int
endpoint string
}

func (p *processor) ErrorFunc(w dns.ResponseWriter, r *dns.Msg, rc int) error {
if rc == p.rcode {
state := request.Request{W: w, Req: r}
qname := state.Name()
log.Printf("[INFO] Send fallback %q to %q", qname, p.endpoint)
_, err := dns.Exchange(r, p.endpoint)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand we use directly Exchange as most of plugin are using Proxy.newLookup(...).
What I see is that in proxy (that is calling exchange), there is a management of the TTL of the query. We do not care here in case of error ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Knowing that this ErrorFunc can now be called whenever in the ServeDNS chain, including before any plugin is called, would it be safe to verify that the request is correctly defined ?

see in server.ServeDNS:

	if r == nil || len(r.Question) == 0 {
		DefaultErrorFunc(w, r, dns.RcodeServerFailure)
		return
	}

return err
}
return nil
}

// Fallback is a plugin that provide fallback in case of error
type Fallback struct {
Next plugin.Handler
zones []string
funcs []processor
}

// ServeDNS implements the plugin.Handler interface.
func (f Fallback) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
}

// Name implements the Handler interface.
func (f Fallback) Name() string { return "fallback" }
86 changes: 86 additions & 0 deletions plugin/fallback/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package fallback

import (
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/request"

"github.com/mholt/caddy"
"github.com/miekg/dns"
)

func init() {
caddy.RegisterPlugin("fallback", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}

func setup(c *caddy.Controller) error {
f := Fallback{}

for c.Next() {
f.zones = c.RemainingArgs()
if len(f.zones) == 0 {
f.zones = make([]string, len(c.ServerBlockKeys))
copy(f.zones, c.ServerBlockKeys)
}
plugin.Zones(f.zones).Normalize()

for c.NextBlock() {
switch c.Val() {
case "on":
args := c.RemainingArgs()
if len(args) < 2 {
return c.Errf("unknown property '%v'", args)
}
rcode := 0
switch args[0] {
case "SERVFAIL":
rcode = dns.RcodeServerFailure
case "NXDOMAIN":
rcode = dns.RcodeNameError
case "REFUSED":
rcode = dns.RcodeRefused
default:
return c.Errf("unknown property '%v'", args)
}
for _, arg := range args[1:] {
f.funcs = append(f.funcs, processor{
rcode: rcode,
endpoint: arg,
})
}

default:
return c.Errf("unknown property '%s'", c.Val())
}
}
}

c.OnStartup(func() error {
defaultErrorFunc := dnsserver.DefaultErrorFunc
dnsserver.DefaultErrorFunc = func(w dns.ResponseWriter, r *dns.Msg, rc int) {
state := request.Request{W: w, Req: r}
qname := state.Name()

zone := plugin.Zones(f.zones).Matches(qname)
if zone != "" {
for i := range f.funcs {
if err := f.funcs[i].ErrorFunc(w, r, rc); err == nil {
break
}
}
}
defaultErrorFunc(w, r, rc)
}
return nil
})

dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
f.Next = next
return f
})

return nil
}
21 changes: 21 additions & 0 deletions plugin/fallback/setup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package fallback

import (
"testing"

"github.com/mholt/caddy"
)

func TestSetupFallback(t *testing.T) {
c := caddy.NewTestController("dns", `fallback`)
if err := setup(c); err != nil {
t.Fatalf("Expected no errors, but got: %v", err)
}

c = caddy.NewTestController("dns", `fallback example.org {
on NXDOMAIN 10.10.10.10:100 8.8.8.8:53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test with several "on XXXX" ?
test with file (/etc/resolv.conf)

}`)
if err := setup(c); err != nil {
t.Fatalf("Expected no errors, but got: %v", err)
}
}
46 changes: 46 additions & 0 deletions test/fallback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package test

import (
"bytes"
"log"
"strings"
"testing"

"github.com/coredns/coredns/plugin/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"

"github.com/miekg/dns"
)

func TestFallbackLookup(t *testing.T) {
corefile := `fallback.org:0 {
fallback fallback.org {
on SERVFAIL 127.0.0.1:999
}
}`

i, udp, _, err := CoreDNSServerAndPorts(corefile)
if err != nil {
t.Fatalf("Could not get CoreDNS serving instance: %s", err)
}
defer i.Stop()

var b bytes.Buffer
log.SetOutput(&b)

p := proxy.NewLookup([]string{udp})
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}

resp, err := p.Lookup(state, "nop.fallback.org.", dns.TypeA)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
// expect no answer section
if len(resp.Answer) != 0 {
t.Fatalf("Expected no RR in the answer section, got %d", len(resp.Answer))
}
if !strings.Contains(b.String(), `[INFO] Send fallback "nop.fallback.org." to "127.0.0.1:999"`) {
t.Fatal("Expected to receive log but didn't")
}
}