Spoof-when-possible browser personas, Tier-2 telemetry kill + artifact wipe, UI polish
Browser: identity persona dropdown (blend Windows-Chrome / Firefox / mac-Safari / hardened / custom) + cookie policy dropdown. Blend personas relax RFP/FPI/strict-TP so the machine looks normal while still hiding behind the chain. Persona TZ applied via env var; UA / language / locale / screen via user.js. WebRTC still forced off (it leaks real IP). Privacy: Telemetry kill toggle (DiagTrack, Activity History, ad ID, Cortana, scheduled tasks) with full snapshot/restore. One-button forensic artifact wipe (TEMP, Recent, Jump Lists, Prefetch, MRU, clipboard). UI: neon palette, glow card borders, gradient header banner. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -35,6 +35,13 @@ from .firewall import (
|
|||||||
is_engaged as fw_is_engaged,
|
is_engaged as fw_is_engaged,
|
||||||
request_admin_relaunch,
|
request_admin_relaunch,
|
||||||
)
|
)
|
||||||
|
from .artifact_wipe import wipe_artifacts
|
||||||
|
from .browser_identity import (
|
||||||
|
COOKIE_LABELS,
|
||||||
|
COOKIE_MODES,
|
||||||
|
PERSONA_LABELS,
|
||||||
|
PERSONAS,
|
||||||
|
)
|
||||||
from .dns_leak import check_dns_leak_hint, flush_dns_cache
|
from .dns_leak import check_dns_leak_hint, flush_dns_cache
|
||||||
from .leak_audit import AuditReport, run_audit_sync
|
from .leak_audit import AuditReport, run_audit_sync
|
||||||
from .browser_launcher import (
|
from .browser_launcher import (
|
||||||
@@ -80,22 +87,26 @@ class UILogHandler(logging.Handler):
|
|||||||
|
|
||||||
|
|
||||||
# ── palette ──────────────────────────────────────────────────────────────────
|
# ── palette ──────────────────────────────────────────────────────────────────
|
||||||
BG = "#0d0d1a"
|
# Neon "blueprint terminal" theme — same overall vibe, deeper black, sharper
|
||||||
CARD = "#12122a"
|
# cyan/magenta accent, glow borders rendered via 1px border_color on cards.
|
||||||
PANEL = "#1a1a3e"
|
BG = "#070713"
|
||||||
ACCENT = "#1e3a8a"
|
CARD = "#0f0f24"
|
||||||
ACCENT2 = "#2563eb"
|
PANEL = "#171738"
|
||||||
GREEN = "#00e676"
|
ACCENT = "#3052ff" # primary action / outlines
|
||||||
RED = "#ff1744"
|
ACCENT2 = "#00e5ff" # secondary highlight / glow
|
||||||
YELLOW = "#ffab00"
|
GREEN = "#00ff9c"
|
||||||
ORANGE = "#ff6d00"
|
RED = "#ff2a6d"
|
||||||
PURPLE = "#bb86fc"
|
YELLOW = "#ffd000"
|
||||||
BLUE = "#448aff"
|
ORANGE = "#ff8a00"
|
||||||
CYAN = "#00bcd4"
|
PURPLE = "#c77dff"
|
||||||
DIM = "#4a5568"
|
BLUE = "#5b8bff"
|
||||||
TEXT = "#e2e8f0"
|
CYAN = "#00f5ff"
|
||||||
TEXT2 = "#94a3b8"
|
MAGENTA = "#ff00d6"
|
||||||
FONT = "Segoe UI"
|
DIM = "#2a2f4a"
|
||||||
|
TEXT = "#eaf2ff"
|
||||||
|
TEXT2 = "#7a8aab"
|
||||||
|
GLOW = "#0a87ff"
|
||||||
|
FONT = "Segoe UI"
|
||||||
|
|
||||||
|
|
||||||
def _ts() -> str:
|
def _ts() -> str:
|
||||||
@@ -108,10 +119,56 @@ def main() -> None:
|
|||||||
|
|
||||||
root = ctk.CTk()
|
root = ctk.CTk()
|
||||||
root.title("Proxy God v2")
|
root.title("Proxy God v2")
|
||||||
root.geometry("1080x740")
|
root.geometry("1080x760")
|
||||||
root.minsize(900, 620)
|
root.minsize(900, 620)
|
||||||
root.configure(fg_color=BG)
|
root.configure(fg_color=BG)
|
||||||
|
|
||||||
|
# ── 3D-ish neon header banner (drawn on a Canvas for a true gradient) ────
|
||||||
|
def _draw_header_banner(parent: Any) -> Any:
|
||||||
|
import tkinter as tk
|
||||||
|
canvas = tk.Canvas(
|
||||||
|
parent, height=44, highlightthickness=0, bd=0, bg=BG,
|
||||||
|
)
|
||||||
|
canvas.pack(fill="x", side="top")
|
||||||
|
|
||||||
|
def _hex(rgb: tuple[int, int, int]) -> str:
|
||||||
|
return "#" + "".join(f"{v:02x}" for v in rgb)
|
||||||
|
|
||||||
|
def _lerp(a: tuple[int, int, int], b: tuple[int, int, int], t: float) -> tuple[int, int, int]:
|
||||||
|
return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(3)) # type: ignore[return-value]
|
||||||
|
|
||||||
|
def _repaint(_event: Any = None) -> None:
|
||||||
|
canvas.delete("grad")
|
||||||
|
w = canvas.winfo_width() or 1080
|
||||||
|
h = 44
|
||||||
|
left = (8, 32, 64) # near-black blue
|
||||||
|
mid = (48, 82, 255) # ACCENT
|
||||||
|
right = (0, 229, 255) # ACCENT2 cyan
|
||||||
|
for x in range(0, w, 2):
|
||||||
|
t = x / max(1, w - 1)
|
||||||
|
c = _lerp(left, mid, t * 2) if t < 0.5 else _lerp(mid, right, (t - 0.5) * 2)
|
||||||
|
canvas.create_line(x, 0, x, h, fill=_hex(c), tags="grad")
|
||||||
|
# 1px neon underline
|
||||||
|
canvas.create_line(0, h - 1, w, h - 1, fill=CYAN, width=1, tags="grad")
|
||||||
|
# Title text + small subtitle
|
||||||
|
canvas.create_text(
|
||||||
|
18, h // 2 - 2, anchor="w",
|
||||||
|
text="P R O X Y G O D",
|
||||||
|
fill="#ffffff", font=(FONT, 16, "bold"),
|
||||||
|
tags="grad",
|
||||||
|
)
|
||||||
|
canvas.create_text(
|
||||||
|
190, h // 2 - 1, anchor="w",
|
||||||
|
text="// multi-hop · anti-fingerprint · paranoid mode",
|
||||||
|
fill="#bfeaff", font=(FONT, 9, "italic"),
|
||||||
|
tags="grad",
|
||||||
|
)
|
||||||
|
|
||||||
|
canvas.bind("<Configure>", _repaint)
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
_draw_header_banner(root)
|
||||||
|
|
||||||
ui_q: queue.Queue[dict[str, Any]] = queue.Queue()
|
ui_q: queue.Queue[dict[str, Any]] = queue.Queue()
|
||||||
svc = ChainService(notify=lambda m: ui_q.put(m))
|
svc = ChainService(notify=lambda m: ui_q.put(m))
|
||||||
s = load_settings()
|
s = load_settings()
|
||||||
@@ -847,7 +904,10 @@ def main() -> None:
|
|||||||
ctk.CTkLabel(
|
ctk.CTkLabel(
|
||||||
br_scroll, text=subtitle, font=(FONT, 10), text_color=TEXT2, wraplength=900, justify="left"
|
br_scroll, text=subtitle, font=(FONT, 10), text_color=TEXT2, wraplength=900, justify="left"
|
||||||
).pack(anchor="w", padx=8, pady=(0, 4))
|
).pack(anchor="w", padx=8, pady=(0, 4))
|
||||||
f = ctk.CTkFrame(br_scroll, fg_color=CARD, corner_radius=8)
|
f = ctk.CTkFrame(
|
||||||
|
br_scroll, fg_color=CARD, corner_radius=10,
|
||||||
|
border_width=1, border_color=GLOW,
|
||||||
|
)
|
||||||
f.pack(fill="x", padx=8, pady=(4, 8))
|
f.pack(fill="x", padx=8, pady=(4, 8))
|
||||||
return f
|
return f
|
||||||
|
|
||||||
@@ -895,6 +955,78 @@ def main() -> None:
|
|||||||
"Browser controls",
|
"Browser controls",
|
||||||
"These settings are applied each time you launch from this tab.",
|
"These settings are applied each time you launch from this tab.",
|
||||||
)
|
)
|
||||||
|
persona_card = _section_inline(
|
||||||
|
br_scroll,
|
||||||
|
"Identity persona (spoof when possible, block when not)",
|
||||||
|
"Blend-in personas mimic the most common Windows + Chrome setup so the "
|
||||||
|
"browser looks normal. Hardened mode is the classic 'block everything' "
|
||||||
|
"profile but stands out as anti-fingerprint.",
|
||||||
|
) if "_section_inline" in dir() else None
|
||||||
|
if persona_card is None:
|
||||||
|
persona_card = ctk.CTkFrame(
|
||||||
|
br_scroll, fg_color=CARD, corner_radius=10,
|
||||||
|
border_width=1, border_color=GLOW,
|
||||||
|
)
|
||||||
|
persona_card.pack(fill="x", padx=8, pady=(2, 8))
|
||||||
|
ctk.CTkLabel(
|
||||||
|
persona_card,
|
||||||
|
text="Identity persona (spoof when possible, block when not)",
|
||||||
|
font=(FONT, 12, "bold"),
|
||||||
|
text_color=ORANGE,
|
||||||
|
).pack(anchor="w", padx=12, pady=(10, 0))
|
||||||
|
ctk.CTkLabel(
|
||||||
|
persona_card,
|
||||||
|
text=(
|
||||||
|
"Blend-in personas mimic the most common Windows + Chrome setup "
|
||||||
|
"so the browser looks normal. Hardened = full anti-fingerprint."
|
||||||
|
),
|
||||||
|
font=(FONT, 9), text_color=TEXT2, justify="left", wraplength=820,
|
||||||
|
).pack(anchor="w", padx=12, pady=(0, 6))
|
||||||
|
|
||||||
|
persona_row = ctk.CTkFrame(persona_card, fg_color="transparent")
|
||||||
|
persona_row.pack(fill="x", padx=12, pady=(0, 6))
|
||||||
|
ctk.CTkLabel(persona_row, text="Persona:", font=(FONT, 11), text_color=TEXT,
|
||||||
|
width=80, anchor="w").pack(side="left")
|
||||||
|
persona_var = ctk.StringVar(
|
||||||
|
value=PERSONA_LABELS.get(s.browser_persona, PERSONA_LABELS["blend_windows_chrome"])
|
||||||
|
)
|
||||||
|
persona_menu = ctk.CTkOptionMenu(
|
||||||
|
persona_row,
|
||||||
|
values=[PERSONA_LABELS[k] for k in PERSONAS],
|
||||||
|
variable=persona_var,
|
||||||
|
fg_color=BG, button_color=ACCENT2, button_hover_color=ACCENT,
|
||||||
|
text_color=TEXT, font=(FONT, 11), width=500,
|
||||||
|
)
|
||||||
|
persona_menu.pack(side="left", padx=(4, 0))
|
||||||
|
|
||||||
|
cookie_row = ctk.CTkFrame(persona_card, fg_color="transparent")
|
||||||
|
cookie_row.pack(fill="x", padx=12, pady=(0, 10))
|
||||||
|
ctk.CTkLabel(cookie_row, text="Cookies:", font=(FONT, 11), text_color=TEXT,
|
||||||
|
width=80, anchor="w").pack(side="left")
|
||||||
|
cookie_var = ctk.StringVar(
|
||||||
|
value=COOKIE_LABELS.get(s.browser_cookie_mode, COOKIE_LABELS["block_third_party"])
|
||||||
|
)
|
||||||
|
cookie_menu = ctk.CTkOptionMenu(
|
||||||
|
cookie_row,
|
||||||
|
values=[COOKIE_LABELS[k] for k in COOKIE_MODES],
|
||||||
|
variable=cookie_var,
|
||||||
|
fg_color=BG, button_color=ACCENT2, button_hover_color=ACCENT,
|
||||||
|
text_color=TEXT, font=(FONT, 11), width=500,
|
||||||
|
)
|
||||||
|
cookie_menu.pack(side="left", padx=(4, 0))
|
||||||
|
|
||||||
|
def _persona_key() -> str:
|
||||||
|
for k in PERSONAS:
|
||||||
|
if PERSONA_LABELS[k] == persona_var.get():
|
||||||
|
return k
|
||||||
|
return "blend_windows_chrome"
|
||||||
|
|
||||||
|
def _cookie_key() -> str:
|
||||||
|
for k in COOKIE_MODES:
|
||||||
|
if COOKIE_LABELS[k] == cookie_var.get():
|
||||||
|
return k
|
||||||
|
return "block_third_party"
|
||||||
|
|
||||||
br_force_proxy_var = ctk.BooleanVar(value=s.browser_force_proxy)
|
br_force_proxy_var = ctk.BooleanVar(value=s.browser_force_proxy)
|
||||||
br_disable_webrtc_var = ctk.BooleanVar(value=s.browser_disable_webrtc)
|
br_disable_webrtc_var = ctk.BooleanVar(value=s.browser_disable_webrtc)
|
||||||
br_rfp_var = ctk.BooleanVar(value=s.browser_resist_fingerprinting)
|
br_rfp_var = ctk.BooleanVar(value=s.browser_resist_fingerprinting)
|
||||||
@@ -984,6 +1116,8 @@ def main() -> None:
|
|||||||
first_party_isolation=bool(br_fpi_var.get()),
|
first_party_isolation=bool(br_fpi_var.get()),
|
||||||
strict_tracking_protection=bool(br_strict_tp_var.get()),
|
strict_tracking_protection=bool(br_strict_tp_var.get()),
|
||||||
timezone_utc=bool(br_tz_utc_var.get()),
|
timezone_utc=bool(br_tz_utc_var.get()),
|
||||||
|
persona=_persona_key(),
|
||||||
|
cookie_mode=_cookie_key(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _refresh_browser_status() -> None:
|
def _refresh_browser_status() -> None:
|
||||||
@@ -1043,7 +1177,10 @@ def main() -> None:
|
|||||||
ctk.CTkLabel(priv_scroll, text=subtitle, font=(FONT, 10),
|
ctk.CTkLabel(priv_scroll, text=subtitle, font=(FONT, 10),
|
||||||
text_color=TEXT2, wraplength=900, justify="left").pack(
|
text_color=TEXT2, wraplength=900, justify="left").pack(
|
||||||
anchor="w", padx=8, pady=(0, 4))
|
anchor="w", padx=8, pady=(0, 4))
|
||||||
f = ctk.CTkFrame(priv_scroll, fg_color=CARD, corner_radius=8)
|
f = ctk.CTkFrame(
|
||||||
|
priv_scroll, fg_color=CARD, corner_radius=10,
|
||||||
|
border_width=1, border_color=GLOW,
|
||||||
|
)
|
||||||
f.pack(fill="x", padx=8, pady=(4, 8))
|
f.pack(fill="x", padx=8, pady=(4, 8))
|
||||||
return f
|
return f
|
||||||
|
|
||||||
@@ -1090,6 +1227,7 @@ def main() -> None:
|
|||||||
ipv6_var = ctk.BooleanVar(value=s.disable_ipv6_while_active)
|
ipv6_var = ctk.BooleanVar(value=s.disable_ipv6_while_active)
|
||||||
webrtc_var = ctk.BooleanVar(value=s.harden_webrtc_enabled)
|
webrtc_var = ctk.BooleanVar(value=s.harden_webrtc_enabled)
|
||||||
lan_var = ctk.BooleanVar(value=s.lan_lockdown_enabled)
|
lan_var = ctk.BooleanVar(value=s.lan_lockdown_enabled)
|
||||||
|
telemetry_var = ctk.BooleanVar(value=s.telemetry_kill_enabled)
|
||||||
|
|
||||||
def _priv_toggle(parent: Any, text: str, var: ctk.BooleanVar, tip: str = "") -> ctk.CTkCheckBox:
|
def _priv_toggle(parent: Any, text: str, var: ctk.BooleanVar, tip: str = "") -> ctk.CTkCheckBox:
|
||||||
cb = ctk.CTkCheckBox(
|
cb = ctk.CTkCheckBox(
|
||||||
@@ -1132,6 +1270,13 @@ def main() -> None:
|
|||||||
"Stops your machine from advertising its hostname on the local network "
|
"Stops your machine from advertising its hostname on the local network "
|
||||||
"(admin required, reversible on stop).",
|
"(admin required, reversible on stop).",
|
||||||
)
|
)
|
||||||
|
_priv_toggle(
|
||||||
|
toggles_card,
|
||||||
|
"Telemetry kill: DiagTrack, Activity History, Cortana web, ad ID",
|
||||||
|
telemetry_var,
|
||||||
|
"Stops Windows telemetry pipelines while chain is up. Reversible on stop "
|
||||||
|
"(snapshot of original state taken).",
|
||||||
|
)
|
||||||
|
|
||||||
fp_card = _priv_section(
|
fp_card = _priv_section(
|
||||||
"Device fingerprint",
|
"Device fingerprint",
|
||||||
@@ -1177,6 +1322,35 @@ def main() -> None:
|
|||||||
_btn(dns_btn_row, "Flush DNS now", _manual_dns_flush, w=110, h=28,
|
_btn(dns_btn_row, "Flush DNS now", _manual_dns_flush, w=110, h=28,
|
||||||
fg_color=DIM).pack(side="left")
|
fg_color=DIM).pack(side="left")
|
||||||
|
|
||||||
|
wipe_card = _priv_section(
|
||||||
|
"Forensic artifact wipe",
|
||||||
|
"One-button purge of common Windows breadcrumb trails. Irreversible.",
|
||||||
|
)
|
||||||
|
wipe_result_lbl = ctk.CTkLabel(
|
||||||
|
wipe_card, text="Click 'Wipe now' to purge %TEMP%, Recent, Jump Lists, "
|
||||||
|
"Prefetch (Admin), MRU lists, and the clipboard.",
|
||||||
|
font=(FONT, 10), text_color=TEXT2, wraplength=880, justify="left",
|
||||||
|
)
|
||||||
|
wipe_result_lbl.pack(anchor="w", padx=12, pady=8)
|
||||||
|
|
||||||
|
def _do_wipe() -> None:
|
||||||
|
wipe_result_lbl.configure(text="Wiping…", text_color=YELLOW)
|
||||||
|
|
||||||
|
def work() -> None:
|
||||||
|
try:
|
||||||
|
rep = wipe_artifacts(include_prefetch=True)
|
||||||
|
except Exception as e:
|
||||||
|
root.after(0, lambda: wipe_result_lbl.configure(
|
||||||
|
text=f"Wipe failed: {e}", text_color=RED))
|
||||||
|
return
|
||||||
|
root.after(0, lambda: wipe_result_lbl.configure(
|
||||||
|
text=rep.summary(), text_color=GREEN if not rep.errors else YELLOW))
|
||||||
|
|
||||||
|
threading.Thread(target=work, daemon=True).start()
|
||||||
|
|
||||||
|
_btn(wipe_card, "Wipe now", _do_wipe, w=110, h=28,
|
||||||
|
fg_color=RED, hover_color="#8a1d1d").pack(anchor="w", padx=12, pady=(0, 10))
|
||||||
|
|
||||||
audit_card = _priv_section(
|
audit_card = _priv_section(
|
||||||
"Leak audit (mission-critical)",
|
"Leak audit (mission-critical)",
|
||||||
"Probes every leak surface: IP, DNS, IPv6, WPAD, Group Policy, "
|
"Probes every leak surface: IP, DNS, IPv6, WPAD, Group Policy, "
|
||||||
@@ -1310,6 +1484,9 @@ def main() -> None:
|
|||||||
ipv6_var.set(False)
|
ipv6_var.set(False)
|
||||||
webrtc_var.set(True)
|
webrtc_var.set(True)
|
||||||
lan_var.set(False)
|
lan_var.set(False)
|
||||||
|
telemetry_var.set(False)
|
||||||
|
persona_var.set(PERSONA_LABELS["blend_windows_chrome"])
|
||||||
|
cookie_var.set(COOKIE_LABELS["block_third_party"])
|
||||||
br_force_proxy_var.set(True)
|
br_force_proxy_var.set(True)
|
||||||
br_disable_webrtc_var.set(True)
|
br_disable_webrtc_var.set(True)
|
||||||
br_rfp_var.set(True)
|
br_rfp_var.set(True)
|
||||||
@@ -1381,6 +1558,7 @@ def main() -> None:
|
|||||||
disable_ipv6_while_active=bool(ipv6_var.get()),
|
disable_ipv6_while_active=bool(ipv6_var.get()),
|
||||||
harden_webrtc_enabled=bool(webrtc_var.get()),
|
harden_webrtc_enabled=bool(webrtc_var.get()),
|
||||||
lan_lockdown_enabled=bool(lan_var.get()),
|
lan_lockdown_enabled=bool(lan_var.get()),
|
||||||
|
telemetry_kill_enabled=bool(telemetry_var.get()),
|
||||||
firefox_path=firefox_path_var.get().strip(),
|
firefox_path=firefox_path_var.get().strip(),
|
||||||
firefox_profile_dir=firefox_profile_var.get().strip(),
|
firefox_profile_dir=firefox_profile_var.get().strip(),
|
||||||
browser_clear_on_close=bool(br_clear_var.get()),
|
browser_clear_on_close=bool(br_clear_var.get()),
|
||||||
@@ -1395,6 +1573,8 @@ def main() -> None:
|
|||||||
browser_first_party_isolation=bool(br_fpi_var.get()),
|
browser_first_party_isolation=bool(br_fpi_var.get()),
|
||||||
browser_strict_tracking_protection=bool(br_strict_tp_var.get()),
|
browser_strict_tracking_protection=bool(br_strict_tp_var.get()),
|
||||||
browser_timezone_utc=bool(br_tz_utc_var.get()),
|
browser_timezone_utc=bool(br_tz_utc_var.get()),
|
||||||
|
browser_persona=_persona_key(),
|
||||||
|
browser_cookie_mode=_cookie_key(),
|
||||||
)
|
)
|
||||||
if not (1 <= ns.local_port <= 65535):
|
if not (1 <= ns.local_port <= 65535):
|
||||||
raise ValueError("Port must be 1-65535")
|
raise ValueError("Port must be 1-65535")
|
||||||
|
|||||||
218
proxy_chain_manager/artifact_wipe.py
Normal file
218
proxy_chain_manager/artifact_wipe.py
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
"""One-button forensic artifact wipe.
|
||||||
|
|
||||||
|
Surfaces purged:
|
||||||
|
|
||||||
|
• %TEMP% — per-user temp
|
||||||
|
• %SystemRoot%\\Prefetch\\*.pf (Admin only) — file-launch history
|
||||||
|
• %APPDATA%\\Microsoft\\Windows\\Recent\\* — recent files
|
||||||
|
• %APPDATA%\\Microsoft\\Windows\\Recent\\AutomaticDestinations\\* — Jump Lists
|
||||||
|
• %APPDATA%\\Microsoft\\Windows\\Recent\\CustomDestinations\\* — Jump Lists
|
||||||
|
• HKCU\\…\\Explorer\\RunMRU — Win+R history
|
||||||
|
• HKCU\\…\\Explorer\\TypedPaths — Explorer typed paths
|
||||||
|
• HKCU\\…\\Explorer\\WordWheelQuery — Start / Explorer search history
|
||||||
|
• HKCU\\…\\Explorer\\RecentDocs — recent docs MRU
|
||||||
|
• Clipboard — current contents
|
||||||
|
|
||||||
|
Counts are reported but specific filenames are never logged (this is the
|
||||||
|
opposite of what we want to leak). All deletes use ``ignore_errors=True``
|
||||||
|
because files in use by other apps are expected.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import winreg
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .firewall import is_admin
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
_REG_MRU_KEYS = (
|
||||||
|
r"Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU",
|
||||||
|
r"Software\Microsoft\Windows\CurrentVersion\Explorer\TypedPaths",
|
||||||
|
r"Software\Microsoft\Windows\CurrentVersion\Explorer\WordWheelQuery",
|
||||||
|
r"Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WipeReport:
|
||||||
|
files_deleted: int = 0
|
||||||
|
bytes_freed: int = 0
|
||||||
|
folders_skipped: int = 0
|
||||||
|
registry_keys_cleared: int = 0
|
||||||
|
clipboard_cleared: bool = False
|
||||||
|
errors: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
def summary(self) -> str:
|
||||||
|
mb = self.bytes_freed / (1024 * 1024)
|
||||||
|
return (
|
||||||
|
f"{self.files_deleted} files / {mb:.1f} MB freed, "
|
||||||
|
f"{self.registry_keys_cleared} MRU entries cleared, "
|
||||||
|
f"clipboard={'yes' if self.clipboard_cleared else 'no'}, "
|
||||||
|
f"errors={len(self.errors)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _walk_size(path: Path) -> int:
|
||||||
|
total = 0
|
||||||
|
for p in path.rglob("*"):
|
||||||
|
try:
|
||||||
|
if p.is_file():
|
||||||
|
total += p.stat().st_size
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def _purge_dir(path: Path, rep: WipeReport, keep_root: bool = True) -> None:
|
||||||
|
"""Delete contents of `path`. Preserves the directory itself when keep_root."""
|
||||||
|
if not path.exists():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
size_before = _walk_size(path)
|
||||||
|
except Exception:
|
||||||
|
size_before = 0
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for child in path.iterdir():
|
||||||
|
try:
|
||||||
|
if child.is_dir():
|
||||||
|
shutil.rmtree(child, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
child.unlink(missing_ok=True)
|
||||||
|
count += 1
|
||||||
|
except OSError:
|
||||||
|
rep.folders_skipped += 1
|
||||||
|
|
||||||
|
if not keep_root:
|
||||||
|
try:
|
||||||
|
path.rmdir()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
rep.files_deleted += count
|
||||||
|
try:
|
||||||
|
size_after = _walk_size(path)
|
||||||
|
except Exception:
|
||||||
|
size_after = 0
|
||||||
|
rep.bytes_freed += max(0, size_before - size_after)
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_mru_key(path: str, rep: WipeReport) -> None:
|
||||||
|
"""Remove every value under a Run/Typed/Search MRU key."""
|
||||||
|
try:
|
||||||
|
with winreg.OpenKey(
|
||||||
|
winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_ALL_ACCESS
|
||||||
|
) as key:
|
||||||
|
count = 0
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
name, _, _ = winreg.EnumValue(key, 0)
|
||||||
|
try:
|
||||||
|
winreg.DeleteValue(key, name)
|
||||||
|
count += 1
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
# Recurse into subkeys (e.g. RecentDocs/.png)
|
||||||
|
sub_count = 0
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
sub_count += 1
|
||||||
|
sub_name = winreg.EnumKey(key, 0)
|
||||||
|
try:
|
||||||
|
winreg.DeleteKey(key, sub_name)
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
if sub_count > 200: # safety cap
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if count or sub_count:
|
||||||
|
rep.registry_keys_cleared += 1
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_clipboard(rep: WipeReport) -> None:
|
||||||
|
try:
|
||||||
|
user32 = ctypes.windll.user32 # type: ignore[attr-defined]
|
||||||
|
if user32.OpenClipboard(None):
|
||||||
|
try:
|
||||||
|
user32.EmptyClipboard()
|
||||||
|
rep.clipboard_cleared = True
|
||||||
|
finally:
|
||||||
|
user32.CloseClipboard()
|
||||||
|
except Exception as e:
|
||||||
|
rep.errors.append(f"clipboard: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def wipe_artifacts(include_prefetch: bool = True) -> WipeReport:
|
||||||
|
rep = WipeReport()
|
||||||
|
appdata = Path(os.environ.get("APPDATA", "")) if os.environ.get("APPDATA") else None
|
||||||
|
temp = Path(os.environ.get("TEMP", "")) if os.environ.get("TEMP") else None
|
||||||
|
sysroot = Path(os.environ.get("SystemRoot", r"C:\Windows"))
|
||||||
|
|
||||||
|
# %TEMP%
|
||||||
|
if temp and temp.exists():
|
||||||
|
_purge_dir(temp, rep, keep_root=True)
|
||||||
|
|
||||||
|
# Prefetch (Admin)
|
||||||
|
if include_prefetch and is_admin():
|
||||||
|
pf = sysroot / "Prefetch"
|
||||||
|
if pf.exists():
|
||||||
|
count = 0
|
||||||
|
for f in pf.glob("*.pf"):
|
||||||
|
try:
|
||||||
|
sz = f.stat().st_size
|
||||||
|
f.unlink(missing_ok=True)
|
||||||
|
count += 1
|
||||||
|
rep.bytes_freed += sz
|
||||||
|
except OSError as e:
|
||||||
|
rep.errors.append(f"prefetch: {e}")
|
||||||
|
rep.files_deleted += count
|
||||||
|
|
||||||
|
# Recent / Jump Lists
|
||||||
|
if appdata:
|
||||||
|
recent = appdata / "Microsoft" / "Windows" / "Recent"
|
||||||
|
if recent.exists():
|
||||||
|
_purge_dir(recent / "AutomaticDestinations", rep, keep_root=True)
|
||||||
|
_purge_dir(recent / "CustomDestinations", rep, keep_root=True)
|
||||||
|
_purge_dir(recent, rep, keep_root=True)
|
||||||
|
|
||||||
|
# MRU registry keys
|
||||||
|
for path in _REG_MRU_KEYS:
|
||||||
|
_clear_mru_key(path, rep)
|
||||||
|
|
||||||
|
# Clipboard
|
||||||
|
_clear_clipboard(rep)
|
||||||
|
|
||||||
|
# Trigger Explorer's "Clear recent items" via Shell API (covers Win10/11
|
||||||
|
# Quick Access pinned ↔ recent lists not covered by raw file delete).
|
||||||
|
try:
|
||||||
|
shell32 = ctypes.windll.shell32 # type: ignore[attr-defined]
|
||||||
|
# SHCNE_ASSOCCHANGED tells Explorer to refresh its caches.
|
||||||
|
shell32.SHChangeNotify(0x08000000, 0x0000, None, None)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Best-effort Defender quick-scan history flush — non-fatal.
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["powershell", "-NoProfile", "-NonInteractive", "-Command",
|
||||||
|
"Clear-RecycleBin -Force -ErrorAction SilentlyContinue"],
|
||||||
|
capture_output=True, text=True, timeout=20,
|
||||||
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return rep
|
||||||
141
proxy_chain_manager/browser_identity.py
Normal file
141
proxy_chain_manager/browser_identity.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
"""Browser identity personas + cookie policies.
|
||||||
|
|
||||||
|
The "lock everything down" approach (Firefox RFP + WebRTC off + cookies blocked)
|
||||||
|
keeps you safe from fingerprinting but makes the machine *look weird* — sites
|
||||||
|
flag you as suspicious and ban / captcha you. Worse: very few users use that
|
||||||
|
config, so RFP-on is itself a fingerprint.
|
||||||
|
|
||||||
|
Two strategies are exposed:
|
||||||
|
|
||||||
|
• **Blend-in personas** — mimic the most common Windows-Chrome-US-en setup.
|
||||||
|
JS / cookies / WebGL all work; everything looks normal; only WebRTC is
|
||||||
|
blocked (because that leaks the real IP behind the proxy). The proxy
|
||||||
|
chain itself does the actual hiding.
|
||||||
|
|
||||||
|
• **Hardened** — full RFP / blocked WebRTC / strict tracking / FPI. Stand-out
|
||||||
|
but maximum block. Same behavior as the original profile.
|
||||||
|
|
||||||
|
Cookie modes are independent of persona and let the user pick per-session
|
||||||
|
behavior from a dropdown.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
# Order matters: dropdown order in the UI.
|
||||||
|
PERSONAS: list[str] = [
|
||||||
|
"blend_windows_chrome",
|
||||||
|
"blend_windows_firefox",
|
||||||
|
"blend_mac_safari",
|
||||||
|
"hardened",
|
||||||
|
"custom",
|
||||||
|
]
|
||||||
|
|
||||||
|
PERSONA_LABELS: dict[str, str] = {
|
||||||
|
"blend_windows_chrome": "Blend in — Windows 10 + Chrome (US-en, most common)",
|
||||||
|
"blend_windows_firefox": "Blend in — Windows 10 + Firefox (US-en)",
|
||||||
|
"blend_mac_safari": "Blend in — macOS + Safari (US-en)",
|
||||||
|
"hardened": "Hardened — block + RFP (stands out; max anti-fingerprint)",
|
||||||
|
"custom": "Custom — use individual hardening toggles",
|
||||||
|
}
|
||||||
|
|
||||||
|
COOKIE_MODES: list[str] = [
|
||||||
|
"accept_all",
|
||||||
|
"block_third_party",
|
||||||
|
"block_third_party_trackers",
|
||||||
|
"session_only",
|
||||||
|
"block_all",
|
||||||
|
]
|
||||||
|
|
||||||
|
COOKIE_LABELS: dict[str, str] = {
|
||||||
|
"accept_all": "Accept all cookies (most sites work)",
|
||||||
|
"block_third_party": "Block 3rd-party cookies (recommended)",
|
||||||
|
"block_third_party_trackers": "Block 3rd-party trackers only (Firefox default-strict)",
|
||||||
|
"session_only": "Session only — clear on close",
|
||||||
|
"block_all": "Block ALL cookies (breaks logins)",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Firefox network.cookie.cookieBehavior values
|
||||||
|
_COOKIE_BEHAVIOR = {
|
||||||
|
"accept_all": 0,
|
||||||
|
"block_third_party": 1,
|
||||||
|
"block_third_party_trackers": 4,
|
||||||
|
"session_only": 0,
|
||||||
|
"block_all": 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Persona:
|
||||||
|
"""A user-agent / locale / timezone / screen tuple that mimics a real,
|
||||||
|
common configuration. All fields are deliberately popular values so the
|
||||||
|
persona blends in.
|
||||||
|
"""
|
||||||
|
key: str
|
||||||
|
user_agent: str
|
||||||
|
accept_language: str
|
||||||
|
timezone: str # IANA tz; used only when persona drives env TZ
|
||||||
|
screen_w: int
|
||||||
|
screen_h: int
|
||||||
|
platform: str # navigator.platform
|
||||||
|
locale: str # general.useragent.locale equivalent
|
||||||
|
|
||||||
|
|
||||||
|
PERSONA_DATA: dict[str, Persona] = {
|
||||||
|
"blend_windows_chrome": Persona(
|
||||||
|
key="blend_windows_chrome",
|
||||||
|
# Stable Chrome on Win10 x64 — by far the most common UA on the web.
|
||||||
|
user_agent=(
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||||
|
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||||
|
),
|
||||||
|
accept_language="en-US,en;q=0.9",
|
||||||
|
timezone="America/New_York",
|
||||||
|
screen_w=1920,
|
||||||
|
screen_h=1080,
|
||||||
|
platform="Win32",
|
||||||
|
locale="en-US",
|
||||||
|
),
|
||||||
|
"blend_windows_firefox": Persona(
|
||||||
|
key="blend_windows_firefox",
|
||||||
|
user_agent=(
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) "
|
||||||
|
"Gecko/20100101 Firefox/128.0"
|
||||||
|
),
|
||||||
|
accept_language="en-US,en;q=0.5",
|
||||||
|
timezone="America/New_York",
|
||||||
|
screen_w=1920,
|
||||||
|
screen_h=1080,
|
||||||
|
platform="Win32",
|
||||||
|
locale="en-US",
|
||||||
|
),
|
||||||
|
"blend_mac_safari": Persona(
|
||||||
|
key="blend_mac_safari",
|
||||||
|
user_agent=(
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||||
|
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15"
|
||||||
|
),
|
||||||
|
accept_language="en-US,en;q=0.9",
|
||||||
|
timezone="America/Los_Angeles",
|
||||||
|
screen_w=1680,
|
||||||
|
screen_h=1050,
|
||||||
|
platform="MacIntel",
|
||||||
|
locale="en-US",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def cookie_behavior_value(mode: str) -> int:
|
||||||
|
return _COOKIE_BEHAVIOR.get(mode, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def cookie_session_only(mode: str) -> bool:
|
||||||
|
return mode == "session_only"
|
||||||
|
|
||||||
|
|
||||||
|
def get_persona(key: str) -> Persona | None:
|
||||||
|
return PERSONA_DATA.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
def is_blend_persona(key: str) -> bool:
|
||||||
|
return key.startswith("blend_")
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .browser_profile import FirefoxHardening, ensure_firefox_profile
|
from .browser_profile import FirefoxHardening, ensure_firefox_profile, persona_env
|
||||||
from .paths import app_data_dir
|
from .paths import app_data_dir
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -29,6 +30,8 @@ class BrowserConfig:
|
|||||||
first_party_isolation: bool = True
|
first_party_isolation: bool = True
|
||||||
strict_tracking_protection: bool = True
|
strict_tracking_protection: bool = True
|
||||||
timezone_utc: bool = True
|
timezone_utc: bool = True
|
||||||
|
persona: str = "blend_windows_chrome"
|
||||||
|
cookie_mode: str = "block_third_party"
|
||||||
|
|
||||||
|
|
||||||
def default_firefox_path() -> str:
|
def default_firefox_path() -> str:
|
||||||
@@ -80,6 +83,8 @@ class BrowserSession:
|
|||||||
strict_tracking_protection=cfg.strict_tracking_protection,
|
strict_tracking_protection=cfg.strict_tracking_protection,
|
||||||
clear_on_shutdown=cfg.clear_on_close,
|
clear_on_shutdown=cfg.clear_on_close,
|
||||||
timezone_utc=cfg.timezone_utc,
|
timezone_utc=cfg.timezone_utc,
|
||||||
|
persona_key=cfg.persona,
|
||||||
|
cookie_mode=cfg.cookie_mode,
|
||||||
)
|
)
|
||||||
if cfg.lock_managed_profile:
|
if cfg.lock_managed_profile:
|
||||||
ensure_firefox_profile(profile_dir, proxy_host, int(proxy_port), hard)
|
ensure_firefox_profile(profile_dir, proxy_host, int(proxy_port), hard)
|
||||||
@@ -89,6 +94,8 @@ class BrowserSession:
|
|||||||
args = [str(exe), "-no-remote", "-profile", str(profile_dir)]
|
args = [str(exe), "-no-remote", "-profile", str(profile_dir)]
|
||||||
else:
|
else:
|
||||||
args = [str(exe)]
|
args = [str(exe)]
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.update(persona_env(hard))
|
||||||
try:
|
try:
|
||||||
self._proc = subprocess.Popen(
|
self._proc = subprocess.Popen(
|
||||||
args,
|
args,
|
||||||
@@ -96,6 +103,7 @@ class BrowserSession:
|
|||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
text=True,
|
text=True,
|
||||||
|
env=env,
|
||||||
)
|
)
|
||||||
# Smoke-check: catch immediate startup crashes and report clearly.
|
# Smoke-check: catch immediate startup crashes and report clearly.
|
||||||
time.sleep(0.4)
|
time.sleep(0.4)
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .browser_identity import (
|
||||||
|
Persona,
|
||||||
|
cookie_behavior_value,
|
||||||
|
cookie_session_only,
|
||||||
|
get_persona,
|
||||||
|
is_blend_persona,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FirefoxHardening:
|
class FirefoxHardening:
|
||||||
|
# Proxy & network always-on
|
||||||
force_proxy: bool = True
|
force_proxy: bool = True
|
||||||
disable_webrtc: bool = True
|
disable_webrtc: bool = True # always strongly recommended; leaks real IP
|
||||||
|
|
||||||
|
# Hardened block mode — overrides persona spoofing
|
||||||
resist_fingerprinting: bool = True
|
resist_fingerprinting: bool = True
|
||||||
disable_telemetry: bool = True
|
disable_telemetry: bool = True
|
||||||
first_party_isolation: bool = True
|
first_party_isolation: bool = True
|
||||||
@@ -15,6 +26,46 @@ class FirefoxHardening:
|
|||||||
clear_on_shutdown: bool = True
|
clear_on_shutdown: bool = True
|
||||||
timezone_utc: 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:
|
def _bool(v: bool) -> str:
|
||||||
return "true" if v else "false"
|
return "true" if v else "false"
|
||||||
@@ -25,8 +76,10 @@ def build_user_js(
|
|||||||
proxy_port: int,
|
proxy_port: int,
|
||||||
hard: FirefoxHardening,
|
hard: FirefoxHardening,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
hard = apply_persona(hard)
|
||||||
lines: list[str] = [
|
lines: list[str] = [
|
||||||
"// Managed by Proxy God. Changes are overwritten on next launch.",
|
"// 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.normandy.enabled", false);',
|
||||||
'user_pref("app.shield.optoutstudies.enabled", false);',
|
'user_pref("app.shield.optoutstudies.enabled", false);',
|
||||||
'user_pref("browser.newtabpage.activity-stream.feeds.telemetry", false);',
|
'user_pref("browser.newtabpage.activity-stream.feeds.telemetry", false);',
|
||||||
@@ -45,14 +98,14 @@ def build_user_js(
|
|||||||
'user_pref("network.trr.mode", 5);',
|
'user_pref("network.trr.mode", 5);',
|
||||||
'user_pref("network.captive-portal-service.enabled", false);',
|
'user_pref("network.captive-portal-service.enabled", false);',
|
||||||
'user_pref("geo.enabled", false);',
|
'user_pref("geo.enabled", false);',
|
||||||
'user_pref("media.peerconnection.enabled", false);',
|
|
||||||
'user_pref("media.navigator.enabled", false);',
|
'user_pref("media.navigator.enabled", false);',
|
||||||
'user_pref("dom.battery.enabled", false);',
|
'user_pref("dom.battery.enabled", false);',
|
||||||
'user_pref("dom.gamepad.enabled", false);',
|
'user_pref("dom.gamepad.enabled", false);',
|
||||||
'user_pref("dom.netinfo.enabled", false);',
|
'user_pref("dom.netinfo.enabled", false);',
|
||||||
'user_pref("webgl.disabled", false);',
|
# WebGL allowed by default (blocking it makes you stand out massively)
|
||||||
'user_pref("webgl.enable-debug-renderer-info", false);',
|
'user_pref("webgl.enable-debug-renderer-info", false);',
|
||||||
]
|
]
|
||||||
|
|
||||||
if hard.force_proxy:
|
if hard.force_proxy:
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
@@ -71,7 +124,9 @@ def build_user_js(
|
|||||||
'user_pref("network.proxy.no_proxies_on", "");',
|
'user_pref("network.proxy.no_proxies_on", "");',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if hard.disable_webrtc:
|
if hard.disable_webrtc:
|
||||||
|
# Off regardless of persona — WebRTC leaks real IP through STUN.
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
'user_pref("media.peerconnection.enabled", false);',
|
'user_pref("media.peerconnection.enabled", false);',
|
||||||
@@ -79,6 +134,7 @@ def build_user_js(
|
|||||||
'user_pref("media.peerconnection.ice.no_host", true);',
|
'user_pref("media.peerconnection.ice.no_host", true);',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if hard.resist_fingerprinting:
|
if hard.resist_fingerprinting:
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
@@ -88,10 +144,13 @@ def build_user_js(
|
|||||||
'user_pref("privacy.window.maxInnerHeight", 900);',
|
'user_pref("privacy.window.maxInnerHeight", 900);',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if hard.first_party_isolation:
|
if hard.first_party_isolation:
|
||||||
lines.append('user_pref("privacy.firstparty.isolate", true);')
|
lines.append('user_pref("privacy.firstparty.isolate", true);')
|
||||||
|
|
||||||
if hard.disable_telemetry:
|
if hard.disable_telemetry:
|
||||||
lines.append('user_pref("browser.send_pings", false);')
|
lines.append('user_pref("browser.send_pings", false);')
|
||||||
|
|
||||||
if hard.strict_tracking_protection:
|
if hard.strict_tracking_protection:
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
@@ -99,6 +158,38 @@ def build_user_js(
|
|||||||
'user_pref("privacy.trackingprotection.pbmode.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 (persona-independent)
|
||||||
|
behavior = cookie_behavior_value(hard.cookie_mode)
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f'user_pref("network.cookie.cookieBehavior", {behavior});',
|
||||||
|
# 0 = expire normally, 2 = current session only
|
||||||
|
f'user_pref("network.cookie.lifetimePolicy", {2 if cookie_session_only(hard.cookie_mode) else 0});',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
if hard.clear_on_shutdown:
|
if hard.clear_on_shutdown:
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
@@ -111,12 +202,26 @@ def build_user_js(
|
|||||||
'user_pref("privacy.clearOnShutdown.sessions", true);',
|
'user_pref("privacy.clearOnShutdown.sessions", true);',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if hard.timezone_utc:
|
if hard.timezone_utc:
|
||||||
lines.append('user_pref("privacy.resistFingerprinting.reduceTimerPrecision", true);')
|
lines.append('user_pref("privacy.resistFingerprinting.reduceTimerPrecision", true);')
|
||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
return "\n".join(lines)
|
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(
|
def ensure_firefox_profile(
|
||||||
profile_dir: Path,
|
profile_dir: Path,
|
||||||
proxy_host: str,
|
proxy_host: str,
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ class Settings:
|
|||||||
disable_ipv6_while_active: bool = False # disable IPv6 bindings while chain runs (Admin)
|
disable_ipv6_while_active: bool = False # disable IPv6 bindings while chain runs (Admin)
|
||||||
harden_webrtc_enabled: bool = False # Chrome/Edge WebRTC policy (Admin)
|
harden_webrtc_enabled: bool = False # Chrome/Edge WebRTC policy (Admin)
|
||||||
lan_lockdown_enabled: bool = False # kill LLMNR/NetBIOS/mDNS while chain runs
|
lan_lockdown_enabled: bool = False # kill LLMNR/NetBIOS/mDNS while chain runs
|
||||||
|
telemetry_kill_enabled: bool = False # disable DiagTrack + Activity History (Admin)
|
||||||
|
|
||||||
# ── hardened browser ────────────────────────────────────────────────────
|
# ── hardened browser ────────────────────────────────────────────────────
|
||||||
firefox_path: str = ""
|
firefox_path: str = ""
|
||||||
@@ -210,6 +211,8 @@ class Settings:
|
|||||||
browser_first_party_isolation: bool = True
|
browser_first_party_isolation: bool = True
|
||||||
browser_strict_tracking_protection: bool = True
|
browser_strict_tracking_protection: bool = True
|
||||||
browser_timezone_utc: bool = True
|
browser_timezone_utc: bool = True
|
||||||
|
browser_persona: str = "blend_windows_chrome"
|
||||||
|
browser_cookie_mode: str = "block_third_party"
|
||||||
|
|
||||||
# ── sources ───────────────────────────────────────────────────────────
|
# ── sources ───────────────────────────────────────────────────────────
|
||||||
sources: list[str] = field(
|
sources: list[str] = field(
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ from .privacy_lan import (
|
|||||||
engage_lan_lockdown,
|
engage_lan_lockdown,
|
||||||
restore_lan,
|
restore_lan,
|
||||||
)
|
)
|
||||||
|
from .telemetry_kill import (
|
||||||
|
TelemetrySnapshot,
|
||||||
|
engage_telemetry_kill,
|
||||||
|
restore_telemetry,
|
||||||
|
)
|
||||||
from .sysproxy import (
|
from .sysproxy import (
|
||||||
clear_system_proxy,
|
clear_system_proxy,
|
||||||
detect_policy_overrides,
|
detect_policy_overrides,
|
||||||
@@ -82,6 +87,7 @@ class ChainService:
|
|||||||
self._ipv6_adapters: list[str] = []
|
self._ipv6_adapters: list[str] = []
|
||||||
self._webrtc_was_applied: bool = False
|
self._webrtc_was_applied: bool = False
|
||||||
self._lan_snap: LanSnapshot | None = None
|
self._lan_snap: LanSnapshot | None = None
|
||||||
|
self._telemetry_snap: TelemetrySnapshot | None = None
|
||||||
|
|
||||||
def _manual_exit_url(self) -> str | None:
|
def _manual_exit_url(self) -> str | None:
|
||||||
u = normalize_proxy_url(self._settings.manual_exit_proxy)
|
u = normalize_proxy_url(self._settings.manual_exit_proxy)
|
||||||
@@ -181,6 +187,16 @@ class ChainService:
|
|||||||
elif s.lan_lockdown_enabled:
|
elif s.lan_lockdown_enabled:
|
||||||
self._notify({"type": "log", "text": "LAN lockdown enabled but not Admin — skipped."})
|
self._notify({"type": "log", "text": "LAN lockdown enabled but not Admin — skipped."})
|
||||||
|
|
||||||
|
if s.telemetry_kill_enabled and is_admin():
|
||||||
|
tsnap, tlogs = engage_telemetry_kill()
|
||||||
|
self._telemetry_snap = tsnap
|
||||||
|
for ln in tlogs[:8]:
|
||||||
|
self._notify({"type": "log", "text": f"Telemetry: {ln}"})
|
||||||
|
if len(tlogs) > 8:
|
||||||
|
self._notify({"type": "log", "text": f"Telemetry: … +{len(tlogs) - 8} more changes."})
|
||||||
|
elif s.telemetry_kill_enabled:
|
||||||
|
self._notify({"type": "log", "text": "Telemetry kill enabled but not Admin — skipped."})
|
||||||
|
|
||||||
def _restore_privacy(self) -> None:
|
def _restore_privacy(self) -> None:
|
||||||
if self._mac_originals:
|
if self._mac_originals:
|
||||||
for ln in restore_macs(self._mac_originals):
|
for ln in restore_macs(self._mac_originals):
|
||||||
@@ -202,6 +218,10 @@ class ChainService:
|
|||||||
for ln in restore_lan(self._lan_snap):
|
for ln in restore_lan(self._lan_snap):
|
||||||
self._notify({"type": "log", "text": f"LAN: {ln}"})
|
self._notify({"type": "log", "text": f"LAN: {ln}"})
|
||||||
self._lan_snap = None
|
self._lan_snap = None
|
||||||
|
if self._telemetry_snap is not None and is_admin():
|
||||||
|
for ln in restore_telemetry(self._telemetry_snap)[:8]:
|
||||||
|
self._notify({"type": "log", "text": f"Telemetry: {ln}"})
|
||||||
|
self._telemetry_snap = None
|
||||||
|
|
||||||
def _run_thread(self) -> None:
|
def _run_thread(self) -> None:
|
||||||
try:
|
try:
|
||||||
|
|||||||
292
proxy_chain_manager/telemetry_kill.py
Normal file
292
proxy_chain_manager/telemetry_kill.py
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
"""Reversibly disable Windows telemetry / data collection.
|
||||||
|
|
||||||
|
Targets, in order of leak severity:
|
||||||
|
|
||||||
|
• DiagTrack service — primary telemetry pipeline ("Connected User
|
||||||
|
Experiences and Telemetry"). Phones home every
|
||||||
|
few minutes.
|
||||||
|
• dmwappushservice — WAP Push routing (DSMS), historically tied to
|
||||||
|
DiagTrack. Disabled when present.
|
||||||
|
• AllowTelemetry (HKLM) — DataCollection policy. 0 = lowest tier.
|
||||||
|
• Activity History — EnableActivityFeed / Upload / PublishUserActivities.
|
||||||
|
• Advertising ID — HKCU\\…\\AdvertisingInfo\\Enabled.
|
||||||
|
• Cortana Bing search — HKCU\\…\\Search\\BingSearchEnabled.
|
||||||
|
• Scheduled tasks — Compatibility Appraiser + CEIP Consolidator
|
||||||
|
+ UsbCeip.
|
||||||
|
|
||||||
|
Every change is snapshotted before mutation; ``restore_telemetry`` rolls back.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import winreg
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from .firewall import is_admin
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_SERVICES = ("DiagTrack", "dmwappushservice")
|
||||||
|
|
||||||
|
_SCHED_TASKS = (
|
||||||
|
r"\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser",
|
||||||
|
r"\Microsoft\Windows\Customer Experience Improvement Program\Consolidator",
|
||||||
|
r"\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip",
|
||||||
|
)
|
||||||
|
|
||||||
|
# (hive, path, value_name, dword_when_killed)
|
||||||
|
_REG_KILLS = (
|
||||||
|
(winreg.HKEY_LOCAL_MACHINE,
|
||||||
|
r"SOFTWARE\Policies\Microsoft\Windows\DataCollection",
|
||||||
|
"AllowTelemetry", 0),
|
||||||
|
(winreg.HKEY_LOCAL_MACHINE,
|
||||||
|
r"SOFTWARE\Policies\Microsoft\Windows\System",
|
||||||
|
"EnableActivityFeed", 0),
|
||||||
|
(winreg.HKEY_LOCAL_MACHINE,
|
||||||
|
r"SOFTWARE\Policies\Microsoft\Windows\System",
|
||||||
|
"PublishUserActivities", 0),
|
||||||
|
(winreg.HKEY_LOCAL_MACHINE,
|
||||||
|
r"SOFTWARE\Policies\Microsoft\Windows\System",
|
||||||
|
"UploadUserActivities", 0),
|
||||||
|
(winreg.HKEY_LOCAL_MACHINE,
|
||||||
|
r"SOFTWARE\Policies\Microsoft\Windows\CloudContent",
|
||||||
|
"DisableWindowsConsumerFeatures", 1),
|
||||||
|
(winreg.HKEY_CURRENT_USER,
|
||||||
|
r"Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo",
|
||||||
|
"Enabled", 0),
|
||||||
|
(winreg.HKEY_CURRENT_USER,
|
||||||
|
r"Software\Microsoft\Windows\CurrentVersion\Search",
|
||||||
|
"BingSearchEnabled", 0),
|
||||||
|
(winreg.HKEY_CURRENT_USER,
|
||||||
|
r"Software\Microsoft\Windows\CurrentVersion\Search",
|
||||||
|
"CortanaConsent", 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TelemetrySnapshot:
|
||||||
|
service_state: dict[str, str] = field(default_factory=dict) # name -> 'auto'|'manual'|'disabled'|'absent'
|
||||||
|
sched_tasks_state: dict[str, bool] = field(default_factory=dict) # task -> was_enabled
|
||||||
|
reg_values: list[dict] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(args: list[str], timeout: float = 12.0) -> tuple[int, str, str]:
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
args, capture_output=True, text=True, timeout=timeout,
|
||||||
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||||
|
)
|
||||||
|
return r.returncode, r.stdout or "", r.stderr or ""
|
||||||
|
except Exception as e:
|
||||||
|
return 1, "", str(e)
|
||||||
|
|
||||||
|
|
||||||
|
# ── service helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _service_start_type(name: str) -> str:
|
||||||
|
code, out, _ = _run(["sc", "qc", name])
|
||||||
|
if code != 0:
|
||||||
|
return "absent"
|
||||||
|
for ln in out.splitlines():
|
||||||
|
s = ln.strip().lower()
|
||||||
|
if "start_type" in s:
|
||||||
|
if "2" in s or "auto" in s:
|
||||||
|
return "auto"
|
||||||
|
if "3" in s or "demand" in s or "manual" in s:
|
||||||
|
return "manual"
|
||||||
|
if "4" in s or "disabled" in s:
|
||||||
|
return "disabled"
|
||||||
|
return "absent"
|
||||||
|
|
||||||
|
|
||||||
|
def _set_service_disabled(name: str) -> bool:
|
||||||
|
_run(["sc", "stop", name], timeout=15)
|
||||||
|
code, _, _ = _run(["sc", "config", name, "start=", "disabled"])
|
||||||
|
return code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def _set_service_start(name: str, want: str) -> bool:
|
||||||
|
start_value = {"auto": "auto", "manual": "demand", "disabled": "disabled"}.get(want)
|
||||||
|
if not start_value:
|
||||||
|
return False
|
||||||
|
_run(["sc", "stop", name], timeout=10)
|
||||||
|
code, _, _ = _run(["sc", "config", name, "start=", start_value])
|
||||||
|
if want == "auto":
|
||||||
|
_run(["sc", "start", name], timeout=15)
|
||||||
|
return code == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ── scheduled-task helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _task_is_enabled(name: str) -> bool | None:
|
||||||
|
code, out, _ = _run(["schtasks", "/Query", "/TN", name])
|
||||||
|
if code != 0:
|
||||||
|
return None
|
||||||
|
for ln in out.splitlines():
|
||||||
|
s = ln.strip().lower()
|
||||||
|
if s.startswith("status:") or "ready" in s or "disabled" in s:
|
||||||
|
if "disabled" in s:
|
||||||
|
return False
|
||||||
|
if "ready" in s or "running" in s:
|
||||||
|
return True
|
||||||
|
return True # assume ready when query succeeded
|
||||||
|
|
||||||
|
|
||||||
|
def _set_task_state(name: str, enable: bool) -> bool:
|
||||||
|
code, _, _ = _run([
|
||||||
|
"schtasks", "/Change", "/TN", name,
|
||||||
|
"/Enable" if enable else "/Disable",
|
||||||
|
])
|
||||||
|
return code == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ── registry helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _read_reg_dword(hive: int, path: str, name: str) -> tuple[bool, int | None]:
|
||||||
|
"""Returns (present, value)."""
|
||||||
|
try:
|
||||||
|
with winreg.OpenKey(hive, path, 0, winreg.KEY_QUERY_VALUE) as key:
|
||||||
|
v, _ = winreg.QueryValueEx(key, name)
|
||||||
|
return True, int(v)
|
||||||
|
except OSError:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def _write_reg_dword(hive: int, path: str, name: str, value: int) -> bool:
|
||||||
|
try:
|
||||||
|
with winreg.CreateKeyEx(hive, path, 0, winreg.KEY_SET_VALUE) as key:
|
||||||
|
winreg.SetValueEx(key, name, 0, winreg.REG_DWORD, value)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_reg_value(hive: int, path: str, name: str) -> None:
|
||||||
|
try:
|
||||||
|
with winreg.OpenKey(hive, path, 0, winreg.KEY_SET_VALUE) as key:
|
||||||
|
winreg.DeleteValue(key, name)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ── public API ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def telemetry_status() -> dict[str, str]:
|
||||||
|
"""Read-only snapshot for the audit panel."""
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
for svc in _SERVICES:
|
||||||
|
out[f"svc.{svc}"] = _service_start_type(svc)
|
||||||
|
pres, val = _read_reg_dword(
|
||||||
|
winreg.HKEY_LOCAL_MACHINE,
|
||||||
|
r"SOFTWARE\Policies\Microsoft\Windows\DataCollection",
|
||||||
|
"AllowTelemetry",
|
||||||
|
)
|
||||||
|
out["AllowTelemetry"] = "0 (killed)" if pres and val == 0 else (
|
||||||
|
f"{val}" if pres else "unset"
|
||||||
|
)
|
||||||
|
pres, val = _read_reg_dword(
|
||||||
|
winreg.HKEY_CURRENT_USER,
|
||||||
|
r"Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo",
|
||||||
|
"Enabled",
|
||||||
|
)
|
||||||
|
out["AdvertisingID"] = "off" if pres and val == 0 else (
|
||||||
|
"on" if pres and val == 1 else "unset"
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def engage_telemetry_kill() -> tuple[TelemetrySnapshot | None, list[str]]:
|
||||||
|
logs: list[str] = []
|
||||||
|
if not is_admin():
|
||||||
|
return None, ["Telemetry kill skipped — needs Administrator."]
|
||||||
|
snap = TelemetrySnapshot()
|
||||||
|
|
||||||
|
for svc in _SERVICES:
|
||||||
|
st = _service_start_type(svc)
|
||||||
|
snap.service_state[svc] = st
|
||||||
|
if st == "absent":
|
||||||
|
continue
|
||||||
|
if _set_service_disabled(svc):
|
||||||
|
logs.append(f"Service '{svc}' stopped + disabled (was {st}).")
|
||||||
|
else:
|
||||||
|
logs.append(f"Service '{svc}' could not be disabled.")
|
||||||
|
|
||||||
|
for task in _SCHED_TASKS:
|
||||||
|
was = _task_is_enabled(task)
|
||||||
|
if was is None:
|
||||||
|
continue
|
||||||
|
snap.sched_tasks_state[task] = was
|
||||||
|
if was:
|
||||||
|
if _set_task_state(task, enable=False):
|
||||||
|
logs.append(f"Scheduled task disabled: {task}")
|
||||||
|
|
||||||
|
for hive, path, name, kill_val in _REG_KILLS:
|
||||||
|
present, prev = _read_reg_dword(hive, path, name)
|
||||||
|
snap.reg_values.append({
|
||||||
|
"hive": "HKLM" if hive == winreg.HKEY_LOCAL_MACHINE else "HKCU",
|
||||||
|
"path": path,
|
||||||
|
"name": name,
|
||||||
|
"present": present,
|
||||||
|
"prev": prev,
|
||||||
|
})
|
||||||
|
if _write_reg_dword(hive, path, name, kill_val):
|
||||||
|
logs.append(f"reg: {('HKLM' if hive == winreg.HKEY_LOCAL_MACHINE else 'HKCU')}\\{path}\\{name} → {kill_val}")
|
||||||
|
else:
|
||||||
|
logs.append(f"reg write failed: {path}\\{name}")
|
||||||
|
|
||||||
|
return snap, logs
|
||||||
|
|
||||||
|
|
||||||
|
def restore_telemetry(snap: TelemetrySnapshot | None) -> list[str]:
|
||||||
|
if snap is None or not is_admin():
|
||||||
|
return []
|
||||||
|
logs: list[str] = []
|
||||||
|
|
||||||
|
for svc, prev in snap.service_state.items():
|
||||||
|
if prev == "absent":
|
||||||
|
continue
|
||||||
|
if _set_service_start(svc, prev):
|
||||||
|
logs.append(f"Service '{svc}' restored → {prev}.")
|
||||||
|
|
||||||
|
for task, was in snap.sched_tasks_state.items():
|
||||||
|
if was:
|
||||||
|
_set_task_state(task, enable=True)
|
||||||
|
logs.append(f"Scheduled task re-enabled: {task}")
|
||||||
|
|
||||||
|
for entry in snap.reg_values:
|
||||||
|
hive = winreg.HKEY_LOCAL_MACHINE if entry["hive"] == "HKLM" else winreg.HKEY_CURRENT_USER
|
||||||
|
path = entry["path"]
|
||||||
|
name = entry["name"]
|
||||||
|
if entry["present"]:
|
||||||
|
_write_reg_dword(hive, path, name, int(entry["prev"]))
|
||||||
|
logs.append(f"reg restore: {entry['hive']}\\{path}\\{name} ← {entry['prev']}")
|
||||||
|
else:
|
||||||
|
_delete_reg_value(hive, path, name)
|
||||||
|
logs.append(f"reg removed (was absent): {entry['hive']}\\{path}\\{name}")
|
||||||
|
return logs
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_to_json(snap: TelemetrySnapshot | None) -> str:
|
||||||
|
if snap is None:
|
||||||
|
return ""
|
||||||
|
return json.dumps({
|
||||||
|
"services": snap.service_state,
|
||||||
|
"tasks": snap.sched_tasks_state,
|
||||||
|
"reg": snap.reg_values,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_from_json(raw: str) -> TelemetrySnapshot | None:
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
d = json.loads(raw)
|
||||||
|
return TelemetrySnapshot(
|
||||||
|
service_state=dict(d.get("services") or {}),
|
||||||
|
sched_tasks_state={k: bool(v) for k, v in (d.get("tasks") or {}).items()},
|
||||||
|
reg_values=list(d.get("reg") or []),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
@@ -217,6 +217,65 @@ class TestValidator(unittest.TestCase):
|
|||||||
self.assertIsNone(asyncio.run(run()))
|
self.assertIsNone(asyncio.run(run()))
|
||||||
|
|
||||||
|
|
||||||
|
class TestBrowserPersona(unittest.TestCase):
|
||||||
|
def test_blend_persona_relaxes_block_flags(self) -> None:
|
||||||
|
from proxy_chain_manager.browser_profile import (
|
||||||
|
FirefoxHardening, apply_persona, build_user_js,
|
||||||
|
)
|
||||||
|
h = FirefoxHardening(
|
||||||
|
persona_key="blend_windows_chrome",
|
||||||
|
resist_fingerprinting=True,
|
||||||
|
first_party_isolation=True,
|
||||||
|
strict_tracking_protection=True,
|
||||||
|
timezone_utc=True,
|
||||||
|
)
|
||||||
|
applied = apply_persona(h)
|
||||||
|
self.assertFalse(applied.resist_fingerprinting)
|
||||||
|
self.assertFalse(applied.first_party_isolation)
|
||||||
|
self.assertFalse(applied.strict_tracking_protection)
|
||||||
|
self.assertFalse(applied.timezone_utc)
|
||||||
|
self.assertIsNotNone(applied.persona)
|
||||||
|
# And the user.js carries a spoofed UA when blend is selected.
|
||||||
|
js = build_user_js("127.0.0.1", 18888, h)
|
||||||
|
self.assertIn("general.useragent.override", js)
|
||||||
|
self.assertIn("Chrome/124", js)
|
||||||
|
|
||||||
|
def test_hardened_persona_keeps_block_flags(self) -> None:
|
||||||
|
from proxy_chain_manager.browser_profile import FirefoxHardening, apply_persona
|
||||||
|
h = FirefoxHardening(
|
||||||
|
persona_key="hardened",
|
||||||
|
resist_fingerprinting=False, # should be forced True
|
||||||
|
first_party_isolation=False,
|
||||||
|
)
|
||||||
|
applied = apply_persona(h)
|
||||||
|
self.assertTrue(applied.resist_fingerprinting)
|
||||||
|
self.assertTrue(applied.first_party_isolation)
|
||||||
|
self.assertTrue(applied.strict_tracking_protection)
|
||||||
|
self.assertIsNone(applied.persona)
|
||||||
|
|
||||||
|
def test_cookie_behavior_mapping(self) -> None:
|
||||||
|
from proxy_chain_manager.browser_identity import (
|
||||||
|
cookie_behavior_value, cookie_session_only,
|
||||||
|
)
|
||||||
|
self.assertEqual(cookie_behavior_value("accept_all"), 0)
|
||||||
|
self.assertEqual(cookie_behavior_value("block_third_party"), 1)
|
||||||
|
self.assertEqual(cookie_behavior_value("block_third_party_trackers"), 4)
|
||||||
|
self.assertEqual(cookie_behavior_value("session_only"), 0)
|
||||||
|
self.assertEqual(cookie_behavior_value("block_all"), 2)
|
||||||
|
self.assertTrue(cookie_session_only("session_only"))
|
||||||
|
self.assertFalse(cookie_session_only("accept_all"))
|
||||||
|
|
||||||
|
def test_persona_env_returns_tz_for_blend(self) -> None:
|
||||||
|
from proxy_chain_manager.browser_profile import FirefoxHardening, persona_env
|
||||||
|
env = persona_env(FirefoxHardening(persona_key="blend_windows_chrome"))
|
||||||
|
self.assertIn("TZ", env)
|
||||||
|
self.assertTrue(env["TZ"])
|
||||||
|
|
||||||
|
def test_persona_env_empty_for_hardened(self) -> None:
|
||||||
|
from proxy_chain_manager.browser_profile import FirefoxHardening, persona_env
|
||||||
|
self.assertEqual(persona_env(FirefoxHardening(persona_key="hardened")), {})
|
||||||
|
|
||||||
|
|
||||||
class TestPrivacyLan(unittest.TestCase):
|
class TestPrivacyLan(unittest.TestCase):
|
||||||
def test_snapshot_round_trip(self) -> None:
|
def test_snapshot_round_trip(self) -> None:
|
||||||
from proxy_chain_manager.privacy_lan import (
|
from proxy_chain_manager.privacy_lan import (
|
||||||
|
|||||||
Reference in New Issue
Block a user