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

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, 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, the Sniffer::start() method establishes the channel infrastructure:

// 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, 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:

// 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) 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:

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) 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. When BackendTrafficMessage::TickRun arrives, the UI updates its models and charts:

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:

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:

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:

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:

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.

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.

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 →