fix: audit round 3 - fail-closed leak, shared asyncio loop, GOST bundling, close-X UX, persona key lookup, +29 tests
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 18:23:52 -07:00
parent ad56f75e8a
commit b852fd264f
15 changed files with 556 additions and 19 deletions

View File

@@ -389,6 +389,13 @@ def _main_inner() -> None:
if not is_admin():
def _elevate() -> None:
# Persist UI state to disk before handing off to the elevated
# process — otherwise unsaved edits in Chain Builder / Settings
# are lost when this window closes.
try:
_save_settings()
except Exception as exc: # noqa: BLE001
log.warning("Could not save settings before admin relaunch: %s", exc)
if request_admin_relaunch():
_close()
_btn(topbar, "Run as Admin", _elevate, w=104,
@@ -1334,17 +1341,17 @@ def _main_inner() -> None:
cookie_menu.pack(side="left", padx=(4, 0))
cookie_note_lbl.pack(anchor="w", padx=(92, 12), pady=(0, 10))
# Build a label→key lookup ONCE so the reverse mapping doesn't break
# when a label changes copy. Falls through to the configured default if
# the label string somehow no longer exists in the dropdown.
_PERSONA_LABEL_TO_KEY = {PERSONA_LABELS[k]: k for k in PERSONAS}
_COOKIE_LABEL_TO_KEY = {COOKIE_LABELS[k]: k for k in COOKIE_MODES}
def _persona_key() -> str:
for k in PERSONAS:
if PERSONA_LABELS[k] == persona_var.get():
return k
return "blend_windows_chrome"
return _PERSONA_LABEL_TO_KEY.get(persona_var.get(), "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"
return _COOKIE_LABEL_TO_KEY.get(cookie_var.get(), "block_third_party")
br_force_proxy_var = ctk.BooleanVar(value=s.browser_force_proxy)
br_disable_webrtc_var = ctk.BooleanVar(value=s.browser_disable_webrtc)
@@ -1878,16 +1885,26 @@ def _main_inner() -> None:
def work() -> None:
from .leak_detect import is_chain_leak, leak_reason
from .validator import get_direct_ip as _get_direct_ip
import asyncio as _asyncio
timeout = min(15.0, max(8.0, float(svc.settings.validation_timeout_seconds) + 2.0))
warns: list[str] = []
vpn_active = detect_vpn().active
# Single dedicated event loop for the whole preflight pass. Using
# `asyncio.run()` per call tore down the loop each time, which
# caused nested-loop errors when caller context (e.g. async http
# libraries) leaked tasks across invocations.
_loop = _asyncio.new_event_loop()
_asyncio.set_event_loop(_loop)
def _run_async(coro):
return _loop.run_until_complete(coro)
# ── 1. Direct IP (bypass chain) ──────────────────────────────────
from .validator import get_direct_ip as _get_direct_ip
import asyncio as _asyncio
try:
direct_ip = _asyncio.run(_get_direct_ip(svc.settings.ip_check_url, timeout / 2))
direct_ip = _run_async(_get_direct_ip(svc.settings.ip_check_url, timeout / 2))
except Exception:
direct_ip = None
@@ -1994,8 +2011,7 @@ def _main_inner() -> None:
if chain_ok:
try:
from .validator import check_https_tunnel
import asyncio as _as2
tun_ok, tun_msg = _as2.run(
tun_ok, tun_msg = _run_async(
check_https_tunnel(proxy_url, timeout)
)
def _set_tun(ok_: bool, msg_: str) -> None:
@@ -2039,6 +2055,13 @@ def _main_inner() -> None:
root.after(0, _final)
# Tear down the dedicated event loop now that all coroutines are
# done; leaving it open would leak the underlying selector socket.
try:
_loop.close()
except Exception: # noqa: BLE001
pass
threading.Thread(target=work, daemon=True).start()
pf_btn_row = ctk.CTkFrame(preflight_card, fg_color="transparent")
@@ -3001,7 +3024,27 @@ def _main_inner() -> None:
fw_disengage()
root.destroy()
root.protocol("WM_DELETE_WINDOW", lambda: root.withdraw())
def _on_window_x() -> None:
"""X-button handler: warn the operator that the chain is still active
before silently going to tray, then offer to fully quit instead."""
chain_alive = bool(svc.current_chain) or fw_is_engaged()
if chain_alive:
from tkinter import messagebox
choice = messagebox.askyesnocancel(
"Proxy God still running",
"The proxy chain and/or kill-switch are still active.\n\n"
" • Yes — minimize to tray (chain keeps running)\n"
" • No — fully quit (stop chain + remove firewall rules)\n"
" • Cancel — keep window open",
)
if choice is None:
return
if choice is False:
_close()
return
root.withdraw()
root.protocol("WM_DELETE_WINDOW", _on_window_x)
# ─────────────────────────────────────────────────────────────────────────
# INIT