How do you get around NATs using WebRTC without a TURN server?
Asked Answered
W

2

8

I'm trying to make a peer to peer Javascript game that can be played on mobile browsers.

  • I have been able to successfully set up a p2p connection between two phones within my local WiFi network.
  • I am unable to connect two phones over mobile networks or one on WiFi and one on a mobile network.
  • I tried turning off my Windows firewall and could not connect my PC to my phone on a mobile network.
  • I tried having both peers set up their own data channels and set negotiated.

I've read that 80% to 90% of devices are able to connect over WebRTC without TURN servers so I am at a complete loss of what to do next.

Desktop: Google Chrome 79.0.3945.130 (Official Build) (64-bit) (cohort: Stable)

Mobile (Pixel 3/Android 10): Google Chrome 79.0.3945.116

MOBILE NETWORK

Time    Event
1/24/2020, 11:58:17 PM  createLocalDataChannel
label: Test, reliable: true
1/24/2020, 11:58:17 PM  negotiationneeded
1/24/2020, 11:58:17 PM  createOffer
1/24/2020, 11:58:17 PM  createOfferOnSuccess
1/24/2020, 11:58:17 PM  setLocalDescription
1/24/2020, 11:58:17 PM  signalingstatechange
1/24/2020, 11:58:17 PM  setLocalDescriptionOnSuccess
1/24/2020, 11:58:17 PM  icegatheringstatechange
1/24/2020, 11:58:17 PM  icecandidate (host)
1/24/2020, 11:58:17 PM  icecandidate (srflx)
1/24/2020, 11:58:17 PM  setRemoteDescription
1/24/2020, 11:58:17 PM  addIceCandidate (host)
1/24/2020, 11:58:17 PM  signalingstatechange
1/24/2020, 11:58:17 PM  setRemoteDescriptionOnSuccess
1/24/2020, 11:58:17 PM  iceconnectionstatechange
1/24/2020, 11:58:17 PM  iceconnectionstatechange (legacy)
1/24/2020, 11:58:17 PM  connectionstatechange
1/24/2020, 11:58:18 PM  addIceCandidate (srflx)
1/24/2020, 11:58:33 PM  iceconnectionstatechange
disconnected
1/24/2020, 11:58:33 PM  iceconnectionstatechange (legacy)
failed
1/24/2020, 11:58:33 PM  connectionstatechange
failed

WIFI NETWORK

Time    Event
1/25/2020, 12:02:45 AM  
createLocalDataChannel
label: Test, reliable: true
1/25/2020, 12:02:45 AM  negotiationneeded
1/25/2020, 12:02:45 AM  createOffer
1/25/2020, 12:02:45 AM  createOfferOnSuccess
1/25/2020, 12:02:45 AM  setLocalDescription
1/25/2020, 12:02:45 AM  signalingstatechange
1/25/2020, 12:02:45 AM  setLocalDescriptionOnSuccess
1/25/2020, 12:02:45 AM  icegatheringstatechange
1/25/2020, 12:02:45 AM  icecandidate (host)
1/25/2020, 12:02:45 AM  icecandidate (srflx)
1/25/2020, 12:02:46 AM  setRemoteDescription
1/25/2020, 12:02:46 AM  signalingstatechange
1/25/2020, 12:02:46 AM  setRemoteDescriptionOnSuccess
1/25/2020, 12:02:46 AM  icegatheringstatechange
1/25/2020, 12:02:46 AM  addIceCandidate (host)
1/25/2020, 12:02:46 AM  iceconnectionstatechange
1/25/2020, 12:02:46 AM  iceconnectionstatechange (legacy)
1/25/2020, 12:02:46 AM  connectionstatechange
1/25/2020, 12:02:46 AM  addIceCandidate (srflx)
1/25/2020, 12:02:46 AM  iceconnectionstatechange
connected
1/25/2020, 12:02:46 AM  iceconnectionstatechange (legacy)
connected
1/25/2020, 12:02:46 AM  connectionstatechange
connected
1/25/2020, 12:02:46 AM  iceconnectionstatechange (legacy)
completed

Peer to peer code

"use strict";

import { isAssetLoadingComplete } from '/game/assetManager.js';
import { playerInputHandler } from '/game/game.js';

const rtcPeerConnectionConfiguration = {
    // Server for negotiating traversing NATs when establishing peer-to-peer communication sessions
    iceServers: [{
        urls: [
            'stun:stun.l.google.com:19302'
        ]
    }]
};

let rtcPeerConn;
// For UDP semantics, set maxRetransmits to 0 and ordered to false
const dataChannelOptions = {
    // TODO: Set this to a unique number returned from joinRoomResponse
    //id: 1,
    // json for JSON and raw for binary
    protocol: "json",
    // If true both peers can call createDataChannel as long as they use the same ID
    negotiated: false,
    // TODO: Set to false so the messages are faster and less reliable
    ordered: true,
    // If maxRetransmits and maxPacketLifeTime aren't set then reliable mode will be on
    // TODO: Send multiple frames of player input every frame to avoid late/missing frames
    //maxRetransmits: 0,
    // The maximum number of milliseconds that attempts to transfer a message may take in unreliable mode.
    //maxPacketLifeTime: 30000
};

let dataChannel;

export let isConnectedToPeers = false;

export function createDataChannel(roomName, socket) {
    rtcPeerConn = new RTCPeerConnection(rtcPeerConnectionConfiguration);
    // Send any ice candidates to the other peer
    rtcPeerConn.onicecandidate = onIceCandidate(socket);
    // Let the 'negotiationneeded' event trigger offer generation
    rtcPeerConn.onnegotiationneeded = function () {
        console.log("Creating an offer")
        rtcPeerConn.createOffer(sendLocalDesc(socket), logError('createOffer'));
    };
    console.log("Creating a data channel");
    dataChannel = rtcPeerConn.createDataChannel(roomName, dataChannelOptions);
    dataChannel.onopen = dataChannelStateOpen;
    dataChannel.onmessage = receiveDataChannelMessage;
    dataChannel.onerror = logError('createAnswer');
    dataChannel.onclose = function(TODO) {
        console.log(`Data channel closed for scoket: ${socket}`, TODO)
    };
}

export function joinDataChannel(socket) {
    console.log("Joining a data channel");
    rtcPeerConn = new RTCPeerConnection(rtcPeerConnectionConfiguration);
    rtcPeerConn.ondatachannel = receiveDataChannel;
    // Send any ice candidates to the other peer
    rtcPeerConn.onicecandidate = onIceCandidate(socket);
}

function receiveDataChannel(rtcDataChannelEvent) {
    console.log("Receiving a data channel", rtcDataChannelEvent);
    dataChannel = rtcDataChannelEvent.channel;
    dataChannel.onopen = dataChannelStateOpen;
    dataChannel.onmessage = receiveDataChannelMessage;
    dataChannel.onerror = logError('createAnswer');
    dataChannel.onclose = function(TODO) {
        console.log(`Data channel closed for scoket: ${socket}`, TODO)
    };
}

function onIceCandidate(socket) {
    return function (event) {
        if (event.candidate) {
            console.log("Sending ice candidates to peer.");
            socket.emit('signalRequest', {
                signal: event.candidate
            });
        }
    }
}

function dataChannelStateOpen(event) {
    console.log("Data channel opened", event);
    isConnectedToPeers = true;

    if(!isAssetLoadingComplete) {
        document.getElementById("startGameButton").textContent = "Loading...";
    }
    else {
        document.getElementById('startGameButton').removeAttribute('disabled');
        document.getElementById("startGameButton").textContent = "Start Game";
    }
}

function receiveDataChannelMessage(messageEvent) {
    switch(dataChannel.protocol) {
        case "json":
            const data = JSON.parse(messageEvent.data)
            playerInputHandler(data);
            break;
        case "raw":
            break;
      }
}

export function signalHandler(socket) {
    return function (signal) {
        if (signal.sdp) {
            console.log("Setting remote description", signal);
            rtcPeerConn.setRemoteDescription(
                signal,
                function () {
                    // If we received an offer, we need to answer
                    if (rtcPeerConn.remoteDescription.type === 'offer') {
                        console.log("Offer received, sending answer")
                        rtcPeerConn.createAnswer(sendLocalDesc(socket), logError('createAnswer'));
                    }
                },
                logError('setRemoteDescription'));
        }
        else if (signal.candidate){
            console.log("Adding ice candidate ", signal)
            rtcPeerConn.addIceCandidate(new RTCIceCandidate(signal));
        }
    }
}

function sendLocalDesc(socket) {
    return function(description) {
        rtcPeerConn.setLocalDescription(
            description,
            function () {
                console.log("Setting local description", description);
                socket.emit('signalRequest', {
                    playerNumber: socket.id,
                    signal: description
                });
            },
            logError('setLocalDescription'));
    };
}

export function sendPlayerInput(playerInput){
    dataChannel.send(JSON.stringify(playerInput));
}

function logError(caller) {
    return function(error) {
        console.log('[' + caller + '] [' + error.name + '] ' + error.message);
    }
}

Wattmeter answered 25/1, 2020 at 5:9 Comment(0)
H
3

There are a few different factors that could at at play here.

  • NAT Types on both sides
  • IP Family (IPv4 or IPv6)
  • Protocols (Is UDP Allowed at all?)

I would determine what NAT type you are behind on each side, you can read more about that here https://webrtchacks.com/symmetric-nat. If both networks are behind symmetric NATs you are going to need a TURN server.

If you don't have a browser you could also use Pion TURN a Go TURN client and server.

I would also check when gathering candidates if there is an intersection on IPv4/IPv6. Some phone providers are only giving out IPv6.

UDP may not be allowed at all. This isn't common, but possible. In this case you will be forced to use TURN. NAT traversal via TCP is possible, but not supported by WebRTC AFAIK.

Hypophosphate answered 25/1, 2020 at 5:22 Comment(5)
Thanks for the tips I'll look into these. By intersection do you mean one peer using IPv6 and one using IPv4?Wattmeter
Yes exactly! People will use TURN servers as a bridge for IPv4 <-> IPv6Hypophosphate
I checked the ICE candidates and it shows my phones IPv4 IP address on the mobile network and my computers IPv4 IP address over LAN.Wattmeter
My phone is behind a symmetric NAT and my desktop computer is not. Doesn't that mean they should be able to connect with STUN?Wattmeter
I tried out test.webrtc.org and both devices passed data throughput but had this message for connectivity: [ WARN ] Could not connect using reflexive candidates, likely due to the network environment/configuration.Wattmeter
D
6

A TURN server is a solution to the problem. If there were workarounds that didn't require it, nobody would be using it. A common misconception here is that if you add a TURN server to the system, it will relay all traffic. That isn't true, it is only used as a fallback for the connections that can not be established otherwise. In comparison to the alternative of routing all your game messages through a websocket server this will still save you 80%+ of the traffic.

The next step is to install a TURN server. coturn is widely used and reasonably well documented. It is stable enough that, once setup, the amount of maintenance required is very low.

Deaconess answered 25/1, 2020 at 6:51 Comment(2)
I was looking at hosting coturn or paying Twilio but I want to confirm that what I'm doing works without TURN for even one client.Wattmeter
I ended up setting up a coturn server.Wattmeter
H
3

There are a few different factors that could at at play here.

  • NAT Types on both sides
  • IP Family (IPv4 or IPv6)
  • Protocols (Is UDP Allowed at all?)

I would determine what NAT type you are behind on each side, you can read more about that here https://webrtchacks.com/symmetric-nat. If both networks are behind symmetric NATs you are going to need a TURN server.

If you don't have a browser you could also use Pion TURN a Go TURN client and server.

I would also check when gathering candidates if there is an intersection on IPv4/IPv6. Some phone providers are only giving out IPv6.

UDP may not be allowed at all. This isn't common, but possible. In this case you will be forced to use TURN. NAT traversal via TCP is possible, but not supported by WebRTC AFAIK.

Hypophosphate answered 25/1, 2020 at 5:22 Comment(5)
Thanks for the tips I'll look into these. By intersection do you mean one peer using IPv6 and one using IPv4?Wattmeter
Yes exactly! People will use TURN servers as a bridge for IPv4 <-> IPv6Hypophosphate
I checked the ICE candidates and it shows my phones IPv4 IP address on the mobile network and my computers IPv4 IP address over LAN.Wattmeter
My phone is behind a symmetric NAT and my desktop computer is not. Doesn't that mean they should be able to connect with STUN?Wattmeter
I tried out test.webrtc.org and both devices passed data throughput but had this message for connectivity: [ WARN ] Could not connect using reflexive candidates, likely due to the network environment/configuration.Wattmeter

© 2022 - 2024 — McMap. All rights reserved.