diff --git a/CHANGELOG.md b/CHANGELOG.md index 1452486..595326a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. - **LICENSE**: MIT license added to repo root. - **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. - **`--version` flag**: `run.py` now supports `--version` / `-V`. diff --git a/ProxyChainManager.spec b/ProxyChainManager.spec index 572bf45..a7fcd41 100644 --- a/ProxyChainManager.spec +++ b/ProxyChainManager.spec @@ -60,7 +60,7 @@ exe = EXE( debug=False, bootloader_ignore_signals=False, strip=False, - upx=True, + upx=False, upx_exclude=[], runtime_tmpdir=None, console=False, diff --git a/docs/AUDIT_FINDINGS.md b/docs/AUDIT_FINDINGS.md index 1e348fb..0f2675a 100644 --- a/docs/AUDIT_FINDINGS.md +++ b/docs/AUDIT_FINDINGS.md @@ -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 `` 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-02–E-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-07–E-10** | Pool/pinned/exit edge cases need operator UX, not one-line fixes | +| **E-12–E-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-16–E-18** | OS adapter/MAC spoof recovery after crash | +| **E-19–E-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 “don’t 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-28–E-30** | Third-party intel APIs / map geo / GOST blacklist rules | +| **M-01–M-14** | README alignment, GUI kill-switch button, encrypted vault, ban history, etc. | +| **U-01–U-10** | Tray/minimize UX, admin relaunch save state, About version, cookie menu keys, password masking in list | +| **T-01–T-14** | Test coverage expansion | +| **B-03–B-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* diff --git a/proxy_chain_manager/app.py b/proxy_chain_manager/app.py index fc6408e..445dd27 100644 --- a/proxy_chain_manager/app.py +++ b/proxy_chain_manager/app.py @@ -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) diff --git a/proxy_chain_manager/ban_tester.py b/proxy_chain_manager/ban_tester.py index 8d4f27a..c84ecfb 100644 --- a/proxy_chain_manager/ban_tester.py +++ b/proxy_chain_manager/ban_tester.py @@ -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", ) diff --git a/proxy_chain_manager/config.py b/proxy_chain_manager/config.py index 78b9054..7d89723 100644 --- a/proxy_chain_manager/config.py +++ b/proxy_chain_manager/config.py @@ -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: diff --git a/proxy_chain_manager/firewall.py b/proxy_chain_manager/firewall.py index c47f4d6..3a2b22b 100644 --- a/proxy_chain_manager/firewall.py +++ b/proxy_chain_manager/firewall.py @@ -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() diff --git a/proxy_chain_manager/leak_audit.py b/proxy_chain_manager/leak_audit.py index 4841238..947bbdc 100644 --- a/proxy_chain_manager/leak_audit.py +++ b/proxy_chain_manager/leak_audit.py @@ -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 diff --git a/proxy_chain_manager/service.py b/proxy_chain_manager/service.py index cdff6ed..b070c72 100644 --- a/proxy_chain_manager/service.py +++ b/proxy_chain_manager/service.py @@ -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).