Skip to content

Commit

Permalink
Add alternate option to forward plugin
Browse files Browse the repository at this point in the history
Allows the forward plugin to execute the next plugin based on the return code. Similar to the externally mainted alternate plugin https://github.com/coredns/alternate

Based on the idea of chrisohaver@ in coredns#6549 (comment)
Also incoperated the request to rename `alternate` to `next` as an option

I am having issues adding a proper test for functionality. Primarily, I do not know the code base enough and having multiple `dnstest.NewServer` with ResponseWriter does not work. From my testing these are "Singletons'' and only the last defined response writer is used for all servers

Signed-off-by: Jasper Bernhardt <jasper.bernhardt@live.de>
  • Loading branch information
dihmandrake committed Jun 5, 2024
1 parent e196de3 commit 386f068
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 0 deletions.
17 changes: 17 additions & 0 deletions plugin/forward/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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...]
}
~~~

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
11 changes: 11 additions & 0 deletions plugin/forward/forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ type Forward struct {
from string
ignored []string

nextAlternateRcodes []int

tlsConfig *tls.Config
tlsServerName string
maxfails uint32
Expand Down Expand Up @@ -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
}
Expand Down
16 changes: 16 additions & 0 deletions plugin/forward/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/coredns/caddy"
Expand Down Expand Up @@ -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())
}
Expand Down
41 changes: 41 additions & 0 deletions plugin/forward/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

}

0 comments on commit 386f068

Please sign in to comment.