How Sniffnet Analyzes Packet Headers at Different Network Layers: A Deep Dive into the Rust Implementation

Sniffnet uses the etherparse crate to decode raw packets into a LaxPacketHeaders structure, then processes them through three specialized functions—analyze_link_header, analyze_network_header, and analyze_transport_header—to extract MAC addresses, IP addresses, ports, and protocol information across the OSI stack.

Sniffnet, an open-source network monitoring tool written in Rust, implements a sophisticated packet inspection pipeline capable of dissecting network traffic from the link layer through the transport layer. Understanding how Sniffnet analyzes packet headers at different network layers reveals a clean, modular architecture that separates raw byte decoding from high-level traffic analysis. This walkthrough examines the actual source code from the GyulyVGC/sniffnet repository to show exactly how the application handles Ethernet frames, IP packets, and transport protocols.

The Two-Stage Architecture: From Raw Bytes to Structured Headers

Sniffnet’s packet inspection follows a strict two-stage pipeline. First, raw captured bytes are normalized into a flexible header container. Second, those headers are decomposed into concrete fields used to build traffic statistics.

Stage 1: Extracting LaxPacketHeaders with get_sniffable_headers

The entry point for all packet parsing is get_sniffable_headers in src/networking/parse_packets.rs. This function inspects the link-layer type reported by the capture device and selects the appropriate parser from the etherparse crate.

pub(super) fn get_sniffable_headers(
    packet: &[u8],
    my_link_type: MyLinkType,
) -> Option<LaxPacketHeaders<'_>> {
    match my_link_type {
        MyLinkType::Ethernet(_) | MyLinkType::Unsupported(_) | MyLinkType::NotYetAssigned => {
            LaxPacketHeaders::from_ethernet(packet).ok()
        }
        MyLinkType::RawIp(_) | MyLinkType::IPv4(_) | MyLinkType::IPv6(_) => {
            LaxPacketHeaders::from_ip(packet).ok()
        }
        MyLinkType::LinuxSll(_) => from_linux_sll(packet, true),
        MyLinkType::LinuxSll2(_) => from_linux_sll(packet, false),
        MyLinkType::Null(_) | MyLinkType::Loop(_) => from_null(packet),
    }
}
  • Location: src/networking/parse_packets.rs lines 298–312.
  • Behavior: Returns a LaxPacketHeaders struct containing optional LinkHeader, NetHeaders (IPv4/IPv6/ARP), and TransportHeader (TCP/UDP/ICMP) fields.

Not all packets arrive as standard Ethernet II frames. Sniffnet handles BSD/Unix "Null/Loopback" headers and Linux "cooked" (SLL) headers through dedicated helper functions:

  • from_null: Parses 4-byte Null/Loopback headers (lines 314–341) that indicate the address family before the IP header.
  • from_linux_sll: Handles 16-byte (SLL) or 20-byte (SLL2) cooked headers (lines 443–560), extracting sender MAC addresses when available and passing the payload to LaxPacketHeaders::from_ether_type.

Deconstructing Headers by OSI Layer

Once LaxPacketHeaders is populated, analyze_headers in src/networking/manage_packets.rs (lines 30–71) orchestrates the layer-specific extraction. This function builds an AddressPortPair—the fundamental key used for aggregating traffic statistics—while populating auxiliary data like MAC addresses and byte counts.

The analyze_link_header function processes the physical-layer information, tracking header sizes and extracting MAC addresses:

fn analyze_link_header(
    link_header: Option<LinkHeader>,
    mac_address1: &mut Option<String>,
    mac_address2: &mut Option<String>,
    exchanged_bytes: &mut u128,
) {
    match link_header {
        Some(LinkHeader::Ethernet2(header)) => {
            *exchanged_bytes += 14;
            *mac_address1 = Some(mac_from_dec_to_hex(header.source));
            *mac_address2 = Some(mac_from_dec_to_hex(header.destination));
        }
        Some(LinkHeader::LinuxSll(header)) => {
            *exchanged_bytes += 16;
            *mac_address1 = if header.sender_address_valid_length == 6
                && header.arp_hrd_type == ArpHardwareId::ETHERNET
                && let Ok(sender) = header.sender_address[0..6].try_into()
            {
                Some(mac_from_dec_to_hex(sender))
            } else {
                None
            };
        }
        None => {
            *mac_address1 = None;
            *mac_address2 = None;
        }
    }
}
  • Location: src/networking/manage_packets.rs lines 73–105.
  • Key logic: Adds 14 bytes for Ethernet II or 16 bytes for Linux SLL to the byte counter, converting raw MAC bytes to hexadecimal strings only when the header type supports physical addresses.

Network-Layer Analysis: IPv4, IPv6, and ARP

The analyze_network_header function extracts IP addresses and handles the Address Resolution Protocol (ARP) as a special case:

fn analyze_network_header(
    network_header: Option<NetHeaders>,
    exchanged_bytes: &mut u128,
    address1: &mut IpAddr,
    address2: &mut IpAddr,
    arp_type: &mut ArpType,
) -> bool {
    match network_header {
        Some(NetHeaders::Ipv4(ipv4header, _)) => {
            *address1 = IpAddr::from(ipv4header.source);
            *address2 = IpAddr::from(ipv4header.destination);
            *exchanged_bytes += u128::from(ipv4header.total_len);
            true
        }
        Some(NetHeaders::Ipv6(ipv6header, _)) => {
            *address1 = IpAddr::from(ipv6header.source);
            *address2 = IpAddr::from(ipv6header.destination);
            *exchanged_bytes += u128::from(40 + ipv6header.payload_length);
            true
        }
        Some(NetHeaders::Arp(arp_packet)) => {
            *exchanged_bytes += arp_packet.packet_len() as u128;
            *arp_type = ArpType::from_etherparse(arp_packet.operation);
            true
        }
        None => false,
    }
}
  • Location: src/networking/manage_packets.rs lines 107–164.
  • Details: For IPv4, it uses total_len; for IPv6, it adds the fixed 40-byte header to payload_length. ARP packets populate an ArpType enum instead of IP addresses.

Transport-Layer Analysis: TCP, UDP, and ICMP

The final layer extracts port numbers for connection-oriented protocols and type codes for ICMP:

fn analyze_transport_header(
    transport_header: Option<TransportHeader>,
    port1: &mut Option<u16>,
    port2: &mut Option<u16>,
    protocol: &mut Protocol,
    icmp_type: &mut IcmpType,
) -> bool {
    match transport_header {
        Some(TransportHeader::Udp(udp_header)) => {
            *port1 = Some(udp_header.source_port);
            *port2 = Some(udp_header.destination_port);
            *protocol = Protocol::UDP;
            true
        }
        Some(TransportHeader::Tcp(tcp_header)) => {
            *port1 = Some(tcp_header.source_port);
            *port2 = Some(tcp_header.destination_port);
            *protocol = Protocol::TCP;
            true
        }
        Some(TransportHeader::Icmpv4(icmpv4_header)) => {
            *port1 = None;
            *port2 = None;
            *protocol = Protocol::ICMP;
            *icmp_type = IcmpTypeV4::from_etherparse(&icmpv4_header.icmp_type);
            true
        }
        Some(TransportHeader::Icmpv6(icmpv6_header)) => {
            *port1 = None;
            *port2 = None;
            *protocol = Protocol::ICMP;
            *icmp_type = IcmpTypeV6::from_etherparse(&icmpv6_header.icmp_type);
            true
        }
        None => false,
    }
}
  • Location: src/networking/manage_packets.rs lines 166–204.
  • Behavior: TCP and UDP populate source/destination ports, while ICMP (v4 and v6) clears ports and records the specific ICMP type code. If no transport header exists (and the packet isn't ARP), the function returns false and the packet is discarded.

The Packet Processing Pipeline in Practice

Inside the main capture loop in src/networking/parse_packets.rs, these functions chain together to process live traffic:

if let Some(headers) = get_sniffable_headers(&packet.data, my_link_type) {
    let key_option = analyze_headers(
        headers,
        &mut mac_addresses,
        &mut exchanged_bytes,
        &mut icmp_type,
        &mut arp_type,
    );
    // key_option yields an AddressPortPair or None (packet skipped)
}

This decoupled design allows Sniffnet to analyze packet headers across different network layers while maintaining thread safety—the parsing runs on a dedicated thread, feeding results to the UI via async channels without blocking the capture process.

Summary

  • Sniffnet analyzes packet headers using a two-stage pipeline: first decoding raw bytes with get_sniffable_headers, then extracting specific fields with analyze_headers.
  • Layer-specific functions in src/networking/manage_packets.rs handle Link-layer (MAC addresses), Network-layer (IPs, ARP), and Transport-layer (ports, ICMP types) separately.
  • The etherparse crate provides the underlying "lax" parsing that accommodates malformed packets without panicking.
  • Modular architecture means the same analyze_link_header, analyze_network_header, and analyze_transport_header logic can be reused for offline PCAP analysis or custom network tools.

Frequently Asked Questions

What Rust library does Sniffnet use to parse packet headers?

Sniffnet relies on the etherparse crate, which provides zero-copy, "lax" parsers capable of handling partial or malformed packets. According to the Cargo.toml in the repository, this dependency enables the LaxPacketHeaders structure that forms the foundation of Sniffnet's cross-layer analysis.

The MyLinkType enum in src/networking/parse_packets.rs discriminates between Ethernet II, Linux SLL/SLL2, Null/Loopback, and raw IP captures. The get_sniffable_headers function matches against this enum to select the correct entry point—from_ethernet, from_linux_sll, or from_null—ensuring accurate header extraction regardless of the capture interface.

What happens when Sniffnet encounters a malformed or truncated packet?

Because Sniffnet uses LaxPacketHeaders from the etherparse crate, it tolerates missing or corrupted headers. If a mandatory layer is absent (such as a truncated IP header), analyze_network_header or analyze_transport_header returns false, causing analyze_headers to return None and the packet to be silently skipped without crashing the application.

Can Sniffnet's packet header analysis be used outside the GUI application?

Yes. The functions get_sniffable_headers and analyze_headers are modular and stateless, depending only on the etherparse types and Sniffnet's internal type definitions. You can import these from src/networking/manage_packets.rs and src/networking/parse_packets.rs to build command-line tools or automated traffic analyzers that leverage Sniffnet's exact parsing logic without initializing the graphical interface.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →