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:
72
src/ctx.rs
72
src/ctx.rs
@@ -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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user