pcap-frame-parser 0.2.1

Zero-copy parser for legacy PCAP and PCAPng capture files, plus Ethernet/VLAN/IP/UDP/TCP frame dissection. No libpcap dependency.
Documentation
//! End-to-end test: build a synthetic PCAPng capture containing one
//! VLAN-tagged Ethernet/IPv4/UDP frame, parse the container, then walk
//! the frame all the way down to the UDP payload.

use pcap_frame_parser::{ethernet, pcapng};

fn build_udp_over_vlan_frame(payload: &[u8]) -> Vec<u8> {
    let mut frame = Vec::new();
    frame.extend_from_slice(&[0u8; 12]); // dst MAC + src MAC
    frame.extend_from_slice(&ethernet::ETHERTYPE_8021Q.to_be_bytes());
    frame.extend_from_slice(&0x0010u16.to_be_bytes()); // VLAN id 16
    frame.extend_from_slice(&ethernet::ETHERTYPE_IP.to_be_bytes());

    let udp_len = 8 + payload.len();
    let total_len = 20 + udp_len;

    // IPv4 header
    frame.push(0x45);
    frame.push(0);
    frame.extend_from_slice(&(total_len as u16).to_be_bytes());
    frame.extend_from_slice(&0u16.to_be_bytes());
    frame.extend_from_slice(&0u16.to_be_bytes());
    frame.push(64);
    frame.push(ethernet::PROTO_UDP);
    frame.extend_from_slice(&0u16.to_be_bytes());
    frame.extend_from_slice(&[10, 0, 0, 1]);
    frame.extend_from_slice(&[10, 0, 0, 2]);

    // UDP header + payload
    frame.extend_from_slice(&5353u16.to_be_bytes()); // src port
    frame.extend_from_slice(&53u16.to_be_bytes()); // dst port (DNS)
    frame.extend_from_slice(&(udp_len as u16).to_be_bytes());
    frame.extend_from_slice(&0u16.to_be_bytes()); // checksum
    frame.extend_from_slice(payload);

    frame
}

fn build_pcapng_with_frame(frame: &[u8]) -> Vec<u8> {
    let mut buf = Vec::new();
    const BLOCK_SHB: u32 = 0x0A0D_0D0A;
    const BOM: u32 = 0x1A2B_3C4D;
    const BLOCK_IDB: u32 = 1;
    const BLOCK_EPB: u32 = 6;

    let shb_len: u32 = 28;
    buf.extend_from_slice(&BLOCK_SHB.to_le_bytes());
    buf.extend_from_slice(&shb_len.to_le_bytes());
    buf.extend_from_slice(&BOM.to_le_bytes());
    buf.extend_from_slice(&1u16.to_le_bytes());
    buf.extend_from_slice(&0u16.to_le_bytes());
    buf.extend_from_slice(&(-1i64).to_le_bytes());
    buf.extend_from_slice(&shb_len.to_le_bytes());

    let idb_len: u32 = 20;
    buf.extend_from_slice(&BLOCK_IDB.to_le_bytes());
    buf.extend_from_slice(&idb_len.to_le_bytes());
    buf.extend_from_slice(&1u16.to_le_bytes()); // Ethernet
    buf.extend_from_slice(&0u16.to_le_bytes());
    buf.extend_from_slice(&65535u32.to_le_bytes());
    buf.extend_from_slice(&idb_len.to_le_bytes());

    let padded_len = (frame.len() + 3) & !3;
    let epb_len = 32 + padded_len as u32;
    buf.extend_from_slice(&BLOCK_EPB.to_le_bytes());
    buf.extend_from_slice(&epb_len.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&2_000_000u32.to_le_bytes()); // 2s in microseconds
    buf.extend_from_slice(&(frame.len() as u32).to_le_bytes());
    buf.extend_from_slice(&(frame.len() as u32).to_le_bytes());
    buf.extend_from_slice(frame);
    buf.extend(std::iter::repeat_n(0u8, padded_len - frame.len()));
    buf.extend_from_slice(&epb_len.to_le_bytes());

    buf
}

#[test]
fn full_pipeline_pcapng_to_udp_payload() {
    let dns_query_stub = b"fake-dns-query-bytes";
    let frame = build_udp_over_vlan_frame(dns_query_stub);
    let capture = build_pcapng_with_frame(&frame);

    let packets = pcapng::parse_pcapng(&capture).expect("valid capture");
    assert_eq!(packets.len(), 1);

    let (ts_sec, _ts_usec, raw_frame) = &packets[0];
    assert_eq!(*ts_sec, 2);

    let (ip_header, ip_payload) = ethernet::extract_ip(raw_frame).expect("VLAN-tagged IPv4 frame");
    assert_eq!(ip_header.protocol, ethernet::PROTO_UDP);
    assert_eq!(ip_header.src_str(), "10.0.0.1");
    assert_eq!(ip_header.dst_str(), "10.0.0.2");

    let (src_port, dst_port, udp_payload) =
        ethernet::extract_udp(ip_payload).expect("well-formed UDP datagram");
    assert_eq!(src_port, 5353);
    assert_eq!(dst_port, 53);
    assert_eq!(udp_payload, dns_query_stub);
}