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

> Discover how Sniffnet analyzes packet headers across network layers using Rust and the etherparse crate. Extract key network details efficiently.

- Repository: [Giuliano Bellini/sniffnet](https://github.com/GyulyVGC/sniffnet)
- Tags: deep-dive
- Published: 2026-04-28

---

**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`](https://github.com/GyulyVGC/sniffnet/blob/main/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.

```rust
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`](https://github.com/GyulyVGC/sniffnet/blob/main/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.

### Handling Special Link-Layer Types

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`](https://github.com/GyulyVGC/sniffnet/blob/main/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.

### Link-Layer Analysis: Ethernet II and Linux SLL

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

```rust
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`](https://github.com/GyulyVGC/sniffnet/blob/main/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:

```rust
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`](https://github.com/GyulyVGC/sniffnet/blob/main/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:

```rust
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`](https://github.com/GyulyVGC/sniffnet/blob/main/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`](https://github.com/GyulyVGC/sniffnet/blob/main/src/networking/parse_packets.rs), these functions chain together to process live traffic:

```rust
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`](https://github.com/GyulyVGC/sniffnet/blob/main/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`](https://github.com/GyulyVGC/sniffnet/blob/main/Cargo.toml) in the repository, this dependency enables the `LaxPacketHeaders` structure that forms the foundation of Sniffnet's cross-layer analysis.

### How does Sniffnet handle different link-layer types like Wi-Fi or Loopback?

The **`MyLinkType`** enum in [`src/networking/parse_packets.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/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`](https://github.com/GyulyVGC/sniffnet/blob/main/src/networking/manage_packets.rs) and [`src/networking/parse_packets.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/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.