ICE gathering never reaches "complete" with webrtc:ipv4/ipv6 set — MaybeFabricateSrflxCandidate never fires (tested with a real UDP-relaying SOCKS5 proxy)
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:1073741823gathering_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)
- Launch the vendored Camoufox binary with
CAMOU_CONFIGcontaining at leastwebrtc:ipv4set 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). - Optionally wire
network.proxy.type=1+network.proxy.socks/network.proxy.socks_port+network.proxy.socks_version=5to point at a real SOCKS5 proxy that implements a genuine bidirectionalUDP ASSOCIATErelay (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. - Navigate to a page that opens
new RTCPeerConnection({})(noiceServersconfigured — this matters, see below) and callscreateDataChannel+createOffer/setLocalDescription. - Listen to
pc.onicegatheringstatechangeandpc.oniceconnectionstatechange, log every state with a timestamp, and wait up to 60s. - Observe: gathering state moves
new → gatheringand 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.
Source: daijro/camoufox