[Bug] --fingerprint-webrtc-ip allows direct STUN despite disable_non_proxied_udp (151.0.7922.108.4)
Description
On CloakBrowser Pro 151.0.7922.108.4, --fingerprint-webrtc-ip=<proxy exit IP> correctly rewrites the JavaScript-visible ICE address, but the browser still sends STUN UDP directly from the host, even when both disable_non_proxied_udp flags are present. Removing only the IP-replacement flag makes the UDP restriction effective in the controlled comparison.
This was measured with an authenticated HTTP proxy, empty browser contexts, synthetic pages, and packet metadata captured on the host's external interface. HTTPS from the top-level page and a cross-origin iframe used the proxy throughout.
Related: #95. This report adds packet-level evidence and an A/B comparison for the newer .4 Pro binary.
CloakBrowser version / wrapper
- JavaScript:
[email protected],[email protected]. - Actual selected executable: Pro Chromium
151.0.7922.108.4. - Measurements taken September 7, 2026.
Environment
Linux x64 in Docker, bridge networking; application image based on mcr.microsoft.com/playwright:v1.61.1-noble. Headless, Windows fingerprint persona, fixed seed, humanize: true with the careful preset, nonpersistent contexts, --disable-http2. Windows fonts and screen/hardware fingerprint overrides were also retained consistently across the A/B runs. The container had no usable external IPv6 route.
The integration uses buildLaunchOptions() -> Playwright chromium.launch() -> humanizeBrowser() -> browser.newContext(buildContextOptions(...)). No JavaScript WebRTC spoofing patch was injected.
Launch options
Relevant options, with private values replaced by placeholders:
{
browserVersion: '151.0.7922.108.4',
releaseChannel: 'stable',
headless: true,
proxy: {
server: 'http://PROXY_HOST:PROXY_PORT',
username: 'PROXY_USERNAME',
password: 'PROXY_PASSWORD'
},
args: [
'--fingerprint=FIXED_UINT32_SEED',
'--fingerprint-platform=windows',
'--disable-http2',
'--fingerprint-webrtc-ip=PROXY_EXIT_IP',
'--force-webrtc-ip-handling-policy=disable_non_proxied_udp',
'--webrtc-ip-handling-policy=disable_non_proxied_udp'
]
}The exact selected binary was verified; the channel label above is the value passed by the integration. A valid Pro license was supplied separately.
Steps to reproduce
- Launch the pinned binary with an authenticated HTTP proxy and the options above. Keep the same proxy and all other launch/context settings for the comparison.
- Verify HTTPS egress with an IP echo endpoint.
- On an empty synthetic page, run the ICE-gathering operation below. Repeat it in a cross-origin iframe. We used the literal IPv4 address of a public STUN endpoint to separate UDP routing from DNS resolution in this test.
- Capture only that STUN endpoint's packet metadata on the external host interface. Correlate its source port with the browser candidate. Check for actual packets, rather than treating a NetLog UDP
connect()event as proof of transmission. - Close the browser. Repeat with only
--fingerprint-webrtc-ipomitted; retain both UDP policy flags. Avoidgeoip: trueor other automatic reinsertion of the omitted flag.
The ICE operation used in the measurements, with the endpoint supplied as an argument:
const observed = await frame.evaluate(async (stunUrl) => {
const pc = new RTCPeerConnection({ iceServers: [{ urls: stunUrl }] });
const candidates = [];
const errors = [];
let timer;
try {
pc.createDataChannel('network-diagnostic');
const complete = new Promise(resolve => {
timer = setTimeout(resolve, 10000);
pc.onicecandidate = event => event.candidate
? candidates.push(event.candidate.toJSON())
: resolve();
pc.onicecandidateerror = event => errors.push({
code: event.errorCode,
address: event.address,
text: event.errorText
});
});
await pc.setLocalDescription(await pc.createOffer());
await complete;
return { candidates, errors, gatheringState: pc.iceGatheringState };
} finally {
clearTimeout(timer);
pc.close();
}
}, process.env.STUN_URL);Observed results / sanitized evidence
| Configuration | HTTPS egress, page + iframe | JavaScript ICE result | Browser STUN packets on host egress |
|---|---|---|---|
| IP replacement; no explicit UDP policy | Proxy IP | Proxy IP in public candidate | Direct from host public IP |
| IP replacement + both UDP policy flags | Proxy IP | Proxy IP in public candidate | Still direct from host public IP |
| Both UDP policy flags; IP replacement omitted | Proxy IP | No candidates; gathering complete | No browser STUN packets |
Example correlation from the second configuration; addresses are replaced by role names:
page ICE: ... PROXY_EXIT_IP 43422 typ srflx ...
wire: HOST_PUBLIC_IP:43422 -> STUN_ENDPOINT:19302 UDP
iframe ICE: ... PROXY_EXIT_IP 34485 typ srflx ...
wire: HOST_PUBLIC_IP:34485 -> STUN_ENDPOINT:19302 UDPA separately marked STUN control packet was captured in the no-replacement run, so the absence of browser STUN packets was not caused by a nonfunctional capture.
Additional readbacks with IP replacement enabled (candidate.address, relatedAddress, SDP, getStats(), and ICE transport local candidates) did not expose the host's public IP to JavaScript. The confirmed public-IP exposure is to the receiving STUN endpoint.
Related DNS observation
In a separate run with IP replacement enabled, using a unique hostname for the STUN endpoint produced both A and AAAA queries outside the HTTP proxy:
HOST_PUBLIC_IP -> HOST_DNS_RESOLVER:53 UDP
question: <unique STUN hostname>, type A / AAAAThe DNS resolver therefore sees the host IP. Two resolver-visibility checks showed only the recursive resolver's address at the authoritative side, with no client-subnet information; this is not evidence that the authoritative domain operator received the full host IP. This DNS measurement was not repeated under both UDP-policy configurations.
Expected behavior / requested clarification
disable_non_proxied_udpshould prevent direct STUN UDP when an HTTP proxy is configured, regardless of whether ICE IP replacement is enabled.- Our requirement is to keep WebRTC reporting the configured proxy IP while preventing direct WebRTC traffic and ICE hostname lookups outside the intended proxy path. Simply returning no candidates does not meet the reporting requirement.
- Is the policy interaction intentional, or a regression? Is there a supported configuration/build that satisfies both requirements with an HTTP proxy? If a different transport is required, please clarify the supported approach.
Tested with a different IP or proxy?
The baseline direct-STUN behavior reproduced on two separate Linux/Docker hosts using the same HTTP proxy. The policy A/B comparison and detailed DNS capture were performed on one host. A different proxy provider was not tested.
Works outside Docker / on host machine?
Not tested outside Docker.
Dockerfile / additional notes
Base image listed above; the application Dockerfile is omitted. These are isolated networking measurements and do not depend on any particular website, login, CAPTCHA, or account state. No inference about a website's detection or account-enforcement decisions is needed to reproduce the packet-level issue.
Source: CloakHQ/CloakBrowser