# How Sniffnet's Multi-Threaded Packet Capture Backend Interacts with the GUI

> Discover how Sniffnet's multi-threaded packet capture backend communicates with its GUI using typed message channels for efficient data flow and control, ensuring a seamless user experience.

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

---

**Sniffnet isolates all packet capture and analysis work on dedicated OS threads, communicating with the GUI exclusively through typed message channels (`async_channel` and `std::sync::mpsc`), while pause/resume control flows through a `tokio::sync::broadcast` channel.**

Sniffnet is a cross-platform network traffic analyzer written in Rust that maintains a responsive user interface during high-volume packet capture by strictly separating blocking I/O operations from the GUI thread. Understanding how the multi-threaded packet capture backend interacts with the GUI reveals a strict message-passing architecture that prevents UI freezing while handling real-time traffic analysis, reverse DNS resolution, and process identification.

## Architectural Overview

Sniffnet's architecture follows a strict **thread-per-concern** model where the GUI (`Sniffer` struct) never shares memory with background workers. According to the Sniffnet source code in `GyulyVGC/sniffnet`, the system spawns several dedicated threads:

- **Main GUI thread** – Runs the iced application loop, holds all UI state, and renders pages. It receives `BackendTrafficMessage` events via an `async_channel::Receiver`.
- **Capture parser thread** (`thread_parse_packets`) – Reads packets from `pcap::Capture`, analyzes headers using functions from [`src/networking/manage_packets.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/src/networking/manage_packets.rs), aggregates traffic statistics in `InfoTraffic`, and spawns reverse-DNS lookups. It sends messages back to the GUI through an `async_channel::Sender`.
- **Reverse-DNS lookup thread** – Performs blocking `dns_lookup::lookup_addr` calls. It receives requests via `std::sync::mpsc::Receiver` and returns results through a separate sender.
- **Program lookup thread** (`thread_lookup_program`) – Queries process names for network ports using netstat-style lookups (live captures only).
- **Program-icon fetch thread** (`thread_get_picon`) – Retrieves application icons (picons) asynchronously.
- **Traffic preview thread** (`thread_traffic_preview`) – Samples per-device packet counts periodically for the welcome page preview cards.

All background threads operate in **fire-and-forget** mode, meaning they never access GUI widgets directly. The only way the UI updates is when the `Sniffer::update` method processes incoming messages from these channels.

## Starting the Backend from the GUI

When a user initiates capture, the GUI creates bidirectional communication channels and spawns the packet parser. In [`src/gui/sniffer.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/src/gui/sniffer.rs), the `Sniffer::start()` method establishes the channel infrastructure:

```rust
// src/gui/sniffer.rs – Sniffer::start()
let (tx, rx) = async_channel::unbounded();               // UI ← backend channel
let (freeze_tx, freeze_rx) = tokio::sync::broadcast::channel(1_048_575);
let freeze_rx2 = freeze_tx.subscribe();                  // clone for the parse thread

// Spawn the packet-parsing thread
thread::Builder::new()
    .name("thread_parse_packets".to_string())
    .spawn(move || {
        parse_packets(
            curr_cap_id,
            capture_source,
            mmdb_readers,
            &ip_blacklist,
            capture_context,
            filters,
            &tx,                       // send traffic messages back
            (freeze_rx, freeze_rx2),   // allow pausing/resuming
        );
    })
    .log_err(location!());

self.current_capture_rx.1 = Some(rx.clone());  // store UI side receiver
self.freeze_tx = Some(freeze_tx);

```

The GUI creates an unbounded `async_channel` for traffic data and a `tokio::sync::broadcast` channel for pause/resume commands. It then launches `parse_packets` in a new OS thread named `thread_parse_packets`.

For live captures, `Sniffer::start` also spawns program lookup and icon fetch threads. The traffic preview thread starts earlier during GUI initialization via `start_traffic_previews`.

## The Capture Thread Event Loop

Inside [`src/networking/parse_packets.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/src/networking/parse_packets.rs), the `parse_packets` function runs an infinite loop that handles packet acquisition, analysis, and message dispatch. The loop structure demonstrates how the backend handles flow control and data processing:

```rust
// src/networking/parse_packets.rs – parse_packets()
loop {
    // 1️⃣  Freeze handling (pause the capture)
    if freeze_rx.try_recv().is_ok() {
        let _ = freeze_rx.blocking_recv();  // wait for unfreeze signal
        first_packet_ticks = Some(Instant::now());
    }

    // 2️⃣  Pull a packet from pcap (or from a file)
    let (packet_res, cap_stats) = pcap_rx.recv_timeout(Duration::from_millis(150))
        .unwrap_or((Err(pcap::Error::TimeoutExpired), None));

    // 3️⃣  If live capture → maybe send a TickRun every second
    if matches!(cs, CaptureSource::Device(_)) {
        maybe_send_tick_run_live(...);
    }

    // 4️⃣  Process a successful packet
    if let Ok(packet) = packet_res {
        // – Parse headers, analyse link/network/transport
        // – Build AddressPortPair key
        // – Update traffic map (modify_or_insert_in_map)
        // – Request reverse-DNS if needed (push to lookup_request_tx)
        // – Accumulate stats (tot_data_info)
    }

    // 5️⃣  Send messages to GUI:
    //    BackendTrafficMessage::TickRun (periodic) or
    //    BackendTrafficMessage::PendingHosts (final hosts after a file ends)
    //    BackendTrafficMessage::OfflineGap (gap detection in offline files)
}

```

The loop uses `recv_timeout` to allow periodic checking of the freeze channel without blocking indefinitely. When packets arrive, the thread calls `analyze_headers` and `modify_or_insert_in_map` (defined in [`src/networking/manage_packets.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/src/networking/manage_packets.rs)) to update traffic statistics.

## Reverse DNS and Asynchronous Lookups

When the capture thread encounters an IP address requiring hostname resolution, it delegates the blocking DNS call to a dedicated thread. Inside `parse_packets`, the code checks resolution state and queues requests:

```rust
if !r_dns_already_resolved && !r_dns_waiting_resolution {
    resolutions_state.addresses_waiting_resolution.insert(
        address_to_lookup,
        DataInfo::new_with_first_packet(exchanged_bytes, traffic_direction),
    );

    // Send lookup request to the dedicated DNS thread
    let _ = resolutions_state.lookup_request_tx.send((
        key,
        traffic_direction,
        cs.get_addresses().clone(),
    ));
}

```

The reverse-DNS thread (`reverse_dns_lookups` in [`parse_packets.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/parse_packets.rs)) consumes requests from its `std::sync::mpsc::Receiver`, performs the blocking `dns_lookup::lookup_addr` call, builds a `HostMessage` containing the resolved hostname, country, and ASN data, then pushes results back through `lookup_result_tx`. The capture thread periodically drains these results and forwards them to the GUI as part of the next `TickRun` message.

## GUI Message Handling and Updates

The GUI receives backend messages through a `Task::run` stream in [`src/gui/sniffer.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/src/gui/sniffer.rs). When `BackendTrafficMessage::TickRun` arrives, the UI updates its models and charts:

```rust
Task::run(rx, |backend_msg| match backend_msg {
    BackendTrafficMessage::TickRun(cap_id, msg, host_msg, no_more_packets) => {
        Message::TickRun(cap_id, msg, host_msg, no_more_packets)
    }
    BackendTrafficMessage::PendingHosts(cap_id, host_msg) => {
        Message::PendingHosts(cap_id, host_msg)
    }
    BackendTrafficMessage::OfflineGap(cap_id, gap) => {
        Message::OfflineGap(cap_id, gap)
    }
})

```

The `tick_run` method processes these messages:

```rust
fn tick_run(&mut self, cap_id: usize, msg: InfoTraffic,
            host_msgs: Vec<HostMessage>, no_more_packets: bool) {
    if cap_id != self.current_capture_rx.0 { return; }

    // New hosts (reverse-DNS resolved)
    for host_msg in host_msgs {
        self.handle_new_host(host_msg);
    }

    // Program lookup results (if any)
    if let Some(pl) = &mut self.program_lookup {
        pl.handle_pending_icons();
        for prog_res in pl.pending_results() {
            self.handle_program_lookup_result(prog_res);
        }
    }

    // Refresh UI data, charts, notifications
    self.refresh_data(msg, no_more_packets);
}

```

*`refresh_data` updates the `InfoTraffic` model, triggers notifications, and feeds the traffic chart via `self.traffic_chart.update_charts_data`.* The `PendingHosts` variant flushes remaining DNS results after offline file capture completes, while `OfflineGap` handles time discontinuities in PCAP files.

## Pause and Resume Control

The GUI controls capture flow through a broadcast channel. When the user presses **Ctrl+Space**, the `freeze()` method toggles the pause state:

```rust
fn freeze(&mut self) {
    self.frozen = !self.frozen;
    if let Some(tx) = &self.freeze_tx {
        let _ = tx.send(());
    }
}

```

The capture thread detects this signal using `try_recv()` on its `freeze_rx` handle:

```rust
if freeze_rx.try_recv().is_ok() {
    let _ = freeze_rx.blocking_recv();  // blocks until next broadcast
    first_packet_ticks = Some(Instant::now());
}

```

This mechanism allows **non-blocking coordination**: the parser checks for pause signals during its timeout loop, enters a blocked state until the next toggle, then resumes cleanly with reset timing statistics.

## Traffic Preview Thread

Beyond the main capture flow, Sniffnet runs a continuous `thread_traffic_preview` to sample live device statistics for the welcome page. Started during GUI initialization via `start_traffic_previews`, this thread periodically emits `TrafficPreview` messages through a dedicated `async_channel`:

```rust
let (tx, rx) = async_channel::unbounded();
thread::Builder::new()
    .name("thread_traffic_preview".to_string())
    .spawn(move || traffic_preview(&tx))
    .log_err(location!());
self.preview_captures_rx = Some(rx.clone());
Task::run(rx, |traffic_preview| Message::TrafficPreview(traffic_preview))

```

The handler `fn traffic_preview(&mut self, msg: TrafficPreview)` merges incoming data into `self.preview_charts`, updating the small device preview cards without interfering with active capture sessions.

## Summary

- **Thread isolation** – All blocking operations (packet parsing, DNS lookups, program identification) execute on dedicated OS threads, preventing GUI freezes during high-throughput capture.
- **Channel-based communication** – The GUI and backend communicate exclusively through `async_channel` and `std::sync::mpsc`, exchanging immutable message types like `BackendTrafficMessage::TickRun` and `HostMessage`.
- **Broadcast control** – Pause/resume functionality uses `tokio::sync::broadcast` to signal the capture thread without destroying the channel or losing state.
- **Immutable data snapshots** – The GUI never shares mutable state with background threads; it receives periodic snapshots (`InfoTraffic`, `TrafficPreview`) that update models and charts atomically.

## Frequently Asked Questions

### What message types does the multi-threaded packet capture backend send to the GUI?

The backend sends three primary variants of `BackendTrafficMessage`: `TickRun` (containing periodic traffic statistics and resolved host data), `PendingHosts` (flushing remaining DNS results after file capture ends), and `OfflineGap` (indicating time discontinuities in offline PCAP files). These messages travel through an `async_channel::Sender<BackendTrafficMessage>` created in [`src/gui/sniffer.rs`](https://github.com/GyulyVGC/sniffnet/blob/main/src/gui/sniffer.rs).

### How does Sniffnet pause packet capture without losing data?

Sniffnet uses a `tokio::sync::broadcast` channel named `freeze_tx`/`freeze_rx` for flow control. When the user triggers a pause, the GUI broadcasts a `()` signal. The capture thread detects this via `freeze_rx.try_recv()`, then blocks on `freeze_rx.blocking_recv()` until the next broadcast resumes execution. This pauses packet processing while maintaining the `pcap::Capture` handle and internal state.

### Why does Sniffnet use separate threads for reverse DNS lookups?

Reverse DNS resolution uses blocking system calls (`dns_lookup::lookup_addr`) that can take hundreds of milliseconds or timeout entirely. Performing these lookups on the capture thread would stall packet processing and drop packets. Instead, `parse_packets` spawns `thread_reverse_dns_lookups` and communicates via `std::sync::mpsc` channels, allowing continuous packet capture while hostname resolution proceeds asynchronously.

### What is the purpose of the `thread_traffic_preview` thread?

The traffic preview thread runs continuously in the background to sample per-device packet statistics for the welcome page cards. Unlike the main capture thread which operates only during active analysis, this thread provides live device activity indicators before the user selects a specific interface, using a separate `async_channel` to send `TrafficPreview` messages to the GUI without interfering with the primary capture flow.