Build comprehensive privacy suite and hardened browser controls.
Adds VPN-aware leak handling, chain testing UX improvements, hardened Firefox launch/profile management, privacy/device hardening modules, and tray/status upgrades so the app is production-ready as the new baseline. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
221
proxy_chain_manager/fingerprint.py
Normal file
221
proxy_chain_manager/fingerprint.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""Device fingerprint audit and OS-level hardening helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import subprocess
|
||||
import winreg
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .firewall import is_admin
|
||||
from .mac_spoof import list_nics
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_WEBRTC_CHROME = r"SOFTWARE\Policies\Google\Chrome"
|
||||
_WEBRTC_EDGE = r"SOFTWARE\Policies\Microsoft\Edge"
|
||||
_WEBRTC_VALUE = "DefaultWebRtcIpHandlingPolicy"
|
||||
_WEBRTC_DISABLE = 2 # disable_non_proxied_udp
|
||||
|
||||
|
||||
@dataclass
|
||||
class FingerprintAudit:
|
||||
lines: list[str] = field(default_factory=list)
|
||||
hostname: str = ""
|
||||
machine_guid: str = ""
|
||||
username: str = ""
|
||||
macs: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _run_ps(script: str, timeout: float = 15.0) -> str:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
)
|
||||
return (r.stdout or "").strip()
|
||||
except Exception as e:
|
||||
log.debug("fingerprint ps: %s", e)
|
||||
return ""
|
||||
|
||||
|
||||
def get_computer_name() -> str:
|
||||
try:
|
||||
import os
|
||||
return os.environ.get("COMPUTERNAME", "") or ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def get_machine_guid() -> str:
|
||||
try:
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE,
|
||||
r"SOFTWARE\Microsoft\Cryptography",
|
||||
) as key:
|
||||
val, _ = winreg.QueryValueEx(key, "MachineGuid")
|
||||
return str(val)
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def random_hostname(prefix: str = "PC") -> str:
|
||||
suffix = "".join(random.choices(string.ascii_uppercase + string.digits, k=7))
|
||||
return f"{prefix}-{suffix}"[:15]
|
||||
|
||||
|
||||
def set_computer_name(name: str) -> tuple[bool, str]:
|
||||
"""Set NetBIOS / computer name (Admin). Reboot may be required for all apps."""
|
||||
if not is_admin():
|
||||
return False, "Administrator required to change computer name."
|
||||
name = re.sub(r"[^A-Za-z0-9\-]", "", name)[:15]
|
||||
if len(name) < 1:
|
||||
return False, "Invalid hostname."
|
||||
code = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command",
|
||||
f'Rename-Computer -NewName "{name}" -Force -ErrorAction Stop'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
).returncode
|
||||
if code != 0:
|
||||
# NetBIOS name via WMI (often works without full rename)
|
||||
wmi = (
|
||||
f'$n = Get-WmiObject Win32_ComputerSystem; '
|
||||
f'$r = $n.Rename("{name}"); if ($r.ReturnValue -ne 0) {{ exit $r.ReturnValue }}'
|
||||
)
|
||||
code = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", wmi],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
).returncode
|
||||
if code == 0:
|
||||
return True, f"Computer name set to {name} (some apps need reconnect/reboot)."
|
||||
return False, "Could not change computer name."
|
||||
|
||||
|
||||
def disable_ipv6_on_adapters() -> tuple[list[str], list[str]]:
|
||||
"""Disable IPv6 binding on up physical adapters. Returns (adapter names, log lines)."""
|
||||
if not is_admin():
|
||||
return [], ["IPv6 disable skipped (not Admin)."]
|
||||
script = (
|
||||
"Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | "
|
||||
"ForEach-Object { $_.Name }"
|
||||
)
|
||||
names = [n.strip() for n in _run_ps(script).splitlines() if n.strip()]
|
||||
logs: list[str] = []
|
||||
changed: list[str] = []
|
||||
skip = ("virtual", "vmware", "hyper-v", "loopback", "bluetooth")
|
||||
for name in names:
|
||||
if any(h in name.lower() for h in skip):
|
||||
continue
|
||||
cmd = (
|
||||
f'Disable-NetAdapterBinding -Name "{name}" -ComponentID ms_tcpip6 -Confirm:$false '
|
||||
f'-ErrorAction SilentlyContinue'
|
||||
)
|
||||
subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", cmd],
|
||||
capture_output=True,
|
||||
timeout=20,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
)
|
||||
changed.append(name)
|
||||
logs.append(f"IPv6 disabled on {name}")
|
||||
return changed, logs
|
||||
|
||||
|
||||
def enable_ipv6_on_adapters(adapters: list[str]) -> list[str]:
|
||||
if not is_admin() or not adapters:
|
||||
return []
|
||||
logs: list[str] = []
|
||||
for name in adapters:
|
||||
cmd = (
|
||||
f'Enable-NetAdapterBinding -Name "{name}" -ComponentID ms_tcpip6 -Confirm:$false '
|
||||
f'-ErrorAction SilentlyContinue'
|
||||
)
|
||||
subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", cmd],
|
||||
capture_output=True,
|
||||
timeout=20,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
)
|
||||
logs.append(f"IPv6 re-enabled on {name}")
|
||||
return logs
|
||||
|
||||
|
||||
def apply_webrtc_hardening(enable: bool) -> tuple[bool, str]:
|
||||
"""Chrome/Edge: disable WebRTC non-proxied UDP (Admin, HKLM policies)."""
|
||||
if not is_admin():
|
||||
return False, "Administrator required for browser WebRTC policy."
|
||||
paths = [_WEBRTC_CHROME, _WEBRTC_EDGE]
|
||||
try:
|
||||
for path in paths:
|
||||
if enable:
|
||||
try:
|
||||
key = winreg.CreateKeyEx(
|
||||
winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_SET_VALUE
|
||||
)
|
||||
except OSError:
|
||||
continue
|
||||
with key:
|
||||
winreg.SetValueEx(key, _WEBRTC_VALUE, 0, winreg.REG_DWORD, _WEBRTC_DISABLE)
|
||||
else:
|
||||
try:
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_SET_VALUE
|
||||
) as key:
|
||||
try:
|
||||
winreg.DeleteValue(key, _WEBRTC_VALUE)
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
return True, (
|
||||
"WebRTC hardened (Chrome/Edge: non-proxied UDP disabled)."
|
||||
if enable
|
||||
else "WebRTC policy removed."
|
||||
)
|
||||
except OSError as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def audit_device() -> FingerprintAudit:
|
||||
"""Collect identifiers sites and trackers often fingerprint."""
|
||||
import getpass
|
||||
import os
|
||||
|
||||
host = get_computer_name()
|
||||
guid = get_machine_guid()
|
||||
user = getpass.getuser()
|
||||
macs = [f"{n.name}: {n.mac}" for n in list_nics()]
|
||||
|
||||
lines = [
|
||||
f"Computer name: {host or '—'}",
|
||||
f"Windows username: {user}",
|
||||
f"MachineGuid: {guid[:8]}…{guid[-4:]}" if len(guid) > 12 else f"MachineGuid: {guid or '—'}",
|
||||
f"Network adapters ({len(macs)}):",
|
||||
]
|
||||
lines.extend([f" • {m}" for m in macs[:8]] or [" • (none detected)"])
|
||||
if len(macs) > 8:
|
||||
lines.append(f" … and {len(macs) - 8} more")
|
||||
|
||||
lines.append("")
|
||||
lines.append("Browser fingerprint (Canvas/WebGL/fonts) is not changed by this app.")
|
||||
lines.append("Use hardened browser profiles + proxy chain for web traffic.")
|
||||
lines.append("WebRTC toggle here affects Chrome/Edge system policy only.")
|
||||
|
||||
return FingerprintAudit(
|
||||
lines=lines,
|
||||
hostname=host,
|
||||
machine_guid=guid,
|
||||
username=user,
|
||||
macs=[n.mac for n in list_nics()],
|
||||
)
|
||||
Reference in New Issue
Block a user