diff --git a/README.md b/README.md
index ec78d7a..90ab86a 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ Camoufox aims to be a minimalistic browser for robust fingerprint injection & an
- Patches to avoid bot detection ✅
- Custom Playwright Juggler implementation for the latest Firefox ✅
- Font spoofing & anti-fingerprinting ✅
+- WebRTC IP spoofing ✅
- Patches from LibreWolf & Ghostery to remove Mozilla services ✅
- Optimized for memory and speed ✅
- Stays up to date with the latest Firefox version 🕓
@@ -45,7 +46,7 @@ Camoufox is built on top of Firefox/Juggler instead of Chromium because:
## Fingerprint Injection
-Camoufox overrides Javascript properties on the lowest level possible, allowing values to be spoofed across all scopes.
+In Camoufox, data is intercepted at the C++ implementation level, making the changes undetectable through JavaScript inspection.
To spoof fingerprint properties, pass a JSON containing properties to spoof:
@@ -221,7 +222,24 @@ Camoufox can override the following network headers:
+
+WebRTC IP
+
+Camoufox implements WebRTC IP spoofing at the protocol level by modifying ICE candidates and SDP before they're sent.
+
+| Property | Status | Description |
+| ----------- | ------ | ------------------- |
+| webrtc:ipv4 | ✅ | IPv4 address to use |
+| webrtc:ipv6 | ✅ | IPv6 address to use |
+
+**Notes:**
+
+- To completely disable WebRTC, set the `media.peerconnection.enabled` preference to `false`.
+
+
+
+
Addons
@@ -281,6 +299,7 @@ Miscellaneous (WebGl spoofing, battery status, etc)
- Battery API spoofing
- Support for spoofing both inner and outer window viewport sizes
- Network headers (Accept-Languages and User-Agent) are spoofed to match the navigator properties
+- WebRTC IP spoofing at the protocol level
- etc.
#### Anti font fingerprinting
@@ -321,7 +340,7 @@ Miscellaneous (WebGl spoofing, battery status, etc)
### Tests
-Camoufox performs well against every major WAF I've tested. (Test sites from [Botright](https://github.com/Vinyzu/botright/?tab=readme-ov-file#browser-stealth))
+Camoufox performs well against every major WAF I've tested. (Original test sites from [Botright](https://github.com/Vinyzu/botright/?tab=readme-ov-file#browser-stealth))
| Test | Status |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
@@ -340,6 +359,10 @@ Camoufox performs well against every major WAF I've tested. (Test sites from [Bo
| **Cloudflare** | ✔️ |
| ‣ [Turnstile](https://nopecha.com/demo/turnstile) | ✔️ |
| ‣ [Interstitial](https://nopecha.com/demo/cloudflare) | ✔️ Unstable on Chrome fingerprints |
+| **WebRTC IP Spoofing** | ✔️ |
+| ‣ [Browserleaks WebRTC](https://browserleaks.net/webrtc) | ✔️ |
+| ‣ [CreepJS WebRTC](https://abrahamjuliot.github.io/creepjs/) | ✔️ |
+| ‣ [BrowserScan WebRTC](https://www.browserscan.net/webrtc) | ✔️ |
| **Font Fingerprinting** | ✔️ |
| ‣ [Browserleaks Fonts](https://browserleaks.net/fonts) | ✔️ |
| ‣ [CreepJS TextMetrics](https://abrahamjuliot.github.io/creepjs/tests/fonts.html) | ✔️ |
diff --git a/patches/webrtc-ip-spoofing.patch b/patches/webrtc-ip-spoofing.patch
new file mode 100644
index 0000000..0d2aa53
--- /dev/null
+++ b/patches/webrtc-ip-spoofing.patch
@@ -0,0 +1,191 @@
+diff --git a/dom/media/webrtc/jsapi/PeerConnectionImpl.cpp b/dom/media/webrtc/jsapi/PeerConnectionImpl.cpp
+index 5b70a58104..579a620c48 100644
+--- a/dom/media/webrtc/jsapi/PeerConnectionImpl.cpp
++++ b/dom/media/webrtc/jsapi/PeerConnectionImpl.cpp
+@@ -116,6 +116,8 @@
+
+ #include "mozilla/dom/BrowserChild.h"
+ #include "mozilla/net/WebrtcProxyConfig.h"
++#include "MaskConfig.hpp"
++#include "mozilla/RustRegex.h"
+
+ #ifdef XP_WIN
+ // We need to undef the MS macro again in case the windows include file
+@@ -1705,7 +1707,15 @@ PeerConnectionImpl::SetLocalDescription(int32_t aAction, const char* aSDP) {
+ }
+ };
+
+- mLocalRequestedSDP = aSDP;
++ // Sanitize the SDP to prevent IP leaks
++ std::string sanitizedSDP(aSDP);
++ nsresult sanitizeResult = SanitizeSDPForIPLeak(sanitizedSDP);
++ if (NS_FAILED(sanitizeResult)) {
++ CSFLogError(LOGTAG, "%s - Failed to sanitize SDP", __FUNCTION__);
++ return sanitizeResult;
++ }
++
++ mLocalRequestedSDP = sanitizedSDP;
+
+ SyncToJsep();
+
+@@ -3275,12 +3285,22 @@ const std::string& PeerConnectionImpl::GetName() {
+ return mName;
+ }
+
+-void PeerConnectionImpl::CandidateReady(const std::string& candidate,
++void PeerConnectionImpl::CandidateReady(const std::string& candidate_,
+ const std::string& transportId,
+ const std::string& ufrag) {
+ STAMP_TIMECARD(mTimeCard, "Ice Candidate gathered");
+ PC_AUTO_ENTER_API_CALL_VOID_RETURN(false);
+
++ const std::string& candidate = [&]() -> const std::string& {
++ if (ShouldSpoofCandidateIP()) {
++ static std::string spoofedCandidate;
++ spoofedCandidate = SpoofCandidateIP(candidate_);
++ return spoofedCandidate;
++ } else {
++ return candidate_;
++ }
++ }();
++
+ if (mForceIceTcp && std::string::npos != candidate.find(" UDP ")) {
+ CSFLogWarn(LOGTAG, "Blocking local UDP candidate: %s", candidate.c_str());
+ STAMP_TIMECARD(mTimeCard, "UDP Ice Candidate blocked");
+@@ -3343,10 +3363,89 @@ void PeerConnectionImpl::SendLocalIceCandidateToContent(
+ uint16_t level, const std::string& mid, const std::string& candidate,
+ const std::string& ufrag) {
+ STAMP_TIMECARD(mTimeCard, "Send Ice Candidate to content");
+- JSErrorResult rv;
+- mPCObserver->OnIceCandidate(level, ObString(mid.c_str()),
+- ObString(candidate.c_str()),
+- ObString(ufrag.c_str()), rv);
++
++ // Check if IP spoofing is enabled
++ if (ShouldSpoofCandidateIP()) {
++ std::string spoofedCandidate = SpoofCandidateIP(candidate);
++ JSErrorResult rv;
++ mPCObserver->OnIceCandidate(level, ObString(mid.c_str()),
++ ObString(spoofedCandidate.c_str()),
++ ObString(ufrag.c_str()), rv);
++ } else {
++ JSErrorResult rv;
++ mPCObserver->OnIceCandidate(level, ObString(mid.c_str()),
++ ObString(candidate.c_str()),
++ ObString(ufrag.c_str()), rv);
++ }
++}
++
++bool PeerConnectionImpl::ShouldSpoofCandidateIP() const {
++ // Checks if either webrtc:ipv4 or webrtc:ipv6 is set in the config
++ return MaskConfig::GetString("webrtc:ipv4").has_value() ||
++ MaskConfig::GetString("webrtc:ipv6").has_value();
++}
++
++nsresult PeerConnectionImpl::SanitizeSDPForIPLeak(std::string& sdp) {
++ auto replaceIP = [&sdp](const char* pattern, const char* pref) {
++ if (auto value = MaskConfig::GetString(pref)) {
++ mozilla::RustRegex regex(pattern);
++ if (regex.IsValid()) {
++ std::string result;
++ auto iter = regex.IterMatches(sdp);
++ size_t lastEnd = 0;
++ while (auto match = iter.Next()) {
++ result.append(sdp, lastEnd, match->start - lastEnd);
++ result.append(value.value());
++ lastEnd = match->end;
++ }
++ result.append(sdp, lastEnd);
++ sdp = std::move(result);
++ }
++ }
++ };
++
++ replaceIP("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b", "webrtc:ipv4");
++ replaceIP("\\b(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}\\b", "webrtc:ipv6");
++
++ return NS_OK;
++}
++
++std::string PeerConnectionImpl::SpoofCandidateIP(const std::string& candidate) {
++ CSFLogDebug(LOGTAG, "Original candidate: %s", candidate.c_str());
++
++ std::istringstream iss(candidate);
++ std::vector tokens{std::istream_iterator{iss},
++ std::istream_iterator{}};
++
++ bool spoofed = false;
++ // Check for IPv4 or .local address
++ if (tokens.size() >= 8) {
++ if (tokens[4].find('.') != std::string::npos) { // IPv4 or .local
++ if (auto value = MaskConfig::GetString("webrtc:ipv4")) {
++ tokens[4] = value.value();
++ spoofed = true;
++ }
++ }
++ // Check for IPv6
++ else if (tokens[4].find(':') != std::string::npos) {
++ if (auto value = MaskConfig::GetString("webrtc:ipv6")) {
++ tokens[4] = value.value();
++ spoofed = true;
++ }
++ }
++ }
++
++ if (spoofed) {
++ std::ostringstream oss;
++ std::copy(tokens.begin(), tokens.end(),
++ std::ostream_iterator(oss, " "));
++ std::string result = oss.str();
++ CSFLogDebug(LOGTAG, "Spoofed candidate: %s", result.c_str());
++ return result;
++ }
++
++ CSFLogDebug(LOGTAG, "Candidate not spoofed: %s", candidate.c_str());
++ return candidate;
+ }
+
+ void PeerConnectionImpl::IceConnectionStateChange(
+@@ -4446,8 +4545,10 @@ bool PeerConnectionImpl::GetPrefDefaultAddressOnly() const {
+
+ uint64_t winId = mWindow->WindowID();
+
+- bool default_address_only = Preferences::GetBool(
+- "media.peerconnection.ice.default_address_only", false);
++ bool default_address_only =
++ Preferences::GetBool("media.peerconnection.ice.default_address_only",
++ false) ||
++ ShouldSpoofCandidateIP();
+ default_address_only |=
+ !MediaManager::Get()->IsActivelyCapturingOrHasAPermission(winId);
+ return default_address_only;
+diff --git a/dom/media/webrtc/jsapi/PeerConnectionImpl.h b/dom/media/webrtc/jsapi/PeerConnectionImpl.h
+index d7b54ad721..8d8e92d14b 100644
+--- a/dom/media/webrtc/jsapi/PeerConnectionImpl.h
++++ b/dom/media/webrtc/jsapi/PeerConnectionImpl.h
+@@ -624,8 +624,12 @@ class PeerConnectionImpl final
+ RefPtr GetMediaPipelineForTrack(
+ dom::MediaStreamTrack& aRecvTrack);
+
+- void CandidateReady(const std::string& candidate,
++ void CandidateReady(const std::string& candidate_,
+ const std::string& transportId, const std::string& ufrag);
++
++ bool ShouldSpoofCandidateIP() const;
++ nsresult SanitizeSDPForIPLeak(std::string& sdp);
++ std::string SpoofCandidateIP(const std::string& candidate);
+ void SendLocalIceCandidateToContent(uint16_t level, const std::string& mid,
+ const std::string& candidate,
+ const std::string& ufrag);
+diff --git a/dom/media/webrtc/jsapi/moz.build b/dom/media/webrtc/jsapi/moz.build
+index 05920fc151..34dcf3b9a1 100644
+--- a/dom/media/webrtc/jsapi/moz.build
++++ b/dom/media/webrtc/jsapi/moz.build
+@@ -62,3 +62,6 @@ EXPORTS.mozilla.dom += [
+ ]
+
+ FINAL_LIBRARY = "xul"
++
++# DOM Mask
++LOCAL_INCLUDES += ["/dom/mask"]
+\ No newline at end of file
diff --git a/settings/camoufox.cfg b/settings/camoufox.cfg
index 399ddb0..ee995e1 100644
--- a/settings/camoufox.cfg
+++ b/settings/camoufox.cfg
@@ -13,6 +13,8 @@ pref("browser.sessionstore.max_resumed_crashes", 0);
pref("browser.sessionstore.restore_on_demand", false);
pref("browser.sessionstore.restore_tabs_lazily", false);
+pref("media.peerconnection.ice.no_host", true);
+
// Tweaks that undo Playwright:
// Force enable content isolation (WAFs can detect this!)