Add alternate option to forward plugin (#6681)

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 #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>
This commit is contained in:
Jasper Bernhardt 2024-07-01 17:20:12 +02:00 committed by GitHub
parent 3f388442cc
commit 2e9986c622
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 84 additions and 0 deletions

View file

@ -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.

View file

@ -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
}

View file

@ -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())
}

View file

@ -342,3 +342,43 @@ 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)
}
}
}