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
Some checks failed
CI / Test Python 3.10 (push) Has been cancelled
CI / Test Python 3.11 (push) Has been cancelled
CI / Test Python 3.12 (push) Has been cancelled

This commit is contained in:
Dr Jones
2026-05-22 00:43:57 -07:00
parent 311abb933a
commit 255fdf3e8c
7 changed files with 267 additions and 199 deletions

View File

@@ -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()

View File

@@ -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]

View File

@@ -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():

View File

@@ -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]:

View File

@@ -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

View File

@@ -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