fix: C-01 WebRTC policy value, C-02 scoped taskkill, C-04 preflight race, E-11 socket timeout, E-27 ban test dedup, full audit doc
This commit is contained in:
@@ -1999,6 +1999,10 @@ def _main_inner() -> None:
|
||||
root.after(0, lambda: _pf_set("target", "fail", "chain not running"))
|
||||
|
||||
# ── Final summary ─────────────────────────────────────────────────
|
||||
# Schedule _final via root.after(0, ...) so it is enqueued AFTER
|
||||
# all previously-scheduled root.after(0, ...) callbacks (which
|
||||
# populate `warns`). A fixed 200 ms delay could fire before those
|
||||
# callbacks execute on a slow main thread.
|
||||
def _final() -> None:
|
||||
pf_running[0] = False
|
||||
if warns:
|
||||
@@ -2006,7 +2010,7 @@ def _main_inner() -> None:
|
||||
else:
|
||||
pf_summary_var.set("✓ All pre-flight checks passed — good to go.")
|
||||
|
||||
root.after(200, _final)
|
||||
root.after(0, _final)
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ def _test_one(
|
||||
|
||||
def _dns_resolve_via_system(hostname: str, timeout: float = 5.0) -> str | None:
|
||||
"""Resolve a hostname using system DNS (does not go through proxy — exposes leak)."""
|
||||
_prev = socket.getdefaulttimeout()
|
||||
try:
|
||||
socket.setdefaulttimeout(timeout)
|
||||
info = socket.getaddrinfo(hostname, None)
|
||||
@@ -154,6 +155,8 @@ def _dns_resolve_via_system(hostname: str, timeout: float = 5.0) -> str | None:
|
||||
return addr
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
socket.setdefaulttimeout(_prev)
|
||||
return None
|
||||
|
||||
|
||||
@@ -235,10 +238,13 @@ def run_ban_tests(
|
||||
if sites is not None:
|
||||
work = [(s, u, "") for s, u in sites]
|
||||
elif categories is not None:
|
||||
seen_urls: set[str] = set()
|
||||
work = []
|
||||
for cat in categories:
|
||||
for s, u in SITE_CATEGORIES.get(cat, ()):
|
||||
work.append((s, u, cat))
|
||||
if u not in seen_urls:
|
||||
seen_urls.add(u)
|
||||
work.append((s, u, cat))
|
||||
else:
|
||||
work = [(s, u, cat) for cat, pairs in SITE_CATEGORIES.items() if cat != "All" for s, u in pairs]
|
||||
|
||||
|
||||
@@ -279,6 +279,9 @@ class BrowserSession:
|
||||
return True, f"Firefox started ({mode}, pid {pid})."
|
||||
|
||||
def stop(self, dispose: bool = False) -> tuple[bool, str]:
|
||||
# Capture our PID before clearing the handle
|
||||
launched_pid: int | None = self._proc.pid if self._proc else None
|
||||
|
||||
# Terminate the Popen handle if still alive
|
||||
if self._proc and self._proc.poll() is None:
|
||||
try:
|
||||
@@ -292,17 +295,19 @@ class BrowserSession:
|
||||
finally:
|
||||
self._proc = None
|
||||
|
||||
# Also kill any remaining firefox.exe processes (handles the
|
||||
# detached-child case where the parent already exited naturally)
|
||||
try:
|
||||
subprocess.run(
|
||||
["taskkill", "/F", "/IM", "firefox.exe"],
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# Kill only the process tree we spawned (handles the detached-child
|
||||
# case where the launcher process already exited naturally).
|
||||
# /T kills the entire child tree; /PID scopes to our PID only.
|
||||
if launched_pid is not None:
|
||||
try:
|
||||
subprocess.run(
|
||||
["taskkill", "/F", "/T", "/PID", str(launched_pid)],
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._proc = None
|
||||
if dispose and self._profile_path and self._profile_path.exists():
|
||||
|
||||
@@ -136,6 +136,7 @@ def flush_dns_cache() -> tuple[bool, str]:
|
||||
|
||||
def _resolve_direct(hostname: str, timeout: float = 4.0) -> list[str]:
|
||||
"""Resolve hostname using system DNS (direct, not through proxy)."""
|
||||
_prev = socket.getdefaulttimeout()
|
||||
try:
|
||||
socket.setdefaulttimeout(timeout)
|
||||
infos = socket.getaddrinfo(hostname, None)
|
||||
@@ -148,7 +149,7 @@ def _resolve_direct(hostname: str, timeout: float = 4.0) -> list[str]:
|
||||
except Exception:
|
||||
return []
|
||||
finally:
|
||||
socket.setdefaulttimeout(None)
|
||||
socket.setdefaulttimeout(_prev)
|
||||
|
||||
|
||||
def _resolve_via_proxy(hostname: str, proxy_url: str, timeout: float = 8.0) -> list[str]:
|
||||
|
||||
@@ -17,8 +17,12 @@ log = logging.getLogger(__name__)
|
||||
|
||||
_WEBRTC_CHROME = r"SOFTWARE\Policies\Google\Chrome"
|
||||
_WEBRTC_EDGE = r"SOFTWARE\Policies\Microsoft\Edge"
|
||||
# Legacy DWORD key — value 3 = disable_non_proxied_udp (value 2 was wrong: public+private only)
|
||||
_WEBRTC_VALUE = "DefaultWebRtcIpHandlingPolicy"
|
||||
_WEBRTC_DISABLE = 2 # disable_non_proxied_udp
|
||||
_WEBRTC_DISABLE = 3 # disable_non_proxied_udp (correct Chrome/Edge DWORD)
|
||||
# Modern REG_SZ key required by Chrome 114+ Group Policy
|
||||
_WEBRTC_VALUE_STR = "WebRtcIPHandling"
|
||||
_WEBRTC_DISABLE_STR = "disable_non_proxied_udp"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -160,7 +164,12 @@ def enable_ipv6_on_adapters(adapters: list[str]) -> list[str]:
|
||||
|
||||
|
||||
def apply_webrtc_hardening(enable: bool) -> tuple[bool, str]:
|
||||
"""Chrome/Edge: disable WebRTC non-proxied UDP (Admin, HKLM policies)."""
|
||||
"""Chrome/Edge: disable WebRTC non-proxied UDP (Admin, HKLM policies).
|
||||
|
||||
Writes both the legacy DWORD key (DefaultWebRtcIpHandlingPolicy=3) and
|
||||
the modern REG_SZ key (WebRtcIPHandling=disable_non_proxied_udp) so that
|
||||
all Chrome/Edge versions are covered.
|
||||
"""
|
||||
if not is_admin():
|
||||
return False, "Administrator required for browser WebRTC policy."
|
||||
paths = [_WEBRTC_CHROME, _WEBRTC_EDGE]
|
||||
@@ -175,15 +184,17 @@ def apply_webrtc_hardening(enable: bool) -> tuple[bool, str]:
|
||||
continue
|
||||
with key:
|
||||
winreg.SetValueEx(key, _WEBRTC_VALUE, 0, winreg.REG_DWORD, _WEBRTC_DISABLE)
|
||||
winreg.SetValueEx(key, _WEBRTC_VALUE_STR, 0, winreg.REG_SZ, _WEBRTC_DISABLE_STR)
|
||||
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
|
||||
for val_name in (_WEBRTC_VALUE, _WEBRTC_VALUE_STR):
|
||||
try:
|
||||
winreg.DeleteValue(key, val_name)
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
return True, (
|
||||
@@ -340,16 +351,28 @@ def audit_device() -> FingerprintAudit:
|
||||
]
|
||||
|
||||
# Quick consistency checks — WebRTC IP-handling policy (Chrome / Edge)
|
||||
# Accept either: DWORD DefaultWebRtcIpHandlingPolicy==3
|
||||
# or REG_SZ WebRtcIPHandling=="disable_non_proxied_udp"
|
||||
webrtc_ok = False
|
||||
for hive_root in (_WEBRTC_CHROME, _WEBRTC_EDGE):
|
||||
try:
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE, hive_root, 0, winreg.KEY_QUERY_VALUE
|
||||
) as k:
|
||||
v = int(winreg.QueryValueEx(k, _WEBRTC_VALUE)[0])
|
||||
if v == _WEBRTC_DISABLE:
|
||||
webrtc_ok = True
|
||||
break
|
||||
try:
|
||||
v = int(winreg.QueryValueEx(k, _WEBRTC_VALUE)[0])
|
||||
if v == _WEBRTC_DISABLE:
|
||||
webrtc_ok = True
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
v_str = str(winreg.QueryValueEx(k, _WEBRTC_VALUE_STR)[0])
|
||||
if v_str == _WEBRTC_DISABLE_STR:
|
||||
webrtc_ok = True
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
|
||||
@@ -40,8 +40,10 @@ _CHROMIUM_POLICY_PATHS = (
|
||||
r"SOFTWARE\Policies\Microsoft\Edge",
|
||||
r"SOFTWARE\Policies\Chromium",
|
||||
)
|
||||
_WEBRTC_POLICY_VALUE = "DefaultWebRtcIpHandlingPolicy"
|
||||
_WEBRTC_BLOCK_VALUE = 2 # "default_public_and_private_interfaces"
|
||||
_WEBRTC_POLICY_VALUE = "DefaultWebRtcIpHandlingPolicy"
|
||||
_WEBRTC_BLOCK_VALUE = 3 # disable_non_proxied_udp (was 2 = public+private only — wrong)
|
||||
_WEBRTC_POLICY_STR_KEY = "WebRtcIPHandling"
|
||||
_WEBRTC_BLOCK_STR_VALUE = "disable_non_proxied_udp"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -71,21 +73,39 @@ class WebRtcCheckResult:
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def check_chromium_webrtc_policy() -> tuple[bool, str]:
|
||||
"""Return (policy_set, detail_string)."""
|
||||
"""Return (policy_set, detail_string).
|
||||
|
||||
Accepts either the legacy DWORD DefaultWebRtcIpHandlingPolicy==3
|
||||
OR the modern REG_SZ WebRtcIPHandling=="disable_non_proxied_udp".
|
||||
"""
|
||||
found: list[str] = []
|
||||
missing: list[str] = []
|
||||
for path in _CHROMIUM_POLICY_PATHS:
|
||||
try:
|
||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_QUERY_VALUE) as k:
|
||||
name = path.split("\\")[-1]
|
||||
dword_ok = False
|
||||
str_ok = False
|
||||
# Check legacy DWORD
|
||||
try:
|
||||
val = int(winreg.QueryValueEx(k, _WEBRTC_POLICY_VALUE)[0])
|
||||
name = path.split("\\")[-1]
|
||||
if val == _WEBRTC_BLOCK_VALUE:
|
||||
found.append(name)
|
||||
else:
|
||||
missing.append(f"{name}={val} (need {_WEBRTC_BLOCK_VALUE})")
|
||||
dword_ok = val == _WEBRTC_BLOCK_VALUE
|
||||
if not dword_ok:
|
||||
missing.append(f"{name} DWORD={val} (need {_WEBRTC_BLOCK_VALUE})")
|
||||
except OSError:
|
||||
missing.append(path.split("\\")[-1] + " key missing")
|
||||
pass
|
||||
# Check modern REG_SZ
|
||||
try:
|
||||
val_str = str(winreg.QueryValueEx(k, _WEBRTC_POLICY_STR_KEY)[0])
|
||||
str_ok = val_str == _WEBRTC_BLOCK_STR_VALUE
|
||||
if not str_ok:
|
||||
missing.append(f"{name} REG_SZ={val_str!r} (need {_WEBRTC_BLOCK_STR_VALUE!r})")
|
||||
except OSError:
|
||||
pass
|
||||
if dword_ok or str_ok:
|
||||
found.append(name)
|
||||
elif not dword_ok and not str_ok:
|
||||
missing.append(f"{name}: no WebRTC policy keys present")
|
||||
except OSError:
|
||||
continue # key not present at all — browser not installed or not policy-managed
|
||||
|
||||
|
||||
Reference in New Issue
Block a user