Includes React PWA frontend, WebSocket signaling server, shared types, K8s manifests, Gitea CI/CD workflow, nginx config, and Dockerfiles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.4 KiB
Flux WebRTC détaillé
Ce document décrit pas-à-pas comment deux clients AnyDrop établissent une connexion P2P.
Acteurs
- Alice — émetteur (veut envoyer un fichier)
- Bob — récepteur
- Signaling — serveur WebSocket AnyDrop
- STUN —
stun.l.google.com:19302 - TURN — fallback si nécessaire
Scénario 1 : Alice et Bob sur le même Wi-Fi
1. Connexion au signaling
Alice ──WS──▶ Signaling : { type: "hello" }
Signaling ──▶ Alice : { type: "welcome", peerId: "a1", displayName: "Renard Bleu" }
Bob ──WS──▶ Signaling : { type: "hello" }
Signaling ──▶ Bob : { type: "welcome", peerId: "b2", displayName: "Tigre Rouge" }
Le serveur voit que les deux connexions proviennent de la même IP publique (la box d'Arthur) → il les place dans la room LAN hash(IP).
2. Annonce mutuelle
Signaling ──▶ Alice : { type: "peer-joined", peerId: "b2", displayName: "Tigre Rouge" }
Signaling ──▶ Bob : { type: "peer-joined", peerId: "a1", displayName: "Renard Bleu" }
Chaque client affiche un pair cliquable dans l'UI.
3. Alice déclenche l'envoi
Alice drop un fichier sur "Tigre Rouge". Son client crée une RTCPeerConnection en mode initiator :
const pc = new SimplePeer({ initiator: true, trickle: true });
pc.addStream(file); // simplifié
4. Collecte des ICE candidates
Le navigateur d'Alice :
- Regarde ses interfaces réseau → trouve
192.168.1.42→ candidate host - Interroge STUN → apprend son IP publique
82.x.x.x:54321→ candidate srflx (server reflexive) - Si TURN configuré → réserve une allocation → candidate relay
5. Échange SDP via signaling
Alice ──▶ Signaling : { type: "signal", to: "b2", data: <SDP offer + ICE host> }
Signaling ──▶ Bob : { type: "signal", from: "a1", data: <SDP offer + ICE host> }
Bob reçoit l'offer, crée sa propre RTCPeerConnection (non-initiator) :
Bob ──▶ Signaling : { type: "signal", to: "a1", data: <SDP answer + ICE host> }
Signaling ──▶ Alice : { type: "signal", from: "b2", data: <SDP answer + ICE host> }
Les nouveaux ICE candidates découverts après coup sont envoyés par la même route (trickle ICE).
6. Connectivity checks
WebRTC teste toutes les paires de candidates (A.host × B.host, A.srflx × B.host, etc.) avec des paquets STUN binding request. Sur même LAN, A.host ↔ B.host répond en premier → ce chemin est sélectionné.
Durée typique : 500 ms à 2 s.
7. DataChannel ouvert
peer.on('connect', () => {
peer.send(JSON.stringify({ name: "photo.jpg", size: 2458112, mime: "image/jpeg", id: "f1" }));
streamFileInChunks(file);
});
Le transfert se fait directement via le Wi-Fi local — débit proche du lien physique (WiFi 6 : 500 Mo/s possibles).
8. Fin
Chunk final { eof: true, id: "f1" }, les deux pairs ferment la RTCPeerConnection ou la gardent ouverte pour un prochain envoi.
Scénario 2 : Alice partage un lien public à Charlie (autre réseau)
1. Alice génère un code
Alice clique sur "Partager publiquement". Son client demande au serveur :
Alice ──▶ Signaling : { type: "create-public-room" }
Signaling ──▶ Alice : { type: "public-room-created", code: "x7k", url: "https://anydrop.arthurbarre.fr/x7k" }
L'UI affiche le QR code + l'URL + le code en gros.
2. Charlie ouvre l'URL
Charlie sur son téléphone scanne le QR → son navigateur ouvre /x7k. Son client se connecte au signaling avec l'intention de rejoindre la room x7k :
Charlie ──▶ Signaling : { type: "hello", joinCode: "x7k" }
Signaling ──▶ Charlie : { type: "welcome", peerId: "c3", ... }
Signaling ──▶ Alice : { type: "peer-joined", peerId: "c3", displayName: "Ours Vert" }
Signaling ──▶ Charlie : { type: "peer-joined", peerId: "a1", displayName: "Renard Bleu" }
3. Négociation WebRTC (comme au scénario 1)
Sauf que maintenant les candidates host (IPs locales) ne se joindront jamais (réseaux différents). Ce sont les candidates srflx (IP publique via STUN) qui vont s'apparier :
- NAT d'Alice fait un "hole punching" via STUN
- NAT de Charlie pareil
- Les paquets UDP se croisent et établissent le tunnel
Ça fonctionne dans ~80% des cas avec juste STUN. Sinon → fallback TURN (relay).
4. Transfert
Même logique de chunks que scénario 1, mais débit limité par la bande passante internet (typiquement 10-100 Mo/s selon connexions).
Pourquoi le P2P parfois échoue (→ TURN)
- NAT symétrique (certaines box 4G, firewalls d'entreprise) : le port change à chaque destination → STUN impuissant
- Firewall corporate bloquant UDP sortant
- Dans ces cas, TURN relaie le trafic via un serveur intermédiaire (coût bande passante réel)
On détecte l'échec de connectivité via iceConnectionState === "failed" et on peut afficher un message à l'utilisateur ou retenter avec TURN.
Sécurité
- DTLS obligatoire : WebRTC refuse d'ouvrir un DataChannel non chiffré
- Les clés DTLS sont négociées dans le SDP, échangées via le signaling
- Un attaquant sur le signaling pourrait théoriquement faire du MITM en modifiant les empreintes DTLS dans le SDP → pour une V2, on peut ajouter une vérification out-of-band (ex : afficher une "safety number" à comparer)
- Pour la V1, on s'appuie sur HTTPS/WSS pour sécuriser le signaling