WebRTC ICE Candidates and Network Traversal

This article provides an overview of Interactive Connectivity Establishment (ICE) candidates and explains how JavaScript manages network traversal to establish peer-to-peer (P2P) connections in WebRTC. It covers the challenges posed by NATs and firewalls, the distinct types of ICE candidates, the roles of STUN and TURN servers, and the step-by-step JavaScript implementation required to gather, signal, and negotiate connection paths between remote browsers.

The Network Traversal Problem in WebRTC

WebRTC enables real-time audio, video, and data communication directly between browsers. However, most consumer devices do not have a public, static IP address. Instead, they reside behind Network Address Translation (NAT) devices and firewalls. NAT assigns private IP addresses (such as 192.168.x.x or 10.x.x.x) to local devices and translates them to a single public IP address when communicating with the internet.

Because two devices behind different NATs cannot directly see each other’s private IP addresses, direct communication fails without an intermediary mechanism to discover public routing paths and bypass firewall restrictions.

What Are ICE Candidates?

An ICE (Interactive Connectivity Establishment) candidate is a network configuration profile that describes a potential method and path for communicating with a peer. Each candidate contains vital networking information, including:

During the connection phase, WebRTC gathers multiple candidates from both peers to test all possible combinations and select the most efficient, viable path.

Types of ICE Candidates

  1. Host Candidates: Represent the local IP address and port directly assigned to the device’s network interfaces (Wi-Fi or Ethernet). These work only if both peers are on the same local network.
  2. Server Reflexive (srflx) Candidates: Represent the public IP address and port allocated to the device by its NAT. These are discovered using a STUN (Session Traversal Utilities for NAT) server.
  3. Relay Candidates: Represent an allocated address on an external TURN (Traversal Using Relays around NAT) server. If symmetric NATs or strict firewalls block direct communication, media and data packets are relayed through this third-party server.
  4. Peer Reflexive (prflx) Candidates: Discovered dynamically during ICE connectivity checks when an incoming packet arrives from an address different from known host or reflexive candidates.

How JavaScript Handles ICE in WebRTC

JavaScript utilizes the RTCPeerConnection API to automate network traversal. The browser handles the low-level discovery and connectivity testing, while JavaScript coordinates signaling via the application’s signaling server (typically implemented with WebSockets).

1. Configuring ICE Servers

When instantiating the RTCPeerConnection, JavaScript provides an array of STUN and TURN server URLs.

const configuration = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: 'turn:turn.example.com:3478',
      username: 'user',
      credential: 'password'
    }
  ]
};

const peerConnection = new RTCPeerConnection(configuration);

2. Gathering ICE Candidates

Once local session descriptions (Offer/Answer) are set via setLocalDescription(), the browser begins the ICE gathering process automatically. As it discovers potential endpoints, it triggers the icecandidate event on the RTCPeerConnection instance.

peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    // Send the candidate to the remote peer via your signaling server
    signalingServer.send(JSON.stringify({
      type: 'candidate',
      candidate: event.candidate
    }));
  } else {
    // All ICE candidates have been gathered
    console.log('ICE gathering complete');
  }
};

3. Signaling Candidates

WebSockets, HTTP requests, or any message-passing layer transmit the serialized ICE candidates to the remote peer. This process is known as Trickle ICE, where candidates are transmitted incrementally as they are found rather than waiting for all candidates to be collected, drastically reducing connection setup time.

4. Adding Remote Candidates

When the remote peer receives candidate data from the signaling channel, it registers the candidate using the addIceCandidate() method.

signalingServer.onmessage = async (message) => {
  const data = JSON.parse(message.data);

  if (data.type === 'candidate') {
    try {
      await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
    } catch (error) {
      console.error('Error adding received ICE candidate', error);
    }
  }
};

5. Connectivity Checks and Path Selection

Under the hood, WebRTC pairs each local candidate with each remote candidate to form candidate pairs. It systematically tests each pair with STUN binding requests. The browser selects the highest-priority pair that successfully establishes two-way communication (preferring Host, then Server Reflexive, and falling back to Relay), transitioning the connection state to connected.