Signal

WebRTC SDP

Benny samples the browser's WebRTC stack capabilities via a probe offer, then hashes a canonical representation of the captured capability detail. Engine-bound.

Reviewed

Tier 2 engine

What it measures

The signal captures the browser's compiled-in WebRTC capabilities and hashes a canonical representation of that capability detail. Because the underlying capability set is compiled into the browser binary, Chrome, Firefox, and Safari each produce a different hash on the same machine, making the signal a stable, high-entropy engine discriminator.

The captured detail covers audio, video, and data-channel surfaces. Per-call-random state is normalised away before hashing so the result is stable across repeated calls on the same browser. The captured detail is intentionally high-entropy: it reflects the engine's full negotiated capability profile rather than any single property, which is what gives it discriminatory power across browser engines.

How it's collected

The collector first checks for an iframe context. If it is running inside an iframe, it settles synchronously as absent and performs no WebRTC work. Otherwise it checks that the WebRTC API is exposed on `globalThis`; if not, it settles as absent.

For normal collection, Phase A drives the WebRTC stack through a short, contained probe sequence and stashes the resulting promise on the dispatch handle. The probe is engineered so that no ICE gathering ever starts, which means no STUN/TURN packets are sent, no local network interfaces are probed, and no IP addresses are ever exposed by this signal. Every WebRTC handle the collector opens is closed on every exit path, including error paths.

Phase B awaits the handle's payload, parses it into a canonical structure with per-call-random state stripped, serialises the structure deterministically, and hashes the result. If parsing fails, the collector returns an error-tagged absent result. Because the probe is fired in Phase A alongside every other collector, its 2 to 10 ms async wait is entirely hidden by concurrent Phase B work on other signals.

typescript
// Public-facing value shape (WebRTC SDP signal)
interface WebrtcSdpValue {
  capabilitySummary: string; // short preview of the canonical capability fingerprint, for debug
}

// Reading the result
const sdp = result.signals.webrtc_sdp;
if (sdp.confidence === 'normal') {
  // sdp.hash is the stable per-engine identifier
  // sdp.value?.capabilitySummary is a debug preview, present when options.debug === true
}

Treat the field shape as the stable interface. The probe construction, the canonical parsing rules, and the extract/strip lists used to normalise the captured detail are part of the internal recipe and not part of the public contract.

Confidence rules

ConfidenceTrigger
normalProbe completed and the captured capability detail was parsed into a canonical structure
absentCollector self-detected an iframe context; sentinel: 'unsupported'
absentWebRTC API is missing or blocked on the global object; sentinel: 'unsupported'
absentThe WebRTC stack refused to construct (e.g. Brave aggressive Shields blocks construction); sentinel: 'unsupported'
absentProbe rejected at runtime; sentinel: 'threw'
absentCaptured payload could not be parsed into a canonical structure; sentinel: 'threw'
stabilizedCaller passed stabilize: ['iframe']. The iframe rule set excludes webrtc_sdp unconditionally; result tagged 'stabilized' rather than 'absent'

Why engine-bound

The WebRTC capability set is compiled into the browser binary. Chrome and Safari on the same machine produce different probe results because the engines ship different capability profiles. Placing this signal in `the hardware signal set` would shatter cross-browser stability: a visitor switching browsers on the same Mac would produce a different hardware fingerprint even though the physical device is unchanged.

The hash is stable within an engine version but shifts between major browser versions that change the underlying capability set. This is the same version-drift risk profile as `webgl_params`, and it is expected: the signal is deliberately capturing engine capabilities, not stabilising across versions. Each major engine capability change should be treated as a one-time fingerprint migration event, analogous to the canvas RGBA migration in 1.3.0.

Things worth knowing

  • No ICE gathering is ever started, so the signal performs no network I/O and never exposes a local IP address. This is a deliberate design constraint and a key difference from naive WebRTC IP-leak probes.
  • Per-call-random state is normalised away before hashing, so the canonical representation is declaration-order-independent and stable across repeated calls on the same browser.
  • The signal is tier 2, so it does not run by default. Callers must opt in with tiers: [1, 2]. The tier-2 placement reflects that the signal shifts the fingerprint on browser version upgrades and that it is blocked by aggressive privacy tools.
  • The two-stage Source pattern was adopted from day one for this signal: the async probe is fired in Phase A so its 2 to 10 ms wait is entirely hidden by concurrent Phase B work on other signals. No serialisation cost is paid for this signal beyond its own async latency.
  • Some legacy WebView environments expose only a partial WebRTC surface. The collector tolerates per-step failures non-fatally; the resulting hash will differ from a full-surface browser but is still stable across page loads in that environment.

Last reviewed 2026-06-04