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

147 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 :
```ts
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
```ts
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