Skip to content

fix(server): compress responses larger than 512 bytes - #2213

Merged
0xERR0R merged 2 commits into
mainfrom
fix/compress-responses-larger-than-512
Aug 11, 2026
Merged

0xERR0R merged 2 commits into
mainfrom
fix/compress-responses-larger-than-512

Conversation

@0xERR0R

@0xERR0R 0xERR0R commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Fixes #2212.

Since #2102 the server lets Truncate decide compression, and Truncate clears Compress whenever the message already fits uncompressed. Fitting the buffer the client advertised turns out not to be sufficient: that client may be a forwarder relaying our answer to a stub resolver with a smaller buffer, and it can only truncate what it received.

A query arriving through such a forwarder carries the forwarder's OPT record, so getMaxResponseSize returns its buffer (1232 or more) rather than 512. For the reporter's name, eight A records are 562 bytes uncompressed and 202 compressed — both fit 1232, so we send the uncompressed one, and the forwarder has to truncate it for its non-EDNS0 clients. Most do that by dropping every section, so the client is left with TC and no answer at all. Before #2102 the same response went out compressed, fit in 512, and was relayed intact.

Compressing anything that wouldn't fit a bare 512-byte UDP message restores that, while keeping #2102's optimization where it pays: short responses still skip the compression map.

Reproduction

dig +noedns → dnsmasq (add-subnet=32,128, forwarding to blocky) → blocky → upstream returning 8 A records:

before
;; flags: qr tc rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
;; MSG SIZE  rcvd: 63

after
;; flags: qr rd ra; QUERY: 1, ANSWER: 8, AUTHORITY: 0, ADDITIONAL: 0
;; MSG SIZE  rcvd: 191

Both match the reporter's v0.34 and v0.31 output byte for byte.

Also fixed

The same change bounds response size in general. Sixty A records, client advertising 4096:

wire size
before 3734 B
after 1034 B

A 3734-byte UDP response fragments on a 1500-byte MTU path, and it raises the amplification factor for anything reachable from outside.

Tests

New case in server_test.go under response compression: a response that fits the advertised 1232-byte buffer uncompressed but exceeds 512 must pack to 512 bytes or less, with the answer section intact. It fails on main and passes here. The two existing cases from #2102 are unchanged — a single-A-record response still goes out uncompressed.

go test ./server/... ./resolver/... ./util/... is green.

Since #2102 the server lets Truncate decide compression, and Truncate
clears Compress whenever the message already fits uncompressed. Fitting
the buffer the client advertised is not sufficient: that client may be a
forwarder relaying our answer to a stub resolver with a smaller buffer,
and it can only truncate what it received.

A query arriving via such a forwarder carries the forwarder's OPT record,
so getMaxResponseSize returns its buffer (1232 or more) rather than 512.
An eight-record A answer for a 45-byte name is 562 bytes uncompressed and
202 compressed: both fit 1232, so we now send the uncompressed one, and
the forwarder has to truncate it for its non-EDNS0 clients -- typically by
dropping every section, leaving them with TC and no answer at all. Before
#2102 the same answer went out compressed, fit 512, and was relayed intact.

Compress anything that would not fit a bare 512-byte UDP message so it
survives that hop. Below that threshold there is nothing to gain, so the
compression-map alloc #2102 set out to avoid is still skipped for the
short responses that make up the bulk of the traffic.

Fixes #2212
Copilot AI lite review requested due to automatic review settings August 11, 2026 03:08
@0xERR0R 0xERR0R added the 🐞 bug Something isn't working label Aug 11, 2026
@0xERR0R 0xERR0R added this to the v0.35.0 milestone Aug 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 adjusts DNS response compression behavior to improve interoperability with forwarders and non‑EDNS stub resolvers by ensuring responses that exceed the classic 512‑byte UDP size get compressed even if they fit the client-advertised EDNS0 buffer.

Changes:

  • Forces dns.Msg.Compress = true when an otherwise-uncompressed response would exceed 512 bytes.
  • Adds a regression test covering the “fits EDNS0 buffer but exceeds 512 uncompressed” forwarder scenario.

Reviewed changes

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

File Description
server/client_query.go Enables compression for responses whose uncompressed size exceeds 512 bytes after truncation to the client buffer.
server/server_test.go Adds a test ensuring responses in the forwarder scenario pack to ≤ 512 bytes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread server/client_query.go
Comment on lines +99 to +103
// what it received. Compress anything that wouldn't fit a bare 512-byte UDP message so it
// survives that hop; below that there is nothing to gain and the compression map costs more
// than the bytes it would save.
if !res.Compress && res.Len() > dns.MinMsgSize {
res.Compress = true
Comment thread server/server_test.go
Comment on lines +1513 to +1520
// guard the scenario: it fits the advertised 1232 uncompressed, so Truncate
// leaves the answer complete and sees no reason to compress.
Expect(resp.Res.Truncated).Should(BeFalse())
Expect(resp.Res.Answer).Should(HaveLen(20))

packed, err := resp.Res.Pack()
Expect(err).Should(Succeed())
Expect(len(packed)).Should(BeNumerically("<=", dns.MinMsgSize))
@codecov

codecov Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.99%. Comparing base (190d512) to head (cb52d75).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2213      +/-   ##
==========================================
+ Coverage   87.98%   87.99%   +0.01%     
==========================================
  Files         126      126              
  Lines        9878     9880       +2     
==========================================
+ Hits         8691     8694       +3     
  Misses        924      924              
+ Partials      263      262       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…gful

Assert the answer really exceeds 512 bytes uncompressed, so shrinking the
RR set can't leave the test passing without exercising the scenario.
@0xERR0R
0xERR0R merged commit db8d889 into main Aug 11, 2026
22 checks passed
@0xERR0R
0xERR0R deleted the fix/compress-responses-larger-than-512 branch August 11, 2026 03:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-EDNS clients receive TC with no answers for responses that fit when compressed

2 participants