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