Files
coredns/plugin/forward/dot_test.go
2026-09-18 16:56:31 -07:00

200 lines
5.8 KiB
Go

package forward
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/coredns/caddy"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
)
// stalledTLSListener accepts TCP but withholds the TLS handshake on the first
// connections. Later connections are served normally by dns.Server.
type stalledTLSListener struct {
net.Listener
config *tls.Config
stalls int64
accepted atomic.Int64
firstAccepted chan struct{}
closed chan error
wg sync.WaitGroup
}
func (l *stalledTLSListener) Accept() (net.Conn, error) {
for {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
n := l.accepted.Add(1)
if n == 1 {
close(l.firstAccepted)
}
if n > l.stalls {
return tls.Server(conn, l.config), nil
}
l.wg.Go(func() {
defer conn.Close()
// Release an unbounded old client after the forwarding window has elapsed.
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
_, err := io.Copy(io.Discard, conn)
l.closed <- err
})
}
}
func TestForwardTLSHandshakeRetry(t *testing.T) {
originalTimeout := defaultTimeout
defaultTimeout = 800 * time.Millisecond
t.Cleanup(func() { defaultTimeout = originalTimeout })
for _, tc := range []struct {
name string
setting string
stalls int64
wantConns int64
wantError bool
tcp bool
deadline time.Duration
cancel bool
health bool
}{
{name: "default", stalls: 1, wantConns: 2, health: true},
{name: "TCP downstream", stalls: 1, wantConns: 2, tcp: true},
{name: "explicit two attempts", setting: "max_connect_attempts 2", stalls: 1, wantConns: 2},
{name: "unlimited attempts", setting: "max_connect_attempts 0", stalls: 1, wantConns: 2},
{name: "single attempt", setting: "max_connect_attempts 1", stalls: 1, wantConns: 1, wantError: true},
{name: "all handshakes stall", stalls: 2, wantConns: 2, wantError: true},
{name: "caller deadline", stalls: 1, wantConns: 2, deadline: 500 * time.Millisecond},
{name: "caller canceled", stalls: 1, wantConns: 1, wantError: true, cancel: true},
} {
t.Run(tc.name, func(t *testing.T) {
serverTLS, roots := makeForwardDoQTestTLS(t)
serverTLS.NextProtos = nil
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
upstream := &stalledTLSListener{
Listener: listener, config: serverTLS, stalls: tc.stalls,
closed: make(chan error, tc.stalls), firstAccepted: make(chan struct{}),
}
started := make(chan struct{})
server := &dns.Server{
Listener: upstream,
Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
response := new(dns.Msg)
response.SetReply(r)
response.Answer = []dns.RR{test.A("example.org. 60 IN A 192.0.2.53")}
w.WriteMsg(response)
}),
NotifyStartedFunc: func() { close(started) },
}
serverDone := make(chan error, 1)
go func() { serverDone <- server.ActivateAndServe() }()
<-started
t.Cleanup(func() {
server.Shutdown()
if err := <-serverDone; err != nil {
t.Error(err)
}
upstream.wg.Wait()
})
controller := caddy.NewTestController("dns", fmt.Sprintf(`forward . tls://%s {
tls_servername doq.test
max_fails 0
%s
}`, listener.Addr(), tc.setting))
fs, err := parseForward(controller)
if err != nil {
t.Fatal(err)
}
f := fs[0]
p := f.proxies[0]
config := p.GetTransport().GetTLSConfig().Clone()
config.RootCAs = roots
p.SetTLSConfig(config)
// Manage the cache explicitly without background health-check connections.
runtime.SetFinalizer(p, nil)
p.GetTransport().Start()
t.Cleanup(p.GetTransport().Stop)
ctx := t.Context()
if tc.deadline != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, tc.deadline)
defer cancel()
} else if tc.cancel {
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(ctx)
defer cancel()
go func() {
select {
case <-upstream.firstAccepted:
cancel()
case <-ctx.Done():
}
}()
}
msg := new(dns.Msg)
msg.SetQuestion("example.org.", dns.TypeA)
msg.Id = 1234
recorder := dnstest.NewRecorder(&test.ResponseWriter{TCP: tc.tcp})
rcode, err := f.ServeDNS(ctx, recorder, msg)
if tc.wantError {
if err == nil || rcode != dns.RcodeServerFailure {
t.Fatalf("rcode = %d, error = %v, want SERVFAIL and an error", rcode, err)
}
} else {
if err != nil || rcode != 0 {
t.Fatalf("rcode = %d, error = %v, want a successful retry", rcode, err)
}
if recorder.Msg == nil || recorder.Msg.Id != 1234 || len(recorder.Msg.Answer) != 1 ||
recorder.Msg.Answer[0].String() != "example.org.\t60\tIN\tA\t192.0.2.53" {
t.Fatalf("unexpected response: %v", recorder.Msg)
}
// A successful TLS connection must survive cancellation of its dial context.
if _, err := f.ServeDNS(t.Context(), recorder, msg); err != nil {
t.Fatalf("cached TLS connection failed: %v", err)
}
}
if got := upstream.accepted.Load(); got != tc.wantConns {
t.Errorf("accepted %d connections, want %d", got, tc.wantConns)
}
if msg.Id != 1234 {
t.Errorf("request ID = %d, want 1234", msg.Id)
}
for range min(tc.stalls, upstream.accepted.Load()) {
select {
case err := <-upstream.closed:
if err != nil {
t.Errorf("stalled connection was not closed by the client: %v", err)
}
case <-time.After(time.Second):
t.Fatal("stalled connection was not closed")
}
}
if tc.health {
if err := p.GetHealthchecker().Check(p); err != nil {
t.Fatalf("TLS health check failed after recovery: %v", err)
}
if p.Fails() != 0 || upstream.accepted.Load() != tc.wantConns+1 {
t.Fatal("health check did not use a fresh, successful TLS connection")
}
}
})
}
}