From 8288fb0d5d18798f24b55d4faae0f16a158390fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E3=81=84=E7=86=8A?= Date: Fri, 17 Jul 2026 16:12:37 +0200 Subject: [PATCH] doh: keep pooled conns shorter-lived than server idle-timeouts; h2 PING health-checks Some resolvers close idle DoH connections well under 30s (Quad9 measured at <=30s from the same LAN; Cloudflare survives >240s). With the 3m IdleConnTimeout, any DNS lull left only dead connections in the pool and subsequent queries were written into them, hanging ~10s and surfacing as 'Post ...: unexpected EOF' / http-status 502 bursts (maxEOFTries retries often just picked the next corpse). Downstream this expired alg mappings (realips([])), killed established flows of allowed apps, and failed the OS connectivity probes routed through the tunnel. - IdleConnTimeout 3m -> 10s: never reuse a connection older than the shortest observed server-side idle window. - http2.ConfigureTransports + ReadIdleTimeout/PingTimeout: actively evict half-dead h2 connections via PING instead of losing a query. --- intra/doh/doh.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/intra/doh/doh.go b/intra/doh/doh.go index 9b2157cb..dc3a8bd1 100644 --- a/intra/doh/doh.go +++ b/intra/doh/doh.go @@ -54,6 +54,7 @@ import ( "github.com/celzero/firestack/intra/xdns" "github.com/cloudflare/odoh-go" "github.com/miekg/dns" + "golang.org/x/net/http2" ) const ( @@ -304,16 +305,29 @@ func asDialContext(d protect.DialFn) func(context.Context, string, string) (net. } func h2(d protect.DialFn, c *tls.Config) *http.Transport { - return &http.Transport{ - DialContext: asDialContext(d), - ForceAttemptHTTP2: true, - IdleConnTimeout: 3 * time.Minute, + tr := &http.Transport{ + DialContext: asDialContext(d), + ForceAttemptHTTP2: true, + // Some resolvers close idle DoH connections well under 30s (Quad9 measured at <=30s + // from a fixed network; Cloudflare survives >240s). With the previous 3m pool, any DNS + // lull longer than the server's idle window left only dead connections in the pool, and + // subsequent queries were written into them — hanging ~10s and dying with + // "unexpected EOF" / http-status 502 bursts (maxEOFTries retries often just pick the next + // corpse). Keep pooled connections shorter-lived than any observed server-side idle window. + IdleConnTimeout: 10 * time.Second, TLSHandshakeTimeout: 7 * time.Second, // Android's DNS-over-TLS sets it to 30s ResponseHeaderTimeout: 20 * time.Second, // SNI (hostname) must always be inferred from http-request TLSClientConfig: c, } + // Health-check h2 connections with PING frames so half-dead ones are evicted instead of + // eating a query first (net/http does not do this by default). + if t2, err := http2.ConfigureTransports(tr); err == nil && t2 != nil { + t2.ReadIdleTimeout = 10 * time.Second + t2.PingTimeout = 5 * time.Second + } + return tr } // always called from a go-routine