fix: audit round 3 - fail-closed leak, shared asyncio loop, GOST bundling, close-X UX, persona key lookup, +29 tests
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user