mirror of
https://github.com/coredns/coredns.git
synced 2026-10-09 12:05:22 -04:00
200 lines
5.8 KiB
Go
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")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|