diff --git a/plugin/forward/README.md b/plugin/forward/README.md index 7dd66f768a5..bcfb4f35510 100644 --- a/plugin/forward/README.md +++ b/plugin/forward/README.md @@ -50,6 +50,7 @@ forward FROM TO... { policy random|round_robin|sequential health_check DURATION [no_rec] [domain FQDN] max_concurrent MAX + next RCODE_1 [RCODE_2] [RCODE_3...] } ~~~ @@ -95,6 +96,7 @@ forward FROM TO... { response does not count as a health failure. When choosing a value for **MAX**, pick a number at least greater than the expected *upstream query rate* * *latency* of the upstream servers. As an upper bound for **MAX**, consider that each concurrent query will use about 2kb of memory. +* `next` If the `RCODE` (i.e. `NXDOMAIN`) is returned by the remote then execute the next plugin. If no next plugin is defined, or the next plugin is not a `forward` plugin, this setting is ignored Also note the TLS config is "global" for the whole forwarding proxy if you need a different `tls_servername` for different upstreams you're out of luck. @@ -268,6 +270,21 @@ Or when you have multiple DoT upstreams with different `tls_servername`s, you ca } ~~~ +The following would try 1.2.3.4 first. If the response is `NXDOMAIN`, try 5.6.7.8. If the response from 5.6.7.8 is `NXDOMAIN`, try 9.0.1.2. + +~~~ corefile +. { + forward . 1.2.3.4 { + next NXDOMAIN + } + forward . 5.6.7.8 { + next NXDOMAIN + } + forward . 9.0.1.2 { + } +} +~~~ + ## See Also [RFC 7858](https://tools.ietf.org/html/rfc7858) for DNS over TLS. diff --git a/plugin/forward/forward.go b/plugin/forward/forward.go index d8bbe7ab9ce..cb22391e2e9 100644 --- a/plugin/forward/forward.go +++ b/plugin/forward/forward.go @@ -43,6 +43,8 @@ type Forward struct { from string ignored []string + nextAlternateRcodes []int + tlsConfig *tls.Config tlsServerName string maxfails uint32 @@ -194,6 +196,15 @@ func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg return 0, nil } + // Check if we have an alternate Rcode defined, check if we match on the code + for _, alternateRcode := range f.nextAlternateRcodes { + if alternateRcode == ret.Rcode && f.Next != nil { // In case we do not have a Next handler, just continue normally + if _, ok := f.Next.(*Forward); ok { // Only continue if the next forwarder is also a Forworder + return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r) + } + } + } + w.WriteMsg(ret) return 0, nil } diff --git a/plugin/forward/setup.go b/plugin/forward/setup.go index 5341b7e60bf..e8211abf80d 100644 --- a/plugin/forward/setup.go +++ b/plugin/forward/setup.go @@ -6,6 +6,7 @@ import ( "fmt" "path/filepath" "strconv" + "strings" "time" "github.com/coredns/caddy" @@ -289,7 +290,22 @@ func parseBlock(c *caddy.Controller, f *Forward) error { } f.ErrLimitExceeded = errors.New("concurrent queries exceeded maximum " + c.Val()) f.maxConcurrent = int64(n) + case "next": + args := c.RemainingArgs() + if len(args) == 0 { + return c.ArgErr() + } + + for _, rcode := range args { + var rc int + var ok bool + if rc, ok = dns.StringToRcode[strings.ToUpper(rcode)]; !ok { + return fmt.Errorf("%s is not a valid rcode", rcode) + } + + f.nextAlternateRcodes = append(f.nextAlternateRcodes, rc) + } default: return c.Errf("unknown property '%s'", c.Val()) } diff --git a/plugin/forward/setup_test.go b/plugin/forward/setup_test.go index 95f642f6843..c39cd6c85ee 100644 --- a/plugin/forward/setup_test.go +++ b/plugin/forward/setup_test.go @@ -342,3 +342,44 @@ func TestMultiForward(t *testing.T) { t.Error("expected third plugin to be last, but Next is not nil") } } +func TestNextAlternate(t *testing.T) { + testsValid := []struct { + input string + expected []int + }{ + {"forward . 127.0.0.1 {\nnext NXDOMAIN\n}\n", []int{dns.RcodeNameError}}, + {"forward . 127.0.0.1 {\nnext SERVFAIL\n}\n", []int{dns.RcodeServerFailure}}, + {"forward . 127.0.0.1 {\nnext NXDOMAIN SERVFAIL\n}\n", []int{dns.RcodeNameError, dns.RcodeServerFailure}}, + {"forward . 127.0.0.1 {\nnext NXDOMAIN SERVFAIL REFUSED\n}\n", []int{dns.RcodeNameError, dns.RcodeServerFailure, dns.RcodeRefused}}, + } + for i, test := range testsValid { + c := caddy.NewTestController("dns", test.input) + f, err := parseForward(c) + forward := f[0] + if err != nil { + t.Errorf("Test %d: %v", i, err) + } + if len(forward.nextAlternateRcodes) != len(test.expected) { + t.Errorf("Test %d: expected %d next rcodes, got %d", i, len(test.expected), len(forward.nextAlternateRcodes)) + } + for j, rcode := range forward.nextAlternateRcodes { + if rcode != test.expected[j] { + t.Errorf("Test %d: expected next rcode %d, got %d", i, test.expected[j], rcode) + } + } + } + + testsInvalid := []string{ + "forward . 127.0.0.1 {\nnext\n}\n", + "forward . 127.0.0.1 {\nnext INVALID\n}\n", + "forward . 127.0.0.1 {\nnext NXDOMAIN INVALID\n}\n", + } + for i, test := range testsInvalid { + c := caddy.NewTestController("dns", test) + _, err := parseForward(c) + if err == nil { + t.Errorf("Test %d: expected error, got nil", i) + } + } + +}