Du peer-to-peer à travers un NAT, un proxy ou un firewall grâce à WebRTC, STUN et TURN

Comment utiliser le WebRTC et les services STUN et TURN pour des échanges P2P ?

1 mai 2018

Introduction

La plupart des tutoriels sur le WebRTC et leurs exemples concernent la communication entre deux navigateurs sur le même poste.

Cela fonctionne très bien jusqu'au cas concret où l'on souhaite faire communiquer deux machines distantes situées derrière un proxy ou un firewall.

Pour que la communication WebRTC fonctionne dans la majorité des cas, il est nécessaire de mettre en place trois services logiciels :

Ils peuvent être situés sur le même serveur physique ou dispatchés sur des serveurs différents.

Nous avons mis en place ces services dans le cadre de notre solution SaaS CHADEO qui permet de discuter instantanément et gratuitement par webcam.

Scénario simplifié utilisé par WebRTC

Supposons que Bob veut communiquer avec Patrick par Internet avec son appareil (ordinateur, téléphone portable, ...)

Bob n'est pas visible de Patrick et vice versa car ils sont généralement cachés derrière un NAT et/ou un firewall, par exemple une box Internet.


1


Les serveurs Signal Channel, STUN et TURN ont une IP public sur Internet et sont donc directement accessibles par Bob et Patrick.


2


Les navigateurs de Bob et de Patrick téléchargent une page HTML depuis une URL public d'Internet contenant le code en JavaScript et les adresses IP/Port pour se connecter aux différents services Signal Channel, STUN et TURN. Dans notre schéma, les fichiers HTML et JS concernés sont hébergés sur le même serveur que le service Signal Channel.


3


Les navigateurs de Patrick et de Bob interrogent le serveur STUN, qui informe chacun, de son adresse IP public et des ports accessibles depuis Internet.


4


Le navigateur de Bob crée un objet appelé Session Description Protocol (SDP) qu'il communique à Patrick.

Cet objet contient notamment :

Bob envoie le SDP au navigateur de Patrick via le service Signal Channel. Patrick génère aussi son SDP et le communique à Bob via également le service Signal Channel.


5


Les navigateurs de Bob et Patrick négocient afin de communiquer directement ensemble.


6


S'il s'avère qu'il n'est pas possible de communiquer directement, Bob et Patrick utilisent le serveur relais appelé TURN.


7


Le service Signal Channel

Ce service très simple permet d'échanger des messages de faible taille entre deux clients via un serveur relais.

Ces messages contiennent les configurations de chacun (IP public, protocole, port, résolution, vidéo, son, ...) obtenues grâce au serveur STUN et aux informations fournies par le système d'exploitation.

Le moyen pour échanger ces messages est laissé libre, par exemple des messages JSON à l'aide de WebSockets ou de XMLHTTPRequest.

Exemple en JavaScript

Côté serveur

this.ws = require('socket.io')(this.server);
this.ws.on("connection", (client) => {
    client.on('webrtc', (message) => {
        client.to(message.to_socket_id).emit('webrtc', { from_socket_id: client.id, webrtc: message.webrtc });
    });
});

Côté client

this.socket.emit('webrtc', {
     to_socket_id: socket_id, webrtc: {
         type: 'candidate',
         label: event.candidate.sdpMLineIndex,
         id: event.candidate.sdpMid,
         candidate: event.candidate.candidate
     }
 });

Le service STUN (Simple Traversal of Udp through Nats)

Ce service permet à deux clients de savoir s'il leur est possible de communiquer directement entre eux en pair-à-pair, c'est-à-dire sans passer par un serveur relais.

Un navigateur interroge le serveur STUN pour :

Pour effectuer tous ces tests, le serveur hébergeant le service STUN doit posséder deux adresses IP public sur Internet.

Il existe des serveurs STUN en SaaS gratuits ou payants mais les messages échangés contiennent des informations sensibles comme la configuration et l'IP interne qui permettent de vous identifier précisément même dans un réseau d'entreprise.

Exemple en JavaScript

Utilisons Puppeteer (Chromium en mode headless) pour tester la connexion à un serveur STUN de Google et afficher les informations échangées.

const puppeteer = require('puppeteer');
const path = require('path');
const webrtc_adapter_directory = path.dirname(require.resolve('webrtc-adapter'));

const peerConnectionConfig = {
  'iceServers': [
    {
      'urls': ['stun:stun.l.google.com:19302']
    }
  ]
};

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.addScriptTag({ path: `${webrtc_adapter_directory}/../../out/adapter.js` });

  page.on('console', msg => console.log('PAGE LOG:', msg.text()));
  const result = await page.evaluate(config => {
    return new Promise((resolve, reject) => {

      var connection = new RTCPeerConnection(config);

      connection.onicecandidate = function (event) {
        let icecandidate = event.candidate;
        switch (icecandidate.type) {
          case 'host':
                console.log('Host candidate');
                break;
          case 'srflx':
                console.log('Server reflexive');
                break;
          case 'prflx':
                console.log('Peer reflexive');
                break;
          case 'relay':
                console.log('Relay reflexive');
                break;
          default:
                console.log('Unknown candidate type : ' + icecandidate.type);
        }
        console.log('\tIP : ' + icecandidate.ip + ' Derivated :' + icecandidate.relatedAddress);
        console.log('\tPort : ' + icecandidate.port + ' Derivated : ' + icecandidate.relatedPort);
        console.log('\tProtocol : ' + icecandidate.protocol);
      };
      connection.createDataChannel("");
      connection.createOffer((sessionDescription) => {
        connection.setLocalDescription(sessionDescription);
      }, (event) => {
        console.log('createOffer() error: ' + JSON.stringify(event));
        reject(false);
      });
    })
  }, peerConnectionConfig);
  await browser.close();
})();

Ce code affichera par exemple :

PAGE LOG: Host candidate
PAGE LOG:       IP : 192.168.0.12 Derivated :undefined
PAGE LOG:       Port : 53749 Derivated : undefined
PAGE LOG:       Protocol : udp
PAGE LOG: Server reflexive
PAGE LOG:       IP : 77.150.14.128 Derivated :192.168.0.12
PAGE LOG:       Port : 53749 Derivated : 53749
PAGE LOG:       Protocol : udp

Ces informations indiquent que le poste est joignable de deux manières différentes :

Le service TURN (Traversal Using Relays around Nat)

Le serveur TURN paramétré est automatiquement utilisé par le navigateur si la communication vidéo/son pair-à-pair est impossible.

Il fonctionne comme un serveur relais c'est-à-dire que toutes les données transitent par ce serveur pour être renvoyées au destinataire concerné.

Il est aussi conseillé de l'utiliser avec plus de quatre participants simultanés car les connexions internet domestiques ont souvent un débit montant (upload) insuffisant pour plus de quatre flux vidéos simultanés.

De même que pour le service STUN, il existe des serveurs en mode SaaS facturés au débit utilisé.

Le code open source https://github.com/coturn/coturn permet de mettre en place un serveur privé avec les services TURN et STUN inclus.

Exemple

En ajoutant la configuration du serveur TURN dans l'objet peerConnectionConfig du code source ci-dessus :

const puppeteer = require('puppeteer');
const path = require('path');
const webrtc_adapter_directory = path.dirname(require.resolve('webrtc-adapter'));

const peerConnectionConfig = {
  'iceServers': [
    {
      'urls': ['stun:stun.l.google.com:19302']
    },
    {
        'urls': 'turn:163.25.135.102:3478',
        username: "chadeo",
        credential: "AEYUUk2NtwRPaufu45Gn"
    }
  ]
};

...

Le code affichera une information supplémentaire, par exemple :

PAGE LOG: Host candidate
PAGE LOG:       IP : 192.168.0.12 Derivated :undefined
PAGE LOG:       Port : 55581 Derivated : undefined
PAGE LOG:       Protocol : udp
PAGE LOG: Server reflexive
PAGE LOG:       IP : 77.150.14.128 Derivated :192.168.0.12
PAGE LOG:       Port : 55581 Derivated : 55581
PAGE LOG:       Protocol : udp
PAGE LOG: Relay reflexive
PAGE LOG:       IP : 163.25.135.102 Derivated :77.150.14.128
PAGE LOG:       Port : 57003 Derivated : 55581
PAGE LOG:       Protocol : udp

Le poste avec l'IP public est 77.150.14.128 devient maintenant joignable via le serveur relais TURN avec l'IP public 163.25.135.102.

Démonstration

Application chat et vidéo permettant de discuter instantanément avec les proches ou les collaborateurs