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
Programimmediately. - Cache miss: Sends the tuple via the
port_txchannel to the background lookup thread and registers a pending entry inself.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:
GetExtendedTcpTableandGetExtendedUdpTable - Linux:
/proc/net/tcp,/proc/net/udp, and/proc/<pid>/fd/ - macOS:
proc_pidfdinfoandsysctlnetwork 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
listenerscrate to abstract OS-specific APIs (WindowsGetExtendedTcpTable, Linux/proc/net/tcp, macOSproc_pidfdinfo) for cross-platform process identification. - The
ProgramLookupstruct decouples packet capture from expensive system calls usingport_txandprogram_rxchannels, ensuring real-time performance. - Process mappings are cached in
self.stateand aggregated per-application inProgramLookup.programswith traffic statistics (DataInfo). - Unidentified connections are temporarily marked as
Unknownand retroactively updated when the OS query completes viaProgramLookup::update. - The GUI consumes these mappings via
ProgramLookup::programs()and loads icons asynchronously via thepiconcrate 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →