diff --git a/src/dnssec.rs b/src/dnssec.rs index 8614810..877b495 100644 --- a/src/dnssec.rs +++ b/src/dnssec.rs @@ -882,6 +882,28 @@ fn record_rdata_canonical(record: &DnsRecord) -> Vec { rdata.extend(type_bitmap); rdata } + DnsRecord::SOA { + mname, + rname, + serial, + refresh, + retry, + expire, + minimum, + .. + } => { + let mname_wire = name_to_wire(mname); + let rname_wire = name_to_wire(rname); + let mut rdata = Vec::with_capacity(mname_wire.len() + rname_wire.len() + 20); + rdata.extend(&mname_wire); + rdata.extend(&rname_wire); + rdata.extend(&serial.to_be_bytes()); + rdata.extend(&refresh.to_be_bytes()); + rdata.extend(&retry.to_be_bytes()); + rdata.extend(&expire.to_be_bytes()); + rdata.extend(&minimum.to_be_bytes()); + rdata + } DnsRecord::UNKNOWN { data, .. } => data.clone(), DnsRecord::RRSIG { .. } => Vec::new(), } diff --git a/src/record.rs b/src/record.rs index 7de9bb4..0fefd72 100644 --- a/src/record.rs +++ b/src/record.rs @@ -24,6 +24,17 @@ pub enum DnsRecord { host: String, ttl: u32, }, + SOA { + domain: String, + mname: String, + rname: String, + serial: u32, + refresh: u32, + retry: u32, + expire: u32, + minimum: u32, + ttl: u32, + }, CNAME { domain: String, host: String, @@ -100,6 +111,7 @@ impl DnsRecord { | DnsRecord::RRSIG { domain, .. } | DnsRecord::NSEC { domain, .. } | DnsRecord::NSEC3 { domain, .. } + | DnsRecord::SOA { domain, .. } | DnsRecord::UNKNOWN { domain, .. } => domain, } } @@ -111,6 +123,7 @@ impl DnsRecord { DnsRecord::NS { .. } => QueryType::NS, DnsRecord::CNAME { .. } => QueryType::CNAME, DnsRecord::MX { .. } => QueryType::MX, + DnsRecord::SOA { .. } => QueryType::SOA, DnsRecord::DNSKEY { .. } => QueryType::DNSKEY, DnsRecord::DS { .. } => QueryType::DS, DnsRecord::RRSIG { .. } => QueryType::RRSIG, @@ -132,6 +145,7 @@ impl DnsRecord { | DnsRecord::RRSIG { ttl, .. } | DnsRecord::NSEC { ttl, .. } | DnsRecord::NSEC3 { ttl, .. } + | DnsRecord::SOA { ttl, .. } | DnsRecord::UNKNOWN { ttl, .. } => *ttl, } } @@ -172,6 +186,12 @@ impl DnsRecord { + next_hashed_owner.capacity() + type_bitmap.capacity() } + DnsRecord::SOA { + domain, + mname, + rname, + .. + } => domain.capacity() + mname.capacity() + rname.capacity(), DnsRecord::UNKNOWN { domain, data, .. } => domain.capacity() + data.capacity(), } } @@ -188,6 +208,7 @@ impl DnsRecord { | DnsRecord::RRSIG { ttl, .. } | DnsRecord::NSEC { ttl, .. } | DnsRecord::NSEC3 { ttl, .. } + | DnsRecord::SOA { ttl, .. } | DnsRecord::UNKNOWN { ttl, .. } => *ttl = new_ttl, } } @@ -365,8 +386,31 @@ impl DnsRecord { ttl, }) } + QueryType::SOA => { + // MNAME/RNAME compressible per RFC 1035 §3.3.13 — decompress to avoid stale pointers on re-emit. + let mut mname = String::with_capacity(64); + buffer.read_qname(&mut mname)?; + let mut rname = String::with_capacity(64); + buffer.read_qname(&mut rname)?; + let serial = buffer.read_u32()?; + let refresh = buffer.read_u32()?; + let retry = buffer.read_u32()?; + let expire = buffer.read_u32()?; + let minimum = buffer.read_u32()?; + Ok(DnsRecord::SOA { + domain, + mname, + rname, + serial, + refresh, + retry, + expire, + minimum, + ttl, + }) + } _ => { - // SOA, TXT, SRV, etc. — stored as opaque bytes until parsed natively + // TXT, SRV, HTTPS, SVCB, etc. — stored as opaque bytes until parsed natively let data = buffer.get_range(buffer.pos(), data_len as usize)?.to_vec(); buffer.step(data_len as usize)?; Ok(DnsRecord::UNKNOWN { @@ -430,6 +474,30 @@ impl DnsRecord { let size = buffer.pos() - (pos + 2); buffer.set_u16(pos, size as u16)?; } + DnsRecord::SOA { + ref domain, + ref mname, + ref rname, + serial, + refresh, + retry, + expire, + minimum, + ttl, + } => { + write_header(buffer, domain, QueryType::SOA.to_num(), ttl)?; + let rdlen_pos = buffer.pos(); + buffer.write_u16(0)?; + buffer.write_qname(mname)?; + buffer.write_qname(rname)?; + buffer.write_u32(serial)?; + buffer.write_u32(refresh)?; + buffer.write_u32(retry)?; + buffer.write_u32(expire)?; + buffer.write_u32(minimum)?; + let rdlen = buffer.pos() - (rdlen_pos + 2); + buffer.set_u16(rdlen_pos, rdlen as u16)?; + } DnsRecord::AAAA { ref domain, ref addr, diff --git a/tests/soa_compression_bug.rs b/tests/soa_compression_bug.rs new file mode 100644 index 0000000..5f4f2f0 --- /dev/null +++ b/tests/soa_compression_bug.rs @@ -0,0 +1,115 @@ +//! Regression test for issue #128: SOA with compressed MNAME/RNAME must +//! survive Numa's round-trip — compression pointers reference the upstream +//! packet's byte layout, so we have to decompress on read and re-compress +//! on write. + +use numa::buffer::BytePacketBuffer; +use numa::packet::DnsPacket; + +const COMPRESSION_FLAG: u16 = 0xC000; + +fn upstream_packet() -> Vec { + let mut p = Vec::::new(); + + p.extend_from_slice(&[ + 0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, + ]); + + assert_eq!(p.len(), 12); + write_name(&mut p, &["odin", "adobe", "com"]); + p.extend_from_slice(&[0x00, 0x41, 0x00, 0x01]); + + p.extend_from_slice(&[0xC0, 0x0C]); + p.extend_from_slice(&[0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x23, 0x7F]); + let rdlen_pos_1 = p.len(); + p.extend_from_slice(&[0x00, 0x00]); + let cname1_start = p.len(); + write_name(&mut p, &["cdn", "adobeaemcloud", "com"]); + let rdlen_1 = (p.len() - cname1_start) as u16; + p[rdlen_pos_1..rdlen_pos_1 + 2].copy_from_slice(&rdlen_1.to_be_bytes()); + + p.extend_from_slice(&(COMPRESSION_FLAG | cname1_start as u16).to_be_bytes()); + p.extend_from_slice(&[0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x23, 0x7F]); + let rdlen_pos_2 = p.len(); + p.extend_from_slice(&[0x00, 0x00]); + let cname2_start = p.len(); + p.push(9); + p.extend_from_slice(b"adobe-aem"); + let map_label_off = p.len(); + p.push(3); + p.extend_from_slice(b"map"); + let fastly_label_off = p.len(); + p.push(6); + p.extend_from_slice(b"fastly"); + p.push(3); + p.extend_from_slice(b"net"); + p.push(0); + let rdlen_2 = (p.len() - cname2_start) as u16; + p[rdlen_pos_2..rdlen_pos_2 + 2].copy_from_slice(&rdlen_2.to_be_bytes()); + + p.extend_from_slice(&(COMPRESSION_FLAG | fastly_label_off as u16).to_be_bytes()); + p.extend_from_slice(&[0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x07, 0x08]); + let rdlen_pos_soa = p.len(); + p.extend_from_slice(&[0x00, 0x00]); + let soa_rdata_start = p.len(); + p.extend_from_slice(&(COMPRESSION_FLAG | map_label_off as u16).to_be_bytes()); + p.extend_from_slice(&(COMPRESSION_FLAG | fastly_label_off as u16).to_be_bytes()); + p.extend_from_slice(&1u32.to_be_bytes()); + p.extend_from_slice(&7200u32.to_be_bytes()); + p.extend_from_slice(&3600u32.to_be_bytes()); + p.extend_from_slice(&1209600u32.to_be_bytes()); + p.extend_from_slice(&1800u32.to_be_bytes()); + let rdlen_soa = (p.len() - soa_rdata_start) as u16; + p[rdlen_pos_soa..rdlen_pos_soa + 2].copy_from_slice(&rdlen_soa.to_be_bytes()); + + p +} + +fn write_name(p: &mut Vec, labels: &[&str]) { + for l in labels { + p.push(l.len() as u8); + p.extend_from_slice(l.as_bytes()); + } + p.push(0); +} + +#[test] +fn compressed_soa_survives_numa_round_trip() { + let upstream = upstream_packet(); + + let hickory_in = hickory_proto::op::Message::from_vec(&upstream) + .expect("hand-crafted upstream must be valid"); + let soa_in_rd = hickory_in.name_servers()[0] + .data() + .clone() + .into_soa() + .expect("SOA rdata"); + assert_eq!(soa_in_rd.mname().to_string(), "map.fastly.net."); + assert_eq!(soa_in_rd.rname().to_string(), "fastly.net."); + + let mut in_buf = BytePacketBuffer::from_bytes(&upstream); + let pkt = DnsPacket::from_buffer(&mut in_buf).expect("numa parses upstream"); + assert_eq!(pkt.answers.len(), 2); + assert_eq!(pkt.authorities.len(), 1); + + let mut out_buf = BytePacketBuffer::new(); + pkt.write(&mut out_buf).expect("numa writes"); + let out = out_buf.filled().to_vec(); + + let hickory_out = + hickory_proto::op::Message::from_vec(&out).expect("numa re-emission must parse strictly"); + + let soa_out_rd = hickory_out.name_servers()[0] + .data() + .clone() + .into_soa() + .expect("SOA rdata on output"); + + assert_eq!(soa_out_rd.mname().to_string(), "map.fastly.net."); + assert_eq!(soa_out_rd.rname().to_string(), "fastly.net."); + assert_eq!(soa_out_rd.serial(), 1); + assert_eq!(soa_out_rd.refresh(), 7200); + assert_eq!(soa_out_rd.retry(), 3600); + assert_eq!(soa_out_rd.expire(), 1209600); + assert_eq!(soa_out_rd.minimum(), 1800); +}