Summary
The ICE server configuration is STUN-only. No TURN relay is configured. Peers behind symmetric NAT (common on corporate networks) or carrier-grade NAT (common on mobile) will fail to establish a connection and no relay fallback exists.
This is a known, documented limitation. The README now discloses it. This issue tracks implementing an actual fallback.
STUN-only ICE configuration
In src/peer-mesh.ts, connectToPeer() at line 72 and handleOffer() at line 100, both peer connection creation sites use:
const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
One STUN server. No TURN servers. The iceServers configuration is hardcoded and is not overridable by the caller.
Protocol-level failure mode
STUN resolves public IP/port pairs (server-reflexive candidates). For peers behind symmetric NAT, each outbound connection uses a different port mapping, so the STUN-discovered candidate is not reusable by the remote peer's inbound connection attempt. ICE cannot complete. No relay exists to carry the traffic.
This could not be tested live: the audit environment has HTTP/HTTPS-only egress; UDP is blocked, so STUN and TURN cannot run. The conclusion is protocol-level, not an observed test result.
ICE failure handling in this repo
Unlike some other repos in this project, peer-mesh.ts does register an oniceconnectionstatechange handler (line 206):
pc.oniceconnectionstatechange = () => {
if (pc.iceConnectionState === 'disconnected' || pc.iceConnectionState === 'failed') {
this.removePeer(peerId);
this.handleConnectionFailure(peerId);
}
};
removePeer emits peer_disconnected. handleConnectionFailure schedules an exponential-backoff reconnect attempt (starting at 500 ms, doubling to a maximum of 30 seconds) that calls connectToPeer again.
This is better than silently losing the peer, but the reconnect will fail for the same reason as the original attempt — the ICE configuration does not change between attempts. Against symmetric NAT, the retry loop will consume resources and eventually converge on the 30-second backoff, never succeeding. The emitted peer_disconnected event carries no reason code, so callers still cannot distinguish a NAT-traversal failure from a voluntary departure.
Additionally, the ping/timeout logic in setupPeer (line 156) will fire handleConnectionFailure again after 30 seconds of no pong — this can cause duplicate events if oniceconnectionstatechange and the ping timeout both fire for the same peer.
Impact
- Peers on symmetric NAT or CGNAT cannot communicate; they receive
peer_disconnected followed by repeated peer_disconnected events from retry-and-fail cycles.
- Callers cannot distinguish NAT traversal failure from normal departure.
- Retry attempts against symmetric NAT will never succeed without a TURN relay.
Proposed fixes
- TURN relay: Accept an optional
iceServers array in the PeerMesh constructor and merge it with the STUN server, so callers can supply TURN credentials.
- Distinguishable error reason: Emit an
ice_failed event (or include a reason field on peer_disconnected) when the ICE state is specifically 'failed', separate from 'disconnected'.
- Deduplication of failure events: Guard
handleConnectionFailure so the ping timeout and oniceconnectionstatechange paths for the same peer do not both fire (the removePeer guard partially handles this but the reconnect callback re-adds the peer before the ping timeout clears).
Summary
The ICE server configuration is STUN-only. No TURN relay is configured. Peers behind symmetric NAT (common on corporate networks) or carrier-grade NAT (common on mobile) will fail to establish a connection and no relay fallback exists.
This is a known, documented limitation. The README now discloses it. This issue tracks implementing an actual fallback.
STUN-only ICE configuration
In
src/peer-mesh.ts,connectToPeer()at line 72 andhandleOffer()at line 100, both peer connection creation sites use:One STUN server. No TURN servers. The
iceServersconfiguration is hardcoded and is not overridable by the caller.Protocol-level failure mode
STUN resolves public IP/port pairs (server-reflexive candidates). For peers behind symmetric NAT, each outbound connection uses a different port mapping, so the STUN-discovered candidate is not reusable by the remote peer's inbound connection attempt. ICE cannot complete. No relay exists to carry the traffic.
This could not be tested live: the audit environment has HTTP/HTTPS-only egress; UDP is blocked, so STUN and TURN cannot run. The conclusion is protocol-level, not an observed test result.
ICE failure handling in this repo
Unlike some other repos in this project,
peer-mesh.tsdoes register anoniceconnectionstatechangehandler (line 206):removePeeremitspeer_disconnected.handleConnectionFailureschedules an exponential-backoff reconnect attempt (starting at 500 ms, doubling to a maximum of 30 seconds) that callsconnectToPeeragain.This is better than silently losing the peer, but the reconnect will fail for the same reason as the original attempt — the ICE configuration does not change between attempts. Against symmetric NAT, the retry loop will consume resources and eventually converge on the 30-second backoff, never succeeding. The emitted
peer_disconnectedevent carries no reason code, so callers still cannot distinguish a NAT-traversal failure from a voluntary departure.Additionally, the ping/timeout logic in
setupPeer(line 156) will firehandleConnectionFailureagain after 30 seconds of no pong — this can cause duplicate events ifoniceconnectionstatechangeand the ping timeout both fire for the same peer.Impact
peer_disconnectedfollowed by repeatedpeer_disconnectedevents from retry-and-fail cycles.Proposed fixes
iceServersarray in thePeerMeshconstructor and merge it with the STUN server, so callers can supply TURN credentials.ice_failedevent (or include areasonfield onpeer_disconnected) when the ICE state is specifically'failed', separate from'disconnected'.handleConnectionFailureso the ping timeout andoniceconnectionstatechangepaths for the same peer do not both fire (theremovePeerguard partially handles this but the reconnect callback re-adds the peer before the ping timeout clears).