diff --git a/ci.sh b/ci.sh index 2c2dca67a..05853d244 100755 --- a/ci.sh +++ b/ci.sh @@ -23,6 +23,7 @@ FEATURES_TEST=( "std,medium-ethernet,proto-ipv4,socket-udp,socket-tcp,socket-dns" "std,medium-ethernet,proto-ipv4,proto-dhcpv4,socket-udp" "std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv6,multicast,proto-rpl,socket-udp,socket-dns,auto-icmp-echo-reply" + "std,medium-ethernet,medium-ip,proto-ipv6,proto-ipv6-fragmentation,multicast,socket-raw,socket-udp,socket-tcp,socket-icmp,auto-icmp-echo-reply" "std,medium-ethernet,proto-ipv6,socket-tcp" "std,medium-ethernet,proto-ipv6,socket-tcp,proto-ipv6-slaac" "std,medium-ethernet,medium-ip,proto-ipv4,socket-icmp,socket-tcp" diff --git a/src/iface/fragmentation.rs b/src/iface/fragmentation.rs index aa0341dca..e2ad6d843 100644 --- a/src/iface/fragmentation.rs +++ b/src/iface/fragmentation.rs @@ -55,6 +55,31 @@ pub struct PacketAssembler { assembler: Assembler, total_size: Option, expires_at: Instant, + + /// The largest datagram this slot may assemble, stamped from the set + /// when the slot is handed out. + max_len: usize, + + /// What the FIRST fragment of an IPv6 datagram said, kept because the + /// reassembled packet's header comes from it (RFC 8200 section 4.5) and + /// the fragment that completes the datagram is usually not that one. + /// + /// It lives here rather than in a table beside the set so that it is + /// cleared by the same `reset` that frees the slot; a side table would be + /// one more thing to keep in step with expiry and eviction. + #[cfg(feature = "proto-ipv6-fragmentation")] + ipv6_first_fragment: Option, +} + +/// The fields of an IPv6 datagram that only its first fragment carries. +#[cfg(feature = "proto-ipv6-fragmentation")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub(crate) struct Ipv6FirstFragment { + /// The upper-layer protocol, from the first fragment's Fragment header. + pub next_header: IpProtocol, + /// The hop limit the reassembled packet is presented with. + pub hop_limit: u8, } impl PacketAssembler { @@ -71,13 +96,34 @@ impl PacketAssembler { assembler: Assembler::new(), total_size: None, expires_at: Instant::ZERO, + max_len: REASSEMBLY_MAX_LEN_DEFAULT, + + #[cfg(feature = "proto-ipv6-fragmentation")] + ipv6_first_fragment: None, } } + /// Record what this datagram's first fragment carried, if it has arrived. + #[cfg(feature = "proto-ipv6-fragmentation")] + pub(crate) fn set_ipv6_first_fragment(&mut self, first: Ipv6FirstFragment) { + self.ipv6_first_fragment = Some(first); + } + + /// What this datagram's first fragment carried, or `None` while it has + /// not arrived yet. + #[cfg(feature = "proto-ipv6-fragmentation")] + pub(crate) fn ipv6_first_fragment(&self) -> Option { + self.ipv6_first_fragment + } + pub(crate) fn reset(&mut self) { self.key = None; self.assembler.clear(); self.total_size = None; + #[cfg(feature = "proto-ipv6-fragmentation")] + { + self.ipv6_first_fragment = None; + } self.expires_at = Instant::ZERO; } @@ -89,6 +135,10 @@ impl PacketAssembler { return Err(AssemblerError); } + if size > self.max_len { + return Err(AssemblerError); + } + #[cfg(not(feature = "alloc"))] if self.buffer.len() < size { return Err(AssemblerError); @@ -126,7 +176,9 @@ impl PacketAssembler { offset ); - self.assembler.add(offset, len); + self.assembler + .add(offset, len) + .map_err(|_| AssemblerError)?; Ok(()) } @@ -134,9 +186,20 @@ impl PacketAssembler { /// /// # Errors /// - /// - Returns [`Error::PacketAssemblerBufferTooSmall`] when trying to add data into the buffer at a non-existing - /// place. + /// - Returns [`AssemblerError`] when trying to add data into the buffer at a non-existing + /// place, or when the range cannot be recorded because the assembler + /// already holds [`ASSEMBLER_MAX_SEGMENT_COUNT`] discontiguous ranges. + /// + /// [`ASSEMBLER_MAX_SEGMENT_COUNT`]: crate::config::ASSEMBLER_MAX_SEGMENT_COUNT pub(crate) fn add(&mut self, data: &[u8], offset: usize) -> Result<(), AssemblerError> { + // Before any growth: under `alloc` the buffer resizes to whatever a + // fragment's offset asks for, so without this one datagram could + // make the slot hold `max_len` octets on the strength of a single + // packet claiming a far offset. + if offset + data.len() > self.max_len { + return Err(AssemblerError); + } + #[cfg(not(feature = "alloc"))] if self.buffer.len() < offset + data.len() { return Err(AssemblerError); @@ -156,7 +219,9 @@ impl PacketAssembler { offset ); - self.assembler.add(offset, data.len()); + self.assembler + .add(offset, data.len()) + .map_err(|_| AssemblerError)?; Ok(()) } @@ -173,6 +238,13 @@ impl PacketAssembler { Some(&self.buffer[..total_size]) } + /// Whether a fragment at `offset` of `len` octets would land on top of + /// one already received (RFC 5722). + #[cfg(feature = "proto-ipv6-fragmentation")] + pub(crate) fn overlaps(&self, offset: usize, len: usize) -> bool { + self.assembler.overlaps(offset, len) + } + /// Returns `true` when all fragments have been received, otherwise `false`. pub(crate) fn is_complete(&self) -> bool { self.total_size == Some(self.assembler.peek_front()) @@ -184,10 +256,46 @@ impl PacketAssembler { } } +/// The largest datagram reassembly will produce unless an embedder lowers +/// it: the most an IP header can describe. +pub const REASSEMBLY_MAX_LEN_DEFAULT: usize = u16::MAX as usize; + +/// A datagram currently being reassembled, as an embedder sees it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ReassemblyEntry { + /// What identifies the datagram. + pub key: K, + /// Octets recorded so far. Not the same as `capacity`: a datagram + /// whose first fragment claimed a far offset holds a buffer much + /// larger than what has actually arrived, which is the shape a + /// memory budget has to be able to see. + pub received: usize, + /// The datagram's total length, once its final fragment has said. + pub total: Option, + /// When the slot is discarded if it is still incomplete. + pub expires_at: Instant, + /// Octets the slot's buffer is holding on the datagram's behalf. + pub capacity: usize, +} + +impl ReassemblyEntry { + fn of(slot: &PacketAssembler) -> Self { + Self { + key: slot.key.expect("a slot with no key is not an entry"), + received: slot.assembler.iter_data().map(|(a, b)| b - a).sum(), + total: slot.total_size, + expires_at: slot.expires_at, + capacity: slot.buffer.len(), + } + } +} + /// Set holding multiple [`PacketAssembler`]. #[derive(Debug)] pub struct PacketAssemblerSet { assemblers: [PacketAssembler; REASSEMBLY_BUFFER_COUNT], + max_len: usize, } impl PacketAssemblerSet { @@ -197,9 +305,24 @@ impl PacketAssemblerSet { pub fn new() -> Self { Self { assemblers: [Self::NEW_PA; REASSEMBLY_BUFFER_COUNT], + max_len: REASSEMBLY_MAX_LEN_DEFAULT, } } + /// The largest datagram any slot of this set will assemble. + pub fn max_len(&self) -> usize { + self.max_len + } + + /// Bound the largest datagram any slot of this set will assemble. + /// + /// Applies to slots handed out from now on; a datagram already being + /// assembled keeps the bound it started under, so lowering this never + /// strands a slot holding more than it is allowed to. + pub fn set_max_len(&mut self, max_len: usize) { + self.max_len = max_len; + } + /// Get a [`PacketAssembler`] for a specific key. /// /// If it doesn't exist, it is created, with the `expires_at` timestamp. @@ -221,11 +344,68 @@ impl PacketAssemblerSet { } let slot = empty_slot.ok_or(AssemblerFullError)?; + // Hand the slot over clean. Under `alloc` its buffer still carries + // the previous datagram's high-water mark, so without this a single + // large datagram would pin `max_len` octets per slot for the life of + // the interface; and clearing keeps stale octets of one datagram + // from ever being read as part of the next. + #[cfg(feature = "alloc")] + { + slot.buffer.clear(); + slot.buffer.shrink_to(REASSEMBLY_BUFFER_SIZE); + } slot.key = Some(*key); slot.expires_at = expires_at; + slot.max_len = self.max_len; Ok(slot) } + /// Every datagram currently being reassembled. + /// + /// An embedder that owns a budget of its own — a kernel charging + /// retained octets to an interface, a namespace or a tenant — needs to + /// see what is held and to be able to let go of it. Without this the + /// only lever is the timeout, which is the wrong instrument: it is + /// per-interface, it is coarse, and it cannot single out the datagram + /// whose owner has just gone away. + pub fn entries(&self) -> impl Iterator> + '_ { + self.assemblers + .iter() + .filter(|slot| !slot.is_free()) + .map(ReassemblyEntry::of) + } + + /// The datagram being reassembled under `key`, if there is one. + pub fn entry(&self, key: &K) -> Option> { + self.assemblers + .iter() + .find(|slot| slot.key.as_ref() == Some(key)) + .map(ReassemblyEntry::of) + } + + /// Discard the datagram being reassembled under `key`, freeing its + /// slot. Returns whether there was one. + pub fn evict(&mut self, key: &K) -> bool { + match self + .assemblers + .iter_mut() + .find(|slot| slot.key.as_ref() == Some(key)) + { + Some(slot) => { + slot.reset(); + true + } + None => false, + } + } + + /// Discard every datagram being reassembled. + pub fn clear(&mut self) { + for slot in &mut self.assemblers { + slot.reset(); + } + } + /// Remove all [`PacketAssembler`]s that are expired. pub fn remove_expired(&mut self, timestamp: Instant) { for frag in &mut self.assemblers { @@ -243,13 +423,48 @@ pub(crate) const MAX_DECOMPRESSED_LEN: usize = 1500; #[cfg(feature = "_proto-fragmentation")] #[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub(crate) enum FragKey { +pub enum FragKey { #[cfg(feature = "proto-ipv4-fragmentation")] Ipv4(Ipv4FragKey), + #[cfg(feature = "proto-ipv6-fragmentation")] + Ipv6(Ipv6FragKey), #[cfg(feature = "proto-sixlowpan-fragmentation")] Sixlowpan(SixlowpanFragKey), } +/// The reassembly state the IPv6 receive path needs, borrowed field by field +/// rather than as a whole [`FragmentsBuffer`]. +/// +/// The 6LoWPAN path is why. It decompresses into `FragmentsBuffer`'s +/// `decompress_buf` and hands the resulting slice to `process_ipv6`, so that +/// slice keeps an immutable borrow of one field alive for as long as the +/// packet it produces — which leaves no way to pass `&mut FragmentsBuffer` +/// alongside it. The assembler is a disjoint field, so it can still be +/// handed over on its own. +pub(crate) struct Ipv6Reassembly<'a> { + #[cfg(feature = "proto-ipv6-fragmentation")] + pub assembler: &'a mut PacketAssemblerSet, + #[cfg(feature = "proto-ipv6-fragmentation")] + pub timeout: Duration, + /// Keeps the lifetime and the type inhabited when IPv6 reassembly is + /// compiled out, so that callers need no `cfg` on the argument they pass. + #[cfg(not(feature = "proto-ipv6-fragmentation"))] + pub _borrow: core::marker::PhantomData<&'a mut ()>, +} + +impl<'a> From<&'a mut FragmentsBuffer> for Ipv6Reassembly<'a> { + fn from(_frag: &'a mut FragmentsBuffer) -> Self { + Self { + #[cfg(feature = "proto-ipv6-fragmentation")] + timeout: _frag.reassembly_timeout, + #[cfg(feature = "proto-ipv6-fragmentation")] + assembler: &mut _frag.assembler, + #[cfg(not(feature = "proto-ipv6-fragmentation"))] + _borrow: core::marker::PhantomData, + } + } +} + pub(crate) struct FragmentsBuffer { #[cfg(feature = "proto-sixlowpan")] pub decompress_buf: [u8; MAX_DECOMPRESSED_LEN], @@ -397,6 +612,7 @@ impl Fragmenter { #[cfg(test)] mod tests { use super::*; + use crate::config::ASSEMBLER_MAX_SEGMENT_COUNT; #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] struct Key { @@ -410,12 +626,90 @@ mod tests { p_assembler.set_total_size(5).unwrap(); let data = b"Rust"; - p_assembler.add(&data[..], 0); - p_assembler.add(&data[..], 1); + p_assembler.add(&data[..], 0).unwrap(); + p_assembler.add(&data[..], 1).unwrap(); assert_eq!(p_assembler.assemble(), Some(&b"RRust"[..])) } + /// `Assembler::add` refuses a range once `ASSEMBLER_MAX_SEGMENT_COUNT` + /// discontiguous ranges are already recorded. That refusal used to be + /// dropped on the floor: the bytes were copied into the buffer, the range + /// was not recorded, and the datagram could then never complete — it sat + /// in its slot until the reassembly timeout, with nothing reported. + #[test] + fn packet_assembler_reports_a_range_it_could_not_record() { + let mut p_assembler = PacketAssembler::::new(); + p_assembler.set_total_size(64).unwrap(); + + // One byte every other offset: each is its own contig, so the budget + // is spent after ASSEMBLER_MAX_SEGMENT_COUNT of them. + let mut offset = 0; + for _ in 0..ASSEMBLER_MAX_SEGMENT_COUNT { + p_assembler.add(&[0xff], offset).unwrap(); + offset += 2; + } + + assert_eq!(p_assembler.add(&[0xff], offset), Err(AssemblerError)); + } + + /// The bound is what multiplies by the slot count to give the worst + /// case, so it has to stop the growth rather than notice it afterwards. + #[test] + fn a_bounded_set_refuses_to_grow_past_its_ceiling() { + let mut set = PacketAssemblerSet::::new(); + assert_eq!(set.max_len(), REASSEMBLY_MAX_LEN_DEFAULT); + set.set_max_len(64); + + let assr = set.get(&Key { id: 1 }, Instant::ZERO).unwrap(); + assert_eq!(assr.add(&[0xff; 8], 56), Ok(()), "the last octet it may"); + assert_eq!( + assr.add(&[0xff; 8], 57), + Err(AssemblerError), + "one past, refused before anything is copied or resized" + ); + assert_eq!(assr.set_total_size(65), Err(AssemblerError)); + } + + /// A slot handed out for a new datagram starts clean: under `alloc` the + /// previous datagram's high-water mark would otherwise be pinned for + /// the life of the interface. + #[test] + #[cfg(feature = "alloc")] + fn a_reused_slot_gives_its_buffer_back() { + // Which slot `get` hands out is its own business — it happens to + // pick the last free one — so this looks at the whole set. + fn widest(set: &PacketAssemblerSet) -> usize { + set.assemblers + .iter() + .map(|slot| slot.buffer.capacity()) + .max() + .unwrap() + } + + let mut set = PacketAssemblerSet::::new(); + + let assr = set.get(&Key { id: 1 }, Instant::ZERO).unwrap(); + assr.set_total_size(8192).unwrap(); + assr.add(&[0xff; 8192], 0).unwrap(); + assert!(assr.assemble().is_some()); + assert!( + widest(&set) >= 8192, + "the datagram grew a buffer: {}", + widest(&set) + ); + + // Every slot handed out again, so every buffer is given back. + for id in 0..REASSEMBLY_BUFFER_COUNT { + let _ = set.get(&Key { id }, Instant::ZERO).unwrap(); + } + assert!( + widest(&set) <= REASSEMBLY_BUFFER_SIZE, + "a slot still holds {} octets", + widest(&set) + ); + } + #[test] fn packet_assembler_assemble() { let mut p_assembler = PacketAssembler::::new(); diff --git a/src/iface/interface/ethernet.rs b/src/iface/interface/ethernet.rs index 19d203b4b..40721d1cb 100644 --- a/src/iface/interface/ethernet.rs +++ b/src/iface/interface/ethernet.rs @@ -37,8 +37,14 @@ impl InterfaceInner { #[cfg(feature = "proto-ipv6")] EthernetProtocol::Ipv6 => { let ipv6_packet = check!(Ipv6Packet::new_checked(eth_frame.payload())); - self.process_ipv6(sockets, meta, eth_frame.src_addr().into(), &ipv6_packet) - .map(EthernetPacket::Ip) + self.process_ipv6( + sockets, + meta, + eth_frame.src_addr().into(), + &ipv6_packet, + Ipv6Reassembly::from(fragments), + ) + .map(EthernetPacket::Ip) } // Drop all other traffic. _ => None, diff --git a/src/iface/interface/ipv6.rs b/src/iface/interface/ipv6.rs index 27795c36e..43059c7cf 100644 --- a/src/iface/interface/ipv6.rs +++ b/src/iface/interface/ipv6.rs @@ -13,6 +13,43 @@ enum HopByHopResponse<'frame> { Discard(Option>), } +/// The same shape as [`HopByHopResponse`], for the Fragment header: either +/// the datagram is now whole and processing continues on it, or there is +/// nothing to deliver — because the fragment was absorbed, or refused, in +/// which case this carries the ICMPv6 answer it earned. +#[cfg(feature = "proto-ipv6-fragmentation")] +#[allow(clippy::large_enum_variant)] +enum Ipv6FragmentResponse<'frame> { + /// A complete datagram: its upper-layer protocol and its payload. + Continue((IpProtocol, &'frame [u8])), + /// Nothing to deliver, and maybe an ICMPv6 packet to send back. + Discard(Option>), +} + +/// Offset of the Payload Length field from the start of an IPv6 header, +/// which is where RFC 8200 section 4.5 points a Parameter Problem for a +/// non-final fragment that is not a multiple of 8 octets. +#[cfg(feature = "proto-ipv6-fragmentation")] +const IPV6_PAYLOAD_LEN_OFFSET: usize = 4; + +/// The largest payload an IPv6 header can describe, and so the largest +/// datagram reassembly may produce. +#[cfg(feature = "proto-ipv6-fragmentation")] +const IPV6_MAX_PAYLOAD_LEN: usize = u16::MAX as usize; + +/// The smallest header the named upper-layer protocol can have, or `None` +/// when this code cannot say — an extension header, or one it does not +/// know, in which case RFC 7112 is not enforced rather than guessed at. +#[cfg(feature = "proto-ipv6-fragmentation")] +const fn upper_layer_min_len(protocol: IpProtocol) -> Option { + match protocol { + IpProtocol::Udp => Some(8), + IpProtocol::Tcp => Some(20), + IpProtocol::Icmpv6 => Some(4), + _ => None, + } +} + // We implement `Default` such that we can use the check! macro. impl Default for HopByHopResponse<'_> { fn default() -> Self { @@ -192,8 +229,10 @@ impl InterfaceInner { meta: PacketMeta, source_hardware_addr: HardwareAddress, ipv6_packet: &Ipv6Packet<&'frame [u8]>, + _reassembly: Ipv6Reassembly<'frame>, ) -> Option> { - let ipv6_repr = check!(Ipv6Repr::parse(ipv6_packet)); + #[allow(unused_mut)] + let mut ipv6_repr = check!(Ipv6Repr::parse(ipv6_packet)); if !ipv6_repr.src_addr.x_is_unicast() { // Discard packets with non-unicast source addresses. @@ -236,6 +275,31 @@ impl InterfaceInner { return None; } + // Reassembly sits AFTER the address checks, unlike the IPv4 path's: + // RFC 8200 section 4.5 reassembles at the destination, so a datagram + // this interface is not the destination of must never take a slot. + // It sits before the raw-socket filter so that a raw socket is + // handed the datagram rather than its pieces. + #[cfg(feature = "proto-ipv6-fragmentation")] + let (next_header, ip_payload) = if next_header == IpProtocol::Ipv6Frag { + // Where the Fragment header begins inside the packet, which is + // what the ICMP pointers below are measured from: the fixed + // header plus whatever extension headers were consumed above. + let header_offset = + ipv6_repr.buffer_len() + (ipv6_packet.payload().len() - ip_payload.len()); + match self.process_ipv6_fragment( + &mut ipv6_repr, + header_offset, + ip_payload, + _reassembly, + ) { + Ipv6FragmentResponse::Discard(reply) => return reply, + Ipv6FragmentResponse::Continue(next) => next, + } + } else { + (next_header, ip_payload) + }; + #[cfg(feature = "socket-raw")] let handled_by_raw_socket = self.raw_socket_filter(sockets, &ipv6_repr.into(), ip_payload); #[cfg(not(feature = "socket-raw"))] @@ -318,6 +382,194 @@ impl InterfaceInner { )) } + /// Absorb one IPv6 fragment, and hand back the datagram once its last + /// hole is filled (RFC 8200 section 4.5). + #[cfg(feature = "proto-ipv6-fragmentation")] + fn process_ipv6_fragment<'frame>( + &mut self, + ipv6_repr: &mut Ipv6Repr, + header_offset: usize, + ip_payload: &'frame [u8], + reassembly: Ipv6Reassembly<'frame>, + ) -> Ipv6FragmentResponse<'frame> { + // A Fragment header is a fixed eight octets — Next Header, one + // reserved octet, then the six the wire type covers. It carries no + // Hdr Ext Len field, so it is parsed here rather than through + // `Ipv6ExtHeader`, whose second octet is a length. + const FRAG_HEADER_LEN: usize = 2 + 6; + + if ip_payload.len() < FRAG_HEADER_LEN { + net_debug!("IPv6 fragment header truncated"); + return Ipv6FragmentResponse::Discard(None); + } + let next_header = IpProtocol::from(ip_payload[0]); + let frag_header = match Ipv6FragmentHeader::new_checked(&ip_payload[2..FRAG_HEADER_LEN]) { + Ok(header) => header, + Err(_) => return Ipv6FragmentResponse::Discard(None), + }; + let frag_repr = match Ipv6FragmentRepr::parse(&frag_header) { + Ok(repr) => repr, + Err(_) => return Ipv6FragmentResponse::Discard(None), + }; + let data = &ip_payload[FRAG_HEADER_LEN..]; + // `frag_offset` is already in 8-octet units. + let offset = frag_repr.frag_offset as usize * 8; + + // RFC 6946: an atomic fragment — offset zero and no more fragments — + // is a whole datagram that happens to carry the header. It must not + // touch the reassembly queue, where it could otherwise collide with a + // genuinely fragmented datagram of the same identification. + if offset == 0 && !frag_repr.more_frags { + ipv6_repr.next_header = next_header; + ipv6_repr.payload_len = data.len(); + return Ipv6FragmentResponse::Continue((next_header, data)); + } + + // RFC 8200 section 4.5: a non-final fragment whose payload is not a + // multiple of 8 octets is discarded, pointing at Payload Length. + if frag_repr.more_frags && !data.len().is_multiple_of(8) { + net_debug!("IPv6 non-final fragment is not a multiple of 8 octets"); + return Ipv6FragmentResponse::Discard(self.ipv6_fragment_param_problem( + *ipv6_repr, + ip_payload, + Icmpv6ParamProblem::ErroneousHdrField, + IPV6_PAYLOAD_LEN_OFFSET as u32, + )); + } + + // ... and one that would carry the reassembled payload past 65535 + // octets is discarded, pointing at its Fragment Offset. + if offset + data.len() > IPV6_MAX_PAYLOAD_LEN { + net_debug!("IPv6 fragment would reassemble past 65535 octets"); + return Ipv6FragmentResponse::Discard(self.ipv6_fragment_param_problem( + *ipv6_repr, + ip_payload, + Icmpv6ParamProblem::ErroneousHdrField, + (header_offset + 2) as u32, + )); + } + + // RFC 7112: a first fragment that does not carry the whole header + // chain is discarded with code 3. The chain is not walked here — + // what is checked is that the upper-layer header the Fragment header + // names is itself present, which is the case the RFC was written + // for: a chain split so that a stateless filter cannot see the + // ports it is meant to be filtering on. + if offset == 0 + && let Some(min) = upper_layer_min_len(next_header) + && data.len() < min + { + net_debug!("IPv6 first fragment does not carry its upper-layer header"); + return Ipv6FragmentResponse::Discard(self.ipv6_fragment_param_problem( + *ipv6_repr, + ip_payload, + Icmpv6ParamProblem::IncompleteHdrChain, + header_offset as u32, + )); + } + + let key = FragKey::Ipv6(Ipv6FragKey { + src_addr: ipv6_repr.src_addr, + dst_addr: ipv6_repr.dst_addr, + ident: frag_repr.ident, + }); + let slot = match reassembly + .assembler + .get(&key, self.now + reassembly.timeout) + { + Ok(slot) => slot, + Err(_) => { + net_debug!("No available packet assembler for fragmented packet"); + return Ipv6FragmentResponse::Discard(None); + } + }; + + // RFC 5722: an overlapping fragment makes the whole datagram + // untrustworthy — two writers claim the same octets and the reader + // cannot say which the firewall in the middle saw — so everything + // received for it is dropped. Silently: an ICMP answer here would be + // a reflection whose size the attacker chooses. + // + // This treats a fragment that repeats an earlier one EXACTLY as an + // overlap too. It is the stricter reading: a same-offset, + // same-length repeat with different octets is precisely the rewrite + // the RFC is about, and telling it apart from a duplicate the + // network produced would mean comparing the payloads. The cost is + // that a genuinely duplicated fragment drops a datagram the upper + // layer then has to send again. + if slot.overlaps(offset, data.len()) { + net_debug!("IPv6 fragment overlaps one already received; dropping the datagram"); + slot.reset(); + return Ipv6FragmentResponse::Discard(None); + } + + if !frag_repr.more_frags { + // The final fragment is the only one that fixes the total, and + // two finals that disagree are an attack, not a retransmission. + if slot.set_total_size(offset + data.len()).is_err() { + net_debug!("IPv6 fragment disagrees with the datagram's total length"); + slot.reset(); + return Ipv6FragmentResponse::Discard(None); + } + } + + if offset == 0 { + slot.set_ipv6_first_fragment(Ipv6FirstFragment { + next_header, + hop_limit: ipv6_repr.hop_limit, + }); + } + + if let Err(e) = slot.add(data, offset) { + net_debug!("IPv6 fragmentation error: {:?}", e); + slot.reset(); + return Ipv6FragmentResponse::Discard(None); + } + + // Read before `assemble`, which resets the slot and with it this. + let Some(first) = slot.ipv6_first_fragment() else { + return Ipv6FragmentResponse::Discard(None); + }; + let Some(payload) = slot.assemble() else { + return Ipv6FragmentResponse::Discard(None); + }; + + // RFC 8200 section 4.5: the reassembled packet's header comes from + // the FIRST fragment, not from whichever one completed it. + ipv6_repr.next_header = first.next_header; + ipv6_repr.hop_limit = first.hop_limit; + ipv6_repr.payload_len = payload.len(); + Ipv6FragmentResponse::Continue((first.next_header, payload)) + } + + /// The Parameter Problem an offending fragment earns, or `None` when + /// RFC 4443 section 2.4 forbids answering it. + #[cfg(feature = "proto-ipv6-fragmentation")] + fn ipv6_fragment_param_problem<'frame>( + &mut self, + ipv6_repr: Ipv6Repr, + ip_payload: &'frame [u8], + reason: Icmpv6ParamProblem, + pointer: u32, + ) -> Option> { + // An ICMPv6 error is never sent for a packet addressed to a + // multicast group: the answer would be multiplied by every member. + if !ipv6_repr.dst_addr.x_is_unicast() { + return None; + } + let payload_len = + icmp_reply_payload_len(ip_payload.len(), IPV6_MIN_MTU, ipv6_repr.buffer_len()); + self.icmpv6_reply( + ipv6_repr, + Icmpv6Repr::ParamProblem { + reason, + pointer, + header: ipv6_repr, + data: &ip_payload[0..payload_len], + }, + ) + } + /// Given the next header value forward the payload onto the correct process /// function. fn process_nxt_hdr<'frame>( diff --git a/src/iface/interface/mod.rs b/src/iface/interface/mod.rs index b17571e85..aa6486193 100644 --- a/src/iface/interface/mod.rs +++ b/src/iface/interface/mod.rs @@ -31,8 +31,15 @@ use heapless::Vec; #[cfg(feature = "_proto-fragmentation")] use super::fragmentation::FragKey; -#[cfg(any(feature = "proto-ipv4", feature = "proto-sixlowpan"))] -use super::fragmentation::PacketAssemblerSet; +// Gated on the umbrella rather than on ipv4/sixlowpan: `FragmentsBuffer`'s +// `assembler` field is gated on the umbrella too, and `proto-ipv6-fragmentation` +// alone now reaches it. +#[cfg(feature = "_proto-fragmentation")] +use super::fragmentation::{PacketAssemblerSet, ReassemblyEntry}; +#[cfg(feature = "proto-ipv6-fragmentation")] +use super::fragmentation::Ipv6FirstFragment; +#[cfg(feature = "proto-ipv6")] +use super::fragmentation::Ipv6Reassembly; use super::fragmentation::{Fragmenter, FragmentsBuffer}; #[cfg(any(feature = "medium-ethernet", feature = "medium-ieee802154"))] @@ -447,6 +454,60 @@ impl Interface { self.fragments.reassembly_timeout } + /// Get the largest datagram reassembly will produce. + #[cfg(feature = "_proto-fragmentation")] + pub fn reassembly_max_len(&self) -> usize { + self.fragments.assembler.max_len() + } + + /// Bound the largest datagram reassembly will produce. + /// + /// Defaults to 65535, the most an IP header can describe. Lower it to + /// bound what an interface can be made to hold: under `alloc` a slot's + /// buffer grows to whatever offset a fragment claims, so the ceiling is + /// what multiplies by the reassembly slot count to give the worst case. + /// + /// Applies to datagrams started from now on. A fragment that would + /// carry a datagram past the bound is discarded, and discarded quietly: + /// this is a local resource decision, not a protocol error the sender + /// could have avoided. + #[cfg(feature = "_proto-fragmentation")] + pub fn set_reassembly_max_len(&mut self, max_len: usize) { + self.fragments.assembler.set_max_len(max_len); + } + + /// Every datagram this interface is currently reassembling. + /// + /// For an embedder that keeps a budget of its own — retained octets + /// charged to an interface, a namespace, a tenant — and needs to see + /// what is held rather than infer it from the timeout. + #[cfg(feature = "_proto-fragmentation")] + pub fn reassembly_entries(&self) -> impl Iterator> + '_ { + self.fragments.assembler.entries() + } + + /// The datagram being reassembled under `key`, if there is one. + #[cfg(feature = "_proto-fragmentation")] + pub fn reassembly_entry(&self, key: &FragKey) -> Option> { + self.fragments.assembler.entry(key) + } + + /// Discard one datagram being reassembled, freeing its slot and its + /// buffer. Returns whether there was one. + /// + /// The timeout cannot do this job: it is per-interface and coarse, and + /// it cannot single out the datagram whose owner has just gone away. + #[cfg(feature = "_proto-fragmentation")] + pub fn reassembly_evict(&mut self, key: &FragKey) -> bool { + self.fragments.assembler.evict(key) + } + + /// Discard every datagram being reassembled. + #[cfg(feature = "_proto-fragmentation")] + pub fn reassembly_clear(&mut self) { + self.fragments.assembler.clear(); + } + /// Set the packet reassembly timeout. #[cfg(feature = "_proto-fragmentation")] pub fn set_reassembly_timeout(&mut self, timeout: Duration) { @@ -954,7 +1015,13 @@ impl InterfaceInner { #[cfg(feature = "proto-ipv6")] Ok(IpVersion::Ipv6) => { let ipv6_packet = check!(Ipv6Packet::new_checked(ip_payload)); - self.process_ipv6(sockets, meta, HardwareAddress::Ip, &ipv6_packet) + self.process_ipv6( + sockets, + meta, + HardwareAddress::Ip, + &ipv6_packet, + Ipv6Reassembly::from(frag), + ) } // Drop all other traffic. _ => None, diff --git a/src/iface/interface/sixlowpan.rs b/src/iface/interface/sixlowpan.rs index 2f4693627..a22e795ac 100644 --- a/src/iface/interface/sixlowpan.rs +++ b/src/iface/interface/sixlowpan.rs @@ -87,6 +87,18 @@ impl InterfaceInner { } }; + // `payload` borrows `f.decompress_buf` for as long as the packet this + // returns, so the assembler beside it can only travel as a disjoint + // field borrow — see `Ipv6Reassembly`. + let reassembly = Ipv6Reassembly { + #[cfg(feature = "proto-ipv6-fragmentation")] + timeout: f.reassembly_timeout, + #[cfg(feature = "proto-ipv6-fragmentation")] + assembler: &mut f.assembler, + #[cfg(not(feature = "proto-ipv6-fragmentation"))] + _borrow: core::marker::PhantomData, + }; + self.process_ipv6( sockets, meta, @@ -95,6 +107,7 @@ impl InterfaceInner { None => HardwareAddress::Ieee802154(Ieee802154Address::Absent), }, &check!(Ipv6Packet::new_checked(payload)), + reassembly, ) } diff --git a/src/iface/interface/tests/ipv6.rs b/src/iface/interface/tests/ipv6.rs index 427cb6ecf..8a4430101 100644 --- a/src/iface/interface/tests/ipv6.rs +++ b/src/iface/interface/tests/ipv6.rs @@ -85,7 +85,8 @@ fn any_ip(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), None ); @@ -99,7 +100,8 @@ fn any_ip(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ) .is_some() ); @@ -128,7 +130,8 @@ fn multicast_source_address(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -177,7 +180,8 @@ fn hop_by_hop_skip_with_icmp(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -213,7 +217,8 @@ fn hop_by_hop_discard_with_icmp(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -268,7 +273,8 @@ fn hop_by_hop_discard_param_problem(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -326,7 +332,8 @@ fn hop_by_hop_discard_with_multicast(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -386,7 +393,8 @@ fn imcp_empty_echo_request(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -447,7 +455,8 @@ fn icmp_echo_request(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -495,7 +504,8 @@ fn icmp_echo_reply_as_input(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -544,7 +554,8 @@ fn unknown_proto_with_multicast_dst_address(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -594,7 +605,8 @@ fn unknown_proto(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -639,7 +651,8 @@ fn ndisc_neighbor_advertisement_ethernet(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -696,7 +709,8 @@ fn ndisc_neighbor_advertisement_ethernet_multicast_addr(#[case] medium: Medium) &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -749,7 +763,8 @@ fn ndisc_neighbor_advertisement_ieee802154(#[case] medium: Medium) { &mut sockets, PacketMeta::default(), HardwareAddress::default(), - &Ipv6Packet::new_checked(&data[..]).unwrap() + &Ipv6Packet::new_checked(&data[..]).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments) ), response ); @@ -1866,3 +1881,444 @@ fn test_solicited_node_multicast_autojoin(#[case] medium: Medium) { assert!(!iface.has_multicast_group(addr1.solicited_node())); assert!(!iface.has_multicast_group(addr2.solicited_node())); } + +#[cfg(feature = "proto-ipv6-fragmentation")] +mod fragmentation { + use super::*; + + const REMOTE: Ipv6Address = Ipv6Address::new(0xfdbe, 0, 0, 0, 0, 0, 0, 0x0002); + const LOCAL: Ipv6Address = Ipv6Address::new(0xfdbe, 0, 0, 0, 0, 0, 0, 0x0001); + /// Offset of the Fragment header's Fragment Offset field from the start + /// of a packet whose only extension header is the Fragment header. + const FRAG_OFFSET_FIELD: u32 = 40 + 2; + + /// `[IPv6 header][Fragment header][data]`, emitted by hand because the + /// dispatch path cannot produce IPv6 fragments. + #[allow(clippy::too_many_arguments)] + fn fragment( + dst_addr: Ipv6Address, + next_header: IpProtocol, + ident: u32, + frag_offset: u16, + more_frags: bool, + hop_limit: u8, + data: &[u8], + ) -> Vec { + let repr = Ipv6Repr { + src_addr: REMOTE, + dst_addr, + next_header: IpProtocol::Ipv6Frag, + payload_len: 8 + data.len(), + hop_limit, + }; + let mut bytes = std::vec![0u8; repr.buffer_len() + repr.payload_len]; + repr.emit(&mut Ipv6Packet::new_unchecked(&mut bytes[..])); + + bytes[40] = u8::from(next_header); + bytes[41] = 0; + { + let mut header = Ipv6FragmentHeader::new_unchecked(&mut bytes[42..48]); + header.clear_reserved(); + header.set_frag_offset(frag_offset); + header.set_more_frags(more_frags); + header.set_ident(ident); + } + bytes[48..].copy_from_slice(data); + bytes + } + + /// A complete ICMPv6 echo request, checksummed over the whole message — + /// which is what makes reassembly observable end to end: the reply only + /// comes back if every octet landed at the right offset. + fn echo_request(payload: &[u8]) -> Vec { + let repr = Icmpv6Repr::EchoRequest { + ident: 0x1234, + seq_no: 0x5678, + data: payload, + }; + let mut bytes = std::vec![0u8; repr.buffer_len()]; + repr.emit( + &REMOTE, + &LOCAL, + &mut Icmpv6Packet::new_unchecked(&mut bytes[..]), + &ChecksumCapabilities::default(), + ); + bytes + } + + fn feed<'a>( + iface: &'a mut Interface, + sockets: &mut SocketSet<'_>, + data: &'a [u8], + ) -> Option> { + iface.inner.process_ipv6( + sockets, + PacketMeta::default(), + HardwareAddress::default(), + &Ipv6Packet::new_checked(data).unwrap(), + Ipv6Reassembly::from(&mut iface.fragments), + ) + } + + /// The reply an echo request of `payload` should draw once reassembled. + fn expect_echo_reply(packet: Option>, payload: &[u8]) { + match packet { + Some(Packet::Ipv6(p)) => match p.payload { + IpPayload::Icmpv6(Icmpv6Repr::EchoReply { + ident, + seq_no, + data, + }) => { + assert_eq!(ident, 0x1234); + assert_eq!(seq_no, 0x5678); + assert_eq!(data, payload, "every octet, at its own offset"); + } + other => panic!("expected an echo reply, got {other:?}"), + }, + other => panic!("expected an IPv6 packet, got {other:?}"), + } + } + + /// The Parameter Problem a refused fragment should draw. + fn expect_param_problem( + packet: Option>, + expected_reason: Icmpv6ParamProblem, + expected_pointer: u32, + ) { + match packet { + Some(Packet::Ipv6(p)) => match p.payload { + IpPayload::Icmpv6(Icmpv6Repr::ParamProblem { + reason, pointer, .. + }) => { + assert_eq!(reason, expected_reason); + assert_eq!(pointer, expected_pointer); + } + other => panic!("expected a parameter problem, got {other:?}"), + }, + other => panic!("expected an IPv6 packet, got {other:?}"), + } + } + + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn two_fragments_reassemble(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let payload: Vec = (0..64u8).collect(); + let message = echo_request(&payload); + assert_eq!(message.len(), 8 + 64); + + let first = fragment(LOCAL, IpProtocol::Icmpv6, 0xaa, 0, true, 64, &message[..40]); + let last = fragment(LOCAL, IpProtocol::Icmpv6, 0xaa, 5, false, 64, &message[40..]); + + assert!( + feed(&mut iface, &mut sockets, &first).is_none(), + "a first fragment delivers nothing on its own" + ); + expect_echo_reply(feed(&mut iface, &mut sockets, &last), &payload); + } + + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn fragments_reassemble_out_of_order(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let payload: Vec = (0..64u8).collect(); + let message = echo_request(&payload); + + let first = fragment(LOCAL, IpProtocol::Icmpv6, 0xbb, 0, true, 64, &message[..40]); + let last = fragment(LOCAL, IpProtocol::Icmpv6, 0xbb, 5, false, 64, &message[40..]); + + // The last fragment first: it fixes the total, and the datagram + // completes only when the head finally lands. + assert!(feed(&mut iface, &mut sockets, &last).is_none()); + expect_echo_reply(feed(&mut iface, &mut sockets, &first), &payload); + } + + /// RFC 6946: offset zero with no more fragments is a whole datagram. It + /// must not go through the assembler, where it would otherwise be able + /// to collide with a genuinely fragmented datagram of the same identity. + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn an_atomic_fragment_is_delivered_whole(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let payload: Vec = (0..16u8).collect(); + let message = echo_request(&payload); + + // A half-finished datagram under the SAME identification, which the + // atomic fragment must leave untouched. + let head = fragment(LOCAL, IpProtocol::Icmpv6, 0xcc, 0, true, 64, &message[..8]); + assert!(feed(&mut iface, &mut sockets, &head).is_none()); + + let atomic = fragment(LOCAL, IpProtocol::Icmpv6, 0xcc, 0, false, 64, &message); + expect_echo_reply(feed(&mut iface, &mut sockets, &atomic), &payload); + } + + /// RFC 8200 section 4.5: a non-final fragment that is not a multiple of + /// eight octets is discarded, pointing at Payload Length. + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn a_non_final_fragment_must_be_a_multiple_of_eight(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let data = [0u8; 12]; + let odd = fragment(LOCAL, IpProtocol::Icmpv6, 0xdd, 0, true, 64, &data); + expect_param_problem( + feed(&mut iface, &mut sockets, &odd), + Icmpv6ParamProblem::ErroneousHdrField, + 4, + ); + } + + /// ... and one whose tail would put the datagram past 65535 octets is + /// discarded, pointing at Fragment Offset. + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn a_fragment_past_the_payload_ceiling_is_refused(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let data = [0u8; 16]; + // 8191 * 8 + 16 = 65544. + let beyond = fragment(LOCAL, IpProtocol::Icmpv6, 0xee, 8191, true, 64, &data); + expect_param_problem( + feed(&mut iface, &mut sockets, &beyond), + Icmpv6ParamProblem::ErroneousHdrField, + FRAG_OFFSET_FIELD, + ); + } + + /// RFC 7112: a first fragment that does not carry its upper-layer header + /// is discarded with code 3. Splitting the chain so a stateless filter + /// cannot see the ports is the attack this closes. + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn a_first_fragment_without_its_upper_layer_header_is_refused(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + // Sixteen octets: a whole number of eight-octet units, so the length + // rule above lets it past, and still four short of a TCP header. + let stub = [0u8; 16]; + let split = fragment(LOCAL, IpProtocol::Tcp, 0x11, 0, true, 64, &stub); + expect_param_problem( + feed(&mut iface, &mut sockets, &split), + Icmpv6ParamProblem::IncompleteHdrChain, + 40, + ); + } + + /// RFC 8200 section 4.5: the reassembled packet's header fields come + /// from the FIRST fragment, not from whichever one completes it. + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn the_reassembled_header_comes_from_the_first_fragment(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let payload: Vec = (0..64u8).collect(); + let message = echo_request(&payload); + + // The first fragment names the protocol and carries hop limit 64; + // the last one lies about both. + let first = fragment(LOCAL, IpProtocol::Icmpv6, 0x22, 0, true, 64, &message[..40]); + let last = fragment( + LOCAL, + IpProtocol::Unknown(0xfd), + 0x22, + 5, + false, + 1, + &message[40..], + ); + + assert!(feed(&mut iface, &mut sockets, &first).is_none()); + // Still an echo reply: the ICMPv6 next header came from the first + // fragment. Had the last one's `Unknown(0xfd)` been taken, this + // would be an unrecognised-next-header parameter problem instead. + expect_echo_reply(feed(&mut iface, &mut sockets, &last), &payload); + } + + /// Two finals that disagree on where the datagram ends are an attack, + /// not a retransmission: the slot is dropped rather than resized. + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn finals_that_disagree_on_the_total_are_dropped(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let payload: Vec = (0..64u8).collect(); + let message = echo_request(&payload); + + let first = fragment(LOCAL, IpProtocol::Icmpv6, 0x33, 0, true, 64, &message[..40]); + let short_last = fragment(LOCAL, IpProtocol::Icmpv6, 0x33, 5, false, 64, &message[40..56]); + let real_last = fragment(LOCAL, IpProtocol::Icmpv6, 0x33, 5, false, 64, &message[40..]); + + assert!(feed(&mut iface, &mut sockets, &first).is_none()); + assert!(feed(&mut iface, &mut sockets, &short_last).is_none()); + // The second final disagrees, so the slot is reset and this one + // starts over rather than completing a datagram of two minds. + assert!( + feed(&mut iface, &mut sockets, &real_last).is_none(), + "the disagreement dropped the partial datagram" + ); + } + + /// RFC 4443 section 2.4: no ICMPv6 error for a packet sent to a + /// multicast group, or one answer is multiplied by every member. + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn a_refused_fragment_to_a_multicast_group_is_silent(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let group = Ipv6Address::new(0xff02, 0, 0, 0, 0, 0, 0, 1); + let data = [0u8; 12]; + let odd = fragment(group, IpProtocol::Icmpv6, 0x44, 0, true, 64, &data); + assert!( + feed(&mut iface, &mut sockets, &odd).is_none(), + "discarded, and silently" + ); + } + /// RFC 5722: an overlapping fragment condemns the whole datagram, and + /// silently — an ICMP answer would be a reflection sized by the sender. + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn an_overlapping_fragment_drops_the_datagram(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let payload: Vec = (0..64u8).collect(); + let message = echo_request(&payload); + + let first = fragment(LOCAL, IpProtocol::Icmpv6, 0x55, 0, true, 64, &message[..40]); + // Offset 32 reaches back into the octets the first fragment already + // claimed: 32..40 belongs to both. + let overlapping = fragment(LOCAL, IpProtocol::Icmpv6, 0x55, 4, true, 64, &message[32..48]); + let last = fragment(LOCAL, IpProtocol::Icmpv6, 0x55, 5, false, 64, &message[40..]); + + assert!(feed(&mut iface, &mut sockets, &first).is_none()); + assert!( + feed(&mut iface, &mut sockets, &overlapping).is_none(), + "the overlap is answered with nothing at all" + ); + assert!( + feed(&mut iface, &mut sockets, &last).is_none(), + "the datagram went with it: the final fragment completes nothing" + ); + } + + /// The strict reading, written down so the trade is visible: a fragment + /// that repeats an earlier one exactly counts as an overlap, because + /// telling a rewrite from a network duplicate would mean comparing + /// payloads. + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn an_exactly_repeated_fragment_counts_as_an_overlap(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let payload: Vec = (0..64u8).collect(); + let message = echo_request(&payload); + + let first = fragment(LOCAL, IpProtocol::Icmpv6, 0x66, 0, true, 64, &message[..40]); + let last = fragment(LOCAL, IpProtocol::Icmpv6, 0x66, 5, false, 64, &message[40..]); + + assert!(feed(&mut iface, &mut sockets, &first).is_none()); + assert!(feed(&mut iface, &mut sockets, &first.clone()).is_none()); + assert!( + feed(&mut iface, &mut sockets, &last).is_none(), + "the repeat dropped the datagram, so the final completes nothing" + ); + } + + /// The bound is a local resource decision, so it is enforced quietly — + /// the sender did nothing a protocol error could name. + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn a_datagram_past_the_reassembly_bound_is_dropped(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let payload: Vec = (0..64u8).collect(); + let message = echo_request(&payload); + assert_eq!(message.len(), 72); + + // Room for the head but not for the whole datagram. + iface.set_reassembly_max_len(48); + assert_eq!(iface.reassembly_max_len(), 48); + + let first = fragment(LOCAL, IpProtocol::Icmpv6, 0x77, 0, true, 64, &message[..40]); + let last = fragment(LOCAL, IpProtocol::Icmpv6, 0x77, 5, false, 64, &message[40..]); + + assert!(feed(&mut iface, &mut sockets, &first).is_none()); + assert!( + feed(&mut iface, &mut sockets, &last).is_none(), + "72 octets is past the bound, and nothing is said about it" + ); + + // Raised again, the same datagram reassembles: the bound was the + // only thing standing in its way. + iface.set_reassembly_max_len(65_535); + assert!(feed(&mut iface, &mut sockets, &first).is_none()); + expect_echo_reply(feed(&mut iface, &mut sockets, &last), &payload); + } + + /// What the API is for: an embedder keeping a budget of its own can see + /// what is held, name it, and let go of it — none of which the timeout + /// can do, being per-interface and coarse. + #[rstest] + #[case::ip(Medium::Ip)] + #[cfg(feature = "medium-ip")] + #[case::ethernet(Medium::Ethernet)] + #[cfg(feature = "medium-ethernet")] + fn a_half_assembled_datagram_can_be_seen_and_evicted(#[case] medium: Medium) { + let (mut iface, mut sockets, _device) = setup(medium); + let payload: Vec = (0..64u8).collect(); + let message = echo_request(&payload); + let first = fragment(LOCAL, IpProtocol::Icmpv6, 0x88, 0, true, 64, &message[..40]); + let last = fragment(LOCAL, IpProtocol::Icmpv6, 0x88, 5, false, 64, &message[40..]); + + assert_eq!(iface.reassembly_entries().count(), 0); + assert!(feed(&mut iface, &mut sockets, &first).is_none()); + + let entries: Vec<_> = iface.reassembly_entries().collect(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].received, 40); + assert_eq!(entries[0].total, None, "no final fragment has said yet"); + assert!(entries[0].capacity >= 40); + + // The identity an embedder derives from the wire names the same + // datagram the interface is holding. + let derived = FragKey::Ipv6(Ipv6FragKey::new(REMOTE, LOCAL, 0x88)); + assert_eq!(entries[0].key, derived); + assert!(iface.reassembly_entry(&derived).is_some()); + + assert!(iface.reassembly_evict(&derived)); + assert_eq!(iface.reassembly_entries().count(), 0); + assert!( + feed(&mut iface, &mut sockets, &last).is_none(), + "the head was let go of, so the final fragment completes nothing" + ); + + iface.reassembly_clear(); + assert_eq!(iface.reassembly_entries().count(), 0); + } +} diff --git a/src/iface/mod.rs b/src/iface/mod.rs index 16e3be29f..71ebf8627 100644 --- a/src/iface/mod.rs +++ b/src/iface/mod.rs @@ -24,6 +24,8 @@ pub use self::interface::{ Config, Interface, InterfaceInner as Context, PollIngressSingleResult, PollResult, }; +#[cfg(feature = "_proto-fragmentation")] +pub use self::fragmentation::{FragKey, ReassemblyEntry, REASSEMBLY_MAX_LEN_DEFAULT}; pub use self::route::{Route, RouteTableFull, Routes}; #[cfg(feature = "proto-ipv6-slaac")] pub use self::slaac::Slaac; diff --git a/src/storage/assembler.rs b/src/storage/assembler.rs index ec3886800..fb06aaedf 100644 --- a/src/storage/assembler.rs +++ b/src/storage/assembler.rs @@ -322,6 +322,20 @@ impl Assembler { /// |--- 100 ---|--- 200 ---|--- 100 ---| /// /// Would return the ranges: ``(0, 100), (300, 400)`` + /// Whether `[offset, offset + size)` intersects a range already recorded. + /// + /// RFC 5722 makes an overlapping IPv6 fragment a reason to discard the + /// whole datagram, so the receive path has to be able to ask BEFORE it + /// copies the fragment's octets over whatever is already in the buffer. + pub fn overlaps(&self, offset: usize, size: usize) -> bool { + if size == 0 { + return false; + } + let end = offset + size; + self.iter_data() + .any(|(left, right)| offset < right && left < end) + } + pub fn iter_data(&self) -> impl Iterator + '_ { let mut offset = 0; self.contigs.iter().filter_map(move |contig| { diff --git a/src/wire/icmpv6.rs b/src/wire/icmpv6.rs index d47383ee1..042a3d7d1 100644 --- a/src/wire/icmpv6.rs +++ b/src/wire/icmpv6.rs @@ -156,7 +156,9 @@ enum_with_unknown! { /// Unrecognized Next Header type encountered. UnrecognizedNxtHdr = 1, /// Unrecognized IPv6 option encountered. - UnrecognizedOption = 2 + UnrecognizedOption = 2, + /// IPv6 First Fragment has incomplete IPv6 Header Chain (RFC 7112). + IncompleteHdrChain = 3 } } @@ -166,6 +168,9 @@ impl fmt::Display for ParamProblem { ParamProblem::ErroneousHdrField => write!(f, "erroneous header field."), ParamProblem::UnrecognizedNxtHdr => write!(f, "unrecognized next header type."), ParamProblem::UnrecognizedOption => write!(f, "unrecognized IPv6 option."), + ParamProblem::IncompleteHdrChain => { + write!(f, "first fragment has an incomplete header chain.") + } ParamProblem::Unknown(id) => write!(f, "{id}"), } } diff --git a/src/wire/ipv6.rs b/src/wire/ipv6.rs index 941790151..0feda3fc9 100644 --- a/src/wire/ipv6.rs +++ b/src/wire/ipv6.rs @@ -77,6 +77,57 @@ impl From for MulticastScope { pub use core::net::Ipv6Addr as Address; +/// The identity fragments of one datagram share, per RFC 8200 section 4.5. +/// +/// Source address, destination address and the Fragment header's +/// Identification, and deliberately nothing else. Unlike [`ipv4::Key`], which +/// includes the protocol field, the upper-layer protocol is NOT part of an +/// IPv6 datagram's reassembly identity: it lives in the Fragment header's Next +/// Header field, which non-first fragments carry too but which the RFC does +/// not make part of the key. Keying on it would let a sender split one +/// datagram across two reassembly slots by lying in a later fragment. +/// +/// [`ipv4::Key`]: super::ipv4::Key +#[cfg(feature = "proto-ipv6-fragmentation")] +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Key { + pub(crate) src_addr: Address, + pub(crate) dst_addr: Address, + pub(crate) ident: u32, +} + +#[cfg(feature = "proto-ipv6-fragmentation")] +impl Key { + /// The identity of the datagram these three fields name. + /// + /// Public because an embedder that watches frames on their way in + /// derives the identity itself, and then has to be able to name the + /// datagram it derived when it asks the interface about it. + pub const fn new(src_addr: Address, dst_addr: Address, ident: u32) -> Self { + Self { + src_addr, + dst_addr, + ident, + } + } + + /// The source address fragments of this datagram share. + pub const fn src_addr(&self) -> Address { + self.src_addr + } + + /// The destination address fragments of this datagram share. + pub const fn dst_addr(&self) -> Address { + self.dst_addr + } + + /// The Fragment header Identification fragments of this datagram share. + pub const fn ident(&self) -> u32 { + self.ident + } +} + pub(crate) trait AddressExt { /// Create an IPv6 address based on the provided prefix and hardware identifier. fn from_link_prefix( diff --git a/src/wire/mod.rs b/src/wire/mod.rs index 37237191d..f3ec88ae8 100644 --- a/src/wire/mod.rs +++ b/src/wire/mod.rs @@ -204,6 +204,8 @@ pub use self::ipv6::{ LINK_LOCAL_ALL_RPL_NODES as IPV6_LINK_LOCAL_ALL_RPL_NODES, MIN_MTU as IPV6_MIN_MTU, Packet as Ipv6Packet, Repr as Ipv6Repr, }; +#[cfg(feature = "proto-ipv6-fragmentation")] +pub use self::ipv6::Key as Ipv6FragKey; #[cfg(feature = "proto-ipv6")] pub(crate) use self::ipv6::{AddressExt as Ipv6AddressExt, MulticastScope as Ipv6MulticastScope};