272 lines
12 KiB
Python
272 lines
12 KiB
Python
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",
|
|
)
|