doq: configure QUIC keep-alive and retry on idle timeout

Pass a quic.Config with KeepAlivePeriod (15s) to DoQ dial calls instead
of nil, so pooled connections send periodic QUIC PINGs to stay alive and
detect dead paths proactively.

Also add IdleTimeoutError to the DoQ retry conditions alongside io.EOF,
so stale pooled connections trigger a transparent retry instead of
propagating as a query failure.
This commit is contained in:
Codescribe
2026-04-02 11:47:20 -04:00
committed by Cuong Manh Le
parent 839b8236e7
commit eaa171f66f
+24 -13
View File
@@ -42,11 +42,12 @@ const doqPoolSize = 16
// doqConnPool manages a pool of QUIC connections for DoQ queries using a buffered channel. // doqConnPool manages a pool of QUIC connections for DoQ queries using a buffered channel.
type doqConnPool struct { type doqConnPool struct {
uc *UpstreamConfig uc *UpstreamConfig
addrs []string addrs []string
port string port string
tlsConfig *tls.Config tlsConfig *tls.Config
conns chan *doqConn quicConfig *quic.Config
conns chan *doqConn
} }
type doqConn struct { type doqConn struct {
@@ -65,12 +66,17 @@ func newDOQConnPool(uc *UpstreamConfig, addrs []string) *doqConnPool {
ServerName: uc.Domain, ServerName: uc.Domain,
} }
quicConfig := &quic.Config{
KeepAlivePeriod: 15 * time.Second,
}
pool := &doqConnPool{ pool := &doqConnPool{
uc: uc, uc: uc,
addrs: addrs, addrs: addrs,
port: port, port: port,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
conns: make(chan *doqConn, doqPoolSize), quicConfig: quicConfig,
conns: make(chan *doqConn, doqPoolSize),
} }
// Use SetFinalizer here because we need to call a method on the pool itself. // Use SetFinalizer here because we need to call a method on the pool itself.
@@ -85,12 +91,17 @@ func newDOQConnPool(uc *UpstreamConfig, addrs []string) *doqConnPool {
// Resolve performs a DNS query using a pooled QUIC connection. // Resolve performs a DNS query using a pooled QUIC connection.
func (p *doqConnPool) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { func (p *doqConnPool) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
// Retry logic for io.EOF errors (as per original implementation) // Retry logic for transient errors: io.EOF (connection reset) and
// IdleTimeoutError (stale pooled connection timed out).
for range 5 { for range 5 {
answer, err := p.doResolve(ctx, msg) answer, err := p.doResolve(ctx, msg)
if err == io.EOF { if err == io.EOF {
continue continue
} }
var idleErr *quic.IdleTimeoutError
if errors.As(err, &idleErr) {
continue
}
if err != nil { if err != nil {
return nil, wrapCertificateVerificationError(err) return nil, wrapCertificateVerificationError(err)
} }
@@ -226,7 +237,7 @@ func (p *doqConnPool) dialConn(ctx context.Context) (string, *quic.Conn, error)
udpConn.Close() udpConn.Close()
return "", nil, err return "", nil, err
} }
conn, err := quic.DialEarly(ctx, udpConn, remoteAddr, p.tlsConfig, nil) conn, err := quic.DialEarly(ctx, udpConn, remoteAddr, p.tlsConfig, p.quicConfig)
if err != nil { if err != nil {
udpConn.Close() udpConn.Close()
return "", nil, err return "", nil, err
@@ -241,7 +252,7 @@ func (p *doqConnPool) dialConn(ctx context.Context) (string, *quic.Conn, error)
} }
pd := &quicParallelDialer{} pd := &quicParallelDialer{}
conn, err := pd.Dial(ctx, dialAddrs, p.tlsConfig, nil) conn, err := pd.Dial(ctx, dialAddrs, p.tlsConfig, p.quicConfig)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }