#774·camoufox

ICE gathering never reaches "complete" with webrtc:ipv4/ipv6 set — MaybeFabricateSrflxCandidate never fires (tested with a real UDP-relaying SOCKS5 proxy)

Author: luisrosasxCreated Sep 13, 2026Updated Sep 14, 2026

Camoufox version: v152.0.4-beta.30 (vendored release, linux x86_64) OS: Ubuntu-based Linux, kernel 7.0.0-30-generic, x86_64 (native, not a VM/container — confirmed via uname -a before this investigation, per project policy of not diagnosing on an unconfirmed architecture) Python version: N/A — this report drives the vendored binary directly via CAMOU_CONFIG (env var) + WebDriver BiDi, the same "global mode" that build-tester exercises, not through the camoufox Python package. Happy to re-verify against service-tester/AsyncNewContext if that's useful, but the phenomenon described here is at the ICE/SDP layer, upstream of anything the Python package controls.

Summary

Per patches/webrtc-ip-spoofing.patch, MaybeFabricateSrflxCandidate() is documented to run once ICE gathering reaches complete (or a terminal connection state) without having seen a real public candidate first, and ShouldSpoofCandidateIP() is a pure config check on webrtc:ipv4/ webrtc:ipv6 being present. We set those two CAMOU_CONFIG keys, open a real RTCPeerConnection with no ICE/STUN/TURN servers configured (deliberately — matching how a typical antidetect/privacy consumer of Camoufox would configure it, and how media.peerconnection. ice.proxy_only_if_behind_proxy is meant to be exercised), and observe icegatheringstatechange and iceconnectionstatechange for up to 60s per case, capturing raw SDP (createOffer() and the final local description) at the end.

In all 7 configurations we tried — with or without a proxy, and across three different SOCKS5/ HTTP proxy behaviors — ICE gathering state goes from new to gathering and then simply never changes again. No complete event ever fires, iceconnectionstatechange never fires either (the array of observed connection states is empty in every case), and the offer/final SDP only ever contain the two local mDNS-obfuscated host candidates (typ host, one UDP one TCP) that Firefox produces natively — never a srflx candidate, never the IP we configured.

Since gathering never reaches a terminal state, MaybeFabricateSrflxCandidate() — by its own documented trigger condition — has no opportunity to run. This looks less like "the proxy detection heuristic picked the wrong branch" and more like ICE gathering itself getting stuck before it can ever reach the state that would trigger the spoofing path, in this specific environment (no STUN/ TURN servers configured, media.peerconnection.ice.proxy_only_if_behind_proxy at its Camoufox default already true, no TURN/STUN reachable to complete host-candidate-only gathering).

The 7 scenarios (all captured with raw SDP + ICE gathering/connection state timelines)

We built our own SOCKS5/HTTP test-proxy harness (not a mock/stub — a real relay, verified against 2 independent external SOCKS5 client implementations before trusting any result against Camoufox) to isolate exactly which piece breaks:

# Case Proxy behavior Gathering states observed Connection states observed srflx with our configured IP?
1 Disabled (not applicable — WebRTC off via media.peerconnection.enabled=false)
2 Forward reaches expected states (real candidate reported) N/A (mode doesn't ask for spoofing)
3 Replace, no proxy new → gathering only none No
4 Replace, ipv4+ipv6 both set, no proxy new → gathering only none No
5 Replace, real SOCKS5 proxy, HTTP/HTTPS traffic confirmed routed through it (DNS-canary fetch to a name with no real DNS record succeeds only via the proxy) proxy wiring for normal traffic works new → gathering only none No
6 Replace, SOCKS5 proxy that rejects UDP ASSOCIATE honestly (REP=0x07) rejects UDP at the protocol level new → gathering only none No
7 Replace, SOCKS5 proxy that accepts UDP ASSOCIATE and genuinely relays UDP datagrams bidirectionally (own ephemeral relay socket, real BND.ADDR/BND.PORT, verified with an external SOCKS5 reference client before trusting it) full real UDP relay new → gathering only, t≈41s none No
8 Same as #7 but the proxy accepts UDP ASSOCIATE and then silently drops every datagram (control) accepts, never forwards new → gathering only, t≈40s none No
9 HTTP CONNECT-only proxy (structurally incapable of UDP at the protocol level, control) no UDP path at all new → gathering only, t≈36s none No

(Numbered 1–9 above for clarity of what we ran; the 7 that specifically exercise the Replace+ fabrication path are #3, #4, and #6–9 — #1/#2 are included as controls confirming the harness itself is sound, since Disabled/Forward behave exactly as documented.)

Critically: case #7 is the one meant to activate MaybeFabricateSrflxCandidate — a real SOCKS5 proxy with genuine, bidirectionally-verified UDP ASSOCIATE support, exactly the condition media.peerconnection.ice.proxy_only_if_behind_proxy is documented to gate on. It behaves identically to the proxy that honestly rejects UDP (#6) and to no proxy at all (#3/#4): gathering gets stuck at gathering, never reaches complete, and the fabricated candidate never appears.

Raw SDP — case #7 (Replace, real UDP-relaying SOCKS5 proxy)

offer_sdp:
v=0
o=mozilla...THIS_IS_SDPARTA-99.0 5142515311993443112 0 IN IP4 0.0.0.0
s=-
t=0 0
a=fingerprint:sha-256 5B:DF:3C:78:9B:30:0D:CB:BB:F1:8A:35:F2:54:9B:31:29:3E:4F:05:B3:A8:EE:37:57:E6:89:7D:79:4D:CD:32
a=group:BUNDLE 0
a=ice-options:trickle
a=msid-semantic:WMS *
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=sendrecv
a=ice-pwd:65f2b6876ce0bc827ead4641466d1a4b
a=ice-ufrag:d6404a23
a=mid:0
a=setup:actpass
a=sctp-port:5000
a=max-message-size:1073741823

final_sdp (after gathering stalls at "gathering", never reaches "complete"):
v=0
o=mozilla...THIS_IS_SDPARTA-99.0 5142515311993443112 0 IN IP4 0.0.0.0
s=-
t=0 0
a=sendrecv
a=fingerprint:sha-256 5B:DF:3C:78:9B:30:0D:CB:BB:F1:8A:35:F2:54:9B:31:29:3E:4F:05:B3:A8:EE:37:57:E6:89:7D:79:4D:CD:32
a=group:BUNDLE 0
a=ice-options:trickle
a=msid-semantic:WMS *
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=candidate:0 1 UDP 2122252543 0330e8c1-e151-4434-beb6-1eb9c7bb17ed.local 49936 typ host
a=candidate:2 1 TCP 2105524479 0330e8c1-e151-4434-beb6-1eb9c7bb17ed.local 9 typ host tcptype active
a=sendrecv
a=ice-pwd:65f2b6876ce0bc827ead4641466d1a4b
a=ice-ufrag:d6404a23
a=mid:0
a=setup:actpass
a=sctp-port:5000
a=max-message-size:1073741823

gathering_states = ["gathering" @ t≈41s], connection_states = []. webrtc:ipv4 was set to 203.0.113.77 in CAMOU_CONFIG for this run — it appears nowhere in either SDP, and no srflx candidate line appears at all. Cases #6, #8, #9 produce byte-for-byte structurally identical SDP (only the random ice-ufrag/ice-pwd/mDNS UUID differ) — attaching one representative capture here rather than 4 near-duplicates; happy to attach the other 3 raw captures on request.

Steps to reproduce (framework-agnostic — this harness is internal to our project, but the

repro does not depend on anything project-specific)

  1. Launch the vendored Camoufox binary with CAMOU_CONFIG containing at least webrtc:ipv4 set to any literal IPv4 (e.g. 203.0.113.77), and connect over WebDriver BiDi (or any other Gecko remote-control channel — nothing here depends on BiDi specifically).
  2. Optionally wire network.proxy.type=1 + network.proxy.socks/network.proxy.socks_port + network.proxy.socks_version=5 to point at a real SOCKS5 proxy that implements a genuine bidirectional UDP ASSOCIATE relay (RFC 1928 §4/§7) — confirm independently (e.g. with a reference client like PySocks) that the relay actually works before trusting any Camoufox-side result.
  3. Navigate to a page that opens new RTCPeerConnection({}) (no iceServers configured — this matters, see below) and calls createDataChannel + createOffer/setLocalDescription.
  4. Listen to pc.onicegatheringstatechange and pc.oniceconnectionstatechange, log every state with a timestamp, and wait up to 60s.
  5. Observe: gathering state moves new → gathering and then never changes again in any of the configurations above (with proxy, without proxy, with a proxy that has UDP, without one, with a proxy that fakes UDP support).

The central question

Given MaybeFabricateSrflxCandidate()'s documented precondition (ICE gathering reaching complete, or a terminal connection state, without a public candidate seen first) — why does ICE gathering never reach complete in this environment at all, regardless of proxy behavior? Is a STUN/TURN server required to ever be configured for gathering to reach a terminal state in the first place (i.e., is "no ICE servers configured at all" — which is exactly how the webrtc:ipv4 spoofing feature is meant to be usable without exposing a fingerprintable STUN dependency — actually an unsupported combination upstream in mozilla-central's ICE state machine)? Or is there a Camoufox-specific interaction with media.peerconnection.ice.proxy_only_if_behind_proxy (or one of the other WebRTC-related prefs already set in camoufox.cfg) that's preventing the terminal transition even though the documented trigger condition should otherwise be satisfied?

We were not able to answer this from the vendored binary + the two patch files we could read without access to (or compiling) the underlying mozilla-central ICE state machine sources, which is explicitly out of scope for us (see note below) — any pointer to the relevant place in third_party/libwebrtc/media/mtransport (or wherever the gathering-state transition actually lives for this Camoufox build) would be enough for us to keep digging on our side, or to confirm this is a mozilla-central-level limitation rather than anything Camoufox-specific.

What we did not do

Per our own project's scope decision, we did not compile or instrument Camoufox from source to dig further — 3 internal reviewers of 4 explicitly judged that disproportionate relative to what a well-documented upstream issue could resolve faster, given the causal layer in question (mozilla-central's ICE state machine) isn't vendored as source in this repo.

Internal references (not public — for anyone at Camoufox who wants more raw data)

Our reproduction harness lives in a private repository; the specific files (not linked here since the repo isn't public) are crates/browser-adapter/tests/webrtc_e2e_real.rs (the 7-scenario harness against the real vendored binary) and crates/browser-adapter/tests/support/ socks5_udp_test_proxy.rs (the 3 proxy behaviors: real UDP relay, UDP-accepting blackhole, HTTP CONNECT-only), commit e2775f8. Happy to share raw logs, the full 9-case run, or extract the test harness into a standalone minimal repro if that's more useful than what's pasted above — just let us know what format helps most.