Skip to content

Commit e7af9f2

Browse files
committed
Retry HTTP/3 after transient failures.
That will cause all DoH connections to block every 30 minute, but Fixes #3258
1 parent b9f971e commit e7af9f2

1 file changed

Lines changed: 94 additions & 37 deletions

File tree

dnscrypt-proxy/xtransport.go

Lines changed: 94 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const (
5050
resolverRetryCount = 3
5151
resolverRetryInitialBackoff = 150 * time.Millisecond
5252
resolverRetryMaxBackoff = 1 * time.Second
53+
54+
DefaultHTTP3NegativeCacheTTL = 30 * time.Minute
5355
)
5456

5557
type CachedIPItem struct {
@@ -63,9 +65,18 @@ type CachedIPs struct {
6365
cache map[string]*CachedIPItem
6466
}
6567

68+
// AltSupportEntry records what we know about a host's HTTP/3 support.
69+
// A positive port means HTTP/3 works on that port. A zero port means HTTP/3 is
70+
// suppressed: temporarily until negativeUntil when that is set, or permanently
71+
// (an explicit Alt-Svc: clear) when negativeUntil is the zero value.
72+
type AltSupportEntry struct {
73+
port uint16
74+
negativeUntil time.Time
75+
}
76+
6677
type AltSupport struct {
6778
sync.RWMutex
68-
cache map[string]uint16
79+
cache map[string]AltSupportEntry
6980
}
7081

7182
type XTransport struct {
@@ -98,7 +109,7 @@ func NewXTransport() *XTransport {
98109
}
99110
xTransport := XTransport{
100111
cachedIPs: CachedIPs{cache: make(map[string]*CachedIPItem)},
101-
altSupport: AltSupport{cache: make(map[string]uint16)},
112+
altSupport: AltSupport{cache: make(map[string]AltSupportEntry)},
102113
keepAlive: DefaultKeepAlive,
103114
timeout: DefaultTimeout,
104115
bootstrapResolvers: []string{DefaultBootstrapResolver},
@@ -114,6 +125,54 @@ func NewXTransport() *XTransport {
114125
return &xTransport
115126
}
116127

128+
// loadAltSupport reports the cached HTTP/3 state for a host. An expired
129+
// temporary negative entry is reported as absent (found=false) so that HTTP/3
130+
// can be probed again and a fresh Alt-Svc header can be parsed.
131+
func (xTransport *XTransport) loadAltSupport(host string) (port uint16, found bool, negative bool) {
132+
xTransport.altSupport.RLock()
133+
entry, found := xTransport.altSupport.cache[host]
134+
xTransport.altSupport.RUnlock()
135+
if !found {
136+
return 0, false, false
137+
}
138+
if entry.port > 0 {
139+
return entry.port, true, false
140+
}
141+
if entry.negativeUntil.IsZero() || time.Now().Before(entry.negativeUntil) {
142+
return 0, true, true
143+
}
144+
return 0, false, false
145+
}
146+
147+
// saveAltSupport stores a host's HTTP/3 state with no expiry. A positive port
148+
// records HTTP/3 support; a zero port records an explicit, permanent negative,
149+
// used for Alt-Svc: clear and for hosts that turned out not to support HTTP/3.
150+
func (xTransport *XTransport) saveAltSupport(host string, port uint16) {
151+
xTransport.altSupport.Lock()
152+
xTransport.altSupport.cache[host] = AltSupportEntry{port: port}
153+
xTransport.altSupport.Unlock()
154+
}
155+
156+
// saveAltSupportNegative suppresses HTTP/3 for a host for the given duration,
157+
// after which it is retried.
158+
func (xTransport *XTransport) saveAltSupportNegative(host string, ttl time.Duration) {
159+
xTransport.altSupport.Lock()
160+
xTransport.altSupport.cache[host] = AltSupportEntry{negativeUntil: time.Now().Add(ttl)}
161+
xTransport.altSupport.Unlock()
162+
}
163+
164+
// recordHTTP3ProbeFailure caches the outcome of a failed HTTP/3 probe once the
165+
// HTTP/2 fallback has revealed the server's Alt-Svc header. A host that still
166+
// advertises h3 is suppressed only temporarily, since the probe failure was
167+
// likely transient; any other host is suppressed permanently.
168+
func (xTransport *XTransport) recordHTTP3ProbeFailure(host string, advertisesH3 bool) {
169+
if advertisesH3 {
170+
xTransport.saveAltSupportNegative(host, DefaultHTTP3NegativeCacheTTL)
171+
} else {
172+
xTransport.saveAltSupport(host, 0)
173+
}
174+
}
175+
117176
func ParseIP(ipStr string) net.IP {
118177
return net.ParseIP(strings.TrimRight(strings.TrimLeft(ipStr, "["), "]"))
119178
}
@@ -660,30 +719,24 @@ func (xTransport *XTransport) Fetch(
660719
}
661720
host, port := ExtractHostAndPort(url.Host, 443)
662721
hasAltSupport := false
722+
http3Suppressed := false
663723

664724
if xTransport.h3Transport != nil {
725+
altPort, found, negative := xTransport.loadAltSupport(url.Host)
726+
http3Suppressed = negative
665727
if xTransport.http3Probe {
666-
xTransport.altSupport.RLock()
667-
altPort, inNegativeCache := xTransport.altSupport.cache[url.Host]
668-
inNegativeCache = inNegativeCache && altPort == 0
669-
xTransport.altSupport.RUnlock()
670-
if !inNegativeCache {
728+
if !negative {
671729
client.Transport = xTransport.h3Transport
672730
dlog.Debugf("Probing HTTP/3 transport for [%s]", url.Host)
673731
} else {
674732
dlog.Debugf("Skipping HTTP/3 probe for [%s] - previously failed", url.Host)
675733
}
676734
} else {
677735
// Otherwise use traditional Alt-Svc detection
678-
xTransport.altSupport.RLock()
679-
var altPort uint16
680-
altPort, hasAltSupport = xTransport.altSupport.cache[url.Host]
681-
xTransport.altSupport.RUnlock()
682-
if hasAltSupport && altPort > 0 { // altPort > 0 ensures we're not in the negative cache
683-
if int(altPort) == port {
684-
client.Transport = xTransport.h3Transport
685-
dlog.Debugf("Using HTTP/3 transport for [%s]", url.Host)
686-
}
736+
hasAltSupport = found && !negative
737+
if hasAltSupport && altPort > 0 && int(altPort) == port {
738+
client.Transport = xTransport.h3Transport
739+
dlog.Debugf("Using HTTP/3 transport for [%s]", url.Host)
687740
}
688741
}
689742
}
@@ -731,18 +784,21 @@ func (xTransport *XTransport) Fetch(
731784
rtt := time.Since(start)
732785

733786
// Handle HTTP/3 error case - fallback to HTTP/2 when HTTP/3 fails
787+
h3ProbeFailed := false
734788
if err != nil && client.Transport == xTransport.h3Transport {
735789
if xTransport.http3Probe {
790+
// A probe is only an optimistic guess at HTTP/3 support, so the
791+
// negative-cache decision is deferred until the HTTP/2 fallback below
792+
// lets us inspect the server's Alt-Svc header.
736793
dlog.Debugf("HTTP/3 probe failed for [%s]: [%s] - falling back to HTTP/2", url.Host, err)
794+
h3ProbeFailed = true
737795
} else {
796+
// Here the host explicitly advertised Alt-Svc: h3, so the failure is
797+
// more likely transient. Suppress HTTP/3 only until the TTL expires.
738798
dlog.Debugf("HTTP/3 connection failed for [%s]: [%s] - falling back to HTTP/2", url.Host, err)
799+
xTransport.saveAltSupportNegative(url.Host, DefaultHTTP3NegativeCacheTTL)
739800
}
740801

741-
// Add server to negative cache when HTTP/3 fails
742-
xTransport.altSupport.Lock()
743-
xTransport.altSupport.cache[url.Host] = 0 // 0 port means HTTP/3 failed and should not be tried again
744-
xTransport.altSupport.Unlock()
745-
746802
// Retry with HTTP/2
747803
client.Transport = xTransport.transport
748804
if body != nil {
@@ -773,27 +829,24 @@ func (xTransport *XTransport) Fetch(
773829
return nil, statusCode, nil, rtt, err
774830
}
775831
if xTransport.h3Transport != nil && !hasAltSupport {
776-
// Check if there's entry in negative cache when using http3_probe
777-
skipAltSvcParsing := false
778-
if xTransport.http3Probe {
779-
xTransport.altSupport.RLock()
780-
altPort, inCache := xTransport.altSupport.cache[url.Host]
781-
xTransport.altSupport.RUnlock()
782-
// If server is in negative cache (altPort == 0), don't attempt to parse Alt-Svc header
783-
if inCache && altPort == 0 {
784-
dlog.Debugf("Skipping Alt-Svc parsing for [%s] - previously failed HTTP/3 probe", url.Host)
785-
skipAltSvcParsing = true
786-
}
832+
// In probe mode nothing rewrites the entry between the negative-cache read
833+
// above and here, so reuse it rather than locking and looking it up again.
834+
skipAltSvcParsing := xTransport.http3Probe && http3Suppressed
835+
if skipAltSvcParsing {
836+
dlog.Debugf("Skipping Alt-Svc parsing for [%s] - previously failed HTTP/3 probe", url.Host)
787837
}
788838

789839
if !skipAltSvcParsing {
790-
if alt, found := resp.Header["Alt-Svc"]; found {
840+
alt, found := resp.Header["Alt-Svc"]
841+
altPort := uint16(port & 0xffff)
842+
advertisesH3 := false
843+
if found {
791844
dlog.Debugf("Alt-Svc [%s]: [%s]", url.Host, alt)
792-
altPort := uint16(port & 0xffff)
793845
for i, xalt := range alt {
794846
if strings.TrimSpace(xalt) == "clear" {
795847
dlog.Debugf("Alt-Svc clear for [%s] - HTTP/3 not available", url.Host)
796848
altPort = 0
849+
advertisesH3 = false
797850
break
798851
}
799852
for j, v := range strings.Split(xalt, ";") {
@@ -806,16 +859,20 @@ func (xTransport *XTransport) Fetch(
806859
v = strings.TrimSuffix(v, "\"")
807860
if xAltPort, err := strconv.ParseUint(v, 10, 16); err == nil && xAltPort <= 65535 {
808861
altPort = uint16(xAltPort)
862+
advertisesH3 = true
809863
dlog.Debugf("Using HTTP/3 for [%s]", url.Host)
810864
break
811865
}
812866
}
813867
}
814868
}
815-
xTransport.altSupport.Lock()
816-
xTransport.altSupport.cache[url.Host] = altPort
869+
}
870+
switch {
871+
case h3ProbeFailed:
872+
xTransport.recordHTTP3ProbeFailure(url.Host, advertisesH3)
873+
case found:
874+
xTransport.saveAltSupport(url.Host, altPort)
817875
dlog.Debugf("Caching altPort for [%v]", url.Host)
818-
xTransport.altSupport.Unlock()
819876
}
820877
}
821878
}

0 commit comments

Comments
 (0)