diff --git a/src/ctx.rs b/src/ctx.rs index 0b7dd80..0dcef51 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -511,27 +511,17 @@ fn strip_dnssec_records(pkt: &mut DnsPacket) { pkt.resources.retain(|r| !is_dnssec_record(r)); } -/// HTTPS RR type code (RFC 9460). Numa stores HTTPS/SVCB records as -/// `DnsRecord::UNKNOWN { qtype: 65, .. }` since it doesn't have a -/// dedicated variant. -const HTTPS_TYPE: u16 = 65; - fn strip_https_ipv6_hints(pkt: &mut DnsPacket) { - let rewrite = |rec: &mut DnsRecord| { - if let DnsRecord::UNKNOWN { - qtype: HTTPS_TYPE, - data, - .. - } = rec - { - if let Some(new_data) = crate::svcb::strip_ipv6hint(data) { - *data = new_data; + let https_qtype = QueryType::HTTPS.to_num(); + pkt.for_each_record_mut(|rec| { + if let DnsRecord::UNKNOWN { qtype, data, .. } = rec { + if *qtype == https_qtype { + if let Some(new_data) = crate::svcb::strip_ipv6hint(data) { + *data = new_data; + } } } - }; - pkt.answers.iter_mut().for_each(rewrite); - pkt.authorities.iter_mut().for_each(rewrite); - pkt.resources.iter_mut().for_each(rewrite); + }); } fn is_special_use_domain(qname: &str) -> bool { @@ -1285,22 +1275,20 @@ mod tests { #[tokio::test] async fn pipeline_filter_aaaa_strips_ipv6hint_from_https() { - // Build an HTTPS record (type 65) with ipv6hint (key 6). Cache it, + // Build an HTTPS record (type 65) with alpn + ipv6hint, cache it, // then query with filter_aaaa on — the returned rdata must have - // ipv6hint removed. - let mut rdata = Vec::new(); - rdata.extend_from_slice(&1u16.to_be_bytes()); // priority - rdata.push(0); // empty target (".") - // alpn = ["h3"] - rdata.extend_from_slice(&1u16.to_be_bytes()); - rdata.extend_from_slice(&3u16.to_be_bytes()); - rdata.extend_from_slice(&[0x02, b'h', b'3']); - // ipv6hint = [2606:4700::1] - 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, - ]); + // ipv6hint (20 bytes) removed. + let rdata = crate::svcb::build_rdata( + 1, + &[], + &[ + (1, vec![0x02, b'h', b'3']), + ( + 6, + vec![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; @@ -1349,14 +1337,14 @@ mod tests { // 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 rdata = crate::svcb::build_rdata( + 1, + &[], + &[( + 6, + vec![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; diff --git a/src/packet.rs b/src/packet.rs index ba9e30a..a621c13 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -85,6 +85,14 @@ impl DnsPacket { + self.edns.as_ref().map_or(0, |e| e.options.capacity()) } + /// Apply `f` to every record in the three RR sections (answers, + /// authorities, resources). Does not touch questions or edns. + pub fn for_each_record_mut(&mut self, mut f: impl FnMut(&mut DnsRecord)) { + self.answers.iter_mut().for_each(&mut f); + self.authorities.iter_mut().for_each(&mut f); + self.resources.iter_mut().for_each(&mut f); + } + pub fn response_from(query: &DnsPacket, rescode: crate::header::ResultCode) -> DnsPacket { let mut resp = DnsPacket::new(); resp.header.id = query.header.id; diff --git a/src/svcb.rs b/src/svcb.rs index 2228443..444b063 100644 --- a/src/svcb.rs +++ b/src/svcb.rs @@ -80,28 +80,34 @@ pub fn strip_ipv6hint(rdata: &[u8]) -> Option> { Some(out) } +/// Build an SVCB RDATA blob from a priority, target labels, and +/// (key, value) param pairs. Shared by `svcb` unit tests and `ctx` +/// pipeline tests that need to seed the cache with a synthetic HTTPS RR. +#[cfg(test)] +pub(crate) fn build_rdata( + priority: u16, + target: &[&str], + params: &[(u16, Vec)], +) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&priority.to_be_bytes()); + for label in target { + out.push(label.len() as u8); + out.extend_from_slice(label.as_bytes()); + } + out.push(0); + for (key, value) in params { + out.extend_from_slice(&key.to_be_bytes()); + out.extend_from_slice(&(value.len() as u16).to_be_bytes()); + out.extend_from_slice(value); + } + out +} + #[cfg(test)] mod tests { use super::*; - /// Build an SVCB RDATA blob from a priority, target labels, and - /// (key, value) param pairs. Used for constructing test vectors. - fn build(priority: u16, target: &[&str], params: &[(u16, Vec)]) -> Vec { - let mut out = Vec::new(); - out.extend_from_slice(&priority.to_be_bytes()); - for label in target { - out.push(label.len() as u8); - out.extend_from_slice(label.as_bytes()); - } - out.push(0); - for (key, value) in params { - out.extend_from_slice(&key.to_be_bytes()); - out.extend_from_slice(&(value.len() as u16).to_be_bytes()); - out.extend_from_slice(value); - } - out - } - fn alpn_h3() -> (u16, Vec) { // alpn = ["h3"]: one length-prefixed ALPN id (1, vec![0x02, b'h', b'3']) @@ -123,35 +129,35 @@ mod tests { #[test] fn strips_ipv6hint_and_keeps_other_params() { - let rdata = build(1, &[], &[alpn_h3(), ipv4hint_single(), ipv6hint_single()]); + let rdata = build_rdata(1, &[], &[alpn_h3(), ipv4hint_single(), ipv6hint_single()]); let stripped = strip_ipv6hint(&rdata).expect("ipv6hint present → stripped"); - let expected = build(1, &[], &[alpn_h3(), ipv4hint_single()]); + let expected = build_rdata(1, &[], &[alpn_h3(), ipv4hint_single()]); assert_eq!(stripped, expected); } #[test] fn no_ipv6hint_returns_none() { - let rdata = build(1, &[], &[alpn_h3(), ipv4hint_single()]); + let rdata = build_rdata(1, &[], &[alpn_h3(), ipv4hint_single()]); assert!(strip_ipv6hint(&rdata).is_none()); } #[test] fn alias_mode_empty_params_returns_none() { - let rdata = build(0, &["example", "com"], &[]); + let rdata = build_rdata(0, &["example", "com"], &[]); assert!(strip_ipv6hint(&rdata).is_none()); } #[test] fn only_ipv6hint_yields_empty_param_section() { - let rdata = build(1, &[], &[ipv6hint_single()]); + let rdata = build_rdata(1, &[], &[ipv6hint_single()]); let stripped = strip_ipv6hint(&rdata).expect("ipv6hint present → stripped"); - let expected = build(1, &[], &[]); + let expected = build_rdata(1, &[], &[]); assert_eq!(stripped, expected); } #[test] fn preserves_target_name() { - let rdata = build(1, &["svc", "example", "net"], &[ipv6hint_single()]); + let rdata = build_rdata(1, &["svc", "example", "net"], &[ipv6hint_single()]); let stripped = strip_ipv6hint(&rdata).unwrap(); assert!(stripped.starts_with(&[0x00, 0x01])); // priority assert_eq!(&stripped[2..6], b"\x03svc");