From 22dd3cd2222f7d19994125f61800c5eb3af672b5 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sun, 19 Apr 2026 05:52:37 +0300 Subject: [PATCH] fix(resolver): skip ipv6hint strip for DO-bit clients Modifying HTTPS rdata invalidates any accompanying RRSIG, so a DNSSEC- validating downstream would reject the response as Bogus. Gate the strip on !client_do, matching the existing DNSSEC-records strip. Adds a regression test that catches the gate being removed: builds a query with EDNS DO=1, asserts the HTTPS rdata round-trips untouched. --- src/ctx.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/src/ctx.rs b/src/ctx.rs index b3f7ae2..0b7dd80 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -347,8 +347,10 @@ pub async fn resolve_query( // filter_aaaa: also strip ipv6hint from HTTPS/SVCB answers so modern // browsers (Chrome ≥103 etc.) don't receive v6 address hints via the - // HTTPS record path that bypasses AAAA entirely. - if ctx.filter_aaaa { + // HTTPS record path that bypasses AAAA entirely. Gated on !client_do + // because modifying rdata invalidates any accompanying RRSIG — a DO-bit + // validator downstream would reject the response as Bogus. + if ctx.filter_aaaa && !client_do { strip_https_ipv6_hints(&mut response); } @@ -1342,6 +1344,71 @@ mod tests { } } + #[tokio::test] + async fn pipeline_filter_aaaa_preserves_ipv6hint_for_dnssec_clients() { + // Regression guard for the DO-bit gate in resolve_query: modifying + // HTTPS rdata invalidates any accompanying RRSIG, so a DO=1 client + // must receive the record untouched even when filter_aaaa is on. + let mut rdata = Vec::new(); + rdata.extend_from_slice(&1u16.to_be_bytes()); + rdata.push(0); + rdata.extend_from_slice(&6u16.to_be_bytes()); + rdata.extend_from_slice(&16u16.to_be_bytes()); + rdata.extend_from_slice(&[ + 0x26, 0x06, 0x47, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01, + ]); + + let mut pkt = DnsPacket::new(); + pkt.header.response = true; + pkt.header.rescode = ResultCode::NOERROR; + pkt.questions.push(crate::question::DnsQuestion { + name: "hints.test".to_string(), + qtype: QueryType::HTTPS, + }); + pkt.answers.push(DnsRecord::UNKNOWN { + domain: "hints.test".to_string(), + qtype: 65, + data: rdata.clone(), + ttl: 300, + }); + + let mut ctx = crate::testutil::test_ctx().await; + ctx.filter_aaaa = true; + ctx.cache + .write() + .unwrap() + .insert("hints.test", QueryType::HTTPS, &pkt); + let ctx = Arc::new(ctx); + + // Build a query with EDNS DO bit set — can't use resolve_in_test + // because it constructs a plain query without EDNS. + let mut query = DnsPacket::query(0xBEEF, "hints.test", QueryType::HTTPS); + query.edns = Some(crate::packet::EdnsOpt { + do_bit: true, + ..Default::default() + }); + let mut buf = BytePacketBuffer::new(); + query.write(&mut buf).unwrap(); + let raw = &buf.buf[..buf.pos]; + let src: SocketAddr = "127.0.0.1:1234".parse().unwrap(); + + let (resp_buf, _) = resolve_query(query, raw, src, &ctx, Transport::Udp) + .await + .unwrap(); + let mut resp_parse_buf = BytePacketBuffer::from_bytes(resp_buf.filled()); + let resp = DnsPacket::from_buffer(&mut resp_parse_buf).unwrap(); + + match &resp.answers[0] { + DnsRecord::UNKNOWN { data, .. } => { + assert_eq!( + data, &rdata, + "ipv6hint must be preserved for DO-bit clients" + ); + } + other => panic!("expected UNKNOWN record, got {:?}", other), + } + } + #[tokio::test] async fn pipeline_blocklist_sinkhole() { let ctx = crate::testutil::test_ctx().await;