fix: audit pass — WebRTC audit, HTTPS gate, leak compare, kill-switch guard, doc backlog
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:00:45 -07:00
parent 255fdf3e8c
commit 04d486a335
9 changed files with 164 additions and 31 deletions

View File

@@ -33,7 +33,7 @@ All meaningful changes to this repository should be recorded here.
discover` on Python 3.10 / 3.11 / 3.12 on `windows-latest`. discover` on Python 3.10 / 3.11 / 3.12 on `windows-latest`.
- **LICENSE**: MIT license added to repo root. - **LICENSE**: MIT license added to repo root.
- **requirements.txt**: All four runtime dependencies pinned to exact versions; `cryptography` - **requirements.txt**: All four runtime dependencies pinned to exact versions; `cryptography`
added for future Fernet-based secret storage. listed for optional future credential encryption (not yet wired).
- **`.gitignore`**: Added `settings.json`, `*.json` credential files, and screenshot noise. - **`.gitignore`**: Added `settings.json`, `*.json` credential files, and screenshot noise.
- **`--version` flag**: `run.py` now supports `--version` / `-V`. - **`--version` flag**: `run.py` now supports `--version` / `-V`.

View File

@@ -60,7 +60,7 @@ exe = EXE(
debug=False, debug=False,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, strip=False,
upx=True, upx=False,
upx_exclude=[], upx_exclude=[],
runtime_tmpdir=None, runtime_tmpdir=None,
console=False, console=False,

View File

@@ -199,4 +199,74 @@ All remaining E/M/B/U/T items, installer signing, metrics, coverage gates
--- ---
*Proxy God Audit — 2026-05-22* ## 9. Remediation log (2026-05-22 passes)
| ID | Status | Notes |
|----|--------|-------|
| C-01 | **Fixed** | `fingerprint.py`, `webrtc_check.py` — DWORD `3` + `WebRtcIPHandling` REG_SZ |
| C-02 | **Fixed** | `browser_launcher.py` — scoped `taskkill /PID` |
| C-04 | **Fixed** | `app.py``_final` scheduled after all `after(0)` callbacks |
| C-05 | **Fixed** | `app.py` preflight uses `is_chain_leak()` + VPN-aware `/16` |
| C-06 | **Fixed** | `service.py` — HTTPS CONNECT failure rotates chain (not healthy) |
| C-08 | **Fixed** | `firewall.py``emergency_disengage()` no-op unless `is_engaged()` |
| C-10 | **Fixed** | `app.py` — auto-save on launch is `pending` with empty password |
| E-06 | **Fixed** | `service.py` — log when pinned chain enabled but empty |
| E-11 | **Fixed** | `dns_leak.py`, `ban_tester.py` — restore socket timeout in `finally` |
| E-24 | **Fixed** | `config.py` — pool sources must be `https://` only |
| E-26 | **Fixed** | `ban_tester.py` — removed bare `"captcha"` hint; stricter phrases |
| E-27 | **Fixed** | `ban_tester.py` — dedupe by URL |
| M-06 / U-06 | **Fixed** | Preflight subtitle no longer claims “fingerprint” check |
| M-15 | **Fixed** | `CHANGELOG.md` — Fernet/DPAPI not implemented (clarified) |
| B-02 | **Fixed** | `ProxyChainManager.spec``upx=False` |
| leak_audit | **Fixed** | `_check_webrtc_policy()` accepts DWORD `3` or REG_SZ |
| (prior) | **Fixed** | CI, GOST zip SHA256, settings migration/backup, LICENSE, log rotation |
---
## 10. Still needs to be fixed
Items below were **not** changed in this pass because they need design decisions, external tooling, or behavior that is not safe to guess.
### Critical / security (P0)
| ID | Why left open | Suggested direction |
|----|---------------|---------------------|
| **C-03 / S-01** | Encrypting `settings.json` / signup JSON requires key management (DPAPI vs Fernet passphrase) and migration for existing installs | Windows DPAPI via `cryptography` or OS credential store; one-time migration on load |
| **C-07** | Pinned chains need per-hop health probes without breaking intentional manual order | Optional “validate pinned hops on start” toggle + blacklist dead hops only |
| **C-09** | Narrowing signup extension off `<all_urls>` breaks **custom signup URLs** | Dynamic host permissions or user-approved host list per session |
| **C-11** | No pinned SHA256 for extracted `gost.exe` in repo | Add `GOST_EXE_SHA256` constant from official release manifest |
| **C-12** | `asyncio.run()` in worker threads (`app.py` preflight) — refactor needs dedicated event-loop policy | Run network checks via `asyncio.new_event_loop()` in thread or shared async runner |
| **S-03** | `verify=False` on httpx is intentional for broken proxy TLS; enabling verify breaks many public proxies | Per-setting toggle; document risk |
| **B-01** | Authenticode signing needs cert + build pipeline | Sign `dist\ProxyChainManager.exe` in release script |
### High (P1)
| ID | Why left open |
|----|---------------|
| **E-01** | Refresh `real_ip` when VPN state changes mid-session |
| **E-02E-03** | Sticky exit + leak interaction needs product rule (rotate vs warn) |
| **E-04** | `is_chain_leak(real_ip=None)` fail-open — changing affects rotation semantics |
| **E-05** | /16 neighbor policy is intentional without VPN |
| **E-07E-10** | Pool/pinned/exit edge cases need operator UX, not one-line fixes |
| **E-12E-13** | DNS leak test methodology (DoH vs system DNS) needs spec |
| **E-14 / S-08** | Kill-switch DNS allow rule required for GOST hostname resolution |
| **E-15** | Non-admin kill-switch skip — document or block Start without admin |
| **E-16E-18** | OS adapter/MAC spoof recovery after crash |
| **E-19E-20** | Firefox PID tracking / relaunch loop limits |
| **E-21** | Boot task LIMITED vs admin kill-switch |
| **E-22** | GP proxy lock remediation (destructive) |
| **E-23** | Auto-rewrite sanitized settings — add “dont auto-save” option |
| **E-25** | `ban_tester.check_dns_leak()` uses useless `x-real-ip` header — **dead code**, unused by GUI; remove or replace with `dns_leak.run_dns_leak_test` |
| **E-28E-30** | Third-party intel APIs / map geo / GOST blacklist rules |
| **M-01M-14** | README alignment, GUI kill-switch button, encrypted vault, ban history, etc. |
| **U-01U-10** | Tray/minimize UX, admin relaunch save state, About version, cookie menu keys, password masking in list |
| **T-01T-14** | Test coverage expansion |
| **B-03B-10** | UAC manifest, PyInstaller pin, SHA256 sidecar, bundle GOST, SBOM, Defender scope |
### Medium (P2)
Installer polish, metrics, coverage gates, IPv6 chain path, SOCKS5 remote DNS policy (`browser_profile.py:125` — may be intentional for Firefox+GOST).
---
*Proxy God Audit — last updated 2026-05-22*

View File

@@ -1773,7 +1773,7 @@ def _main_inner() -> None:
preflight_card = _signup_card( preflight_card = _signup_card(
"Pre-flight check", "Pre-flight check",
"Exhaustive readiness check: chain, exit IP, geo/ASN, DNS leak, WebRTC, " "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]] = {} 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)" target_host = urlparse(url).hostname or "(unknown)"
def work() -> None: 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)) timeout = min(15.0, max(8.0, float(svc.settings.validation_timeout_seconds) + 2.0))
warns: list[str] = [] warns: list[str] = []
vpn_active = detect_vpn().active
# ── 1. Direct IP (bypass chain) ────────────────────────────────── # ── 1. Direct IP (bypass chain) ──────────────────────────────────
from .validator import get_direct_ip as _get_direct_ip from .validator import get_direct_ip as _get_direct_ip
@@ -1882,12 +1885,12 @@ def _main_inner() -> None:
_pf_set("exit_ip", "ok", ip_) _pf_set("exit_ip", "ok", ip_)
else: else:
_pf_set("exit_ip", "warn", "no exit IP yet") _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 and ip_ and ip_ not in ("", ""):
if direct_ip == ip_: if is_chain_leak(ip_, direct_ip, vpn_active):
_pf_set("leak_compare", "fail", reason = leak_reason(ip_, direct_ip, vpn_active)
f"exit == direct ({ip_}) — chain not working!") _pf_set("leak_compare", "fail", reason[:80])
warns.append("exit IP matches direct IP (chain not routing)") warns.append(f"IP leak: {reason}")
else: else:
_pf_set("leak_compare", "ok", _pf_set("leak_compare", "ok",
f"exit {ip_} ≠ direct {direct_ip}") f"exit {ip_} ≠ direct {direct_ip}")
@@ -2148,12 +2151,38 @@ def _main_inner() -> None:
tag = f" [{' · '.join(extras)}]" if extras else "" tag = f" [{' · '.join(extras)}]" if extras else ""
signup_status_var.set(f"Opened {label}{tag} — autofill active (you submit + captcha).") signup_status_var.set(f"Opened {label}{tag} — autofill active (you submit + captcha).")
_log(f"Signup prep: opened {url} with autofill extension{tag}.") _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 # Record draft metadata only — password saved when user clicks Save Account
_save_signup_account() _save_signup_account_draft_only()
else: else:
signup_status_var.set("Browser launch failed.") signup_status_var.set("Browser launch failed.")
svc.set_sticky(0) 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: def _save_signup_account() -> None:
draft = _persist_signup_draft() draft = _persist_signup_draft()
url = resolve_signup_url(draft) url = resolve_signup_url(draft)

View File

@@ -75,17 +75,18 @@ _BANNED_TEXT_HINTS = (
"temporarily blocked", "temporarily blocked",
"request blocked", "request blocked",
"unusual traffic", "unusual traffic",
"captcha",
"challenge required", "challenge required",
"bot detected", "bot detected",
"suspicious activity", "suspicious activity",
"geo-blocked", "geo-blocked",
"not available in your region", "not available in your region",
"your ip",
"ip address",
"cloudflare ray", "cloudflare ray",
"ddos protection", "ddos protection",
"attention required", "attention required",
"complete the captcha",
"captcha required",
"g-recaptcha",
"hcaptcha",
) )

View File

@@ -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).""" """Return True for HTTPS URLs pointing at public hosts (not RFC-1918/link-local)."""
try: try:
p = urlparse(url) p = urlparse(url)
if p.scheme not in ("http", "https"): if p.scheme != "https":
return False return False
host = p.hostname or "" host = p.hostname or ""
if not host: if not host:

View File

@@ -168,7 +168,12 @@ def emergency_disengage() -> None:
Unlike ``disengage()``, this never raises and does not require prior Unlike ``disengage()``, this never raises and does not require prior
admin check — it simply tries, logs the outcome, and returns. Safe to admin check — it simply tries, logs the outcome, and returns. Safe to
call from atexit or signal handlers where exceptions must not propagate. 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: try:
_set_outbound_policy("blockinbound,allowoutbound") _set_outbound_policy("blockinbound,allowoutbound")
_delete_rules() _delete_rules()

View File

@@ -141,14 +141,29 @@ def _check_webrtc_policy() -> tuple[bool, str]:
r"SOFTWARE\Policies\Google\Chrome", r"SOFTWARE\Policies\Google\Chrome",
r"SOFTWARE\Policies\Microsoft\Edge", r"SOFTWARE\Policies\Microsoft\Edge",
) )
_DWORD_DISABLE = 3 # disable_non_proxied_udp
_STR_KEY = "WebRtcIPHandling"
_STR_DISABLE = "disable_non_proxied_udp"
found = [] found = []
for p in paths: for p in paths:
try: try:
with winreg.OpenKey( with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, p, 0, winreg.KEY_QUERY_VALUE winreg.HKEY_LOCAL_MACHINE, p, 0, winreg.KEY_QUERY_VALUE
) as key: ) as key:
ok = False
try:
v = int(winreg.QueryValueEx(key, "DefaultWebRtcIpHandlingPolicy")[0]) v = int(winreg.QueryValueEx(key, "DefaultWebRtcIpHandlingPolicy")[0])
if v == 2: 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]) found.append(p.split("\\")[-1])
except OSError: except OSError:
continue continue

View File

@@ -390,6 +390,14 @@ class ChainService:
) )
# Pinned (manual) chain mode — skip pool management # 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: if self._settings.use_pinned_chain and self._settings.pinned_chain:
chain = list(self._settings.pinned_chain) chain = list(self._settings.pinned_chain)
rotation_num += 1 rotation_num += 1
@@ -564,22 +572,27 @@ class ChainService:
self._proc = None self._proc = None
return False 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": "hops", "hops": chain, "status": "healthy", "exit_ip": exit_ip})
self._notify({"type": "log", "text": f"✓ Chain 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}"}) 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": "phase", "phase": "running"}) self._notify({"type": "phase", "phase": "running"})
# Pre-flight: surface Group Policy locks (they will override us). # Pre-flight: surface Group Policy locks (they will override us).