feat(resolver): apply ipv6hint strip to SVCB (type 64) too

HTTPS (65) and SVCB (64) share the RDATA wire format, so the existing
parser already handles both — only the call site was HTTPS-only. Widen
the qtype check and extend the existing pipeline test with a second
query for SVCB.
This commit is contained in:
Razvan Dimescu
2026-04-19 06:52:30 +03:00
parent d6bb9a0f01
commit 5e85b147b9

View File

@@ -351,7 +351,7 @@ pub async fn resolve_query(
// because modifying rdata invalidates any accompanying RRSIG — a DO-bit // because modifying rdata invalidates any accompanying RRSIG — a DO-bit
// validator downstream would reject the response as Bogus. // validator downstream would reject the response as Bogus.
if ctx.filter_aaaa && !client_do { if ctx.filter_aaaa && !client_do {
strip_https_ipv6_hints(&mut response); strip_svcb_ipv6_hints(&mut response);
} }
// Echo EDNS back if client sent it // Echo EDNS back if client sent it
@@ -511,11 +511,16 @@ fn strip_dnssec_records(pkt: &mut DnsPacket) {
pkt.resources.retain(|r| !is_dnssec_record(r)); pkt.resources.retain(|r| !is_dnssec_record(r));
} }
fn strip_https_ipv6_hints(pkt: &mut DnsPacket) { /// SVCB and HTTPS share the same RDATA wire format (RFC 9460), so the
/// ipv6hint strip applies to both. SVCB has no `QueryType` variant — it
/// arrives as `UNKNOWN { qtype: 64, .. }`.
const SVCB_QTYPE: u16 = 64;
fn strip_svcb_ipv6_hints(pkt: &mut DnsPacket) {
let https_qtype = QueryType::HTTPS.to_num(); let https_qtype = QueryType::HTTPS.to_num();
pkt.for_each_record_mut(|rec| { pkt.for_each_record_mut(|rec| {
if let DnsRecord::UNKNOWN { qtype, data, .. } = rec { if let DnsRecord::UNKNOWN { qtype, data, .. } = rec {
if *qtype == https_qtype { if *qtype == https_qtype || *qtype == SVCB_QTYPE {
if let Some(new_data) = crate::svcb::strip_ipv6hint(data) { if let Some(new_data) = crate::svcb::strip_ipv6hint(data) {
*data = new_data; *data = new_data;
} }
@@ -1274,10 +1279,12 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn pipeline_filter_aaaa_strips_ipv6hint_from_https() { async fn pipeline_filter_aaaa_strips_ipv6hint_from_https_and_svcb() {
// Build an HTTPS record (type 65) with alpn + ipv6hint, cache it, // HTTPS (type 65) and SVCB (type 64) share the same RDATA wire
// then query with filter_aaaa on — the returned rdata must have // format (RFC 9460); the filter must strip ipv6hint from both.
// ipv6hint (20 bytes) removed. // Build one HTTPS record with alpn + ipv6hint, then re-key it as
// SVCB and assert the returned rdata has the 20-byte hint removed
// in both cases.
let rdata = crate::svcb::build_rdata( let rdata = crate::svcb::build_rdata(
1, 1,
&[], &[],
@@ -1306,31 +1313,50 @@ mod tests {
ttl: 300, ttl: 300,
}); });
// Seed an SVCB record (type 64) under a different name — same wire
// format as HTTPS, must get the same treatment.
let mut svcb_pkt = pkt.clone();
svcb_pkt.questions[0].name = "svc.test".to_string();
svcb_pkt.questions[0].qtype = QueryType::UNKNOWN(64);
if let DnsRecord::UNKNOWN { domain, qtype, .. } = &mut svcb_pkt.answers[0] {
*domain = "svc.test".to_string();
*qtype = 64;
}
let mut ctx = crate::testutil::test_ctx().await; let mut ctx = crate::testutil::test_ctx().await;
ctx.filter_aaaa = true; ctx.filter_aaaa = true;
ctx.cache ctx.cache
.write() .write()
.unwrap() .unwrap()
.insert("hints.test", QueryType::HTTPS, &pkt); .insert("hints.test", QueryType::HTTPS, &pkt);
ctx.cache
.write()
.unwrap()
.insert("svc.test", QueryType::UNKNOWN(64), &svcb_pkt);
let ctx = Arc::new(ctx); let ctx = Arc::new(ctx);
let (resp, path) = resolve_in_test(&ctx, "hints.test", QueryType::HTTPS).await; for (name, qtype, label) in [
assert_eq!(path, QueryPath::Cached); ("hints.test", QueryType::HTTPS, "HTTPS"),
assert_eq!(resp.answers.len(), 1); ("svc.test", QueryType::UNKNOWN(64), "SVCB"),
match &resp.answers[0] { ] {
DnsRecord::UNKNOWN { data, .. } => { let (resp, path) = resolve_in_test(&ctx, name, qtype).await;
assert!( assert_eq!(path, QueryPath::Cached, "{label}");
data.len() < rdata.len(), assert_eq!(resp.answers.len(), 1, "{label}");
"ipv6hint (20 bytes) must be removed" match &resp.answers[0] {
); DnsRecord::UNKNOWN { data, .. } => {
// Bytes for key=6 must not appear at any 4-byte boundary in the assert!(
// params section — cheap structural check. data.len() < rdata.len(),
assert!( "{label}: ipv6hint (20 bytes) must be removed"
!data.windows(4).any(|w| w == [0, 6, 0, 16]), );
"ipv6hint TLV header must be absent" // Bytes for key=6 must not appear at any 4-byte boundary in the
); // params section — cheap structural check.
assert!(
!data.windows(4).any(|w| w == [0, 6, 0, 16]),
"{label}: ipv6hint TLV header must be absent"
);
}
other => panic!("{label}: expected UNKNOWN record, got {other:?}"),
} }
other => panic!("expected UNKNOWN record, got {:?}", other),
} }
} }