fix: audit pass — WebRTC audit, HTTPS gate, leak compare, kill-switch guard, doc backlog
This commit is contained in:
@@ -1773,7 +1773,7 @@ def _main_inner() -> None:
|
||||
preflight_card = _signup_card(
|
||||
"Pre-flight check",
|
||||
"Exhaustive readiness check: chain, exit IP, geo/ASN, DNS leak, WebRTC, "
|
||||
"IPv6, fingerprint, target site reachability, and direct-IP comparison.",
|
||||
"IPv6, HTTPS tunnel, target site reachability, and direct-IP comparison.",
|
||||
)
|
||||
pf_rows: dict[str, dict[str, ctk.StringVar | ctk.CTkLabel]] = {}
|
||||
|
||||
@@ -1853,8 +1853,11 @@ def _main_inner() -> None:
|
||||
target_host = urlparse(url).hostname or "(unknown)"
|
||||
|
||||
def work() -> None:
|
||||
from .leak_detect import is_chain_leak, leak_reason
|
||||
|
||||
timeout = min(15.0, max(8.0, float(svc.settings.validation_timeout_seconds) + 2.0))
|
||||
warns: list[str] = []
|
||||
vpn_active = detect_vpn().active
|
||||
|
||||
# ── 1. Direct IP (bypass chain) ──────────────────────────────────
|
||||
from .validator import get_direct_ip as _get_direct_ip
|
||||
@@ -1882,12 +1885,12 @@ def _main_inner() -> None:
|
||||
_pf_set("exit_ip", "ok", ip_)
|
||||
else:
|
||||
_pf_set("exit_ip", "warn", "no exit IP yet")
|
||||
# IP leak compare
|
||||
# IP leak compare (same logic as production leak_detect)
|
||||
if direct_ip and ip_ and ip_ not in ("—", ""):
|
||||
if direct_ip == ip_:
|
||||
_pf_set("leak_compare", "fail",
|
||||
f"exit == direct ({ip_}) — chain not working!")
|
||||
warns.append("exit IP matches direct IP (chain not routing)")
|
||||
if is_chain_leak(ip_, direct_ip, vpn_active):
|
||||
reason = leak_reason(ip_, direct_ip, vpn_active)
|
||||
_pf_set("leak_compare", "fail", reason[:80])
|
||||
warns.append(f"IP leak: {reason}")
|
||||
else:
|
||||
_pf_set("leak_compare", "ok",
|
||||
f"exit {ip_} ≠ direct {direct_ip} ✓")
|
||||
@@ -2148,12 +2151,38 @@ def _main_inner() -> None:
|
||||
tag = f" [{' · '.join(extras)}]" if extras else ""
|
||||
signup_status_var.set(f"Opened {label}{tag} — autofill active (you submit + captcha).")
|
||||
_log(f"Signup prep: opened {url} with autofill extension{tag}.")
|
||||
# Auto-save entry so the account is recorded even if user forgets to save manually
|
||||
_save_signup_account()
|
||||
# Record draft metadata only — password saved when user clicks Save Account
|
||||
_save_signup_account_draft_only()
|
||||
else:
|
||||
signup_status_var.set("Browser launch failed.")
|
||||
svc.set_sticky(0)
|
||||
|
||||
def _save_signup_account_draft_only() -> None:
|
||||
"""After browser open: save email/exit metadata without password until user confirms."""
|
||||
draft = _persist_signup_draft()
|
||||
url = resolve_signup_url(draft)
|
||||
exit_ip, exit_hop = _current_exit_meta()
|
||||
preset = SIGNUP_PRESETS.get(draft.site_key)
|
||||
site_label = preset.label if preset else "Custom"
|
||||
rec = AccountRecord(
|
||||
id=datetime.now().strftime("%Y%m%d%H%M%S%f"),
|
||||
site=site_label,
|
||||
url=url,
|
||||
email=draft.email,
|
||||
password="",
|
||||
username=draft.username,
|
||||
first_name=draft.first_name,
|
||||
last_name=draft.last_name,
|
||||
exit_ip=exit_ip,
|
||||
exit_hop=exit_hop,
|
||||
notes=draft.notes,
|
||||
status="pending",
|
||||
created_at=datetime.now().isoformat(timespec="seconds"),
|
||||
)
|
||||
append_account(rec)
|
||||
_refresh_accounts_ui()
|
||||
_log(f"Signup prep: draft account {draft.email or draft.username} ({site_label}) — save password after signup.")
|
||||
|
||||
def _save_signup_account() -> None:
|
||||
draft = _persist_signup_draft()
|
||||
url = resolve_signup_url(draft)
|
||||
|
||||
@@ -75,17 +75,18 @@ _BANNED_TEXT_HINTS = (
|
||||
"temporarily blocked",
|
||||
"request blocked",
|
||||
"unusual traffic",
|
||||
"captcha",
|
||||
"challenge required",
|
||||
"bot detected",
|
||||
"suspicious activity",
|
||||
"geo-blocked",
|
||||
"not available in your region",
|
||||
"your ip",
|
||||
"ip address",
|
||||
"cloudflare ray",
|
||||
"ddos protection",
|
||||
"attention required",
|
||||
"complete the captcha",
|
||||
"captcha required",
|
||||
"g-recaptcha",
|
||||
"hcaptcha",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ def _is_safe_https_url(url: str) -> bool:
|
||||
"""Return True for HTTPS URLs pointing at public hosts (not RFC-1918/link-local)."""
|
||||
try:
|
||||
p = urlparse(url)
|
||||
if p.scheme not in ("http", "https"):
|
||||
if p.scheme != "https":
|
||||
return False
|
||||
host = p.hostname or ""
|
||||
if not host:
|
||||
|
||||
@@ -168,7 +168,12 @@ def emergency_disengage() -> None:
|
||||
Unlike ``disengage()``, this never raises and does not require prior
|
||||
admin check — it simply tries, logs the outcome, and returns. Safe to
|
||||
call from atexit or signal handlers where exceptions must not propagate.
|
||||
|
||||
No-op when our rules are not present (avoids touching firewall policy on
|
||||
every process exit).
|
||||
"""
|
||||
if not is_engaged():
|
||||
return
|
||||
try:
|
||||
_set_outbound_policy("blockinbound,allowoutbound")
|
||||
_delete_rules()
|
||||
|
||||
@@ -141,14 +141,29 @@ def _check_webrtc_policy() -> tuple[bool, str]:
|
||||
r"SOFTWARE\Policies\Google\Chrome",
|
||||
r"SOFTWARE\Policies\Microsoft\Edge",
|
||||
)
|
||||
_DWORD_DISABLE = 3 # disable_non_proxied_udp
|
||||
_STR_KEY = "WebRtcIPHandling"
|
||||
_STR_DISABLE = "disable_non_proxied_udp"
|
||||
found = []
|
||||
for p in paths:
|
||||
try:
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE, p, 0, winreg.KEY_QUERY_VALUE
|
||||
) as key:
|
||||
v = int(winreg.QueryValueEx(key, "DefaultWebRtcIpHandlingPolicy")[0])
|
||||
if v == 2:
|
||||
ok = False
|
||||
try:
|
||||
v = int(winreg.QueryValueEx(key, "DefaultWebRtcIpHandlingPolicy")[0])
|
||||
if v == _DWORD_DISABLE:
|
||||
ok = True
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
v_str = str(winreg.QueryValueEx(key, _STR_KEY)[0]).strip().lower()
|
||||
if v_str == _STR_DISABLE:
|
||||
ok = True
|
||||
except OSError:
|
||||
pass
|
||||
if ok:
|
||||
found.append(p.split("\\")[-1])
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
@@ -390,6 +390,14 @@ class ChainService:
|
||||
)
|
||||
|
||||
# Pinned (manual) chain mode — skip pool management
|
||||
if self._settings.use_pinned_chain and not self._settings.pinned_chain:
|
||||
self._notify({
|
||||
"type": "log",
|
||||
"text": (
|
||||
"Pinned chain enabled but list is empty — add hops in Chain Builder "
|
||||
"or disable 'Use pinned chain'."
|
||||
),
|
||||
})
|
||||
if self._settings.use_pinned_chain and self._settings.pinned_chain:
|
||||
chain = list(self._settings.pinned_chain)
|
||||
rotation_num += 1
|
||||
@@ -564,22 +572,27 @@ class ChainService:
|
||||
self._proc = None
|
||||
return False
|
||||
|
||||
# HTTPS CONNECT must work before the chain is marked healthy.
|
||||
https_ok, https_msg = await check_https_tunnel(local_proxy, timeout)
|
||||
if not https_ok:
|
||||
self._notify({"type": "hops", "hops": chain, "status": "dead", "exit_ip": exit_ip})
|
||||
self._notify({"type": "log", "text": (
|
||||
"HTTPS tunnel FAILED — chain forwards plain HTTP but refuses CONNECT. "
|
||||
"Browsers will time out on HTTPS sites. Rotating. " + https_msg
|
||||
)})
|
||||
fixed = self._manual_exit_url()
|
||||
for h in chain:
|
||||
if fixed and h == fixed:
|
||||
continue
|
||||
self._blacklist.add(h)
|
||||
self._available = [x for x in self._available if x != h]
|
||||
terminate_process(self._proc)
|
||||
self._proc = None
|
||||
return False
|
||||
|
||||
self._notify({"type": "hops", "hops": chain, "status": "healthy", "exit_ip": exit_ip})
|
||||
self._notify({"type": "log", "text": f"✓ Chain healthy — Exit IP: {exit_ip}"})
|
||||
|
||||
# HTTPS-tunnel probe: a chain can pass HTTP IP check but refuse CONNECT.
|
||||
# Without this warning, "all proxies green" yet "every browser broken"
|
||||
# is a black-box failure for the user.
|
||||
https_ok, https_msg = await check_https_tunnel(local_proxy, timeout)
|
||||
if https_ok:
|
||||
self._notify({"type": "log", "text": f"✓ HTTPS tunnel OK — {https_msg}"})
|
||||
else:
|
||||
self._notify({"type": "log", "text": (
|
||||
"⚠ HTTPS tunnel FAILED — chain forwards plain HTTP but refuses "
|
||||
"CONNECT. Browsers will time out on every HTTPS page (i.e. "
|
||||
"every site). Replace the proxies that don't support CONNECT, "
|
||||
"or use a SOCKS5 / paid HTTPS-capable exit. " + https_msg
|
||||
)})
|
||||
self._notify({"type": "log", "text": f"✓ HTTPS tunnel OK — {https_msg}"})
|
||||
|
||||
self._notify({"type": "phase", "phase": "running"})
|
||||
# Pre-flight: surface Group Policy locks (they will override us).
|
||||
|
||||
Reference in New Issue
Block a user