Skip to main content

pcap_frame_parser/
ethernet.rs

1//! Ethernet → VLAN → IP → UDP/TCP frame dissection.
2//!
3//! This module walks a raw captured Ethernet frame down through
4//! optional VLAN tags, the IPv4 header, and finally UDP or TCP, handing
5//! back the transport-layer payload for protocol-specific parsers to
6//! consume.
7
8use nom::{
9    bytes::complete::take,
10    number::complete::{be_u16, be_u8},
11    IResult,
12};
13
14pub const PROTO_UDP: u8 = 17;
15pub const PROTO_TCP: u8 = 6;
16
17pub const ETHERTYPE_IP: u16 = 0x0800;
18/// 802.1Q VLAN tag (single tag).
19pub const ETHERTYPE_8021Q: u16 = 0x8100;
20/// 802.1ad "Q-in-Q" outer service tag. Frames carrying this ethertype
21/// have a second, inner 802.1Q tag before the real payload ethertype.
22pub const ETHERTYPE_8021AD: u16 = 0x88a8;
23
24#[derive(Debug, Clone)]
25pub struct IpHeader {
26    pub src: [u8; 4],
27    pub dst: [u8; 4],
28    pub protocol: u8,
29    pub ttl: u8,
30}
31
32impl IpHeader {
33    pub fn src_str(&self) -> String {
34        format!(
35            "{}.{}.{}.{}",
36            self.src[0], self.src[1], self.src[2], self.src[3]
37        )
38    }
39    pub fn dst_str(&self) -> String {
40        format!(
41            "{}.{}.{}.{}",
42            self.dst[0], self.dst[1], self.dst[2], self.dst[3]
43        )
44    }
45}
46
47/// Strips the Ethernet header (and any VLAN tags) from a raw frame,
48/// returning the IPv4 payload.
49///
50/// Handles untagged frames, single 802.1Q tags, and double-tagged
51/// 802.1ad "Q-in-Q" frames (outer 0x88a8 tag followed by an inner
52/// 0x8100 tag). Only IPv4 (`ETHERTYPE_IP`) payloads are recognized;
53/// IPv6 and other ethertypes return `None`.
54pub fn strip_ethernet(input: &[u8]) -> Option<&[u8]> {
55    if input.len() < 14 {
56        return None;
57    }
58
59    let ethertype = u16::from_be_bytes([input[12], input[13]]);
60    match ethertype {
61        e if e == ETHERTYPE_IP => Some(&input[14..]),
62
63        e if e == ETHERTYPE_8021Q => {
64            // 4-byte VLAN tag, then the real ethertype.
65            if input.len() < 18 {
66                return None;
67            }
68            let inner_et = u16::from_be_bytes([input[16], input[17]]);
69            if inner_et == ETHERTYPE_IP {
70                Some(&input[18..])
71            } else {
72                None
73            }
74        }
75
76        e if e == ETHERTYPE_8021AD => {
77            // Outer service tag (4 bytes), then an inner 802.1Q tag (4
78            // bytes), then the real ethertype.
79            if input.len() < 22 {
80                return None;
81            }
82            let mid_et = u16::from_be_bytes([input[16], input[17]]);
83            if mid_et != ETHERTYPE_8021Q {
84                return None;
85            }
86            let inner_et = u16::from_be_bytes([input[20], input[21]]);
87            if inner_et == ETHERTYPE_IP {
88                Some(&input[22..])
89            } else {
90                None
91            }
92        }
93
94        _ => None,
95    }
96}
97
98fn parse_ip_header(input: &[u8]) -> IResult<&[u8], (IpHeader, &[u8])> {
99    let (input, ver_ihl) = be_u8(input)?;
100    let ihl = ((ver_ihl & 0x0f) * 4) as usize;
101
102    if ihl < 20 {
103        return Err(nom::Err::Error(nom::error::Error::new(
104            input,
105            nom::error::ErrorKind::Verify,
106        )));
107    }
108
109    let (input, _dscp_ecn) = be_u8(input)?;
110    let (input, total_len) = be_u16(input)?;
111    let (input, _ident) = be_u16(input)?;
112    let (input, _flags_frag) = be_u16(input)?;
113    let (input, ttl) = be_u8(input)?;
114    let (input, protocol) = be_u8(input)?;
115    let (input, _checksum) = be_u16(input)?;
116    let (input, src_raw) = take(4usize)(input)?;
117    let (input, dst_raw) = take(4usize)(input)?;
118
119    let src = [src_raw[0], src_raw[1], src_raw[2], src_raw[3]];
120    let dst = [dst_raw[0], dst_raw[1], dst_raw[2], dst_raw[3]];
121
122    // Skip IP options, if present.
123    let options_len = ihl - 20;
124    let (input, _) = take(options_len)(input)?;
125
126    let payload_len = (total_len as usize).saturating_sub(ihl);
127    let (input, payload) = take(payload_len)(input)?;
128
129    Ok((
130        input,
131        (
132            IpHeader {
133                src,
134                dst,
135                protocol,
136                ttl,
137            },
138            payload,
139        ),
140    ))
141}
142
143/// Extracts the IPv4 header and payload from a raw Ethernet frame,
144/// transparently handling untagged, 802.1Q, and 802.1ad Q-in-Q framing.
145pub fn extract_ip(frame: &[u8]) -> Option<(IpHeader, &[u8])> {
146    let ip_data = strip_ethernet(frame)?;
147    parse_ip_header(ip_data).ok().map(|(_, result)| result)
148}
149
150/// Extracts `(src_port, dst_port, udp_payload)` from an IP payload whose
151/// protocol is [`PROTO_UDP`].
152pub fn extract_udp(payload: &[u8]) -> Option<(u16, u16, &[u8])> {
153    if payload.len() < 8 {
154        return None;
155    }
156    let src_port = u16::from_be_bytes([payload[0], payload[1]]);
157    let dst_port = u16::from_be_bytes([payload[2], payload[3]]);
158    let length = u16::from_be_bytes([payload[4], payload[5]]) as usize;
159    if length < 8 || length > payload.len() {
160        return None;
161    }
162    Some((src_port, dst_port, &payload[8..length]))
163}
164
165/// A dissected TCP segment: ports plus payload past the header/options.
166pub struct TcpSegment<'a> {
167    pub src_port: u16,
168    pub dst_port: u16,
169    pub payload: &'a [u8],
170}
171
172/// Extracts ports and payload from an IP payload whose protocol is
173/// [`PROTO_TCP`], accounting for the variable-length TCP header
174/// (data offset field) so that TCP options don't leak into `payload`.
175pub fn extract_tcp(payload: &[u8]) -> Option<TcpSegment<'_>> {
176    if payload.len() < 20 {
177        return None;
178    }
179    let src_port = u16::from_be_bytes([payload[0], payload[1]]);
180    let dst_port = u16::from_be_bytes([payload[2], payload[3]]);
181    // Data offset: high 4 bits of byte 12, in 32-bit words.
182    let data_off = ((payload[12] >> 4) as usize) * 4;
183    if data_off < 20 || data_off > payload.len() {
184        return None;
185    }
186    let tcp_payload = &payload[data_off..];
187    Some(TcpSegment {
188        src_port,
189        dst_port,
190        payload: tcp_payload,
191    })
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    fn build_ip_frame(ethertype_prefix: &[u8], protocol: u8, payload: &[u8]) -> Vec<u8> {
199        let mut frame = Vec::new();
200        frame.extend_from_slice(&[0u8; 12]); // dst MAC + src MAC
201        frame.extend_from_slice(ethertype_prefix);
202
203        // Minimal 20-byte IPv4 header, no options.
204        let total_len = 20 + payload.len();
205        frame.push(0x45); // version=4, ihl=5
206        frame.push(0); // dscp/ecn
207        frame.extend_from_slice(&(total_len as u16).to_be_bytes());
208        frame.extend_from_slice(&0u16.to_be_bytes()); // ident
209        frame.extend_from_slice(&0u16.to_be_bytes()); // flags/frag
210        frame.push(64); // ttl
211        frame.push(protocol);
212        frame.extend_from_slice(&0u16.to_be_bytes()); // checksum
213        frame.extend_from_slice(&[192, 0, 2, 1]); // src
214        frame.extend_from_slice(&[192, 0, 2, 2]); // dst
215        frame.extend_from_slice(payload);
216        frame
217    }
218
219    fn build_udp_payload(src_port: u16, dst_port: u16, data: &[u8]) -> Vec<u8> {
220        let mut buf = Vec::new();
221        buf.extend_from_slice(&src_port.to_be_bytes());
222        buf.extend_from_slice(&dst_port.to_be_bytes());
223        buf.extend_from_slice(&((8 + data.len()) as u16).to_be_bytes());
224        buf.extend_from_slice(&0u16.to_be_bytes()); // checksum
225        buf.extend_from_slice(data);
226        buf
227    }
228
229    #[test]
230    fn strips_untagged_ethernet() {
231        let frame = build_ip_frame(&ETHERTYPE_IP.to_be_bytes(), PROTO_UDP, &[1, 2, 3]);
232        let ip = strip_ethernet(&frame).unwrap();
233        assert_eq!(ip[9], PROTO_UDP); // protocol field offset within IP header
234    }
235
236    #[test]
237    fn strips_single_8021q_tag() {
238        let mut prefix = ETHERTYPE_8021Q.to_be_bytes().to_vec();
239        prefix.extend_from_slice(&0x0064u16.to_be_bytes()); // VLAN id 100
240        prefix.extend_from_slice(&ETHERTYPE_IP.to_be_bytes());
241        let frame = build_ip_frame(&prefix, PROTO_UDP, &[1, 2, 3]);
242        assert!(strip_ethernet(&frame).is_some());
243    }
244
245    #[test]
246    fn strips_double_tagged_qinq_frame() {
247        let mut prefix = ETHERTYPE_8021AD.to_be_bytes().to_vec();
248        prefix.extend_from_slice(&0x002Au16.to_be_bytes()); // outer VLAN id
249        prefix.extend_from_slice(&ETHERTYPE_8021Q.to_be_bytes());
250        prefix.extend_from_slice(&0x0064u16.to_be_bytes()); // inner VLAN id
251        prefix.extend_from_slice(&ETHERTYPE_IP.to_be_bytes());
252        let frame = build_ip_frame(&prefix, PROTO_UDP, &[1, 2, 3]);
253        assert!(strip_ethernet(&frame).is_some());
254    }
255
256    #[test]
257    fn rejects_non_ip_ethertype() {
258        let frame = build_ip_frame(&0x86DDu16.to_be_bytes(), PROTO_UDP, &[1, 2, 3]); // IPv6
259        assert!(strip_ethernet(&frame).is_none());
260    }
261
262    #[test]
263    fn extracts_ip_and_udp_end_to_end() {
264        let udp_data = b"hello";
265        let udp = build_udp_payload(1234, 53, udp_data);
266        let frame = build_ip_frame(&ETHERTYPE_IP.to_be_bytes(), PROTO_UDP, &udp);
267
268        let (ip_header, ip_payload) = extract_ip(&frame).unwrap();
269        assert_eq!(ip_header.protocol, PROTO_UDP);
270        assert_eq!(ip_header.src_str(), "192.0.2.1");
271        assert_eq!(ip_header.dst_str(), "192.0.2.2");
272
273        let (src_port, dst_port, payload) = extract_udp(ip_payload).unwrap();
274        assert_eq!(src_port, 1234);
275        assert_eq!(dst_port, 53);
276        assert_eq!(payload, udp_data);
277    }
278
279    #[test]
280    fn extracts_tcp_skipping_options() {
281        let mut tcp = Vec::new();
282        tcp.extend_from_slice(&4321u16.to_be_bytes()); // src port
283        tcp.extend_from_slice(&80u16.to_be_bytes()); // dst port
284        tcp.extend_from_slice(&0u32.to_be_bytes()); // seq
285        tcp.extend_from_slice(&0u32.to_be_bytes()); // ack
286        tcp.push(0x70); // data offset = 7 (28 bytes), i.e. 8 bytes of options
287        tcp.push(0x02); // flags (SYN)
288        tcp.extend_from_slice(&0u16.to_be_bytes()); // window
289        tcp.extend_from_slice(&0u16.to_be_bytes()); // checksum
290        tcp.extend_from_slice(&0u16.to_be_bytes()); // urgent ptr
291        tcp.extend_from_slice(&[0u8; 8]); // options
292        tcp.extend_from_slice(b"payload");
293
294        let seg = extract_tcp(&tcp).unwrap();
295        assert_eq!(seg.src_port, 4321);
296        assert_eq!(seg.dst_port, 80);
297        assert_eq!(seg.payload, b"payload");
298    }
299
300    #[test]
301    fn rejects_truncated_udp() {
302        assert!(extract_udp(&[0u8; 4]).is_none());
303    }
304
305    #[test]
306    fn rejects_truncated_tcp() {
307        assert!(extract_tcp(&[0u8; 10]).is_none());
308    }
309}