Skip to content

Commit a04d330

Browse files
committed
Fix PQ certificate fallback on fragmented UDP paths
Try the rollover-sized certificate query over UDP first and return as soon as the small query succeeds so that a truncated response can trigger TCP fallback instead of waiting for large UDP probes to time out. We keep relayed UDP packets at the intended size after accounting for the Anonymized DNSCrypt header. When retrying through a relay over TCP, retain a rollover-sized inner query because the relay still forwards it to the resolver over UDP and applies the anti-amplification thing. Preserve the UDP RTT when TCP is used only to complete certificate retrieval, so resolver latency does not incorrectly appear to be the TCP fallback latency.
1 parent ebd5d3c commit a04d330

3 files changed

Lines changed: 60 additions & 28 deletions

File tree

dnscrypt-proxy/dnscrypt_certs.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func FetchCurrentDNSCryptCert(
7474
}
7575
if in.Truncated && proto != "tcp" {
7676
dlog.Debugf("[%v] certificate response was truncated, retrying over TCP", *serverName)
77-
if inTCP, rttTCP, _, errTCP := DNSExchange(
77+
if inTCP, _, _, errTCP := DNSExchange(
7878
proxy,
7979
"tcp",
8080
query,
@@ -83,7 +83,9 @@ func FetchCurrentDNSCryptCert(
8383
serverName,
8484
false,
8585
); errTCP == nil {
86-
in, rtt = inTCP, rttTCP
86+
// TCP completed the certificate set; the UDP RTT still represents
87+
// normal query latency when force_tcp is disabled.
88+
in = inTCP
8789
}
8890
}
8991
now := uint32(time.Now().Unix())

dnscrypt-proxy/dnsutils.go

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -372,15 +372,10 @@ type DNSExchangeResponse struct {
372372
err error
373373
}
374374

375-
// A resolver and an Anonymized DNSCrypt relay both refuse to return a UDP
376-
// response larger than the request that triggered it, so a post-quantum
377-
// certificate only comes back over UDP when the certificate probe is padded past
378-
// the response. A PQ certificate is ~1.3 KB, ~1.5 KB once paired with the classical
379-
// certificate; during a key rotation the set doubles to two classical plus two PQ
380-
// certificates, roughly 3 KB. The large probe is padded past that rollover size so
381-
// PQ discovery keeps working through relays even mid-rotation. The fragments-blocked
382-
// probe stays small on purpose, so a PQ resolver truncates it and we still learn of
383-
// PQ support through the TC bit and fall back to TCP.
375+
// The large certificate probe covers the PQ rollover set. It is tried first;
376+
// the small probe follows later to detect paths that block fragmented UDP.
377+
// Relayed TCP retries retain the large inner query because the relay forwards
378+
// the lookup over UDP and enforces amplification limits.
384379
const (
385380
certProbePaddedLen = 3200
386381
certProbeFragmentsBlockedLen = 480
@@ -422,6 +417,10 @@ func DNSExchange(
422417
}
423418
queryCopy := cloneMsg(query)
424419
queryCopy.ID += uint16(options)
420+
delay := time.Duration(250*tries) * time.Millisecond
421+
if tryFragmentsSupport {
422+
delay += 250 * time.Millisecond
423+
}
425424
go func(query *dns.Msg, delay time.Duration) {
426425
time.Sleep(delay)
427426
option := DNSExchangeResponse{err: errors.New("Canceled")}
@@ -431,9 +430,9 @@ func DNSExchange(
431430
option = _dnsExchange(proxy, proto, query, serverAddress, relay, certProbeFragmentsBlockedLen)
432431
}
433432
option.fragmentsBlocked = true
434-
option.priority = 1
433+
option.priority = 0
435434
channel <- option
436-
}(queryCopy, time.Duration(250*tries)*time.Millisecond)
435+
}(queryCopy, delay)
437436
options++
438437
}
439438
var bestOption *DNSExchangeResponse
@@ -487,21 +486,10 @@ func _dnsExchange(
487486
var rtt time.Duration
488487

489488
if proto == "udp" {
490-
qNameLen, padding := len(query.Question[0].Header().Name), 0
491-
if qNameLen < paddedLen {
492-
padding = paddedLen - qNameLen
493-
}
494-
if padding > 0 {
495-
paddingRR := &dns.PADDING{Padding: strings.Repeat("00", padding)}
496-
query.Pseudo = append(query.Pseudo, paddingRR)
497-
if query.UDPSize == 0 {
498-
query.UDPSize = uint16(MaxDNSPacketSize)
499-
}
500-
}
501-
if err := query.Pack(); err != nil {
489+
binQuery, err := packDNSExchangeQuery(query, paddedLen, relay != nil)
490+
if err != nil {
502491
return DNSExchangeResponse{err: err}
503492
}
504-
binQuery := query.Data
505493
udpAddr, err := net.ResolveUDPAddr("udp", serverAddress)
506494
if err != nil {
507495
return DNSExchangeResponse{err: err}
@@ -531,10 +519,19 @@ func _dnsExchange(
531519
rtt = time.Since(now)
532520
packet = packet[:length]
533521
} else {
534-
if err := query.Pack(); err != nil {
522+
var binQuery []byte
523+
var err error
524+
if relay == nil {
525+
err = query.Pack()
526+
binQuery = query.Data
527+
} else {
528+
// The TCP stream itself does not fragment. Padding gives the relay
529+
// enough request bytes for the full PQ response on its UDP hop.
530+
binQuery, err = packDNSExchangeQuery(query, certProbePaddedLen, false)
531+
}
532+
if err != nil {
535533
return DNSExchangeResponse{err: err}
536534
}
537-
binQuery := query.Data
538535
tcpAddr, err := net.ResolveTCPAddr("tcp", serverAddress)
539536
if err != nil {
540537
return DNSExchangeResponse{err: err}
@@ -581,3 +578,34 @@ func _dnsExchange(
581578
}
582579
return DNSExchangeResponse{response: &msg, rtt: rtt, err: nil}
583580
}
581+
582+
func packDNSExchangeQuery(query *dns.Msg, paddedLen int, viaRelay bool) ([]byte, error) {
583+
if paddedLen <= 0 {
584+
if err := query.Pack(); err != nil {
585+
return nil, err
586+
}
587+
return query.Data, nil
588+
}
589+
if viaRelay {
590+
paddedLen -= anonymizedDNSHeaderSize
591+
}
592+
if paddedLen <= 0 {
593+
return nil, errors.New("Padded length is too small for an anonymized DNS header")
594+
}
595+
596+
paddingRR := &dns.PADDING{}
597+
query.Pseudo = append(query.Pseudo, paddingRR)
598+
if query.UDPSize == 0 {
599+
query.UDPSize = uint16(MaxDNSPacketSize)
600+
}
601+
if err := query.Pack(); err != nil {
602+
return nil, err
603+
}
604+
if padding := paddedLen - len(query.Data); padding > 0 {
605+
paddingRR.Padding = strings.Repeat("00", padding)
606+
if err := query.Pack(); err != nil {
607+
return nil, err
608+
}
609+
}
610+
return query.Data, nil
611+
}

dnscrypt-proxy/proxy.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,8 @@ func (proxy *Proxy) startAcceptingClients() {
587587
proxy.localDoHListeners = nil
588588
}
589589

590+
const anonymizedDNSHeaderSize = 8 + 2 + net.IPv6len + 2
591+
590592
func (proxy *Proxy) prepareForRelay(ip net.IP, port int, encryptedQuery *[]byte) {
591593
anonymizedDNSHeader := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00}
592594
relayedQuery := append(anonymizedDNSHeader, ip.To16()...)

0 commit comments

Comments
 (0)