net/dns/resolver: set TC flag when UDP responses exceed size limits (#18157)

The forwarder was not setting the Truncated (TC) flag when UDP DNS
responses exceeded either the EDNS buffer size (if present) or the
RFC 1035 default 512-byte limit. This affected DoH, TCP fallback,
and UDP response paths.

The fix ensures checkResponseSizeAndSetTC is called in all code paths
that return UDP responses, enforcing both EDNS and default UDP size
limits.

Added comprehensive unit tests and consolidated duplicate test helpers.

Updates #18107

Signed-off-by: Brendan Creane <bcreane@gmail.com>
This commit is contained in:
Brendan Creane
2026-01-30 17:52:54 -08:00
committed by GitHub
parent b4d39e2fd9
commit 8cac8b117b
4 changed files with 672 additions and 82 deletions
+99
View File
@@ -1572,3 +1572,102 @@ func TestServfail(t *testing.T) {
t.Errorf("response was %X, want %X", pkt, wantPkt)
}
}
// TestLocalResponseTCFlagIntegration tests that checkResponseSizeAndSetTC is
// correctly applied to local DNS responses through the Resolver.Query integration path.
// This complements the unit test in forwarder_test.go by verifying the end-to-end behavior.
func TestLocalResponseTCFlagIntegration(t *testing.T) {
r := newResolver(t)
defer r.Close()
r.SetConfig(dnsCfg)
tests := []struct {
name string
query []byte
family string
wantTCSet bool
desc string
}{
{
name: "UDP_small_local_response_no_TC",
query: dnspacket("test1.ipn.dev.", dns.TypeA, noEdns),
family: "udp",
wantTCSet: false,
desc: "Small local response (< 512 bytes) should not have TC flag set",
},
{
name: "TCP_local_response_no_TC",
query: dnspacket("test1.ipn.dev.", dns.TypeA, noEdns),
family: "tcp",
wantTCSet: false,
desc: "TCP queries should skip TC flag setting (even for large responses)",
},
{
name: "UDP_EDNS_request_small_response",
query: dnspacket("test1.ipn.dev.", dns.TypeA, 1500),
family: "udp",
wantTCSet: false,
desc: "Small response with EDNS request should not have TC flag set",
},
{
name: "UDP_IPv6_response_no_TC",
query: dnspacket("test2.ipn.dev.", dns.TypeAAAA, noEdns),
family: "udp",
wantTCSet: false,
desc: "Small IPv6 local response should not have TC flag set",
},
{
name: "UDP_reverse_lookup_no_TC",
query: dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR, noEdns),
family: "udp",
wantTCSet: false,
desc: "Small reverse lookup response should not have TC flag set",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
response, err := r.Query(context.Background(), tt.query, tt.family, netip.AddrPort{})
if err != nil {
t.Fatalf("Query failed: %v", err)
}
if len(response) < headerBytes {
t.Fatalf("Response too small: %d bytes", len(response))
}
hasTC := truncatedFlagSet(response)
if hasTC != tt.wantTCSet {
t.Errorf("%s: TC flag = %v, want %v (response size=%d bytes)", tt.desc, hasTC, tt.wantTCSet, len(response))
}
// Verify response is valid by parsing it (if possible)
// Note: unpackResponse may not support all record types (e.g., PTR)
parsed, err := unpackResponse(response)
if err == nil {
// Verify the truncated field in parsed response matches the flag
if parsed.truncated != hasTC {
t.Errorf("Parsed truncated field (%v) doesn't match TC flag (%v)", parsed.truncated, hasTC)
}
} else {
// For unsupported types, just verify we can parse the header
var parser dns.Parser
h, err := parser.Start(response)
if err != nil {
t.Errorf("Failed to parse DNS header: %v", err)
} else {
// Verify header truncated flag matches
if h.Truncated != hasTC {
t.Errorf("Header truncated field (%v) doesn't match TC flag (%v)", h.Truncated, hasTC)
}
}
}
// Verify response size is reasonable (local responses are typically small)
if len(response) > 1000 {
t.Logf("Warning: Local response is unusually large: %d bytes", len(response))
}
})
}
}