Skip to content

Latest commit

 

History

469 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

packet_parser

CI codecov Crates.io License: MIT docs.rs Book

packet_parser is a Rust crate for parsing raw network packets. It starts at the data-link layer and progressively decodes internet, transport and application-layer information.

The main API is PacketFlow: a borrowed, zero-copy representation of a parsed packet. Unknown or unsupported protocols above the data-link layer do not make the whole parse fail. The crate keeps the layers it could decode and leaves the next layers as None when parsing cannot safely continue.

Version francaise

Packet parser overview

Documentation

Installation

[dependencies]
packet_parser = "11.6.0"

For examples that decode hexadecimal packet dumps:

[dependencies]
hex = "0.4"
packet_parser = "11.0.0"

Quick Example

use packet_parser::{LinkType, parse};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let raw = hex::decode(
        "feaa81e86d1efeaa818ec864080045500034000000003d06206b36e6700d\
         ac140a0201bbc1087d7f02aa4e2b998e80100081748300000101080a9373\
         c9c207ef14e3",
    )?;

    // Always pass the LINKTYPE the capture declares.
    let flow = parse(LinkType::ETHERNET, &raw)?;

    println!("L2: {}", flow.data_link);

    if let Some(internet) = &flow.internet {
        println!(
            "L3: {} {:?} -> {:?}",
            internet.protocol_name, internet.source, internet.destination
        );
    }

    if let Some(transport) = &flow.transport {
        println!(
            "L4: {:?} {:?} -> {:?}",
            transport.protocol, transport.source_port, transport.destination_port
        );
    }

    if let Some(application) = &flow.application {
        println!("L7: {}", application.application_protocol);
    }

    Ok(())
}

Choosing the LINKTYPE

parse is fail-closed on the link layer: the LINKTYPE comes from the caller and is never guessed from the bytes. Check it up front to reject a capture before reading a single packet:

use packet_parser::{LinkType, is_supported, parse};

let link_type = LinkType::ETHERNET;
if !is_supported(link_type) {
    return Err(format!("unsupported LINKTYPE {}", link_type).into());
}

let flow = parse(link_type, packet_bytes)?;

packet_bytes must contain exactly one packet, without a PCAP or PCAPNG record header. LinkType uses the canonical LINKTYPE_* namespace stored in capture files. A live-capture adapter must normalize DLT_* values first when their numeric value differs. For PCAPNG, the capture reader resolves the interface referenced by each packet and passes that interface's LINKTYPE.

Do not use the Ethernet shortcut on an unknown capture

PacketFlow::try_from(&[u8]) and DataLink::try_from(&[u8]) take a bare byte slice and assume Ethernet. They are kept for compatibility and will be removed in a future major release.

They do not fail on a non-Ethernet capture — they silently produce wrong data. A LINKTYPE_LINUX_SLL frame (what a capture on the Linux any interface yields) has its 16 cooked-header bytes reinterpreted as an Ethernet header: the call returns Ok with fabricated MAC addresses, internet: None and corrupted: None. Nothing signals the mistake, and a flow matrix built from it fills up with addresses that never existed on the wire.

Supported LINKTYPEs:

LINKTYPE Value Decoder status
BSD loopback (NULL) 0 Supported: four address-family bytes, then the IP packet. The byte order is the capturing host's, so the field is read both ways and cross-checked against the IP version. LINKTYPE_LOOP (108), its network-order OpenBSD twin, is not handled — no capture attests it
Ethernet 1 Supported
RAW IP 101 Supported for IPv4 and IPv6
Native IEEE 802.11 105 Modelled for CAPWAP inner flows; top-level decoder not yet supported
Linux SLL v1 113 Supported
Bluetooth H4 with pseudo-header 201 Identified, explicitly unsupported
IPv4 raw 228 Supported
IPv6 raw 229 Supported
IEEE 802.3br mPacket 274 Supported for express mPackets (SMD-E); preemptible fragments (SMD-S/C) are refused with a named error, their reassembly being stateful
Linux SLL v2 276 Supported
Any other value Preserved as-is ParseError::UnsupportedLinkType

An unsupported LINKTYPE is rejected before packet bytes are decoded. Unknown upper-layer protocols still use the graceful None/corrupted behaviour described above.

For RAW IP, an empty packet or a version nibble other than 4/6 returns a structured InvalidLinkLayer(LinkLayerError). Once IPv4 or IPv6 is identified, an invalid or truncated IP header remains a successful partial flow with corrupted: Internet; the link layer and its accounting are preserved.

Linux SLL v1 decodes its 16-byte cooked header in network byte order and keeps the packet type, raw ARPHRD hardware type, declared address length, available source-address bytes and protocol value. Unknown numeric values are preserved; an address longer than the eight-byte wire slot is reported as truncated rather than rejected. Use canonical LinkType::LINUX_SLL (113): the value 25 displayed by some Wireshark fields is an internal WTAP encapsulation identifier.

Linux SLL v2 independently decodes its 20-byte header and additionally keeps the numeric capture-machine interface index and the reserved-MBZ field. A non-zero reserved value is preserved and reported by reserved_is_zero() rather than discarding an otherwise decodable packet, matching Tshark's tolerant dissection. Interface names are not resolved because they belong to the capture machine. Use canonical LinkType::LINUX_SLL2 (276); Wireshark's current internal WTAP encapsulation identifier for this format is 210.

Every parsed flow now carries a generic LinkLayer. Its common accessors do not assume Ethernet:

println!("LINKTYPE={}", flow.data_link.link_type());
println!("next={:?}", flow.data_link.network_protocol());

if let Some(ethernet) = flow.data_link.as_ethernet() {
    println!("{} -> {}", ethernet.source_mac, ethernet.destination_mac);
}

network_payload() returns the borrowed L3 slice. Format-specific views are explicit (as_ethernet(), as_raw_ip(), as_linux_sll(), as_linux_sll2(), as_ieee80211()), so RAW and both SLL formats cannot silently manufacture Ethernet fields.

Main API

Need API
Check whether a link decoder exists is_supported(LinkType)
Parse a packet (canonical) parse(LinkType, &[u8])
Parse with caller-declared ports ("Decode As": my FTP runs on 2121) parse_with(LinkType, &[u8], &ParseConfig::new().decode_as(port, DecodeAsProtocol::..))
Parse Ethernet, compat shortcut — assumes Ethernet, see above PacketFlow::try_from(&[u8])
Parse only Ethernet/VLAN — assumes Ethernet, see above DataLink::try_from(&[u8])
Parse only L3, from the protocol the link layer announced Internet::try_from_network_parts(NetworkProtocol, &[u8])
Parse only L4, from the protocol the IP header announced Transport::try_from_parts(Option<TransportProtocol>, &[u8])
Detach the result from the original buffer flow.to_owned_flow() → PacketFlowOwned
Iterate over encapsulated flows flow.flatten()
Measure an explicit LINKTYPE parse_timed(...) with the parse_timing feature
Measure Ethernet through the compatibility API PacketFlow::try_from_timed(...) with the parse_timing feature

PacketFlow contains:

pub struct PacketFlow<'a> {
    pub data_link: LinkLayer<'a>,
    pub internet: Option<Internet<'a>>,
    pub transport: Option<Transport<'a>>,
    pub application: Option<Application>,
    pub inner: Option<Box<PacketFlow<'a>>>,
}

The 7.0 serialization schema nests the link layer and uses stable tags. The borrowed and owned link models serialize identically (payload bytes are not serialized):

{
  "data_link": {
    "link_type": 1,
    "network_protocol": { "kind": "ipv4" },
    "link_kind": "ethernet",
    "link_details": {
      "destination_mac": "00:11:22:33:44:55",
      "source_mac": "66:77:88:99:aa:bb",
      "ethertype": "IPv4"
    }
  }
}

Protocol Support

Data Link

  • Ethernet II
  • VLAN 802.1Q
  • PPPoE session (EtherType 0x8864, RFC 2516): unwrapped like a VLAN tag when PPP carries IPv4 or IPv6 (DataLink::pppoe keeps the session id, length and PPP protocol); PPP control protocols (LCP, IPCP, IPv6CP) are named, PPPoE Discovery (0x8863) only by its EtherType
  • RAW IPv4/IPv6 (LINKTYPE_RAW)
  • Linux cooked capture v1 (LINKTYPE_LINUX_SLL)
  • Linux cooked capture v2 (LINKTYPE_LINUX_SLL2)
  • MAC addresses and internal OUI resolution
  • Native IEEE 802.11 representation for CAPWAP inner flows (not yet a top-level LINKTYPE decoder)

Internet

  • ARP
  • IPv4
  • IPv6
  • Profinet: every RT Frame ID class (cyclic data, alarms, PTCP, fragments) is validated; DCP messages are decoded by ProfinetPacket, block by block
  • PTP (IEEE 1588) directly over Ethernet, EtherType 0x88F7, decoded into InternetDetails::Ptp
  • GOOSE (IEC 61850-8-1) directly over Ethernet, EtherType 0x88B8, with or without a VLAN tag, decoded into InternetDetails::Goose: APPID, length, reserved words (simulation bit), then the goosePdu (control block reference, time allowed to live, data set, goID, UtcTime timestamp, state and sequence numbers, simulation, configuration revision, needs commissioning, number of entries, every allData value with its BER tag and bytes); also recognised blindly when the EtherType is unknown
  • LLDP (IEEE 802.1AB) directly over Ethernet, EtherType 0x88CC, decoded into InternetDetails::Lldp: every TLV borrowed, the mandatory chassis ID, port ID and time to live, then port description, system name and description, capabilities, management addresses and organizationally specific TLVs (OUI, subtype, information: PROFINET, IEEE 802.1, IEEE 802.3, LLDP-MED...); also recognised blindly when the EtherType is unknown

For fragmented IPv4 packets, the crate does not perform IP reassembly. In that case payload_protocol is set to None so the transport layer is not parsed from incomplete data.

Transport

  • TCP
  • UDP
  • ICMPv4 (echo request/reply, and the error reports that quote the original datagram: destination unreachable, redirect, time exceeded, parameter problem). Reached through IP protocol number 1, never through probing.
  • ICMPv6 (echo request/reply, the error reports that quote the invoking packet, and the neighbor discovery messages of RFC 4861: router solicitation and advertisement — the latter with its hop limit, M/O flags, router lifetime, reachable time and retransmit timer — plus neighbor solicitation and advertisement with their target address and R/S/O flags). Reached through IPv6 next header 58. Type numbering is disjoint from ICMPv4 — 128 is an echo request there, 8 is undefined — so the two have separate parsers.

Neither ICMP version carries an application layer, so payload stays None and the decoded message is exposed through TransportDetails::Icmp / TransportDetails::Icmpv6.

  • Mapping from many IP protocol numbers to TransportProtocol

Protocols other than TCP/UDP/ICMP can be represented by the enum, but they do not always expose ports or application payloads.

Application

Application detection is intentionally best-effort. Parser modules include:

  • DNS (including mDNS through DnsPacket::try_from_mdns)
  • TLS
  • SNMP
  • NTP (modes 1 to 5) on UDP 123; on any other port only when the poll, precision and root delay are values real implementations send and the bytes after the 48-byte header are well-formed extension fields and MAC (NTS, MS-SNTP)
  • DHCP / DHCPv6
  • HTTP
  • MQTT
  • PostgreSQL
  • FTP
  • SMTP
  • NNTP
  • SSH (identification string only: everything after the version exchange is encrypted, so a stateless parser labels the banner frames and nothing else)
  • Modbus TCP on TCP 502 or a Decode As port; on any other port only when every PDU carries a common public function code and the length it implies (a bounded Read Device Identification for function 43), Active Directory LDAP and DICOM association rejects excepted
  • UMAS (Schneider Electric)
  • EtherNet/IP
  • OPC UA
  • S7Comm
  • COTP
  • AMS
  • GIOP
  • SRVLOC
  • ASTERIX (EUROCONTROL surveillance data, eleven categories: CAT 048 and 034 monoradar reports and service messages, CAT 001 and 002 their first-generation counterparts, CAT 008 weather, CAT 021 ADS-B, CAT 023 ground station status, CAT 062 system tracks, CAT 063 sensor status, CAT 065 SDPS status, CAT 247 version numbers). Recognised by structure alone — every data block of a known category, records split exactly along the UAP, Random Field Sequencing included — since the protocol has neither a magic nor an IANA port
  • QUIC
  • Bitcoin
  • RTPS (OMG DDSI-RTPS 2.x, the wire protocol of DDS): header, submessages split and typed (DATA, DATA_FRAG, HEARTBEAT, ACKNACK, GAP, INFO_TS, INFO_DST, INFO_SRC, fragment submessages, DDS Security and vendor ones), entity ids, sequence numbers and counts; user data (CDR) and inline QoS stay opaque. Recognised by its magic (RTPS, or RTI's RTPX) and the exact tiling of its submessages, on any UDP port; RTI's sixteen-byte NDDSPING pings are reported too
  • BACnet/IP (ASHRAE 135 annex J) on UDP 47808: BVLC (function named, length equal to the datagram, origin of a Forwarded-NPDU), NPDU (version, control, destination and source network addresses, hop count, network messages with vendor id) and APDU (type, flags, invoke id, segmentation, confirmed and unconfirmed services named); recognised on its ports and blindly on any UDP port
  • IEC 60870-5-104 on TCP 2404: APDUs of a segment (the last one cut by segmentation is decoded as far as it goes), I/S/U formats with sequence numbers or the STARTDT/STOPDT/TESTFR function, and for each ASDU the type named by its IEC 60870-5-101 mnemonic, cause of transmission, originator and common addresses, and the information objects (address, element sized from the standard, in sequence or not)
  • RDP (MS-RDPBCGR) on TCP 3389: TPKT and COTP X.224 Connection Request (routing token or mstshash cookie, negotiation request) and Confirm (negotiation response or failure), then the MCS PDUs of the standard RDP security path (Connect-Initial/Response, Erect Domain, Attach User, Channel Join, Send Data with user and channel, Disconnect Provider Ultimatum); several TPKTs per segment, a TPKT cut by segmentation is decoded as far as it goes. After a TLS or CredSSP negotiation the connection is labelled TLS, by decision
  • RADIUS (RFC 2865, accounting RFC 2866, CoA RFC 5176): header (code named, identifier, length, authenticator) and the attribute list (type named from the IANA registry, value borrowed, accessors for integers, IPv4 addresses, texts and Vendor-Specific); recognised on UDP 1812/1813/1645/1646/3799 and blindly on any port (known code, length equal to the datagram, attributes tiling it)
  • WireGuard: the four messages (handshake initiation and response, cookie reply, transport data) with their reserved bytes, session indexes, data counter and fixed fields (ephemeral key, encrypted fields, mac1, mac2), borrowed; nothing is decrypted. Recognised through Decode As by its shape; on UDP 51820 like tshark's heuristic (zero reserved bytes for a cookie or data); blindly on any other port only with zero reserved bytes and, for data, a counter below 2^60 and a plaintext padded to 16 bytes or cut at an MTU of at least 552 bytes. boringtun-based peers, which do not pad, are only recognised blindly on their handshakes, keepalives and round-sized packets; the official Cloudflare WARP client (message types 0xC1-0xC4) is not decoded
  • DNP3 (IEEE 1815): link frames with their header and per-block CRC-16/DNP verified (length, control bits and function, addresses), transport header, application header (control, function code, internal indications) and the chain of object headers (group, variation, qualifier, range or count, position of the data sized from annex A), several frames per payload. Recognised blindly on TCP and UDP, the CRCs are the signature
  • IKE (ISAKMP/IKEv1 RFC 2408, IKEv2 RFC 7296) on UDP 500 and 4500: 28-byte header (SPIs, version, exchange type named, flags, message ID, length), chain of top-level payloads with named types, encrypted bytes kept as such (IKEv1 E flag, IKEv2 SK payload); UDP 4500 demultiplexed per RFC 3948 (non-ESP marker, NAT-keepalive, ESP)
  • ESP (RFC 4303): SPI and sequence number, native (IP protocol 50, exposed as TransportDetails::Esp) and UDP-encapsulated on 4500 (EspPacket), the rest kept as encrypted bytes
  • DIS (IEEE 1278.1, Distributed Interactive Simulation): PDU header (versions 1 to 7, the 72 standard PDU types named, family, timestamp, length, PDU Status), Entity State, Transmitter and Signal bodies typed, other bodies as bytes, several PDUs per datagram. Recognised blindly when the PDUs tile the datagram exactly; on UDP 3000 (or a Decode As port) a wrong declared length is tolerated and the rest kept as trailing bytes
  • STANAG 5066 SIS (Subnetwork Interface Sublayer, NATO HF radio data stack) on TCP 5066: every S_PDU type (bind, hard links, subnet availability, management, unidata and expedited unidata with their confirmations and rejections), node addresses, SAPs, service types, TTL, U_PDU sizes; the first S_PDU of a segment, flagged when it overflows into the next segment, consumed for the following ones. Port-guarded, other ports via Decode As
  • PTP (IEEE 1588) on UDP 319/320: PTPv2 decoded in full (common header, the ten message bodies, TLVs listed without decoding their value), PTPv1 recognised by its header. Port-guarded: a Delay_Req has exactly the shape of an SLPv1 header

FTP, SMTP and NNTP detection in PacketFlow is both parser-validated and restricted to their plaintext control ports (TCP 21, 25/587 and 119 respectively). Payloads on other ports are not labelled as these protocols: their line-oriented syntax can also occur inside another text protocol's body. Complete TLS records on implicit-TLS ports such as 465, 563 and 990 are reported as TLS. To inspect externally decrypted application data, call the detailed protocol parser directly.

PacketFlow currently exposes a lightweight application protocol name through Application { application_protocol }. For detailed protocol-specific parsing, use the corresponding module under packet_parser::parse::application::protocols.

Tunnels

PacketFlow can represent several flow levels through inner.

The currently supported tunnels are:

Tunnel Carried over Inner
CAPWAP-Data UDP/5247 IEEE 802.11 → LLC/SNAP → L3
GRE (v0) IP protocol 47 IPv4, IPv6 or Ethernet
IP-in-IP IP protocols 4 and 41 bare IPv4 / IPv6
VXLAN UDP/4789 full Ethernet frame
Geneve UDP/6081 Ethernet (0x6558) or bare IP
GTP-U UDP/2152 bare IP, no L2

Variants we cannot attest are refused rather than guessed: ERSPAN and GRE version 1, the VXLAN GBP/GPE flag extensions, Geneve OAM control messages, and — for GTP-U — GTPv0, GTP' and every message type other than G-PDU.

Fragmented outer datagrams are not peeled: reassembly is stateful and out of scope for this parser, so a tunnel is never reported as more complete than the datagram carrying it.

Example:

let flow = PacketFlow::try_from(packet.as_slice())?;

for level in flow.flatten() {
    println!("{:?} -> {:?}", level.internet, level.transport);
}

Features

Feature Effect
parse_timing Makes parse_timed and PacketFlow::try_from_timed actually measure each layer; without it the same API exists and ParseTiming stays zeroed

The parse_timing feature is intended for benchmarks. The normal PacketFlow::try_from path does not measure parsing time.

Example:

use packet_parser::{PacketFlow, timing::ParseTiming};

let mut timing = ParseTiming::default();
let flow = PacketFlow::try_from_timed(packet.as_slice(), &mut timing)?;

println!("L2={}ns L3={}ns L4={}ns L7={}ns total={}ns",
    timing.l2_ns,
    timing.l3_ns,
    timing.l4_ns,
    timing.l7_ns,
    timing.total_ns,
);

Enable it with:

cargo test --features parse_timing

Benchmarks and HTML Report

The main benchmark harness is tools/verbench. It compares published crate versions from crates.io with the local working copy, then generates:

  • perf_by_version.json
  • perf_by_version.html

Run the full benchmark:

tools/verbench/run.sh

Regenerate only the HTML report from an existing JSON file:

python3 tools/verbench/report.py

The HTML report is standalone. It opens directly in a browser and does not need Docker, Postgres, Grafana or any CDN.

xdg-open perf_by_version.html

tools/verbench reports average l2_ns, l3_ns, l4_ns, l7_ns and total_ns values on a fixed reference packet after warmup. Use these numbers to compare trends between versions on the same machine, not as universal absolute latency claims.

Optional PCAP Pipeline

The workspace also contains benchmark_db, a binary that parses local PCAP files and writes JSONL events containing:

  • run_id
  • crate_code
  • pcap
  • packet index
  • packet hash
  • total duration
  • OSI timings when parse_timing is enabled

Run it with:

cargo run -p benchmark_db --release

Output files are written to:

~/.local/share/packet_parser_bench/jsonl/

The optional docker-compose.yml pipeline can ingest those JSONL files into Postgres and display them in Grafana. This is not required for the standalone verbench HTML report.

Examples

The examples/ directory contains several useful entry points:

cargo run --example parse_tcp
cargo run --example parse_hex_dump
cargo run --example pars_quic
cargo run --example parse_pgadm

Tests and Quality

Common checks:

cargo fmt -- --check
cargo clippy --all-targets -- -D warnings
cargo test --all-features
cargo build --release

For binaries that read PCAP files through the pcap crate, install the system libpcap development package. On Debian/Ubuntu:

sudo apt-get install libpcap-dev

Known Limitations

  • No TCP reassembly.
  • No IP reassembly.
  • Application detection is heuristic and best-effort.
  • The parse_timing path is dedicated to measurement and should not be treated as the standard parsing path.
  • Timed parsing does not yet recursively measure inner flows produced by tunnel parsing.

License

Distributed under the MIT license. See LICENSE.md.

Releases

Packages

Used by

Contributors

Languages