Skip to content

Comment capturer des paquets réseau en Rust avec Tauri

Pour comprendre comment capturer des paquets réseau en Rust dans une application Tauri, je vais te montrer mon cheminement complet — depuis l’idée d’un simple compteur jusqu’à une sonde réseau robuste.

Ce n’est pas juste un tutoriel. C’est un journal technique, une sorte de carnet de bord dans lequel chaque problème m’a permis d’apprendre quelque chose de nouveau : sur les threads, la gestion d’état, la communication UI/backend, ou les particularités de la crate pcap.


🛠 Étape 1 – Démarrer avec un compteur dans un thread

Section titled “🛠 Étape 1 – Démarrer avec un compteur dans un thread”

Avant de capturer des paquets réseau, je veux maîtriser le principe d’un thread Rust contrôlé par Tauri. Voici une fonction très simple qui lance un thread de comptage :

use tauri::command;
use std::time::Duration;
use std::thread;
/// Cette commande démarre un thread qui compte chaque seconde.
#[command(async)]
pub fn start_counter() {
thread::spawn(move || {
let mut count = 0;
loop {
count += 1;
println!("Compteur: {}", count);
thread::sleep(Duration::from_secs(1));
}
});
println!("Thread de comptage lancé !");
}

start_counter() appelée

Spawn du thread

Boucle infinie

Incrémenter compteur

Afficher avec println!

Dormir 1 seconde


🧩 Étape 2 – Ajouter le contrôle start / stop du thread

Section titled “🧩 Étape 2 – Ajouter le contrôle start / stop du thread”

Créer un compteur Rust contrôlable à travers des commandes Tauri :

  • ✅ Démarrer le thread avec start_counter
  • ✅ L’arrêter proprement avec stop_counter

Tu introduis :

  • une structure globale AppState partagée dans un Arc<Mutex<...>>
  • un CounterHandle qui possède un stop_flag partagé entre thread et handler
  • deux commandes Tauri :
    • start_counter qui vérifie qu’il n’y a pas de thread déjà actif
    • stop_counter qui signale au thread qu’il doit s’arrêter
pub fn stop(&self) {
self.stop_flag.store(true, Ordering::Relaxed);
}

➡️ Cela coupe la boucle du thread dans while !stop_flag.load(...).


thread::spawn(move || {
let mut count = 0;
while !stop_flag.load(Ordering::Relaxed) {
count += 1;
println!("Compteur: {}", count);
thread::sleep(Duration::from_secs(1));
}
println!("Thread terminé.");
});

  • Arc permet de partager la donnée entre le thread et l’extérieur (via le CounterHandle)
  • AtomicBool évite les verrous pour un flag booléen simple
  • Ordering::Relaxed est suffisant ici : pas besoin de synchronisation complexe

if app.counter.is_some() {
println!("Déjà en cours.");
return;
}

➡️ Tu empêches le démarrage de plusieurs threads (bonne pratique).

if let Some(counter) = app.counter.take() {
counter.stop();
}

➡️ Tu “consommes” le compteur en le retirant de l’état global (take()).


_____Loop

true

false

UI (Vue.js)

start_counter

stop_counter

stop_flag.load() == false ?

thread::spawn

count += 1; sleep(1s)

Thread terminé


  • ✅ Tu contrôles le lancement et l’arrêt du thread à volonté.
  • ✅ Tu gères un seul thread actif à la fois.
  • ✅ Tu poses les bases pour les étapes suivantes : pause, resume, puis pcap.

📁 Étape 3 – Ajout de pause et resume avec Condvar

Section titled “📁 Étape 3 – Ajout de pause et resume avec Condvar”

Contrôler l’état d’exécution d’un thread via une pause réversible, sans tuer le thread, pour préparer les futurs cas d’usage comme la capture réseau avec interruption temporaire.


Tu introduis ici un mécanisme de pause/reprise avec :

  • Un Arc<(Mutex<bool>, Condvar)> appelé pause_flag
  • Le thread se met en attente (Condvar::wait) tant que pause_flag == true
  • Une fonction pause() qui met true
  • Une fonction resume() qui met false et notify_all()

while !stop_flag.load(Ordering::Relaxed) {
let (lock, cvar) = &*pause_flag;
let mut paused = lock.lock().unwrap();
if *paused {
println!("[DEBUG] Le thread est en pause, en attente...");
}
while *paused {
paused = cvar.wait(paused).unwrap();
println!("[DEBUG] Signal de reprise reçu");
}
drop(paused);
count += 1;
println!("Compteur: {}", count);
thread::sleep(Duration::from_secs(1));
}

thread while

Non

Oui

start() appelé

spawn du thread

Boucle: !stop_flag

pause_flag == true ?

count += 1; sleep

Condvar::wait

Reprise: notify_all

stop() appelé

stop_flag = true


  • Permet de bloquer le thread efficacement (pas de spin/sleep inutile)
  • Attente conditionnelle, sans gaspiller de CPU
  • Synchronisation typique dans les threads bloquants

FonctionRôle
start()Lance le thread si pas déjà actif
stop()Coupe la boucle et réveille s’il était en pause
pause()Met le thread en attente
resume()Réveille le thread bloqué

  • Si on ne veut pas bloquer le thread (ex : avec pcap en mode nonblock), ce modèle est trop complexe
  • Risques de deadlock ou de signal perdu si mal utilisé

✅ C’est pour cette raison qu’à l’étape suivante, tu as décidé d’abandonner Condvar au profit d’un simple AtomicBool.


📊 Etape 4 — Intégration de pcap avec pause/reprise

Section titled “📊 Etape 4 — Intégration de pcap avec pause/reprise”

Passer du compteur simple à une vraie capture de paquets réseau via la crate pcap, tout en gardant la possibilité de mettre la capture en pause ou de l’arrêter grâce à des flags de contrôle partagés avec le thread de capture.


pub struct CaptureHandle {
stop_flag: Arc<AtomicBool>,
pause_flag: Arc<(Mutex<bool>, Condvar)>,
}
  • stop_flag : permet d’interrompre la boucle de capture
  • pause_flag : permet de suspendre temporairement le thread sans le tuer

while !stop_flag.load(Ordering::Relaxed) {
// pause si activée
let (lock, cvar) = &*pause_flag;
let mut paused = lock.lock().unwrap();
while *paused {
println!("[DEBUG] Pause active, en attente...");
paused = cvar.wait(paused).unwrap();
println!("[DEBUG] Reprise de la capture");
}
drop(paused);
match cap.next_packet() {
Ok(packet) => println!("[CAPTURE] paquet de {} octets", packet.data.len()),
Err(e) => println!("[ERROR] Erreur de capture : {:?}", e),
}
let stats = cap.stats().unwrap();
println!(
"Received: {}, dropped: {}, if_dropped: {}",
stats.received, stats.dropped, stats.if_dropped
);
}

Non

Oui

UI Vue.js

start(interface)

thread::spawn

while !stop_flag

pause_flag == true ?

cap.next_packet()

Affichage stats

Condvar::wait

Reprise


  • Condvar peut bloquer le thread alors que pcap supporte le mode non-bloquant
  • La gestion des erreurs devient plus complexe si des signaux sont manqués (ex : notify_all() raté)
  • Ce modèle est plus adapté à des processus CPU-bloquants que à de la capture IO non-bloquante

Cette étape permet de valider la chaîne :

  • Contrôle de thread avec pause/reprise sur une vraie capture réseau
  • Intégration du pcap::Capture dans un thread
  • Premiers tests de robustesse via stop() et resume()

Mais les limites du modèle avec Condvar se font sentir.

🚧 Prochaine étape : remplacer Condvar par un simple AtomicBool pour un modèle plus adapté au non-blocking.


🚀 Etape 5 — Remplacement de Condvar par AtomicBool

Section titled “🚀 Etape 5 — Remplacement de Condvar par AtomicBool”

Remplacer la pause/reprise basée sur Condvar par une solution plus simple et plus robuste avec un AtomicBool, mieux adapté à la capture réseau en mode non-bloquant (setnonblock()).


pub struct CaptureHandle {
stop_flag: Arc<AtomicBool>,
pause_flag: Arc<AtomicBool>,
}
  • stop_flag : indique au thread de s’arrêter
  • pause_flag : contrôle le gel temporaire de la capture

CondvarAtomicBool
Bloque le threadPolling actif mais contrôlé
Difficile à synchroniser avec pcapCompatible avec setnonblock()
Risques de signal perduSimplicité, aucun verrou requis

if pause_flag.load(Ordering::Relaxed) {
thread::sleep(Duration::from_millis(100));
continue;
}

Cela permet au thread de rester réactif et non-bloquant tout en réduisant la charge CPU.


Un cache last_stats permet d’éviter d’afficher les statistiques en boucle :

if last_stats != Some(current) {
println!("[STATS] Received: {}, dropped: {}, if_dropped: {}", ...);
last_stats = Some(current);
}

true

false

UI Vue

start(interface)

thread::spawn

while !stop_flag

pause_flag ?

sleep(100ms)

cap.next_packet()

stats

pause

pause_flag = true

resume

pause_flag = false

stop

stop_flag = true


  • Simplicité de la logique de pause
  • Plus de compatibilité avec les captures pcap non bloquantes
  • Moins de complexité et de risque de bug

  • Un thread de capture contrôlable en start, stop, pause, resume
  • Une capture réseau performante et robuste
  • Un code propre, testable et cross-plateforme

Prochaine étape : 📊 **Séparer complètement la capture des paquets et leur traitement **

🧠 Étape 6 — Thread séparé pour traitement + stats

Section titled “🧠 Étape 6 — Thread séparé pour traitement + stats”

Séparer complètement la capture des paquets et leur traitement (parse, stats, etc.) dans deux threads indépendants.


  • Un CaptureHandle avec deux flags : stop_flag et pause_flag
  • Un thread de capture qui lit les paquets via pcap et les envoie dans un canal
  • Un thread de traitement qui lit depuis ce canal, décode les paquets, met à jour les compteurs et affiche les stats
  • Un enum CaptureMessage pour transmettre soit un paquet, soit les statistiques pcap::Stat

enum CaptureMessage {
Packet(PacketOwned),
Stats(Stat),
}

thread::spawn(move || {
// boucle de capture pcap
match cap.next_packet() {
Ok(packet) => {
let owned = codec.decode(packet);
tx.send(CaptureMessage::Packet(owned)).unwrap();
},
// ...
}
// stats périodiques
if let Ok(stats) = cap.stats() {
tx.send(CaptureMessage::Stats(stats)).unwrap();
}
});

thread::spawn(move || {
while let Ok(msg) = rx.recv() {
match msg {
CaptureMessage::Packet(pkt) => {
println!("Packet len={} ts={}.{:06}", pkt.data.len(), pkt.header.ts.tv_sec, pkt.header.ts.tv_usec);
// traitement, analyse...
}
CaptureMessage::Stats(stats) => {
println!("[STATS] received={}, dropped={}, if_dropped={}...", stats.received, stats.dropped, stats.if_dropped);
}
}
}
});

📊 Flowchart – Éviter les paquets perdus en déléguant le traitement

reciev

capture

true

false

fifo

Packet

Stat

start

thread::spawn capture

thread::spawn traitement

while !stop_flag

pause_flag ?

sleep(100ms)

cap.next_packet()

tx.send(Packet/Stat)

recv_loop

match msg

Decode + Analyse

Afficher + Calculer delta


GainDétail
📦 Séparation des responsabilitéscapture vs traitement
🚀 Meilleure performancecapture non bloquée, traitement découplé
🧱 Meilleure architecturethreads indépendants, plus testables

  • 🔄 Utiliser crossbeam::bounded() pour remplacer mpsc::sync_channel

🧱 Étape 7 — Transition de mpsc vers crossbeam::channel

Section titled “🧱 Étape 7 — Transition de mpsc vers crossbeam::channel”
  • std::sync::mpsc est simple, mais limité :
    • pas de fonction len()
    • performances moindres en haute fréquence
    • moins de flexibilité sur la stratégie de saturation
Fonctionnalitémpsccrossbeam::channel
Taille bornée (bounded)✅ (avec sync_channel)
try_send()
len() pour monitoring
PerformancesMoyennesExcellentes
Sécurité threadBonneTrès bonne
let (tx, rx) = crossbeam::channel::bounded::<CaptureMessage>(10_000);

Et dans le thread :

if let Err(err) = tx.try_send(CaptureMessage::Packet(owned)) {
error!("[TX] Packet dropped (buffer plein) : {}", err);
}

  • Le canal est plein car le thread de traitement est trop lent
  • Grâce à try_send(), on peut logguer ou ignorer les paquets plutôt que bloquer
  • Avec rx.len() on peut détecter la saturation imminente et agir proactivement (pause, discard, alertes…)

📄 Étape bonus : mise en place d’un backoff exponentiel

Section titled “📄 Étape bonus : mise en place d’un backoff exponentiel”

Quand pcap n’a pas de paquet à lire, on dort un tout petit peu — mais progressivement plus longtemps, jusqu’à 10 ms max.

let mut backoff = 1; // µs
backoff = (backoff * 2).min(10_000);

Cela réduit la charge CPU tout en gardant une réactivité raisonnable.


On mesure rx.len() dans le thread de traitement, et on déclenche un warn! si le canal est proche de la saturation :

if rx.len() > 9000 {
warn!("[BACKPRESSURE] Canal presque plein !");
}

Cela permet de :

  • Visualiser en prod si le thread de capture produit trop vite
  • Éventuellement adapter dynamiquement la stratégie (discard, buffer, etc.)

thread::spawn(move || {
while let Ok(msg) = rx.recv() {
match msg {
CaptureMessage::Packet(pkt) => { /* traitement */ },
CaptureMessage::Stats(stats) => { /* stats */ },
}
if rx.len() > 9000 {
warn!("Canal presque saturé !");
}
}
});

GainDétail
📦 Séparation des responsabilitéscapture vs traitement
🚀 Meilleure performancecapture non bloquée, traitement découplé
🧱 Meilleure architecturethreads indépendants, plus testables
⚠️ Détection proactivebackpressure visible dans les logs

📊 Flowchart – Éviter les paquets perdus en déléguant le traitement

Section titled “📊 Flowchart – Éviter les paquets perdus en déléguant le traitement”

Thread de traitement

Packet

Stat

Oui

Non

rx.recv()

Message ?

Analyse + Affichage

Calcul stats + delta

rx.len() > seuil ?

warn!(Canal saturé)

Thread de capture

cap.next_packet()

try_send(Packet)

try_send(Stat)

Backoff exponentiel si vide


  • ♻️ Réutiliser les buffers (Vec<u8> pré-alloués)
  • 🧠 Implémenter un pool d’objets pour éviter les reallocations fréquentes

Tu es maintenant prêt à benchmark chaque taille de buffer, et visualiser en direct l’impact sur la perf 🧪🚀