From 0bbd15246fb73c595f152b60ecc65b2109a2a437 Mon Sep 17 00:00:00 2001 From: Jake Writer Date: Fri, 25 Sep 2026 20:32:08 -0600 Subject: [PATCH] fix(python): send an IPv6 WebRTC address to setWebRTCIPv6 The per-context init script passed every webrtc_ip, IPv6 included, to window.setWebRTCIPv4(), and never called setWebRTCIPv6(). An IPv6 address (given directly, or resolved as a proxy's exit IP) was stored as the context's IPv4 value and the IPv6 slot stayed empty. The script now picks the setter by address family, and an address that is neither raises InvalidIP instead of being passed through. Co-Authored-By: Claude Opus 5.5 --- docs/per-context-patches.md | 2 +- pythonlib/camoufox/async_api.py | 2 +- pythonlib/camoufox/fingerprints.py | 5 ++++- pythonlib/camoufox/sync_api.py | 2 +- pythonlib/tests/test_webrtc_ip_setter.py | 23 +++++++++++++++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 pythonlib/tests/test_webrtc_ip_setter.py diff --git a/docs/per-context-patches.md b/docs/per-context-patches.md index 5f71c08..fc7c941 100644 --- a/docs/per-context-patches.md +++ b/docs/per-context-patches.md @@ -617,7 +617,7 @@ bundles are shipped in the wheel. | Audio seed | Derived from the identity (NewBrowser) or `randint(1, 2^32-1)` (NewContext) | Never 0 | | Timezone | From preset, or `timezone` in `CAMOU_CONFIG` | The init script calls `setTimezone()` only for an explicit value; otherwise the C++ side falls back to `CAMOU_CONFIG` (set from geoip at launch) or the browser default. | | Speech voices | `_generate_random_voice_subset()` | Follows the measured model in `voice-manifests.json`: Windows gets the display language's OneCore pack plus its legacy Desktop voices at their measured rate; macOS the compact + Eloquence base plus rare downloads; Linux speech-dispatcher's espeak-ng list. Seeded by the identity. NOT from presets. | -| WebRTC IP | Not set by default | User sets via `window.setWebRTCIPv4()`. NewContext init script defaults to empty string `""` | +| WebRTC IP | Not set by default | NewContext's `webrtc_ip` (or the proxy's exit IP) goes to `window.setWebRTCIPv4()` or `window.setWebRTCIPv6()` by address family; an invalid address raises `InvalidIP`. Without one, the init script calls `setWebRTCIPv4("")` | | Geolocation | User parameter or geoip detection | Via Playwright `context.setGeolocation()` | ### Key Files diff --git a/pythonlib/camoufox/async_api.py b/pythonlib/camoufox/async_api.py index e05c813..52cb498 100644 --- a/pythonlib/camoufox/async_api.py +++ b/pythonlib/camoufox/async_api.py @@ -204,7 +204,7 @@ async def AsyncNewContext( preset: A fingerprint preset dict to use. If None, fpgen draws a new identity. os: Target OS for the drawn identity ("windows", "macos", "linux"). ff_version: Firefox major version to claim in the UA. Defaults to the browser's own. - webrtc_ip: IPv4 address to spoof for WebRTC ICE candidates. + webrtc_ip: IPv4 or IPv6 address to spoof for WebRTC ICE candidates. proxy: Per-context proxy (Playwright format: {"server": "...", "username": "...", "password": "..."}). Unless webrtc_ip and timezone_id are both given, they are looked up from the proxy's exit IP; InvalidIP is raised if that lookup fails. diff --git a/pythonlib/camoufox/fingerprints.py b/pythonlib/camoufox/fingerprints.py index 752076c..9e4ab16 100644 --- a/pythonlib/camoufox/fingerprints.py +++ b/pythonlib/camoufox/fingerprints.py @@ -11,6 +11,7 @@ from random import Random, choice, randint, randrange from typing import Any, Dict, FrozenSet, List, Optional, Set, Tuple from camoufox._warnings import FallbackWarning +from camoufox.ip import valid_ipv4, validate_ip from camoufox.pkgman import load_yaml from camoufox.webgl import sample_webgl @@ -1759,8 +1760,10 @@ def _build_init_script(values: Dict[str, Any]) -> str: # WebRTC IP ip = values.get('webrtcIP') if ip: + validate_ip(ip) + fn_name = 'setWebRTCIPv4' if valid_ipv4(ip) else 'setWebRTCIPv6' lines.append( - f' if (typeof w.setWebRTCIPv4 === "function") w.setWebRTCIPv4({_json.dumps(ip)});' + f' if (typeof w.{fn_name} === "function") w.{fn_name}({_json.dumps(ip)});' ) else: lines.append( diff --git a/pythonlib/camoufox/sync_api.py b/pythonlib/camoufox/sync_api.py index be07644..5cd65d7 100644 --- a/pythonlib/camoufox/sync_api.py +++ b/pythonlib/camoufox/sync_api.py @@ -182,7 +182,7 @@ def NewContext( preset: A fingerprint preset dict to use. If None, fpgen draws a new identity. os: Target OS for the drawn identity ("windows", "macos", "linux"). ff_version: Firefox major version to claim in the UA. Defaults to the browser's own. - webrtc_ip: IPv4 address to spoof for WebRTC ICE candidates. + webrtc_ip: IPv4 or IPv6 address to spoof for WebRTC ICE candidates. proxy: Per-context proxy (Playwright format: {"server": "...", "username": "...", "password": "..."}). Unless webrtc_ip and timezone_id are both given, they are looked up from the proxy's exit IP; InvalidIP is raised if that lookup fails. diff --git a/pythonlib/tests/test_webrtc_ip_setter.py b/pythonlib/tests/test_webrtc_ip_setter.py new file mode 100644 index 0000000..45dd224 --- /dev/null +++ b/pythonlib/tests/test_webrtc_ip_setter.py @@ -0,0 +1,23 @@ +"""The per-context init script hands a WebRTC IP to the setter for its family.""" + +import pytest + +from camoufox.exceptions import InvalidIP +from camoufox.fingerprints import _build_init_script + + +def test_ipv4_goes_to_the_ipv4_setter(): + script = _build_init_script({"webrtcIP": "203.0.113.7"}) + assert 'w.setWebRTCIPv4("203.0.113.7")' in script + assert "setWebRTCIPv6(" not in script + + +def test_ipv6_goes_to_the_ipv6_setter(): + script = _build_init_script({"webrtcIP": "2001:db8::7"}) + assert 'w.setWebRTCIPv6("2001:db8::7")' in script + assert "2001:db8::7" not in script.split("setWebRTCIPv6")[0] + + +def test_an_invalid_address_is_refused(): + with pytest.raises(InvalidIP): + _build_init_script({"webrtcIP": "not-an-ip"})