Skip to main content

rawlink/
pcap.rs

1//! Classic libpcap file reader, either byte order, microsecond or nanosecond
2//! magic. pcapng is rejected with a hint to convert.
3
4use anyhow::{bail, Context, Result};
5use std::path::Path;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub struct PcapPacket<'a> {
9    pub ts_sec: u32,
10    /// Microseconds, also for nanosecond-resolution files.
11    pub ts_usec: u32,
12    /// The Ethernet frame from the destination MAC.
13    pub data: &'a [u8],
14}
15
16const GLOBAL_HDR_LEN: usize = 24;
17const RECORD_HDR_LEN: usize = 16;
18
19/// A classic pcap file held in memory; `packets` walks it without copying.
20#[derive(Clone, Debug)]
21pub struct Pcap {
22    bytes: Vec<u8>,
23    header: Header,
24}
25
26impl Pcap {
27    /// # Errors
28    /// Fails if the global header is short or the magic is not classic pcap.
29    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
30        let header = Header::parse(&bytes)?;
31        Ok(Self { bytes, header })
32    }
33
34    pub fn packets(&self) -> Packets<'_> {
35        self.header.packets(&self.bytes)
36    }
37}
38
39/// # Errors
40/// Fails if the file cannot be read or is not a classic pcap file.
41pub fn read_pcap(path: impl AsRef<Path>) -> Result<Pcap> {
42    let path = path.as_ref();
43    let bytes = std::fs::read(path).with_context(|| format!("read {}", path.display()))?;
44    Pcap::from_bytes(bytes)
45}
46
47/// Walk the packets of a classic pcap file. A truncated last record is dropped.
48///
49/// # Errors
50/// Fails if the global header is short or the magic is not classic pcap.
51pub fn parse_pcap(d: &[u8]) -> Result<Packets<'_>> {
52    Ok(Header::parse(d)?.packets(d))
53}
54
55/// Record-header byte order and timestamp unit, from the magic number.
56#[derive(Clone, Copy, Debug)]
57struct Header {
58    read_u32: fn([u8; 4]) -> u32,
59    nano: bool,
60}
61
62impl Header {
63    fn parse(d: &[u8]) -> Result<Self> {
64        if d.len() < GLOBAL_HDR_LEN {
65            bail!("pcap too short");
66        }
67        let (le, nano) = match u32::from_le_bytes(bytes_at(d, 0)) {
68            0xa1b2_c3d4 => (true, false),
69            0xd4c3_b2a1 => (false, false),
70            0xa1b2_3c4d => (true, true),
71            0x4d3c_b2a1 => (false, true),
72            m => bail!(
73                "not a classic pcap file (magic {m:08x}); if pcapng, convert: tcpdump -r in -w out"
74            ),
75        };
76        let read_u32 = if le {
77            u32::from_le_bytes
78        } else {
79            u32::from_be_bytes
80        };
81        Ok(Self { read_u32, nano })
82    }
83
84    fn packets(self, d: &[u8]) -> Packets<'_> {
85        Packets {
86            header: self,
87            rest: &d[GLOBAL_HDR_LEN..],
88        }
89    }
90}
91
92#[derive(Clone, Debug)]
93pub struct Packets<'a> {
94    header: Header,
95    rest: &'a [u8],
96}
97
98impl<'a> Iterator for Packets<'a> {
99    type Item = PcapPacket<'a>;
100
101    fn next(&mut self) -> Option<PcapPacket<'a>> {
102        let d = self.rest;
103        if d.len() < RECORD_HDR_LEN {
104            return None;
105        }
106        let rd = self.header.read_u32;
107        let ts_sec = rd(bytes_at(d, 0));
108        let mut ts_usec = rd(bytes_at(d, 4));
109        let caplen = rd(bytes_at(d, 8)) as usize;
110        let data = d[RECORD_HDR_LEN..].get(..caplen)?;
111        if self.header.nano {
112            ts_usec /= 1000;
113        }
114        self.rest = &d[RECORD_HDR_LEN + caplen..];
115        Some(PcapPacket {
116            ts_sec,
117            ts_usec,
118            data,
119        })
120    }
121}
122
123fn bytes_at(d: &[u8], off: usize) -> [u8; 4] {
124    [d[off], d[off + 1], d[off + 2], d[off + 3]]
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    fn file(magic: u32, le: bool, records: &[(u32, u32, &[u8])]) -> Vec<u8> {
132        let w = |v: u32| if le { v.to_le_bytes() } else { v.to_be_bytes() };
133        let mut d = Vec::new();
134        d.extend_from_slice(&magic.to_le_bytes());
135        d.extend_from_slice(&[0u8; 20]);
136        for (sec, frac, data) in records {
137            d.extend_from_slice(&w(*sec));
138            d.extend_from_slice(&w(*frac));
139            d.extend_from_slice(&w(data.len() as u32));
140            d.extend_from_slice(&w(data.len() as u32));
141            d.extend_from_slice(data);
142        }
143        d
144    }
145
146    fn packets(d: &[u8]) -> Result<Vec<PcapPacket<'_>>> {
147        Ok(parse_pcap(d)?.collect())
148    }
149
150    #[test]
151    fn little_endian_records_come_back_in_order() {
152        let d = file(0xa1b2_c3d4, true, &[(1, 2, &[0xaa, 0xbb]), (3, 4, &[0xcc])]);
153        let p = packets(&d).unwrap();
154        assert_eq!(
155            p,
156            [
157                PcapPacket {
158                    ts_sec: 1,
159                    ts_usec: 2,
160                    data: &[0xaa, 0xbb]
161                },
162                PcapPacket {
163                    ts_sec: 3,
164                    ts_usec: 4,
165                    data: &[0xcc]
166                },
167            ]
168        );
169    }
170
171    #[test]
172    fn big_endian_magic_selects_big_endian_headers() {
173        let d = file(0xd4c3_b2a1, false, &[(0x0102_0304, 7, &[1, 2, 3])]);
174        let p = packets(&d).unwrap();
175        assert_eq!((p[0].ts_sec, p[0].ts_usec), (0x0102_0304, 7));
176        assert_eq!(p[0].data, [1, 2, 3]);
177    }
178
179    #[test]
180    fn nanosecond_files_report_microseconds() {
181        let d = file(0xa1b2_3c4d, true, &[(9, 123_456_789, &[0])]);
182        let p = packets(&d).unwrap();
183        assert_eq!(p[0].ts_usec, 123_456);
184        let d = file(0x4d3c_b2a1, false, &[(9, 5_000, &[0])]);
185        assert_eq!(packets(&d).unwrap()[0].ts_usec, 5);
186    }
187
188    #[test]
189    fn truncated_last_record_is_dropped() {
190        let mut d = file(0xa1b2_c3d4, true, &[(1, 1, &[1, 2, 3, 4])]);
191        d.pop();
192        assert!(packets(&d).unwrap().is_empty());
193    }
194
195    #[test]
196    fn pcapng_and_short_files_are_rejected() {
197        let mut d = vec![0u8; 24];
198        d[..4].copy_from_slice(&0x0a0d_0d0au32.to_le_bytes());
199        assert!(parse_pcap(&d).unwrap_err().to_string().contains("tcpdump"));
200        assert!(parse_pcap(&d[..10]).is_err());
201    }
202
203    #[test]
204    fn an_owned_file_walks_the_same_packets() {
205        let d = file(0xa1b2_c3d4, true, &[(1, 2, &[0xaa]), (3, 4, &[0xbb, 0xcc])]);
206        let pcap = Pcap::from_bytes(d.clone()).unwrap();
207        assert_eq!(pcap.packets().collect::<Vec<_>>(), packets(&d).unwrap());
208        assert!(Pcap::from_bytes(vec![0; 24]).is_err());
209    }
210}