How Sniffnet Determines Which Processes Are Generating Network Traffic

Sniffnet determines which processes are generating network traffic through an asynchronous, multi-stage pipeline that uses the listeners crate to query OS-specific APIs, decoupling expensive process lookups from real-time packet capture via cross-thread channels and aggressive caching.

Sniffnet is an open-source network monitoring tool written in Rust that attributes live traffic to specific applications. Understanding how Sniffnet determines which processes are generating network traffic reveals a sophisticated mechanism designed to maintain high-performance packet analysis while querying OS process tables without blocking the main capture thread.

The Architecture: Decoupling Capture from Process Lookup

Sniffnet’s process-to-traffic mapping is fundamentally asynchronous. Rather than querying the operating system process table synchronously for every packet—which would cause severe latency—it implements a producer-consumer pattern using Rust’s std::sync::mpsc channels. The main packet capture thread delegates expensive lookups to a dedicated background thread, while continuing to process traffic at line speed. Results are cached in the ProgramLookup struct and applied retroactively to connections that arrived before the OS query completed.

The Six-Stage Process Identification Pipeline

Stage 1: Packet Capture and Flow Extraction

When Sniffnet captures raw packets via pcap, it decodes them using the etherparse library in manage_packets::analyze_headers (src/networking/manage_packets.rs, lines 28-71). This function extracts the source and destination IPs, ports, and transport protocol (TCP/UDP/ICMP), constructing an AddressPortPair struct.

If the packet represents the first observation of a network flow (indicated by the is_new_connection flag), Sniffnet flags it for process lookup. This ensures only new connections trigger OS queries, minimizing redundant system calls.

Stage 2: Asynchronous Lookup Requests

The ProgramLookup struct manages the lookup lifecycle. When ProgramLookup::lookup_and_add_data (src/networking/types/program_lookup.rs, lines 52-67) receives a new (port, protocol) tuple, it first checks its internal cache (self.state).

  • Cache hit: Returns the cached Program immediately.
  • Cache miss: Sends the tuple via the port_tx channel to the background lookup thread and registers a pending entry in self.state.

This non-blocking approach ensures the packet capture thread never waits for OS API calls.

Stage 3: OS-Level Process Resolution

The background thread runs the lookup_program function (src/networking/types/program_lookup.rs, lines 241-244), which listens on port_rx for incoming tuples. For each request, it calls listeners::get_process_by_port(port, protocol).

The listeners crate abstracts platform-specific APIs:

  • Windows: GetExtendedTcpTable and GetExtendedUdpTable
  • Linux: /proc/net/tcp, /proc/net/udp, and /proc/<pid>/fd/
  • macOS: proc_pidfdinfo and sysctl network sockets

This returns an optional Process struct containing the PID, process name, and executable path.

Stage 4: Result Aggregation and Retroactive Mapping

When the OS query completes, the background thread sends (port, protocol, Option<Process>) back via the program_tx channel. The main thread consumes this in ProgramLookup::update (src/networking/types/program_lookup.rs, lines 124-152), which converts the Process into a Program enum (wrapping the process data or representing Unknown/NotApplicable).

Crucially, update retroactively associates this program with any recent connections whose program field was temporarily marked as Unknown, ensuring traffic captured during the lookup window is correctly attributed.

Stage 5: Per-Process Traffic Aggregation

Once resolved, the Program enum serves as a key in ProgramLookup.programs, a HashMap<Program, DataInfo> that accumulates traffic statistics (bytes sent/received, packet counts) per application. The DataInfo struct tracks real-time metrics, enabling the UI to display bandwidth consumption per process without re-scanning the connection table.

Stage 6: GUI Integration and Icon Retrieval

The GUI (implemented in src/gui/sniffer.rs, lines 1030-1075) calls ProgramLookup::programs() to retrieve the aggregated map. For each identified process, Sniffnet fetches application icons asynchronously via the picon crate. The Program::icon_key() method sends requests via icon_key_tx to a second helper thread running get_picon, which loads and caches icon handles in ProgramLookup.picons for display alongside traffic data.

Key Source Files and Functions

File Role Key Function/Struct
src/networking/manage_packets.rs Packet parsing and flow detection analyze_headers, AddressPortPair
src/networking/types/program_lookup.rs Async lookup orchestration and caching ProgramLookup, lookup_and_add_data, update, lookup_program
src/networking/types/program.rs Process data representation Program enum, Process wrapper
src/gui/sniffer.rs UI consumption and display draw (GUI loop consuming program_lookup.programs())

Implementation Examples

Example 1: Manual Process Lookup (Stand-alone)

This example mirrors Sniffnet’s internal channel-based lookup mechanism:

use std::sync::mpsc;
use listeners::{Protocol, Process};

fn main() {
    // Channels used by ProgramLookup internally
    let (port_tx, port_rx) = mpsc::channel::<(u16, Protocol)>();
    let (program_tx, program_rx) = mpsc::channel::<(u16, Protocol, Option<Process>)>();

    // Spawn the background lookup thread (same code used by Sniffnet)
    std::thread::spawn(move || {
        listeners::lookup_program(&port_rx, &program_tx);
    });

    // Request a lookup for port 443 (HTTPS) over TCP
    let _ = port_tx.send((443, Protocol::TCP));

    // Receive the result
    if let Ok((port, proto, proc_opt)) = program_rx.recv() {
        println!("Port {port}/{proto:?} → {:?}", proc_opt.map(|p| p.name));
    }
}

Example 2: Using ProgramLookup in Sniffnet’s Core Loop

// Inside the packet-processing loop:
let mut prog_lookup = ProgramLookup::new(
    port_tx.clone(),
    program_rx.clone(),
    icon_key_tx.clone(),
    picon_rx.clone(),
);

// When a new connection is observed:
let key = (port, protocol);
let is_new = true; // first packet of this flow
let new_data = DataInfo::default(); // traffic counters for this packet
let program = prog_lookup.lookup_and_add_data(key, is_new, new_data);
println!("Traffic belongs to process: {}", program.display_name());

Example 3: Updating Pending Lookups (UI Integration)

// Periodically (e.g., every UI refresh):
let pending = prog_lookup.pending_results();
for result in pending {
    prog_lookup.update(result, &mut connections);
}
prog_lookup.handle_pending_icons(); // refresh icons if any arrived

Summary

  • Sniffnet uses the external listeners crate to abstract OS-specific APIs (Windows GetExtendedTcpTable, Linux /proc/net/tcp, macOS proc_pidfdinfo) for cross-platform process identification.
  • The ProgramLookup struct decouples packet capture from expensive system calls using port_tx and program_rx channels, ensuring real-time performance.
  • Process mappings are cached in self.state and aggregated per-application in ProgramLookup.programs with traffic statistics (DataInfo).
  • Unidentified connections are temporarily marked as Unknown and retroactively updated when the OS query completes via ProgramLookup::update.
  • The GUI consumes these mappings via ProgramLookup::programs() and loads icons asynchronously via the picon crate for visual identification.

Frequently Asked Questions

How does Sniffnet identify which process owns a specific network connection?

Sniffnet extracts the local port and protocol (TCP/UDP) from packet headers using manage_packets::analyze_headers and forwards them to a background thread. This thread invokes listeners::get_process_by_port, which queries OS-specific process-port mapping APIs. The result—a Process struct containing PID, name, and path—is returned via channel and converted into a Program enum for internal storage.

Does Sniffnet slow down packet capture when looking up process information?

No. Sniffnet uses asynchronous channels to decouple process lookups from packet capture. The main thread calls ProgramLookup::lookup_and_add_data to send lookup requests via port_tx and immediately continues processing packets. A dedicated background thread handles the expensive OS queries, ensuring the capture loop never blocks on system calls.

What happens when Sniffnet cannot identify a process for a connection?

When listeners::get_process_by_port returns None—either because the process terminated or OS permissions restrict access—Sniffnet creates a Program::Unknown variant. The system retains the connection data, and ProgramLookup::update attempts to re-attribute the traffic if the process is later identified. Traffic statistics for unknown processes are still collected but displayed without application names or icons.

How does Sniffnet display application icons for identified processes?

After resolving a process, Sniffnet generates an icon key via Program::icon_key() and sends it via icon_key_tx to a second helper thread. This thread uses the picon crate to load the executable’s native icon, caching the result as an IconHandle in ProgramLookup.picons. The GUI retrieves these handles during the render loop to display visual indicators alongside per-process traffic statistics.

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 →