Files
coredns/plugin/forward/setup_test.go
Filippo125 e4990abfa3 feat(forward): add source_address directive (#8011)
* fix(dnssec): avoid caching empty signing results (#7996)

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* save only

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* do #8008

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* Fix Address used if tcp

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* Fix bad using of dialer type

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* core: Add full TSIG verification in gRPC transport (#8006)

* core: Add full TSIG verification in gRPC transport

This PR add full TSIG verification in gRPC using dns.TsigVerify() so invalid signatures and timestamps are correctly detected instead of only checking key presence.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

* Fix

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

* Fix

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

---------

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* core: Add full TSIG verification in QUIC transport (#8007)

* core: Add full TSIG verification in QUIC transport

This PR add full TSIG verification in QUIC using dns.TsigVerify()

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

* Fix

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

---------

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* fix(test): deduplicate TSIG test helpers (#8009)

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* fix(dnssec): return nil sigs on sign error (#7999)

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* fix(dnssec): return nil from ParseKeyFile on error (#8000)

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* fix(dnsserver): allow view server blocks in any declaration order (#8001)

When using the view plugin, filtered and unfiltered server blocks can
share the same zone and port. The zone overlap validation rejected this
configuration when the unfiltered block was not declared last, because
filtered configs treated an already-registered zone as an error.

Skip the 'already defined' check for configs that have filter functions,
since they are expected to coexist with an unfiltered catch-all block on
the same zone/port.

Fixes #7733

Signed-off-by: umut-polat <52835619+umut-polat@users.noreply.github.com>
Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* fix(doh): use per-connection local address for PROXY protocol (#8005)

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* fix(transfer): batch AXFR records by message size instead of count (#8002)

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* fix(tls): use temp dir for keylog test path (#8010)

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* Rename local_address option to source_address

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* Rename local_address also in readme
Add test

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* Resolve change request in pr

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* fix ci lint

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* Improve doc on source_address routing needs

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* Remove added timeout

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* add use of source address also for health check query

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* remove untrailing newline from health_test.go

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* fix file format

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* Update plugin/forward/setup_test.go

Co-authored-by: Ville Vesilehto <ville@vesilehto.fi>
Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* Remove dead code in TestHealthLocalAddress

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* Fix misspelling

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* Try to set default timeout

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

* fix format in health.go

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>

---------

Signed-off-by: Filippo <filippo.ferrazini@gmail.com>
Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
Signed-off-by: umut-polat <52835619+umut-polat@users.noreply.github.com>
Co-authored-by: Ville Vesilehto <ville@vesilehto.fi>
Co-authored-by: Yong Tang <yong.tang.github@outlook.com>
Co-authored-by: Umut Polat <52835619+umut-polat@users.noreply.github.com>
Co-authored-by: Cedric Wang <wangzongqi@msn.com>
2026-07-09 17:38:22 -07:00

909 lines
29 KiB
Go

package forward
import (
"context"
"fmt"
"net"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/proxy"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
)
func TestSetup(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedFrom string
expectedIgnored []string
expectedFails uint32
expectedOpts proxy.Options
expectedErr string
}{
// positive
{"forward . 127.0.0.1", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . 127.0.0.1 {\nhealth_check 0.5s domain example.org\n}\n", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "example.org."}, ""},
{"forward . 127.0.0.1 {\nexcept miek.nl\n}\n", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . 127.0.0.1 {\nmax_fails 3\n}\n", false, ".", nil, 3, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . 127.0.0.1 {\nforce_tcp\n}\n", false, ".", nil, 2, proxy.Options{ForceTCP: true, HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . 127.0.0.1 {\nprefer_udp\n}\n", false, ".", nil, 2, proxy.Options{PreferUDP: true, HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . 127.0.0.1 {\nforce_tcp\nprefer_udp\n}\n", false, ".", nil, 2, proxy.Options{PreferUDP: true, ForceTCP: true, HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . 127.0.0.1:53", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . 127.0.0.1:8080", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . [::1]:53", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . [2003::1]:53", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . 127.0.0.1 \n", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward 10.9.3.0/18 127.0.0.1", false, "0.9.10.in-addr.arpa.", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{`forward . ::1
forward com ::2`, false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "plugin"},
{"forward . tls://[2400:3200::1%dns.alidns.com]:853 {\ntls\n}\n", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . https://127.0.0.1 \n", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
// negative
{"forward . https://1.1.1.1/ \n", true, "", nil, 0, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "paths are not allowed in HTTPS upstream addresses"},
{"forward . a27.0.0.1", true, "", nil, 0, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "failed to resolve"},
{"forward . 127.0.0.1 {\nblaatl\n}\n", true, "", nil, 0, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "unknown property"},
{"forward . 127.0.0.1 {\nhealth_check 0.5s domain\n}\n", true, "", nil, 0, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "Wrong argument count or unexpected line ending after 'domain'"},
{"forward xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 127.0.0.1 \n", true, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "unable to normalize 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Fatalf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if !test.shouldErr {
f := fs[0]
if f.from != test.expectedFrom {
t.Errorf("Test %d: expected: %s, got: %s", i, test.expectedFrom, f.from)
}
if test.expectedIgnored != nil {
if !reflect.DeepEqual(f.ignored, test.expectedIgnored) {
t.Errorf("Test %d: expected: %q, actual: %q", i, test.expectedIgnored, f.ignored)
}
}
if f.maxfails != test.expectedFails {
t.Errorf("Test %d: expected: %d, got: %d", i, test.expectedFails, f.maxfails)
}
if f.opts != test.expectedOpts {
t.Errorf("Test %d: expected: %v, got: %v", i, test.expectedOpts, f.opts)
}
}
}
}
func TestSourceAddress(t *testing.T) {
tests := []struct {
input string
expectedSourceAddress net.IP
expectedErr string
}{
{"forward . 127.0.0.1 {\nsource_address 192.0.2.1\n}\n", net.ParseIP("192.0.2.1"), ""},
{"forward . 127.0.0.1 {\nsource_address not-an-ip\n}\n", nil, "invalid IP address"},
{"forward . 127.0.0.1 {\nsource_address 2001:0db8:85a3:0000:1319:8a2e:0370:7344\n}\n", net.ParseIP("2001:0db8:85a3:0000:1319:8a2e:0370:7344"), ""},
{"forward . 127.0.0.1 {\nsource_address ::ffff:192.0.2.1\n}\n", net.ParseIP("192.0.2.1"), ""},
{"forward . 127.0.0.1 {\nsource_address \n}\n", nil, "Error during parsing: Wrong argument count or unexpected line ending after 'source_address'"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if test.expectedErr != "" && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if test.expectedErr == "" {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if test.expectedErr == "" {
f := fs[0]
if !test.expectedSourceAddress.Equal(f.sourceAddress) {
t.Errorf("Test %d: expected: %v, got: %v", i, test.expectedSourceAddress, f.sourceAddress)
}
}
}
}
func TestSplitZone(t *testing.T) {
tests := []struct {
input string
expectedHost string
expectedZone string
}{
{
"tls://127.0.0.1%example.net:854", "tls://127.0.0.1:854", "example.net",
}, {
"tls://127.0.0.1%example.net", "tls://127.0.0.1", "example.net",
}, {
"tls://127.0.0.1:854", "tls://127.0.0.1:854", "",
}, {
"dns://127.0.0.1", "dns://127.0.0.1", "",
}, {
"foo%bar:baz", "foo:baz", "bar",
}, {
"tls://[::1%example.net]:853", "tls://[::1]:853", "example.net",
},
}
for i, test := range tests {
host, zone := splitZone(test.input)
if host != test.expectedHost {
t.Errorf("Test %d: expected host %q, actual: %q", i, test.expectedHost, host)
}
if zone != test.expectedZone {
t.Errorf("Test %d: expected zone %q, actual: %q", i, test.expectedZone, zone)
}
}
}
func TestSetupTLS(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedServerName string
expectedErr string
}{
// positive
{`forward . tls://127.0.0.1 {
tls_servername dns
}`, false, "dns", ""},
{`forward . tls://127.0.0.1%example.net {
tls
}`, false, "example.net", ""},
{`forward . tls://127.0.0.1%example.net:854 tls://127.0.0.2%example.net tls://fe80::1%example.com {
tls
}`, false, "example.net", ""},
{`forward . tls://127.0.0.1%example.net:854 {
tls
}`, false, "example.net", ""},
// SNI specifications clash test
{`forward . tls://127.0.0.1%example.net:854 {
tls_servername foo
}`, true, "", "both forward ('foo') and proxy level ('example.net') TLS servernames are set for upstream proxy 'tls://127.0.0.1:854'"},
{`forward . 127.0.0.1 {
tls_servername dns
}`, false, "", ""},
{`forward . 127.0.0.1 {
tls
}`, false, "", ""},
{`forward . tls://127.0.0.1`, false, "", ""},
{`forward . tls://[2400:3200::1%dns.alidns.com]:853 {
tls
}`, false, "dns.alidns.com", ""},
{`forward . tls://[2400:3200::1]:853 {
tls
tls_servername dns.alidns.com
}`, false, "dns.alidns.com", ""},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
continue
}
/*
if len(fs) == 0 {
continue
}
*/
f := fs[0]
if !test.shouldErr && test.expectedServerName != "" && test.expectedServerName != f.proxies[0].GetTransport().GetTLSConfig().ServerName {
t.Errorf("Test %d: expected server name: %q, actual: %q", i, test.expectedServerName, f.proxies[0].GetTransport().GetTLSConfig().ServerName)
}
if !test.shouldErr && test.expectedServerName != "" && test.expectedServerName != f.proxies[0].GetHealthchecker().GetTLSConfig().ServerName {
t.Errorf("Test %d: expected server name: %q, actual: %q", i, test.expectedServerName, f.proxies[0].GetHealthchecker().GetTLSConfig().ServerName)
}
}
}
func TestSetupTLSclientSessionCacheCount(t *testing.T) {
tests := []struct {
input string
}{
{`forward . tls://127.0.0.1%foo tls://127.0.0.2%foo tls://127.0.0.3%foo tls://127.0.0.4%bar tls://127.0.0.5%bar { }`},
{`forward . tls://127.0.0.1%foo tls://127.0.0.2%foo tls://127.0.0.3%bar tls://127.0.0.4%bar tls://127.0.0.5%bar { }`},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if err != nil {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if fs[0].proxies[0].GetTransport().GetTLSConfig() == fs[0].proxies[len(fs[0].proxies)-1].GetTransport().GetTLSConfig() {
t.Errorf("Test %d: tlsConfig is the same for both the first and last proxies", i)
}
if fs[0].proxies[0].GetTransport().GetTLSConfig() != fs[0].proxies[1].GetTransport().GetTLSConfig() {
t.Errorf("Test %d: tlsConfig differs for the first two proxies", i)
}
if fs[0].proxies[len(fs[0].proxies)-1].GetTransport().GetTLSConfig() != fs[0].proxies[len(fs[0].proxies)-2].GetTransport().GetTLSConfig() {
t.Errorf("Test %d: tlsConfig differs for the last two proxies", i)
}
}
}
func TestSetupResolvconf(t *testing.T) {
const resolv = "resolv.conf"
if err := os.WriteFile(resolv,
[]byte(`nameserver 10.10.255.252
nameserver 10.10.255.253`), 0666); err != nil {
t.Fatalf("Failed to write resolv.conf file: %s", err)
}
defer os.Remove(resolv)
const resolvIPV6 = "resolv-ipv6.conf"
if err := os.WriteFile(resolvIPV6,
[]byte(`nameserver 0388:d254:7aec:6892:9f7f:e93b:5806:1b0f%en0`), 0666); err != nil {
t.Fatalf("Failed to write %v file: %s", resolvIPV6, err)
}
defer os.Remove(resolvIPV6)
const emptyResolv = "empty.conf"
if err := os.WriteFile(emptyResolv,
[]byte(`# nameserver 1.1.1.1
# nameserver 1.0.0.1`), 0666); err != nil {
t.Fatalf("Failed to write empty.conf file: %s", err)
}
defer os.Remove(emptyResolv)
tests := []struct {
input string
shouldErr bool
expectedErr string
expectedNames []string
}{
// pass
{`forward . ` + resolv, false, "", []string{"10.10.255.252:53", "10.10.255.253:53"}},
// fail
{`forward . /dev/null`, true, "no valid upstream addresses found", nil},
// IPV6 with local zone
{`forward . ` + resolvIPV6, false, "", []string{"[0388:d254:7aec:6892:9f7f:e93b:5806:1b0f]:53"}},
// pass when empty forward file is found
{`forward . ` + emptyResolv + ` 127.0.0.1`, false, "", []string{"127.0.0.1:53"}},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
continue
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if test.shouldErr {
continue
}
f := fs[0]
for j, n := range test.expectedNames {
addr := f.proxies[j].Addr()
if n != addr {
t.Errorf("Test %d, expected %q, got %q", j, n, addr)
}
}
for _, p := range f.proxies {
p.Healthcheck() // this should almost always err, we don't care it shouldn't crash
}
}
}
func TestSetupMaxConcurrent(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedVal int64
expectedErr string
}{
// positive
{"forward . 127.0.0.1 {\nmax_concurrent 1000\n}\n", false, 1000, ""},
// negative
{"forward . 127.0.0.1 {\nmax_concurrent many\n}\n", true, 0, "invalid"},
{"forward . 127.0.0.1 {\nmax_concurrent -4\n}\n", true, 0, "negative"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if test.shouldErr {
continue
}
f := fs[0]
if f.maxConcurrent != test.expectedVal {
t.Errorf("Test %d: expected: %d, got: %d", i, test.expectedVal, f.maxConcurrent)
}
}
}
func TestSetupMaxConnectAttempts(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedVal uint32
expectedErr string
}{
{"forward . 127.0.0.1 {\n}\n", false, 0, ""},
{"forward . 127.0.0.1 {\nmax_connect_attempts 5\n}\n", false, 5, ""},
{"forward . 127.0.0.1 {\nmax_connect_attempts many\n}\n", true, 0, "invalid"},
{"forward . 127.0.0.1 {\nmax_connect_attempts -4\n}\n", true, 0, "invalid"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if !test.shouldErr {
f := fs[0]
if f.maxConnectAttempts != test.expectedVal {
t.Errorf("Test %d: expected: %d, got: %d", i, test.expectedVal, f.maxConnectAttempts)
}
}
}
}
func TestSetupMaxIdleConns(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedVal int
expectedErr string
}{
{"forward . 127.0.0.1\n", false, 0, ""},
{"forward . 127.0.0.1 {\nmax_idle_conns 10\n}\n", false, 10, ""},
{"forward . 127.0.0.1 {\nmax_idle_conns 0\n}\n", false, 0, ""},
{"forward . 127.0.0.1 {\nmax_idle_conns many\n}\n", true, 0, "invalid"},
{"forward . 127.0.0.1 {\nmax_idle_conns -1\n}\n", true, 0, "negative"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found none for input %s", i, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if test.shouldErr {
continue
}
f := fs[0]
if f.maxIdleConns != test.expectedVal {
t.Errorf("Test %d: expected: %d, got: %d", i, test.expectedVal, f.maxIdleConns)
}
}
}
func TestSetupHealthCheck(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedRecVal bool
expectedDomain string
expectedErr string
}{
// positive
{"forward . 127.0.0.1\n", false, true, ".", ""},
{"forward . 127.0.0.1 {\nhealth_check 0.5s\n}\n", false, true, ".", ""},
{"forward . 127.0.0.1 {\nhealth_check 0.5s no_rec\n}\n", false, false, ".", ""},
{"forward . 127.0.0.1 {\nhealth_check 0.5s no_rec domain example.org\n}\n", false, false, "example.org.", ""},
{"forward . 127.0.0.1 {\nhealth_check 0.5s domain example.org\n}\n", false, true, "example.org.", ""},
{"forward . 127.0.0.1 {\nhealth_check 0.5s domain .\n}\n", false, true, ".", ""},
{"forward . 127.0.0.1 {\nhealth_check 0.5s domain example.org.\n}\n", false, true, "example.org.", ""},
// negative
{"forward . 127.0.0.1 {\nhealth_check no_rec\n}\n", true, true, ".", "time: invalid duration"},
{"forward . 127.0.0.1 {\nhealth_check domain example.org\n}\n", true, true, "example.org", "time: invalid duration"},
{"forward . 127.0.0.1 {\nhealth_check 0.5s rec\n}\n", true, true, ".", "health_check: unknown option rec"},
{"forward . 127.0.0.1 {\nhealth_check 0.5s domain\n}\n", true, true, ".", "Wrong argument count or unexpected line ending after 'domain'"},
{"forward . 127.0.0.1 {\nhealth_check 0.5s domain example..org\n}\n", true, true, ".", "health_check: invalid domain name"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if test.shouldErr {
continue
}
f := fs[0]
if f.opts.HCRecursionDesired != test.expectedRecVal || f.proxies[0].GetHealthchecker().GetRecursionDesired() != test.expectedRecVal ||
f.opts.HCDomain != test.expectedDomain || f.proxies[0].GetHealthchecker().GetDomain() != test.expectedDomain || !dns.IsFqdn(f.proxies[0].GetHealthchecker().GetDomain()) {
t.Errorf("Test %d: expectedRec: %v, got: %v. expectedDomain: %s, got: %s. ", i, test.expectedRecVal, f.opts.HCRecursionDesired, test.expectedDomain, f.opts.HCDomain)
}
}
}
func TestMultiForward(t *testing.T) {
input := `
forward 1st.example.org 10.0.0.1
forward 2nd.example.org 10.0.0.2
forward 3rd.example.org 10.0.0.3
`
c := caddy.NewTestController("dns", input)
setup(c)
dnsserver.NewServer("", []*dnsserver.Config{dnsserver.GetConfig(c)})
handlers := dnsserver.GetConfig(c).Handlers()
f1, ok := handlers[0].(*Forward)
if !ok {
t.Fatalf("expected first plugin to be Forward, got %v", reflect.TypeOf(handlers[0]))
}
if f1.from != "1st.example.org." {
t.Errorf("expected first forward from \"1st.example.org.\", got %q", f1.from)
}
if f1.Next == nil {
t.Fatal("expected first forward to point to next forward instance, not nil")
}
f2, ok := f1.Next.(*Forward)
if !ok {
t.Fatalf("expected second plugin to be Forward, got %v", reflect.TypeOf(f1.Next))
}
if f2.from != "2nd.example.org." {
t.Errorf("expected second forward from \"2nd.example.org.\", got %q", f2.from)
}
if f2.Next == nil {
t.Fatal("expected second forward to point to third forward instance, got nil")
}
f3, ok := f2.Next.(*Forward)
if !ok {
t.Fatalf("expected third plugin to be Forward, got %v", reflect.TypeOf(f2.Next))
}
if f3.from != "3rd.example.org." {
t.Errorf("expected third forward from \"3rd.example.org.\", got %q", f3.from)
}
if f3.Next != nil {
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)
}
}
}
func TestFailfastAllUnhealthyUpstreams(t *testing.T) {
tests := []struct {
input string
expectedRecVal bool
expectedErr string
}{
// positive
{"forward . 127.0.0.1\n", false, ""},
{"forward . 127.0.0.1 {\nfailfast_all_unhealthy_upstreams\n}\n", true, ""},
// negative
{"forward . 127.0.0.1 {\nfailfast_all_unhealthy_upstreams false\n}\n", false, "Wrong argument count"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if err != nil {
if test.expectedErr == "" {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
} else {
if test.expectedErr != "" {
t.Errorf("Test %d: expected error but found no error for input %s", i, test.input)
}
}
if test.expectedErr != "" {
continue
}
f := fs[0]
if f.failfastUnhealthyUpstreams != test.expectedRecVal {
t.Errorf("Test %d: Expected Rec:%v, got:%v", i, test.expectedRecVal, f.failfastUnhealthyUpstreams)
}
}
}
func TestFailover(t *testing.T) {
server_fail_s := dnstest.NewMultipleServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetRcode(r, dns.RcodeServerFailure)
w.WriteMsg(ret)
})
defer server_fail_s.Close()
server_refused_s := dnstest.NewMultipleServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetRcode(r, dns.RcodeRefused)
w.WriteMsg(ret)
})
defer server_refused_s.Close()
s := dnstest.NewMultipleServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
ret.Answer = append(ret.Answer, test.A("example.org. IN A 127.0.0.1"))
w.WriteMsg(ret)
})
defer s.Close()
tests := []struct {
input string
hasRecord bool
failMsg string
}{
{fmt.Sprintf(
`forward . %s %s %s {
policy sequential
failover ServFail Refused
}`, server_fail_s.Addr, server_refused_s.Addr, s.Addr), true, "If failover is set, records should be returned as long as one of the upstreams is work"},
{fmt.Sprintf(
`forward . %s %s %s {
policy sequential
}`, server_fail_s.Addr, server_refused_s.Addr, s.Addr), false, "If failover is not set and the first upstream is not work, no records should be returned"},
{fmt.Sprintf(
`forward . %s %s %s {
policy sequential
}`, s.Addr, server_fail_s.Addr, server_refused_s.Addr), true, "Although failover is not set, as long as the first upstream is work, there should be has a record return"},
}
for i, testCase := range tests {
c := caddy.NewTestController("dns", testCase.input)
fs, err := parseForward(c)
f := fs[0]
if err != nil {
t.Errorf("Test #%d: Failed to create forwarder: %s", i, err)
}
f.OnStartup()
defer f.OnShutdown()
// Reduce per-upstream read timeout to make the test fit within the
// per-query deadline defaultTimeout of 5 seconds.
for _, p := range f.proxies {
p.SetReadTimeout(500 * time.Millisecond)
}
m := new(dns.Msg)
m.SetQuestion("example.org.", dns.TypeA)
rec := dnstest.NewRecorder(&test.ResponseWriter{})
if _, err := f.ServeDNS(context.TODO(), rec, m); err != nil {
t.Fatalf("Test #%d: Expected to receive reply, but didn't", i)
}
if (len(rec.Msg.Answer) > 0) != testCase.hasRecord {
t.Errorf("Test #%d: %s: \n %s", i, testCase.failMsg, testCase.input)
}
}
}
func TestFailoverValidation(t *testing.T) {
cases := []struct {
name string
input string
wantError string
}{
{
name: "NoErrorDisallowed",
input: `forward . 127.0.0.1 {
failover NOERROR
}`,
wantError: "NoError cannot be used in failover",
},
{
name: "InvalidRcode",
input: `forward . 127.0.0.1 {
failover NOT_A_VALID_RCODE
}`,
wantError: "not a valid rcode",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := caddy.NewTestController("dns", tc.input)
_, err := parseForward(c)
if err == nil {
t.Fatalf("expected error for %s, got nil", tc.name)
}
if !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("expected error to contain %q, got: %v", tc.wantError, err)
}
})
}
}
func TestSetupMaxAge(t *testing.T) {
tests := []struct {
name string
input string
shouldErr bool
expectedVal time.Duration
expectedErr string
}{
{
name: "default (no max_age)",
input: "forward . 127.0.0.1\n",
expectedVal: 0,
},
{
name: "valid max_age",
input: "forward . 127.0.0.1 {\nmax_age 30s\n}\n",
expectedVal: 30 * time.Second,
},
{
name: "max_age equal to expire",
input: "forward . 127.0.0.1 {\nexpire 10s\nmax_age 10s\n}\n",
expectedVal: 10 * time.Second,
},
{
name: "max_age zero (unlimited)",
input: "forward . 127.0.0.1 {\nmax_age 0s\n}\n",
expectedVal: 0,
},
{
name: "negative max_age",
input: "forward . 127.0.0.1 {\nmax_age -1s\n}\n",
shouldErr: true,
expectedErr: "negative",
},
{
name: "invalid max_age value",
input: "forward . 127.0.0.1 {\nmax_age invalid\n}\n",
shouldErr: true,
expectedErr: "invalid",
},
{
name: "max_age less than expire",
input: "forward . 127.0.0.1 {\nexpire 30s\nmax_age 10s\n}\n",
shouldErr: true,
expectedErr: "max_age",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if test.shouldErr {
if err == nil {
t.Errorf("expected error but found none for input %s", test.input)
return
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("expected error to contain %q, got: %v", test.expectedErr, err)
}
return
}
if err != nil {
t.Errorf("expected no error but found: %v", err)
return
}
if fs[0].maxAge != test.expectedVal {
t.Errorf("expected maxAge %v, got %v", test.expectedVal, fs[0].maxAge)
}
})
}
}
func TestSetupReadTimeout(t *testing.T) {
tests := []struct {
name string
input string
shouldErr bool
expectedVal time.Duration
expectedErr string
}{
{
name: "default (no read_timeout)",
input: "forward . 127.0.0.1\n",
expectedVal: defaultReadTimeout,
},
{
name: "valid read_timeout",
input: "forward . 127.0.0.1 {\nread_timeout 5s\n}\n",
expectedVal: 5 * time.Second,
},
{
name: "sub-second read_timeout",
input: "forward . 127.0.0.1 {\nread_timeout 500ms\n}\n",
expectedVal: 500 * time.Millisecond,
},
{
name: "zero read_timeout",
input: "forward . 127.0.0.1 {\nread_timeout 0s\n}\n",
shouldErr: true,
expectedErr: "positive",
},
{
name: "negative read_timeout",
input: "forward . 127.0.0.1 {\nread_timeout -1s\n}\n",
shouldErr: true,
expectedErr: "positive",
},
{
name: "invalid read_timeout value",
input: "forward . 127.0.0.1 {\nread_timeout invalid\n}\n",
shouldErr: true,
expectedErr: "invalid",
},
{
name: "missing read_timeout value",
input: "forward . 127.0.0.1 {\nread_timeout\n}\n",
shouldErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
if test.shouldErr {
if err == nil {
t.Errorf("expected error but found none for input %s", test.input)
return
}
if test.expectedErr != "" && !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("expected error to contain %q, got: %v", test.expectedErr, err)
}
return
}
if err != nil {
t.Errorf("expected no error but found: %v", err)
return
}
if fs[0].readTimeout != test.expectedVal {
t.Errorf("expected readTimeout %v, got %v", test.expectedVal, fs[0].readTimeout)
}
})
}
}