diff --git a/plugin/forward/README.md b/plugin/forward/README.md index 7dd66f768a5..4c0a0d3d044 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 + alternate 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. +* `alternate` If the `RCODE` (i.e. `NXDOMAIN`) is returned by the remote then execute the next plugin. If no next plugin is defined 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 { + alternate NXDOMAIN + } + forward . 5.6.7.8 { + alternate 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..555077cd0fa 100644 --- a/plugin/forward/forward.go +++ b/plugin/forward/forward.go @@ -43,6 +43,8 @@ type Forward struct { from string ignored []string + alternateRcodes []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.alternateRcodes { + 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..4cc1321aecd 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 "alternate": + 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.alternateRcodes = append(f.alternateRcodes, 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..d8a630a3a4f 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 TestAlternate(t *testing.T) { + testsValid := []struct { + input string + expected []int + }{ + {"forward . 127.0.0.1 {\nalternate NXDOMAIN\n}\n", []int{dns.RcodeNameError}}, + {"forward . 127.0.0.1 {\nalternate SERVFAIL\n}\n", []int{dns.RcodeServerFailure}}, + {"forward . 127.0.0.1 {\nalternate NXDOMAIN SERVFAIL\n}\n", []int{dns.RcodeNameError, dns.RcodeServerFailure}}, + {"forward . 127.0.0.1 {\nalternate 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.alternateRcodes) != len(test.expected) { + t.Errorf("Test %d: expected %d alternate rcodes, got %d", i, len(test.expected), len(forward.alternateRcodes)) + } + for j, rcode := range forward.alternateRcodes { + if rcode != test.expected[j] { + t.Errorf("Test %d: expected alternate rcode %d, got %d", i, test.expected[j], rcode) + } + } + } + + testsInvalid := []string{ + "forward . 127.0.0.1 {\nalternate\n}\n", + "forward . 127.0.0.1 {\nalternate INVALID\n}\n", + "forward . 127.0.0.1 {\nalternate 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) + } + } + +}