Skip to main content

pcap_frame_parser/
pcap.rs

1//! Legacy PCAP file format parser.
2//!
3//! Spec: <https://wiki.wireshark.org/Development/LibpcapFileFormat>
4//!
5//! Only Ethernet-linked captures (`network == 1`) are supported; this
6//! matches the vast majority of real-world captures produced by
7//! `tcpdump`/Wireshark on wired or bridged interfaces.
8
9use nom::{
10    bytes::complete::take,
11    number::complete::{be_u32, le_u16, le_u32},
12    IResult,
13};
14
15/// Magic numbers identifying a legacy PCAP file and its byte order.
16const MAGIC_LE: u32 = 0xa1b2c3d4; // little-endian, microsecond resolution
17const MAGIC_BE: u32 = 0xd4c3b2a1; // big-endian, microsecond resolution
18
19/// Global file header of a legacy PCAP capture.
20#[derive(Debug, Clone)]
21pub struct PcapHeader {
22    pub magic: u32,
23    pub version_major: u16,
24    pub version_minor: u16,
25    pub snaplen: u32,
26    /// Link-layer header type (LINKTYPE_*). Only `1` (Ethernet) is
27    /// supported by [`iter_packets`].
28    pub network: u32,
29    pub big_endian: bool,
30}
31
32/// A single captured frame with its timestamp.
33#[derive(Debug, Clone)]
34pub struct RawPacket<'a> {
35    pub ts_sec: u32,
36    pub ts_usec: u32,
37    pub data: &'a [u8],
38}
39
40/// Parses the 24-byte global PCAP header.
41pub fn parse_pcap_header(input: &[u8]) -> IResult<&[u8], PcapHeader> {
42    let (input, magic) = le_u32(input)?;
43
44    let big_endian = match magic {
45        MAGIC_LE => false,
46        MAGIC_BE => true,
47        _ => {
48            return Err(nom::Err::Error(nom::error::Error::new(
49                input,
50                nom::error::ErrorKind::Tag,
51            )))
52        }
53    };
54
55    if big_endian {
56        let (input, version_major) = nom::number::complete::be_u16(input)?;
57        let (input, version_minor) = nom::number::complete::be_u16(input)?;
58        let (input, _thiszone) = be_u32(input)?;
59        let (input, _sigfigs) = be_u32(input)?;
60        let (input, snaplen) = be_u32(input)?;
61        let (input, network) = be_u32(input)?;
62        Ok((
63            input,
64            PcapHeader { magic, version_major, version_minor, snaplen, network, big_endian },
65        ))
66    } else {
67        let (input, version_major) = le_u16(input)?;
68        let (input, version_minor) = le_u16(input)?;
69        let (input, _thiszone) = le_u32(input)?;
70        let (input, _sigfigs) = le_u32(input)?;
71        let (input, snaplen) = le_u32(input)?;
72        let (input, network) = le_u32(input)?;
73        Ok((
74            input,
75            PcapHeader { magic, version_major, version_minor, snaplen, network, big_endian },
76        ))
77    }
78}
79
80/// Parses a single packet record (16-byte record header + captured bytes).
81pub fn parse_packet(input: &[u8], big_endian: bool) -> IResult<&[u8], RawPacket<'_>> {
82    let (input, ts_sec) = if big_endian { be_u32(input)? } else { le_u32(input)? };
83    let (input, ts_usec) = if big_endian { be_u32(input)? } else { le_u32(input)? };
84    let (input, incl_len) = if big_endian { be_u32(input)? } else { le_u32(input)? };
85    let (input, _orig_len) = if big_endian { be_u32(input)? } else { le_u32(input)? };
86    let (input, data) = take(incl_len)(input)?;
87
88    Ok((input, RawPacket { ts_sec, ts_usec, data }))
89}
90
91/// Parses a full legacy PCAP buffer and returns the header plus every
92/// packet it contains. Truncated trailing records are silently dropped
93/// rather than causing the whole capture to be rejected, matching the
94/// tolerant behavior of Wireshark.
95///
96/// # Errors
97/// Returns `Err` if the global header is malformed or the link-layer
98/// type is not Ethernet (`network != 1`).
99pub fn iter_packets(mut input: &[u8]) -> Result<(PcapHeader, Vec<RawPacket<'_>>), String> {
100    let (rest, header) =
101        parse_pcap_header(input).map_err(|e| format!("failed to parse PCAP header: {e:?}"))?;
102
103    if header.network != 1 {
104        return Err(format!(
105            "unsupported link type: {} (only Ethernet/1 is supported)",
106            header.network
107        ));
108    }
109
110    input = rest;
111    let mut packets = Vec::new();
112
113    while !input.is_empty() {
114        match parse_packet(input, header.big_endian) {
115            Ok((rest, pkt)) => {
116                packets.push(pkt);
117                input = rest;
118            }
119            Err(_) => break,
120        }
121    }
122
123    Ok((header, packets))
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    fn le_u32_bytes(v: u32) -> [u8; 4] {
131        v.to_le_bytes()
132    }
133    fn le_u16_bytes(v: u16) -> [u8; 2] {
134        v.to_le_bytes()
135    }
136
137    /// Builds a minimal little-endian PCAP file with the given raw frames.
138    fn build_pcap_le(frames: &[&[u8]]) -> Vec<u8> {
139        let mut buf = Vec::new();
140        buf.extend_from_slice(&le_u32_bytes(MAGIC_LE));
141        buf.extend_from_slice(&le_u16_bytes(2)); // version_major
142        buf.extend_from_slice(&le_u16_bytes(4)); // version_minor
143        buf.extend_from_slice(&le_u32_bytes(0)); // thiszone
144        buf.extend_from_slice(&le_u32_bytes(0)); // sigfigs
145        buf.extend_from_slice(&le_u32_bytes(65535)); // snaplen
146        buf.extend_from_slice(&le_u32_bytes(1)); // network = Ethernet
147
148        for (i, frame) in frames.iter().enumerate() {
149            buf.extend_from_slice(&le_u32_bytes(1_700_000_000 + i as u32)); // ts_sec
150            buf.extend_from_slice(&le_u32_bytes(0)); // ts_usec
151            buf.extend_from_slice(&le_u32_bytes(frame.len() as u32)); // incl_len
152            buf.extend_from_slice(&le_u32_bytes(frame.len() as u32)); // orig_len
153            buf.extend_from_slice(frame);
154        }
155        buf
156    }
157
158    #[test]
159    fn parses_header_little_endian() {
160        let file = build_pcap_le(&[]);
161        let (_, header) = parse_pcap_header(&file).unwrap();
162        assert!(!header.big_endian);
163        assert_eq!(header.network, 1);
164        assert_eq!(header.snaplen, 65535);
165    }
166
167    #[test]
168    fn rejects_bad_magic() {
169        let mut file = build_pcap_le(&[]);
170        file[0] = 0xff;
171        assert!(parse_pcap_header(&file).is_err());
172    }
173
174    #[test]
175    fn iterates_multiple_packets() {
176        let frame_a = [0xAAu8; 20];
177        let frame_b = [0xBBu8; 34];
178        let file = build_pcap_le(&[&frame_a, &frame_b]);
179
180        let (header, packets) = iter_packets(&file).unwrap();
181        assert_eq!(header.network, 1);
182        assert_eq!(packets.len(), 2);
183        assert_eq!(packets[0].data, &frame_a[..]);
184        assert_eq!(packets[1].data, &frame_b[..]);
185    }
186
187    #[test]
188    fn rejects_non_ethernet_linktype() {
189        let mut file = build_pcap_le(&[]);
190        // Patch network field (last 4 bytes of the 24-byte global header) to
191        // LINKTYPE_RAW (101) instead of Ethernet (1).
192        file[20..24].copy_from_slice(&le_u32_bytes(101));
193        assert!(iter_packets(&file).is_err());
194    }
195
196    #[test]
197    fn truncated_trailing_record_is_dropped_not_fatal() {
198        let frame_a = [0xCCu8; 16];
199        let mut file = build_pcap_le(&[&frame_a]);
200        // Append a truncated, incomplete record header at the end.
201        file.extend_from_slice(&[0u8; 5]);
202
203        let (_, packets) = iter_packets(&file).unwrap();
204        assert_eq!(packets.len(), 1);
205    }
206}