from __future__ import annotations from dataclasses import dataclass, field from pathlib import Path from .browser_identity import ( CookiePolicy, Persona, cookie_behavior_value, cookie_session_only, get_cookie_policy, get_persona, is_blend_persona, ) @dataclass class FirefoxHardening: # Proxy & network always-on force_proxy: bool = True disable_webrtc: bool = True # always strongly recommended; leaks real IP # Hardened block mode — overrides persona spoofing resist_fingerprinting: bool = True disable_telemetry: bool = True first_party_isolation: bool = True strict_tracking_protection: bool = True clear_on_shutdown: bool = True timezone_utc: bool = True # Persona / cookie selectors (added in "spoof when possible" mode) persona_key: str = "custom" # see browser_identity.PERSONAS cookie_mode: str = "block_third_party" # see browser_identity.COOKIE_MODES # Optional Firefox-managed user-agent override even outside of a persona user_agent_override: str = "" # Filled in by ``apply_persona`` when persona_key starts with "blend_". persona: Persona | None = field(default=None, repr=False, compare=False) def apply_persona(hard: FirefoxHardening) -> FirefoxHardening: """Mutate hardening flags consistent with the persona choice. Blend-in personas relax the "stand out" toggles (RFP, FPI, strict TP) but keep the IP-leakers off (WebRTC, geolocation, etc.). Cookie mode is honored. The result is a profile that looks like a normal browser. """ if hard.persona_key == "hardened": hard.resist_fingerprinting = True hard.first_party_isolation = True hard.strict_tracking_protection = True hard.timezone_utc = True hard.persona = None return hard if is_blend_persona(hard.persona_key): hard.resist_fingerprinting = False # RFP itself is a fingerprint hard.first_party_isolation = False # breaks logins on many sites hard.strict_tracking_protection = False # Firefox default ETP is enough hard.timezone_utc = False hard.persona = get_persona(hard.persona_key) if hard.persona and not hard.user_agent_override: hard.user_agent_override = hard.persona.user_agent return hard # custom — honor the individual flags as-is. hard.persona = None return hard def _bool(v: bool) -> str: return "true" if v else "false" def build_user_js( proxy_host: str, proxy_port: int, hard: FirefoxHardening, ) -> str: hard = apply_persona(hard) lines: list[str] = [ "// Managed by Proxy God. Changes are overwritten on next launch.", # Always-on baseline (no UX cost) 'user_pref("app.normandy.enabled", false);', 'user_pref("app.shield.optoutstudies.enabled", false);', 'user_pref("browser.newtabpage.activity-stream.feeds.telemetry", false);', 'user_pref("browser.newtabpage.activity-stream.telemetry", false);', 'user_pref("browser.ping-centre.telemetry", false);', 'user_pref("toolkit.telemetry.archive.enabled", false);', 'user_pref("toolkit.telemetry.bhrPing.enabled", false);', 'user_pref("toolkit.telemetry.enabled", false);', 'user_pref("toolkit.telemetry.firstShutdownPing.enabled", false);', 'user_pref("toolkit.telemetry.hybridContent.enabled", false);', 'user_pref("toolkit.telemetry.newProfilePing.enabled", false);', 'user_pref("toolkit.telemetry.shutdownPingSender.enabled", false);', 'user_pref("toolkit.telemetry.unified", false);', 'user_pref("datareporting.healthreport.uploadEnabled", false);', 'user_pref("datareporting.policy.dataSubmissionEnabled", false);', 'user_pref("network.trr.mode", 5);', 'user_pref("network.captive-portal-service.enabled", false);', 'user_pref("geo.enabled", false);', 'user_pref("media.navigator.enabled", false);', 'user_pref("dom.battery.enabled", false);', 'user_pref("dom.gamepad.enabled", false);', 'user_pref("dom.netinfo.enabled", false);', # WebGL allowed by default (blocking it makes you stand out massively) 'user_pref("webgl.enable-debug-renderer-info", false);', ] if hard.force_proxy: lines.extend( [ 'user_pref("network.proxy.type", 1);', 'user_pref("network.proxy.share_proxy_settings", true);', f'user_pref("network.proxy.http", "{proxy_host}");', f'user_pref("network.proxy.http_port", {int(proxy_port)});', f'user_pref("network.proxy.ssl", "{proxy_host}");', f'user_pref("network.proxy.ssl_port", {int(proxy_port)});', 'user_pref("network.proxy.ftp", "");', 'user_pref("network.proxy.ftp_port", 0);', 'user_pref("network.proxy.socks", "");', 'user_pref("network.proxy.socks_port", 0);', 'user_pref("network.proxy.socks_version", 5);', 'user_pref("network.proxy.socks_remote_dns", false);', 'user_pref("network.proxy.no_proxies_on", "");', ] ) if hard.disable_webrtc: # Off regardless of persona — WebRTC leaks real IP through STUN. lines.extend( [ 'user_pref("media.peerconnection.enabled", false);', 'user_pref("media.peerconnection.ice.default_address_only", true);', 'user_pref("media.peerconnection.ice.no_host", true);', ] ) if hard.resist_fingerprinting: lines.extend( [ 'user_pref("privacy.resistFingerprinting", true);', 'user_pref("privacy.resistFingerprinting.letterboxing", true);', 'user_pref("privacy.window.maxInnerWidth", 1600);', 'user_pref("privacy.window.maxInnerHeight", 900);', ] ) if hard.first_party_isolation: lines.append('user_pref("privacy.firstparty.isolate", true);') if hard.disable_telemetry: lines.append('user_pref("browser.send_pings", false);') if hard.strict_tracking_protection: lines.extend( [ 'user_pref("privacy.trackingprotection.enabled", true);', 'user_pref("privacy.trackingprotection.pbmode.enabled", true);', ] ) # Persona spoofing (UA, accept-language, screen, platform) if hard.persona is not None: p = hard.persona lines.extend( [ f'user_pref("general.useragent.override", "{p.user_agent}");', f'user_pref("intl.accept_languages", "{p.accept_language}");', f'user_pref("general.useragent.locale", "{p.locale}");', 'user_pref("javascript.use_us_english_locale", true);', f'user_pref("privacy.window.maxInnerWidth", {int(p.screen_w)});', f'user_pref("privacy.window.maxInnerHeight", {int(p.screen_h)});', # Hint timezone — Firefox honors TZ env var at launch (set by # the launcher) but this pref nudges some site detection too. f'user_pref("intl.locale.requested", "{p.locale}");', ] ) elif hard.user_agent_override: lines.append( f'user_pref("general.useragent.override", "{hard.user_agent_override}");' ) # ── Cookie policy ──────────────────────────────────────────────────────── cp: CookiePolicy = get_cookie_policy(hard.cookie_mode) lines.extend([ f'user_pref("network.cookie.cookieBehavior", {cp.behavior});', # pbmode = private-browsing mode uses same behavior f'user_pref("network.cookie.cookieBehavior.pbmode", {cp.behavior});', # lifetime: 0 = normal, 2 = session-only f'user_pref("network.cookie.lifetimePolicy", {cp.lifetime});', ]) # Total Cookie Protection (TCP / Firefox partitioning) if cp.partition: lines.extend([ 'user_pref("network.cookie.cookieBehavior.optInPartitioning", true);', # Also isolate cache, localStorage, IndexedDB per top-level site 'user_pref("privacy.partition.serviceWorkers", true);', 'user_pref("privacy.partition.network_state", true);', 'user_pref("privacy.partition.bloburl_by_default", true);', ]) else: lines.extend([ 'user_pref("network.cookie.cookieBehavior.optInPartitioning", false);', ]) # SameSite=None cookies must be Secure (good hygiene regardless of mode) lines.append('user_pref("network.cookie.sameSite.noneRequiresSecure", true);') # ── Sanitize-on-shutdown ───────────────────────────────────────────────── # The CookiePolicy sanitize flag takes priority; the hard.clear_on_shutdown # toggle acts as an additional override for cookies+cache even when the # cookie mode doesn't request it. do_sanitize = cp.sanitize or hard.clear_on_shutdown if do_sanitize: lines.append('user_pref("privacy.sanitize.sanitizeOnShutdown", true);') # Cookies lines.append(f'user_pref("privacy.clearOnShutdown.cookies", {_bool(cp.clear_cookies or hard.clear_on_shutdown)});') # Cache lines.append(f'user_pref("privacy.clearOnShutdown.cache", {_bool(cp.clear_cache or hard.clear_on_shutdown)});') # History lines.append(f'user_pref("privacy.clearOnShutdown.history", {_bool(cp.clear_history or hard.clear_on_shutdown)});') # Downloads list lines.append('user_pref("privacy.clearOnShutdown.downloads", true);') # Form / search bar autofill data lines.append(f'user_pref("privacy.clearOnShutdown.formdata", {_bool(cp.clear_formdata or hard.clear_on_shutdown)});') # Active sessions (HTTP auth, logged-in sites) lines.append('user_pref("privacy.clearOnShutdown.sessions", true);') # localStorage + IndexedDB lines.append(f'user_pref("privacy.clearOnShutdown.localStorage", {_bool(cp.clear_ls or hard.clear_on_shutdown)});') lines.append(f'user_pref("privacy.clearOnShutdown.indexedDB", {_bool(cp.clear_ls or hard.clear_on_shutdown)});') # Service worker registrations lines.append('user_pref("privacy.clearOnShutdown.serviceWorkers", true);') # Offline web app cache lines.append('user_pref("privacy.clearOnShutdown.offlineApps", true);') else: lines.append('user_pref("privacy.sanitize.sanitizeOnShutdown", false);') if hard.timezone_utc: lines.append('user_pref("privacy.resistFingerprinting.reduceTimerPrecision", true);') lines.append("") return "\n".join(lines) def persona_env(hard: FirefoxHardening) -> dict[str, str]: """Environment overrides applied when launching Firefox under a persona. Only the timezone needs the OS-level env var (TZ) — Firefox reads it even when running under Windows. """ hard = apply_persona(hard) if hard.persona is None: return {} return {"TZ": hard.persona.timezone} def ensure_firefox_profile( profile_dir: Path, proxy_host: str, proxy_port: int, hard: FirefoxHardening, ) -> None: profile_dir.mkdir(parents=True, exist_ok=True) (profile_dir / "user.js").write_text( build_user_js(proxy_host, proxy_port, hard), encoding="utf-8", )