From b6d98e5100716f8212c813da5f1883e2fa439242 Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Tue, 17 Sep 2024 09:07:42 +0200 Subject: [PATCH] Extended tests for frag pool --- README.md | 6 +- etherparse/Cargo.toml | 2 +- etherparse/examples/ip_defrag.rs | 78 ++++ etherparse/src/defrag/ip_defrag_pool.rs | 451 +++++++++++++++++++- etherparse/src/lib.rs | 6 +- etherparse/src/sliced_packet.rs | 10 + etherparse/src/transport/transport_slice.rs | 6 +- 7 files changed, 545 insertions(+), 14 deletions(-) create mode 100644 etherparse/examples/ip_defrag.rs diff --git a/README.md b/README.md index 67bbc7fd..19a2505e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # etherparse -A mostly zero allocation library for parsing & writing a bunch of packet based protocols (EthernetII, IPv4, IPv6, UDP, TCP ...). +A zero allocation supporting library for parsing & writing a bunch of packet based protocols (EthernetII, IPv4, IPv6, UDP, TCP ...). Currently supported are: * Ethernet II @@ -17,13 +17,15 @@ Currently supported are: * TCP * ICMP & ICMPv6 (not all message types are supported) +Reconstruction of fragmented IP packets is also supported, but requires allocations. + ## Usage Add the following to your `Cargo.toml`: ```toml [dependencies] -etherparse = "0.15" +etherparse = "0.16" ``` ## What is etherparse? diff --git a/etherparse/Cargo.toml b/etherparse/Cargo.toml index 0385e40e..64a338d0 100644 --- a/etherparse/Cargo.toml +++ b/etherparse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "etherparse" -version = "0.15.0" +version = "0.16.0" authors = ["Julian Schmid "] edition = "2021" repository = "https://github.com/JulianSchmid/etherparse" diff --git a/etherparse/examples/ip_defrag.rs b/etherparse/examples/ip_defrag.rs new file mode 100644 index 00000000..111e2b62 --- /dev/null +++ b/etherparse/examples/ip_defrag.rs @@ -0,0 +1,78 @@ +use etherparse::*; + +fn main() { + // setup some network data to parse + let builder = PacketBuilder::ethernet2( + // source mac + [1, 2, 3, 4, 5, 6], + // destination mac + [7, 8, 9, 10, 11, 12], + ) + .ip(IpHeaders::Ipv4( + Ipv4Header { + total_len: 0, // will be overwritten by builder + identification: 1234, + dont_fragment: false, + more_fragments: true, + fragment_offset: IpFragOffset::try_new(1024 / 8).unwrap(), + time_to_live: 20, + protocol: IpNumber::UDP, + header_checksum: 0, // will be overwritten by builder + source: [1, 2, 3, 4], + destination: [2, 3, 4, 5], + ..Default::default() + }, + Default::default(), + )) + .udp( + 21, // source port + 1234, // desitnation port + ); + + // payload of the udp packet + let payload = [1, 2, 3, 4, 5, 6, 7, 8]; + + // get some memory to store the serialized data + let mut serialized = Vec::::with_capacity(builder.size(payload.len())); + builder.write(&mut serialized, &payload).unwrap(); + + // pool that manages the different fragmented packets & the different memory buffers for re-assembly + let mut ip_defrag_pool = defrag::IpDefragPool::<(), ()>::new(); + + // slice the packet into the different header components + let sliced_packet = match SlicedPacket::from_ethernet(&serialized) { + Err(err) => { + println!("Err {:?}", err); + return; + } + Ok(v) => v, + }; + + // constructed + if sliced_packet.is_ip_payload_fragmented() { + let defrag_result = ip_defrag_pool.process_sliced_packet(&sliced_packet, (), ()); + match defrag_result { + Ok(Some(finished)) => { + println!( + "Successfully reconstructed fragmented IP packet ({} bytes, protocol {:?})", + finished.payload.len(), + finished.ip_number, + ); + + // continue parsing the payload + // ... fill in your code here + + // IMPORTANT: After done return the finished packet buffer to avoid unneeded allocations + ip_defrag_pool.return_buf(finished); + } + Ok(None) => { + println!( + "Received a fragmented packet, but the reconstruction was not yet finished" + ); + } + Err(err) => { + println!("Error reconstructing fragmented IPv4 packet: {err}"); + } + } + } +} diff --git a/etherparse/src/defrag/ip_defrag_pool.rs b/etherparse/src/defrag/ip_defrag_pool.rs index 59110987..b5a5277f 100644 --- a/etherparse/src/defrag/ip_defrag_pool.rs +++ b/etherparse/src/defrag/ip_defrag_pool.rs @@ -5,6 +5,11 @@ use std::vec::Vec; /// Pool of buffers to reconstruct multiple fragmented IP packets in /// parallel (re-uses buffers to minimize allocations). /// +/// It differentiates the packets based on their inner & outer vlan as well as +/// source and destination ip address and allows the user to add their own +/// custom "channel id" type to further differentiate different streams. +/// The custom channel id can be used to +/// /// # This implementation is NOT safe against "Out of Memory" attacks /// /// If you use the [`DefragPool`] in an untrusted environment an attacker could @@ -87,12 +92,6 @@ where ) } Some(NetSlice::Ipv6(ipv6)) => { - // skip unfragmented packets - if false == ipv6.is_payload_fragmented() { - // nothing to defragment here, skip packet - return Ok(None); - } - // get fragmentation header let frag = { let mut f = None; @@ -104,7 +103,12 @@ where } } if let Some(f) = f { - f.to_header() + if f.is_fragmenting_payload() { + f.to_header() + } else { + // nothing to defragment here, skip packet + return Ok(None); + } } else { // nothing to defragment here, skip packet return Ok(None); @@ -218,7 +222,438 @@ where pub fn return_buf(&mut self, buf: IpDefragPayloadVec) { self.finished_data_bufs.push(buf.payload); } + + /// Retains only the elements specified by the predicate. + pub fn retain(&mut self, f: F) + where + F: Fn(&Timestamp) -> bool, + { + if self.active.iter().any(|(_, (_, t))| false == f(t)) { + self.active = self + .active + .drain() + .filter_map(|(k, v)| { + if f(&v.1) { + Some((k, v)) + } else { + let (data, sections) = v.0.take_bufs(); + self.finished_data_bufs.push(data); + self.finished_section_bufs.push(sections); + None + } + }) + .collect(); + } + } } #[cfg(test)] -mod test {} +mod test { + use std::cmp::max; + + use super::*; + + #[test] + fn new() { + { + let pool = IpDefragPool::<(), ()>::new(); + assert_eq!(pool.active.len(), 0); + assert_eq!(pool.finished_data_bufs.len(), 0); + assert_eq!(pool.finished_section_bufs.len(), 0); + } + { + let pool = IpDefragPool::::new(); + assert_eq!(pool.active.len(), 0); + assert_eq!(pool.finished_data_bufs.len(), 0); + assert_eq!(pool.finished_section_bufs.len(), 0); + } + } + + fn build_packet( + id: IpFragId, + offset: u16, + more: bool, + payload: &[u8], + ) -> Vec { + let mut buf = Vec::with_capacity( + Ethernet2Header::LEN + + SingleVlanHeader::LEN + + SingleVlanHeader::LEN + + max( + Ipv4Header::MIN_LEN, + Ipv6Header::LEN + Ipv6FragmentHeader::LEN, + ) + + payload.len(), + ); + + let ip_ether_type = match id.ip { + IpFragVersionSpecId::Ipv4 { + source: _, + destination: _, + identification: _, + } => EtherType::IPV4, + IpFragVersionSpecId::Ipv6 { + source: _, + destination: _, + identification: _, + } => EtherType::IPV6, + }; + + buf.extend_from_slice( + &Ethernet2Header { + source: [0; 6], + destination: [0; 6], + ether_type: if id.outer_vlan_id.is_some() || id.inner_vlan_id.is_some() { + EtherType::VLAN_TAGGED_FRAME + } else { + ip_ether_type + }, + } + .to_bytes(), + ); + + if let Some(vlan_id) = id.outer_vlan_id { + buf.extend_from_slice( + &SingleVlanHeader { + pcp: VlanPcp::try_new(0).unwrap(), + drop_eligible_indicator: false, + vlan_id, + ether_type: if id.inner_vlan_id.is_some() { + EtherType::VLAN_TAGGED_FRAME + } else { + ip_ether_type + }, + } + .to_bytes(), + ); + } + + if let Some(vlan_id) = id.inner_vlan_id { + buf.extend_from_slice( + &SingleVlanHeader { + pcp: VlanPcp::try_new(0).unwrap(), + drop_eligible_indicator: false, + vlan_id, + ether_type: ip_ether_type, + } + .to_bytes(), + ); + } + + match id.ip { + IpFragVersionSpecId::Ipv4 { + source, + destination, + identification, + } => { + let mut header = Ipv4Header { + identification, + more_fragments: more, + fragment_offset: IpFragOffset::try_new(offset).unwrap(), + protocol: id.payload_ip_number, + source, + destination, + total_len: (Ipv4Header::MIN_LEN + payload.len()) as u16, + time_to_live: 2, + ..Default::default() + }; + header.header_checksum = header.calc_header_checksum(); + buf.extend_from_slice(&header.to_bytes()); + } + IpFragVersionSpecId::Ipv6 { + source, + destination, + identification, + } => { + buf.extend_from_slice( + &Ipv6Header { + traffic_class: 0, + flow_label: Default::default(), + payload_length: (payload.len() + Ipv6FragmentHeader::LEN) as u16, + next_header: IpNumber::IPV6_FRAGMENTATION_HEADER, + hop_limit: 2, + source, + destination, + } + .to_bytes(), + ); + buf.extend_from_slice( + &Ipv6FragmentHeader { + next_header: id.payload_ip_number, + fragment_offset: IpFragOffset::try_new(offset).unwrap(), + more_fragments: more, + identification, + } + .to_bytes(), + ); + } + } + buf.extend_from_slice(payload); + buf + } + + #[test] + fn process_sliced_packet() { + // v4 non fragmented + { + let mut pool = IpDefragPool::<(), ()>::new(); + let pdata = build_packet( + IpFragId { + outer_vlan_id: None, + inner_vlan_id: None, + ip: IpFragVersionSpecId::Ipv4 { + source: [0; 4], + destination: [0; 4], + identification: 0, + }, + payload_ip_number: IpNumber::UDP, + channel_id: (), + }, + 0, + false, + &UdpHeader { + source_port: 0, + destination_port: 0, + length: 0, + checksum: 0, + } + .to_bytes(), + ); + let pslice = SlicedPacket::from_ethernet(&pdata).unwrap(); + let v = pool.process_sliced_packet(&pslice, (), ()); + assert_eq!(Ok(None), v); + + // check the effect had no effect + assert_eq!(pool.active.len(), 0); + assert_eq!(pool.finished_data_bufs.len(), 0); + assert_eq!(pool.finished_section_bufs.len(), 0); + } + + // v6 non fragmented + { + let mut pool = IpDefragPool::<(), ()>::new(); + let pdata = build_packet( + IpFragId { + outer_vlan_id: None, + inner_vlan_id: None, + ip: IpFragVersionSpecId::Ipv6 { + source: [0; 16], + destination: [0; 16], + identification: 0, + }, + payload_ip_number: IpNumber::UDP, + channel_id: (), + }, + 0, + false, + &UdpHeader { + source_port: 0, + destination_port: 0, + length: 0, + checksum: 0, + } + .to_bytes(), + ); + let pslice = SlicedPacket::from_ethernet(&pdata).unwrap(); + let v = pool.process_sliced_packet(&pslice, (), ()); + assert_eq!(Ok(None), v); + + // check the effect had no effect + assert_eq!(pool.active.len(), 0); + assert_eq!(pool.finished_data_bufs.len(), 0); + assert_eq!(pool.finished_section_bufs.len(), 0); + } + + // v4 & v6 basic test + { + let frag_ids = [ + // v4 (no vlan) + IpFragId { + outer_vlan_id: None, + inner_vlan_id: None, + ip: IpFragVersionSpecId::Ipv4 { + source: [1, 2, 3, 4], + destination: [5, 6, 7, 8], + identification: 9, + }, + payload_ip_number: IpNumber::UDP, + channel_id: (), + }, + // v4 (single vlan) + IpFragId { + outer_vlan_id: Some(VlanId::try_new(12).unwrap()), + inner_vlan_id: None, + ip: IpFragVersionSpecId::Ipv4 { + source: [1, 2, 3, 4], + destination: [5, 6, 7, 8], + identification: 9, + }, + payload_ip_number: IpNumber::UDP, + channel_id: (), + }, + // v4 (double vlan) + IpFragId { + outer_vlan_id: Some(VlanId::try_new(12).unwrap()), + inner_vlan_id: Some(VlanId::try_new(23).unwrap()), + ip: IpFragVersionSpecId::Ipv4 { + source: [1, 2, 3, 4], + destination: [5, 6, 7, 8], + identification: 9, + }, + payload_ip_number: IpNumber::UDP, + channel_id: (), + }, + // v6 (no vlan) + IpFragId { + outer_vlan_id: None, + inner_vlan_id: None, + ip: IpFragVersionSpecId::Ipv6 { + source: [0; 16], + destination: [0; 16], + identification: 0, + }, + payload_ip_number: IpNumber::UDP, + channel_id: (), + }, + // v6 (single vlan) + IpFragId { + outer_vlan_id: Some(VlanId::try_new(12).unwrap()), + inner_vlan_id: None, + ip: IpFragVersionSpecId::Ipv6 { + source: [0; 16], + destination: [0; 16], + identification: 0, + }, + payload_ip_number: IpNumber::UDP, + channel_id: (), + }, + // v6 (double vlan) + IpFragId { + outer_vlan_id: Some(VlanId::try_new(12).unwrap()), + inner_vlan_id: Some(VlanId::try_new(23).unwrap()), + ip: IpFragVersionSpecId::Ipv6 { + source: [0; 16], + destination: [0; 16], + identification: 0, + }, + payload_ip_number: IpNumber::UDP, + channel_id: (), + }, + ]; + + let mut pool = IpDefragPool::<(), ()>::new(); + + for frag_id in frag_ids { + { + let pdata = build_packet(frag_id.clone(), 0, true, &[1, 2, 3, 4, 5, 6, 7, 8]); + let pslice = SlicedPacket::from_ethernet(&pdata).unwrap(); + let v = pool.process_sliced_packet(&pslice, (), ()); + assert_eq!(Ok(None), v); + + // check the frag id was correctly calculated + assert_eq!(1, pool.active.len()); + assert_eq!(pool.active.iter().next().unwrap().0, &frag_id); + } + + { + let pdata = build_packet(frag_id.clone(), 1, false, &[9, 10]); + let pslice = SlicedPacket::from_ethernet(&pdata).unwrap(); + let v = pool + .process_sliced_packet(&pslice, (), ()) + .unwrap() + .unwrap(); + assert_eq!(v.ip_number, IpNumber::UDP); + assert_eq!( + v.len_source, + if matches!( + frag_id.ip, + IpFragVersionSpecId::Ipv4 { + source: _, + destination: _, + identification: _ + } + ) { + LenSource::Ipv4HeaderTotalLen + } else { + LenSource::Ipv6HeaderPayloadLen + } + ); + assert_eq!(v.payload, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + // there should be nothing left + assert_eq!(pool.active.len(), 0); + assert_eq!(pool.finished_data_bufs.len(), 0); + assert_eq!(pool.finished_section_bufs.len(), 1); + + // return buffer + pool.return_buf(v); + + assert_eq!(pool.active.len(), 0); + assert_eq!(pool.finished_data_bufs.len(), 1); + assert_eq!(pool.finished_section_bufs.len(), 1); + } + } + } + } + + #[test] + fn retain() { + let frag_id_0 = IpFragId { + outer_vlan_id: None, + inner_vlan_id: None, + ip: IpFragVersionSpecId::Ipv4 { + source: [1, 2, 3, 4], + destination: [5, 6, 7, 8], + identification: 0, + }, + payload_ip_number: IpNumber::UDP, + channel_id: (), + }; + let frag_id_1 = IpFragId { + outer_vlan_id: None, + inner_vlan_id: None, + ip: IpFragVersionSpecId::Ipv4 { + source: [1, 2, 3, 4], + destination: [5, 6, 7, 8], + identification: 1, + }, + payload_ip_number: IpNumber::UDP, + channel_id: (), + }; + + let mut pool = IpDefragPool::::new(); + + // packet timestamp 1 + { + let pdata = build_packet(frag_id_0.clone(), 0, true, &[1, 2, 3, 4, 5, 6, 7, 8]); + let pslice = SlicedPacket::from_ethernet(&pdata).unwrap(); + let v = pool.process_sliced_packet(&pslice, 1, ()); + assert_eq!(Ok(None), v); + } + // packet timestamp 2 + { + let pdata = build_packet(frag_id_1.clone(), 0, true, &[1, 2, 3, 4, 5, 6, 7, 8]); + let pslice = SlicedPacket::from_ethernet(&pdata).unwrap(); + let v = pool.process_sliced_packet(&pslice, 2, ()); + assert_eq!(Ok(None), v); + } + + // check buffers are active + assert_eq!(pool.active.len(), 2); + assert_eq!(pool.finished_data_bufs.len(), 0); + assert_eq!(pool.finished_section_bufs.len(), 0); + + // call retain without effect + pool.retain(|ts| *ts > 0); + assert_eq!(pool.active.len(), 2); + assert_eq!(pool.finished_data_bufs.len(), 0); + assert_eq!(pool.finished_section_bufs.len(), 0); + + // call retain and delete timestamp 1 + pool.retain(|ts| *ts > 1); + assert_eq!(pool.active.len(), 1); + assert_eq!(pool.finished_data_bufs.len(), 1); + assert_eq!(pool.finished_section_bufs.len(), 1); + assert_eq!(pool.active.iter().next().unwrap().0, &frag_id_1); + } +} diff --git a/etherparse/src/lib.rs b/etherparse/src/lib.rs index 8863133c..2da16e7e 100644 --- a/etherparse/src/lib.rs +++ b/etherparse/src/lib.rs @@ -1,4 +1,4 @@ -//! A zero allocation library for parsing & writing a bunch of packet based protocols (EthernetII, IPv4, IPv6, UDP, TCP ...). +//! A zero allocation supporting library for parsing & writing a bunch of packet based protocols (EthernetII, IPv4, IPv6, UDP, TCP ...). //! //! Currently supported are: //! * Ethernet II @@ -9,13 +9,15 @@ //! * TCP //! * ICMP & ICMPv6 (not all message types are supported) //! +//! Reconstruction of fragmented IP packets is also supported, but requires allocations. +//! //! # Usage //! //! Add the following to your `Cargo.toml`: //! //! ```toml //! [dependencies] -//! etherparse = "0.15" +//! etherparse = "0.16" //! ``` //! //! # What is etherparse? diff --git a/etherparse/src/sliced_packet.rs b/etherparse/src/sliced_packet.rs index b5feff8e..54a88465 100644 --- a/etherparse/src/sliced_packet.rs +++ b/etherparse/src/sliced_packet.rs @@ -337,6 +337,16 @@ impl<'a> SlicedPacket<'a> { None } } + + /// Returns true if `net` contains an fragmented IPv4 or IPv6 payload. + pub fn is_ip_payload_fragmented(&self) -> bool { + use NetSlice::*; + match &self.net { + Some(Ipv4(v)) => v.is_payload_fragmented(), + Some(Ipv6(v)) => v.is_payload_fragmented(), + None => false, + } + } } #[cfg(test)] diff --git a/etherparse/src/transport/transport_slice.rs b/etherparse/src/transport/transport_slice.rs index a7b46b51..8cfab0bb 100644 --- a/etherparse/src/transport/transport_slice.rs +++ b/etherparse/src/transport/transport_slice.rs @@ -1,14 +1,18 @@ use crate::*; +/// Slice containing UDP, TCP, ICMP or ICMPv4 header & payload. #[derive(Clone, Debug, Eq, PartialEq)] pub enum TransportSlice<'a> { /// A slice containing an Icmp4 header & payload. Icmpv4(Icmpv4Slice<'a>), + /// A slice containing an Icmp6 header & payload. Icmpv6(Icmpv6Slice<'a>), + /// A slice containing an UDP header & payload. Udp(UdpSlice<'a>), - /// A slice containing a TCP header. + + /// A slice containing a TCP header & payload. Tcp(TcpSlice<'a>), }