Files
coredns/plugin/forward/setup_test.go

936 lines
30 KiB
Go
Raw Normal View History

package forward
import (
"context"
"fmt"
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-10 02:38:22 +02:00
"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)
}
}
}
}
2018-04-13 17:02:54 +01:00
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-10 02:38:22 +02:00
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)
}
}
}
2018-04-13 17:02:54 +01:00
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'"},
2018-04-13 17:02:54 +01:00
{`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", ""},
2018-04-13 17:02:54 +01:00
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
fs, err := parseForward(c)
2018-04-13 17:02:54 +01:00
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
2018-04-13 17:02:54 +01:00
}
/*
if len(fs) == 0 {
continue
}
*/
2018-04-13 17:02:54 +01:00
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)
2018-04-13 17:02:54 +01:00
}
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)
}
2018-04-13 17:02:54 +01:00
}
}
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)
}
}
}
}
perf(proxy): use mutex-based connection pool (#7790) * perf(proxy): use mutex-based connection pool The proxy package (used for example by the forward plugin) utilized an actor model where a single connManager goroutine managed connection pooling via unbuffered channels (dial, yield, ret). This design serialized all connection acquisition and release operations through a single goroutine, creating a bottleneck under high concurrency. This was observable as a performance degradation when using a single upstream backend compared to multiple backends (which sharded the bottleneck). Changes: - Removed dial, yield, and ret channels from the Transport struct. - Removed the connManager goroutine's request processing loop. - Implemented Dial() and Yield() using a sync.Mutex to protect the connection slice, allowing for fast concurrent access without context switching. - Downgraded connManager to a simple background cleanup loop that only handles connection expiration on a ticker. - Updated plugin/pkg/proxy/connect.go to use direct method calls instead of channel sends. - Updated tests to reflect the removal of internal channels. Benchmarks show that this change eliminates the single-backend bottleneck. Now a single upstream backend performs on par with multiple backends, and overall throughput is improved. The implementation aligns with standard Go patterns for connection pooling (e.g., net/http.Transport). Signed-off-by: Ville Vesilehto <ville@vesilehto.fi> * fix: address PR review for persistent.go - Named mutex field instead of embedding, to not expose Lock() and Unlock() - Move stop check outside of lock in Yield() - Close() without a separate goroutine - Change stop channel to struct Signed-off-by: Ville Vesilehto <ville@vesilehto.fi> * fix: address code review feedback for conn pool - Switch from LIFO to FIFO connection selection for source port diversity, reducing DNS cache poisoning risk (RFC 5452). - Remove "clear entire cache" optimization as it was LIFO-specific. FIFO naturally iterates and skips expired connections. - Remove all goroutines for closing connections; collect connections while holding lock, close synchronously after releasing lock. Signed-off-by: Ville Vesilehto <ville@vesilehto.fi> * fix: remove unused error consts No longer utilised after refactoring the channel based approach. Signed-off-by: Ville Vesilehto <ville@vesilehto.fi> * feat(forward): add max_idle_conns option Add configurable connection pool limit for the forward plugin via the max_idle_conns Corefile option. Changes: - Add SetMaxIdleConns to proxy - Add maxIdleConns field to Forward struct - Add max_idle_conns parsing in forward plugin setup - Apply setting to each proxy during configuration - Update forward plugin README with new option By default the value is 0 (unbounded). When set, excess connections returned to the pool are closed immediately rather than cached. Also add a yield related test. Signed-off-by: Ville Vesilehto <ville@vesilehto.fi> * chore(proxy): simple Dial by closing conns inline Remove toClose slice collection to reduce complexity. Instead close expired connections directly while iterating. Reduces complexity with negligible lock-time impact. Signed-off-by: Ville Vesilehto <ville@vesilehto.fi> * chore: fewer explicit Unlock calls Cleaner and less chance of forgetting to unlock on new possible code paths. Signed-off-by: Ville Vesilehto <ville@vesilehto.fi> --------- Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
2026-01-14 03:49:46 +02:00
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)
}
})
}
}
plugin/forward: add max_age option to enforce an absolute connection lifetime (#7903) * plugin/pkg/proxy: add max_age for per-connection lifetime cap Introduce a max_age setting on Transport that closes connections based on creation time, independent of idle-timeout (expire). Background: PR #7790 changed the connection pool from LIFO to FIFO for source-port diversity. Under FIFO, every connection is cycled through the pool and its used timestamp is refreshed continuously. When request rate is high enough that pool_size / request_rate < expire, no connection ever becomes idle and expire never fires. This prevents CoreDNS from opening new connections to upstreams that scale out (e.g. new Kubernetes pods behind a ClusterIP service with conntrack pinning). max_age addresses this by enforcing an absolute upper bound on connection lifetime regardless of activity: - persistConn gains a created field set at dial time. - Transport gains maxAge (default 0 = unlimited, preserving existing behaviour). - Dial(): rejects cached connections whose creation age exceeds max_age. - cleanup(): when maxAge > 0, uses a linear scan over both idle-timeout and max-age predicates; when maxAge == 0, preserves the original binary-search path on used time (sorted by FIFO insertion order). - Both hot paths pre-compute the deadline outside any inner loop to avoid repeated time.Now() calls. Tests added: - TestMaxAgeExpireByCreation: connection with old created but fresh used must be rejected even though idle-timeout would pass. - TestMaxAgeFIFORotation: three FIFO-rotated connections (old created, fresh used) must all be rejected, confirming that continuous rotation cannot prevent max-age expiry. Signed-off-by: cangming <cangming@cangming.app> * plugin/forward: add max_age option Expose Transport.SetMaxAge through the forward plugin so operators can set an absolute upper bound on connection lifetime via the Corefile. Usage: forward . 1.2.3.4 { max_age 30s } Default is 0 (unlimited), which preserves existing behaviour. A positive value causes connections older than max_age to be closed and re-dialled on the next request, ensuring CoreDNS reconnects to newly scaled-out upstream pods even under sustained load where the idle timeout (expire) would never fire. If max_age is set, it must not be less than expire; the parser rejects this combination at startup with a clear error message. Signed-off-by: cangming <cangming@cangming.app> --------- Signed-off-by: cangming <cangming@cangming.app>
2026-03-10 02:50:03 +08:00
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)
}
})
}
}
func TestSetupDOHHealthcheckTLSConfig(t *testing.T) {
c := caddy.NewTestController(
"dns",
`forward . https://127.0.0.1 {
tls_servername dns.example
}`,
)
fs, err := parseForward(c)
if err != nil {
t.Fatal(err)
}
got := fs[0].
proxies[0].
GetHealthchecker().
GetTLSConfig()
if got.ServerName != "dns.example" {
t.Fatalf(
"Expected DoH healthcheck TLS server name %q, got %q",
"dns.example",
got.ServerName,
)
}
}