Skip to content

Conversation

@bootjp
Copy link

@bootjp bootjp commented Feb 3, 2026

This PR fixes two related DoQ parsing bugs: the client response path could mix stale pooled bytes into responses on partial reads (surfacing as rare “wrong domain” A/AAAA answers), and the QUIC server request path could parse beyond the actual read length, allowing leftover pooled bytes to influence request parsing.

What changed

  • upstream/doq.go: Switched readMsg to ReadFull the 2‑byte length prefix, validate it, ReadFull the exact message bytes, and unpack only those bytes.
  • upstream/doq_internal_test.go: Added TestUpstreamDoQ_readMsg_partialRead to simulate 1‑byte chunk reads with a prefilled pool buffer.
  • proxy/serverquic.go: Unpack only the bytes actually read (buf[2:n] / buf[:n]) to avoid stale pooled bytes affecting request parsing.
  • proxy/serverquic_internal_test.go: Added TestQuicProxy_truncatedRequestUsesOnlyReadBytes to simulate a truncated request with a prefilled pool buffer.

Why this matters
DoQ messages are length‑prefixed and stream reads are not guaranteed to return a full message in one call. When combined with buffer pooling, ignoring the length or the actual read size can let leftover bytes be parsed as part of a different message, causing incorrect responses or unintended request handling. These changes make DoQ parsing deterministic and resilient to partial reads and pooled buffers.

Test

  • go test ./upstream -run TestUpstreamDoQ_readMsg_partialRead
  • go test ./proxy -run TestQuicProxy_truncatedRequestUsesOnlyReadBytes

@bootjp bootjp marked this pull request as ready for review February 4, 2026 13:17
Copilot AI review requested due to automatic review settings February 4, 2026 13:17
Copy link

@windsurf-bot windsurf-bot bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 To request another review, post a new comment with "/windsurf-review".

Comment on lines +420 to +425
msgLen := int(binary.BigEndian.Uint16(lenBuf[:]))
if msgLen == 0 {
return nil, fmt.Errorf("empty response from %s", p.addr)
} else if msgLen > dns.MaxMsgSize {
return nil, fmt.Errorf("response size %d exceeds max %d from %s", msgLen, dns.MaxMsgSize, p.addr)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important fix: The previous implementation was ignoring the length prefix and potentially processing stale data from the buffer pool. This change properly validates the message length and ensures we only unpack the actual message bytes.

// does not support receiving multiple messages over a single connection.
m = new(dns.Msg)
err = m.Unpack(respBuf[2:])
err = m.Unpack(respBuf)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct fix for the unpacking logic. Since we're now reading the length prefix separately and then reading exactly the message bytes into the buffer, we should unpack the entire buffer rather than skipping the first 2 bytes.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a subtle DoQ (DNS-over-QUIC) response parsing bug where partial reads combined with buffer pooling could cause stale data from previous queries to contaminate current responses, leading to incorrect DNS records appearing in answers.

Changes:

  • Updated readMsg to properly handle the 2-byte length prefix by using io.ReadFull for deterministic reading
  • Added length validation to ensure responses are within valid bounds
  • Added comprehensive regression test that simulates partial reads with pooled buffers containing stale data

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
upstream/doq.go Introduced quicStream interface for testability and rewrote readMsg to properly read and respect the RFC 9250 length prefix, eliminating the stale-buffer issue
upstream/doq_internal_test.go Added TestUpstreamDoQ_readMsg_partialRead regression test with mock stream that simulates partial reads and pre-filled buffer pool to validate the fix

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

When the QUIC server parsed requests it passed the entire pooled
buffer to dns.Msg.Unpack, even though only n bytes were read. This
could let leftover bytes from previous requests influence parsing.
Tighten the slice to the actual read length and add a regression
test that simulates a truncated request with a prefilled pool buffer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant