Skip to main content

pcap_frame_parser/
pcapng.rs

1//! PCAPng file format parser.
2//!
3//! Spec: <https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-05.txt>
4//!
5//! Only the block types produced by common capture tools are handled:
6//! Section Header Block, Interface Description Block, Enhanced/Obsolete
7//! Packet Block, and Simple Packet Block. Unknown block types are
8//! skipped using their declared length, so unrecognized-but-well-formed
9//! files still parse successfully.
10
11const SHB_MAGIC: u32 = 0x0A0D_0D0A;
12const BYTE_ORDER_MAGIC: u32 = 0x1A2B_3C4D;
13const BYTE_ORDER_MAGIC_SWAPPED: u32 = 0x4D3C_2B1A;
14
15const BLOCK_SHB: u32 = 0x0A0D_0D0A;
16const BLOCK_IDB: u32 = 0x0000_0001;
17const BLOCK_OPB: u32 = 0x0000_0002; // Obsolete Packet Block
18const BLOCK_SPB: u32 = 0x0000_0003;
19const BLOCK_EPB: u32 = 0x0000_0006;
20
21/// Per-interface metadata parsed out of an Interface Description Block,
22/// needed to convert packet timestamps to seconds/microseconds.
23#[derive(Debug, Clone)]
24struct Interface {
25    link_type: u16,
26    /// Timestamp resolution: interpreted as 10^-resol seconds, except
27    /// the common defaults 6 (microseconds) and 9 (nanoseconds) which
28    /// are handled directly for clarity and precision.
29    ts_resol: u8,
30    ts_offset: u64,
31}
32
33impl Interface {
34    fn ts_to_secs_usecs(&self, ts: u64) -> (u32, u32) {
35        let resol = self.ts_resol;
36        let (sec, frac) = if resol == 6 {
37            (ts / 1_000_000, ts % 1_000_000)
38        } else if resol == 9 {
39            (ts / 1_000_000_000, (ts % 1_000_000_000) / 1_000)
40        } else {
41            let factor = 10u64.pow(resol as u32);
42            (ts / factor, (ts % factor) * 1_000_000 / factor)
43        };
44        ((sec + self.ts_offset) as u32, frac as u32)
45    }
46}
47
48fn read_u16(data: &[u8], offset: usize, big_endian: bool) -> Option<u16> {
49    if offset + 2 > data.len() {
50        return None;
51    }
52    let b = &data[offset..offset + 2];
53    Some(if big_endian { u16::from_be_bytes([b[0], b[1]]) } else { u16::from_le_bytes([b[0], b[1]]) })
54}
55
56fn read_u32(data: &[u8], offset: usize, big_endian: bool) -> Option<u32> {
57    if offset + 4 > data.len() {
58        return None;
59    }
60    let b = &data[offset..offset + 4];
61    Some(if big_endian {
62        u32::from_be_bytes([b[0], b[1], b[2], b[3]])
63    } else {
64        u32::from_le_bytes([b[0], b[1], b[2], b[3]])
65    })
66}
67
68fn read_u64(data: &[u8], offset: usize, big_endian: bool) -> Option<u64> {
69    if offset + 8 > data.len() {
70        return None;
71    }
72    let b = &data[offset..offset + 8];
73    Some(if big_endian {
74        u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
75    } else {
76        u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
77    })
78}
79
80/// Parses a PCAPng buffer and returns every Ethernet-linked packet found
81/// as `(ts_sec, ts_usec, frame_bytes)`. Non-Ethernet interfaces are
82/// skipped. Packet data is copied out (owned `Vec<u8>`) because PCAPng
83/// blocks are not guaranteed to be contiguous with a single packet's
84/// logical payload once padding/options are accounted for.
85///
86/// # Errors
87/// Returns `Err` if the buffer is too short, doesn't start with a
88/// Section Header Block, or declares an invalid byte-order magic.
89pub fn parse_pcapng(data: &[u8]) -> Result<Vec<(u32, u32, Vec<u8>)>, String> {
90    if data.len() < 12 {
91        return Err("file too short for PCAPng".into());
92    }
93
94    let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
95    if magic != SHB_MAGIC {
96        return Err(format!("not a PCAPng file (magic: 0x{magic:08x})"));
97    }
98
99    let mut pos = 0usize;
100    let mut big_endian = false;
101    let mut interfaces: Vec<Interface> = Vec::new();
102    let mut packets: Vec<(u32, u32, Vec<u8>)> = Vec::new();
103
104    while pos + 8 <= data.len() {
105        // SHB's magic (0x0A0D0D0A) reads identically in both byte orders,
106        // so it's always safe to read block_type with the current
107        // endianness guess before it's been confirmed.
108        let block_type = read_u32(data, pos, big_endian).ok_or("failed to read block type")?;
109        // block_len for an SHB is always read little-endian first, since
110        // the byte-order magic that disambiguates it lives inside the
111        // block itself, at offset 8.
112        let block_len_raw = read_u32(data, pos + 4, false).ok_or("failed to read block length")?;
113        let block_len = if block_type == BLOCK_SHB {
114            block_len_raw
115        } else {
116            read_u32(data, pos + 4, big_endian).ok_or("failed to read block length")?
117        };
118
119        if block_len < 12 || pos + block_len as usize > data.len() {
120            break;
121        }
122
123        let block_data = &data[pos..pos + block_len as usize];
124
125        match block_type {
126            BLOCK_SHB => {
127                // offset 8: Byte-Order Magic
128                if block_len < 16 {
129                    pos += block_len as usize;
130                    continue;
131                }
132                let bom =
133                    u32::from_le_bytes([block_data[8], block_data[9], block_data[10], block_data[11]]);
134                big_endian = match bom {
135                    BYTE_ORDER_MAGIC => false,
136                    BYTE_ORDER_MAGIC_SWAPPED => true,
137                    _ => return Err(format!("invalid byte-order magic: 0x{bom:08x}")),
138                };
139                // A new section resets the interface table.
140                interfaces.clear();
141            }
142
143            BLOCK_IDB => {
144                // offset 8: LinkType (2), Reserved (2), SnapLen (4), then options
145                if block_len < 20 {
146                    pos += block_len as usize;
147                    continue;
148                }
149                let link_type = read_u16(block_data, 8, big_endian).unwrap_or(1);
150                let mut ts_resol = 6u8; // default: microseconds
151                let mut ts_offset = 0u64;
152
153                let opts_start = 16usize;
154                let opts_end = block_len as usize - 4;
155                let mut opos = opts_start;
156                while opos + 4 <= opts_end {
157                    let opt_code = read_u16(block_data, opos, big_endian).unwrap_or(0);
158                    let opt_len = read_u16(block_data, opos + 2, big_endian).unwrap_or(0) as usize;
159                    opos += 4;
160                    if opt_code == 0 {
161                        break; // opt_endofopt
162                    }
163                    if opos + opt_len <= opts_end {
164                        match opt_code {
165                            9 => {
166                                // if_tsresol
167                                if opt_len >= 1 {
168                                    ts_resol = block_data[opos];
169                                }
170                            }
171                            14 => {
172                                // if_tsoffset
173                                if opt_len >= 8 {
174                                    ts_offset = read_u64(block_data, opos, big_endian).unwrap_or(0);
175                                }
176                            }
177                            _ => {}
178                        }
179                    }
180                    // Options are padded to 4-byte boundaries.
181                    opos += (opt_len + 3) & !3;
182                }
183
184                interfaces.push(Interface { link_type, ts_resol, ts_offset });
185            }
186
187            BLOCK_EPB => {
188                // offset 8:  Interface ID (4)
189                // offset 12: Timestamp High (4)
190                // offset 16: Timestamp Low (4)
191                // offset 20: Captured Packet Length (4)
192                // offset 24: Original Packet Length (4)
193                // offset 28: Packet Data
194                if block_len < 32 {
195                    pos += block_len as usize;
196                    continue;
197                }
198                let iface_id = read_u32(block_data, 8, big_endian).unwrap_or(0) as usize;
199                let ts_high = read_u32(block_data, 12, big_endian).unwrap_or(0) as u64;
200                let ts_low = read_u32(block_data, 16, big_endian).unwrap_or(0) as u64;
201                let cap_len = read_u32(block_data, 20, big_endian).unwrap_or(0) as usize;
202                let ts = (ts_high << 32) | ts_low;
203
204                let iface = interfaces
205                    .get(iface_id)
206                    .cloned()
207                    .unwrap_or(Interface { link_type: 1, ts_resol: 6, ts_offset: 0 });
208
209                if iface.link_type != 1 {
210                    pos += block_len as usize;
211                    continue;
212                }
213
214                let data_start = 28usize;
215                if data_start + cap_len <= block_len as usize - 4 {
216                    let pkt_data = block_data[data_start..data_start + cap_len].to_vec();
217                    let (ts_sec, ts_usec) = iface.ts_to_secs_usecs(ts);
218                    packets.push((ts_sec, ts_usec, pkt_data));
219                }
220            }
221
222            BLOCK_OPB => {
223                // Obsolete Packet Block (pre-EPB format).
224                // offset 8:  Interface ID (2), Drops Count (2)
225                // offset 12: Timestamp High (4)
226                // offset 16: Timestamp Low (4)
227                // offset 20: Captured Length (4)
228                // offset 24: Original Length (4)
229                // offset 28: Packet Data
230                if block_len < 32 {
231                    pos += block_len as usize;
232                    continue;
233                }
234                let iface_id = read_u16(block_data, 8, big_endian).unwrap_or(0) as usize;
235                let ts_high = read_u32(block_data, 12, big_endian).unwrap_or(0) as u64;
236                let ts_low = read_u32(block_data, 16, big_endian).unwrap_or(0) as u64;
237                let cap_len = read_u32(block_data, 20, big_endian).unwrap_or(0) as usize;
238                let ts = (ts_high << 32) | ts_low;
239
240                let iface = interfaces
241                    .get(iface_id)
242                    .cloned()
243                    .unwrap_or(Interface { link_type: 1, ts_resol: 6, ts_offset: 0 });
244
245                if iface.link_type != 1 {
246                    pos += block_len as usize;
247                    continue;
248                }
249
250                let data_start = 28usize;
251                if data_start + cap_len <= block_len as usize - 4 {
252                    let pkt_data = block_data[data_start..data_start + cap_len].to_vec();
253                    let (ts_sec, ts_usec) = iface.ts_to_secs_usecs(ts);
254                    packets.push((ts_sec, ts_usec, pkt_data));
255                }
256            }
257
258            BLOCK_SPB => {
259                // Simple Packet Block — no timestamp available, reported as 0.
260                if block_len < 16 {
261                    pos += block_len as usize;
262                    continue;
263                }
264                let orig_len = read_u32(block_data, 8, big_endian).unwrap_or(0) as usize;
265                let cap_len = orig_len.min(block_len as usize - 16);
266                let iface = interfaces
267                    .first()
268                    .cloned()
269                    .unwrap_or(Interface { link_type: 1, ts_resol: 6, ts_offset: 0 });
270
271                if iface.link_type == 1 && cap_len > 0 {
272                    let pkt_data = block_data[12..12 + cap_len].to_vec();
273                    packets.push((0, 0, pkt_data));
274                }
275            }
276
277            _ => {
278                // Unknown block type — skip using its declared length.
279            }
280        }
281
282        pos += block_len as usize;
283    }
284
285    Ok(packets)
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    fn pad4(len: usize) -> usize {
293        (len + 3) & !3
294    }
295
296    /// Builds a minimal little-endian PCAPng file with one interface and
297    /// the given Ethernet frames, each wrapped in an Enhanced Packet Block.
298    fn build_pcapng_le(frames: &[&[u8]]) -> Vec<u8> {
299        let mut buf = Vec::new();
300
301        // Section Header Block: type, len, BOM, major, minor, section_len(-1), len(again)
302        let shb_len: u32 = 28;
303        buf.extend_from_slice(&BLOCK_SHB.to_le_bytes());
304        buf.extend_from_slice(&shb_len.to_le_bytes());
305        buf.extend_from_slice(&BYTE_ORDER_MAGIC.to_le_bytes());
306        buf.extend_from_slice(&1u16.to_le_bytes()); // major
307        buf.extend_from_slice(&0u16.to_le_bytes()); // minor
308        buf.extend_from_slice(&(-1i64).to_le_bytes()); // section length unknown
309        buf.extend_from_slice(&shb_len.to_le_bytes());
310
311        // Interface Description Block: type, len, linktype+reserved, snaplen, len(again)
312        let idb_len: u32 = 20;
313        buf.extend_from_slice(&BLOCK_IDB.to_le_bytes());
314        buf.extend_from_slice(&idb_len.to_le_bytes());
315        buf.extend_from_slice(&1u16.to_le_bytes()); // LinkType = Ethernet
316        buf.extend_from_slice(&0u16.to_le_bytes()); // reserved
317        buf.extend_from_slice(&65535u32.to_le_bytes()); // snaplen
318        buf.extend_from_slice(&idb_len.to_le_bytes());
319
320        for frame in frames {
321            let padded = pad4(frame.len());
322            let epb_len = 32 + padded as u32;
323            buf.extend_from_slice(&BLOCK_EPB.to_le_bytes());
324            buf.extend_from_slice(&epb_len.to_le_bytes());
325            buf.extend_from_slice(&0u32.to_le_bytes()); // interface id
326            buf.extend_from_slice(&0u32.to_le_bytes()); // ts high
327            buf.extend_from_slice(&1_000_000u32.to_le_bytes()); // ts low = 1s in microseconds
328            buf.extend_from_slice(&(frame.len() as u32).to_le_bytes()); // captured len
329            buf.extend_from_slice(&(frame.len() as u32).to_le_bytes()); // original len
330            buf.extend_from_slice(frame);
331            buf.extend(std::iter::repeat(0u8).take(padded - frame.len()));
332            buf.extend_from_slice(&epb_len.to_le_bytes());
333        }
334
335        buf
336    }
337
338    #[test]
339    fn rejects_bad_magic() {
340        let mut file = build_pcapng_le(&[]);
341        file[0] = 0x00;
342        assert!(parse_pcapng(&file).is_err());
343    }
344
345    #[test]
346    fn parses_single_packet() {
347        let frame = [0xAAu8; 40];
348        let file = build_pcapng_le(&[&frame]);
349
350        let packets = parse_pcapng(&file).unwrap();
351        assert_eq!(packets.len(), 1);
352        let (ts_sec, ts_usec, data) = &packets[0];
353        assert_eq!(*ts_sec, 1);
354        assert_eq!(*ts_usec, 0);
355        assert_eq!(data.as_slice(), &frame[..]);
356    }
357
358    #[test]
359    fn parses_multiple_packets_with_unaligned_length() {
360        let frame_a = [0x11u8; 7]; // not a multiple of 4, exercises padding
361        let frame_b = [0x22u8; 60];
362        let file = build_pcapng_le(&[&frame_a, &frame_b]);
363
364        let packets = parse_pcapng(&file).unwrap();
365        assert_eq!(packets.len(), 2);
366        assert_eq!(packets[0].2, frame_a.to_vec());
367        assert_eq!(packets[1].2, frame_b.to_vec());
368    }
369
370    #[test]
371    fn empty_capture_yields_no_packets() {
372        let file = build_pcapng_le(&[]);
373        let packets = parse_pcapng(&file).unwrap();
374        assert!(packets.is_empty());
375    }
376}