use std::collections::{BTreeMap, HashMap};
use std::net::IpAddr;
use std::time::Duration;
use crate::Timestamp;
#[cfg(feature = "extractors")]
use crate::layers::Ipv4Slice;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FragmentKey {
pub src: IpAddr,
pub dst: IpAddr,
pub protocol: u8,
pub id: u32,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FragmentConfig {
pub max_datagrams: usize,
pub max_datagram_bytes: usize,
pub timeout: Duration,
}
impl Default for FragmentConfig {
fn default() -> Self {
Self {
max_datagrams: 4096,
max_datagram_bytes: 65_535,
timeout: Duration::from_secs(30),
}
}
}
struct Reassembly {
fragments: BTreeMap<usize, Vec<u8>>,
total_len: Option<usize>,
buffered: usize,
first_seen: Timestamp,
poisoned: bool,
}
#[derive(Default)]
pub struct IpFragmentReassembler {
pending: HashMap<FragmentKey, Reassembly>,
config: FragmentConfig,
reassembled: u64,
timed_out: u64,
overlaps: u64,
oversize_dropped: u64,
}
impl IpFragmentReassembler {
pub fn new() -> Self {
Self::with_config(FragmentConfig::default())
}
pub fn with_config(config: FragmentConfig) -> Self {
Self {
pending: HashMap::new(),
config,
reassembled: 0,
timed_out: 0,
overlaps: 0,
oversize_dropped: 0,
}
}
pub fn push(
&mut self,
key: FragmentKey,
offset: usize,
more_fragments: bool,
payload: &[u8],
now: Timestamp,
) -> Option<Vec<u8>> {
if offset == 0 && !more_fragments {
return Some(payload.to_vec());
}
self.evict_expired(now);
if !self.pending.contains_key(&key) && self.pending.len() >= self.config.max_datagrams {
self.drop_oldest();
}
let cap = self.config.max_datagram_bytes;
let entry = self.pending.entry(key).or_insert_with(|| Reassembly {
fragments: BTreeMap::new(),
total_len: None,
buffered: 0,
first_seen: now,
poisoned: false,
});
if entry.poisoned {
return None;
}
let end = offset.saturating_add(payload.len());
let overlaps = entry.fragments.iter().any(|(&o, data)| {
let o_end = o + data.len();
offset < o_end && o < end
});
if overlaps {
self.overlaps += 1;
self.pending.remove(&key);
return None;
}
if entry.buffered + payload.len() > cap || end > cap {
self.oversize_dropped += 1;
self.pending.remove(&key);
return None;
}
entry.buffered += payload.len();
entry.fragments.insert(offset, payload.to_vec());
if !more_fragments {
entry.total_len = Some(end);
}
if let Some(total) = entry.total_len
&& is_contiguous(&entry.fragments, total)
{
let mut out = Vec::with_capacity(total);
for (_, data) in std::mem::take(&mut entry.fragments) {
out.extend_from_slice(&data);
}
self.pending.remove(&key);
self.reassembled += 1;
return Some(out);
}
None
}
#[cfg(feature = "extractors")]
pub fn push_ipv4(&mut self, ip: &Ipv4Slice<'_>, now: Timestamp) -> Option<Vec<u8>> {
let key = FragmentKey {
src: IpAddr::V4(ip.source()),
dst: IpAddr::V4(ip.destination()),
protocol: ip.protocol(),
id: ip.identification() as u32,
};
let offset = (ip.fragment_offset() as usize) * 8;
self.push(key, offset, ip.mf(), ip.payload(), now)
}
pub fn evict_expired(&mut self, now: Timestamp) -> usize {
let timeout = self.config.timeout;
let before = self.pending.len();
self.pending
.retain(|_, r| now.to_duration().saturating_sub(r.first_seen.to_duration()) <= timeout);
let dropped = before - self.pending.len();
self.timed_out += dropped as u64;
dropped
}
fn drop_oldest(&mut self) {
if let Some(key) = self
.pending
.iter()
.min_by_key(|(_, r)| r.first_seen.to_duration())
.map(|(k, _)| *k)
{
self.pending.remove(&key);
}
}
pub fn pending_len(&self) -> usize {
self.pending.len()
}
pub fn reassembled(&self) -> u64 {
self.reassembled
}
pub fn timed_out(&self) -> u64 {
self.timed_out
}
pub fn overlaps(&self) -> u64 {
self.overlaps
}
pub fn oversize_dropped(&self) -> u64 {
self.oversize_dropped
}
}
fn is_contiguous(fragments: &BTreeMap<usize, Vec<u8>>, total: usize) -> bool {
let mut expected = 0usize;
for (&offset, data) in fragments {
if offset != expected {
return false;
}
expected += data.len();
}
expected == total
}
#[cfg(test)]
mod tests {
use super::*;
fn key() -> FragmentKey {
FragmentKey {
src: "10.0.0.1".parse().unwrap(),
dst: "10.0.0.2".parse().unwrap(),
protocol: 17,
id: 42,
}
}
fn t(s: u32) -> Timestamp {
Timestamp::new(s, 0)
}
#[test]
fn reassembles_two_in_order_fragments() {
let mut r = IpFragmentReassembler::new();
assert!(r.push(key(), 0, true, b"AAAAAAAA", t(0)).is_none());
let out = r.push(key(), 8, false, b"BBBBB", t(0)).unwrap();
assert_eq!(out, b"AAAAAAAABBBBB");
assert_eq!(r.reassembled(), 1);
assert_eq!(r.pending_len(), 0);
}
#[test]
fn reassembles_out_of_order() {
let mut r = IpFragmentReassembler::new();
assert!(r.push(key(), 8, false, b"BBBBB", t(0)).is_none());
let out = r.push(key(), 0, true, b"AAAAAAAA", t(0)).unwrap();
assert_eq!(out, b"AAAAAAAABBBBB");
}
#[test]
fn non_fragment_returns_immediately() {
let mut r = IpFragmentReassembler::new();
let out = r.push(key(), 0, false, b"whole", t(0)).unwrap();
assert_eq!(out, b"whole");
assert_eq!(r.pending_len(), 0);
}
#[test]
fn overlapping_fragments_drop_datagram() {
let mut r = IpFragmentReassembler::new();
r.push(key(), 0, true, b"AAAAAAAA", t(0));
assert!(r.push(key(), 4, false, b"XXXXXXXX", t(0)).is_none());
assert_eq!(r.overlaps(), 1);
assert_eq!(r.pending_len(), 0, "poisoned datagram dropped");
}
#[test]
fn missing_fragment_never_completes() {
let mut r = IpFragmentReassembler::new();
assert!(r.push(key(), 0, true, b"AAAAAAAA", t(0)).is_none());
assert!(r.push(key(), 16, false, b"CCCCC", t(0)).is_none());
assert_eq!(r.pending_len(), 1);
}
#[test]
fn timeout_evicts_incomplete() {
let mut r = IpFragmentReassembler::new();
r.push(key(), 0, true, b"AAAAAAAA", t(0));
assert_eq!(r.pending_len(), 1);
let dropped = r.evict_expired(t(31));
assert_eq!(dropped, 1);
assert_eq!(r.timed_out(), 1);
}
#[cfg(feature = "extractors")]
fn ipv4_fragment(id: u16, offset_units: u16, mf: bool, payload: &[u8]) -> Vec<u8> {
let mut h = vec![
0x45, 0x00, 0x00, 0x00, ];
h.extend_from_slice(&id.to_be_bytes());
let frag = (offset_units & 0x1FFF) | if mf { 0x2000 } else { 0 };
h.extend_from_slice(&frag.to_be_bytes());
h.push(64); h.push(17); h.extend_from_slice(&[0, 0]); h.extend_from_slice(&[10, 0, 0, 1]); h.extend_from_slice(&[10, 0, 0, 2]); h.extend_from_slice(payload);
h
}
#[cfg(feature = "extractors")]
#[test]
fn push_ipv4_extracts_key_and_reassembles() {
use crate::layers::Ipv4Slice;
let mut r = IpFragmentReassembler::new();
let f1 = ipv4_fragment(7, 0, true, b"AAAAAAAA");
assert!(r.push_ipv4(&Ipv4Slice::new(&f1, 20), t(0)).is_none());
let f2 = ipv4_fragment(7, 1, false, b"BBBB");
let out = r.push_ipv4(&Ipv4Slice::new(&f2, 20), t(0)).unwrap();
assert_eq!(out, b"AAAAAAAABBBB");
assert_eq!(r.reassembled(), 1);
}
#[test]
fn oversize_far_offset_dropped() {
let cfg = FragmentConfig {
max_datagram_bytes: 1000,
..FragmentConfig::default()
};
let mut r = IpFragmentReassembler::with_config(cfg);
assert!(r.push(key(), 2000, false, b"Z", t(0)).is_none());
assert_eq!(r.oversize_dropped(), 1);
}
}