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.rslines 298–312. - Behavior: Returns a
LaxPacketHeadersstruct containing optionalLinkHeader,NetHeaders(IPv4/IPv6/ARP), andTransportHeader(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 toLaxPacketHeaders::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.
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:
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.rslines 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.rslines 107–164. - Details: For IPv4, it uses
total_len; for IPv6, it adds the fixed 40-byte header topayload_length. ARP packets populate anArpTypeenum 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.rslines 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
falseand 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 withanalyze_headers. - Layer-specific functions in
src/networking/manage_packets.rshandle Link-layer (MAC addresses), Network-layer (IPs, ARP), and Transport-layer (ports, ICMP types) separately. - The
etherparsecrate provides the underlying "lax" parsing that accommodates malformed packets without panicking. - Modular architecture means the same
analyze_link_header,analyze_network_header, andanalyze_transport_headerlogic 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.
How does Sniffnet handle different link-layer types like Wi-Fi or Loopback?
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →