# How Sniffnet Determines Which Processes Are Generating Network Traffic

> Discover how Sniffnet identifies network traffic generators. Learn about its efficient pipeline using OS APIs, cross-thread channels, and caching for real-time analysis.

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

---

**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`](https://github.com/GyulyVGC/sniffnet/blob/main/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`](https://github.com/GyulyVGC/sniffnet/blob/main/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`](https://github.com/GyulyVGC/sniffnet/blob/main/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`](https://github.com/GyulyVGC/sniffnet/blob/main/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`](https://github.com/GyulyVGC/sniffnet/blob/main/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`](https://github.com/GyulyVGC/sniffnet/blob/main/src/networking/manage_packets.rs) | Packet parsing and flow detection | `analyze_headers`, `AddressPortPair` |
| [`src/networking/types/program_lookup.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/src/networking/types/program_lookup.rs) | Async lookup orchestration and caching | `ProgramLookup`, `lookup_and_add_data`, `update`, `lookup_program` |
| [`src/networking/types/program.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/src/networking/types/program.rs) | Process data representation | `Program` enum, `Process` wrapper |
| [`src/gui/sniffer.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/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:

```rust
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

```rust
// 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)

```rust
// 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.