anydrop/docs/webrtc-flow.md
ordinarthur 9d6e4da4ae
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 8s
feat: initial commit with full deployment setup
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>
2026-04-14 10:30:45 +02:00

5.4 KiB
Raw Permalink Blame History

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
  • STUNstun.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