From 779fece5be18ca631c0e6446320b19f26ed87f66 Mon Sep 17 00:00:00 2001 From: Alexander Koval Date: Thu, 23 Apr 2026 19:18:43 +0300 Subject: [PATCH 1/5] Test dns outgoing serialization --- src/dns_parser.rs | 171 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/src/dns_parser.rs b/src/dns_parser.rs index 2ebd17c..6e402d9 100644 --- a/src/dns_parser.rs +++ b/src/dns_parser.rs @@ -2603,3 +2603,174 @@ const fn get_expiration_time(created: u64, ttl: u32, percent: u32) -> u64 { // ttl * 1000 * (percent / 100) => ttl * percent * 10 created + (ttl * percent * 10) as u64 } + +#[cfg(test)] +mod tests { + use super::{ + DnsAddress, DnsHostInfo, DnsOutgoing, DnsPointer, DnsTxt, RRType, CLASS_CACHE_FLUSH, + CLASS_IN, + }; + use crate::InterfaceId; + use std::collections::HashMap; + use std::net::{IpAddr, Ipv4Addr}; + + #[test] + fn test_dns_outgoing_serialization_empty() { + let out = DnsOutgoing::new(0); + let packets = out.to_packets(); + assert_eq!(packets.len(), 1); + assert_eq!(packets[0].to_bytes(), &[0; 12]); + let expected_names = HashMap::new(); + assert_eq!(&packets[0].names, &expected_names); + } + + #[test] + fn test_dns_outgoing_serialization_question() { + let mut out = DnsOutgoing::new(0); + out.add_question("123.test", RRType::A); + let packets = out.to_packets(); + assert_eq!(packets.len(), 1); + assert_eq!( + packets[0].to_bytes(), + &[ + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // Header + // Payload + 3, 49, 50, 51, 4, 116, 101, 115, 116, 0, 0, 1, 0, 1, + ] + ); + let mut expected_names = HashMap::new(); + expected_names.insert("123.test".to_string(), 12); + expected_names.insert("test".to_string(), 16); + assert_eq!(&packets[0].names, &expected_names); + } + + #[test] + fn test_dns_outgoing_serialization_question_with_authority() { + let mut out = DnsOutgoing::new(0); + out.add_question("123.test", RRType::ANY); + out.add_authority(Box::new(DnsTxt::new( + "124.test", + CLASS_IN, + 0x00112233, + b"help".to_vec(), + ))); + out.add_authority(Box::new(DnsHostInfo::new( + "124.test", + RRType::CNAME, + CLASS_IN, + 0x00112233, + "arm".to_string(), + "linux".to_string(), + ))); + let packets = out.to_packets(); + assert_eq!(packets.len(), 1); + assert_eq!( + packets[0].to_bytes(), + &[ + 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, // Header + // Payload + 3, 49, 50, 51, 4, 116, 101, 115, 116, 0, 0, 255, 0, 1, 3, 49, 50, 52, 192, 16, 0, + 16, 0, 1, 0, 17, 34, 51, 0, 4, 104, 101, 108, 112, 192, 26, 0, 5, 0, 1, 0, 17, 34, + 51, 0, 8, 97, 114, 109, 108, 105, 110, 117, 120, + ] + ); + let mut expected_names = HashMap::new(); + expected_names.insert("123.test".to_string(), 12); + expected_names.insert("test".to_string(), 16); + expected_names.insert("124.test".to_string(), 26); + assert_eq!(&packets[0].names, &expected_names); + } + + #[test] + fn test_dns_outgoing_serialization_additional_answer() { + let mut out = DnsOutgoing::new(0); + out.add_additional_answer(DnsAddress::new( + "test.local", + RRType::A, + CLASS_IN | CLASS_CACHE_FLUSH, + 0xdead_beef, + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + InterfaceId::default(), + )); + let packets = out.to_packets(); + assert_eq!(packets.len(), 1); + assert_eq!( + packets[0].to_bytes(), + &[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // Header + // Payload + 4, 116, 101, 115, 116, 5, 108, 111, 99, 97, 108, 0, 0, 1, 128, 1, 222, 173, 190, + 239, 0, 4, 127, 0, 0, 1, + ] + ); + let mut expected_names = HashMap::new(); + expected_names.insert("test.local".to_string(), 12); + expected_names.insert("local".to_string(), 17); + assert_eq!(&packets[0].names, &expected_names); + } + + #[test] + fn test_dns_outgoing_serialization_answer_at_time() { + let mut out = DnsOutgoing::new(0); + out.add_answer_at_time( + DnsPointer::new( + "test", + RRType::PTR, + CLASS_IN, + 0xaaaa5555, + "test-service".to_string(), + ), + 0, + ); + let packets = out.to_packets(); + assert_eq!(packets.len(), 1); + assert_eq!( + packets[0].to_bytes(), + &[ + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, // Header + // Payload + 4, 116, 101, 115, 116, 0, 0, 12, 0, 1, 170, 170, 85, 85, 0, 14, 12, 116, 101, 115, + 116, 45, 115, 101, 114, 118, 105, 99, 101, 0, + ] + ); + + let mut out = DnsOutgoing::new(0); + out.add_answer_at_time( + DnsPointer::new( + "test", + RRType::CNAME, + CLASS_IN, + 0xaaaa5555, + "test-service.local".to_string(), + ), + 0, + ); + out.add_answer_at_time( + DnsPointer::new( + "test", + RRType::AAAA, + CLASS_IN, + 0xffffffff, + "test-service.local".to_string(), + ), + 0, + ); + let packets = out.to_packets(); + assert_eq!(packets.len(), 1); + assert_eq!( + packets[0].to_bytes(), + &[ + 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, // Header + // Payload + 4, 116, 101, 115, 116, 0, 0, 5, 0, 1, 170, 170, 85, 85, 0, 20, 12, 116, 101, 115, + 116, 45, 115, 101, 114, 118, 105, 99, 101, 5, 108, 111, 99, 97, 108, 0, 192, 12, 0, + 28, 0, 1, 255, 255, 255, 255, 0, 2, 192, 28, + ] + ); + let mut expected_names = HashMap::new(); + expected_names.insert("test".to_string(), 12); + expected_names.insert("test-service.local".to_string(), 28); + expected_names.insert("local".to_string(), 41); + assert_eq!(&packets[0].names, &expected_names); + } +} From c5bcdc6a190a4f903b4b1e3a0214b61d7afd8675 Mon Sep 17 00:00:00 2001 From: Alexander Koval Date: Thu, 23 Apr 2026 20:51:43 +0300 Subject: [PATCH 2/5] Fix possible overflow when calculating expiration time --- src/dns_parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dns_parser.rs b/src/dns_parser.rs index 6e402d9..15b6548 100644 --- a/src/dns_parser.rs +++ b/src/dns_parser.rs @@ -2601,7 +2601,7 @@ const fn u32_from_be_slice(s: &[u8]) -> u32 { const fn get_expiration_time(created: u64, ttl: u32, percent: u32) -> u64 { // 'created' is in millis, 'ttl' is in seconds, hence: // ttl * 1000 * (percent / 100) => ttl * percent * 10 - created + (ttl * percent * 10) as u64 + created + (ttl as u64 * percent as u64 * 10) } #[cfg(test)] From 98d33737906d507c561ae66c8b24a697540ce3ea Mon Sep 17 00:00:00 2001 From: Alexander Koval Date: Thu, 23 Apr 2026 20:21:06 +0300 Subject: [PATCH 3/5] Optimize dns packet serialization --- src/dns_parser.rs | 94 +++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 56 deletions(-) diff --git a/src/dns_parser.rs b/src/dns_parser.rs index 15b6548..afddd59 100644 --- a/src/dns_parser.rs +++ b/src/dns_parser.rs @@ -1424,11 +1424,8 @@ enum PacketState { /// A single packet for outgoing DNS message. pub struct DnsOutPacket { - /// All bytes in `data` concatenated is the actual packet on the wire. - data: Vec>, - - /// Current logical size of the packet. It starts with the size of the mandatory header. - size: usize, + /// All bytes in `data` is the actual packet on the wire. + data: Vec, /// An internal state, not defined by DNS. state: PacketState, @@ -1440,19 +1437,18 @@ pub struct DnsOutPacket { impl DnsOutPacket { fn new() -> Self { Self { - data: Vec::new(), - size: MSG_HEADER_LEN, // Header is mandatory. + data: vec![0; MSG_HEADER_LEN], state: PacketState::Init, names: HashMap::new(), } } pub fn size(&self) -> usize { - self.size + self.data.len() } - pub fn to_bytes(&self) -> Vec { - self.data.concat() + pub fn as_bytes(&self) -> &[u8] { + &self.data } fn write_question(&mut self, question: &DnsQuestion) { @@ -1465,8 +1461,7 @@ impl DnsOutPacket { /// Returns false if the packet exceeds the max size with this record, nothing is written to the packet. /// otherwise returns true. fn write_record(&mut self, record_ext: &dyn DnsRecordExt, now: u64) -> bool { - let start_data_length = self.data.len(); - let start_size = self.size; + let start_size = self.size(); let record = record_ext.get_record(); self.write_name(record.get_name()); @@ -1484,19 +1479,14 @@ impl DnsOutPacket { self.write_u32(record.get_remaining_ttl(now)); } - let index = self.data.len(); - - // Adjust size for the short we will write before this record - self.size += 2; + // Placeholder for record size + self.write_short(0); + let record_offset = self.size(); record_ext.write(self); - self.size -= 2; + self.insert_short(record_offset - 2, (self.size() - record_offset) as u16); - let length: usize = self.data[index..].iter().map(|x| x.len()).sum(); - self.insert_short(index, length as u16); - - if self.size > MAX_MSG_ABSOLUTE { - self.data.truncate(start_data_length); - self.size = start_size; + if self.size() > MAX_MSG_ABSOLUTE { + self.data.truncate(start_size); self.state = PacketState::Finished; return false; } @@ -1505,8 +1495,7 @@ impl DnsOutPacket { } pub(crate) fn insert_short(&mut self, index: usize, value: u16) { - self.data.insert(index, value.to_be_bytes().to_vec()); - self.size += 2; + self.data[index..index + 2].copy_from_slice(&value.to_be_bytes()); } /// Parses a DNS name that may contain escaped characters according to RFC 6763 Section 4.3. @@ -1613,7 +1602,7 @@ impl DnsOutPacket { } // Store this position for potential future compression - self.names.insert(remaining, self.size as u16); + self.names.insert(remaining, self.size() as u16); // Write the label self.write_utf8(label); @@ -1623,30 +1612,26 @@ impl DnsOutPacket { self.write_byte(0); } - fn write_utf8(&mut self, utf: &str) { - assert!(utf.len() < 64); - self.write_byte(utf.len() as u8); - self.write_bytes(utf.as_bytes()); + fn write_byte(&mut self, v: u8) { + self.data.push(v); } fn write_bytes(&mut self, s: &[u8]) { - self.data.push(s.to_vec()); - self.size += s.len(); + self.data.extend(s); } - fn write_u32(&mut self, int: u32) { - self.data.push(int.to_be_bytes().to_vec()); - self.size += 4; + fn write_utf8(&mut self, s: &str) { + assert!(s.len() < 64); + self.write_byte(s.len() as u8); + self.write_bytes(s.as_bytes()); } - fn write_short(&mut self, short: u16) { - self.data.push(short.to_be_bytes().to_vec()); - self.size += 2; + fn write_u32(&mut self, v: u32) { + self.data.extend(&v.to_be_bytes()); } - fn write_byte(&mut self, byte: u8) { - self.data.push(vec![byte]); - self.size += 1; + fn write_short(&mut self, v: u16) { + self.data.extend(&v.to_be_bytes()); } /// Writes the header fields and finish the packet. @@ -1680,15 +1665,12 @@ impl DnsOutPacket { auth_count: u16, addi_count: u16, ) { - self.insert_short(0, addi_count); - self.insert_short(0, auth_count); - self.insert_short(0, a_count); - self.insert_short(0, q_count); - self.insert_short(0, flags); self.insert_short(0, id); - - // Adjust the size as it was already initialized to include the header. - self.size -= MSG_HEADER_LEN; + self.insert_short(2, flags); + self.insert_short(4, q_count); + self.insert_short(6, a_count); + self.insert_short(8, auth_count); + self.insert_short(10, addi_count); self.state = PacketState::Finished; } @@ -1935,7 +1917,7 @@ impl DnsOutgoing { /// Returns a list of actual DNS packet data to be sent on the wire. pub fn to_data_on_wire(&self) -> Vec> { let packet_list = self.to_packets(); - packet_list.iter().map(|p| p.data.concat()).collect() + packet_list.iter().map(|p| p.data.to_vec()).collect() } /// Encode self into one or more packets. @@ -2619,7 +2601,7 @@ mod tests { let out = DnsOutgoing::new(0); let packets = out.to_packets(); assert_eq!(packets.len(), 1); - assert_eq!(packets[0].to_bytes(), &[0; 12]); + assert_eq!(packets[0].as_bytes(), &[0; 12]); let expected_names = HashMap::new(); assert_eq!(&packets[0].names, &expected_names); } @@ -2631,7 +2613,7 @@ mod tests { let packets = out.to_packets(); assert_eq!(packets.len(), 1); assert_eq!( - packets[0].to_bytes(), + packets[0].as_bytes(), &[ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // Header // Payload @@ -2665,7 +2647,7 @@ mod tests { let packets = out.to_packets(); assert_eq!(packets.len(), 1); assert_eq!( - packets[0].to_bytes(), + packets[0].as_bytes(), &[ 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, // Header // Payload @@ -2695,7 +2677,7 @@ mod tests { let packets = out.to_packets(); assert_eq!(packets.len(), 1); assert_eq!( - packets[0].to_bytes(), + packets[0].as_bytes(), &[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // Header // Payload @@ -2725,7 +2707,7 @@ mod tests { let packets = out.to_packets(); assert_eq!(packets.len(), 1); assert_eq!( - packets[0].to_bytes(), + packets[0].as_bytes(), &[ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, // Header // Payload @@ -2758,7 +2740,7 @@ mod tests { let packets = out.to_packets(); assert_eq!(packets.len(), 1); assert_eq!( - packets[0].to_bytes(), + packets[0].as_bytes(), &[ 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, // Header // Payload From 56659fe0199f91b9b8ba68fd639ceb0ac32990d6 Mon Sep 17 00:00:00 2001 From: Alexander Koval Date: Mon, 27 Apr 2026 18:20:09 +0300 Subject: [PATCH 4/5] Debug derive for DnsOutgoing --- src/dns_parser.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dns_parser.rs b/src/dns_parser.rs index afddd59..87f598c 100644 --- a/src/dns_parser.rs +++ b/src/dns_parser.rs @@ -1677,6 +1677,7 @@ impl DnsOutPacket { } /// Representation of one outgoing DNS message that could be sent in one or more packet(s). +#[derive(Debug)] pub struct DnsOutgoing { flags: u16, id: u16, From 01698d7d9ef119f8108a9d07d38361c33abe62f3 Mon Sep 17 00:00:00 2001 From: Alexander Koval Date: Wed, 29 Apr 2026 16:46:31 +0300 Subject: [PATCH 5/5] Cleanup --- src/dns_parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dns_parser.rs b/src/dns_parser.rs index 87f598c..3ce5b35 100644 --- a/src/dns_parser.rs +++ b/src/dns_parser.rs @@ -1918,7 +1918,7 @@ impl DnsOutgoing { /// Returns a list of actual DNS packet data to be sent on the wire. pub fn to_data_on_wire(&self) -> Vec> { let packet_list = self.to_packets(); - packet_list.iter().map(|p| p.data.to_vec()).collect() + packet_list.into_iter().map(|p| p.data).collect() } /// Encode self into one or more packets.