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