Aller au contenu

NAT, STUN, TURN, ICE — la cause #1 des "pas d'audio"

Si vous ne deviez retenir qu'un chapitre de cette formation, c'est celui-ci. 80% des tickets "ça marche pas en VoIP" sont, à la racine, un problème de NAT mal traversé. Le reste, c'est juste des cas particuliers de ce chapitre.

Le problème : le NAT casse SIP

Rappel NAT

Votre routeur fait du NAT (Network Address Translation) : votre PC en LAN sur 192.168.1.50:5060 est vu de l'extérieur comme l'IP publique de votre box (ex: 81.250.196.76) sur un port différent (ex: 38117).

PC (192.168.1.50:5060)  →  Box (81.250.196.76:38117)  →  Internet

Pour HTTP/HTTPS ça ne pose aucun problème : la requête sort, la réponse revient via la même mapping. Le navigateur s'en fiche de "qui je suis", seul le serveur a besoin d'une IP routable.

Pourquoi SIP casse

SIP a un défaut de naissance : les messages contiennent l'adresse IP du client à l'intérieur du payload, pas seulement dans l'en-tête IP du paquet. Concrètement :

  • Contact: dit "renvoie-moi les futurs messages à 192.168.1.50:5060"
  • Le SDP dit "envoie-moi le RTP à 192.168.1.50:11780"

Le serveur lit ces adresses et les utilise tel-quel. Mais 192.168.1.50 n'existe pas pour lui ! Il va envoyer le RTP dans le vide → pas d'audio.

C'est ça, le problème. Tout le reste (STUN, TURN, ICE) est inventé pour le résoudre.

Solution 1 — Le réécriture côté serveur (NAT helper)

La solution la plus ancienne : Asterisk peut être configuré pour ignorer ce que dit le téléphone et utiliser l'IP source réelle du paquet IP.

Côté PJSIP, ça s'appelle :

endpoint:
  rtp_symmetric = yes
  force_rport = yes
  rewrite_contact = yes

Ces 3 options disent : "ignore ce qu'il y a dans Contact: et SDP, utilise l'IP+port d'où le paquet arrive vraiment".

Ça marche pour la signalisation (le serveur sait où renvoyer les messages SIP). Mais pour le RTP, c'est plus subtil : Asterisk doit attendre le premier paquet RTP du téléphone pour apprendre la mapping NAT, puis lui répondre sur cette même mapping. C'est rtp_symmetric.

Limite : ça marche uniquement pour le scénario téléphone derrière NAT ↔ serveur sur IP publique. Si les deux côtés sont derrière NAT (deux téléphones derrière des box différentes), aucun n'a d'IP publique, et le RTP direct entre eux est impossible.

Solution 2 — STUN

STUN (Session Traversal Utilities for NAT, RFC 5389) est le moyen pour un client de découvrir sa propre IP publique.

Le client envoie un paquet UDP à un serveur STUN public (ex: stun.l.google.com:19302), le serveur répond "voici l'IP:port d'où ton paquet est arrivé chez moi" (= l'IP publique du NAT + le port mappé).

Le client peut alors mettre cette IP:port dans son SDP au lieu de 192.168.1.50:11780.

Téléphone A
  ──── STUN binding request ────► Serveur STUN
  ◄──── reflexive address ─────  (= IP publique de la box A)

→ Téléphone met cette IP:port dans son SDP
→ Téléphone B essaye de lui parler en RTP sur cette IP:port

STUN suffit pour les NAT cone (la plupart des box résidentielles) : le NAT garde la même mapping pour toute destination. L'audio circule direct entre les deux téléphones, sans passer par un serveur média.

STUN ne marche pas pour les NAT symétriques (typiquement les NAT d'opérateur 4G, certains firewalls d'entreprise). Dans un NAT symétrique, la mapping change selon la destination → l'IP:port appris via STUN ne sert à rien pour parler à un autre peer.

Solution 3 — TURN

TURN (Traversal Using Relays around NAT, RFC 5766) est le filet de sécurité : un serveur média qui relaie le RTP entre les deux peers.

Téléphone A          TURN server         Téléphone B
  ──── RTP ─────────►   (relay)   ────── RTP ────►
  ◄──── RTP ────────   (relay)   ◄────── RTP ────

Le téléphone A envoie son RTP au TURN server, qui le forward au B (et inverse). Le serveur agit comme un proxy média.

Avantages : - Marche dans tous les scénarios NAT, y compris symétrique côté opérateur 4G. - Authentification (login/password ou tokens HMAC) — pas n'importe qui peut utiliser ton TURN.

Inconvénients : - Coûte de la bande passante au serveur TURN (chaque appel double : upload+download). - Ajoute de la latence (un hop de plus). - Si TURN tombe, plus aucun appel WebRTC ne fonctionne (côté techno du SDK Wazo, l'app était full-relay obligatoire).

Logiciel courant : coturn (open-source, ce qu'on utilisait sur wazo-prod-01).

Solution 4 — ICE — la stratégie qui combine les 3

ICE (Interactive Connectivity Establishment, RFC 8445) est l'algorithme qui orchestre tout ça. Plutôt que de choisir d'avance entre direct, STUN ou TURN, ICE essaye les 3 en parallèle et utilise celui qui marche.

Le concept de candidat

Chaque endpoint annonce une liste de candidates dans son SDP :

Type Description Coût
host "essaye-moi en direct sur mon IP locale" — marche si même LAN gratuit
srflx "essaye-moi sur mon IP publique reflexive (apprise via STUN)" gratuit
prflx peer-reflexive, découvert pendant ICE gratuit
relay "essaye-moi via mon allocation TURN" facturé en BP TURN

L'autre côté reçoit la liste et lance des connectivity checks : pour chaque paire local-distant, un STUN binding request + réponse. La première paire qui répond avec succès devient la candidate sélectionnée — c'est par cette paire que le RTP va circuler.

Stratégies ICE

  • iceTransportPolicy: 'all' : essaye tous les candidates, choisit le meilleur. Idéal en LAN ou NAT cone. Peut échouer en NAT symétrique si le serveur TURN n'est pas correctement annoncé ou si le timeout est trop court.
  • iceTransportPolicy: 'relay' : ne propose que les candidates TURN — force le passage par TURN. Plus lent, mais déterministe. C'est ce qu'on a fini par forcer dans technotrement-phone build 38 et 41 :
uaConfigOverrides: {
  iceTransportPolicy: 'relay',
  iceCheckingTimeout: 8000
}

Cas Technotrement bug audio inbound : on a observé que 'all' échouait sur l'inbound (audio caller→app KO). Hypothèse retenue : le candidate host côté Asterisk pointait vers une IP non joignable depuis le LTE du mobile, et ICE choisissait ce mauvais candidat avant de tester le relay. Forcer relay contournait le problème mais introduisait peut-être un autre bug (le coturn co-localisé avec Asterisk = "loopback peer" filtré par défaut).

NAT en environnement cloud — le piège du serveur

Tout ce qu'on a vu jusqu'ici concernait le téléphone derrière NAT. Mais quand votre serveur Asterisk est sur un VPS public cloud (IK, OVH, AWS), il a souvent une IP publique qui n'est PAS sur son interface réseau.

Exemple sur Public Cloud Infomaniak : - Interface enp3s0 : 83.228.225.119/24 - Mais pour certaines configs, le routage public passe par une Floating IP attachée → l'IP publique vue de l'extérieur peut être différente de l'IP de l'interface

Asterisk doit savoir cette IP pour la mettre dans son SDP. Sinon il met l'IP de l'interface, qui peut être correcte... ou pas selon la topologie.

Le paramètre clé :

[transport-udp]
type = transport
protocol = udp
bind = 0.0.0.0:5060
external_media_address = 83.228.225.119
external_signaling_address = 83.228.225.119
local_net = 192.168.0.0/16
local_net = 10.0.0.0/8

external_media_address = ce qu'Asterisk met dans le SDP quand il parle à un peer hors local_net. Pour un peer dans local_net, il met l'IP de l'interface (utile pour les softphones LAN).

Cas typique non vu : external_media_address mal configuré → le serveur annonce une IP non routable au mobile → le mobile envoie son RTP dans le vide → audio unidirectionnel. Symptôme : "j'entends mon correspondant mais lui ne m'entend pas".

Le piège du coturn loopback

Un piège qu'on a touché du doigt sur wazo-prod-01 :

Client (mobile) ──RTP──► coturn (83.228.228.98)
                  loopback?
                       Asterisk (83.228.228.98)

Quand coturn et Asterisk tournent sur la même machine, et que le relay TURN doit envoyer le RTP du client vers Asterisk, on a un "loopback peer" — coturn doit relayer vers une IP que lui-même héberge. Par défaut, coturn refuse les peers loopback (no-loopback-peers implicite) pour des raisons de sécurité (anti-relai abusif).

Solution propre : séparer coturn et Asterisk sur deux instances distinctes. C'est la recommandation à graver pour les futurs déploiements clients qui voudront du WebRTC.

Solution rapide : ajouter dans /etc/turnserver.conf :

allowed-peer-ip=<IP publique de l'instance>
external-ip=<IP publique de l'instance>

Et ne pas activer no-loopback-peers.

Lecture d'un trace ICE

Un SDP ICE-enabled ressemble à :

m=audio 49170 RTP/AVP 0
a=ice-ufrag:F7gI
a=ice-pwd:x9cl/YkneCftKwpFEZS37r
a=candidate:1 1 UDP 2130706431 192.168.1.50 49170 typ host
a=candidate:2 1 UDP 1694498815 81.250.196.76 38117 typ srflx raddr 192.168.1.50 rport 49170
a=candidate:3 1 UDP 16777215 turn.technotrement.com 49350 typ relay raddr 81.250.196.76 rport 38117

3 candidates : host (LAN), srflx (NAT mapping vu via STUN), relay (TURN).

Pendant les connectivity checks, on verra des STUN binding requests échangés. En pcap Wireshark, filtre stun les isole.

Récap décisionnel

Scénario Solution
Tous les téléphones en LAN avec serveur en LAN Rien (host candidates suffisent)
Téléphone IP fixe ou DECT en LAN, serveur cloud NAT helper (rewrite_contact + rtp_symmetric) côté serveur
Softphone mobile sur 4G/WiFi public, serveur cloud STUN/TURN obligatoire — préférer ICE en 'all' puis fallback 'relay'
Multi-utilisateur WebRTC TURN obligatoire, séparé du serveur média si possible

À retenir absolument

  • SIP/RTP est cassé par défaut en présence de NAT, parce que les IPs sont dans le payload, pas (que) dans l'en-tête.
  • Sur le serveur, external_media_address + local_net doivent être juste, sinon ça ne marche pas.
  • Sur le client, ICE est l'algo moderne — relay only est le filet de sécurité qui marche partout au prix d'une latence accrue.
  • Coturn et Asterisk ne doivent PAS tourner sur la même IP en prod WebRTC sérieuse.
  • Pour debugger : tcpdump RTP sur le serveur + pcap Wireshark côté client. Voir où s'arrêtent les paquets.

Suivant : Sécurité de transport