Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 8s
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>
147 lines
5.4 KiB
Markdown
147 lines
5.4 KiB
Markdown
# 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
|