How Sniffnet Detects Traffic Direction: IP Address Analysis and Port-Based Heuristics

Sniffnet determines packet direction by comparing source and destination IP addresses against the capture interface's assigned addresses, applying special port-based logic for loopback traffic and handling edge cases like DHCP requests with unspecified source addresses.

Sniffnet, an open-source network traffic analyzer written in Rust, classifies every captured packet as either incoming or outgoing to enable accurate statistics and visualization. Understanding the mechanism for detecting traffic direction in Sniffnet requires examining how the application distinguishes local from remote traffic without relying on operating system APIs. The implementation centers on the get_traffic_direction function in src/networking/manage_packets.rs, which evaluates IP locality against the network interface's assigned address list.

The TrafficDirection Enum

Sniffnet represents traffic direction using a simple enum defined in src/networking/types/traffic_direction.rs. This enum provides two variants to classify packet flow:

/// Enum representing the possible traffic direction (incoming or outgoing).
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum TrafficDirection {
    /// Incoming traffic (from remote address to local interface)
    #[default]
    Incoming,
    /// Outgoing traffic (from local interface to remote address)
    Outgoing,
}

According to the Sniffnet source code, Incoming serves as the default variant, though the get_traffic_direction function explicitly returns the appropriate variant based on packet analysis.

The Direction Detection Algorithm

The core logic for detecting traffic direction resides in get_traffic_direction within src/networking/manage_packets.rs. This function receives the packet's source and destination IP addresses, optional port numbers, and a slice of pcap::Address structs representing every IPv4 and IPv6 address assigned to the capture interface.

Loopback Interface Handling

Sniffnet applies a special heuristic for loopback traffic where both source and destination IPs are loopback addresses (127.0.0.1 or ::1). Since the same host appears as both sender and receiver, the function compares the source and destination ports:

  • If the source port is higher than the destination port, the packet is classified as outgoing
  • Otherwise, the packet is classified as incoming

This port comparison identifies which side of a local connection initiated the communication.

Local Address Validation

The function determines IP locality through an is_local closure that checks whether an address belongs to my_interface_addresses. When analyzing offline PCAP files where the interface address list is empty, Sniffnet falls back to bogon detection using the is_bogon function to infer locality. This ensures traffic direction detection works even without access to the original network interface configuration.

The Decision Tree

After handling the loopback special case, get_traffic_direction follows this logic sequence:

  1. Source is local → Returns TrafficDirection::Outgoing (packet left the local host)
  2. Source is non-local and not unspecified (0.0.0.0 / ::) → Returns TrafficDirection::Incoming (packet arrived from a remote host)
  3. Source is unspecified (0.0.0.0 or ::) and destination is not local → Returns TrafficDirection::Outgoing (handles DHCP discovery and similar broadcast traffic)
  4. Otherwise → Returns TrafficDirection::Incoming (covers traffic addressed to a local IP)

The full implementation (trimmed for brevity) appears as follows:

fn get_traffic_direction(
    source_ip: &IpAddr,
    destination_ip: &IpAddr,
    source_port: Option<u16>,
    dest_port: Option<u16>,
    my_interface_addresses: &[Address],
) -> TrafficDirection {
    // 1️⃣ Loopback special case
    if source_ip.is_loopback()
        && destination_ip.is_loopback()
        && let (Some(sport), Some(dport)) = (source_port, dest_port)
    {
        return if sport > dport {
            TrafficDirection::Outgoing
        } else {
            TrafficDirection::Incoming
        };
    }

    // 2️⃣ Helper to check if an IP belongs to the capture interface
    let is_local = |ip: &IpAddr| -> bool {
        if my_interface_addresses.is_empty() {
            // Offline capture – treat bogon addresses as “local”
            is_bogon(ip).is_some()
        } else {
            my_interface_addresses.iter().any(|a| a.addr == *ip)
        }
    };

    // 3️⃣ Main decision tree
    if is_local(source_ip) {
        TrafficDirection::Outgoing
    } else if source_ip.ne(&IpAddr::V4(Ipv4Addr::UNSPECIFIED))
        && source_ip.ne(&IpAddr::V6(Ipv6Addr::UNSPECIFIED))
    {
        TrafficDirection::Incoming
    } else if !is_local(destination_ip) {
        TrafficDirection::Outgoing
    } else {
        TrafficDirection::Incoming
    }
}

Practical Implementation Examples

Example 1: Standard Traffic from Local Host to Remote Server

use std::net::{IpAddr, Ipv4Addr};
use pcap::Address;
use sniffnet::networking::manage_packets::get_traffic_direction;
use sniffnet::networking::types::traffic_direction::TrafficDirection;

// Interface addresses (normally obtained from CaptureSource::get_addresses())
let iface_addrs = vec![
    Address {
        addr: IpAddr::V4("192.168.1.42".parse().unwrap()),
        netmask: Some(IpAddr::V4("255.255.255.0".parse().unwrap())),
        broadcast_addr: Some(IpAddr::V4("192.168.1.255".parse().unwrap())),
        dst_addr: None,
    },
];

// Packet from local host to example.com
let src = IpAddr::V4("192.168.1.42".parse().unwrap());
let dst = IpAddr::V4("93.184.216.34".parse().unwrap());
let dir = get_traffic_direction(&src, &dst, Some(54321), Some(80), &iface_addrs);
assert_eq!(dir, TrafficDirection::Outgoing);

Example 2: DHCP Discovery with Unspecified Source

let src = IpAddr::V4(Ipv4Addr::UNSPECIFIED); // 0.0.0.0
let dst = IpAddr::V4("255.255.255.255".parse().unwrap()); // broadcast
let dir = get_traffic_direction(&src, &dst, None, Some(67), &iface_addrs);
assert_eq!(dir, TrafficDirection::Outgoing);

Example 3: Loopback Traffic Classification

let src = IpAddr::V4(Ipv4Addr::LOCALHOST);
let dst = IpAddr::V4(Ipv4Addr::LOCALHOST);
// Source port > destination port indicates outgoing traffic
let dir = get_traffic_direction(&src, &dst, Some(60000), Some(5000), &[]);
assert_eq!(dir, TrafficDirection::Outgoing);

Integration with Packet Flow Processing

The modify_or_insert_in_map function serves as Sniffnet's central packet-processing routine, calling get_traffic_direction for every new flow key:

let traffic_direction = get_traffic_direction(
    source_ip,
    destination_ip,
    key.sport,
    key.dport,
    my_interface_addresses,
);

As implemented in GyulyVGC/sniffnet, the resulting TrafficDirection value is stored in the flow's InfoAddressPortPair structure (defined in src/networking/types/address_port_pair.rs). This classification enables the application to aggregate accurate statistics, render directional indicators in the UI, and determine which remote addresses require reverse DNS lookups.

Summary

  • TrafficDirection Enum: Defined in src/networking/types/traffic_direction.rs with Incoming and Outgoing variants to represent packet flow direction.
  • Core Algorithm: The get_traffic_direction function in src/networking/manage_packets.rs implements the classification logic using IP address locality checks.
  • Loopback Handling: Uses port number comparison (higher port = outgoing) when both source and destination are loopback addresses.
  • Offline Support: Falls back to bogon detection when interface addresses are unavailable, ensuring PCAP analysis works correctly.
  • Edge Cases: Correctly handles DHCP requests with 0.0.0.0 sources by checking destination locality.
  • Integration: Results are stored in InfoAddressPortPair structures for use in statistics aggregation and UI visualization.

Frequently Asked Questions

How does Sniffnet handle loopback traffic direction detection?

When both source and destination IP addresses are loopback addresses, Sniffnet compares the source and destination ports. If the source port is numerically higher than the destination port, the packet is classified as outgoing; otherwise, it is classified as incoming. This heuristic effectively identifies which side of a local connection initiated the communication, as implemented in the get_traffic_direction function.

What happens when Sniffnet analyzes an offline PCAP file?

When the my_interface_addresses slice is empty—which occurs when analyzing offline PCAP files without active interface bindings—Sniffnet falls back to bogon detection using the is_bogon function. This fallback allows the application to infer address locality and correctly classify traffic direction even without access to the original network interface configuration.

How does Sniffnet classify packets with 0.0.0.0 or :: source addresses?

Packets with unspecified source addresses (IPv4 0.0.0.0 or IPv6 ::) are classified as outgoing if the destination is not a local address, which correctly handles DHCP discovery and similar bootstrap traffic. If the destination is local, the packet is classified as incoming. This logic is part of the decision tree in src/networking/manage_packets.rs.

Where is the traffic direction stored after classification?

The get_traffic_direction function returns a TrafficDirection enum variant that is immediately stored in the InfoAddressPortPair structure by the modify_or_insert_in_map function. This value persists throughout the flow's lifetime and is used for aggregating connection statistics, rendering directional indicators in the Sniffnet UI, and determining which addresses require reverse DNS resolution.

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 →