pcap-frame-parser 0.1.0

Zero-copy parser for legacy PCAP and PCAPng capture files, plus Ethernet/VLAN/IP/UDP/TCP frame dissection. No libpcap dependency.
Documentation
//! Ethernet → VLAN → IP → UDP/TCP frame dissection.
//!
//! This module walks a raw captured Ethernet frame down through
//! optional VLAN tags, the IPv4 header, and finally UDP or TCP, handing
//! back the transport-layer payload for protocol-specific parsers to
//! consume.

use nom::{
    bytes::complete::take,
    number::complete::{be_u16, be_u8},
    IResult,
};

pub const PROTO_UDP: u8 = 17;
pub const PROTO_TCP: u8 = 6;

pub const ETHERTYPE_IP: u16 = 0x0800;
/// 802.1Q VLAN tag (single tag).
pub const ETHERTYPE_8021Q: u16 = 0x8100;
/// 802.1ad "Q-in-Q" outer service tag. Frames carrying this ethertype
/// have a second, inner 802.1Q tag before the real payload ethertype.
pub const ETHERTYPE_8021AD: u16 = 0x88a8;

#[derive(Debug, Clone)]
pub struct IpHeader {
    pub src: [u8; 4],
    pub dst: [u8; 4],
    pub protocol: u8,
    pub ttl: u8,
}

impl IpHeader {
    pub fn src_str(&self) -> String {
        format!("{}.{}.{}.{}", self.src[0], self.src[1], self.src[2], self.src[3])
    }
    pub fn dst_str(&self) -> String {
        format!("{}.{}.{}.{}", self.dst[0], self.dst[1], self.dst[2], self.dst[3])
    }
}

/// Strips the Ethernet header (and any VLAN tags) from a raw frame,
/// returning the IPv4 payload.
///
/// Handles untagged frames, single 802.1Q tags, and double-tagged
/// 802.1ad "Q-in-Q" frames (outer 0x88a8 tag followed by an inner
/// 0x8100 tag). Only IPv4 (`ETHERTYPE_IP`) payloads are recognized;
/// IPv6 and other ethertypes return `None`.
pub fn strip_ethernet(input: &[u8]) -> Option<&[u8]> {
    if input.len() < 14 {
        return None;
    }

    let ethertype = u16::from_be_bytes([input[12], input[13]]);
    match ethertype {
        e if e == ETHERTYPE_IP => Some(&input[14..]),

        e if e == ETHERTYPE_8021Q => {
            // 4-byte VLAN tag, then the real ethertype.
            if input.len() < 18 {
                return None;
            }
            let inner_et = u16::from_be_bytes([input[16], input[17]]);
            if inner_et == ETHERTYPE_IP {
                Some(&input[18..])
            } else {
                None
            }
        }

        e if e == ETHERTYPE_8021AD => {
            // Outer service tag (4 bytes), then an inner 802.1Q tag (4
            // bytes), then the real ethertype.
            if input.len() < 22 {
                return None;
            }
            let mid_et = u16::from_be_bytes([input[16], input[17]]);
            if mid_et != ETHERTYPE_8021Q {
                return None;
            }
            let inner_et = u16::from_be_bytes([input[20], input[21]]);
            if inner_et == ETHERTYPE_IP {
                Some(&input[22..])
            } else {
                None
            }
        }

        _ => None,
    }
}

fn parse_ip_header(input: &[u8]) -> IResult<&[u8], (IpHeader, &[u8])> {
    let (input, ver_ihl) = be_u8(input)?;
    let ihl = ((ver_ihl & 0x0f) * 4) as usize;

    if ihl < 20 {
        return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Verify)));
    }

    let (input, _dscp_ecn) = be_u8(input)?;
    let (input, total_len) = be_u16(input)?;
    let (input, _ident) = be_u16(input)?;
    let (input, _flags_frag) = be_u16(input)?;
    let (input, ttl) = be_u8(input)?;
    let (input, protocol) = be_u8(input)?;
    let (input, _checksum) = be_u16(input)?;
    let (input, src_raw) = take(4usize)(input)?;
    let (input, dst_raw) = take(4usize)(input)?;

    let src = [src_raw[0], src_raw[1], src_raw[2], src_raw[3]];
    let dst = [dst_raw[0], dst_raw[1], dst_raw[2], dst_raw[3]];

    // Skip IP options, if present.
    let options_len = ihl - 20;
    let (input, _) = take(options_len)(input)?;

    let payload_len = (total_len as usize).saturating_sub(ihl);
    let (input, payload) = take(payload_len)(input)?;

    Ok((input, (IpHeader { src, dst, protocol, ttl }, payload)))
}

/// Extracts the IPv4 header and payload from a raw Ethernet frame,
/// transparently handling untagged, 802.1Q, and 802.1ad Q-in-Q framing.
pub fn extract_ip(frame: &[u8]) -> Option<(IpHeader, &[u8])> {
    let ip_data = strip_ethernet(frame)?;
    parse_ip_header(ip_data).ok().map(|(_, result)| result)
}

/// Extracts `(src_port, dst_port, udp_payload)` from an IP payload whose
/// protocol is [`PROTO_UDP`].
pub fn extract_udp(payload: &[u8]) -> Option<(u16, u16, &[u8])> {
    if payload.len() < 8 {
        return None;
    }
    let src_port = u16::from_be_bytes([payload[0], payload[1]]);
    let dst_port = u16::from_be_bytes([payload[2], payload[3]]);
    let length = u16::from_be_bytes([payload[4], payload[5]]) as usize;
    if length < 8 || length > payload.len() {
        return None;
    }
    Some((src_port, dst_port, &payload[8..length]))
}

/// A dissected TCP segment: ports plus payload past the header/options.
pub struct TcpSegment<'a> {
    pub src_port: u16,
    pub dst_port: u16,
    pub payload: &'a [u8],
}

/// Extracts ports and payload from an IP payload whose protocol is
/// [`PROTO_TCP`], accounting for the variable-length TCP header
/// (data offset field) so that TCP options don't leak into `payload`.
pub fn extract_tcp(payload: &[u8]) -> Option<TcpSegment<'_>> {
    if payload.len() < 20 {
        return None;
    }
    let src_port = u16::from_be_bytes([payload[0], payload[1]]);
    let dst_port = u16::from_be_bytes([payload[2], payload[3]]);
    // Data offset: high 4 bits of byte 12, in 32-bit words.
    let data_off = ((payload[12] >> 4) as usize) * 4;
    if data_off < 20 || data_off > payload.len() {
        return None;
    }
    let tcp_payload = &payload[data_off..];
    Some(TcpSegment { src_port, dst_port, payload: tcp_payload })
}

#[cfg(test)]
mod tests {
    use super::*;

    fn build_ip_frame(ethertype_prefix: &[u8], protocol: u8, payload: &[u8]) -> Vec<u8> {
        let mut frame = Vec::new();
        frame.extend_from_slice(&[0u8; 12]); // dst MAC + src MAC
        frame.extend_from_slice(ethertype_prefix);

        // Minimal 20-byte IPv4 header, no options.
        let total_len = 20 + payload.len();
        frame.push(0x45); // version=4, ihl=5
        frame.push(0); // dscp/ecn
        frame.extend_from_slice(&(total_len as u16).to_be_bytes());
        frame.extend_from_slice(&0u16.to_be_bytes()); // ident
        frame.extend_from_slice(&0u16.to_be_bytes()); // flags/frag
        frame.push(64); // ttl
        frame.push(protocol);
        frame.extend_from_slice(&0u16.to_be_bytes()); // checksum
        frame.extend_from_slice(&[192, 0, 2, 1]); // src
        frame.extend_from_slice(&[192, 0, 2, 2]); // dst
        frame.extend_from_slice(payload);
        frame
    }

    fn build_udp_payload(src_port: u16, dst_port: u16, data: &[u8]) -> Vec<u8> {
        let mut buf = Vec::new();
        buf.extend_from_slice(&src_port.to_be_bytes());
        buf.extend_from_slice(&dst_port.to_be_bytes());
        buf.extend_from_slice(&((8 + data.len()) as u16).to_be_bytes());
        buf.extend_from_slice(&0u16.to_be_bytes()); // checksum
        buf.extend_from_slice(data);
        buf
    }

    #[test]
    fn strips_untagged_ethernet() {
        let frame = build_ip_frame(&ETHERTYPE_IP.to_be_bytes(), PROTO_UDP, &[1, 2, 3]);
        let ip = strip_ethernet(&frame).unwrap();
        assert_eq!(ip[9], PROTO_UDP); // protocol field offset within IP header
    }

    #[test]
    fn strips_single_8021q_tag() {
        let mut prefix = ETHERTYPE_8021Q.to_be_bytes().to_vec();
        prefix.extend_from_slice(&0x0064u16.to_be_bytes()); // VLAN id 100
        prefix.extend_from_slice(&ETHERTYPE_IP.to_be_bytes());
        let frame = build_ip_frame(&prefix, PROTO_UDP, &[1, 2, 3]);
        assert!(strip_ethernet(&frame).is_some());
    }

    #[test]
    fn strips_double_tagged_qinq_frame() {
        let mut prefix = ETHERTYPE_8021AD.to_be_bytes().to_vec();
        prefix.extend_from_slice(&0x002Au16.to_be_bytes()); // outer VLAN id
        prefix.extend_from_slice(&ETHERTYPE_8021Q.to_be_bytes());
        prefix.extend_from_slice(&0x0064u16.to_be_bytes()); // inner VLAN id
        prefix.extend_from_slice(&ETHERTYPE_IP.to_be_bytes());
        let frame = build_ip_frame(&prefix, PROTO_UDP, &[1, 2, 3]);
        assert!(strip_ethernet(&frame).is_some());
    }

    #[test]
    fn rejects_non_ip_ethertype() {
        let frame = build_ip_frame(&0x86DDu16.to_be_bytes(), PROTO_UDP, &[1, 2, 3]); // IPv6
        assert!(strip_ethernet(&frame).is_none());
    }

    #[test]
    fn extracts_ip_and_udp_end_to_end() {
        let udp_data = b"hello";
        let udp = build_udp_payload(1234, 53, udp_data);
        let frame = build_ip_frame(&ETHERTYPE_IP.to_be_bytes(), PROTO_UDP, &udp);

        let (ip_header, ip_payload) = extract_ip(&frame).unwrap();
        assert_eq!(ip_header.protocol, PROTO_UDP);
        assert_eq!(ip_header.src_str(), "192.0.2.1");
        assert_eq!(ip_header.dst_str(), "192.0.2.2");

        let (src_port, dst_port, payload) = extract_udp(ip_payload).unwrap();
        assert_eq!(src_port, 1234);
        assert_eq!(dst_port, 53);
        assert_eq!(payload, udp_data);
    }

    #[test]
    fn extracts_tcp_skipping_options() {
        let mut tcp = Vec::new();
        tcp.extend_from_slice(&4321u16.to_be_bytes()); // src port
        tcp.extend_from_slice(&80u16.to_be_bytes()); // dst port
        tcp.extend_from_slice(&0u32.to_be_bytes()); // seq
        tcp.extend_from_slice(&0u32.to_be_bytes()); // ack
        tcp.push(0x70); // data offset = 7 (28 bytes), i.e. 8 bytes of options
        tcp.push(0x02); // flags (SYN)
        tcp.extend_from_slice(&0u16.to_be_bytes()); // window
        tcp.extend_from_slice(&0u16.to_be_bytes()); // checksum
        tcp.extend_from_slice(&0u16.to_be_bytes()); // urgent ptr
        tcp.extend_from_slice(&[0u8; 8]); // options
        tcp.extend_from_slice(b"payload");

        let seg = extract_tcp(&tcp).unwrap();
        assert_eq!(seg.src_port, 4321);
        assert_eq!(seg.dst_port, 80);
        assert_eq!(seg.payload, b"payload");
    }

    #[test]
    fn rejects_truncated_udp() {
        assert!(extract_udp(&[0u8; 4]).is_none());
    }

    #[test]
    fn rejects_truncated_tcp() {
        assert!(extract_tcp(&[0u8; 10]).is_none());
    }
}