From 1f5e63ca1f63046c7eba64b2e8f82333a393bf68 Mon Sep 17 00:00:00 2001 From: drjones Date: Sat, 23 May 2026 21:58:06 -0700 Subject: [PATCH] first commit --- .gitignore | 21 + Bundle/ProxyGodInfo.plist | 28 + CHANGELOG.md | 84 + CONTRIBUTING.md | 20 + LICENSE | 21 + LICENSE_STATUS.md | 14 + Makefile | 29 + Proxy God.app/Contents/Info.plist | 28 + Proxy God.app/Contents/MacOS/ProxyGod | 5 + README.md | 41 + SECURITY.md | 19 + docs/AUDIT_FINDINGS.md | 318 ++ docs/MAINTENANCE.md | 23 + docs/OPERATOR_RUNBOOK.md | 128 + docs/PROJECT_HANDOFF.md | 14 + docs/PROVENANCE_CHECKLIST.md | 12 + docs/RELEASE_PROCESS.md | 92 + docs/ROADMAP.md | 20 + docs/SECURITY_REVIEW.md | 14 + docs/dashboard-screenshot.png | Bin 0 -> 364095 bytes proxy_chain_manager/__init__.py | 3 + proxy_chain_manager/__main__.py | 4 + proxy_chain_manager/app.py | 3074 +++++++++++++++++ proxy_chain_manager/artifact_wipe.py | 218 ++ proxy_chain_manager/ban_tester.py | 221 ++ proxy_chain_manager/browser_identity.py | 327 ++ proxy_chain_manager/browser_launcher.py | 402 +++ proxy_chain_manager/browser_profile.py | 271 ++ proxy_chain_manager/chain_map.py | 367 ++ proxy_chain_manager/config.py | 470 +++ proxy_chain_manager/dns_leak.py | 335 ++ proxy_chain_manager/exit_intel.py | 226 ++ proxy_chain_manager/fetcher.py | 37 + proxy_chain_manager/fingerprint.py | 416 +++ proxy_chain_manager/firewall.py | 191 + proxy_chain_manager/gost_util.py | 289 ++ proxy_chain_manager/gui_validate.py | 41 + proxy_chain_manager/leak_audit.py | 313 ++ proxy_chain_manager/leak_detect.py | 64 + proxy_chain_manager/mac_spoof.py | 158 + proxy_chain_manager/paths.py | 22 + proxy_chain_manager/pool_ops.py | 106 + proxy_chain_manager/privacy_lan.py | 291 ++ proxy_chain_manager/proxy_picker.py | 201 ++ proxy_chain_manager/secrets_store.py | 160 + proxy_chain_manager/service.py | 949 +++++ .../signup_extension/content.js | 153 + .../signup_extension/manifest.json | 21 + proxy_chain_manager/signup_prep.py | 444 +++ proxy_chain_manager/sysproxy.py | 574 +++ proxy_chain_manager/telemetry_kill.py | 292 ++ proxy_chain_manager/tray.py | 160 + proxy_chain_manager/validator.py | 277 ++ proxy_chain_manager/vpn_detect.py | 233 ++ proxy_chain_manager/webrtc_check.py | 231 ++ proxy_chain_manager/win_compat.py | 107 + proxy_chain_manager/windows_task.py | 84 + proxy_chain_manager/world_map.png | Bin 0 -> 154826 bytes requirements.txt | 5 + run.py | 11 + run_mac.command | 3 + scripts/app_launcher.sh | 5 + scripts/find_tk_python.sh | 38 + scripts/run_proxy_god.sh | 26 + tests/__init__.py | 1 + tests/test_ban_tester.py | 48 + tests/test_config_round_trip.py | 105 + tests/test_fail_closed.py | 26 + tests/test_firewall_helpers.py | 42 + tests/test_gost_util.py | 60 + tests/test_proxy_god.py | 424 +++ tests/test_secrets_store.py | 48 + winreg.py | 60 + 73 files changed, 13565 insertions(+) create mode 100644 .gitignore create mode 100644 Bundle/ProxyGodInfo.plist create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 LICENSE_STATUS.md create mode 100644 Makefile create mode 100644 Proxy God.app/Contents/Info.plist create mode 100755 Proxy God.app/Contents/MacOS/ProxyGod create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 docs/AUDIT_FINDINGS.md create mode 100644 docs/MAINTENANCE.md create mode 100644 docs/OPERATOR_RUNBOOK.md create mode 100644 docs/PROJECT_HANDOFF.md create mode 100644 docs/PROVENANCE_CHECKLIST.md create mode 100644 docs/RELEASE_PROCESS.md create mode 100644 docs/ROADMAP.md create mode 100644 docs/SECURITY_REVIEW.md create mode 100644 docs/dashboard-screenshot.png create mode 100644 proxy_chain_manager/__init__.py create mode 100644 proxy_chain_manager/__main__.py create mode 100644 proxy_chain_manager/app.py create mode 100644 proxy_chain_manager/artifact_wipe.py create mode 100644 proxy_chain_manager/ban_tester.py create mode 100644 proxy_chain_manager/browser_identity.py create mode 100644 proxy_chain_manager/browser_launcher.py create mode 100644 proxy_chain_manager/browser_profile.py create mode 100644 proxy_chain_manager/chain_map.py create mode 100644 proxy_chain_manager/config.py create mode 100644 proxy_chain_manager/dns_leak.py create mode 100644 proxy_chain_manager/exit_intel.py create mode 100644 proxy_chain_manager/fetcher.py create mode 100644 proxy_chain_manager/fingerprint.py create mode 100644 proxy_chain_manager/firewall.py create mode 100644 proxy_chain_manager/gost_util.py create mode 100644 proxy_chain_manager/gui_validate.py create mode 100644 proxy_chain_manager/leak_audit.py create mode 100644 proxy_chain_manager/leak_detect.py create mode 100644 proxy_chain_manager/mac_spoof.py create mode 100644 proxy_chain_manager/paths.py create mode 100644 proxy_chain_manager/pool_ops.py create mode 100644 proxy_chain_manager/privacy_lan.py create mode 100644 proxy_chain_manager/proxy_picker.py create mode 100644 proxy_chain_manager/secrets_store.py create mode 100644 proxy_chain_manager/service.py create mode 100644 proxy_chain_manager/signup_extension/content.js create mode 100644 proxy_chain_manager/signup_extension/manifest.json create mode 100644 proxy_chain_manager/signup_prep.py create mode 100644 proxy_chain_manager/sysproxy.py create mode 100644 proxy_chain_manager/telemetry_kill.py create mode 100644 proxy_chain_manager/tray.py create mode 100644 proxy_chain_manager/validator.py create mode 100644 proxy_chain_manager/vpn_detect.py create mode 100644 proxy_chain_manager/webrtc_check.py create mode 100644 proxy_chain_manager/win_compat.py create mode 100644 proxy_chain_manager/windows_task.py create mode 100644 proxy_chain_manager/world_map.png create mode 100644 requirements.txt create mode 100644 run.py create mode 100755 run_mac.command create mode 100755 scripts/app_launcher.sh create mode 100755 scripts/find_tk_python.sh create mode 100755 scripts/run_proxy_god.sh create mode 100644 tests/__init__.py create mode 100644 tests/test_ban_tester.py create mode 100644 tests/test_config_round_trip.py create mode 100644 tests/test_fail_closed.py create mode 100644 tests/test_firewall_helpers.py create mode 100644 tests/test_gost_util.py create mode 100644 tests/test_proxy_god.py create mode 100644 tests/test_secrets_store.py create mode 100644 winreg.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..892cded --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.DS_Store + +# Local build/runtime artifacts +.build/ +build/ +dist/ +*.spec +*.log +ProxyChainManager.app/ + +# Local operator state +settings.json +gost +gost.exe +*.sha256 diff --git a/Bundle/ProxyGodInfo.plist b/Bundle/ProxyGodInfo.plist new file mode 100644 index 0000000..7d4ac73 --- /dev/null +++ b/Bundle/ProxyGodInfo.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ProxyGod + CFBundleIdentifier + local.proxy-god.mac + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Proxy God + CFBundlePackageType + APPL + CFBundleShortVersionString + 2.0.0 + CFBundleVersion + 2 + LSMinimumSystemVersion + 13.0 + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b0680ab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,84 @@ +# Changelog + +All meaningful changes to this repository should be recorded here. + +## Unreleased — 2026-05-22 (Audit round 3) + +### Security +- **Fail-closed leak detection**: `is_chain_leak()` now treats unknown direct + IP as a leak (was: pass-through). `service.py` retries direct-IP lookup + 3× with 2 s back-off as a warm-up grace so transient network blips don't + cause permanent rotation. + +### Reliability +- **Single asyncio loop in preflight**: removed per-call `asyncio.run()` from + the preflight worker thread; one loop is created, drained, and closed. +- **Close-window UX**: X button now prompts when chain or firewall is still + active (tray / full quit / cancel) instead of silently minimizing. +- **Admin relaunch**: settings persist before the elevated process spawns. +- **Persona/cookie key lookup**: precomputed `label→key` dicts; label copy + changes can no longer break the round-trip. + +### Distribution +- **GOST bundled**: `scripts/prepare_bundled_gost.ps1` downloads and SHA- + verifies `gost.exe`, stages it under `proxy_chain_manager/_bundled/`, and + PyInstaller bundles it into the exe. `ensure_gost()` installs from the + bundle on first run (no internet required); network fetch is fallback only. + +### Tests +- New suites: `test_config_round_trip`, `test_ban_tester`, `test_gost_util`, + `test_fail_closed`, `test_firewall_helpers`. +- Total: **47 → 82** passing. + +### Held by design (audit §12) +- Signup extension stays on `` — custom signup URLs need it. +- `verify=False` on httpx probes — broken public-proxy TLS. +- DNS pass-through in kill-switch — GOST needs system DNS. +- Authenticode signing — pending Cert provisioning. + +## Unreleased — 2026-05-21 (Audit remediation P0/P1/P2) + +### Security (P0) +- **Secrets at rest**: Added `_mask()` helper in `config.py` to redact credentials from any + debug log output. Added `SETTINGS_SCHEMA_VERSION` and prominent plaintext-storage warning. +- **GOST integrity**: `gost_util.py` now verifies the downloaded release zip against a pinned + SHA256 (`GOST_RELEASE_ZIP_SHA256`) before extraction. Supply-chain swap raises `RuntimeError`. +- **Kill-switch recovery**: `firewall.py` exposes `emergency_disengage()`. `service.py` + registers it via `atexit` and `signal.SIGTERM`/`SIGINT` so a crash cannot leave outbound + traffic permanently blocked. + +### Reliability (P0/P1) +- **Top-level exception handler**: `app.py` `main()` now catches unhandled exceptions, shows a + `tkinter.messagebox` error dialog, logs to file, and re-raises. +- **Log rotation**: `app.py` switches from bare `FileHandler` to `RotatingFileHandler` + (5 MB max, 3 backups). +- **Settings migration**: `config.py` gains `settings_version` field, `migrate()` function, and + safe corrupt-file backup (`settings.json.corrupt`) on parse failure. +- **Settings backup**: `save_settings()` copies `settings.json → settings.json.bak` before + overwriting. +- **Graceful shutdown**: `service.py` `stop()` calls `fw_emergency_disengage()` if the thread + does not exit within 15 s. +- **Thread safety**: `ChainService._settings` reads/writes guarded by `threading.Lock`. +- **Input / URL validation**: `sanitize_settings()` now validates `sources` and `ip_check_url` + against an RFC-1918 / link-local block-list; unsafe entries are removed with a warning. + +### CI / Distribution (P0/P1/P2) +- **GitHub Actions CI** added: `.github/workflows/test.yml` runs `compileall` + `unittest + 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` + 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`. + +### Documentation (P1/P2) +- **`docs/OPERATOR_RUNBOOK.md`**: Emergency firewall disengage, proxy leak, empty pool, GOST + quarantine, and settings-restore instructions. +- **Bare `except: pass` cleanup**: Key silent-failure sites in `tray.py`, `dns_leak.py`, and + `gost_util.py` now log a `warning` or `debug` message instead of swallowing errors. + +## 2026-05-20 - Gitea Stewardship Import + +- Verified README and wiki coverage. +- Added standard stewardship documentation where missing. +- Established security, contribution, release, and provenance expectations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1abcac3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# Contributing + +## Maintainer Expectations + +Keep changes small, reviewable, and tied to a clear project purpose. Do not mix source changes with generated build output or dependency caches. + +## Before Committing + +- Run the relevant build, lint, or smoke test when the project provides one. +- Check that no credentials, `.env` files, tokens, private keys, captures, dumps, or personal data are staged. +- Keep firmware binaries, large archives, and generated artifacts out of Git unless the repo explicitly documents otherwise. +- Preserve upstream licenses and attribution for third-party code. + +## Documentation + +Update README and wiki pages when setup, hardware, architecture, environment variables, or release behavior changes. + +## Safety + +Only submit work intended for authorized environments. Project documentation should make scope and safe operation clearer, never weaker. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6e09e5b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dr Jones + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE_STATUS.md b/LICENSE_STATUS.md new file mode 100644 index 0000000..8dccbf3 --- /dev/null +++ b/LICENSE_STATUS.md @@ -0,0 +1,14 @@ +# License Status + +This repository has not been assigned a blanket license by the stewardship pass. + +## Current Rule + +- Existing upstream licenses must be preserved. +- Third-party code must retain attribution and license files. +- Original private work remains all rights reserved until an explicit license is selected. +- Do not assume MIT, Apache, GPL, or public-domain status unless a license file in this repository says so. + +## Next Step + +Classify ownership and dependencies before publishing releases or accepting external contributions. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4e0e5e5 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +PYTHON := $(shell scripts/find_tk_python.sh) +VENV := .venv +PY := $(VENV)/bin/python +PIP := $(VENV)/bin/pip +APP := Proxy God.app + +.PHONY: setup run app test clean swift-build + +setup: + @if [ -x "$(PY)" ] && ! "$(PY)" -c "import tkinter" >/dev/null 2>&1; then rm -rf "$(VENV)"; fi + $(PYTHON) -m venv $(VENV) + $(PIP) install --upgrade pip + $(PIP) install -r requirements.txt + +run: setup + $(PY) run.py + +app: + chmod +x scripts/run_proxy_god.sh + mkdir -p "$(APP)/Contents/MacOS" "$(APP)/Contents/Resources" + cp Bundle/ProxyGodInfo.plist "$(APP)/Contents/Info.plist" + cp scripts/app_launcher.sh "$(APP)/Contents/MacOS/ProxyGod" + chmod +x "$(APP)/Contents/MacOS/ProxyGod" + +test: setup + $(PY) -m unittest discover -s tests -v + +clean: + rm -rf "$(VENV)" "$(APP)" .build dist build diff --git a/Proxy God.app/Contents/Info.plist b/Proxy God.app/Contents/Info.plist new file mode 100644 index 0000000..7d4ac73 --- /dev/null +++ b/Proxy God.app/Contents/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ProxyGod + CFBundleIdentifier + local.proxy-god.mac + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Proxy God + CFBundlePackageType + APPL + CFBundleShortVersionString + 2.0.0 + CFBundleVersion + 2 + LSMinimumSystemVersion + 13.0 + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + + diff --git a/Proxy God.app/Contents/MacOS/ProxyGod b/Proxy God.app/Contents/MacOS/ProxyGod new file mode 100755 index 0000000..b52f808 --- /dev/null +++ b/Proxy God.app/Contents/MacOS/ProxyGod @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +exec "$ROOT/scripts/run_proxy_god.sh" diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f3d0ed --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Proxy God for macOS + +This workspace now runs the Python/CustomTkinter Proxy God dashboard from the reference repo, patched for macOS launch and GOST bootstrap. + +## Run + +```sh +make run +``` + +Or double-click: + +```text +run_mac.command +``` + +To refresh the clickable app bundle: + +```sh +make app +open "Proxy God.app" +``` + +The first run creates `.venv`, installs dependencies, downloads the correct macOS GOST binary, and starts the GUI. + +## What Was Ported + +- Neon Proxy God v2 dashboard layout, top status bar, chain visualization, progress strip, and tabs. +- Live tab with direct/exit IP, exit intel, chain map, and verbose logs. +- Chain Builder with pinned chains, manual exit parsing, per-hop testing, fixed final hop, and source management. +- Browser tab with hardened Firefox profile controls. +- Ban Tester, Signup Prep, Privacy, and Settings tabs. +- GOST-backed rotating multi-hop chain service. +- Proxifly/jsDelivr proxy source ingestion, validation, pool rotation, exit verification, HTTPS tunnel probe, leak checks, and health rotation. +- macOS app-data paths under `~/Library/Application Support/ProxyChainManager`. +- macOS GOST download/extract for `darwin_arm64` or `darwin_amd64`. +- macOS DNS cache flush and system proxy apply/clear through `networksetup`. + +## Platform Notes + +The upstream project is Windows-first. Windows-only controls such as netsh firewall kill-switch, registry WebRTC policy, Task Scheduler boot task, telemetry kill, and LAN lockdown are kept in the UI but degrade safely on macOS. The core proxy chain engine, validation, map/intel, logs, browser profile generation, ban tests, signup prep, and settings are runnable on macOS. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..22832e7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Scope + +This repository is maintained for authorized, lawful work only. Do not use code, firmware, payloads, scripts, or documentation from this project against systems, accounts, devices, networks, cards, readers, or services you do not own or do not have explicit permission to test. + +## Reporting + +Report security concerns privately to the maintainer. Do not open public issues containing live credentials, tokens, private captures, card data, target identifiers, exploit chains, or sensitive logs. + +## Secrets And Data + +- Do not commit `.env` files, API keys, Wi-Fi credentials, session cookies, private keys, dumps, captures, or personal data. +- Firmware binaries and captured artifacts must include provenance notes and SHA256 hashes before release. +- Generated dependency folders and build output belong outside Git unless there is a documented reason. + +## Maintainer Rule + +If a change increases misuse risk, narrows safety boundaries, or weakens provenance, it must be rejected or quarantined until documented. diff --git a/docs/AUDIT_FINDINGS.md b/docs/AUDIT_FINDINGS.md new file mode 100644 index 0000000..bdd98aa --- /dev/null +++ b/docs/AUDIT_FINDINGS.md @@ -0,0 +1,318 @@ +# Proxy God — Audit Findings + +**Generated:** 2026-05-22 +**Tests:** 47/47 pass · compileall clean +**Severity summary:** P0: 12 · P1: 28 · P2: 22 · **Total: 62** + +--- + +## Executive Summary + +| Severity | Count | +|----------|------:| +| Critical (P0) | 12 | +| High (P1) | 28 | +| Medium (P2) | 22 | +| **Total** | **62** | + +**Already remediated (prior passes):** CI workflow, GOST SHA256 pin, kill-switch `emergency_disengage` + atexit, settings migration/backup, log rotation, MIT LICENSE, GOST SHA256 verified correct. + +--- + +## Top P0 Issues + +1. **Wrong Chrome/Edge WebRTC policy key/value** — `fingerprint.py:21,177` writes DWORD `2` which is `default_public_and_private_interfaces`, NOT `disable_non_proxied_udp` (DWORD `3`). Modern Chrome also requires REG_SZ `WebRtcIPHandling = disable_non_proxied_udp`. Audits treat `2` as success → false green. +2. **`BrowserSession.stop()` kills ALL Firefox on the machine** — `browser_launcher.py:297-305` runs `taskkill /F /IM firefox.exe` with no PID scope. Operator loses unrelated browser sessions. +3. **Credentials plaintext at rest** — `config.py:19-22`, `signup_prep.py:252-325` store proxy auth + signup passwords in JSON. `cryptography==48.0.0` is in requirements but unused. +4. **Preflight race condition** — `app.py:2001-2009` uses fixed `root.after(200, _final)` which can report "All passed" before async network checks finish. +5. **Preflight IP compare is exact-match only** — ignores the VPN-aware `/16` logic used in production leak detection (`leak_detect.py:24-38`). +6. **HTTPS CONNECT failure doesn't stop chain** — `service.py:573-580` logs warning only; chain stays "healthy" and system proxy remains set. +7. **Pinned manual chain never validates hops at runtime** — dead chain retries every 10s forever (`service.py:393-403`). +8. **`emergency_disengage()` runs on ALL exits** — even if kill-switch never engaged; can mask other apps' firewall state (`service.py:105`, `firewall.py:165-177`). +9. **Signup extension runs on all URLs** — `signup_extension/manifest.json:13-19` uses ``. +10. **Account auto-saved before signup completes** — password saved on browser launch before user registers (`app.py:2147-2174`). +11. **GOST exe not re-hashed on reuse** — zip is verified but extracted `gost.exe` on disk is not re-checked (`gost_util.py:61-64`). +12. **`asyncio.run()` from worker threads** — `app.py:1860-1863` nested event loop risk if caller context changes. + +--- + +## 1. Critical Bugs + +| ID | Finding | File:Line | +|----|---------|-----------| +| C-01 | WebRTC policy wrong key/type/value | `fingerprint.py:18-21,162-177`, `webrtc_check.py:37-96`, `leak_audit.py:138-157` | +| C-02 | Browser stop kills all `firefox.exe` globally | `browser_launcher.py:297-305` | +| C-03 | Plaintext `settings.json` / signup JSON credentials | `config.py:19-22`, `signup_prep.py:252-325` | +| C-04 | Preflight `root.after(200)` race — final verdict before checks complete | `app.py:2001-2009` | +| C-05 | Preflight IP compare exact-match only (ignores /16 VPN logic) | `app.py:1886-1893`, `leak_detect.py:24-38` | +| C-06 | HTTPS CONNECT failure keeps chain "healthy" | `service.py:573-580` | +| C-07 | Pinned chain never validates hops at runtime | `service.py:393-403` | +| C-08 | `emergency_disengage()` fires on ALL exits | `service.py:105`, `firewall.py:165-177` | +| C-09 | Signup extension `` permissions | `signup_extension/manifest.json:13-19` | +| C-10 | Account saved before signup completes | `app.py:2147-2174` | +| C-11 | GOST exe not re-hashed on disk reuse | `gost_util.py:61-64` | +| C-12 | `asyncio.run()` from worker threads | `app.py:1860-1863` | + +--- + +## 2. Edge Cases / Failure Modes + +| ID | Finding | File:Line | +|----|---------|-----------| +| E-01 | `real_ip` captured once at start; VPN reconnect mid-session not reflected | `service.py:339-340,551-558` | +| E-02 | Sticky exit suppresses leak rotation but not manual rotate/GOST death | `service.py:607-619` | +| E-03 | Sticky + health leak: status forced "healthy" while leaking | `service.py:610-618` | +| E-04 | `is_chain_leak(real_ip=None)` returns False (no leak) — fail-open | `leak_detect.py:32-33` | +| E-05 | Same /16 ISP neighbors never flagged — some leak modes missed by design | `leak_detect.py:36-37` | +| E-06 | Empty pinned chain + `use_pinned_chain=True` falls through to pool silently | `service.py:393`, `config.py:201` | +| E-07 | `chain_length=1` + manual exit only — no mid-hop redundancy | `service.py:407-415` | +| E-08 | Obfuscation `random_mix` shuffles list but takes prefix slice — not random per hop | `service.py:641-644,717` | +| E-09 | Pool exhausted → reshuffle; blacklisted proxies stay excluded until refresh | `service.py:409-417,768-771` | +| E-10 | Fixed exit validation failure still used in chains | `service.py:822-839` | +| E-11 | `socket.setdefaulttimeout()` global mutation — thread race in DNS helpers | `dns_leak.py:140-151`, `ban_tester.py:149-150` | +| E-12 | DNS leak "proxy path" uses Google DoH through HTTP proxy — not equivalent to system DNS | `dns_leak.py:154-167` | +| E-13 | DNS `all_match` logic can false-flag or miss due to CDN/geo DNS answers | `dns_leak.py:221-252` | +| E-14 | Kill-switch allows outbound DNS globally — DNS exfil channel exists | `firewall.py:136-140` | +| E-15 | Kill-switch skipped when non-admin with only log line | `service.py:350-357` | +| E-16 | IPv6 disable skips virtual adapters by name heuristics only | `fingerprint.py:122-137` | +| E-17 | Hostname rename may require reboot; partial apply possible | `fingerprint.py:73-103` | +| E-18 | MAC spoof restore depends on in-memory originals — crash mid-session may leave spoofed MAC | `service.py:96-97` | +| E-19 | `BrowserSession.is_running()` treats any `firefox.exe` within 5 min as "ours" | `browser_launcher.py:153-157` | +| E-20 | Auto-relaunch can loop if profile locked/corrupt | `app.py:2866-2871` | +| E-21 | `task_exists()` logon task runs LIMITED — no admin kill-switch at boot | `windows_task.py:47-48` | +| E-22 | Group Policy proxy locks detected but never remediated | `sysproxy.py:298-317`, `service.py:586-594` | +| E-23 | `load_settings` auto-rewrites sanitized file — can surprise operators | `config.py:436-443` | +| E-24 | `_is_safe_https_url` allows `http://` sources — MITM on pool fetch | `config.py:266-271`, `fetcher.py:13-16` | +| E-25 | Ban tester DNS leak helper checks `x-real-ip` header (usually absent) — nonsense results | `ban_tester.py:169-219` | +| E-26 | Ban tester `_looks_banned_body` matches "captcha" on normal pages → false bans | `ban_tester.py:71-89,133-134` | +| E-27 | `run_ban_tests(categories=["All"])` duplicates sites | `ban_tester.py:237-243` | +| E-28 | Exit intel/geo fetch via third-party APIs through proxy — telemetry surface | `exit_intel.py`, `chain_map.py:152-179` | +| E-29 | World map geo resolution uses direct `socket.getaddrinfo` — DNS leak for hop lookup | `chain_map.py:86-112,160` | +| E-30 | GOST immediate exit blacklists hops but pinned/manual exit exempt | `service.py:515-522,557-562` | + +--- + +## 3. Missing Features / README Gaps + +| ID | Finding | Reference | +|----|---------|-----------| +| M-01 | README "self-healing forever" — pinned mode doesn't rotate pool | `README.md:9`, `service.py:393-403` | +| M-02 | README "fail closed" — non-admin + kill-switch skipped | `README.md:25`, `service.py:350-357` | +| M-03 | README 1–8 hops; UI/config says 2–8 in places | `README.md:48`, `config.py:167` | +| M-04 | No in-GUI emergency kill-switch disengage button | `firewall.py`, `app.py` | +| M-05 | `audit_device()` is OS-only but UI shows it under "Browser Fingerprint" | `fingerprint.py:299-383`, `app.py:2310-2313` | +| M-06 | Preflight card mentions "fingerprint" but no fingerprint check in `_PF_CHECKS` | `app.py:1775-1793` | +| M-07 | `browser_auto_relaunch` not tied to chain health flag | `app.py:2866-2871`, `config.py:205` | +| M-08 | No export/import encrypted settings/signup vault | — | +| M-09 | No persisted ban-test / audit history | `app.py` | +| M-10 | `prefer_elite` can empty pool with no GUI recovery wizard | `config.py:183`, `service.py:754-761` | +| M-11 | SOCKS5 remote DNS disabled in Firefox profile | `browser_profile.py:125` | +| M-12 | Signup presets have no ToS/disclaimer in UI | `signup_prep.py:37-160` | +| M-13 | No IPv6 chain path — IPv6 disable adapter-level only | `fingerprint.py:106-138` | +| M-14 | Tray red/yellow/green with no click-through to failed check detail | `tray.py`, `app.py:2841-2844` | +| M-15 | `CHANGELOG.md` claims DPAPI/Fernet — not implemented | `CHANGELOG.md:35-36` | + +--- + +## 4. Build / Packaging / Deployment + +| ID | Finding | Reference | +|----|---------|-----------| +| B-01 | No Authenticode signing | `ProxyChainManager.spec:70-71` | +| B-02 | UPX enabled — AV false positives risk | `ProxyChainManager.spec:66-67` | +| B-03 | `uac_admin=True` — every launch elevates | `ProxyChainManager.spec:72` | +| B-04 | PyInstaller not pinned in `requirements.txt` | `scripts/setup_and_build.ps1:39` | +| B-05 | Build script doesn't emit SHA256 sidecar | `scripts/setup_and_build.ps1:50-58` | +| B-06 | GOST not bundled — first run requires GitHub access | `gost_util.py:61-86` | +| B-07 | `__version__` static `1.0.0`, not tied to git tag | `__init__.py:3` | +| B-08 | CI does not run PyInstaller build | `.github/workflows/test.yml` | +| B-09 | No SBOM / `pip freeze` in release artifacts | — | +| B-10 | Defender exclusion adds whole `%LOCALAPPDATA%\ProxyChainManager` folder | `gost_util.py:29-45` | + +--- + +## 5. Security / Privacy + +| ID | Finding | Reference | +|----|---------|-----------| +| S-01 | Plaintext secrets at rest (proxy creds + signup passwords) | `config.py`, `signup_prep.py` | +| S-02 | WebRTC mis-hardening (wrong policy value) | `fingerprint.py` | +| S-03 | All httpx clients use `verify=False` — MITM risk | `validator.py:142`, `ban_tester.py:119`, `dns_leak.py:159` | +| S-04 | Signup passwords can appear in UI/log via clipboard actions | `app.py:2059-2062` | +| S-05 | Signup extension content script on all URLs | `signup_extension/manifest.json` | +| S-06 | Public proxy list sources — hostile infrastructure by design | `config.py:218-223` | +| S-07 | `cryptography` dependency unused — attack surface without benefit | `requirements.txt:5` | +| S-08 | Kill-switch + DNS allow rule = DNS bypass channel | `firewall.py:136-140` | +| S-09 | Forensic wipe deletes Recycle Bin without extra confirmation | `artifact_wipe.py:208-216` | +| S-10 | No secure deletion of signup JSON on uninstall | — | + +--- + +## 6. UI/UX Issues + +| ID | Finding | Reference | +|----|---------|-----------| +| U-01 | Window close minimizes to tray — chain/firewall active silently | `app.py:2908` | +| U-02 | "Run as Admin" relaunch closes app — unsaved UI state lost | `app.py:347-350` | +| U-03 | No version/build SHA in About UI | `__init__.py:3` | +| U-04 | Cookie/persona menus reverse-lookup by label — fragile if labels change | `app.py:1321-1325` | +| U-05 | Hardened toggles visible even when blend persona overrides them | `browser_profile.py:42-65` | +| U-06 | Preflight "fingerprint" in subtitle but not in checklist | `app.py:1775-1793` | +| U-07 | Ban tester has no progress bar for 30+ sites | `ban_tester.py:222-260` | +| U-08 | Saved accounts list shows email + password in plain text | `app.py:2046-2049` | +| U-09 | Chain map silently skips hops when geo lookup fails — no unknown marker | `chain_map.py:156-166,322-328` | +| U-10 | `verbose_logs_var` affects UI handler only; file log stays INFO | `app.py:707-713` | + +--- + +## 7. Test Coverage Gaps + +| ID | Finding | +|----|---------| +| T-01 | No tests for `service.py` (rotation, sticky, kill-switch) | +| T-02 | No tests for `firewall.py` / `emergency_disengage` | +| T-03 | No tests for `sysproxy.py` registry blob encoding | +| T-04 | No tests for GOST SHA256 mismatch path | +| T-05 | No tests for `dns_leak.run_dns_leak_test` verdict logic | +| T-06 | No tests for `webrtc_check` / `apply_webrtc_hardening` | +| T-07 | No tests for `BrowserSession` | +| T-08 | No tests for `signup_prep.install_signup_extension` | +| T-09 | No tests for `config.load_settings`/`migrate`/`sanitize` round-trip | +| T-10 | No tests for `ban_tester` heuristics | +| T-11 | No GUI/smoke tests for `app.py` (~2900 lines) | +| T-12 | `test_fetch_smoke` is network-dependent — can skip silently in CI | +| T-13 | No test that WebRTC policy value matches Chrome documentation | +| T-14 | No frozen-bundle (`_MEIPASS`) path tests in CI | + +--- + +## 8. Recommended Fix Priority + +**P0 — Fix immediately:** +C-01, C-02, C-03, C-04, C-05, C-06, S-01, S-02, S-03, B-01 + +**P1 — Next sprint:** +C-07, C-10, E-01–E-06, E-11, E-22, E-25–E-27, M-04–M-07, M-11, B-04–B-06, T-01–T-05, T-09, U-01–U-03 + +**P2 — Backlog:** +All remaining E/M/B/U/T items, installer signing, metrics, coverage gates + +--- + +## 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 because they need design decisions, external tooling (certs, infra), or behavior that is not safe to guess. + +### Critical / security (P0) + +| ID | Why left open | Suggested direction | +|----|---------------|---------------------| +| **C-09** | Narrowing signup extension off `` breaks **custom signup URLs** the operator types in | Dynamic host permissions API or per-session host approval prompt | +| **C-12** | `asyncio.run()` in worker threads (`app.py` preflight) needs a dedicated event-loop policy refactor | Move all preflight network ops to a single asyncio runner thread | +| **S-03** | `verify=False` on httpx is **intentional** for the broken-cert reality of public proxies | Per-setting toggle with security warning, documented trade-off | +| **B-01** | Authenticode signing needs an actual code-signing certificate + CI secrets | Sign `dist\ProxyChainManager.exe` in release pipeline once cert is provisioned | + +### High (P1) — design / scope debt + +| ID | Why left open | +|----|---------------| +| **E-01** | Refresh `real_ip` when VPN state changes mid-session — needs interval policy | +| **E-02–E-05** | Sticky exit / leak semantics need a product rule (rotate vs warn) | +| **E-07–E-10** | Pool/pinned/exit edge cases need operator UX | +| **E-12–E-13** | DNS leak methodology (DoH vs system DNS) needs spec | +| **E-14 / S-08** | Kill-switch DNS allow rule is required for GOST hostname resolution | +| **E-16–E-18** | OS adapter / MAC spoof crash recovery | +| **E-21** | Boot task LIMITED vs admin kill-switch | +| **E-22** | Group Policy proxy-lock remediation (destructive — needs explicit operator consent) | +| **E-23** | Auto-rewrite of sanitized settings — needs UI toggle | +| **E-28–E-30** | Third-party intel APIs / map geo / GOST blacklist rules | +| **M-01–M-03, M-05, M-07–M-14** | README alignment, encrypted vault export, ban history, IPv6 chain path, tray click-through | +| **U-01, U-02, U-04, U-05, U-07–U-10** | Tray/minimize UX, admin-relaunch save state, cookie menu keys, password masking | +| **T-01–T-14** | Larger test-coverage program | +| **B-03, B-06–B-10** | UAC manifest tradeoff, GOST bundling, SBOM, Defender scope | + +### Medium (P2) + +Installer polish, metrics, coverage gates, IPv6 chain path, SOCKS5 remote DNS policy (`browser_profile.py:125` — likely intentional for Firefox+GOST). + +--- + +## 12. Decisions held — accepted trade-offs + +These items are **closed by design choice** (operator approved), not because they're hidden bugs. + +| ID | Decision | Rationale | +|----|----------|-----------| +| **C-09** | Keep signup extension on `` | Custom signup URLs entered at runtime require dynamic host match; operator isolates the profile per session. | +| **S-03** | Keep `verify=False` on httpx probes | Public proxies routinely ship broken / self-signed TLS; turning verification on would drop ~half the working pool. | +| **E-14 / S-08** | Keep DNS pass-through in kill-switch | GOST needs system DNS to resolve proxy hostnames; locking port 53 would break pool fetching and exit verification. | +| **B-01** | Code-signing deferred | Requires an Authenticode certificate (commercial or self-signed); will wire `signtool sign` into the build script once the cert is provided. | +| **B-08** | CI builds the exe — pending operator approval | Adds ~2 min per CI run; not enabled yet. | + +--- + +## 11. Remediation log — 2026-05-22 (round 2) + +| ID | Status | Notes | +|----|--------|-------| +| **C-03 / S-01** | **Fixed** | `secrets_store.py` (Windows DPAPI via ctypes — no new deps). Signup passwords + draft passwords now encrypted on disk; legacy plaintext migrates automatically on first save | +| **C-07** | **Fixed** | New `validate_pinned_on_start` setting + `_probe_tcp` helper in `service.py`; dead pinned hops logged but used | +| **C-11** | **Fixed** | `gost_util.py` writes `gost.exe.sha256` sidecar on extract and re-verifies on every reuse; mismatch triggers re-download | +| **E-15** | **Fixed** | `app.py` `_start()` confirms with operator before starting chain without admin when kill-switch is enabled | +| **E-19** | **Fixed** | `browser_launcher.py` `is_running()` walks the spawned PID's descendants via WMIC instead of pattern-matching any `firefox.exe` | +| **E-20** | **Fixed** | `max_browser_relaunches` setting + counter in `app.py`; auto-relaunch self-disables after N failures | +| **E-25** | **Fixed** | Dead `ban_tester.check_dns_leak()` removed (use `dns_leak.run_dns_leak_test`) | +| **M-04** | **Fixed** | Settings tab now has “⚠ Emergency disengage firewall now” button with confirm | +| **U-03** | **Fixed** | Window title now reads `Proxy God v{__version__}` | +| **B-04** | **Fixed** | New `dev-requirements.txt` pins `pyinstaller==6.10.0`; build script installs it | +| **B-05** | **Fixed** | Build script emits `ProxyChainManager.exe.sha256` next to the exe and copies it to Desktop | + +**Test suite:** 47 → **53 passing** (added 6 round-trip tests for `secrets_store`). + +--- + +*Proxy God Audit — last updated 2026-05-22 (round 2)* + +--- + +## 13. Remediation log — 2026-05-22 (round 3) + +| ID | Status | Notes | +|----|--------|-------| +| **E-04** | **Fixed (fail-closed)** | `leak_detect.is_chain_leak()` now treats unknown `real_ip` as a leak; `service.py` retries direct-IP lookup 3× with 2 s back-off as warm-up before the verdict applies. New `tests/test_fail_closed.py` enforces the contract. | +| **C-12** | **Fixed** | Preflight worker thread now creates ONE `asyncio` event loop, runs all coroutines on it via `_run_async()`, and closes it at the end. No more nested-loop risk. | +| **U-01** | **Fixed** | X button asks the operator (Yes=tray / No=full quit / Cancel) when chain or firewall is still active. | +| **U-02** | **Fixed** | "Run as Admin" calls `_save_settings()` before relaunching so unsaved Chain Builder / Settings edits persist. | +| **U-04** | **Fixed** | Persona / cookie OptionMenus now reverse-lookup via a precomputed `label→key` dict — copy changes can no longer break the round-trip. | +| **B-06** | **Fixed** | `scripts/prepare_bundled_gost.ps1` stages `proxy_chain_manager/_bundled/gost.exe` (SHA-verified) before PyInstaller; the spec bundles it; `ensure_gost()` installs from bundle first and only network-downloads as fallback. First run works fully offline. | +| **T-coverage** | **Fixed** | New tests: `test_config_round_trip.py` (sanitize / migrate / save-load / corrupt backup), `test_ban_tester.py` (banned-body heuristics, categories), `test_gost_util.py` (zip + exe hash sidecar), `test_fail_closed.py` (leak contract), `test_firewall_helpers.py` (emergency_disengage guard). **47 → 82 tests passing** (+35 since session start). | + +**No-action documented (§12):** C-09, S-03, E-14/S-08, B-01, B-08. + +--- + +*Proxy God Audit — last updated 2026-05-22 (round 3)* diff --git a/docs/MAINTENANCE.md b/docs/MAINTENANCE.md new file mode 100644 index 0000000..ab41028 --- /dev/null +++ b/docs/MAINTENANCE.md @@ -0,0 +1,23 @@ +# Maintenance + + +## Stewardship Rules + +- Keep generated files, build outputs, copied SDKs, and raw firmware binaries out of Git unless they are the source of truth. +- Keep credentials, tokens, dumps, private messages, session stores, and local machine paths out of commits. +- Prefer small commits with clear intent and a matching issue or release note. +- Preserve upstream attribution when code is copied, forked, or adapted. + +## Routine Checks + +- README still describes what the project does. +- Setup instructions still work. +- Security policy is accurate for the current risk level. +- Changelog records user-visible changes. +- License status is explicit. + +## Automation Gate + +- Confirm no tokens, session cookies, personal data, or exported credentials are committed. +- Document required environment variables with safe example values only. +- Add rate-limit and account-safety notes before any release. diff --git a/docs/OPERATOR_RUNBOOK.md b/docs/OPERATOR_RUNBOOK.md new file mode 100644 index 0000000..68dce5d --- /dev/null +++ b/docs/OPERATOR_RUNBOOK.md @@ -0,0 +1,128 @@ +# Proxy God — Operator Runbook + +**Version:** 1.0 +**Last updated:** 2026-05-21 + +--- + +## Emergency: Firewall Kill-Switch Stuck (Network Appears Offline) + +If the application crashes or is force-killed while the kill-switch is engaged, +outbound traffic will remain blocked. Follow these steps to restore connectivity. + +### Option 1 — Run the standalone disengage script (fastest) + +Open an **elevated** (Run as Administrator) PowerShell terminal and run: + +```powershell +# Remove all PCM_ firewall rules +netsh advfirewall firewall delete rule name=all +# Restore default outbound policy +netsh advfirewall set allprofiles firewallpolicy blockinbound,allowoutbound +``` + +### Option 2 — Restart the application elevated + +1. Right-click `ProxyChainManager.exe` → **Run as administrator**. +2. The application detects orphan firewall rules on startup and removes them. +3. Click **Stop** if the service does not start, to force disengage. + +### Option 3 — Restart Windows Firewall service + +```powershell +Restart-Service -Name MpsSvc -Force +``` + +This resets all runtime firewall state (not persistent rules). You may still +need to remove the `PCM_*` rules afterward with Option 1. + +### How the application handles this automatically + +`firewall.py` exposes `emergency_disengage()`, which is registered via +`atexit` and `signal.SIGTERM`/`SIGINT` in `service.py`. A clean exit or +SIGTERM will call `emergency_disengage()` automatically, removing all +`PCM_*` rules and restoring `allowoutbound` before the process terminates. +Only a hard kill (`SIGKILL`, power loss, BSOD) can bypass this handler — +in those cases use Option 1 or Option 2 above. + +### Verify rules are gone + +```powershell +netsh advfirewall firewall show rule name=all dir=out | Select-String "PCM_" +# Should return no output when clean +``` + +--- + +## Emergency: Proxy Leak Detected + +If the Live tab reports a leak (exit IP matches your real IP or VPN IP): + +1. Click **Rotate Now** to pick fresh proxies. +2. If leaks persist, click **Stop**, then re-enable the kill-switch and click **Start**. +3. Check the pool size — if < 5 proxies, force a full pool refresh by stopping + and restarting the service. +4. Enable VPN before starting if leak detection mode is "strict (exact IP)" — + a VPN provides a larger subnet mask for the leak check. + +--- + +## Empty Pool / CDN Down + +If every pool refresh returns 0 proxies: + +1. Check internet connectivity (browser → any site through system proxy OFF). +2. Verify the proxy source URLs in **Settings** are reachable. +3. The default sources use `cdn.jsdelivr.net` — if blocked on your network, + replace with a mirror or local JSON file (use `file:///path/to/proxies.json` + format). +4. Temporarily lower `Max candidates` to speed up validation if the CDN is + slow. + +--- + +## GOST Binary Missing or Quarantined + +Symptoms: "GOST setup failed" or "GOST appears quarantined" in the Live log. + +1. Check Windows Defender: **Windows Security → Protection history** — look for + a quarantine event on `gost.exe`. +2. Restore and exclude `gost.exe` from Defender, or re-download by deleting the + GOST folder: + + ```powershell + Remove-Item "$env:LOCALAPPDATA\ProxyChainManager\gost" -Recurse -Force + ``` + +3. Restart the application — it will re-download and re-verify GOST via SHA256. + +--- + +## Settings Corrupted / Reset to Defaults + +A corrupt `settings.json` is automatically backed up to `settings.json.corrupt` +and the application reverts to defaults. + +To restore: + +```powershell +$dir = "$env:LOCALAPPDATA\ProxyChainManager" +Copy-Item "$dir\settings.json.corrupt" "$dir\settings.json" -Force +``` + +A rolling backup is also kept at `settings.json.bak` (last successful save). + +--- + +## Log Files + +| File | Location | Purpose | +|------|----------|---------| +| `proxy_chain_manager.log` | `%LOCALAPPDATA%\ProxyChainManager\` | Main app log (5 MB × 3 backups) | +| `gost.log` | `%LOCALAPPDATA%\ProxyChainManager\` | GOST process output (512 KB rolling) | + +To open the log directory: + +```powershell +explorer "$env:LOCALAPPDATA\ProxyChainManager" +``` diff --git a/docs/PROJECT_HANDOFF.md b/docs/PROJECT_HANDOFF.md new file mode 100644 index 0000000..bf908fc --- /dev/null +++ b/docs/PROJECT_HANDOFF.md @@ -0,0 +1,14 @@ +# Project Handoff + + +## What This Repo Needs From A Maintainer + +- A one-paragraph project summary in README.md. +- Confirmed setup instructions. +- Confirmed license status. +- Confirmed provenance for imported code and binaries. +- A known-good verification command, test, build, flash, or demo path. + +## Current Stewardship State + +This repo has baseline governance files, wiki pages, issue templates, labels, milestones, and a readiness issue. The next maintainer should replace generic stewardship notes with project-specific facts. diff --git a/docs/PROVENANCE_CHECKLIST.md b/docs/PROVENANCE_CHECKLIST.md new file mode 100644 index 0000000..8d41990 --- /dev/null +++ b/docs/PROVENANCE_CHECKLIST.md @@ -0,0 +1,12 @@ +# Provenance Checklist + + +Use this before claiming ownership or publishing artifacts. + +- [ ] Identify original upstream source, if any. +- [ ] Record fork URL, commit, tag, or archive source. +- [ ] Preserve third-party notices and license files. +- [ ] Separate local patches from imported code where practical. +- [ ] Record binary build inputs, toolchain versions, and source commit. +- [ ] Publish checksums for release assets. +- [ ] Mark unknown-origin content as blocked until resolved. diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md new file mode 100644 index 0000000..0a90369 --- /dev/null +++ b/docs/RELEASE_PROCESS.md @@ -0,0 +1,92 @@ +# Release Process + +Production Windows builds use **PyInstaller** with a gated pipeline in `scripts/release_build.ps1`. + +## Quick commands + +| Goal | Command | +|------|---------| +| **Production release** | `build_release.bat` or `powershell -File .\scripts\release_build.ps1` | +| **Dev build (Desktop copy)** | `build_exe.bat` or `powershell -File .\scripts\setup_and_build.ps1` | +| **Skip tests (dev only)** | `powershell -File .\scripts\setup_and_build.ps1 -SkipTests` | + +## Production pipeline (`release_build.ps1`) + +1. Resolve Python 3.10+ +2. Install `requirements.txt` + `dev-requirements.txt` (PyInstaller **6.10.0** pinned) +3. **Test gate**: `compileall` + `unittest discover` (skip with `-SkipTests` — not for prod) +4. Download & SHA-verify bundled `gost.exe` → `proxy_chain_manager/_bundled/` +5. Generate Windows **VERSIONINFO** → `build/version_info.txt` +6. **PyInstaller** one-file build via `ProxyChainManager.spec` +7. Optional **Authenticode** sign (see below) +8. SHA256 sidecar for the exe +9. **SBOM** (`SBOM.json` + `requirements-frozen.txt`) +10. **RELEASE_MANIFEST.json** (version, commit, sha256, build time) +11. Zip → `releases/ProxyGod-v{version}-windows-amd64.zip` + +### Output layout + +``` +releases/ + v1.0.0/ + ProxyChainManager.exe + ProxyChainManager.exe.sha256 + RELEASE_MANIFEST.json + SBOM.json + requirements-frozen.txt + LICENSE + OPERATOR_RUNBOOK.md + ProxyGod-v1.0.0-windows-amd64.zip +dist/ + ProxyChainManager.exe # same binary (developer convenience) + ProxyChainManager.exe.sha256 +``` + +## Version numbering + +Resolved in order: + +1. `-Version` parameter to `release_build.ps1` +2. Exact git tag on current commit (`git describe --tags --exact-match`) +3. `git describe --tags --always --dirty` +4. `proxy_chain_manager.__version__` + +Tag releases with `v1.2.3` — CI **release.yml** runs automatically on `v*` tags. + +## Authenticode signing (optional) + +Set before building: + +```powershell +$env:SIGN_CERT_PATH = "C:\certs\proxygod.pfx" +$env:SIGN_CERT_PASSWORD = "your-password" # optional if pfx has no password +powershell -File .\scripts\release_build.ps1 +``` + +Requires **Windows SDK** (`signtool.exe` on PATH). Without a cert, the build completes unsigned (SmartScreen may warn on first run). + +## CI / GitHub Releases + +- **Every push/PR**: `.github/workflows/test.yml` — unit tests only +- **Tag `v*` or manual dispatch**: `.github/workflows/release.yml` — full release build + artifact upload + GitHub Release assets + +```bash +git tag v1.0.0 +git push origin v1.0.0 +``` + +## Before tagging (checklist) + +- [ ] `python -m unittest discover -s tests -v` passes locally +- [ ] CHANGELOG.md updated +- [ ] No secrets in `settings.json` / signup JSON committed +- [ ] `proxy_chain_manager/_bundled/gost.exe` will be fetched at build time (or pre-staged) +- [ ] Verify SHA256 after build: `Get-FileHash releases\v*\ProxyChainManager.exe -Algorithm SHA256` + +## Verify a release artifact + +```powershell +Get-FileHash -Algorithm SHA256 releases\v1.0.0\ProxyChainManager.exe +Get-Content releases\v1.0.0\ProxyChainManager.exe.sha256 +Get-Content releases\v1.0.0\RELEASE_MANIFEST.json | ConvertFrom-Json +``` diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..393f6b7 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,20 @@ +# Roadmap + + +## Now + +- Confirm the project purpose in the README. +- Confirm build, run, or flash instructions on a clean machine. +- Classify license status and upstream provenance. +- Close the stewardship readiness checklist issue. + +## Next + +- Add project-specific tests or verification steps. +- Publish the first verified release only after provenance and security review. +- Replace placeholder wiki notes with project-specific architecture or hardware details. + +## Later + +- Add examples, screenshots, wiring diagrams, or demo media where useful. +- Decide whether duplicate or experimental branches should be archived. diff --git a/docs/SECURITY_REVIEW.md b/docs/SECURITY_REVIEW.md new file mode 100644 index 0000000..2d56a0d --- /dev/null +++ b/docs/SECURITY_REVIEW.md @@ -0,0 +1,14 @@ +# Security Review + + +## Required Checks + +- [ ] No credentials, tokens, cookies, API keys, private keys, or session files. +- [ ] No private user data, dumps, card data, logs, or captures that should not be stored. +- [ ] No copied dependency trees where package managers or SDK installers should be used instead. +- [ ] No unexplained binaries in source history. +- [ ] Risky behavior is documented and scoped to authorized lab use. + +## Release Gate + +A release is blocked until the checklist is complete or a maintainer explicitly records why the item does not apply. diff --git a/docs/dashboard-screenshot.png b/docs/dashboard-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..2abfe5aee5bcc7fc70680a8864d705f15f3d0bc8 GIT binary patch literal 364095 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

+S-YWDH+-9(X%RczPyQE%@ zRXIniVK5BE*aR&|wH~oR5?Pn9yJP50E!;Q5fIe#QNaaH$`d&nq>x&f23-qBBL%^ux zEhPmeP=yZ>wm5Lth^#$)aJA^>VD3m-TkW5dmlVqT{K|bs-KZ-_LowK6)qv_~&y0V* z8ZfCh`AQ~M&7k3gda0X6lJY1rO%VrB+e2%VER2K{&IKHtRs_uhz-J-$t52>BBlD(p zB2K(1GF3?r)qz|Qoef>RvWfzp;T08C&;dwit0D+nG!LGrqTL z_PHJBcy#__hDEpi+$bmfy9bWXFANu?PKTWo3VPWaB@7o8{I*mqZ70{*YccI>lHO-R z!>@OwLw}SyJTwsRQ3I0wA3k@_EQ6?mXWxEe=DGa#$amF!m&`=-Of0on0C3ve|LwFL zxd5_!|2oek{?(KJXA;ZbyWD97s2;+_suF8j$mG;AiJN(cpxDOgW{WrpAF*~Z=Kv29 zgMyoCa^?MB!>8E8mwI@tVQ&PAK32^P5#q zeL9h@2+ZO;;_A)|lunMsl}y7s!23!}t|vRO^=x4S)?7L{BqTkr3`zg6Z@-jlkvWBi z1h^+80A;$MK0mK{zkvkeNvWs1BZ}GioAQWwj{*K|!Fw*Hf#O-Z+l83`m=Z6fL z7F?m#(@YqkG&k#Zma@gb{Pl;*nxCe8n7?VV{D3^CDN6ZF%S)LE`oQtXuoqvSYnwW_ zTqp=%bEEuB{6+Stva#SnUm&=!N^W)D(q zDCUp?^lBMkCXH^oBzem>zrhJOsR3y;+8O1l@7x10ej2EAcX~8Au9-Z!KDv5JtI%g~ zMdN0blXS*?{S2@X3z^R3 z;2N?nY=&^;AeVFbu}&j8$vzS&Gl)XXcUMZrsJ>}JFKN5Bj#45AOvjbN0X^H>a4s?~ zc;Ymh`n`RUS_ArRh-&f_&~Io~)ruG&EBR}C++7MZEpG%YfwNT!umSD0e3G=VbhX6W zHQjQq{cBgotO>&!i>*=F_I4b>_VbI?S{nKW-F_&1e)?+C(G2M{>L>i9(G95pT$!6w zx%_l;vL9^rs@q8rmb1P%b2(5dE}_kNH&gk{O;G>vomW;el#-xx2zg*(QJ#Bxqde#% z+6REoGaJ9U@Xk3B=vzLcqxl5OUdtJB0L#I+9Mfwsyp#7kGBt}dj~|`(;-aO=mKIn% zlGB^tXLwgar+QJn8TYt&C#YHP7o2;;&WE;$Q<@zW?)paSdHtxnrfDOwGvgRQv5~6x zh?TU@yvuoeCIxF2*3`Ck3bHZOHTu!P#+Of$O@5?`^k&7OEY)=?JsO`^9UJaAwhPR| zeDNO5`W`ISD&Dc#J?`A~`R5`ZJlB{T1^vE*blM~iX;MwQ={yE9VN6)urM1z&G#BEF z&^Ewp=1U6YCCkU=-NMmN%ZG0cgMF#_9I zRDV+#`qu;V>lj>n4G@w0lmP><1IK5Vqt9N@VqG+I+rMTO+pfgw96ByG^ggpb?JgbV z(VGJqCH}*dM0tF|f^OE$wndoN!4zNLpwQGJ3U!y$8V_`$=v8L)nauG~JxPyrAj#u^ z^9mm@c;EbX3N=O*r;VRVhx&{Ml$LXma}~vTGlIBrril-1KM*RUNDkMKwRyG1`_kI)tO2EH!kyrHR$9i zN7;uQRV_b4%A%~YtW?SvQWYXVr*!22?-D?B61oA{Lum%*GT&w50{}`t{ zj7z!m8FeMMpsefMZI>ID=Z_H}^uP}*NF1eN7 zlb-Z)ek&j4x{rVj-T!GL%NYiQ5}$krw(W1DPZqtK?TAy~pFip}&%Zkz3fNKLp2(YA zvRe&6@K~^2A`ZVkqp-H}6~I8I{{?|uog2Lr^Wcaf5iX2^~->-FzM@*Sv*+KjgP*kb@bD~e?maz@lc`*H+rTmXd;0=5_l}|e>T2db3O6K zxqmW$A`Joj=U8GiH?IF@WG7T@HzZ(G7=MHb>dbEzxM zW>EXWAyk{~sO^_SFaL$I5mzJYUB3S?xqj(*oW8lV-s#l`3i!rH0A{x*4@WAK9N1_x zxsH33sPE+kB)#k!W;;IsfFc*Be;(-q9$6ZkpBOO^esbqVZwA274P3gs1zJg9CpEZo z%UR36^B6n>G-=KWr(*OFP%Ku_g(0>fWA2^UJY;y>2 z8uG&~7mv)4T|3~O{KQ&N_E%AB&&10DbecW72E##(*cr($q$HzIsAkC6Ygn9Y+;cvRfqa3|ZGNA)%MGAe`&&!1g#-iAR9 z<#dwlx14U7r$5^K>To^6g_v6*=E% zIJuD38_*B6Qe%})qpPN7xb-DU!a0uXhyblu5&_byN-EVjAe?$(9 zYYa;hYEI6}L+I4jhX)6Kh^+@QSdT7)djKF$RIpS%S`x?UUI2-z&-#-xIbop+mwy1} zC;x{^?!WcW!T(f#4#55x3Anm-4EIRNQ9)hre8k2A_W_@MobP>^Bz3tstGeCjgL0YWL7^*UjYie;M`D}yHOOyy zYYtd_4r}uLkmvP9SAjf&26LGzpW*pR$2H9#?*|Sg;i;qgv&#j7@G?TVGrpalh43mR z*SZ{d@x3e^%#JOpexyxUXY*3B0ajtu5Q;< zd*A{#tvsW{FgNd(^AW;z;2n25)3-11JH05|zMT=qD&!RB>=U-0;VP?G#|a5lnTbFs za{u&X6W3b6_?Y{AkjE?R<^vJlt3boelb=dAnjTDjZovP(HHO2Yl&0P4_zsc*z@{l#FQ)Ozw%Xk@WJtD;`>8@W$}9fZ`*v3N3wFZAT%|9?1-B4$mN#+2fg?b#QYR{oe!knbs=cL+r%Os1c|1r8>=LxGO{kVz%lQQP-lv8Sd5R|MW?e9U<` zoXFsQ#2|;NC5F zl^K@6u_i=|_M*c>7XLwRXJr5Cvl3#Jg=I}>#!B3 z16woC6JEB^OH-Y=&BBN4vByN+h66+%zFu)}^>)wOj}e=q3oN?4PBR5d4YUv~^p@Zo zjDr$8``Pba-O}N zAV2RXmwv*6ihW|sty+ZU=cEgFOFIV*heSE$e;Dn6BU!wEy@dsxu8;j4#@M@->&`8e zA28=LrZ8{MUMQm}7Lx$`@Hzp)g)8s1?mmmK*&W}us7jDroctE#+1rMAGZO0@K=8@j z6R5qLTYU7*=Aof`eH>Y{6VPK7r%QYHth)IrvJBlab*GT2i(P)i-Qi>D2E=0a(&q*7 z{U)O)mcZhH7ZO>O+>&|{4}sHl+R^sYk6v47sBCqwV!nOVt-ztx5X z8}Y<=IQJyfT-VOJ&^z!dqY=S-6~a}Iz#{tA_VlSUdtZi4@cf|uR3<;|sKrTPhzwvD z$7VT)2Q52bd(Vz+#!I7QCRWz0xG{O{V|D$uLU^jn5V1sych~=&sw8fd<(mc<&b`2K zwaD$*##|i-{JFK#;_ulGr|HuqqkdfLp?p)|^E-LU-f=0Rg?{9HD_r`8+|hVV&*lD8 zy)SU-U%1QV@c(ykmw&7Ke?pX>5{1orGMJMA?#UCc29xfER-;B%e)h2y%LQh*HIL@} znAHHaBq!HzwFsmC!t*xm&D?;c1MsHeZwqAEZbquDq&0ehuF#5@59&#{Qr#N;RrJuX zbWb0@-r08$Tx9{o$c&cb9p~2$+@mM=$5T6nn~xZsHup{kv4^`ZL>NLK%}r{+x{_JpAGEE+kJ$9|}^4YLHB z0n~Si{t(w2oqrr<{CI&^lU?C*&F<>F&=jv)^I@GO^(K3g5m@)UAg-!lbVeu(bKN4% zNzBX}VeGc=RkPjQC0FMket!jgwHEy%2>HiMw_nfdWB|t7RS0rmxLSNQ)CM)rF{TAm zf0Np^ub^rM5rV(&CQf=SK*FjaHTPeTdW8A%vv~LGl{xRf`3YIc`ZS=y#TZ&91B!si zJ`bHY&M*pVPTqgwmY_8C$gn}tt!BQ-tj0giJ|Zkon-uK)xgN3ty?-23!uTBp97&HT zYCt>`21pj)PP|Bq)>KIW%`@uf)fQpF|TQ`I#3f*zTX*P!<`k(PDAKB%r?PZ31> zGA+d|Yd_2yD%!Ac9A}OBQyQ|7**DpnZ@s_%VL0@sYJI7O*qi5dkp2wjq+FhRxu#L^5K%0>&nL{vNim!I7XOZc8^9+b6=!_LgY6I+YH_EzAXaDrNeg!r7)$>a; z`-a^t2{5ba_lEnO)_E|`sUAOClfu$-@#0(mdF+a;^>KKj&hpI;x=X(j8|bD})@W$b z%$au-+AM!OQOMGC&t&Lh=>8(_s$w%Nl|uwg*>g)JenPvom%r?;xmmni_Abz2Pkw3d zscYTli6-&TOuwQ6m2(P-U03p~ECF`5VtWk;|$%k2lLv73GGfGIM>$OTXufZ)U+0d7 zLJN9>LA^L2FnhOQU#bJ|C=xnEE`FSO5%EI_rRREv z&`(m(q}Dr@%Yy*J5WiA<;Cz#T=%V-eK0udh=}X#`LIdh&Qw(w)MB17&6y*>WCvpW+ z0&=MhK)gj{k{L;RyvIqWKiZm-u1;ZVT8{D#7)L}da9=BAky9G1=2KDQv(tW6GG<&; zAN1asS7Bnx;YPYMmRVC}y9nTZgcjPA#tn+ zho;X@kK{g40n9}TK!0hbxH#7CJ3l(BYi_T4p~@;~c=(T?sWt=XLS$b}E7qnhPgq4I z*$QAzuVM)%YG)T{+0QpHzdA}eFgrCSvjt5ym7=FMd5I3OdOF|zxnV1F^?4Y)MI@x- z@eCd2XaKUM-*@T-vCtj`S3Jr~UawXBz28-Se08fxLHor&EIsMgv<&K3U5-+ZjxMSH zA5=K7YaTlznHTbE6^k=+)_Q-P(l%awA~J8PJ^X9D&leqv``*gx$_ZT?y_xrzcVGXW znjrVk&CWYQ_=;54<=(;V>b#M9|1qI`XRT8(yIj_8?U5jqAxvrebTU}NN{mZ$AY*=h zN-6JwHqhi&3Yfko1nuoGFIU^qiCncM{a*eRn`(p1<#mFsjaEOpvI3DC-hw1Tf-1?u z1;mRt=i$N{A;;3xRia~DsISVsoR-W)mt4Hu-dCy=$BF;^6_I1PW?B*)rC56Db7B)t z*koeoi|AxkpHdI~W$R+OB=`rZkV4lq)S$N3z)EV&aM#DlPMh2>@rFW0F4@;@%w*IA zM~hPY;DfwDl}REcLCDege0&$5)+iK5K23KcVy-IIGP!)7@xl&f_TELUI@SOU3oIj$u!v@rywlQTw`0v%1~>mYfaG7*SGxeFgeKxJ#kTV&shs#;)+`t z5lYe>c)Jn1)O z#=PxfE;t1o4O^VQStHSZ@hls&X2Nl@^mV7$=_ckBL_cV>bot zv?us_opy?+sYgz`f>SCkjcLkO=epCUn@_0xv@fE%&B|b3%L$Z&uFg(pEvL8wYg^~fi*{I``3HVPyi!^J*MFOD)9eon)` zn5yq~+DRdG4}`=El~j1FYt6fvJgw>f=(yHtT6-C+H?MsD#L&1s!t(5C4Xaqnt^qZg4fX(Gh|gd{Krad?2q7`*>UeL(OMRPZM~KC)cO+ttXLauGZ!D z=?l_3?x|veb{0_v79m}7VY@B$pA;|R@N zD|Kh+cVna@95a<0u(oMuDJ0RK+f$1r^|M84)0o3-`6Ue(~dQ(taOqPw#4XXY?!aolD$;+GTU!)gkMzH$4<50*DN0itm5@%pb~twrkRIj?w0~1iH2>yF4BHw zd##COepg^=A?bgx_Lfm?HebK+UBQZ5(cF6gxKpIK zYY7ruibK&5^3uQdexCE;oVDIBWRc9ZXZ!s2%v>|)sr*;3pP1bChgS!agfQ71po-C~ zzl&L=Zq4;eFn*U|XS3`>AxHJ5w!?&GsVPgJSyeDjYxDo1 zFtFvr8=yPBV6gjEX>)Dl(zlIy(iCE2#4^e!HiM@#%mJ7B)IY3nK%m z3i&9S@w8x!;K;7 zk<9nbaxgqlK~8}#UWtJqLhj*XGoBzB|3Clz{*sDSn+*RSzy1H*gzkMiU|LW-A|SBU z!ISO+c6UsQJTTiOSEPD!YsfVXpVIV>q5p#w(UhAzjrmsj|6Ty-o6Mv8BbNZx#@)c%+F|MW`M9xuV;JyUM~W29h$(c7{>xf ze?AK`xp}4O(8>4FBh1wq)r>q-d=HuU<>K5DG{3!e>JfIL*&6z%gnrvzsftaZ@_`d`%_vfi>8(?z4dI zx{x&vRnvkezsh8CaYqW89g{-ujPyYi9NH z50Uy{HniF)QMbLu7RDXrZQWP9Fv!mTdW~5v!>s-*8|U9Rop_MUty8>)9qm5Zj`&&A zA*mVp>sD@dpS3iT`*#bbRcBZ4umb}Je}b!sj=C#Gd`{DR#EUO#|4(EP>G)>xin!H| zV~HXrCKzr6!*_@?qJTb*g`(3qqm-0?`}%LmBzOIClJT?6vU@zb9;H;xolM*JPqK=P z!R9@#RodS(_5^P?)%^~Pf2-k4dK+K8f|`%~-u+P^?1kLTum{Z>x7|(N^0OV>gu9wh zv$+?VxZ!~SE-Lfp|H&PG`%2{=dA3|<=dz&j&^^#>k6d2zFUNq}?|*kB#kBEb&lrvG zVV2f%|L*u7@rjG|e+)*HnuT%)qCbLk;K^1l8$BuvX*$nxmd1a*Htzku{afyG=U(mM zchYGwkzI26KZX1TDj7oma1nSd^$-`avZeL&O3r z=wdM(I?tr|uh(~4`~Iskf(UUUxvEBomQd!&V;A7x7>V%f{gRy%+_)M5>}3c5sLfQ{Dm>}32VUH*+{5r2}FB)0=d#C^xO`ei@PrOfwg z=qYPrrT*Vd!_!9^{e>mcZT@FAh-N7K&&TQFr`^pK1^&?R1G`5lCDDI!$Z)0<@jo7m zP5QrRf_5nwBLI*cdP*xd&-OQ6p^;Xw*HxC38a=SoRo zR>$#YIJ9VT-$_CKZyraD503ig=FC3NO ze~R>=bi4!xGS3*zQ3*Es_y5aWNkc6P#oO6Q_-^`t@73vOXE)UMitG=6Q(|z(mHQvz zps1p!@TW8cpo0I^By#t{TYdh7;bB4b&me0%o_V-E*z&G6FxP<+6#lEqr0A*;azL&q zNjI_sd|->N@(=4!KeI7I2A72UP^X!-^*#iyH1fOIfw4Rtdj$J`)B6Bg_RXAV`P)ZX-edpW4-^$;h8e@b)n zN*4?Dw+heF2>;g@9VPM48tYPBEHDKqEg zsj>;XvlrrSAJ}yXXmud|uW4|>RcfnCw3!)Qsi-HI0JInPR__pZuUG1C?C5$)u5|+u z$7iXCV{Qb-7zQ2$4!YlmwEXZ2pxX)fnR+w`;{!JhXx=eG8qC(2snn{mA-CtaTxxEo znkQz+8}8q}!v})7q~Rt!47qqxSHw(GC=1*Ofj#Qi{Fe8k_9bf9d&mxe;Hf4u^J2$9 zost*XTvZ)-Lu?~8-;#c3Q2n#J8S^wi2AJ4+YMAELV@kZky76D;srmk2hDUYEu^6)~ z{7__L=y1;EWUt*gC$`zn_HNm)-SMKsGz?rUv?8|)#fQ`nK2^K@QuDJu*=#C=-v907xU$== zr?FJoG~s__l%(|gie^^2)_QbLa{Fzh^7D0M-6Cti(F7Yu2j53XnQ`YhnTT*mRn-aF zi7MG$W7ymsq`~y2#?H`2Jfp%i{WSBV2O&+cl+nVz@YEcFTN5A>cJiJ%ljqZK(WyI- z!N)cAwI$IaSGt`um6_M)X0O7zPxz6enl%^e<7cVJTi6y!#y(_6_XxW)OAi_0Z5{l^ z?a|2>sbfR2JMqAw3XP^zV&eYqlj^n1(eP%W5|ndI43dWPz+2C(H=k4JEx5}SzS^XW zqqrkLlFb4mQFpy&?+iWBPf_k?%x(!KKZNzk>MQK-Kn}@Y(B8}jMhMWD`9ASU8S=+a z2$$+VIAcFR1WVz3xW7L0pi7uHMSa!Yw%Zxyb;UPG@r|MijZ6xVDVg=t?C#|t@7{mZ zMn~<~@c>9r{}&)xn5jxRykHA_KnqvF$ynL{LL_eT;+@8i$xYErXAiwLB<&5Q{Js#5 z*>CvZ`JE9$Y>vHKpGx~3?c6M6!8WN8PL1wCkS$wDIQIQ4as&4DYN+ZY_Wsgri&}Ud zNe{z);(N+?jl9>tKZV_d-AQtvcB4?NQ1{=!#b6&vDyEa!*}MZ1AP9%3L^z5;@1*)U zOa>5OO25dR(_#D+r zevrc2{AmSZ7-7HbhjonAaxYwu2kgamxgcGn>8NmUzYa;?Rt0XnM(JYv1-sV2T!H)U zR^1u3s;uFk1oZwlJyS{D{R4D0@(AoGQ*u>;MIx;413=HDEK?k(eef7uC4wPtZr+U< z;+}V4WEB!{FVb1Vb0g_{N$GOGN->JK3OoTcjB%xhb8jTWHXExx@EZDD5dV;DB+cf9 ztzHG)QpYnBGGdrtxPLgkFE7T^O^%dV5DuAzlU01LmWq_1R++FGT$X!T%qR$5k;mY$7 zW~dVmKEGg?{}u%@DFsG>-p21H7No z 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

^#CCU)h<8N$6`8|uTO5@*1P`~d&o$^Be@$Me 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

W1!6=%Gb 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 EA4)?jZ~y=R literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a3c5dd0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +customtkinter==5.2.2 +httpx[socks]==0.28.1 +pystray==0.19.5 +pillow==12.2.0 +cryptography==48.0.0 diff --git a/run.py b/run.py new file mode 100644 index 0000000..ed42c2b --- /dev/null +++ b/run.py @@ -0,0 +1,11 @@ +"""Launcher for development (pythonw run.py) so scheduled task can find a stable path.""" +import sys + +from proxy_chain_manager import __version__ +from proxy_chain_manager.app import main + +if __name__ == "__main__": + if "--version" in sys.argv or "-V" in sys.argv: + print(f"Proxy God {__version__}") + sys.exit(0) + main() diff --git a/run_mac.command b/run_mac.command new file mode 100755 index 0000000..8dd2ba3 --- /dev/null +++ b/run_mac.command @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +DIR="$(cd "$(dirname "$0")" && pwd)" +exec "$DIR/scripts/run_proxy_god.sh" diff --git a/scripts/app_launcher.sh b/scripts/app_launcher.sh new file mode 100755 index 0000000..b52f808 --- /dev/null +++ b/scripts/app_launcher.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +exec "$ROOT/scripts/run_proxy_god.sh" diff --git a/scripts/find_tk_python.sh b/scripts/find_tk_python.sh new file mode 100755 index 0000000..3a5ffd6 --- /dev/null +++ b/scripts/find_tk_python.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +candidates=( + "${PYTHON:-}" + "/usr/local/bin/python3" + "/Library/Frameworks/Python.framework/Versions/Current/bin/python3" + "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3" + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3" + "/usr/bin/python3" + "/opt/homebrew/bin/python3" + "python3" +) + +seen="" +for py in "${candidates[@]}"; do + [[ -n "$py" ]] || continue + if [[ "$py" != /* ]]; then + resolved="$(command -v "$py" 2>/dev/null || true)" + else + resolved="$py" + fi + [[ -x "$resolved" ]] || continue + case ":$seen:" in + *":$resolved:"*) continue ;; + esac + seen="$seen:$resolved" + if "$resolved" - <<'PY' >/dev/null 2>&1 +import tkinter +PY + then + printf '%s\n' "$resolved" + exit 0 + fi +done + +echo "No Python with tkinter support found. Install Python from python.org or a Tk-enabled Python runtime." >&2 +exit 1 diff --git a/scripts/run_proxy_god.sh b/scripts/run_proxy_god.sh new file mode 100755 index 0000000..808b9c4 --- /dev/null +++ b/scripts/run_proxy_god.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +TK_PY="$("$ROOT/scripts/find_tk_python.sh")" + +needs_rebuild=0 +if [[ ! -x ".venv/bin/python" ]]; then + needs_rebuild=1 +elif ! ".venv/bin/python" - <<'PY' >/dev/null 2>&1 +import tkinter +PY +then + needs_rebuild=1 +fi + +if [[ "$needs_rebuild" == "1" ]]; then + rm -rf .venv + "$TK_PY" -m venv .venv +fi + +".venv/bin/python" -m pip install --upgrade pip >/dev/null +".venv/bin/python" -m pip install -r requirements.txt >/dev/null +exec ".venv/bin/python" run.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d4839a6 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests package diff --git a/tests/test_ban_tester.py b/tests/test_ban_tester.py new file mode 100644 index 0000000..f1f4f65 --- /dev/null +++ b/tests/test_ban_tester.py @@ -0,0 +1,48 @@ +"""Heuristic tests for ban_tester.""" +from __future__ import annotations + +import unittest + +from proxy_chain_manager import ban_tester + + +class TestBannedBodyHints(unittest.TestCase): + def test_normal_page_is_not_banned(self) -> None: + self.assertFalse(ban_tester._looks_banned_body("Welcome to my homepage")) + self.assertFalse(ban_tester._looks_banned_body("hello")) + + def test_naked_captcha_word_is_not_banned(self) -> None: + # 'captcha' alone is too common (privacy policies, login pages) — + # earlier audit removed it from hints. + self.assertFalse(ban_tester._looks_banned_body( + "We use a captcha on the signup page to prevent bots." + )) + + def test_captcha_required_phrase_is_banned(self) -> None: + self.assertTrue(ban_tester._looks_banned_body("Captcha required to continue.")) + self.assertTrue(ban_tester._looks_banned_body("Complete the captcha below.")) + + def test_access_denied_is_banned(self) -> None: + self.assertTrue(ban_tester._looks_banned_body("Access denied")) + self.assertTrue(ban_tester._looks_banned_body("ACCESS DENIED — 403")) + + def test_cloudflare_ray_is_banned(self) -> None: + self.assertTrue(ban_tester._looks_banned_body("Cloudflare Ray ID: abc123")) + + def test_empty_body_is_not_banned(self) -> None: + self.assertFalse(ban_tester._looks_banned_body("")) + self.assertFalse(ban_tester._looks_banned_body(None)) # type: ignore[arg-type] + + +class TestSiteCategories(unittest.TestCase): + def test_all_category_includes_known_sites(self) -> None: + all_urls = {u for _, u in ban_tester.SITE_CATEGORIES["All"]} + self.assertTrue(len(all_urls) > 5) + + def test_categories_present(self) -> None: + for key in ("Social / General", "Shopping", "Crypto Exchanges", "DNS Providers", "All"): + self.assertIn(key, ban_tester.SITE_CATEGORIES) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_config_round_trip.py b/tests/test_config_round_trip.py new file mode 100644 index 0000000..32adc80 --- /dev/null +++ b/tests/test_config_round_trip.py @@ -0,0 +1,105 @@ +"""Round-trip / migrate / sanitize tests for config.Settings.""" +from __future__ import annotations + +import json +import unittest +from dataclasses import asdict +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import patch + +from proxy_chain_manager import config + + +class TestSanitize(unittest.TestCase): + def test_clamps_chain_length(self) -> None: + s = config.Settings() + s.chain_length = 99 + s, changed = config.sanitize_settings(s) + self.assertTrue(changed) + self.assertEqual(s.chain_length, 8) + + def test_chain_length_string_resets_to_default(self) -> None: + s = config.Settings() + s.chain_length = "not a number" # type: ignore[assignment] + s, changed = config.sanitize_settings(s) + self.assertTrue(changed) + self.assertEqual(s.chain_length, 3) + + def test_local_port_clamped(self) -> None: + s = config.Settings() + s.local_port = 99999 + s, changed = config.sanitize_settings(s) + self.assertTrue(changed) + self.assertLessEqual(s.local_port, 65535) + + def test_http_source_url_rejected(self) -> None: + # _is_safe_https_url should strip plaintext HTTP sources. + s = config.Settings() + s.sources = ["http://insecure.example/list.json"] + s, _ = config.sanitize_settings(s) + self.assertTrue(all(u.startswith("https://") for u in s.sources)) + + def test_rfc1918_source_url_rejected(self) -> None: + s = config.Settings() + s.sources = [ + "https://192.168.1.1/list.json", + "https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/protocols/http/data.json", + ] + s, _ = config.sanitize_settings(s) + for u in s.sources: + # rfc1918 host must be gone. + self.assertNotIn("192.168.", u) + + +class TestMigrate(unittest.TestCase): + def test_unversioned_dict_gets_versioned(self) -> None: + raw = {"chain_length": 4} + out = config.migrate(raw) + self.assertEqual(out["settings_version"], config.SETTINGS_SCHEMA_VERSION) + self.assertEqual(out["chain_length"], 4) + + def test_already_current_passthrough(self) -> None: + raw = {"settings_version": config.SETTINGS_SCHEMA_VERSION, "chain_length": 4} + out = config.migrate(raw) + self.assertEqual(out["settings_version"], config.SETTINGS_SCHEMA_VERSION) + + +class TestLoadSaveRoundTrip(unittest.TestCase): + def test_save_then_load_preserves_values(self) -> None: + with TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + with patch.object(config, "app_data_dir", return_value=tmp_path): + s = config.Settings() + s.chain_length = 5 + s.local_port = 19191 + s.kill_switch_enabled = False + config.save_settings(s) + loaded = config.load_settings() + self.assertEqual(loaded.chain_length, 5) + self.assertEqual(loaded.local_port, 19191) + self.assertFalse(loaded.kill_switch_enabled) + + def test_corrupt_settings_backed_up_and_defaults_returned(self) -> None: + with TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "settings.json").write_text("{ not json", encoding="utf-8") + with patch.object(config, "app_data_dir", return_value=tmp_path): + loaded = config.load_settings() + self.assertEqual(loaded.chain_length, config.Settings().chain_length) + self.assertTrue((tmp_path / "settings.json.corrupt").is_file()) + + def test_save_writes_backup_before_overwriting(self) -> None: + with TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + with patch.object(config, "app_data_dir", return_value=tmp_path): + config.save_settings(config.Settings()) + # Mutate + save again — .bak should now exist. + s = config.load_settings() + s.chain_length = 7 + config.save_settings(s) + self.assertTrue((tmp_path / "settings.json.bak").is_file()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_fail_closed.py b/tests/test_fail_closed.py new file mode 100644 index 0000000..d104f25 --- /dev/null +++ b/tests/test_fail_closed.py @@ -0,0 +1,26 @@ +"""Fail-closed leak-detection contract tests.""" +from __future__ import annotations + +import unittest + +from proxy_chain_manager.leak_detect import is_chain_leak, leak_reason + + +class TestFailClosed(unittest.TestCase): + def test_unknown_real_ip_is_leak(self) -> None: + """When the direct IP could not be determined we cannot prove the + chain is forwarding — the README promises fail-closed behavior.""" + self.assertTrue(is_chain_leak("5.6.7.8", None, vpn_active=False)) + self.assertTrue(is_chain_leak("5.6.7.8", None, vpn_active=True)) + + def test_unknown_exit_ip_is_leak(self) -> None: + self.assertTrue(is_chain_leak(None, "1.2.3.4", vpn_active=False)) + + def test_leak_reason_explains_unknown_direct_ip(self) -> None: + msg = leak_reason("5.6.7.8", None, vpn_active=False) + self.assertIn("direct IP", msg) + self.assertIn("fail-closed", msg.lower()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_firewall_helpers.py b/tests/test_firewall_helpers.py new file mode 100644 index 0000000..973c010 --- /dev/null +++ b/tests/test_firewall_helpers.py @@ -0,0 +1,42 @@ +"""Non-destructive tests for firewall helpers. + +These DO NOT call netsh — they only exercise pure functions / branches that +should never modify the system firewall state. +""" +from __future__ import annotations + +import sys +import unittest +from unittest.mock import patch + +from proxy_chain_manager import firewall + + +@unittest.skipUnless(sys.platform == "win32", "Windows-only firewall API") +class TestEmergencyDisengageGuard(unittest.TestCase): + def test_no_op_when_not_engaged(self) -> None: + """emergency_disengage() must NOT touch firewall state when our rules + are not present (the audit fix to C-08).""" + called = [] + with patch.object(firewall, "is_engaged", return_value=False), \ + patch.object(firewall, "_set_outbound_policy", + side_effect=lambda *_a, **_k: called.append("policy")), \ + patch.object(firewall, "_delete_rules", + side_effect=lambda *_a, **_k: called.append("delete")): + firewall.emergency_disengage() + self.assertEqual(called, []) + + def test_runs_disengage_when_engaged(self) -> None: + called = [] + with patch.object(firewall, "is_engaged", return_value=True), \ + patch.object(firewall, "_set_outbound_policy", + side_effect=lambda *_a, **_k: called.append("policy")), \ + patch.object(firewall, "_delete_rules", + side_effect=lambda *_a, **_k: called.append("delete")): + firewall.emergency_disengage() + self.assertIn("policy", called) + self.assertIn("delete", called) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_gost_util.py b/tests/test_gost_util.py new file mode 100644 index 0000000..8dc8958 --- /dev/null +++ b/tests/test_gost_util.py @@ -0,0 +1,60 @@ +"""Hash verify / sidecar tests for gost_util.""" +from __future__ import annotations + +import hashlib +import tempfile +import unittest +from pathlib import Path + +from proxy_chain_manager import gost_util + + +class TestVerifyZipSha256(unittest.TestCase): + def test_matching_hash_passes(self) -> None: + data = b"hello world" + digest = hashlib.sha256(data).hexdigest() + gost_util._verify_zip_sha256(data, digest) # must not raise + + def test_wrong_hash_raises(self) -> None: + data = b"hello world" + with self.assertRaises(RuntimeError) as ctx: + gost_util._verify_zip_sha256(data, "0" * 64) + self.assertIn("supply-chain", str(ctx.exception).lower()) + + def test_case_insensitive_match(self) -> None: + data = b"PROXYGOD" + digest = hashlib.sha256(data).hexdigest().upper() + gost_util._verify_zip_sha256(data, digest) + + +class TestExeHashSidecar(unittest.TestCase): + def test_sidecar_round_trip(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + exe = Path(tmp) / "gost.exe" + exe.write_bytes(b"\x90" * 1024) + digest = gost_util._hash_file(exe) + gost_util._write_pinned_exe_hash(exe, digest) + self.assertEqual(gost_util._read_pinned_exe_hash(exe), digest) + + def test_missing_sidecar_returns_none(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + exe = Path(tmp) / "gost.exe" + exe.write_bytes(b"x") + self.assertIsNone(gost_util._read_pinned_exe_hash(exe)) + + def test_hash_file_matches_python_hashlib(self) -> None: + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(b"some payload bytes") + f.flush() + p = Path(f.name) + try: + self.assertEqual( + gost_util._hash_file(p), + hashlib.sha256(b"some payload bytes").hexdigest(), + ) + finally: + p.unlink(missing_ok=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_proxy_god.py b/tests/test_proxy_god.py new file mode 100644 index 0000000..9cdc65a --- /dev/null +++ b/tests/test_proxy_god.py @@ -0,0 +1,424 @@ +"""Automated checks for Proxy God core logic (no GUI). Run: python -m unittest discover -s tests -v""" +from __future__ import annotations + +import asyncio +import unittest + +from proxy_chain_manager.leak_detect import is_chain_leak, is_same_subnet + +from proxy_chain_manager.config import ( + Settings, + merge_proxy_credentials, + normalize_proxy_url, + parse_exit_proxy_input, + redact_proxy_url, + sanitize_settings, + split_proxy_for_edit, +) +from proxy_chain_manager.fetcher import fetch_proxy_json, normalize_entries +from proxy_chain_manager.validator import ( + check_chain_exit_ip, + check_https_tunnel, + get_direct_ip, + validate_proxies, +) + + +class TestSameNetwork(unittest.TestCase): + def test_exact_match(self) -> None: + self.assertTrue(is_same_subnet("1.2.3.4", "1.2.3.4")) + + def test_same_slash16(self) -> None: + self.assertTrue(is_same_subnet("185.220.101.5", "185.220.200.9")) + + def test_different_slash16(self) -> None: + self.assertFalse(is_same_subnet("185.220.101.5", "104.28.3.99")) + + def test_none_safe(self) -> None: + self.assertFalse(is_same_subnet(None, "1.2.3.4")) + self.assertFalse(is_same_subnet("1.2.3.4", None)) + + def test_non_ipv4_safe(self) -> None: + self.assertFalse(is_same_subnet("not-an-ip", "1.2.3.4")) + + +class TestLeakDetect(unittest.TestCase): + def test_strict_no_vpn_same_ip(self) -> None: + self.assertTrue(is_chain_leak("1.2.3.4", "1.2.3.4", vpn_active=False)) + + def test_strict_no_vpn_different_ip(self) -> None: + self.assertFalse(is_chain_leak("5.6.7.8", "1.2.3.4", vpn_active=False)) + + def test_vpn_subnet_leak(self) -> None: + self.assertTrue(is_chain_leak("185.220.101.5", "185.220.200.9", vpn_active=True)) + + def test_vpn_different_subnet_ok(self) -> None: + self.assertFalse(is_chain_leak("104.28.3.99", "185.220.101.5", vpn_active=True)) + + +class TestConfig(unittest.TestCase): + def test_normalize_proxy_url(self) -> None: + self.assertEqual(normalize_proxy_url(""), "") + self.assertEqual(normalize_proxy_url("1.2.3.4:8080"), "http://1.2.3.4:8080") + self.assertEqual(normalize_proxy_url("socks5://x:1"), "socks5://x:1") + + def test_merge_split_proxy_auth(self) -> None: + merged = merge_proxy_credentials("http://1.2.3.4:8080", "user", "p:ass") + self.assertIn("user", merged) + self.assertIn("p%3Aass", merged) + base, u, pw = split_proxy_for_edit(merged) + self.assertEqual(base, "http://1.2.3.4:8080") + self.assertEqual(u, "user") + self.assertEqual(pw, "p:ass") + + def test_merge_preserves_embedded_auth_when_fields_empty(self) -> None: + raw = normalize_proxy_url("http://x:y@9.9.9.9:12") + self.assertEqual(merge_proxy_credentials(raw, "", ""), raw) + + def test_redact_proxy_url(self) -> None: + r = redact_proxy_url("http://alice:secret@proxy.example:8888") + self.assertIn("***:***@", r) + self.assertIn("proxy.example", r) + self.assertNotIn("secret", r) + self.assertNotIn("alice", r) + + def test_sanitize_restores_empty_sources(self) -> None: + s = Settings() + s.sources = [] + s2, changed = sanitize_settings(s) + self.assertTrue(changed) + self.assertGreater(len(s2.sources), 0) + + def test_parse_exit_host_port_only_defaults_to_socks5(self) -> None: + base, u, pw = parse_exit_proxy_input("1.2.3.4:1080") + self.assertEqual(base, "socks5://1.2.3.4:1080") + self.assertEqual(u, "") + self.assertEqual(pw, "") + + def test_parse_exit_colon_quadruple(self) -> None: + base, u, pw = parse_exit_proxy_input("1.2.3.4:1080:alice:s3cret") + self.assertEqual(base, "socks5://1.2.3.4:1080") + self.assertEqual(u, "alice") + self.assertEqual(pw, "s3cret") + + def test_parse_exit_password_with_colon_kept_intact(self) -> None: + base, u, pw = parse_exit_proxy_input("h:9:u:a:b:c") + self.assertEqual(base, "socks5://h:9") + self.assertEqual(u, "u") + self.assertEqual(pw, "a:b:c") + + def test_parse_exit_at_form_no_scheme(self) -> None: + base, u, pw = parse_exit_proxy_input("alice:secret@1.2.3.4:1080") + self.assertEqual(base, "socks5://1.2.3.4:1080") + self.assertEqual(u, "alice") + self.assertEqual(pw, "secret") + + def test_parse_exit_respects_explicit_scheme(self) -> None: + base, u, pw = parse_exit_proxy_input("http://x:y@9.9.9.9:8080") + self.assertEqual(base, "http://9.9.9.9:8080") + self.assertEqual(u, "x") + self.assertEqual(pw, "y") + + def test_parse_exit_whitespace_form(self) -> None: + base, u, pw = parse_exit_proxy_input("1.2.3.4:1080 alice s3cret") + self.assertEqual(base, "socks5://1.2.3.4:1080") + self.assertEqual(u, "alice") + self.assertEqual(pw, "s3cret") + + def test_parse_exit_empty(self) -> None: + self.assertEqual(parse_exit_proxy_input(" "), ("", "", "")) + + def test_sanitize_clamps_extremes(self) -> None: + s = Settings( + chain_length=0, + health_check_seconds=999999, + full_refresh_seconds=10, + validation_timeout_seconds=900.0, + ) + s, changed = sanitize_settings(s) + self.assertTrue(changed) + self.assertEqual(s.chain_length, 1) + self.assertEqual(s.health_check_seconds, 3600) + self.assertEqual(s.full_refresh_seconds, 60) + self.assertEqual(s.validation_timeout_seconds, 120.0) + + +class TestFetcher(unittest.TestCase): + def test_normalize_entries_empty(self) -> None: + self.assertEqual(normalize_entries([], False), []) + + def test_fetch_smoke(self) -> None: + """One real HTTP GET — may fail offline; then skip assertion.""" + try: + rows = fetch_proxy_json( + "https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/protocols/http/data.json", + timeout=25.0, + ) + except Exception: + self.skipTest("network unavailable") + self.assertIsInstance(rows, list) + if rows: + self.assertTrue(isinstance(rows[0], dict)) + + +class TestValidator(unittest.TestCase): + def test_validate_empty_list_returns_fast(self) -> None: + async def run() -> list[str]: + return await validate_proxies( + [], + "https://api.ipify.org?format=json", + 8, + 5.0, + ) + + self.assertEqual(asyncio.run(run()), []) + + def test_validate_dead_proxy_fast(self) -> None: + async def run() -> list[str]: + return await validate_proxies( + ["http://127.0.0.1:1"], + "https://api.ipify.org?format=json", + 1, + 3.0, + ) + + self.assertEqual(asyncio.run(run()), []) + + def test_validate_early_exit_target(self) -> None: + """Once target is reached remaining tasks should be skipped (all dead here → no early exit but no crash).""" + async def run() -> list[str]: + return await validate_proxies( + ["http://127.0.0.1:1"] * 10, + "https://api.ipify.org?format=json", + 4, + 2.0, + target=2, # target unreachable — should still return [] cleanly + ) + + result = asyncio.run(run()) + self.assertEqual(result, []) + + def test_get_direct_ip_network(self) -> None: + async def run() -> str | None: + return await get_direct_ip("https://api.ipify.org?format=json", 12.0) + + ip = asyncio.run(run()) + if ip is None: + self.skipTest("could not reach ipify (offline or blocked)") + self.assertGreater(len(ip), 4) + + def test_check_https_tunnel_invalid_proxy_quick(self) -> None: + async def run() -> tuple[bool, str]: + return await check_https_tunnel("http://127.0.0.1:1", timeout_seconds=4.0) + + ok, msg = asyncio.run(run()) + self.assertFalse(ok) + self.assertTrue(msg) + + def test_check_chain_exit_ip_invalid_proxy_quick(self) -> None: + async def run() -> str | None: + return await check_chain_exit_ip( + "http://127.0.0.1:1", + "https://api.ipify.org?format=json", + 8.0, + ) + + self.assertIsNone(asyncio.run(run())) + + +class TestBrowserPersona(unittest.TestCase): + def test_blend_persona_relaxes_block_flags(self) -> None: + from proxy_chain_manager.browser_profile import ( + FirefoxHardening, apply_persona, build_user_js, + ) + h = FirefoxHardening( + persona_key="blend_windows_chrome", + resist_fingerprinting=True, + first_party_isolation=True, + strict_tracking_protection=True, + timezone_utc=True, + ) + applied = apply_persona(h) + self.assertFalse(applied.resist_fingerprinting) + self.assertFalse(applied.first_party_isolation) + self.assertFalse(applied.strict_tracking_protection) + self.assertFalse(applied.timezone_utc) + self.assertIsNotNone(applied.persona) + # And the user.js carries a spoofed UA when blend is selected. + js = build_user_js("127.0.0.1", 18888, h) + self.assertIn("general.useragent.override", js) + self.assertIn("Chrome/124", js) + + def test_hardened_persona_keeps_block_flags(self) -> None: + from proxy_chain_manager.browser_profile import FirefoxHardening, apply_persona + h = FirefoxHardening( + persona_key="hardened", + resist_fingerprinting=False, # should be forced True + first_party_isolation=False, + ) + applied = apply_persona(h) + self.assertTrue(applied.resist_fingerprinting) + self.assertTrue(applied.first_party_isolation) + self.assertTrue(applied.strict_tracking_protection) + self.assertIsNone(applied.persona) + + def test_cookie_behavior_mapping(self) -> None: + from proxy_chain_manager.browser_identity import ( + cookie_behavior_value, cookie_session_only, + ) + self.assertEqual(cookie_behavior_value("accept_all"), 0) + self.assertEqual(cookie_behavior_value("block_third_party"), 1) + self.assertEqual(cookie_behavior_value("block_third_party_trackers"), 4) + self.assertEqual(cookie_behavior_value("session_only"), 0) + self.assertEqual(cookie_behavior_value("block_all"), 2) + self.assertTrue(cookie_session_only("session_only")) + self.assertFalse(cookie_session_only("accept_all")) + + def test_persona_env_returns_tz_for_blend(self) -> None: + from proxy_chain_manager.browser_profile import FirefoxHardening, persona_env + env = persona_env(FirefoxHardening(persona_key="blend_windows_chrome")) + self.assertIn("TZ", env) + self.assertTrue(env["TZ"]) + + def test_persona_env_empty_for_hardened(self) -> None: + from proxy_chain_manager.browser_profile import FirefoxHardening, persona_env + self.assertEqual(persona_env(FirefoxHardening(persona_key="hardened")), {}) + + +class TestPrivacyLan(unittest.TestCase): + def test_snapshot_round_trip(self) -> None: + from proxy_chain_manager.privacy_lan import ( + LanSnapshot, snapshot_from_json, snapshot_to_json, + ) + s = LanSnapshot( + netbios_per_interface={"{a-b-c}": 0, "{d-e-f}": 2}, + llmnr_present=True, + llmnr_prev_value=1, + mdns_present=False, + mdns_prev_value=None, + ) + round_tripped = snapshot_from_json(snapshot_to_json(s)) + self.assertIsNotNone(round_tripped) + assert round_tripped is not None + self.assertEqual(round_tripped.netbios_per_interface, s.netbios_per_interface) + self.assertEqual(round_tripped.llmnr_present, s.llmnr_present) + self.assertEqual(round_tripped.llmnr_prev_value, s.llmnr_prev_value) + self.assertEqual(round_tripped.mdns_present, s.mdns_present) + self.assertEqual(round_tripped.mdns_prev_value, s.mdns_prev_value) + + def test_lan_status_returns_keys(self) -> None: + from proxy_chain_manager.privacy_lan import lan_status + st = lan_status() + self.assertIn("netbios", st) + self.assertIn("llmnr", st) + self.assertIn("mdns", st) + + +class TestLeakAuditPure(unittest.TestCase): + def test_categorize_exit_private(self) -> None: + from proxy_chain_manager.leak_audit import _categorize_exit + self.assertEqual(_categorize_exit("10.0.0.5"), "PRIVATE — chain broken") + self.assertEqual(_categorize_exit("192.168.1.1"), "PRIVATE — chain broken") + self.assertEqual(_categorize_exit("172.20.5.5"), "PRIVATE — chain broken") + + def test_categorize_exit_loopback(self) -> None: + from proxy_chain_manager.leak_audit import _categorize_exit + self.assertEqual(_categorize_exit("127.0.0.1"), "LOOPBACK — chain broken") + + def test_categorize_exit_public(self) -> None: + from proxy_chain_manager.leak_audit import _categorize_exit + self.assertEqual(_categorize_exit("104.28.3.99"), "public") + self.assertEqual(_categorize_exit("8.8.8.8"), "public") + + def test_categorize_exit_none(self) -> None: + from proxy_chain_manager.leak_audit import _categorize_exit + self.assertEqual(_categorize_exit(None), "unknown") + + def test_audit_finding_aggregation(self) -> None: + from proxy_chain_manager.leak_audit import AuditReport + rep = AuditReport() + rep.add("X", True, "ok") + self.assertTrue(rep.overall_ok) + rep.add("Y", False, "bad", "note") + self.assertFalse(rep.overall_ok) + self.assertEqual(len(rep.findings), 2) + + +class TestExitIntelGeo(unittest.TestCase): + def test_parse_ipapi_lat_lon(self) -> None: + from proxy_chain_manager.exit_intel import _parse_ipapi + + out = _parse_ipapi( + { + "status": "success", + "query": "8.8.8.8", + "country": "United States", + "countryCode": "US", + "regionName": "Virginia", + "city": "Ashburn", + "lat": 39.03, + "lon": -77.5, + "timezone": "America/New_York", + "isp": "Google LLC", + "org": "Google Public DNS", + "as": "AS15169 Google LLC", + "mobile": False, + "proxy": False, + "hosting": True, + } + ) + self.assertTrue(out.ok) + self.assertAlmostEqual(out.lat or 0, 39.03) + self.assertAlmostEqual(out.lon or 0, -77.5) + + def test_parse_ipwhois_lat_lon(self) -> None: + from proxy_chain_manager.exit_intel import _parse_ipwhois + + out = _parse_ipwhois( + { + "success": True, + "ip": "1.1.1.1", + "country": "Australia", + "country_code": "AU", + "city": "Sydney", + "region": "New South Wales", + "latitude": -33.86, + "longitude": 151.2, + "timezone": {"id": "Australia/Sydney"}, + "connection": {"asn": 13335, "org": "Cloudflare", "isp": "Cloudflare"}, + } + ) + self.assertTrue(out.ok) + self.assertAlmostEqual(out.lat or 0, -33.86) + self.assertAlmostEqual(out.lon or 0, 151.2) + + +class TestChainMap(unittest.TestCase): + def test_extract_hop_host(self) -> None: + from proxy_chain_manager.chain_map import extract_hop_host + + self.assertEqual(extract_hop_host("http://203.0.113.10:8080"), "203.0.113.10") + self.assertEqual(extract_hop_host("socks5://proxy.example:1080"), "proxy.example") + self.assertIsNone(extract_hop_host("")) + + def test_project_corners(self) -> None: + from proxy_chain_manager.chain_map import _project + + x, y = _project(0, 0, 360, 180, 0) + self.assertAlmostEqual(x, 180.0) + self.assertAlmostEqual(y, 90.0) + + def test_render_chain_map_with_points(self) -> None: + from proxy_chain_manager.chain_map import MapPoint, render_chain_map + + pts = [ + MapPoint("you", "YOU", 40.0, -74.0, detail="NYC"), + MapPoint("hop", "H1", 51.5, -0.1, detail="London"), + MapPoint("exit", "EXIT", 35.6, 139.7, detail="Tokyo"), + ] + img = render_chain_map(pts, width=640, height=200, status="healthy") + self.assertEqual(img.size, (640, 200)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_secrets_store.py b/tests/test_secrets_store.py new file mode 100644 index 0000000..67938a9 --- /dev/null +++ b/tests/test_secrets_store.py @@ -0,0 +1,48 @@ +"""Round-trip tests for the DPAPI-backed secrets store. + +These are skipped automatically on non-Windows so the test suite stays cross- +platform. On Windows, DPAPI is part of the OS — no extra deps required. +""" +from __future__ import annotations + +import sys +import unittest + +from proxy_chain_manager import secrets_store + + +@unittest.skipUnless(sys.platform == "win32", "DPAPI only on Windows") +class TestSecretsStore(unittest.TestCase): + def test_empty_passthrough(self) -> None: + self.assertEqual(secrets_store.encrypt(""), "") + self.assertEqual(secrets_store.decrypt(""), "") + + def test_legacy_plaintext_passthrough(self) -> None: + # Anything without the DPAPI: prefix decrypts to itself so migration + # from plaintext files is automatic on first read. + self.assertEqual(secrets_store.decrypt("legacy_pw"), "legacy_pw") + + def test_round_trip_ascii(self) -> None: + secret = "hunter2!" + token = secrets_store.encrypt(secret) + self.assertTrue(token.startswith("DPAPI:v1:")) + self.assertEqual(secrets_store.decrypt(token), secret) + + def test_round_trip_unicode(self) -> None: + secret = "пароль · 🔐 · café" + token = secrets_store.encrypt(secret) + self.assertEqual(secrets_store.decrypt(token), secret) + + def test_double_encrypt_idempotent(self) -> None: + token = secrets_store.encrypt("once") + again = secrets_store.encrypt(token) + self.assertEqual(token, again) + + def test_is_encrypted_helper(self) -> None: + self.assertFalse(secrets_store.is_encrypted("")) + self.assertFalse(secrets_store.is_encrypted("plaintext")) + self.assertTrue(secrets_store.is_encrypted(secrets_store.encrypt("x"))) + + +if __name__ == "__main__": + unittest.main() diff --git a/winreg.py b/winreg.py new file mode 100644 index 0000000..8097983 --- /dev/null +++ b/winreg.py @@ -0,0 +1,60 @@ +"""Tiny non-Windows winreg compatibility shim. + +The upstream Proxy God code is Windows-first and imports ``winreg`` in several +modules. macOS does not ship that module. This shim keeps imports working and +makes registry operations behave like unavailable keys. +""" +from __future__ import annotations + +HKEY_CURRENT_USER = object() +HKEY_LOCAL_MACHINE = object() +KEY_QUERY_VALUE = 0x0001 +KEY_SET_VALUE = 0x0002 +KEY_ALL_ACCESS = 0xF003F +REG_BINARY = 3 +REG_DWORD = 4 +REG_SZ = 1 + + +class HKEYType: + def __enter__(self) -> "HKEYType": + return self + + def __exit__(self, exc_type, exc, tb) -> bool: + return False + + +def OpenKey(*args, **kwargs) -> HKEYType: + raise OSError("winreg is unavailable on this platform") + + +def CreateKeyEx(*args, **kwargs) -> HKEYType: + raise OSError("winreg is unavailable on this platform") + + +def QueryValue(*args, **kwargs): + raise OSError("winreg is unavailable on this platform") + + +def QueryValueEx(*args, **kwargs): + raise OSError("winreg is unavailable on this platform") + + +def SetValueEx(*args, **kwargs) -> None: + raise OSError("winreg is unavailable on this platform") + + +def DeleteValue(*args, **kwargs) -> None: + raise OSError("winreg is unavailable on this platform") + + +def DeleteKey(*args, **kwargs) -> None: + raise OSError("winreg is unavailable on this platform") + + +def EnumValue(*args, **kwargs): + raise OSError("winreg is unavailable on this platform") + + +def EnumKey(*args, **kwargs): + raise OSError("winreg is unavailable on this platform")

2}n=o`58!F$Z_JRm+k=Gv8K*u8j} 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 ZHB6V>G-D;wm;l`VQc$^-E%)&Ge*w(^nc@Hd literal 0 HcmV?d00001 diff --git a/proxy_chain_manager/__init__.py b/proxy_chain_manager/__init__.py new file mode 100644 index 0000000..917bc75 --- /dev/null +++ b/proxy_chain_manager/__init__.py @@ -0,0 +1,3 @@ +"""Rotating proxy chain manager with a GOST backend.""" + +__version__ = "2.0.0-mac" diff --git a/proxy_chain_manager/__main__.py b/proxy_chain_manager/__main__.py new file mode 100644 index 0000000..37c2a2d --- /dev/null +++ b/proxy_chain_manager/__main__.py @@ -0,0 +1,4 @@ +from .app import main + +if __name__ == "__main__": + main() diff --git a/proxy_chain_manager/app.py b/proxy_chain_manager/app.py new file mode 100644 index 0000000..db710c3 --- /dev/null +++ b/proxy_chain_manager/app.py @@ -0,0 +1,3074 @@ +""" +Proxy God — GUI +Tabs: Live | Chain Builder | Browser | Ban Tester | Signup Prep | Privacy | Settings +(Live tab includes exit intel + world chain map) +""" +from __future__ import annotations + +import logging +import ipaddress +import queue +import sys +import threading +import time +from dataclasses import asdict +from datetime import datetime +from typing import Any, Literal +from tkinter import filedialog +from urllib.parse import urlparse + +import customtkinter as ctk + +from .config import ( + LISTEN_HOST, + OBFUSCATION_LABELS, + OBFUSCATION_MODES, + Settings, + load_settings, + merge_proxy_credentials, + normalize_proxy_url, + parse_exit_proxy_input, + redact_proxy_url, + save_settings, + split_proxy_for_edit, +) +from .firewall import ( + disengage as fw_disengage, + is_admin, + is_engaged as fw_is_engaged, + request_admin_relaunch, +) +from .artifact_wipe import wipe_artifacts +from .ban_tester import BanTestResult, SITE_CATEGORIES, run_ban_tests, test_single_site +from .exit_intel import ExitIntel, fetch_exit_intel +from .chain_map import build_chain_map_points, render_chain_map, resize_map_display +from .signup_prep import ( + SIGNUP_PRESET_KEYS, + SIGNUP_PRESETS, + AccountRecord, + SignupDraft, + append_account, + generate_password, + load_accounts, + load_draft, + random_first_name, + random_full_name, + random_last_name, + random_username, + resolve_signup_url, + save_draft, +) +from .browser_identity import ( + COOKIE_LABELS, + COOKIE_MODES, + PERSONA_LABELS, + PERSONAS, +) +from .dns_leak import ( + FullDnsLeakReport, + check_dns_leak_hint, + flush_dns_cache, + run_dns_leak_test, +) +from .leak_audit import AuditReport, run_audit_sync +from .webrtc_check import WebRtcCheckResult, run_webrtc_check +from .browser_launcher import ( + BrowserConfig, + BrowserSession, + default_firefox_path, + default_profile_dir, +) +from .fingerprint import audit_device +from .gui_validate import run_validate_each_sync +from .paths import app_data_dir +from .proxy_picker import show_proxy_picker +from .service import ChainService +from .sysproxy import clear_system_proxy, is_system_proxy_set +from .tray import TrayIcon +from .vpn_detect import detect_vpn +from .windows_task import install_logon_task, task_exists, uninstall_logon_task + +LOG_PATH = app_data_dir() / "proxy_chain_manager.log" +from logging.handlers import RotatingFileHandler as _RotatingFileHandler # noqa: E402 +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + handlers=[ + _RotatingFileHandler( + LOG_PATH, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8" + ), + logging.StreamHandler(sys.stdout), + ], +) + + +class UILogHandler(logging.Handler): + """Forward ``proxy_chain_manager`` log records to the Live tab (via ``ui_q``). Thread-safe.""" + + def __init__(self, q: queue.Queue[dict[str, Any]]) -> None: + super().__init__(level=logging.DEBUG) + self._q = q + + def emit(self, record: logging.LogRecord) -> None: + try: + text = self.format(record) + self._q.put_nowait({"type": "log", "text": text}) + except Exception: + self.handleError(record) + + +# ── palette ────────────────────────────────────────────────────────────────── +# Neon "blueprint terminal" theme — same overall vibe, deeper black, sharper +# cyan/magenta accent, glow borders rendered via 1px border_color on cards. +BG = "#070713" +CARD = "#0f0f24" +PANEL = "#171738" +ACCENT = "#3052ff" # primary action / outlines +ACCENT2 = "#00e5ff" # secondary highlight / glow +GREEN = "#00ff9c" +RED = "#ff2a6d" +YELLOW = "#ffd000" +ORANGE = "#ff8a00" +PURPLE = "#c77dff" +BLUE = "#5b8bff" +CYAN = "#00f5ff" +MAGENTA = "#ff00d6" +DIM = "#2a2f4a" +TEXT = "#eaf2ff" +TEXT2 = "#7a8aab" +GLOW = "#0a87ff" +FONT = "Segoe UI" + + +def _ts() -> str: + return datetime.now().strftime("%H:%M:%S") + + +def main() -> None: + try: + _main_inner() + except Exception as _exc: # noqa: BLE001 + _log = logging.getLogger(__name__) + _log.exception("Unhandled exception in main loop") + try: + import tkinter.messagebox as _mb + _mb.showerror( + "Proxy God — Fatal Error", + f"An unexpected error occurred and the application must close.\n\n" + f"{type(_exc).__name__}: {_exc}\n\n" + f"Details have been written to:\n{LOG_PATH}", + ) + except Exception: + pass + raise + + +def _main_inner() -> None: + ctk.set_appearance_mode("dark") + ctk.set_default_color_theme("dark-blue") + + root = ctk.CTk() + from . import __version__ as _APP_VERSION + root.title(f"Proxy God v{_APP_VERSION}") + root.geometry("1080x860") + root.minsize(900, 720) + root.configure(fg_color=BG) + + # ── 3D-ish neon header banner (drawn on a Canvas for a true gradient) ──── + def _draw_header_banner(parent: Any) -> Any: + import tkinter as tk + canvas = tk.Canvas( + parent, height=44, highlightthickness=0, bd=0, bg=BG, + ) + canvas.pack(fill="x", side="top") + + def _hex(rgb: tuple[int, int, int]) -> str: + return "#" + "".join(f"{v:02x}" for v in rgb) + + def _lerp(a: tuple[int, int, int], b: tuple[int, int, int], t: float) -> tuple[int, int, int]: + return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(3)) # type: ignore[return-value] + + def _repaint(_event: Any = None) -> None: + canvas.delete("grad") + w = canvas.winfo_width() or 1080 + h = 44 + left = (8, 32, 64) # near-black blue + mid = (48, 82, 255) # ACCENT + right = (0, 229, 255) # ACCENT2 cyan + for x in range(0, w, 2): + t = x / max(1, w - 1) + c = _lerp(left, mid, t * 2) if t < 0.5 else _lerp(mid, right, (t - 0.5) * 2) + canvas.create_line(x, 0, x, h, fill=_hex(c), tags="grad") + # 1px neon underline + canvas.create_line(0, h - 1, w, h - 1, fill=CYAN, width=1, tags="grad") + # Title text + small subtitle + canvas.create_text( + 18, h // 2 - 2, anchor="w", + text="P R O X Y G O D", + fill="#ffffff", font=(FONT, 16, "bold"), + tags="grad", + ) + canvas.create_text( + 190, h // 2 - 1, anchor="w", + text="// multi-hop · anti-fingerprint · paranoid mode", + fill="#bfeaff", font=(FONT, 9, "italic"), + tags="grad", + ) + + canvas.bind("", _repaint) + return canvas + + _draw_header_banner(root) + + ui_q: queue.Queue[dict[str, Any]] = queue.Queue() + svc = ChainService(notify=lambda m: ui_q.put(m)) + s = load_settings() + browser = BrowserSession() + browser_should_run = [False] + browser_last_launch_ts = [0.0] + browser_relaunch_count = [0] + service_running = [False] + + # ── tray ───────────────────────────────────────────────────────────────── + tray = TrayIcon( + on_show=lambda: root.after(0, lambda: (root.deiconify(), root.lift())), + on_quit=lambda: root.after(0, _close), + on_rotate=svc.rotate_now, + ) + tray.start() + + pulse_job: dict[str, str | None] = {"id": None} + HopState = Literal["untested", "testing", "ok", "fail"] + + def _cancel_pulse() -> None: + if pulse_job["id"]: + try: + root.after_cancel(pulse_job["id"]) # type: ignore[arg-type] + except Exception: + pass + pulse_job["id"] = None + + def _short(u: str) -> str: + u = redact_proxy_url(u) + u = u.replace("http://", "").replace("socks5://", "s5://").replace("socks4://", "s4://").replace("https://", "") + return u[:28] + "…" if len(u) > 30 else u + + def _extract_ip_host(proxy_url: str) -> str | None: + p = urlparse(normalize_proxy_url(proxy_url)) + host = (p.hostname or "").strip() + if not host: + return None + try: + ipaddress.ip_address(host) + return host + except ValueError: + return None + + # ───────────────────────────────────────────────────────────────────────── + # HELPERS + # ───────────────────────────────────────────────────────────────────────── + def _btn(parent: Any, label: str, cmd: Any, w: int = 80, h: int = 28, **kw: Any) -> ctk.CTkButton: + # Pull known keys out first so **kw never duplicates CTkButton parameters (font, width, …). + kw.pop("text", None) + kw.pop("command", None) + font = kw.pop("font", (FONT, 11)) + fg_color = kw.pop("fg_color", ACCENT) + hover_color = kw.pop("hover_color", ACCENT2) + text_color = kw.pop("text_color", TEXT) + corner_radius = kw.pop("corner_radius", 6) + width = kw.pop("width", w) + height = kw.pop("height", h) + return ctk.CTkButton( + parent, + text=label, + command=cmd, + width=width, + height=height, + font=font, + fg_color=fg_color, + hover_color=hover_color, + text_color=text_color, + corner_radius=corner_radius, + **kw, + ) + + # ───────────────────────────────────────────────────────────────────────── + # TOP BAR + # ───────────────────────────────────────────────────────────────────────── + topbar = ctk.CTkFrame(root, fg_color=CARD, corner_radius=0, height=48) + topbar.pack(fill="x", side="top") + topbar.pack_propagate(False) + + # Status + status_dot = ctk.CTkLabel(topbar, text="●", font=(FONT, 20), text_color=RED) + status_dot.pack(side="left", padx=(10, 2)) + status_txt = ctk.CTkLabel(topbar, text="STOPPED", font=(FONT, 12, "bold"), text_color=TEXT) + status_txt.pack(side="left", padx=(0, 8)) + + def _start() -> None: + _save_settings() + # Hard-stop: if the kill-switch is enabled but we are not admin, the + # firewall lockdown is silently skipped — which contradicts the + # README's "fail closed" guarantee. Refuse to start in that mode. + try: + from .firewall import is_admin as _is_admin + except Exception: + _is_admin = lambda: True # noqa: E731 + if svc.settings.kill_switch_enabled and not _is_admin(): + from tkinter import messagebox + if messagebox.askyesno( + "Kill-switch requires Admin", + "Kill-switch is enabled in Settings but this process is not " + "running as Administrator.\n\n" + "Continuing now will start the chain WITHOUT the firewall " + "lockdown — your traffic will not 'fail closed' if the chain " + "drops.\n\n" + "Start anyway?", + ): + svc.start() + return + svc.start() + + start_btn = _btn(topbar, "▶ Start", _start, w=86) + start_btn.pack(side="left", padx=2) + stop_btn = _btn(topbar, "■ Stop", svc.stop, w=76) + stop_btn.pack(side="left", padx=2) + rotate_btn = _btn(topbar, "↻ Rotate", svc.rotate_now, w=76) + rotate_btn.pack(side="left", padx=2) + _btn(topbar, "Quit", lambda: root.after(0, _close), w=52, + fg_color="#7f1d1d", hover_color="#991b1b").pack(side="left", padx=(10, 2)) + + # Boot buttons + ctk.CTkLabel(topbar, text="│", text_color=DIM).pack(side="left", padx=4) + boot_lbl = ctk.CTkLabel(topbar, text="Boot:?", font=(FONT, 10), text_color=TEXT2) + boot_lbl.pack(side="left", padx=2) + + def _refresh_boot() -> None: + on = task_exists() + boot_lbl.configure(text="Boot:ON" if on else "Boot:OFF", + text_color=GREEN if on else DIM) + + def _inst_boot() -> None: + ok, msg = install_logon_task() + _log("Boot task installed." if ok else f"Boot install: {msg}") + _refresh_boot() + + def _rm_boot() -> None: + ok, msg = uninstall_logon_task() + _log("Boot task removed." if ok else f"Boot remove: {msg}") + _refresh_boot() + + _btn(topbar, "Boot+", _inst_boot, w=54).pack(side="left", padx=2) + _btn(topbar, "Boot−", _rm_boot, w=54).pack(side="left", padx=2) + + # Right side indicators + rotation_lbl = ctk.CTkLabel(topbar, text="rot: 0", font=(FONT, 10), text_color=TEXT2) + rotation_lbl.pack(side="right", padx=(4, 12)) + + countdown_lbl = ctk.CTkLabel(topbar, text="next: —", font=(FONT, 10), text_color=TEXT2) + countdown_lbl.pack(side="right", padx=4) + + proxy_lbl = ctk.CTkLabel(topbar, text=f"{LISTEN_HOST}:{s.local_port}", + font=(FONT, 11, "bold"), text_color=BLUE) + proxy_lbl.pack(side="right", padx=(4, 4)) + ctk.CTkLabel(topbar, text="proxy:", font=(FONT, 10), text_color=TEXT2).pack(side="right") + + fw_lbl = ctk.CTkLabel(topbar, text="FW:—", font=(FONT, 10), text_color=DIM) + fw_lbl.pack(side="right", padx=6) + + sys_lbl = ctk.CTkLabel(topbar, text="SYS:—", font=(FONT, 10), text_color=DIM) + sys_lbl.pack(side="right", padx=4) + + admin_badge = ctk.CTkLabel( + topbar, + text="⚡ ADMIN" if is_admin() else "👤 USER", + font=(FONT, 10, "bold"), + text_color=GREEN if is_admin() else YELLOW, + ) + admin_badge.pack(side="right", padx=4) + + 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, + fg_color="#7f1d1d", hover_color="#991b1b").pack(side="right", padx=4) + + # ───────────────────────────────────────────────────────────────────────── + # CHAIN VIZ BAR + # ───────────────────────────────────────────────────────────────────────── + viz_bar = ctk.CTkFrame(root, fg_color=PANEL, corner_radius=0, height=52) + viz_bar.pack(fill="x") + viz_bar.pack_propagate(False) + + your_ip_lbl = ctk.CTkLabel(viz_bar, text="Your IP: —", font=(FONT, 10), text_color=TEXT2) + your_ip_lbl.pack(side="left", padx=(12, 8)) + + chain_canvas = ctk.CTkFrame(viz_bar, fg_color="transparent") + chain_canvas.pack(side="left", fill="both", expand=True) + + exit_ip_lbl = ctk.CTkLabel(viz_bar, text="Exit: —", + font=(FONT, 13, "bold"), text_color=YELLOW) + exit_ip_lbl.pack(side="right", padx=(8, 14)) + + hop_dot_labels: list[ctk.CTkLabel] = [] + outer_hop_label = "VPN" # updated when VPN detected; "Direct" if no tunnel + + def _pulse(step: int = 0) -> None: + if not hop_dot_labels: + pulse_job["id"] = None + return + c = YELLOW if step % 2 == 0 else DIM + for d in hop_dot_labels: + d.configure(text_color=c) + pulse_job["id"] = root.after(380, lambda s=step: _pulse(s + 1)) # type: ignore[arg-type] + + def _render_chain( + hops: list[str], status: str, exit_ip: str | None, fixed_last: bool = False + ) -> None: + _cancel_pulse() + for w in list(chain_canvas.winfo_children()): + w.destroy() + hop_dot_labels.clear() + + hop_c = {"healthy": GREEN, "dead": RED, "connecting": YELLOW}.get(status, DIM) + + def _node(text: str, color: str, bold: bool = False) -> None: + ctk.CTkLabel( + chain_canvas, text=text, + font=(FONT, 11, "bold") if bold else (FONT, 10), + text_color=color, + ).pack(side="left", padx=1) + + def _arr() -> None: + ctk.CTkLabel(chain_canvas, text="→", font=(FONT, 11), text_color=DIM).pack(side="left", padx=2) + + _node("YOU", GREEN, bold=True) + _arr() + _node(outer_hop_label[:12], PURPLE, bold=True) + _arr() + + last_i = len(hops) - 1 + for i, h in enumerate(hops): + is_fixed = fixed_last and i == last_i and last_i >= 0 + lbl_c = ORANGE if is_fixed else hop_c + dot = ctk.CTkLabel(chain_canvas, text="●", font=(FONT, 12), text_color=lbl_c) + dot.pack(side="left", padx=1) + hop_dot_labels.append(dot) + label = _short(h) + (" ★" if is_fixed else "") + ctk.CTkLabel(chain_canvas, text=label, font=(FONT, 10), text_color=lbl_c).pack(side="left", padx=1) + if i < len(hops) - 1: + _arr() + + _arr() + _node("WEB", BLUE, bold=True) + + exit_ip_lbl.configure( + text=f"Exit: {exit_ip}" if exit_ip else "Exit: —", + text_color=GREEN if status == "healthy" else (RED if exit_ip else YELLOW), + ) + + if status == "connecting" and hop_dot_labels: + _pulse() + + # ───────────────────────────────────────────────────────────────────────── + # PROGRESS STRIP + # ───────────────────────────────────────────────────────────────────────── + prog_strip = ctk.CTkFrame(root, fg_color=BG, height=22) + prog_strip.pack(fill="x", padx=8, pady=(4, 0)) + prog_strip.pack_propagate(False) + + phase_lbl = ctk.CTkLabel(prog_strip, text="idle", font=(FONT, 10), text_color=TEXT2, width=70) + phase_lbl.pack(side="left", padx=4) + + prog_bar = ctk.CTkProgressBar(prog_strip, height=8, corner_radius=4, + fg_color=CARD, progress_color=BLUE) + prog_bar.pack(side="left", fill="x", expand=True, padx=4) + prog_bar.set(0) + + pool_lbl = ctk.CTkLabel(prog_strip, text="pool: 0", font=(FONT, 10), text_color=TEXT2, width=70) + pool_lbl.pack(side="right", padx=4) + + # ───────────────────────────────────────────────────────────────────────── + # TABVIEW + # ───────────────────────────────────────────────────────────────────────── + tabs = ctk.CTkTabview( + root, + fg_color=BG, + segmented_button_fg_color=CARD, + segmented_button_selected_color=ACCENT2, + segmented_button_unselected_color=CARD, + segmented_button_selected_hover_color=ACCENT, + border_width=0, + ) + tabs.pack(fill="both", expand=True, padx=8, pady=(4, 8)) + + tab_live = tabs.add(" Live ") + tab_chain = tabs.add(" Chain Builder ") + tab_browser = tabs.add(" Browser ") + tab_ban = tabs.add(" Ban Tester ") + tab_signup = tabs.add(" Signup Prep ") + tab_privacy = tabs.add(" Privacy ") + tab_settings = tabs.add(" Settings ") + for tab in (tab_live, tab_chain, tab_browser, tab_ban, tab_signup, tab_privacy, tab_settings): + tab.configure(fg_color=BG) + + # ─── TAB: LIVE ─────────────────────────────────────────────────────────── + # Stats cards row + stats_row = ctk.CTkFrame(tab_live, fg_color="transparent") + stats_row.pack(fill="x", pady=(4, 6)) + + def _stat_card(parent: Any, title: str, var: ctk.StringVar, color: str = TEXT) -> None: + f = ctk.CTkFrame(parent, fg_color=CARD, corner_radius=8) + f.pack(side="left", fill="x", expand=True, padx=4) + ctk.CTkLabel(f, text=title, font=(FONT, 9), text_color=TEXT2).pack( + anchor="w", padx=8, pady=(6, 0)) + ctk.CTkLabel(f, textvariable=var, font=(FONT, 14, "bold"), text_color=color).pack( + anchor="w", padx=8, pady=(0, 6)) + + v_real_ip = ctk.StringVar(value="—") + v_exit_ip = ctk.StringVar(value="—") + v_pool = ctk.StringVar(value="0") + v_rotation = ctk.StringVar(value="0") + + _stat_card(stats_row, "Direct / VPN IP", v_real_ip) + _stat_card(stats_row, "Exit IP (last hop)", v_exit_ip) + _stat_card(stats_row, "Valid pool size", v_pool) + _stat_card(stats_row, "Rotations this session", v_rotation) + + # ── Exit IP intel card (geo / ASN / datacenter) ────────────────────────── + intel_card = ctk.CTkFrame(tab_live, fg_color=CARD, corner_radius=8) + intel_card.pack(fill="x", pady=(0, 6)) + + intel_head = ctk.CTkFrame(intel_card, fg_color="transparent") + intel_head.pack(fill="x", padx=10, pady=(8, 2)) + ctk.CTkLabel( + intel_head, text="Exit IP intel", font=(FONT, 11, "bold"), text_color=CYAN, + ).pack(side="left") + + v_intel_loc = ctk.StringVar(value="Location: —") + v_intel_asn = ctk.StringVar(value="ASN / ISP: —") + v_intel_tags = ctk.StringVar(value="Flags: —") + + intel_body = ctk.CTkFrame(intel_card, fg_color="transparent") + intel_body.pack(fill="x", padx=10, pady=(0, 8)) + ctk.CTkLabel(intel_body, textvariable=v_intel_loc, font=(FONT, 11), text_color=TEXT, + anchor="w").pack(anchor="w") + ctk.CTkLabel(intel_body, textvariable=v_intel_asn, font=(FONT, 11), text_color=TEXT2, + anchor="w").pack(anchor="w") + intel_tags_lbl = ctk.CTkLabel(intel_body, textvariable=v_intel_tags, font=(FONT, 11, "bold"), + text_color=GREEN, anchor="w") + intel_tags_lbl.pack(anchor="w") + + intel_running = [False] + last_intel: dict[str, ExitIntel | None] = {"intel": None} + + def _apply_intel(intel: ExitIntel | None) -> None: + last_intel["intel"] = intel + if intel is None or not intel.ok: + v_intel_loc.set("Location: —") + v_intel_asn.set("ASN / ISP: —") + v_intel_tags.set("Flags: " + (intel.detail if intel and intel.detail else "—")) + intel_tags_lbl.configure(text_color=TEXT2) + return + loc_parts = [p for p in (intel.city, intel.region, intel.country) if p] + v_intel_loc.set("Location: " + (", ".join(loc_parts) if loc_parts else "—")) + asn_str = f"AS{intel.asn} " if intel.asn else "" + org_str = intel.org or intel.isp or "—" + v_intel_asn.set(f"ASN / ISP: {asn_str}{org_str}") + tags: list[str] = [] + color = GREEN + if intel.is_datacenter: + tags.append("DATACENTER") + color = ORANGE + if intel.is_proxy_flagged: + tags.append("PROXY-FLAGGED") + color = RED + if intel.is_mobile: + tags.append("MOBILE") + if not tags: + tags.append("residential-like") + color = GREEN + v_intel_tags.set("Flags: " + " · ".join(tags)) + intel_tags_lbl.configure(text_color=color) + root.after(200, lambda: _refresh_chain_map(silent=True)) + + def _refresh_exit_intel(silent: bool = False) -> None: + if intel_running[0]: + return + if not service_running[0] or not svc.current_chain: + if not silent: + _log("Exit intel: chain not running.") + _apply_intel(None) + return + intel_running[0] = True + v_intel_tags.set("Flags: looking up…") + intel_tags_lbl.configure(text_color=YELLOW) + proxy_url = f"http://{svc.settings.listen_addr()}" + + def work() -> None: + intel = fetch_exit_intel( + proxy_url, + timeout_seconds=min(15.0, svc.settings.validation_timeout_seconds + 3.0), + ) + + def done() -> None: + _apply_intel(intel) + intel_running[0] = False + if not silent: + if intel.ok: + _log(f"Exit intel: {intel.summary()} (via {intel.source})") + else: + _log(f"Exit intel: lookup failed — {intel.detail}") + + root.after(0, done) + + threading.Thread(target=work, daemon=True).start() + + intel_btn_row = ctk.CTkFrame(intel_card, fg_color="transparent") + intel_btn_row.pack(fill="x", padx=10, pady=(0, 8)) + _btn(intel_btn_row, "Refresh intel", lambda: _refresh_exit_intel(False), w=110, h=24, + fg_color=ACCENT2, hover_color=ACCENT).pack(side="left") + + # ── Chain world map ─────────────────────────────────────────────────────── + map_card = ctk.CTkFrame(tab_live, fg_color=CARD, corner_radius=8) + map_card.pack(fill="x", pady=(0, 6)) + + map_head = ctk.CTkFrame(map_card, fg_color="transparent") + map_head.pack(fill="x", padx=10, pady=(8, 2)) + ctk.CTkLabel( + map_head, text="Chain map", font=(FONT, 11, "bold"), text_color=CYAN, + ).pack(side="left") + v_map_status = ctk.StringVar(value="Plotting: waiting for chain…") + ctk.CTkLabel( + map_head, textvariable=v_map_status, font=(FONT, 10), text_color=TEXT2, + ).pack(side="right") + + map_frame = ctk.CTkFrame(map_card, fg_color=BG, corner_radius=6) + map_frame.pack(fill="x", padx=10, pady=(0, 8)) + map_img_label = ctk.CTkLabel(map_frame, text="") + map_img_label.pack(padx=2, pady=2) + + map_ctk_image: dict[str, ctk.CTkImage | None] = {"img": None} + last_chain_state: dict[str, Any] = { + "hops": [], + "status": "idle", + "exit_ip": "", + "direct_ip": "", + } + map_running = [False] + + def _show_chain_map(pil_img: Any) -> None: + try: + map_frame.update_idletasks() + w = max(640, map_frame.winfo_width() - 8) + h = max(180, min(240, int(w * 0.23))) + resized = resize_map_display(pil_img, w, h) + map_ctk_image["img"] = ctk.CTkImage( + light_image=resized, dark_image=resized, size=(w, h), + ) + map_img_label.configure(image=map_ctk_image["img"], text="") + except Exception as e: + logging.getLogger(__name__).debug("chain map display: %s", e) + + def _refresh_chain_map(silent: bool = True) -> None: + if map_running[0]: + return + hops = list(last_chain_state["hops"]) + status = str(last_chain_state["status"] or "idle") + if not service_running[0] or not hops: + v_map_status.set("Plotting: chain not running") + _show_chain_map(render_chain_map([], status="idle")) + return + map_running[0] = True + v_map_status.set("Plotting: resolving hop locations…") + direct_ip = str(last_chain_state.get("direct_ip") or "") + exit_ip = str(last_chain_state.get("exit_ip") or "") + exit_intel = last_intel.get("intel") + timeout = min(12.0, svc.settings.validation_timeout_seconds + 2.0) + + def work() -> None: + points = build_chain_map_points( + hops, + direct_ip=direct_ip, + exit_ip=exit_ip, + exit_intel=exit_intel if isinstance(exit_intel, ExitIntel) else None, + timeout_seconds=timeout, + ) + img = render_chain_map(points, width=960, height=220, status=status) # type: ignore[arg-type] + + def done() -> None: + _show_chain_map(img) + if points: + v_map_status.set(f"Plotting: {len(points)} nodes · {status}") + if not silent: + _log(f"Chain map updated — {len(points)} geo nodes ({status}).") + else: + v_map_status.set("Plotting: no geo data yet (hop DNS/geo pending)") + map_running[0] = False + + root.after(0, done) + + threading.Thread(target=work, daemon=True).start() + + _btn(map_head, "Refresh map", lambda: _refresh_chain_map(False), w=100, h=22, + fg_color=ACCENT2, hover_color=ACCENT).pack(side="right", padx=(8, 0)) + + _show_chain_map(render_chain_map([], status="idle")) + + # Log box + log_frame = ctk.CTkFrame(tab_live, fg_color=CARD, corner_radius=8) + log_frame.pack(fill="both", expand=True) + log_box = ctk.CTkTextbox( + log_frame, font=("Consolas", 10), + fg_color=BG, text_color=TEXT, + scrollbar_button_color=ACCENT, + ) + log_box.pack(fill="both", expand=True, padx=4, pady=(0, 4)) + + def _log(line: str) -> None: + log_box.insert("end", f"[{_ts()}] {line}\n") + log_box.see("end") + + log_tool = ctk.CTkFrame(log_frame, fg_color="transparent") + log_tool.pack(fill="x", padx=8, pady=(6, 0), before=log_box) + verbose_logs_var = ctk.BooleanVar(value=True) + _pcm_logger = logging.getLogger("proxy_chain_manager") + _ui_log_handler: UILogHandler | None = None + for _h in _pcm_logger.handlers: + if isinstance(_h, UILogHandler): + _ui_log_handler = _h + break + if _ui_log_handler is None: + _ui_log_handler = UILogHandler(ui_q) + _ui_log_handler.setFormatter( + logging.Formatter("%(levelname)-5s [%(name)s] %(message)s") + ) + _pcm_logger.addHandler(_ui_log_handler) + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("httpcore").setLevel(logging.WARNING) + + def _sync_ui_log_level() -> None: + on = bool(verbose_logs_var.get()) + lvl = logging.DEBUG if on else logging.INFO + _pcm_logger.setLevel(lvl) + if _ui_log_handler is not None: + _ui_log_handler.setLevel(lvl) + _log(f"UI log level: {'DEBUG (verbose)' if on else 'INFO'}") + + ctk.CTkCheckBox( + log_tool, + text="Verbose logs (DEBUG in UI — file stays INFO)", + variable=verbose_logs_var, + command=_sync_ui_log_level, + font=(FONT, 11), + fg_color=ACCENT2, + hover_color=ACCENT, + text_color=TEXT, + ).pack(side="left", padx=(0, 12)) + _btn(log_tool, "Clear log", lambda: log_box.delete("1.0", "end"), w=88, h=26, + fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6)) + _sync_ui_log_level() + + # ─── TAB: CHAIN BUILDER ────────────────────────────────────────────────── + cb_split = ctk.CTkFrame(tab_chain, fg_color="transparent") + cb_split.pack(fill="x", pady=(4, 8)) + + # Left — mode + hop slider + cb_left = ctk.CTkFrame(cb_split, fg_color=CARD, corner_radius=8) + cb_left.pack(side="left", fill="both", expand=True, padx=(0, 6)) + + ctk.CTkLabel(cb_left, text="Obfuscation Mode", font=(FONT, 12, "bold"), + text_color=TEXT).pack(anchor="w", padx=12, pady=(12, 4)) + + mode_var = ctk.StringVar(value=s.obfuscation_mode) + for mk in OBFUSCATION_MODES: + ctk.CTkRadioButton( + cb_left, + text=OBFUSCATION_LABELS[mk], + variable=mode_var, + value=mk, + font=(FONT, 11), + fg_color=ACCENT2, + hover_color=ACCENT, + text_color=TEXT, + ).pack(anchor="w", padx=16, pady=3) + + elite_var = ctk.BooleanVar(value=s.prefer_elite) + ctk.CTkCheckBox( + cb_left, text="Elite proxies only (slower but more anonymous)", + variable=elite_var, font=(FONT, 11), + fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT, + ).pack(anchor="w", padx=16, pady=(0, 8)) + + exit_fix_frame = ctk.CTkFrame(cb_left, fg_color=PANEL, corner_radius=8) + exit_fix_frame.pack(fill="x", padx=12, pady=(0, 12)) + exit_hdr = ctk.CTkFrame(exit_fix_frame, fg_color="transparent") + exit_hdr.pack(fill="x", padx=10, pady=(8, 2)) + ctk.CTkLabel( + exit_hdr, + text="Exit node (optional final hop)", + font=(FONT, 11, "bold"), + text_color=ORANGE, + ).pack(side="left") + exit_test_dot = ctk.CTkLabel(exit_hdr, text="●", font=(FONT, 14), text_color=RED) + exit_test_dot.pack(side="left", padx=(8, 4)) + exit_proxy_entry = ctk.CTkEntry( + exit_fix_frame, + placeholder_text="host:port:user:pass (defaults to socks5 — paste any format)", + font=("Consolas", 10), + fg_color=BG, + border_color=ACCENT, + height=28, + ) + exit_proxy_entry.pack(fill="x", padx=10, pady=(0, 4)) + exit_auth_row = ctk.CTkFrame(exit_fix_frame, fg_color="transparent") + exit_auth_row.pack(fill="x", padx=10, pady=(0, 4)) + ctk.CTkLabel(exit_auth_row, text="User", font=(FONT, 10), text_color=TEXT2, width=36).pack( + side="left", padx=(0, 4) + ) + exit_user_entry = ctk.CTkEntry( + exit_auth_row, width=120, height=26, font=("Consolas", 10), + fg_color=BG, border_color=ACCENT, placeholder_text="optional", + ) + exit_user_entry.pack(side="left", padx=(0, 10)) + ctk.CTkLabel(exit_auth_row, text="Pass", font=(FONT, 10), text_color=TEXT2, width=36).pack( + side="left", padx=(0, 4) + ) + exit_pass_entry = ctk.CTkEntry( + exit_auth_row, width=120, height=26, font=("Consolas", 10), + fg_color=BG, border_color=ACCENT, placeholder_text="optional", show="*", + ) + exit_pass_entry.pack(side="left", padx=(0, 0)) + + _eb, _eu, _epw = split_proxy_for_edit(s.manual_exit_proxy) + if _eb: + exit_proxy_entry.insert(0, _eb) + if _eu: + exit_user_entry.insert(0, _eu) + if _epw: + exit_pass_entry.insert(0, _epw) + + exit_btn_row = ctk.CTkFrame(exit_fix_frame, fg_color="transparent") + exit_btn_row.pack(fill="x", padx=10, pady=(0, 4)) + + def _auto_split_exit_paste(_event: Any | None = None) -> None: + """If the URL field looks like ``host:port:user:pass``, distribute + the user/pass parts into their own fields so the user sees what got + parsed. Triggered on key release and explicit paste. + """ + raw = exit_proxy_entry.get() + if "://" in raw: + return + bits = [b for b in raw.strip().split(":") if b != ""] + if len(bits) < 4 and "@" not in raw and not any(c in raw for c in (" ", "\t")): + return + base, user, pw = parse_exit_proxy_input(raw, default_scheme="socks5") + if not base: + return + if base != raw: + exit_proxy_entry.delete(0, "end") + exit_proxy_entry.insert(0, base) + if user and not exit_user_entry.get().strip(): + exit_user_entry.delete(0, "end") + exit_user_entry.insert(0, user) + if pw and not exit_pass_entry.get().strip(): + exit_pass_entry.delete(0, "end") + exit_pass_entry.insert(0, pw) + + exit_proxy_entry.bind("", _auto_split_exit_paste) + exit_proxy_entry.bind("<>", lambda e: exit_proxy_entry.after(1, _auto_split_exit_paste)) + exit_proxy_entry.bind("", _auto_split_exit_paste) + + def _exit_url() -> str: + """Build the final exit-proxy URL. + + Accepts every paste format ``parse_exit_proxy_input`` understands + (host:port:user:pass, user:pass@host:port, scheme://…). Defaults to + SOCKS5 when no scheme is supplied. + """ + base, parsed_user, parsed_pw = parse_exit_proxy_input( + exit_proxy_entry.get(), default_scheme="socks5" + ) + if not base: + return "" + user = (exit_user_entry.get() or "").strip() or parsed_user + pw = (exit_pass_entry.get() or "").strip() or parsed_pw + return merge_proxy_credentials(base, user, pw) + + def _test_exit() -> None: + url = _exit_url() + if not url: + _log("Exit node: enter a proxy URL first.") + return + exit_test_dot.configure(text_color=YELLOW) + test_exit_btn.configure(state="disabled") + + def work() -> None: + def on_result(_u: str, ok: bool) -> None: + root.after(0, lambda: exit_test_dot.configure(text_color=GREEN if ok else RED)) + root.after(0, lambda: _log( + f"Exit node: {'OK' if ok else 'FAILED'} — {_short(url)}" + )) + + run_validate_each_sync( + [url], + svc.settings.ip_check_url, + min(12.0, svc.settings.validation_timeout_seconds), + on_result, + concurrency=1, + ) + root.after(0, lambda: test_exit_btn.configure(state="normal")) + + threading.Thread(target=work, daemon=True).start() + + test_exit_btn = _btn(exit_btn_row, "Test exit", _test_exit, w=88, h=26, + fg_color=ACCENT2, hover_color=ACCENT) + test_exit_btn.pack(side="left") + + def _save_exit_to_chain() -> None: + """Append the typed exit proxy to the manual chain as the final hop.""" + url = _exit_url() + if not url: + _log("Exit node: paste a host:port[:user:pass] first.") + return + # If the URL is already in the chain, move it to the bottom rather than dup. + if url in manual_chain: + manual_chain.remove(url) + manual_chain.append(url) + hop_status[url] = "ok" if ( + exit_test_dot.cget("text_color") == GREEN + ) else "untested" + use_manual_var.set(True) + _rebuild_chain_ui() + exit_proxy_entry.delete(0, "end") + exit_user_entry.delete(0, "end") + exit_pass_entry.delete(0, "end") + exit_test_dot.configure(text_color=RED) + _log(f"Exit node added as final hop ({len(manual_chain)} total).") + + _btn(exit_btn_row, "Save as final hop", _save_exit_to_chain, w=140, h=26, + fg_color=GREEN, hover_color="#00b377").pack(side="left", padx=(6, 0)) + + ctk.CTkLabel( + exit_fix_frame, + text=( + "Test exit, then 'Save as final hop' appends it to YOUR CHAIN above as the last proxy.\n" + "Paste any of: host:port:user:pass • user:pass@host:port • scheme://host:port\n" + "No scheme = SOCKS5. Paste auto-splits into User/Pass below." + ), + font=(FONT, 9), + text_color=TEXT2, + justify="left", + ).pack(anchor="w", padx=10, pady=(0, 8)) + + # Right — your chain + cb_right = ctk.CTkFrame(cb_split, fg_color=CARD, corner_radius=8, width=400) + cb_right.pack(side="left", fill="both", expand=True) + + hdr_row = ctk.CTkFrame(cb_right, fg_color="transparent") + hdr_row.pack(fill="x", padx=12, pady=(12, 2)) + ctk.CTkLabel( + hdr_row, text="Your chain", + font=(FONT, 12, "bold"), text_color=TEXT, + ).pack(side="left") + chain_count_lbl = ctk.CTkLabel(hdr_row, text="", font=(FONT, 10), text_color=TEXT2) + chain_count_lbl.pack(side="left", padx=(8, 0)) + + use_manual_var = ctk.BooleanVar(value=True if s.pinned_chain else s.use_pinned_chain) + use_chain_cb = ctk.CTkCheckBox( + hdr_row, text="Use this chain on Start", + variable=use_manual_var, font=(FONT, 10), + fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT2, + ) + use_chain_cb.pack(side="right") + + ctk.CTkLabel( + cb_right, + text="Traffic flows top → bottom. ● red = not tested ● green = live", + font=(FONT, 9), text_color=TEXT2, + ).pack(anchor="w", padx=12, pady=(0, 4)) + + manual_chain: list[str] = list(s.pinned_chain) + hop_status: dict[str, HopState] = {u: "untested" for u in manual_chain} + status_dots: dict[str, ctk.CTkLabel] = {} + + _HOP_COLOR = {"untested": RED, "testing": YELLOW, "ok": GREEN, "fail": RED} + + chain_list_frame = ctk.CTkScrollableFrame( + cb_right, fg_color=BG, height=200, corner_radius=6, + scrollbar_button_color=ACCENT, + ) + chain_list_frame.pack(fill="x", padx=12, pady=(0, 4)) + + def _update_chain_count() -> None: + n = len(manual_chain) + chain_count_lbl.configure(text=f"({n} hop{'s' if n != 1 else ''})") + + def _rebuild_chain_ui() -> None: + status_dots.clear() + for w in list(chain_list_frame.winfo_children()): + w.destroy() + for i, hop in enumerate(manual_chain): + if hop not in hop_status: + hop_status[hop] = "untested" + _chain_row(i, hop) + _update_chain_count() + + def _chain_row(idx: int, hop: str) -> None: + fr = ctk.CTkFrame(chain_list_frame, fg_color=PANEL, corner_radius=6, height=32) + fr.pack(fill="x", pady=2) + fr.pack_propagate(False) + + st = hop_status.get(hop, "untested") + dot = ctk.CTkLabel(fr, text="●", font=(FONT, 12), + text_color=_HOP_COLOR[st], width=18) + dot.pack(side="left", padx=(4, 0)) + status_dots[hop] = dot + + ctk.CTkLabel(fr, text=f"{idx+1}", font=(FONT, 10, "bold"), + text_color=CYAN, width=22).pack(side="left") + + proto = hop.split("://")[0].upper() if "://" in hop else "HTTP" + badge_c = {"HTTP": "#1e3a8a", "SOCKS5": "#064e3b", "SOCKS4": "#3b1a64"}.get(proto, DIM) + ctk.CTkLabel(fr, text=proto, font=(FONT, 9, "bold"), + fg_color=badge_c, corner_radius=4, padx=4, + text_color=TEXT).pack(side="left", padx=2) + + ctk.CTkLabel(fr, text=_short(hop), font=("Consolas", 10), + text_color=TEXT, anchor="w").pack(side="left", fill="x", expand=True, padx=4) + + def _up(i: int = idx) -> None: + if i > 0: + manual_chain[i], manual_chain[i-1] = manual_chain[i-1], manual_chain[i] + _rebuild_chain_ui() + + def _dn(i: int = idx) -> None: + if i < len(manual_chain) - 1: + manual_chain[i], manual_chain[i+1] = manual_chain[i+1], manual_chain[i] + _rebuild_chain_ui() + + def _rm(i: int = idx) -> None: + removed = manual_chain[i] + del manual_chain[i] + hop_status.pop(removed, None) + _rebuild_chain_ui() + + _btn(fr, "↑", _up, w=26, h=24, fg_color=DIM, hover_color=ACCENT).pack(side="right", padx=1) + _btn(fr, "↓", _dn, w=26, h=24, fg_color=DIM, hover_color=ACCENT).pack(side="right", padx=1) + _btn(fr, "✕", _rm, w=26, h=24, fg_color="#7f1d1d", hover_color=RED).pack(side="right", padx=(1, 6)) + + _rebuild_chain_ui() + + test_row = ctk.CTkFrame(cb_right, fg_color="transparent") + test_row.pack(fill="x", padx=12, pady=(0, 6)) + + def _set_all_status(state: HopState) -> None: + for u in manual_chain: + hop_status[u] = state + if u in status_dots: + status_dots[u].configure(text_color=_HOP_COLOR[state]) + + def _test_chain() -> None: + if not manual_chain: + _log("Add proxies to your chain first.") + return + test_chain_btn.configure(state="disabled") + _set_all_status("testing") + check_url = svc.settings.ip_check_url + timeout = min(12.0, svc.settings.validation_timeout_seconds) + ok_count = [0] + + def on_result(url: str, ok: bool) -> None: + hop_status[url] = "ok" if ok else "fail" + if ok: + ok_count[0] += 1 + + def ui() -> None: + if url in status_dots: + status_dots[url].configure(text_color=GREEN if ok else RED) + + root.after(0, ui) + + def work() -> None: + run_validate_each_sync(manual_chain, check_url, timeout, on_result) + n_ok, n_tot = ok_count[0], len(manual_chain) + + def done() -> None: + test_chain_btn.configure(state="normal") + _log(f"Chain test: {n_ok}/{n_tot} proxies responded OK.") + + root.after(0, done) + + threading.Thread(target=work, daemon=True).start() + + test_chain_btn = _btn(test_row, "▶ Test entire chain", _test_chain, w=150, h=28, + fg_color=ACCENT2, hover_color=ACCENT) + test_chain_btn.pack(side="left", padx=(0, 8)) + + # Add proxy input + add_row = ctk.CTkFrame(cb_right, fg_color="transparent") + add_row.pack(fill="x", padx=12, pady=(0, 4)) + add_entry = ctk.CTkEntry( + add_row, + placeholder_text="http://host:port or user:pass@host:port", + font=(FONT, 10), fg_color=BG, border_color=ACCENT, height=28, + ) + add_entry.pack(side="left", fill="x", expand=True, padx=(0, 4)) + add_user_entry = ctk.CTkEntry( + add_row, width=88, height=28, font=("Consolas", 10), + fg_color=BG, border_color=ACCENT, placeholder_text="user", + ) + add_user_entry.pack(side="left", padx=(0, 2)) + add_pass_entry = ctk.CTkEntry( + add_row, width=88, height=28, font=("Consolas", 10), + fg_color=BG, border_color=ACCENT, placeholder_text="pass", show="*", + ) + add_pass_entry.pack(side="left", padx=(0, 4)) + + def _add_proxy() -> None: + v = add_entry.get().strip() + if not v: + return + if "://" not in v: + v = "http://" + v + au = add_user_entry.get().strip() + ap = add_pass_entry.get().strip() + if au or ap: + v = merge_proxy_credentials(v, au, ap) + else: + v = normalize_proxy_url(v) + manual_chain.append(v) + hop_status[v] = "untested" + _rebuild_chain_ui() + add_entry.delete(0, "end") + add_user_entry.delete(0, "end") + add_pass_entry.delete(0, "end") + + _btn(add_row, "+ Add", _add_proxy, w=68, h=28).pack(side="left") + + chain_btn_row = ctk.CTkFrame(cb_right, fg_color="transparent") + chain_btn_row.pack(pady=(0, 10)) + + def _on_picker_done(urls: list[str], mode: str) -> None: + if mode == "replace": + manual_chain.clear() + hop_status.clear() + for u in urls: + if u not in manual_chain: + manual_chain.append(u) + hop_status[u] = "untested" + _rebuild_chain_ui() + _log(f"{'Replaced' if mode == 'replace' else 'Added'} {len(urls)} proxies to your chain.") + + def _open_picker() -> None: + _save_settings() + show_proxy_picker(root, svc.settings, bool(elite_var.get()), _on_picker_done) + + browse_btn = _btn(chain_btn_row, "Browse proxy lists…", _open_picker, w=150, h=26, + fg_color=ACCENT2, hover_color=ACCENT) + browse_btn.pack(side="left") + + # Sources + src_frame = ctk.CTkFrame(tab_chain, fg_color=CARD, corner_radius=8) + src_frame.pack(fill="x", pady=(0, 6)) + ctk.CTkLabel(src_frame, text="Proxy Sources (one JSON URL per line)", + font=(FONT, 11, "bold"), text_color=TEXT).pack( + anchor="w", padx=12, pady=(8, 4)) + sources_box = ctk.CTkTextbox( + src_frame, height=72, font=("Consolas", 10), + fg_color=BG, text_color=TEXT, scrollbar_button_color=ACCENT, + ) + sources_box.pack(fill="x", padx=12, pady=(0, 8)) + sources_box.insert("end", "\n".join(s.sources)) + + # ─── TAB: BROWSER ───────────────────────────────────────────────────────── + br_scroll = ctk.CTkScrollableFrame(tab_browser, fg_color=BG, scrollbar_button_color=ACCENT) + br_scroll.pack(fill="both", expand=True, padx=4, pady=4) + + def _br_section(title: str, subtitle: str = "") -> ctk.CTkFrame: + ctk.CTkLabel(br_scroll, text=title, font=(FONT, 13, "bold"), text_color=CYAN).pack( + anchor="w", padx=8, pady=(12, 0) + ) + if subtitle: + ctk.CTkLabel( + br_scroll, text=subtitle, font=(FONT, 10), text_color=TEXT2, wraplength=900, justify="left" + ).pack(anchor="w", padx=8, pady=(0, 4)) + f = ctk.CTkFrame( + br_scroll, fg_color=CARD, corner_radius=10, + border_width=1, border_color=GLOW, + ) + f.pack(fill="x", padx=8, pady=(4, 8)) + return f + + browser_status_card = _br_section( + "Hardened Firefox", + "Launches Firefox in an app-controlled profile that enforces proxy/hardening prefs on every launch.", + ) + browser_status_lbl = ctk.CTkLabel( + browser_status_card, text="Browser status: stopped", font=(FONT, 12, "bold"), text_color=TEXT2 + ) + browser_status_lbl.pack(anchor="w", padx=12, pady=(10, 2)) + browser_hint_lbl = ctk.CTkLabel( + browser_status_card, text="Exit IP on hover in tray while chain is healthy.", font=(FONT, 10), text_color=TEXT2 + ) + browser_hint_lbl.pack(anchor="w", padx=12, pady=(0, 10)) + + browser_path_card = _br_section("Executable and profile", "Set your Firefox path once; profile is kept isolated.") + path_row = ctk.CTkFrame(browser_path_card, fg_color="transparent") + path_row.pack(fill="x", padx=12, pady=(10, 4)) + ctk.CTkLabel(path_row, text="Firefox exe", width=90, anchor="w", font=(FONT, 10), text_color=TEXT2).pack(side="left") + firefox_path_var = ctk.StringVar(value=s.firefox_path or default_firefox_path()) + firefox_path_entry = ctk.CTkEntry(path_row, textvariable=firefox_path_var, height=28, fg_color=BG, border_color=ACCENT) + firefox_path_entry.pack(side="left", fill="x", expand=True, padx=(0, 6)) + + def _pick_firefox_path() -> None: + p = filedialog.askopenfilename( + title="Select firefox.exe", + filetypes=[("Firefox", "firefox.exe"), ("Executable", "*.exe"), ("All files", "*.*")], + ) + if p: + firefox_path_var.set(p) + + _btn(path_row, "Browse", _pick_firefox_path, w=72, h=28, fg_color=DIM, hover_color=ACCENT).pack(side="left") + + profile_row = ctk.CTkFrame(browser_path_card, fg_color="transparent") + profile_row.pack(fill="x", padx=12, pady=(0, 10)) + ctk.CTkLabel(profile_row, text="Profile dir", width=90, anchor="w", font=(FONT, 10), text_color=TEXT2).pack(side="left") + firefox_profile_var = ctk.StringVar(value=s.firefox_profile_dir or default_profile_dir()) + firefox_profile_entry = ctk.CTkEntry( + profile_row, textvariable=firefox_profile_var, height=28, fg_color=BG, border_color=ACCENT + ) + firefox_profile_entry.pack(side="left", fill="x", expand=True) + + browser_toggles_card = _br_section( + "Browser controls", + "These settings are applied each time you launch from this tab.", + ) + persona_card = _section_inline( + br_scroll, + "Identity persona (spoof when possible, block when not)", + "Blend-in personas mimic the most common Windows + Chrome setup so the " + "browser looks normal. Hardened mode is the classic 'block everything' " + "profile but stands out as anti-fingerprint.", + ) if "_section_inline" in dir() else None + if persona_card is None: + persona_card = ctk.CTkFrame( + br_scroll, fg_color=CARD, corner_radius=10, + border_width=1, border_color=GLOW, + ) + persona_card.pack(fill="x", padx=8, pady=(2, 8)) + ctk.CTkLabel( + persona_card, + text="Identity persona (spoof when possible, block when not)", + font=(FONT, 12, "bold"), + text_color=ORANGE, + ).pack(anchor="w", padx=12, pady=(10, 0)) + ctk.CTkLabel( + persona_card, + text=( + "Blend-in personas mimic the most common Windows + Chrome setup " + "so the browser looks normal. Hardened = full anti-fingerprint." + ), + font=(FONT, 9), text_color=TEXT2, justify="left", wraplength=820, + ).pack(anchor="w", padx=12, pady=(0, 6)) + + persona_row = ctk.CTkFrame(persona_card, fg_color="transparent") + persona_row.pack(fill="x", padx=12, pady=(0, 6)) + ctk.CTkLabel(persona_row, text="Persona:", font=(FONT, 11), text_color=TEXT, + width=80, anchor="w").pack(side="left") + persona_var = ctk.StringVar( + value=PERSONA_LABELS.get(s.browser_persona, PERSONA_LABELS["blend_windows_chrome"]) + ) + persona_menu = ctk.CTkOptionMenu( + persona_row, + values=[PERSONA_LABELS[k] for k in PERSONAS], + variable=persona_var, + fg_color=BG, button_color=ACCENT2, button_hover_color=ACCENT, + text_color=TEXT, font=(FONT, 11), width=500, + ) + persona_menu.pack(side="left", padx=(4, 0)) + + cookie_row = ctk.CTkFrame(persona_card, fg_color="transparent") + cookie_row.pack(fill="x", padx=12, pady=(0, 4)) + ctk.CTkLabel(cookie_row, text="Cookies:", font=(FONT, 11), text_color=TEXT, + width=80, anchor="w").pack(side="left") + _default_cookie_label = COOKIE_LABELS.get( + s.browser_cookie_mode, COOKIE_LABELS["block_third_party"] + ) + cookie_var = ctk.StringVar(value=_default_cookie_label) + + # Cookie detail sub-labels (key → short description of side effects) + _COOKIE_NOTES: dict[str, str] = { + "accept_all": "Everything works · cookies persist to disk", + "accept_all_session": "Full compat while browsing · cookies wiped on close", + "block_third_party": "Blocks cross-site trackers · most sites work · cookies persist", + "block_third_party_session": "Blocks 3rd-party + nothing persists after close", + "block_third_party_trackers":"ETP Strict — same as Firefox enhanced tracking protection", + "block_social_trackers": "Blocks Facebook pixel, Twitter widgets, social SSO badges", + "block_unvisited": "Very light — only blocks sites you haven't visited", + "partitioned_tcp": "Total Cookie Protection — each site gets isolated 3rd-party jar", + "partitioned_session": "Isolated jars + full disk wipe on close · zero cross-site state", + "session_only": "All cookies expire on close · 1st-party allowed while browsing", + "ghost_mode": "Isolated + session + wipes cache/history/localStorage/formdata", + "block_all": "Breaks most logins · read-only / scraping use only", + } + + cookie_note_lbl = ctk.CTkLabel( + persona_card, text=_COOKIE_NOTES.get(s.browser_cookie_mode, ""), + font=(FONT, 9), text_color=TEXT2, anchor="w", wraplength=820, + ) + + def _on_cookie_change(_choice: str = "") -> None: + key = _cookie_key() + cookie_note_lbl.configure(text=_COOKIE_NOTES.get(key, "")) + + cookie_menu = ctk.CTkOptionMenu( + cookie_row, + values=[COOKIE_LABELS[k] for k in COOKIE_MODES], + variable=cookie_var, + fg_color=BG, button_color=ACCENT2, button_hover_color=ACCENT, + text_color=TEXT, font=(FONT, 11), width=680, + command=_on_cookie_change, + ) + 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: + return _PERSONA_LABEL_TO_KEY.get(persona_var.get(), "blend_windows_chrome") + + def _cookie_key() -> str: + 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) + br_rfp_var = ctk.BooleanVar(value=s.browser_resist_fingerprinting) + br_telemetry_var = ctk.BooleanVar(value=s.browser_disable_telemetry) + br_fpi_var = ctk.BooleanVar(value=s.browser_first_party_isolation) + br_strict_tp_var = ctk.BooleanVar(value=s.browser_strict_tracking_protection) + br_tz_utc_var = ctk.BooleanVar(value=s.browser_timezone_utc) + br_clear_var = ctk.BooleanVar(value=s.browser_clear_on_close) + br_disposable_var = ctk.BooleanVar(value=s.browser_disposable_profile) + br_kill_drop_var = ctk.BooleanVar(value=s.browser_kill_on_chain_drop) + br_auto_relaunch_var = ctk.BooleanVar(value=s.browser_auto_relaunch) + br_lock_profile_var = ctk.BooleanVar(value=s.browser_lock_managed_profile) + + def _br_toggle(parent: Any, text: str, var: ctk.BooleanVar, tip: str = "") -> ctk.CTkCheckBox: + cb = ctk.CTkCheckBox( + parent, text=text, variable=var, font=(FONT, 11), + fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT, + ) + cb.pack(anchor="w", padx=12, pady=5) + return cb + + _br_toggle( + browser_toggles_card, "Force browser proxy to local chain", br_force_proxy_var, + "Forces Firefox traffic through Proxy God listener. Prevents direct bypass.", + ) + _br_toggle( + browser_toggles_card, "Disable WebRTC peer connections", br_disable_webrtc_var, + "Stops common WebRTC local-IP leaks in browser calls.", + ) + _br_toggle( + browser_toggles_card, "Enable Resist Fingerprinting", br_rfp_var, + "Turns on Firefox anti-fingerprinting mode (slightly changes site behavior).", + ) + _br_toggle( + browser_toggles_card, "Disable telemetry and studies", br_telemetry_var, + "Disables browser diagnostics/study pings.", + ) + _br_toggle( + browser_toggles_card, "Enable First-Party Isolation", br_fpi_var, + "Separates site storage per domain to reduce cross-site tracking.", + ) + _br_toggle( + browser_toggles_card, "Strict tracking protection", br_strict_tp_var, + "Uses stronger tracker/cookie blocking in Firefox.", + ) + _br_toggle( + browser_toggles_card, "Timezone hardening (UTC-friendly timer precision)", br_tz_utc_var, + "Reduces timing precision used in fingerprinting.", + ) + _br_toggle( + browser_toggles_card, "Clear cookies/cache/history on close", br_clear_var, + "Wipes session traces from the managed profile when browser closes.", + ) + _br_toggle( + browser_toggles_card, "Disposable profile (delete on close)", br_disposable_var, + "Deletes the entire managed profile folder on stop/close.", + ) + _br_toggle( + browser_toggles_card, "Kill browser if chain drops", br_kill_drop_var, + "Stops browser immediately if chain becomes unhealthy.", + ) + _br_toggle( + browser_toggles_card, "Auto-relaunch browser while chain is healthy", br_auto_relaunch_var, + "If browser exits unexpectedly, relaunch it automatically while chain stays up.", + ) + _br_toggle( + browser_toggles_card, "Lock to managed profile (recommended)", br_lock_profile_var, + "Launches Firefox only with Proxy God profile and re-applies settings each launch.", + ) + + browser_action_row = ctk.CTkFrame(br_scroll, fg_color="transparent") + browser_action_row.pack(fill="x", padx=8, pady=(2, 14)) + + def _browser_cfg() -> BrowserConfig: + return BrowserConfig( + firefox_path=firefox_path_var.get().strip(), + profile_dir=firefox_profile_var.get().strip(), + clear_on_close=bool(br_clear_var.get()), + disposable_profile=bool(br_disposable_var.get()), + kill_on_chain_drop=bool(br_kill_drop_var.get()), + auto_relaunch=bool(br_auto_relaunch_var.get()), + lock_managed_profile=bool(br_lock_profile_var.get()), + force_proxy=bool(br_force_proxy_var.get()), + disable_webrtc=bool(br_disable_webrtc_var.get()), + resist_fingerprinting=bool(br_rfp_var.get()), + disable_telemetry=bool(br_telemetry_var.get()), + first_party_isolation=bool(br_fpi_var.get()), + strict_tracking_protection=bool(br_strict_tp_var.get()), + timezone_utc=bool(br_tz_utc_var.get()), + persona=_persona_key(), + cookie_mode=_cookie_key(), + ) + + def _refresh_browser_status() -> None: + if browser.is_running(): + browser_status_lbl.configure( + text=f"Browser status: running (pid {browser.pid()})", + text_color=GREEN, + ) + else: + if browser_should_run[0]: + browser_status_lbl.configure(text="Browser status: waiting/restarting", text_color=YELLOW) + else: + browser_status_lbl.configure(text="Browser status: stopped", text_color=TEXT2) + + def _launch_browser_with_cfg(cfg: BrowserConfig) -> bool: + if not svc.current_chain: + _log("Chain not ready yet — starting chain and launching browser now.") + _start() + ok, msg = browser.launch(cfg, LISTEN_HOST, svc.settings.local_port) + _log(msg) + _refresh_browser_status() + return bool(ok) + + def _launch_browser() -> None: + _save_settings() + cfg = _browser_cfg() + browser_should_run[0] = True + browser_relaunch_count[0] = 0 # user-initiated launch resets the cap + if _launch_browser_with_cfg(cfg): + browser_last_launch_ts[0] = time.monotonic() + + def _stop_browser() -> None: + cfg = _browser_cfg() + browser_should_run[0] = False + browser_relaunch_count[0] = 0 + ok, msg = browser.stop(dispose=cfg.disposable_profile) + _log(msg) + _refresh_browser_status() + if svc.sticky_remaining() > 0: + svc.set_sticky(0) + _log("Sticky exit released (browser stopped).") + + launch_browser_btn = _btn( + browser_action_row, "Launch Hardened Firefox", _launch_browser, w=190, h=34, font=(FONT, 12) + ) + launch_browser_btn.pack(side="left", padx=(0, 8)) + stop_browser_btn = _btn( + browser_action_row, "Stop Browser", _stop_browser, w=110, h=34, + fg_color="#7f1d1d", hover_color="#991b1b", + ) + stop_browser_btn.pack(side="left") + + # ─── TAB: BAN TESTER ─────────────────────────────────────────────────────── + ban_scroll = ctk.CTkScrollableFrame(tab_ban, fg_color=BG, scrollbar_button_color=ACCENT) + ban_scroll.pack(fill="both", expand=True, padx=4, pady=4) + + ban_head = ctk.CTkFrame(ban_scroll, fg_color=CARD, corner_radius=10, border_width=1, border_color=GLOW) + ban_head.pack(fill="x", padx=8, pady=(6, 8)) + ctk.CTkLabel( + ban_head, text="Ban / Reachability Tester", + font=(FONT, 13, "bold"), text_color=CYAN, + ).pack(anchor="w", padx=12, pady=(10, 2)) + ctk.CTkLabel( + ban_head, + text=( + "Tests social, shopping, crypto exchanges, and DNS providers through your ACTIVE chain. " + "Detects exit-IP bans and geo-blocks." + ), + font=(FONT, 10), text_color=TEXT2, wraplength=880, justify="left", + ).pack(anchor="w", padx=12, pady=(0, 6)) + + # Category selector + ban_cat_row = ctk.CTkFrame(ban_head, fg_color="transparent") + ban_cat_row.pack(fill="x", padx=12, pady=(0, 6)) + ctk.CTkLabel(ban_cat_row, text="Category:", font=(FONT, 10), text_color=TEXT2, width=70, + anchor="w").pack(side="left") + _ban_cat_keys = list(SITE_CATEGORIES.keys()) # "Social / General", "Shopping", etc. + ban_cat_var = ctk.StringVar(value="All") + ban_cat_menu = ctk.CTkOptionMenu( + ban_cat_row, values=_ban_cat_keys, variable=ban_cat_var, + fg_color=BG, button_color=ACCENT2, button_hover_color=ACCENT, + text_color=TEXT, width=200, + ) + ban_cat_menu.pack(side="left", padx=(0, 8)) + + ban_status_var = ctk.StringVar(value="Status: idle") + ctk.CTkLabel(ban_head, textvariable=ban_status_var, font=(FONT, 11, "bold"), text_color=TEXT).pack( + anchor="w", padx=12, pady=(0, 6) + ) + + ban_results_frame = ctk.CTkFrame( + ban_scroll, fg_color=CARD, corner_radius=10, border_width=1, border_color=GLOW, + ) + ban_results_frame.pack(fill="both", expand=True, padx=8, pady=(0, 8)) + + ban_rows_wrap = ctk.CTkFrame(ban_results_frame, fg_color="transparent") + ban_rows_wrap.pack(fill="both", expand=True, padx=8, pady=8) + + def _ban_row(site: str, status: str, detail: str, color: str, cat_header: str = "") -> None: + if cat_header: + ctk.CTkLabel( + ban_rows_wrap, text=f" {cat_header}", + font=(FONT, 10, "bold"), text_color=CYAN, anchor="w", + ).pack(fill="x", padx=4, pady=(6, 1)) + row = ctk.CTkFrame(ban_rows_wrap, fg_color=PANEL, corner_radius=6, height=30) + row.pack(fill="x", pady=2) + row.pack_propagate(False) + ctk.CTkLabel(row, text=site, font=(FONT, 11, "bold"), text_color=TEXT, + width=160, anchor="w").pack(side="left", padx=(8, 4)) + ctk.CTkLabel(row, text=status, font=(FONT, 10, "bold"), text_color=color, + width=72).pack(side="left", padx=4) + ctk.CTkLabel(row, text=detail, font=("Consolas", 10), text_color=TEXT2, + anchor="w").pack(side="left", fill="x", expand=True, padx=(6, 8)) + + def _render_ban_results(results: list[BanTestResult]) -> None: + for child in list(ban_rows_wrap.winfo_children()): + child.destroy() + if not results: + ctk.CTkLabel(ban_rows_wrap, text="No results yet.", font=(FONT, 11), + text_color=TEXT2).pack(anchor="w", padx=8, pady=8) + return + seen_cats: set[str] = set() + for r in results: + cat = r.category or "" + header = cat if cat and cat not in seen_cats else "" + if cat: + seen_cats.add(cat) + if r.status == "ok": + st, color = "OK", GREEN + elif r.status == "banned": + st, color = "BANNED", ORANGE + else: + st, color = "ERROR", RED + _ban_row(r.site, st, r.detail, color, cat_header=header) + + _render_ban_results([]) + + ban_action = ctk.CTkFrame(ban_head, fg_color="transparent") + ban_action.pack(fill="x", padx=12, pady=(0, 10)) + + def _start_ban_test() -> None: + if not service_running[0] or not svc.current_chain: + ban_status_var.set("Status: chain not running") + _log("Ban tester: start the chain first, then run tests.") + return + proxy_url = f"http://{svc.settings.listen_addr()}" + chosen_cat = ban_cat_var.get() + # "All" means every category + cats = None if chosen_cat == "All" else [chosen_cat] + ban_status_var.set("Status: running…") + ban_start_btn.configure(state="disabled") + _render_ban_results([]) + + def work() -> None: + timeout = min(18.0, max(8.0, float(svc.settings.validation_timeout_seconds) + 3.0)) + results = run_ban_tests(proxy_url, timeout_seconds=timeout, categories=cats) + ok_n = sum(1 for r in results if r.status == "ok") + banned_n = sum(1 for r in results if r.status == "banned") + err_n = len(results) - ok_n - banned_n + + def done() -> None: + _render_ban_results(results) + ban_status_var.set(f"Status: done — ok={ok_n} banned={banned_n} errors={err_n}") + _log(f"Ban tester done via {proxy_url} — ok={ok_n}, banned={banned_n}, errors={err_n}.") + ban_start_btn.configure(state="normal") + + root.after(0, done) + + threading.Thread(target=work, daemon=True).start() + + ban_start_btn = _btn( + ban_action, "Start test", _start_ban_test, + w=120, h=30, fg_color=ACCENT2, hover_color=ACCENT, + ) + ban_start_btn.pack(side="left") + + # ─── TAB: SIGNUP PREP ────────────────────────────────────────────────────── + signup_scroll = ctk.CTkScrollableFrame(tab_signup, fg_color=BG, scrollbar_button_color=ACCENT) + signup_scroll.pack(fill="both", expand=True, padx=4, pady=4) + + signup_draft = load_draft() + + def _signup_card(title: str, subtitle: str = "") -> ctk.CTkFrame: + ctk.CTkLabel(signup_scroll, text=title, font=(FONT, 13, "bold"), text_color=CYAN).pack( + anchor="w", padx=8, pady=(12, 0) + ) + if subtitle: + ctk.CTkLabel( + signup_scroll, text=subtitle, font=(FONT, 10), text_color=TEXT2, + wraplength=900, justify="left", + ).pack(anchor="w", padx=8, pady=(0, 4)) + f = ctk.CTkFrame(signup_scroll, fg_color=CARD, corner_radius=10, border_width=1, border_color=GLOW) + f.pack(fill="x", padx=8, pady=(4, 8)) + return f + + signup_head = _signup_card( + "Signup autofill (manual submit)", + "Opens signup pages through your chain and autofills your fields. You solve captchas and click submit.", + ) + + site_row = ctk.CTkFrame(signup_head, fg_color="transparent") + site_row.pack(fill="x", padx=12, pady=(10, 4)) + ctk.CTkLabel(site_row, text="Site", width=70, anchor="w", font=(FONT, 10), text_color=TEXT2).pack(side="left") + signup_site_keys = list(SIGNUP_PRESET_KEYS) + ["custom"] + signup_site_labels = [ + (SIGNUP_PRESETS[k].label if k in SIGNUP_PRESETS else "Custom URL") + for k in signup_site_keys + ] + signup_site_var = ctk.StringVar( + value=next( + (signup_site_labels[i] for i, k in enumerate(signup_site_keys) if k == signup_draft.site_key), + signup_site_labels[0], + ) + ) + custom_url_row = ctk.CTkFrame(signup_head, fg_color="transparent") + custom_url_row.pack(fill="x", padx=12, pady=(0, 4)) + ctk.CTkLabel(custom_url_row, text="URL", width=70, anchor="w", font=(FONT, 10), text_color=TEXT2).pack(side="left") + signup_custom_url = ctk.CTkEntry(custom_url_row, height=28, fg_color=BG, border_color=ACCENT) + signup_custom_url.pack(side="left", fill="x", expand=True) + signup_custom_url.insert(0, signup_draft.custom_url or "") + + def _signup_site_key() -> str: + label = signup_site_var.get() + for i, lbl in enumerate(signup_site_labels): + if lbl == label: + return signup_site_keys[i] + return signup_site_keys[0] + + def _on_signup_site_change(_choice: str = "") -> None: + key = _signup_site_key() + preset = SIGNUP_PRESETS.get(key) + if preset and preset.url: + signup_custom_url.delete(0, "end") + signup_custom_url.insert(0, preset.url) + + signup_site_menu = ctk.CTkOptionMenu( + site_row, values=signup_site_labels, variable=signup_site_var, + fg_color=BG, button_color=ACCENT2, button_hover_color=ACCENT, text_color=TEXT, width=260, + command=_on_signup_site_change, + ) + signup_site_menu.pack(side="left", padx=(0, 8)) + + # Populate URL field for the initially-selected site + _on_signup_site_change() + + def _signup_field(parent: Any, label: str, show: str = "") -> ctk.CTkEntry: + row = ctk.CTkFrame(parent, fg_color="transparent") + row.pack(fill="x", padx=12, pady=3) + ctk.CTkLabel(row, text=label, width=70, anchor="w", font=(FONT, 10), text_color=TEXT2).pack(side="left") + e = ctk.CTkEntry(row, height=28, fg_color=BG, border_color=ACCENT, show=show) + e.pack(side="left", fill="x", expand=True) + return e + + signup_email = _signup_field(signup_head, "Email") + signup_email.insert(0, signup_draft.email or "") + signup_pass = _signup_field(signup_head, "Password", show="*") + signup_pass.insert(0, signup_draft.password or "") + signup_user = _signup_field(signup_head, "Username") + signup_user.insert(0, signup_draft.username or "") + signup_fname = _signup_field(signup_head, "First name") + signup_fname.insert(0, signup_draft.first_name or "") + signup_lname = _signup_field(signup_head, "Last name") + signup_lname.insert(0, signup_draft.last_name or "") + signup_notes = _signup_field(signup_head, "Notes") + signup_notes.insert(0, signup_draft.notes or "") + + def _clipboard_copy(text: str, label: str) -> None: + t = (text or "").strip() + if not t: + _log(f"Signup prep: nothing to copy for {label}.") + return + root.clipboard_clear() + root.clipboard_append(t) + root.update() + _log(f"Copied {label} to clipboard.") + + def _collect_signup_draft() -> SignupDraft: + return SignupDraft( + site_key=_signup_site_key(), + custom_url=signup_custom_url.get().strip(), + email=signup_email.get().strip(), + password=signup_pass.get().strip(), + username=signup_user.get().strip(), + first_name=signup_fname.get().strip(), + last_name=signup_lname.get().strip(), + notes=signup_notes.get().strip(), + ) + + def _persist_signup_draft() -> SignupDraft: + d = _collect_signup_draft() + save_draft(d) + return d + + signup_btn_row = ctk.CTkFrame(signup_head, fg_color="transparent") + signup_btn_row.pack(fill="x", padx=12, pady=(8, 6)) + + def _gen_signup_password() -> None: + pw = generate_password() + signup_pass.delete(0, "end") + signup_pass.insert(0, pw) + _persist_signup_draft() + _log("Generated signup password.") + + def _gen_signup_names() -> None: + f, l = random_full_name() + signup_fname.delete(0, "end"); signup_fname.insert(0, f) + signup_lname.delete(0, "end"); signup_lname.insert(0, l) + _persist_signup_draft() + _log(f"Generated name: {f} {l}") + + def _gen_signup_username() -> None: + f = signup_fname.get().strip() or random_first_name() + l = signup_lname.get().strip() or random_last_name() + u = random_username(f, l) + signup_user.delete(0, "end"); signup_user.insert(0, u) + _persist_signup_draft() + _log(f"Generated username: {u}") + + def _gen_signup_everything() -> None: + f, l = random_full_name() + signup_fname.delete(0, "end"); signup_fname.insert(0, f) + signup_lname.delete(0, "end"); signup_lname.insert(0, l) + signup_user.delete(0, "end"); signup_user.insert(0, random_username(f, l)) + signup_pass.delete(0, "end"); signup_pass.insert(0, generate_password()) + _persist_signup_draft() + _log(f"Generated identity: {f} {l}") + + _btn(signup_btn_row, "Gen password", _gen_signup_password, w=100, h=28, + fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6)) + _btn(signup_btn_row, "Gen name", _gen_signup_names, w=80, h=28, + fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6)) + _btn(signup_btn_row, "Gen username", _gen_signup_username, w=100, h=28, + fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6)) + _btn(signup_btn_row, "Gen ALL", _gen_signup_everything, w=80, h=28, + fg_color=ACCENT2, hover_color=ACCENT).pack(side="left", padx=(0, 12)) + _btn(signup_btn_row, "Copy email", lambda: _clipboard_copy(signup_email.get(), "email"), + w=88, h=28, fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6)) + _btn(signup_btn_row, "Copy pass", lambda: _clipboard_copy(signup_pass.get(), "password"), + w=88, h=28, fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6)) + + # Sticky exit + paranoid mode toggles + signup_opts_row = ctk.CTkFrame(signup_head, fg_color="transparent") + signup_opts_row.pack(fill="x", padx=12, pady=(0, 6)) + sticky_exit_var = ctk.BooleanVar(value=True) + paranoid_signup_var = ctk.BooleanVar(value=False) + ctk.CTkCheckBox( + signup_opts_row, text="Sticky exit (hold final hop while signup is open)", + variable=sticky_exit_var, font=(FONT, 10), + fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT, + ).pack(side="left", padx=(0, 12)) + ctk.CTkCheckBox( + signup_opts_row, text="Paranoid mode (disposable profile + DNS flush)", + variable=paranoid_signup_var, font=(FONT, 10), + fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT, + ).pack(side="left") + + # ── Pre-flight checklist ───────────────────────────────────────────────── + preflight_card = _signup_card( + "Pre-flight check", + "Exhaustive readiness check: chain, exit IP, geo/ASN, DNS leak, WebRTC, " + "IPv6, HTTPS tunnel, target site reachability, and direct-IP comparison.", + ) + pf_rows: dict[str, dict[str, ctk.StringVar | ctk.CTkLabel]] = {} + + _PF_CHECKS = [ + ("chain", "Chain running"), + ("exit_ip", "Exit IP (via chain)"), + ("direct_ip", "Direct IP (no chain)"), + ("leak_compare","IP leak check"), + ("intel", "Geo / ASN intel"), + ("datacenter", "Residential ASN"), + ("dns", "DNS routing"), + ("ipv6", "IPv6 binding"), + ("webrtc", "WebRTC policy"), + ("https_tunnel","HTTPS tunnel (CONNECT)"), + ("target", "Target site reachable"), + ("firefox", "Firefox path"), + ] + + def _pf_row(key: str, label: str) -> None: + row = ctk.CTkFrame(preflight_card, fg_color=PANEL, corner_radius=6) + row.pack(fill="x", padx=12, pady=2) + dot = ctk.CTkLabel(row, text="●", font=(FONT, 14), text_color=DIM, width=18) + dot.pack(side="left", padx=(8, 4)) + ctk.CTkLabel(row, text=label, font=(FONT, 11, "bold"), text_color=TEXT, + width=180, anchor="w").pack(side="left") + var = ctk.StringVar(value="not run") + ctk.CTkLabel(row, textvariable=var, font=(FONT, 10), text_color=TEXT2, + anchor="w").pack(side="left", fill="x", expand=True, padx=(6, 8), pady=4) + pf_rows[key] = {"var": var, "dot": dot} + + for _k, _lbl in _PF_CHECKS: + _pf_row(_k, _lbl) + + def _pf_set(key: str, status: str, detail: str) -> None: + r = pf_rows.get(key) + if not r: + return + color = {"ok": GREEN, "warn": ORANGE, "fail": RED, "running": YELLOW}.get(status, DIM) + r["dot"].configure(text_color=color) # type: ignore[union-attr] + r["var"].set(detail) # type: ignore[union-attr] + + pf_summary_var = ctk.StringVar(value="Not run.") + ctk.CTkLabel(preflight_card, textvariable=pf_summary_var, font=(FONT, 10, "bold"), + text_color=TEXT).pack(anchor="w", padx=12, pady=(6, 8)) + + pf_running = [False] + + def _run_preflight() -> None: + if pf_running[0]: + return + draft = _persist_signup_draft() + url = resolve_signup_url(draft) + if not url: + pf_summary_var.set("Pick a site or enter a custom URL first.") + return + pf_running[0] = True + for k, _ in _PF_CHECKS: + _pf_set(k, "running", "checking…") + pf_summary_var.set("Running pre-flight…") + + # Immediate sync checks ────────────────────────────────────────────── + chain_ok = service_running[0] and bool(svc.current_chain) + if chain_ok: + _pf_set("chain", "ok", f"{len(svc.current_chain)} hop(s) active") + else: + _pf_set("chain", "fail", "chain not running — start first") + + # Firefox path check + ff_path = firefox_path_var.get().strip() or default_firefox_path() + from pathlib import Path as _Path + if ff_path and _Path(ff_path).is_file(): + _pf_set("firefox", "ok", ff_path) + else: + _pf_set("firefox", "fail", f"not found: {ff_path or '(no path set)'}") + + proxy_url = f"http://{svc.settings.listen_addr()}" + target_host = urlparse(url).hostname or "(unknown)" + + 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) ────────────────────────────────── + try: + direct_ip = _run_async(_get_direct_ip(svc.settings.ip_check_url, timeout / 2)) + except Exception: + direct_ip = None + + def _set_direct(ip: str | None) -> None: + if ip: + _pf_set("direct_ip", "ok", ip) + else: + _pf_set("direct_ip", "warn", "could not reach check URL directly") + + root.after(0, lambda ip=direct_ip: _set_direct(ip)) + + # ── 2. Exit IP via chain ───────────────────────────────────────── + if chain_ok: + intel = fetch_exit_intel(proxy_url, timeout_seconds=timeout) + exit_ip_str = intel.ip if intel.ok else (v_exit_ip.get() or "").strip() + + def _set_exit(ip_: str, intel_: Any) -> None: + if ip_ and ip_ not in ("—", ""): + _pf_set("exit_ip", "ok", ip_) + else: + _pf_set("exit_ip", "warn", "no exit IP yet") + # IP leak compare (same logic as production leak_detect) + if direct_ip and ip_ and ip_ not in ("—", ""): + 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} ✓") + else: + _pf_set("leak_compare", "warn", "cannot compare — missing IPs") + # Geo/ASN + if intel_.ok: + _apply_intel(intel_) + loc = ", ".join([b for b in (intel_.city, intel_.country) if b]) or "—" + asn_str = f"AS{intel_.asn}" if intel_.asn else "" + if intel_.is_proxy_flagged: + _pf_set("intel", "fail", f"proxy-flagged · {intel_.org or intel_.isp}") + warns.append("exit IP is proxy-flagged") + elif intel_.is_datacenter: + _pf_set("intel", "warn", f"datacenter · {loc} {asn_str}") + _pf_set("datacenter", "warn", f"DC ASN — {intel_.org or intel_.isp}") + warns.append("exit IP is datacenter ASN") + else: + _pf_set("intel", "ok", f"{loc} {asn_str}") + _pf_set("datacenter", "ok", f"residential/ISP — {intel_.org or intel_.isp}") + else: + _pf_set("intel", "warn", intel_.detail or "lookup failed") + _pf_set("datacenter", "warn", "intel unavailable") + + root.after(0, lambda i=exit_ip_str, d=intel: _set_exit(i, d)) + else: + for k in ("exit_ip", "leak_compare", "intel", "datacenter"): + root.after(0, lambda k_=k: _pf_set(k_, "fail", "chain not running")) + + # ── 3. DNS routing check ───────────────────────────────────────── + try: + dns_rep = run_dns_leak_test( + proxy_url=proxy_url if chain_ok else None, + timeout=min(10.0, timeout), + ) + def _set_dns(r: Any) -> None: + if r.leaked: + _pf_set("dns", "fail", r.summary[:80]) + warns.append("DNS leak detected") + elif r.has_external_resolver: + _pf_set("dns", "warn", r.summary[:80]) + else: + _pf_set("dns", "ok", r.summary[:80]) + root.after(0, lambda r_=dns_rep: _set_dns(r_)) + except Exception as e: + root.after(0, lambda: _pf_set("dns", "warn", f"check error: {e}")) + + # ── 4. IPv6 binding ────────────────────────────────────────────── + try: + from .leak_audit import _check_ipv6_active + v6_on, v6_msg = _check_ipv6_active() + def _set_v6(on_: bool, msg_: str) -> None: + if on_: + _pf_set("ipv6", "warn", f"IPv6 active: {msg_}") + warns.append("IPv6 enabled (possible leak path)") + else: + _pf_set("ipv6", "ok", msg_) + root.after(0, lambda on=v6_on, msg=v6_msg: _set_v6(on, msg)) + except Exception as e: + root.after(0, lambda: _pf_set("ipv6", "warn", str(e))) + + # ── 5. WebRTC ──────────────────────────────────────────────────── + try: + profile = _Path(firefox_profile_var.get().strip() or default_profile_dir()) + rtc_rep = run_webrtc_check(profile_dir=profile) + def _set_rtc(r: Any) -> None: + if r.any_leak_risk: + issues_short = "; ".join(r.issues[:2])[:70] + _pf_set("webrtc", "warn", issues_short) + else: + _pf_set("webrtc", "ok", "all surfaces protected") + root.after(0, lambda r_=rtc_rep: _set_rtc(r_)) + except Exception as e: + root.after(0, lambda: _pf_set("webrtc", "warn", str(e))) + + # ── 6. HTTPS tunnel probe ───────────────────────────────────────── + if chain_ok: + try: + from .validator import check_https_tunnel + tun_ok, tun_msg = _run_async( + check_https_tunnel(proxy_url, timeout) + ) + def _set_tun(ok_: bool, msg_: str) -> None: + if ok_: + _pf_set("https_tunnel", "ok", msg_) + else: + _pf_set("https_tunnel", "fail", msg_) + warns.append("HTTPS CONNECT tunnel broken") + root.after(0, lambda ok=tun_ok, msg=tun_msg: _set_tun(ok, msg)) + except Exception as e: + root.after(0, lambda: _pf_set("https_tunnel", "warn", str(e))) + else: + root.after(0, lambda: _pf_set("https_tunnel", "fail", "chain not running")) + + # ── 7. Target site reachability ─────────────────────────────────── + if chain_ok: + site_res = test_single_site(proxy_url, target_host, url, timeout_seconds=timeout) + def _set_site(r: Any) -> None: + if r.status == "ok": + _pf_set("target", "ok", f"{target_host} — {r.detail}") + elif r.status == "banned": + _pf_set("target", "fail", f"likely BANNED · {r.detail}") + warns.append("target site likely blocks this exit IP") + else: + _pf_set("target", "warn", r.detail) + root.after(0, lambda r_=site_res: _set_site(r_)) + else: + root.after(0, lambda: _pf_set("target", "fail", "chain not running")) + + # ── Final summary ───────────────────────────────────────────────── + # Schedule _final via root.after(0, ...) so it is enqueued AFTER + # all previously-scheduled root.after(0, ...) callbacks (which + # populate `warns`). A fixed 200 ms delay could fire before those + # callbacks execute on a slow main thread. + def _final() -> None: + pf_running[0] = False + if warns: + pf_summary_var.set(f"⚠ {len(warns)} warning(s): " + " · ".join(warns[:3])) + else: + pf_summary_var.set("✓ All pre-flight checks passed — good to go.") + + 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") + pf_btn_row.pack(fill="x", padx=12, pady=(0, 10)) + _btn(pf_btn_row, "Run pre-flight", _run_preflight, w=140, h=28, + fg_color=ACCENT2, hover_color=ACCENT).pack(side="left") + + signup_status_var = ctk.StringVar(value="Ready — start chain, then Open & Autofill.") + ctk.CTkLabel(signup_head, textvariable=signup_status_var, font=(FONT, 10), text_color=TEXT2).pack( + anchor="w", padx=12, pady=(0, 8) + ) + + signup_action = ctk.CTkFrame(signup_scroll, fg_color="transparent") + signup_action.pack(fill="x", padx=8, pady=(0, 6)) + signup_launch_retries = [0] + + accounts_card = _signup_card("Saved accounts", "Logged locally with exit IP + hop used at save time.") + + accounts_list = ctk.CTkScrollableFrame( + accounts_card, fg_color=BG, height=180, corner_radius=6, scrollbar_button_color=ACCENT, + ) + accounts_list.pack(fill="x", padx=12, pady=(8, 10)) + + def _refresh_accounts_ui() -> None: + for child in list(accounts_list.winfo_children()): + child.destroy() + rows = load_accounts() + if not rows: + ctk.CTkLabel(accounts_list, text="No saved accounts yet.", font=(FONT, 10), + text_color=TEXT2).pack(anchor="w", padx=8, pady=8) + return + for rec in rows[:60]: + row_frame = ctk.CTkFrame(accounts_list, fg_color=PANEL, corner_radius=6) + row_frame.pack(fill="x", pady=2, padx=4) + + info_lbl = ctk.CTkLabel( + row_frame, + text=f"{rec.created_at[:16]} {rec.site} {rec.email or rec.username or '—'} exit={rec.exit_ip or '—'}", + font=("Consolas", 10), text_color=TEXT, anchor="w", + ) + info_lbl.pack(side="left", fill="x", expand=True, padx=(8, 4), pady=4) + + def _copy_email(r: Any = rec) -> None: + v = r.email or r.username or "" + root.clipboard_clear() + root.clipboard_append(v) + _log(f"Copied email/user: {v}") + + def _copy_pass(r: Any = rec) -> None: + root.clipboard_clear() + root.clipboard_append(r.password or "") + _log(f"Copied password for {r.email or r.username or r.site}.") + + ctk.CTkButton( + row_frame, text="Copy Email", width=80, height=22, + font=(FONT, 10), fg_color=ACCENT2, hover_color=ACCENT, + command=_copy_email, + ).pack(side="right", padx=(2, 4), pady=4) + ctk.CTkButton( + row_frame, text="Copy Pass", width=76, height=22, + font=(FONT, 10), fg_color="#1e3a1e", hover_color="#2e5a2e", + text_color=GREEN, + command=_copy_pass, + ).pack(side="right", padx=(2, 2), pady=4) + + def _current_exit_meta() -> tuple[str, str]: + ip = (v_exit_ip.get() or "—").strip() + if ip == "—": + ip = "" + hop = "" + chain = svc.current_chain + if chain: + hop = redact_proxy_url(chain[-1]) + return ip, hop + + def _launch_signup_autofill() -> None: + draft = _persist_signup_draft() + url = resolve_signup_url(draft) + if not url: + signup_status_var.set("Missing signup URL.") + _log("Signup prep: pick a site or enter a custom URL.") + return + if not draft.email and not draft.username: + signup_status_var.set("Enter email or username.") + _log("Signup prep: email or username required for autofill.") + return + if not draft.password: + signup_status_var.set("Enter or generate a password.") + _log("Signup prep: password required for autofill.") + return + + cfg = _browser_cfg() + cfg.lock_managed_profile = True + paranoid = bool(paranoid_signup_var.get()) + if paranoid: + cfg.disposable_profile = True + cfg.clear_on_close = True + cfg.kill_on_chain_drop = True + ok_dns, msg_dns = flush_dns_cache() + _log(f"Paranoid mode: DNS flush — {msg_dns}" if ok_dns else f"Paranoid mode: DNS flush failed — {msg_dns}") + if not service_running[0] or not svc.current_chain: + if signup_launch_retries[0] == 0: + _log("Signup prep: starting chain first…") + _start() + signup_launch_retries[0] += 1 + if signup_launch_retries[0] > 12: + signup_status_var.set("Chain did not start in time.") + signup_launch_retries[0] = 0 + _log("Signup prep: chain not ready — start chain manually.") + return + signup_status_var.set("Waiting for chain…") + root.after(2500, _launch_signup_autofill) + return + signup_launch_retries[0] = 0 + + if bool(sticky_exit_var.get()): + svc.set_sticky(1800) + _log("Sticky exit engaged — auto-rotation suspended for 30 min.") + + ok, msg = browser.launch( + cfg, LISTEN_HOST, svc.settings.local_port, + start_url=url, signup_draft=draft, + ) + _log(msg) + _refresh_browser_status() + if ok: + preset = SIGNUP_PRESETS.get(draft.site_key) + label = preset.label if preset else "Custom" + extras = [] + if bool(sticky_exit_var.get()): + extras.append("sticky") + if paranoid: + extras.append("paranoid") + 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}.") + # 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) + 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=draft.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="created", + created_at=datetime.now().isoformat(timespec="seconds"), + ) + append_account(rec) + _refresh_accounts_ui() + signup_status_var.set(f"Saved account for {site_label}.") + _log(f"Signup prep: saved account {draft.email or draft.username} ({site_label}).") + + def _preflight_then_open() -> None: + _run_preflight() + + def follow_up(attempts: int = 0) -> None: + if pf_running[0] and attempts < 30: + root.after(500, lambda: follow_up(attempts + 1)) + return + _launch_signup_autofill() + + root.after(500, follow_up) + + signup_open_btn = _btn( + signup_action, "Open & Autofill", _launch_signup_autofill, + w=140, h=34, fg_color=ACCENT2, hover_color=ACCENT, font=(FONT, 12), + ) + signup_open_btn.pack(side="left", padx=(0, 8)) + _btn( + signup_action, "Pre-flight + Open", _preflight_then_open, + w=150, h=34, fg_color=ACCENT, hover_color=ACCENT2, + ).pack(side="left", padx=(0, 8)) + _btn( + signup_action, "Save account", _save_signup_account, + w=110, h=34, fg_color=GREEN, hover_color="#00b377", + ).pack(side="left", padx=(0, 8)) + _btn( + signup_action, "Refresh list", _refresh_accounts_ui, + w=100, h=34, fg_color=DIM, hover_color=ACCENT, + ).pack(side="left") + + _refresh_accounts_ui() + + # ─── TAB: PRIVACY ────────────────────────────────────────────────────────── + priv_scroll = ctk.CTkScrollableFrame(tab_privacy, fg_color=BG, + scrollbar_button_color=ACCENT) + priv_scroll.pack(fill="both", expand=True, padx=4, pady=4) + + def _priv_section(title: str, subtitle: str = "") -> ctk.CTkFrame: + ctk.CTkLabel(priv_scroll, text=title, font=(FONT, 13, "bold"), + text_color=CYAN).pack(anchor="w", padx=8, pady=(12, 0)) + if subtitle: + ctk.CTkLabel(priv_scroll, text=subtitle, font=(FONT, 10), + text_color=TEXT2, wraplength=900, justify="left").pack( + anchor="w", padx=8, pady=(0, 4)) + f = ctk.CTkFrame( + priv_scroll, fg_color=CARD, corner_radius=10, + border_width=1, border_color=GLOW, + ) + f.pack(fill="x", padx=8, pady=(4, 8)) + return f + + status_card = _priv_section( + "Network identity", + "VPN detection drives leak checks: strict without VPN, subnet-aware with VPN.", + ) + status_row = ctk.CTkFrame(status_card, fg_color="transparent") + status_row.pack(fill="x", padx=12, pady=10) + vpn_status_lbl = ctk.CTkLabel(status_row, text="VPN: scanning…", + font=(FONT, 12, "bold"), text_color=TEXT) + vpn_status_lbl.pack(anchor="w") + leak_mode_lbl = ctk.CTkLabel(status_row, text="Leak check: —", + font=(FONT, 11), text_color=TEXT2) + leak_mode_lbl.pack(anchor="w", pady=(4, 0)) + + def _refresh_vpn_ui() -> None: + vs = detect_vpn() + nonlocal outer_hop_label + if vs.active: + outer_hop_label = vs.short_label()[:12] + vpn_status_lbl.configure( + text=f"VPN: {vs.label}" + (f" ({vs.adapter})" if vs.adapter else ""), + text_color=GREEN, + ) + leak_mode_lbl.configure(text="Leak check: VPN-aware (/16 subnet)") + else: + outer_hop_label = "Direct" + vpn_status_lbl.configure(text="VPN: not detected (direct connection)", + text_color=YELLOW) + leak_mode_lbl.configure(text="Leak check: strict (exit must differ from your IP)") + + _btn(status_card, "Refresh VPN status", _refresh_vpn_ui, w=140, h=28).pack( + anchor="w", padx=12, pady=(0, 10)) + + toggles_card = _priv_section( + "Protections while chain is running", + "Applied on Start, restored on Stop. Admin required for MAC, hostname, IPv6, and WebRTC.", + ) + mac_var = ctk.BooleanVar(value=s.mac_spoof_enabled) + mac_rotate_var = ctk.BooleanVar(value=s.mac_rotate_on_chain_rotate) + host_var = ctk.BooleanVar(value=s.spoof_hostname_enabled) + dns_flush_var = ctk.BooleanVar(value=s.flush_dns_on_rotate) + ipv6_var = ctk.BooleanVar(value=s.disable_ipv6_while_active) + webrtc_var = ctk.BooleanVar(value=s.harden_webrtc_enabled) + lan_var = ctk.BooleanVar(value=s.lan_lockdown_enabled) + telemetry_var = ctk.BooleanVar(value=s.telemetry_kill_enabled) + + def _priv_toggle(parent: Any, text: str, var: ctk.BooleanVar, tip: str = "") -> ctk.CTkCheckBox: + cb = ctk.CTkCheckBox( + parent, text=text, variable=var, font=(FONT, 11), + fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT, + ) + cb.pack(anchor="w", padx=12, pady=6) + return cb + + _priv_toggle( + toggles_card, "Randomize MAC addresses on physical adapters", mac_var, + "Changes NIC MAC values while chain runs (admin required).", + ) + _priv_toggle( + toggles_card, "Re-randomize MAC on EVERY chain rotation (paranoid)", mac_rotate_var, + "Mutates MAC every time the chain rotates — prevents long-session correlation.", + ) + _priv_toggle( + toggles_card, "Spoof computer / NetBIOS hostname", host_var, + "Temporarily renames machine identity while active (admin required).", + ) + _priv_toggle( + toggles_card, "Flush DNS cache when starting or rotating chains", dns_flush_var, + "Clears cached resolver results to reduce stale direct routes.", + ) + _priv_toggle( + toggles_card, "Disable IPv6 on active adapters", ipv6_var, + "Turns off IPv6 bindings while active to reduce IPv6 leak paths.", + ) + _priv_toggle( + toggles_card, + "Harden WebRTC in Chrome / Edge (block non-proxied UDP)", + webrtc_var, + "Applies Windows policy to block direct WebRTC UDP bypass in Chromium browsers.", + ) + _priv_toggle( + toggles_card, + "LAN lockdown: kill LLMNR / NetBIOS / mDNS hostname broadcasts", + lan_var, + "Stops your machine from advertising its hostname on the local network " + "(admin required, reversible on stop).", + ) + _priv_toggle( + toggles_card, + "Telemetry kill: DiagTrack, Activity History, Cortana web, ad ID", + telemetry_var, + "Stops Windows telemetry pipelines while chain is up. Reversible on stop " + "(snapshot of original state taken).", + ) + + fp_card = _priv_section( + "Device fingerprint", + "OS-level identifiers. Browsers still fingerprint Canvas, fonts, and WebGL separately.", + ) + fp_box = ctk.CTkTextbox(fp_card, height=140, font=("Consolas", 10), + fg_color=BG, text_color=TEXT, + scrollbar_button_color=ACCENT) + fp_box.pack(fill="x", padx=12, pady=(4, 8)) + + def _refresh_fingerprint() -> None: + fp_box.delete("1.0", "end") + audit = audit_device() + fp_box.insert("end", "\n".join(audit.lines)) + + _btn(fp_card, "Refresh fingerprint audit", _refresh_fingerprint, w=180, h=28).pack( + anchor="w", padx=12, pady=(0, 10)) + + # ── DNS leak test ──────────────────────────────────────────────────────── + dns_card = _priv_section( + "DNS leak test", + "Compares how DNS resolves through your chain vs direct. Detects resolver bypass.", + ) + dns_box = ctk.CTkTextbox( + dns_card, height=120, font=("Consolas", 10), + fg_color=BG, text_color=TEXT, scrollbar_button_color=ACCENT, + ) + dns_box.pack(fill="x", padx=12, pady=(4, 4)) + dns_box.insert("end", "Click 'Full DNS leak test' or 'Quick check' to probe.") + dns_status_lbl = ctk.CTkLabel(dns_card, text="", font=(FONT, 10, "bold"), text_color=TEXT2) + dns_status_lbl.pack(anchor="w", padx=12, pady=(0, 2)) + + def _render_dns_report(rep: FullDnsLeakReport) -> None: + dns_box.delete("1.0", "end") + mark = "LEAK" if rep.leaked else "OK " + dns_box.insert("end", f"[{mark}] {rep.summary}\n\n") + if rep.system_resolvers: + dns_box.insert("end", f"Configured resolvers: {', '.join(rep.system_resolvers)}\n") + for host, addrs in rep.direct_resolution.items(): + via_proxy = rep.proxy_resolution.get(host, []) + proxy_str = ", ".join(via_proxy) if via_proxy else "(no proxy answer)" + dns_box.insert("end", f" {host:<22} direct={', '.join(addrs) or '—':<20} via-proxy={proxy_str}\n") + if rep.errors: + dns_box.insert("end", "\nErrors: " + "; ".join(rep.errors[:3])) + color = RED if rep.leaked else GREEN + dns_status_lbl.configure( + text="DNS LEAK DETECTED" if rep.leaked else "DNS looks clean", + text_color=color, + ) + + def _run_full_dns_test() -> None: + proxy = f"http://{svc.settings.listen_addr()}" if svc.current_chain else None + dns_status_lbl.configure(text="Running…", text_color=YELLOW) + dns_box.delete("1.0", "end") + dns_box.insert("end", "Probing DNS via proxy vs direct…\n") + + def work() -> None: + try: + rep = run_dns_leak_test(proxy_url=proxy, + timeout=min(12.0, svc.settings.validation_timeout_seconds)) + except Exception as e: + root.after(0, lambda: dns_status_lbl.configure( + text=f"Error: {e}", text_color=RED)) + return + root.after(0, lambda: _render_dns_report(rep)) + + threading.Thread(target=work, daemon=True).start() + + def _run_quick_dns_check() -> None: + listen = f"http://{svc.settings.listen_addr()}" if svc.current_chain else None + r = check_dns_leak_hint(listen) + color = GREEN if r.ok else RED + dns_box.delete("1.0", "end") + dns_box.insert("end", r.message) + if r.system_resolvers: + dns_box.insert("end", f"\nResolvers: {', '.join(r.system_resolvers)}") + dns_status_lbl.configure(text="clean" if r.ok else "issue detected", text_color=color) + + def _manual_dns_flush() -> None: + ok, msg = flush_dns_cache() + dns_status_lbl.configure(text=msg, text_color=GREEN if ok else RED) + + dns_btn_row = ctk.CTkFrame(dns_card, fg_color="transparent") + dns_btn_row.pack(fill="x", padx=12, pady=(0, 10)) + _btn(dns_btn_row, "Full DNS leak test", _run_full_dns_test, w=140, h=28, + fg_color=ACCENT2, hover_color=ACCENT).pack(side="left", padx=(0, 6)) + _btn(dns_btn_row, "Quick check", _run_quick_dns_check, w=100, h=28).pack(side="left", padx=(0, 6)) + _btn(dns_btn_row, "Flush DNS now", _manual_dns_flush, w=110, h=28, + fg_color=DIM).pack(side="left") + + # ── WebRTC leak test ───────────────────────────────────────────────────── + rtc_card = _priv_section( + "WebRTC leak test", + "Scans Chrome/Edge policy, Firefox profile prefs, and live STUN server reachability.", + ) + rtc_box = ctk.CTkTextbox( + rtc_card, height=110, font=("Consolas", 10), + fg_color=BG, text_color=TEXT, scrollbar_button_color=ACCENT, + ) + rtc_box.pack(fill="x", padx=12, pady=(4, 4)) + rtc_box.insert("end", "Click 'WebRTC leak test' to probe.") + rtc_status_lbl = ctk.CTkLabel(rtc_card, text="", font=(FONT, 10, "bold"), text_color=TEXT2) + rtc_status_lbl.pack(anchor="w", padx=12, pady=(0, 2)) + + def _render_rtc_report(rep: WebRtcCheckResult) -> None: + rtc_box.delete("1.0", "end") + for name, ok, detail in rep.checks: + mark = "OK " if ok else "RISK" + rtc_box.insert("end", f"[{mark}] {name:<40} {detail}\n") + color = RED if rep.any_leak_risk else GREEN + rtc_status_lbl.configure( + text="WebRTC LEAK RISK — see items above" if rep.any_leak_risk else "WebRTC looks protected", + text_color=color, + ) + + def _run_rtc_test() -> None: + rtc_status_lbl.configure(text="Running…", text_color=YELLOW) + rtc_box.delete("1.0", "end") + rtc_box.insert("end", "Scanning WebRTC surfaces…\n") + profile = Path(firefox_profile_var.get().strip() or default_profile_dir()) + + def work() -> None: + try: + rep = run_webrtc_check(profile_dir=profile) + except Exception as e: + root.after(0, lambda: rtc_status_lbl.configure(text=f"Error: {e}", text_color=RED)) + return + root.after(0, lambda: _render_rtc_report(rep)) + + threading.Thread(target=work, daemon=True).start() + + rtc_btn_row = ctk.CTkFrame(rtc_card, fg_color="transparent") + rtc_btn_row.pack(fill="x", padx=12, pady=(0, 10)) + _btn(rtc_btn_row, "WebRTC leak test", _run_rtc_test, w=140, h=28, + fg_color=ACCENT2, hover_color=ACCENT).pack(side="left") + + wipe_card = _priv_section( + "Forensic artifact wipe", + "One-button purge of common Windows breadcrumb trails. Irreversible.", + ) + wipe_result_lbl = ctk.CTkLabel( + wipe_card, text="Click 'Wipe now' to purge %TEMP%, Recent, Jump Lists, " + "Prefetch (Admin), MRU lists, and the clipboard.", + font=(FONT, 10), text_color=TEXT2, wraplength=880, justify="left", + ) + wipe_result_lbl.pack(anchor="w", padx=12, pady=8) + + def _do_wipe() -> None: + wipe_result_lbl.configure(text="Wiping…", text_color=YELLOW) + + def work() -> None: + try: + rep = wipe_artifacts(include_prefetch=True) + except Exception as e: + root.after(0, lambda: wipe_result_lbl.configure( + text=f"Wipe failed: {e}", text_color=RED)) + return + root.after(0, lambda: wipe_result_lbl.configure( + text=rep.summary(), text_color=GREEN if not rep.errors else YELLOW)) + + threading.Thread(target=work, daemon=True).start() + + _btn(wipe_card, "Wipe now", _do_wipe, w=110, h=28, + fg_color=RED, hover_color="#8a1d1d").pack(anchor="w", padx=12, pady=(0, 10)) + + audit_card = _priv_section( + "Leak audit (mission-critical)", + "Probes every leak surface: IP, DNS, IPv6, WPAD, Group Policy, " + "ProxySettingsPerUser, LLMNR / NetBIOS / mDNS, VPN, WebRTC policy.", + ) + audit_box = ctk.CTkTextbox( + audit_card, height=220, font=("Consolas", 10), + fg_color=BG, text_color=TEXT, scrollbar_button_color=ACCENT, + ) + audit_box.pack(fill="x", padx=12, pady=(4, 4)) + audit_box.insert("end", "Click 'Run audit' to probe live leak status.") + audit_status_lbl = ctk.CTkLabel( + audit_card, text="", font=(FONT, 11, "bold"), text_color=TEXT2, + ) + audit_status_lbl.pack(anchor="w", padx=12, pady=(0, 4)) + + def _render_audit(rep: AuditReport) -> None: + audit_box.delete("1.0", "end") + for f in rep.findings: + mark = "OK " if f.ok else "LEAK" + line = f"[{mark}] {f.name:<34} {f.value}" + if f.note: + line += f" — {f.note}" + audit_box.insert("end", line + "\n") + bad = sum(1 for f in rep.findings if not f.ok) + if rep.overall_ok: + audit_status_lbl.configure( + text="CLEAN — no leaks detected on probed surfaces.", text_color=GREEN + ) + else: + audit_status_lbl.configure( + text=f"{bad} leak(s) detected — see report above.", text_color=RED + ) + + def _run_audit_async() -> None: + audit_status_lbl.configure(text="Running audit…", text_color=YELLOW) + audit_box.delete("1.0", "end") + audit_box.insert("end", "Probing every leak surface in parallel…\n") + + def work() -> None: + try: + rep = run_audit_sync( + f"http://{svc.settings.listen_addr()}", + svc.settings.ip_check_url, + timeout_seconds=min(12.0, svc.settings.validation_timeout_seconds), + ) + except Exception as e: + root.after(0, lambda: audit_status_lbl.configure( + text=f"Audit failed: {e}", text_color=RED)) + return + root.after(0, lambda: _render_audit(rep)) + + threading.Thread(target=work, daemon=True).start() + + audit_btn_row = ctk.CTkFrame(audit_card, fg_color="transparent") + audit_btn_row.pack(fill="x", padx=12, pady=(0, 10)) + _btn(audit_btn_row, "Run audit", _run_audit_async, w=120, h=28, + fg_color=ACCENT2, hover_color=ACCENT).pack(side="left", padx=(0, 8)) + + _btn(priv_scroll, "Save privacy settings", lambda: _save_settings(verbose=True), + w=200, h=34, font=(FONT, 12)).pack(anchor="w", padx=8, pady=12) + + # ─── TAB: SETTINGS ─────────────────────────────────────────────────────── + sf_outer = ctk.CTkScrollableFrame(tab_settings, fg_color=BG, + scrollbar_button_color=ACCENT) + sf_outer.pack(fill="both", expand=True) + + def _section(title: str) -> ctk.CTkFrame: + ctk.CTkLabel(sf_outer, text=title, font=(FONT, 12, "bold"), + text_color=CYAN).pack(anchor="w", padx=6, pady=(14, 4)) + f = ctk.CTkFrame(sf_outer, fg_color=CARD, corner_radius=8) + f.pack(fill="x", padx=6, pady=(0, 4)) + return f + + entries: dict[str, ctk.CTkEntry] = {} + + def _fld(parent: Any, label: str, key: str, val: str, tip: str = "") -> None: + row = ctk.CTkFrame(parent, fg_color="transparent") + row.pack(fill="x", padx=12, pady=5) + ctk.CTkLabel(row, text=label, font=(FONT, 11), text_color=TEXT, + width=230, anchor="w").pack(side="left") + e = ctk.CTkEntry(row, height=26, font=(FONT, 11), + fg_color=BG, border_color=ACCENT, width=130) + e.insert(0, val) + e.pack(side="left") + if tip: + ctk.CTkLabel(row, text=f" {tip}", font=(FONT, 10), + text_color=TEXT2).pack(side="left") + entries[key] = e + + net = _section("Network") + _fld(net, "Local listener port", "port", str(s.local_port), "(point apps here)") + _fld(net, "Proxy bypass list", "bypass", s.proxy_bypass, "(semicolons)") + _fld(net, "IP check URL", "check_url", s.ip_check_url) + + timing = _section("Timing") + _fld(timing, "Health check interval (sec)", "health", str(s.health_check_seconds), + "(how often exit IP is re-verified)") + _fld(timing, "Full pool refresh (sec)", "refresh", str(s.full_refresh_seconds), + "(re-fetch + re-validate)") + + val_sec = _section("Validation") + _fld(val_sec, "Concurrent validators", "conc", str(s.validation_concurrency)) + _fld(val_sec, "Stop after N valid proxies found", "minpool", str(s.min_pool_size), + "(early-exit — stop testing once enough found)") + _fld(val_sec, "Max candidates to test per cycle", "maxc", str(s.max_candidates), + "(shuffled random sample from fetch)") + _fld(val_sec, "Per-proxy timeout (sec)", "timeout", str(s.validation_timeout_seconds)) + + sec_sec = _section("Security") + ks_row = ctk.CTkFrame(sec_sec, fg_color="transparent") + ks_row.pack(fill="x", padx=12, pady=8) + ks_var = ctk.BooleanVar(value=s.kill_switch_enabled) + ctk.CTkCheckBox( + ks_row, + text="Firewall kill-switch (block ALL traffic if chain is down — requires Admin)", + variable=ks_var, font=(FONT, 11), + fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT, + ).pack(side="left") + + # Emergency disengage — for crashes where the kill-switch lingers and + # the operator needs internet back fast without restarting the app. + def _emergency_disengage_now() -> None: + from tkinter import messagebox + from .firewall import emergency_disengage as _ed, is_engaged as _ie + if not _ie(): + messagebox.showinfo("Kill-switch", "No Proxy God firewall rules detected — nothing to disengage.") + return + if not messagebox.askyesno( + "Emergency disengage", + "Remove ALL Proxy God firewall rules and restore normal outbound traffic?\n\n" + "Use this only when the chain has dropped and the kill-switch is " + "blocking everything.", + ): + return + _ed() + messagebox.showinfo("Kill-switch", "Firewall rules removed.") + _log("Emergency disengage: firewall rules removed by operator.") + + ks_btn_row = ctk.CTkFrame(sec_sec, fg_color="transparent") + ks_btn_row.pack(fill="x", padx=12, pady=(0, 8)) + _btn( + ks_btn_row, "⚠ Emergency disengage firewall now", + _emergency_disengage_now, + w=320, h=28, + fg_color="#7f1d1d", hover_color="#991b1b", + ).pack(side="left") + + def _apply_point_and_shoot() -> None: + """Simple safe defaults: secure + low-friction launch profile.""" + use_manual_var.set(True) + mode_var.set("auto") + elite_var.set(False) + ks_var.set(True) + dns_flush_var.set(True) + mac_var.set(False) + mac_rotate_var.set(False) + host_var.set(False) + ipv6_var.set(False) + webrtc_var.set(True) + lan_var.set(False) + telemetry_var.set(False) + persona_var.set(PERSONA_LABELS["blend_windows_chrome"]) + cookie_var.set(COOKIE_LABELS["block_third_party"]) + br_force_proxy_var.set(True) + br_disable_webrtc_var.set(True) + br_rfp_var.set(True) + br_telemetry_var.set(True) + br_fpi_var.set(True) + br_strict_tp_var.set(True) + br_tz_utc_var.set(True) + br_clear_var.set(True) + br_disposable_var.set(False) + br_kill_drop_var.set(True) + br_auto_relaunch_var.set(False) + br_lock_profile_var.set(True) + if not firefox_path_var.get().strip(): + firefox_path_var.set(default_firefox_path()) + if not firefox_profile_var.get().strip(): + firefox_profile_var.set(default_profile_dir()) + _save_settings(verbose=False) + _log("⚡ Point-and-shoot preset applied.") + + ready_btn = _btn( + topbar, + "⚡ Ready", + _apply_point_and_shoot, + w=78, + fg_color="#0f766e", + hover_color="#0d9488", + ) + ready_btn.pack(side="left", padx=(8, 2)) + + ctk.CTkFrame(sf_outer, fg_color="transparent", height=4).pack() + _btn(sf_outer, "Save All Settings", + lambda: _save_settings(verbose=True), w=220, h=36, + font=(FONT, 13)).pack(anchor="w", padx=6, pady=8) + + # ───────────────────────────────────────────────────────────────────────── + # SAVE SETTINGS + # ───────────────────────────────────────────────────────────────────────── + def _save_settings(verbose: bool = False) -> None: + try: + src_raw = sources_box.get("1.0", "end").strip() + src_list = [ln.strip() for ln in src_raw.splitlines() + if ln.strip().startswith("http")] + + n_hops = max(1, len(manual_chain)) if manual_chain else 1 + + ns = Settings( + local_host=LISTEN_HOST, + local_port=int(entries["port"].get().strip()), + chain_length=n_hops, + obfuscation_mode=mode_var.get(), + use_pinned_chain=bool(use_manual_var.get()), + pinned_chain=list(manual_chain), + manual_exit_proxy=_exit_url(), + health_check_seconds=min(3600, max(10, int(entries["health"].get().strip()))), + full_refresh_seconds=min(86400, max(60, int(entries["refresh"].get().strip()))), + validation_concurrency=max(1, int(entries["conc"].get().strip())), + min_pool_size=max(3, int(entries["minpool"].get().strip())), + max_candidates=max(10, int(entries["maxc"].get().strip())), + validation_timeout_seconds=min(120.0, max(2.0, float(entries["timeout"].get().strip()))), + prefer_elite=bool(elite_var.get()), + kill_switch_enabled=bool(ks_var.get()), + proxy_bypass=entries["bypass"].get().strip() or Settings().proxy_bypass, + sources=src_list or Settings().sources, + ip_check_url=entries["check_url"].get().strip() or Settings().ip_check_url, + mac_spoof_enabled=bool(mac_var.get()), + mac_rotate_on_chain_rotate=bool(mac_rotate_var.get()), + spoof_hostname_enabled=bool(host_var.get()), + flush_dns_on_rotate=bool(dns_flush_var.get()), + disable_ipv6_while_active=bool(ipv6_var.get()), + harden_webrtc_enabled=bool(webrtc_var.get()), + lan_lockdown_enabled=bool(lan_var.get()), + telemetry_kill_enabled=bool(telemetry_var.get()), + firefox_path=firefox_path_var.get().strip(), + firefox_profile_dir=firefox_profile_var.get().strip(), + browser_clear_on_close=bool(br_clear_var.get()), + browser_disposable_profile=bool(br_disposable_var.get()), + browser_kill_on_chain_drop=bool(br_kill_drop_var.get()), + browser_auto_relaunch=bool(br_auto_relaunch_var.get()), + browser_lock_managed_profile=bool(br_lock_profile_var.get()), + browser_force_proxy=bool(br_force_proxy_var.get()), + browser_disable_webrtc=bool(br_disable_webrtc_var.get()), + browser_resist_fingerprinting=bool(br_rfp_var.get()), + browser_disable_telemetry=bool(br_telemetry_var.get()), + browser_first_party_isolation=bool(br_fpi_var.get()), + browser_strict_tracking_protection=bool(br_strict_tp_var.get()), + browser_timezone_utc=bool(br_tz_utc_var.get()), + browser_persona=_persona_key(), + browser_cookie_mode=_cookie_key(), + ) + if not (1 <= ns.local_port <= 65535): + raise ValueError("Port must be 1-65535") + svc.update_settings(ns) + save_settings(ns) + proxy_lbl.configure(text=f"{LISTEN_HOST}:{ns.local_port}") + if verbose: + _log("✓ Settings saved.") + except Exception as e: + _log(f"⚠ Settings error: {e!s}") + + # ───────────────────────────────────────────────────────────────────────── + # STATUS REFRESHERS + # ───────────────────────────────────────────────────────────────────────── + def _refresh_sysproxy() -> None: + on = is_system_proxy_set() + sys_lbl.configure(text="SYS:ON" if on else "SYS:OFF", + text_color=GREEN if on else DIM) + + def _refresh_fw(engaged: bool | None = None) -> None: + if engaged is None: + engaged = fw_is_engaged() + fw_lbl.configure(text="FW:ON" if engaged else "FW:OFF", + text_color=GREEN if engaged else DIM) + + # ───────────────────────────────────────────────────────────────────────── + # MESSAGE PUMP + # ───────────────────────────────────────────────────────────────────────── + def _handle(m: dict[str, Any]) -> None: + t = m.get("type") + + if t == "log": + _log(str(m.get("text", ""))) + + elif t == "state": + running = bool(m.get("running")) + service_running[0] = running + status_dot.configure(text_color=GREEN if running else RED) + status_txt.configure(text="RUNNING" if running else "STOPPED") + _refresh_sysproxy() + if running: + tray.set_state("yellow", exit_ip=None) + if not running: + prog_bar.set(0) + phase_lbl.configure(text="idle") + countdown_lbl.configure(text="next: —", text_color=TEXT2) + tray.set_state("gray", exit_ip=None) + v_exit_ip.set("—") + v_rotation.set("0") + + elif t == "phase": + ph = str(m.get("phase", "")) + phase_labels = { + "validate": "validate", + "fetch": "fetch", + "running": "running", + "exit_check": "fixed exit", + "verify_chain": "verify chain", + "gost_start": "starting chain", + } + phase_lbl.configure(text=phase_labels.get(ph, ph or "idle")) + if ph == "validate": + prog_bar.configure(progress_color=YELLOW) + elif ph == "fetch": + prog_bar.configure(progress_color=ORANGE) + prog_bar.set(0.15) + elif ph == "exit_check": + prog_bar.configure(progress_color=YELLOW) + prog_bar.set(0.96) + elif ph == "gost_start": + prog_bar.configure(progress_color=GREEN) + prog_bar.set(0.985) + elif ph == "verify_chain": + prog_bar.configure(progress_color=GREEN) + prog_bar.set(0.98) + elif ph == "running": + prog_bar.configure(progress_color=GREEN) + prog_bar.set(1.0) + elif ph in ("idle", ""): + prog_bar.set(0) + + elif t == "validate_progress": + d, tot = int(m.get("done", 0)), max(1, int(m.get("total", 1))) + prog_bar.set(d / tot) + + elif t == "pool": + cnt = int(m.get("count", 0)) + pool_lbl.configure(text=f"pool: {cnt}") + v_pool.set(str(cnt)) + + elif t == "real_ip": + ip = str(m.get("ip", "—")) + your_ip_lbl.configure(text=f"Direct IP: {ip}") + v_real_ip.set(ip) + last_chain_state["direct_ip"] = ip + root.after(300, lambda: _refresh_chain_map(silent=True)) + + elif t == "vpn": + nonlocal outer_hop_label + if m.get("active"): + outer_hop_label = str(m.get("label", "VPN"))[:12] + vpn_status_lbl.configure( + text=f"VPN: {m.get('label', 'Active')}" + + (f" ({m.get('adapter')})" if m.get("adapter") else ""), + text_color=GREEN, + ) + leak_mode_lbl.configure( + text=f"Leak check: {m.get('leak_mode', 'VPN-aware')}", + text_color=TEXT2, + ) + else: + outer_hop_label = "Direct" + vpn_status_lbl.configure( + text="VPN: not detected (direct connection)", text_color=YELLOW, + ) + leak_mode_lbl.configure( + text=f"Leak check: {m.get('leak_mode', 'strict')}", + text_color=TEXT2, + ) + + elif t == "firewall": + _refresh_fw(bool(m.get("engaged"))) + + elif t == "rotation": + n = str(m.get("n", 0)) + rotation_lbl.configure(text=f"rot: {n}") + v_rotation.set(n) + + elif t == "countdown": + secs = int(m.get("secs", 0)) + if secs > 0: + m_s, s_s = divmod(secs, 60) + countdown_lbl.configure( + text=f"next: {m_s}m{s_s:02d}s", + text_color=YELLOW if secs < 30 else TEXT2, + ) + else: + countdown_lbl.configure(text="next: checking…", text_color=YELLOW) + + elif t == "hops": + hops = [str(x) for x in (m.get("hops") or [])] + status = str(m.get("status", "connecting")) + exit_ip = m.get("exit_ip") + exit_s = str(exit_ip) if exit_ip else None + # If the live check hasn't resolved yet, show the explicit final-hop IP + # (when available) so tunnel/fixed-exit setups still expose the egress node. + if not exit_s and hops: + exit_s = _extract_ip_host(hops[-1]) + fix = normalize_proxy_url(svc.settings.manual_exit_proxy) + fixed_last = bool(fix) and not svc.settings.use_pinned_chain + _render_chain(hops, status, exit_s, fixed_last=fixed_last) + _refresh_sysproxy() + tray_color = {"healthy": "green", "dead": "red", "connecting": "yellow"}.get( + status, "yellow" + ) + tray.set_state(tray_color, exit_ip=exit_s) + prev_exit = v_exit_ip.get() + v_exit_ip.set(exit_s if exit_s else "—") + last_chain_state["hops"] = hops + last_chain_state["status"] = status + last_chain_state["exit_ip"] = exit_s or "" + root.after(250, lambda: _refresh_chain_map(silent=True)) + if status == "healthy" and exit_s and exit_s != prev_exit: + root.after(400, lambda: _refresh_exit_intel(silent=True)) + elif status != "healthy": + _apply_intel(None) + if status == "dead" and browser.is_running() and bool(br_kill_drop_var.get()): + browser.stop(dispose=bool(br_disposable_var.get())) + _refresh_browser_status() + _log("Browser stopped because chain dropped (Kill-on-drop enabled).") + + def _pump() -> None: + try: + while True: + _handle(ui_q.get_nowait()) + except queue.Empty: + pass + if browser_should_run[0] and not browser.is_running(): + cfg = _browser_cfg() + now = time.monotonic() + max_retries = max(0, int(svc.settings.max_browser_relaunches)) + if cfg.auto_relaunch and (now - browser_last_launch_ts[0]) >= 3.0: + if browser_relaunch_count[0] >= max_retries: + browser_should_run[0] = False + _log( + f"Browser auto-relaunch disabled after " + f"{browser_relaunch_count[0]} attempts — fix the profile " + "or restart manually." + ) + elif _launch_browser_with_cfg(cfg): + browser_last_launch_ts[0] = now + browser_relaunch_count[0] += 1 + root.after(80, _pump) + + # ───────────────────────────────────────────────────────────────────────── + # CLOSE / MINIMIZE + # ───────────────────────────────────────────────────────────────────────── + def _close() -> None: + _cancel_pulse() + browser_should_run[0] = False + browser.stop(dispose=bool(br_disposable_var.get())) + tray.stop() + svc.stop() + clear_system_proxy() + if is_admin() and fw_is_engaged(): + fw_disengage() + root.destroy() + + 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 + # ───────────────────────────────────────────────────────────────────────── + _refresh_boot() + _refresh_sysproxy() + _refresh_fw() + _refresh_vpn_ui() + _refresh_browser_status() + _refresh_fingerprint() + _log("─" * 60) + _log("Proxy God v2 — ready.") + _log(f"Admin: {'YES — kill-switch + privacy hardening available' if is_admin() else 'NO — run as Admin for full privacy tools'}") + _log(f"Log file: {LOG_PATH}") + _log("Press START to fetch, validate and chain proxies.") + _log("Chain Builder → build your chain, Test entire chain, then Start.") + _log("Browser tab → launch hardened Firefox profile that follows your chain.") + _log("Signup Prep → open signup pages with autofill (Google / Proton / HydraProxy / custom).") + _log("Privacy tab → MAC, hostname, IPv6, WebRTC, fingerprint audit, DNS checks.") + _log("Works with or without VPN — leak detection adapts automatically.") + _log("─" * 60) + _pump() + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/proxy_chain_manager/artifact_wipe.py b/proxy_chain_manager/artifact_wipe.py new file mode 100644 index 0000000..f9f7347 --- /dev/null +++ b/proxy_chain_manager/artifact_wipe.py @@ -0,0 +1,218 @@ +"""One-button forensic artifact wipe. + +Surfaces purged: + + • %TEMP% — per-user temp + • %SystemRoot%\\Prefetch\\*.pf (Admin only) — file-launch history + • %APPDATA%\\Microsoft\\Windows\\Recent\\* — recent files + • %APPDATA%\\Microsoft\\Windows\\Recent\\AutomaticDestinations\\* — Jump Lists + • %APPDATA%\\Microsoft\\Windows\\Recent\\CustomDestinations\\* — Jump Lists + • HKCU\\…\\Explorer\\RunMRU — Win+R history + • HKCU\\…\\Explorer\\TypedPaths — Explorer typed paths + • HKCU\\…\\Explorer\\WordWheelQuery — Start / Explorer search history + • HKCU\\…\\Explorer\\RecentDocs — recent docs MRU + • Clipboard — current contents + +Counts are reported but specific filenames are never logged (this is the +opposite of what we want to leak). All deletes use ``ignore_errors=True`` +because files in use by other apps are expected. +""" +from __future__ import annotations + +import ctypes +import logging +import os +import shutil +import subprocess +import winreg +from dataclasses import dataclass, field +from pathlib import Path + +from .firewall import is_admin + +log = logging.getLogger(__name__) + + +_REG_MRU_KEYS = ( + r"Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU", + r"Software\Microsoft\Windows\CurrentVersion\Explorer\TypedPaths", + r"Software\Microsoft\Windows\CurrentVersion\Explorer\WordWheelQuery", + r"Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs", +) + + +@dataclass +class WipeReport: + files_deleted: int = 0 + bytes_freed: int = 0 + folders_skipped: int = 0 + registry_keys_cleared: int = 0 + clipboard_cleared: bool = False + errors: list[str] = field(default_factory=list) + + def summary(self) -> str: + mb = self.bytes_freed / (1024 * 1024) + return ( + f"{self.files_deleted} files / {mb:.1f} MB freed, " + f"{self.registry_keys_cleared} MRU entries cleared, " + f"clipboard={'yes' if self.clipboard_cleared else 'no'}, " + f"errors={len(self.errors)}" + ) + + +def _walk_size(path: Path) -> int: + total = 0 + for p in path.rglob("*"): + try: + if p.is_file(): + total += p.stat().st_size + except OSError: + pass + return total + + +def _purge_dir(path: Path, rep: WipeReport, keep_root: bool = True) -> None: + """Delete contents of `path`. Preserves the directory itself when keep_root.""" + if not path.exists(): + return + try: + size_before = _walk_size(path) + except Exception: + size_before = 0 + + count = 0 + for child in path.iterdir(): + try: + if child.is_dir(): + shutil.rmtree(child, ignore_errors=True) + else: + child.unlink(missing_ok=True) + count += 1 + except OSError: + rep.folders_skipped += 1 + + if not keep_root: + try: + path.rmdir() + except OSError: + pass + + rep.files_deleted += count + try: + size_after = _walk_size(path) + except Exception: + size_after = 0 + rep.bytes_freed += max(0, size_before - size_after) + + +def _clear_mru_key(path: str, rep: WipeReport) -> None: + """Remove every value under a Run/Typed/Search MRU key.""" + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_ALL_ACCESS + ) as key: + count = 0 + try: + while True: + name, _, _ = winreg.EnumValue(key, 0) + try: + winreg.DeleteValue(key, name) + count += 1 + except OSError: + break + except OSError: + pass + # Recurse into subkeys (e.g. RecentDocs/.png) + sub_count = 0 + try: + while True: + sub_count += 1 + sub_name = winreg.EnumKey(key, 0) + try: + winreg.DeleteKey(key, sub_name) + except OSError: + break + if sub_count > 200: # safety cap + break + except OSError: + pass + if count or sub_count: + rep.registry_keys_cleared += 1 + except OSError: + pass + + +def _clear_clipboard(rep: WipeReport) -> None: + try: + user32 = ctypes.windll.user32 # type: ignore[attr-defined] + if user32.OpenClipboard(None): + try: + user32.EmptyClipboard() + rep.clipboard_cleared = True + finally: + user32.CloseClipboard() + except Exception as e: + rep.errors.append(f"clipboard: {e}") + + +def wipe_artifacts(include_prefetch: bool = True) -> WipeReport: + rep = WipeReport() + appdata = Path(os.environ.get("APPDATA", "")) if os.environ.get("APPDATA") else None + temp = Path(os.environ.get("TEMP", "")) if os.environ.get("TEMP") else None + sysroot = Path(os.environ.get("SystemRoot", r"C:\Windows")) + + # %TEMP% + if temp and temp.exists(): + _purge_dir(temp, rep, keep_root=True) + + # Prefetch (Admin) + if include_prefetch and is_admin(): + pf = sysroot / "Prefetch" + if pf.exists(): + count = 0 + for f in pf.glob("*.pf"): + try: + sz = f.stat().st_size + f.unlink(missing_ok=True) + count += 1 + rep.bytes_freed += sz + except OSError as e: + rep.errors.append(f"prefetch: {e}") + rep.files_deleted += count + + # Recent / Jump Lists + if appdata: + recent = appdata / "Microsoft" / "Windows" / "Recent" + if recent.exists(): + _purge_dir(recent / "AutomaticDestinations", rep, keep_root=True) + _purge_dir(recent / "CustomDestinations", rep, keep_root=True) + _purge_dir(recent, rep, keep_root=True) + + # MRU registry keys + for path in _REG_MRU_KEYS: + _clear_mru_key(path, rep) + + # Clipboard + _clear_clipboard(rep) + + # Trigger Explorer's "Clear recent items" via Shell API (covers Win10/11 + # Quick Access pinned ↔ recent lists not covered by raw file delete). + try: + shell32 = ctypes.windll.shell32 # type: ignore[attr-defined] + # SHCNE_ASSOCCHANGED tells Explorer to refresh its caches. + shell32.SHChangeNotify(0x08000000, 0x0000, None, None) + except Exception: + pass + + # Best-effort Defender quick-scan history flush — non-fatal. + try: + subprocess.run( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", + "Clear-RecycleBin -Force -ErrorAction SilentlyContinue"], + capture_output=True, text=True, timeout=20, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + except Exception: + pass + + return rep diff --git a/proxy_chain_manager/ban_tester.py b/proxy_chain_manager/ban_tester.py new file mode 100644 index 0000000..b27b792 --- /dev/null +++ b/proxy_chain_manager/ban_tester.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import concurrent.futures +import socket +from dataclasses import dataclass +from typing import Iterable + +import httpx + +# ── Site lists ──────────────────────────────────────────────────────────────── + +SITES_SOCIAL: tuple[tuple[str, str], ...] = ( + ("Google", "https://www.google.com/generate_204"), + ("YouTube", "https://www.youtube.com/"), + ("Facebook", "https://www.facebook.com/"), + ("Instagram", "https://www.instagram.com/"), + ("X / Twitter", "https://x.com/"), + ("Reddit", "https://www.reddit.com/"), + ("TikTok", "https://www.tiktok.com/"), + ("Wikipedia", "https://www.wikipedia.org/"), + ("DuckDuckGo", "https://duckduckgo.com/"), + ("BBC", "https://www.bbc.com/"), + ("GitHub", "https://github.com/"), +) + +SITES_SHOPPING: tuple[tuple[str, str], ...] = ( + ("Amazon", "https://www.amazon.com/"), + ("eBay", "https://www.ebay.com/"), + ("Walmart", "https://www.walmart.com/"), + ("Etsy", "https://www.etsy.com/"), + ("AliExpress", "https://www.aliexpress.com/"), + ("Best Buy", "https://www.bestbuy.com/"), + ("Target", "https://www.target.com/"), + ("Newegg", "https://www.newegg.com/"), +) + +SITES_CRYPTO: tuple[tuple[str, str], ...] = ( + ("Coinbase", "https://www.coinbase.com/"), + ("Binance", "https://www.binance.com/"), + ("Kraken", "https://www.kraken.com/"), + ("Bybit", "https://www.bybit.com/"), + ("OKX", "https://www.okx.com/"), + ("KuCoin", "https://www.kucoin.com/"), + ("Bitfinex", "https://www.bitfinex.com/"), + ("Gemini", "https://www.gemini.com/"), +) + +SITES_DNS: tuple[tuple[str, str], ...] = ( + ("Cloudflare DNS (1.1.1.1)", "https://1.1.1.1/"), + ("Google DNS (8.8.8.8)", "https://dns.google/"), + ("Quad9 DNS (9.9.9.9)", "https://quad9.net/"), + ("AdGuard DNS", "https://adguard-dns.io/"), + ("NextDNS", "https://nextdns.io/"), + ("Mullvad DNS", "https://mullvad.net/en/help/dns-over-https-and-dns-over-tls/"), + ("OpenDNS", "https://www.opendns.com/"), +) + +# Combined default: all categories +POPULAR_SITES: tuple[tuple[str, str], ...] = ( + SITES_SOCIAL + SITES_SHOPPING + SITES_CRYPTO + SITES_DNS +) + +SITE_CATEGORIES: dict[str, tuple[tuple[str, str], ...]] = { + "Social / General": SITES_SOCIAL, + "Shopping": SITES_SHOPPING, + "Crypto Exchanges": SITES_CRYPTO, + "DNS Providers": SITES_DNS, + "All": POPULAR_SITES, +} + +_BANNED_STATUS_CODES = {401, 403, 407, 418, 429, 451} +_BANNED_TEXT_HINTS = ( + "access denied", + "forbidden", + "temporarily blocked", + "request blocked", + "unusual traffic", + "challenge required", + "bot detected", + "suspicious activity", + "geo-blocked", + "not available in your region", + "cloudflare ray", + "ddos protection", + "attention required", + "complete the captcha", + "captcha required", + "g-recaptcha", + "hcaptcha", +) + + +@dataclass(frozen=True) +class BanTestResult: + site: str + url: str + status: str # ok | banned | error + code: int | None + detail: str + category: str = "" + + +def _looks_banned_body(body: str) -> bool: + t = (body or "").lower() + return any(h in t for h in _BANNED_TEXT_HINTS) + + +def _test_one( + proxy_url: str, + site: str, + url: str, + timeout_seconds: float, + category: str = "", +) -> BanTestResult: + timeout = httpx.Timeout(timeout_seconds, connect=min(8.0, timeout_seconds)) + try: + with httpx.Client( + proxy=proxy_url, + timeout=timeout, + verify=False, + follow_redirects=True, + headers={ + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/124.0.0.0 Safari/537.36" + ), + "Accept": "text/html,application/xhtml+xml,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.9", + }, + ) as c: + r = c.get(url) + body = r.text[:2400] if r.text else "" + if r.status_code in _BANNED_STATUS_CODES or _looks_banned_body(body): + return BanTestResult( + site, url, "banned", r.status_code, + f"HTTP {r.status_code}", category, + ) + if 200 <= r.status_code < 400: + return BanTestResult(site, url, "ok", r.status_code, f"HTTP {r.status_code}", category) + return BanTestResult(site, url, "error", r.status_code, f"HTTP {r.status_code}", category) + except Exception as e: + short = str(e)[:80] + return BanTestResult(site, url, "error", None, f"{type(e).__name__}: {short}", category) + + +def _dns_resolve_via_system(hostname: str, timeout: float = 5.0) -> str | None: + """Resolve a hostname using system DNS (does not go through proxy — exposes leak).""" + _prev = socket.getdefaulttimeout() + try: + socket.setdefaulttimeout(timeout) + info = socket.getaddrinfo(hostname, None) + for entry in info: + addr = entry[4][0] + if addr: + return addr + except Exception: + pass + finally: + socket.setdefaulttimeout(_prev) + return None + + +# NOTE: removed legacy ``check_dns_leak`` / ``DnsLeakResult`` — they relied on +# the ``x-real-ip`` HTTP response header which proxy targets do not set, so +# results were meaningless. Use :func:`proxy_chain_manager.dns_leak.run_dns_leak_test` +# instead. + + +def run_ban_tests( + proxy_url: str, + timeout_seconds: float = 12.0, + sites: Iterable[tuple[str, str]] | None = None, + categories: Iterable[str] | None = None, + max_workers: int = 8, +) -> list[BanTestResult]: + """ + Run ban tests in parallel across requested site categories. + + ``categories`` accepts names from SITE_CATEGORIES (e.g. "Shopping", "Crypto Exchanges"). + Defaults to all categories when neither ``sites`` nor ``categories`` is given. + """ + if sites is not None: + work = [(s, u, "") for s, u in sites] + elif categories is not None: + seen_urls: set[str] = set() + work = [] + for cat in categories: + for s, u in SITE_CATEGORIES.get(cat, ()): + if u not in seen_urls: + seen_urls.add(u) + work.append((s, u, cat)) + else: + work = [(s, u, cat) for cat, pairs in SITE_CATEGORIES.items() if cat != "All" for s, u in pairs] + + out: list[BanTestResult] = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool: + futures = { + pool.submit(_test_one, proxy_url, site, url, timeout_seconds, cat): (site, url, cat) + for site, url, cat in work + } + for fut in concurrent.futures.as_completed(futures): + try: + out.append(fut.result()) + except Exception: + site, url, cat = futures[fut] + out.append(BanTestResult(site, url, "error", None, "internal error", cat)) + + # Sort: category then site name + out.sort(key=lambda r: (r.category, r.site)) + return out + + +def test_single_site( + proxy_url: str, + site: str, + url: str, + timeout_seconds: float = 10.0, +) -> BanTestResult: + """Probe one URL through the proxy. Used by pre-flight checks.""" + return _test_one(proxy_url, site, url, timeout_seconds) diff --git a/proxy_chain_manager/browser_identity.py b/proxy_chain_manager/browser_identity.py new file mode 100644 index 0000000..0d3518e --- /dev/null +++ b/proxy_chain_manager/browser_identity.py @@ -0,0 +1,327 @@ +"""Browser identity personas + cookie policies. + +The "lock everything down" approach (Firefox RFP + WebRTC off + cookies blocked) +keeps you safe from fingerprinting but makes the machine *look weird* — sites +flag you as suspicious and ban / captcha you. Worse: very few users use that +config, so RFP-on is itself a fingerprint. + +Two strategies are exposed: + + • **Blend-in personas** — mimic the most common Windows-Chrome-US-en setup. + JS / cookies / WebGL all work; everything looks normal; only WebRTC is + blocked (because that leaks the real IP behind the proxy). The proxy + chain itself does the actual hiding. + + • **Hardened** — full RFP / blocked WebRTC / strict tracking / FPI. Stand-out + but maximum block. Same behavior as the original profile. + +Cookie modes are independent of persona. Each mode expresses a complete +policy: which cookies to accept, lifetime, whether to wipe on close, and +whether to partition 3rd-party storage. +""" +from __future__ import annotations + +from dataclasses import dataclass + + +# ── Personas ────────────────────────────────────────────────────────────────── + +PERSONAS: list[str] = [ + "blend_windows_chrome", + "blend_windows_chrome_laptop", + "blend_windows_firefox", + "blend_windows_edge", + "blend_mac_safari", + "blend_mac_chrome", + "blend_linux_firefox", + "hardened", + "custom", +] + +PERSONA_LABELS: dict[str, str] = { + "blend_windows_chrome": "Blend — Win10 + Chrome 124 (most common, US-en)", + "blend_windows_chrome_laptop": "Blend — Win11 + Chrome 124 laptop 1366×768", + "blend_windows_firefox": "Blend — Win10 + Firefox 128 (US-en)", + "blend_windows_edge": "Blend — Win11 + Edge 124 (corporate look)", + "blend_mac_safari": "Blend — macOS 14 + Safari 17 (US-en)", + "blend_mac_chrome": "Blend — macOS 14 + Chrome 124 (US-en)", + "blend_linux_firefox": "Blend — Ubuntu + Firefox 128 (en-US)", + "hardened": "Hardened — RFP + FPI + block all (stands out)", + "custom": "Custom — use individual hardening toggles", +} + + +@dataclass(frozen=True) +class Persona: + """A user-agent / locale / timezone / screen tuple that mimics a real, + common configuration.""" + key: str + user_agent: str + accept_language: str + timezone: str + screen_w: int + screen_h: int + platform: str + locale: str + + +PERSONA_DATA: dict[str, Persona] = { + "blend_windows_chrome": Persona( + key="blend_windows_chrome", + user_agent=( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" + ), + accept_language="en-US,en;q=0.9", + timezone="America/New_York", + screen_w=1920, screen_h=1080, + platform="Win32", + locale="en-US", + ), + "blend_windows_chrome_laptop": Persona( + key="blend_windows_chrome_laptop", + user_agent=( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" + ), + accept_language="en-US,en;q=0.9", + timezone="America/Chicago", + screen_w=1366, screen_h=768, + platform="Win32", + locale="en-US", + ), + "blend_windows_firefox": Persona( + key="blend_windows_firefox", + user_agent=( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) " + "Gecko/20100101 Firefox/128.0" + ), + accept_language="en-US,en;q=0.5", + timezone="America/New_York", + screen_w=1920, screen_h=1080, + platform="Win32", + locale="en-US", + ), + "blend_windows_edge": Persona( + key="blend_windows_edge", + user_agent=( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0" + ), + accept_language="en-US,en;q=0.9", + timezone="America/Chicago", + screen_w=1920, screen_h=1080, + platform="Win32", + locale="en-US", + ), + "blend_mac_safari": Persona( + key="blend_mac_safari", + user_agent=( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15" + ), + accept_language="en-US,en;q=0.9", + timezone="America/Los_Angeles", + screen_w=1680, screen_h=1050, + platform="MacIntel", + locale="en-US", + ), + "blend_mac_chrome": Persona( + key="blend_mac_chrome", + user_agent=( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" + ), + accept_language="en-US,en;q=0.9", + timezone="America/Los_Angeles", + screen_w=1440, screen_h=900, + platform="MacIntel", + locale="en-US", + ), + "blend_linux_firefox": Persona( + key="blend_linux_firefox", + user_agent=( + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:128.0) " + "Gecko/20100101 Firefox/128.0" + ), + accept_language="en-US,en;q=0.5", + timezone="America/New_York", + screen_w=1920, screen_h=1080, + platform="Linux x86_64", + locale="en-US", + ), +} + + +# ── Cookie policies ─────────────────────────────────────────────────────────── + +@dataclass(frozen=True) +class CookiePolicy: + """ + Complete Firefox cookie configuration expressed as a single composable unit. + + behavior : network.cookie.cookieBehavior + 0 = accept all + 1 = block 3rd-party + 2 = block ALL cookies + 3 = block 3rd-party from unvisited sites + 4 = block cross-site tracking (ETP Strict) + 5 = block cross-site + social media trackers + lifetime : network.cookie.lifetimePolicy + 0 = normal (respect Set-Cookie expires) + 2 = session-only (all cookies expire on close) + partition : Total Cookie Protection — partition 3rd-party storage + by the top-level site (network.cookie.cookieBehavior.optInPartitioning) + clear_cookies : privacy.clearOnShutdown.cookies + clear_cache : privacy.clearOnShutdown.cache + clear_history : privacy.clearOnShutdown.history + clear_ls : privacy.clearOnShutdown.localStorage + indexedDB + clear_formdata : privacy.clearOnShutdown.formdata + sanitize : privacy.sanitize.sanitizeOnShutdown (master switch) + """ + behavior: int = 1 + lifetime: int = 0 + partition: bool = False + sanitize: bool = False + clear_cookies: bool = False + clear_cache: bool = False + clear_history: bool = False + clear_ls: bool = False + clear_formdata: bool = False + + +# Every cookie mode is a complete CookiePolicy — no guesswork in profile.py. +COOKIE_POLICIES: dict[str, CookiePolicy] = { + + # ── Permissive ────────────────────────────────────────────────────────── + + "accept_all": CookiePolicy( + behavior=0, lifetime=0, + # No clearing, no blocking — maximum site compatibility. + ), + + "accept_all_session": CookiePolicy( + behavior=0, lifetime=2, + sanitize=True, clear_cookies=True, clear_ls=True, + # Accept all cookies while browsing but wipe them when the + # browser closes. Good for signup / form-filling sessions. + ), + + # ── Standard 3rd-party blocking ──────────────────────────────────────── + + "block_third_party": CookiePolicy( + behavior=1, lifetime=0, + # Block 3rd-party, accept 1st-party with normal lifetime. + # Recommended baseline — most sites work fine. + ), + + "block_third_party_session": CookiePolicy( + behavior=1, lifetime=2, + sanitize=True, clear_cookies=True, clear_cache=True, clear_ls=True, + # Block 3rd-party AND treat all accepted cookies as session-only. + # Nothing persists. Use for anonymous sessions. + ), + + # ── Tracker-focused blocking ──────────────────────────────────────────── + + "block_third_party_trackers": CookiePolicy( + behavior=4, lifetime=0, + # Firefox ETP Strict mode: only blocks known trackers, not ALL + # 3rd-party cookies. Best blend-in option — same as Firefox default. + ), + + "block_social_trackers": CookiePolicy( + behavior=5, lifetime=0, + # Block cross-site cookies AND social-media tracking cookies + # (Facebook, Twitter pixels, etc.). Stricter than ETP Strict but + # still allows most logins. + ), + + "block_unvisited": CookiePolicy( + behavior=3, lifetime=0, + # Block 3rd-party from sites the user has never visited. Very light + # — almost invisible to sites; good when you need max compatibility. + ), + + # ── Partitioned storage (Total Cookie Protection) ─────────────────────── + + "partitioned_tcp": CookiePolicy( + behavior=1, lifetime=0, partition=True, + # Block 3rd-party AND enable Total Cookie Protection: each site + # gets its own isolated cookie jar so cross-site tracking via + # cookie sync is impossible. Recommended for blend-in + privacy. + ), + + "partitioned_session": CookiePolicy( + behavior=1, lifetime=2, partition=True, + sanitize=True, clear_cookies=True, clear_cache=True, + clear_ls=True, clear_formdata=True, + # Partitioned + session-only + full wipe on close. Leaves no + # persistent state on disk after the browser exits. + ), + + # ── Maximum wipe ─────────────────────────────────────────────────────── + + "session_only": CookiePolicy( + behavior=0, lifetime=2, + sanitize=True, clear_cookies=True, + # All cookies accepted but expire when the browser closes. + # Note: localStorage is NOT cleared here — only cookies. + ), + + "ghost_mode": CookiePolicy( + behavior=1, lifetime=2, + sanitize=True, clear_cookies=True, clear_cache=True, + clear_history=True, clear_ls=True, clear_formdata=True, + partition=True, + # Maximum ephemeral session: block 3rd-party, TCP partitioning, + # session-only lifetime, full wipe of cookies + cache + history + + # localStorage/IndexedDB + formdata on every close. Zero disk trace. + ), + + "block_all": CookiePolicy( + behavior=2, lifetime=0, + # Block ALL cookies. Most login-dependent sites will break. + # Only useful for read-only scraping sessions. + ), +} + +COOKIE_MODES: list[str] = list(COOKIE_POLICIES) + +COOKIE_LABELS: dict[str, str] = { + "accept_all": "Accept all — maximum compatibility (cookies persist)", + "accept_all_session": "Accept all, session only — wipe cookies on close", + "block_third_party": "Block 3rd-party — recommended baseline (persists 1st-party)", + "block_third_party_session": "Block 3rd-party + session — nothing persists", + "block_third_party_trackers":"Block trackers only — ETP Strict (Firefox default-strict)", + "block_social_trackers": "Block social + cross-site trackers (Facebook pixel, etc.)", + "block_unvisited": "Block 3rd-party from unvisited sites — lightest option", + "partitioned_tcp": "Partitioned (TCP) — each site gets isolated 3rd-party jar", + "partitioned_session": "Partitioned + session — isolated jars, full wipe on close", + "session_only": "Session only — all cookies expire on close (no partition)", + "ghost_mode": "Ghost mode — partitioned + session + full disk wipe on close", + "block_all": "Block ALL cookies — breaks most logins", +} + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +def get_cookie_policy(mode: str) -> CookiePolicy: + return COOKIE_POLICIES.get(mode, COOKIE_POLICIES["block_third_party"]) + + +# Legacy shims kept for code that still calls these directly. +def cookie_behavior_value(mode: str) -> int: + return get_cookie_policy(mode).behavior + + +def cookie_session_only(mode: str) -> bool: + return get_cookie_policy(mode).lifetime == 2 + + +def get_persona(key: str) -> Persona | None: + return PERSONA_DATA.get(key) + + +def is_blend_persona(key: str) -> bool: + return key.startswith("blend_") diff --git a/proxy_chain_manager/browser_launcher.py b/proxy_chain_manager/browser_launcher.py new file mode 100644 index 0000000..9125826 --- /dev/null +++ b/proxy_chain_manager/browser_launcher.py @@ -0,0 +1,402 @@ +from __future__ import annotations + +import logging +import os +import shutil +import subprocess +import sys +import time +import winreg +from dataclasses import dataclass +from pathlib import Path + +from .browser_profile import FirefoxHardening, ensure_firefox_profile, persona_env +from .paths import app_data_dir +from .signup_prep import SignupDraft, install_signup_extension + +log = logging.getLogger(__name__) + +# CREATE_NO_WINDOW suppresses the console; DETACHED_PROCESS fully separates +# the child from Python's job object so Firefox lives past Python's exit. +_CREATE_FLAGS = 0 if sys.platform != "win32" else ( + getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000) + | getattr(subprocess, "DETACHED_PROCESS", 0x00000008) + | getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0x00000200) +) + + +@dataclass +class BrowserConfig: + firefox_path: str = "" + profile_dir: str = "" + clear_on_close: bool = True + disposable_profile: bool = False + kill_on_chain_drop: bool = True + auto_relaunch: bool = False + lock_managed_profile: bool = True + force_proxy: bool = True + disable_webrtc: bool = True + resist_fingerprinting: bool = True + disable_telemetry: bool = True + first_party_isolation: bool = True + strict_tracking_protection: bool = True + timezone_utc: bool = True + persona: str = "blend_windows_chrome" + cookie_mode: str = "block_third_party" + + +def _find_firefox_via_registry() -> str: + """Look up Firefox install path in the Windows registry.""" + hives = [ + (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Mozilla\Mozilla Firefox"), + (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Mozilla\Mozilla Firefox"), + (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Mozilla\Mozilla Firefox"), + ] + for hive, key_path in hives: + try: + with winreg.OpenKey(hive, key_path) as root: + version = winreg.QueryValue(root, None) + for sub in (f"{version}\\Main", "bin"): + try: + with winreg.OpenKey(root, sub) as k: + path = winreg.QueryValueEx(k, "PathToExe")[0] + if path and Path(path).is_file(): + return str(path) + except OSError: + continue + except OSError: + continue + + # Also try the "Uninstall" key + for hive in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER): + for root_path in ( + r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", + r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", + ): + try: + with winreg.OpenKey(hive, root_path) as base: + i = 0 + while True: + try: + sub = winreg.EnumKey(base, i) + i += 1 + if "firefox" not in sub.lower(): + continue + with winreg.OpenKey(base, sub) as k: + loc = winreg.QueryValueEx(k, "InstallLocation")[0] + candidate = Path(loc) / "firefox.exe" + if candidate.is_file(): + return str(candidate) + except OSError: + break + except OSError: + continue + return "" + + +def default_firefox_path() -> str: + """Find Firefox.exe — registry first, then well-known paths.""" + if sys.platform == "darwin": + candidates = [ + Path("/Applications/Firefox.app/Contents/MacOS/firefox"), + Path.home() / "Applications" / "Firefox.app" / "Contents" / "MacOS" / "firefox", + ] + which = shutil.which("firefox") + if which: + candidates.insert(0, Path(which)) + for c in candidates: + if c.is_file(): + return str(c) + return "" + # 1) Registry + reg = _find_firefox_via_registry() + if reg: + return reg + + # 2) Standard install locations + candidates: list[Path] = [ + Path(r"C:\Program Files\Mozilla Firefox\firefox.exe"), + Path(r"C:\Program Files (x86)\Mozilla Firefox\firefox.exe"), + Path(os.path.expandvars(r"%LOCALAPPDATA%\Mozilla Firefox\firefox.exe")), + Path(os.path.expandvars(r"%APPDATA%\Mozilla Firefox\firefox.exe")), + ] + # 3) Scoop / winget / portable in PATH + which = shutil.which("firefox") or shutil.which("firefox.exe") + if which: + candidates.insert(0, Path(which)) + + for c in candidates: + if c.is_file(): + return str(c) + return "" + + +def default_profile_dir() -> str: + return str(app_data_dir() / "browser_profiles" / "firefox_hardened") + + +def _firefox_is_running_on_system() -> bool: + """Check if any firefox.exe process is alive (tasklist, no admin needed).""" + if sys.platform == "darwin": + try: + r = subprocess.run(["pgrep", "-x", "firefox"], capture_output=True, text=True, timeout=6) + return r.returncode == 0 and bool((r.stdout or "").strip()) + except Exception: + return False + try: + r = subprocess.run( + ["tasklist", "/FI", "IMAGENAME eq firefox.exe", "/NH", "/FO", "CSV"], + capture_output=True, text=True, timeout=6, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000), + ) + return "firefox.exe" in (r.stdout or "").lower() + except Exception: + return False + + +def _has_descendants(root_pid: int) -> bool: + """True iff *root_pid* or any descendant process is currently alive. + + Walks the process tree from root_pid using WMIC ProcessId/ParentProcessId + (no admin required). Falls back to checking the root PID's existence via + tasklist if WMIC is missing on the host. + """ + if not root_pid: + return False + if sys.platform == "darwin": + try: + r = subprocess.run(["ps", "-p", str(root_pid)], capture_output=True, text=True, timeout=6) + return str(root_pid) in (r.stdout or "") + except Exception: + return False + # Fast path: is the root PID itself still alive? + try: + r = subprocess.run( + ["tasklist", "/FI", f"PID eq {root_pid}", "/NH", "/FO", "CSV"], + capture_output=True, text=True, timeout=6, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000), + ) + if str(root_pid) in (r.stdout or ""): + return True + except Exception: # noqa: BLE001 + pass + # Walk descendants via WMIC. Builds {parent: [child, ...]} then BFS. + try: + w = subprocess.run( + ["wmic", "process", "get", "ProcessId,ParentProcessId", "/FORMAT:CSV"], + capture_output=True, text=True, timeout=10, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000), + ) + children: dict[int, list[int]] = {} + for line in (w.stdout or "").splitlines(): + parts = [p.strip() for p in line.split(",")] + # CSV header: Node,ParentProcessId,ProcessId + if len(parts) < 3 or not parts[1].isdigit() or not parts[2].isdigit(): + continue + ppid, pid = int(parts[1]), int(parts[2]) + children.setdefault(ppid, []).append(pid) + stack = [root_pid] + seen = {root_pid} + while stack: + cur = stack.pop() + kids = children.get(cur, []) + for k in kids: + if k in seen: + continue + seen.add(k) + # Any live descendant = our session is still alive. + return True + return False + except Exception: # noqa: BLE001 + # WMIC missing (newer Windows) — be conservative and return False + # rather than the old "any firefox.exe = mine" heuristic. + return False + + +class BrowserSession: + def __init__(self) -> None: + self._proc: subprocess.Popen[str] | None = None + self._profile_path: Path | None = None + self._launched_at: float = 0.0 + + def is_running(self) -> bool: + if self._proc is None: + return False + # Firefox's initial launcher process exits quickly (code 0) while + # browser child processes carry on. Poll the parent process first. + if self._proc.poll() is None: + return True + # Parent has exited — check whether any process in our spawned tree + # (the original PID's children) is still alive. Using a PID-scoped + # WMIC query avoids the old failure mode of treating any unrelated + # firefox.exe on the machine as ours. + launched_pid = self._proc.pid + return _has_descendants(launched_pid) + + def pid(self) -> int | None: + if self._proc is None: + return None + return self._proc.pid + + def launch( + self, + cfg: BrowserConfig, + proxy_host: str, + proxy_port: int, + start_url: str = "", + signup_draft: SignupDraft | None = None, + ) -> tuple[bool, str]: + raw_exe = (cfg.firefox_path or default_firefox_path()).strip().strip('"').strip("'") + if not raw_exe: + return ( + False, + "Firefox not found. Install Firefox then set the path in the Browser tab " + "(or browse to it with the Browse button).", + ) + exe = Path(raw_exe) + if not exe.is_file(): + return False, ( + f"Firefox executable not found: {raw_exe} " + "Use the Browse button on the Browser tab to locate firefox.exe." + ) + + raw_profile = (cfg.profile_dir or default_profile_dir()).strip().strip('"').strip("'") + profile_dir = Path(raw_profile) + + hard = FirefoxHardening( + force_proxy=cfg.force_proxy, + disable_webrtc=cfg.disable_webrtc, + resist_fingerprinting=cfg.resist_fingerprinting, + disable_telemetry=cfg.disable_telemetry, + first_party_isolation=cfg.first_party_isolation, + strict_tracking_protection=cfg.strict_tracking_protection, + clear_on_shutdown=cfg.clear_on_close, + timezone_utc=cfg.timezone_utc, + persona_key=cfg.persona, + cookie_mode=cfg.cookie_mode, + ) + + try: + if cfg.lock_managed_profile: + ensure_firefox_profile(profile_dir, proxy_host, int(proxy_port), hard) + if signup_draft is not None: + install_signup_extension(profile_dir, signup_draft) + except Exception as e: + return False, f"Profile setup failed: {e!s}" + + if self.is_running(): + self.stop() + + if cfg.lock_managed_profile: + args = [str(exe), "-no-remote", "-profile", str(profile_dir)] + else: + args = [str(exe)] + + url = (start_url or "").strip() + if url: + args.append(url) + + env = os.environ.copy() + env.update(persona_env(hard)) + + try: + self._proc = subprocess.Popen( + args, + # DETACHED_PROCESS ensures Firefox outlives Python and is not + # killed when Python's job object exits. CREATE_NO_WINDOW + # suppresses any console window. CREATE_NEW_PROCESS_GROUP + # isolates Ctrl-C handling. + creationflags=_CREATE_FLAGS, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + text=True, + env=env, + ) + except Exception as e: + self._proc = None + return False, f"Launch failed: {e!s}" + + self._launched_at = time.monotonic() + + # Firefox's multi-process launcher exits (code 0) within ~0.5 s while + # the actual browser window loads in child processes. We therefore do + # NOT treat a quick exit of the parent as an error — we just verify + # that *some* firefox.exe appears in the process list within 3 s. + deadline = time.monotonic() + 3.0 + found = False + while time.monotonic() < deadline: + time.sleep(0.3) + rc = self._proc.poll() + # If it already exited with an error code, report it. + if rc is not None and rc not in (0, None): + self._proc = None + return False, f"Firefox exited with error code {rc}." + if _firefox_is_running_on_system(): + found = True + break + + if not found: + # One last check — maybe the profile write was the only action needed + # and Firefox opened fast; accept success if parent is still alive. + if self._proc.poll() is None: + found = True + + if not found: + self._proc = None + return False, ( + "Firefox did not appear in the process list after 3 s. " + "Check that the path is correct and Firefox is not already blocked by another -profile lock." + ) + + self._profile_path = profile_dir if cfg.lock_managed_profile else None + pid = self._proc.pid + log.info("Launched hardened Firefox pid=%s profile=%s", pid, profile_dir) + mode = "hardened profile" if cfg.lock_managed_profile else "unlocked mode" + return True, f"Firefox started ({mode}, pid {pid})." + + def stop(self, dispose: bool = False) -> tuple[bool, str]: + # Capture our PID before clearing the handle + launched_pid: int | None = self._proc.pid if self._proc else None + + # Terminate the Popen handle if still alive + if self._proc and self._proc.poll() is None: + try: + self._proc.terminate() + self._proc.wait(timeout=8) + except Exception: + try: + self._proc.kill() + except Exception: + pass + finally: + self._proc = None + + # Kill only the process tree we spawned (handles the detached-child + # case where the launcher process already exited naturally). + # /T kills the entire child tree; /PID scopes to our PID only. + if launched_pid is not None: + if sys.platform == "darwin": + try: + subprocess.run(["pkill", "-P", str(launched_pid)], capture_output=True, timeout=10) + except Exception: + pass + else: + try: + subprocess.run( + ["taskkill", "/F", "/T", "/PID", str(launched_pid)], + capture_output=True, + timeout=10, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000), + ) + except Exception: + pass + + self._proc = None + if dispose and self._profile_path and self._profile_path.exists(): + try: + shutil.rmtree(self._profile_path, ignore_errors=True) + except Exception: + pass + self._launched_at = 0.0 + return True, "Browser stopped." diff --git a/proxy_chain_manager/browser_profile.py b/proxy_chain_manager/browser_profile.py new file mode 100644 index 0000000..1d83e02 --- /dev/null +++ b/proxy_chain_manager/browser_profile.py @@ -0,0 +1,271 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path + +from .browser_identity import ( + CookiePolicy, + Persona, + cookie_behavior_value, + cookie_session_only, + get_cookie_policy, + get_persona, + is_blend_persona, +) + + +@dataclass +class FirefoxHardening: + # Proxy & network always-on + force_proxy: bool = True + disable_webrtc: bool = True # always strongly recommended; leaks real IP + + # Hardened block mode — overrides persona spoofing + resist_fingerprinting: bool = True + disable_telemetry: bool = True + first_party_isolation: bool = True + strict_tracking_protection: bool = True + clear_on_shutdown: bool = True + timezone_utc: bool = True + + # Persona / cookie selectors (added in "spoof when possible" mode) + persona_key: str = "custom" # see browser_identity.PERSONAS + cookie_mode: str = "block_third_party" # see browser_identity.COOKIE_MODES + + # Optional Firefox-managed user-agent override even outside of a persona + user_agent_override: str = "" + + # Filled in by ``apply_persona`` when persona_key starts with "blend_". + persona: Persona | None = field(default=None, repr=False, compare=False) + + +def apply_persona(hard: FirefoxHardening) -> FirefoxHardening: + """Mutate hardening flags consistent with the persona choice. + + Blend-in personas relax the "stand out" toggles (RFP, FPI, strict TP) but + keep the IP-leakers off (WebRTC, geolocation, etc.). Cookie mode is + honored. The result is a profile that looks like a normal browser. + """ + if hard.persona_key == "hardened": + hard.resist_fingerprinting = True + hard.first_party_isolation = True + hard.strict_tracking_protection = True + hard.timezone_utc = True + hard.persona = None + return hard + + if is_blend_persona(hard.persona_key): + hard.resist_fingerprinting = False # RFP itself is a fingerprint + hard.first_party_isolation = False # breaks logins on many sites + hard.strict_tracking_protection = False # Firefox default ETP is enough + hard.timezone_utc = False + hard.persona = get_persona(hard.persona_key) + if hard.persona and not hard.user_agent_override: + hard.user_agent_override = hard.persona.user_agent + return hard + + # custom — honor the individual flags as-is. + hard.persona = None + return hard + + +def _bool(v: bool) -> str: + return "true" if v else "false" + + +def build_user_js( + proxy_host: str, + proxy_port: int, + hard: FirefoxHardening, +) -> str: + hard = apply_persona(hard) + lines: list[str] = [ + "// Managed by Proxy God. Changes are overwritten on next launch.", + # Always-on baseline (no UX cost) + 'user_pref("app.normandy.enabled", false);', + 'user_pref("app.shield.optoutstudies.enabled", false);', + 'user_pref("browser.newtabpage.activity-stream.feeds.telemetry", false);', + 'user_pref("browser.newtabpage.activity-stream.telemetry", false);', + 'user_pref("browser.ping-centre.telemetry", false);', + 'user_pref("toolkit.telemetry.archive.enabled", false);', + 'user_pref("toolkit.telemetry.bhrPing.enabled", false);', + 'user_pref("toolkit.telemetry.enabled", false);', + 'user_pref("toolkit.telemetry.firstShutdownPing.enabled", false);', + 'user_pref("toolkit.telemetry.hybridContent.enabled", false);', + 'user_pref("toolkit.telemetry.newProfilePing.enabled", false);', + 'user_pref("toolkit.telemetry.shutdownPingSender.enabled", false);', + 'user_pref("toolkit.telemetry.unified", false);', + 'user_pref("datareporting.healthreport.uploadEnabled", false);', + 'user_pref("datareporting.policy.dataSubmissionEnabled", false);', + 'user_pref("network.trr.mode", 5);', + 'user_pref("network.captive-portal-service.enabled", false);', + 'user_pref("geo.enabled", false);', + 'user_pref("media.navigator.enabled", false);', + 'user_pref("dom.battery.enabled", false);', + 'user_pref("dom.gamepad.enabled", false);', + 'user_pref("dom.netinfo.enabled", false);', + # WebGL allowed by default (blocking it makes you stand out massively) + 'user_pref("webgl.enable-debug-renderer-info", false);', + ] + + if hard.force_proxy: + lines.extend( + [ + 'user_pref("network.proxy.type", 1);', + 'user_pref("network.proxy.share_proxy_settings", true);', + f'user_pref("network.proxy.http", "{proxy_host}");', + f'user_pref("network.proxy.http_port", {int(proxy_port)});', + f'user_pref("network.proxy.ssl", "{proxy_host}");', + f'user_pref("network.proxy.ssl_port", {int(proxy_port)});', + 'user_pref("network.proxy.ftp", "");', + 'user_pref("network.proxy.ftp_port", 0);', + 'user_pref("network.proxy.socks", "");', + 'user_pref("network.proxy.socks_port", 0);', + 'user_pref("network.proxy.socks_version", 5);', + 'user_pref("network.proxy.socks_remote_dns", false);', + 'user_pref("network.proxy.no_proxies_on", "");', + ] + ) + + if hard.disable_webrtc: + # Off regardless of persona — WebRTC leaks real IP through STUN. + lines.extend( + [ + 'user_pref("media.peerconnection.enabled", false);', + 'user_pref("media.peerconnection.ice.default_address_only", true);', + 'user_pref("media.peerconnection.ice.no_host", true);', + ] + ) + + if hard.resist_fingerprinting: + lines.extend( + [ + 'user_pref("privacy.resistFingerprinting", true);', + 'user_pref("privacy.resistFingerprinting.letterboxing", true);', + 'user_pref("privacy.window.maxInnerWidth", 1600);', + 'user_pref("privacy.window.maxInnerHeight", 900);', + ] + ) + + if hard.first_party_isolation: + lines.append('user_pref("privacy.firstparty.isolate", true);') + + if hard.disable_telemetry: + lines.append('user_pref("browser.send_pings", false);') + + if hard.strict_tracking_protection: + lines.extend( + [ + 'user_pref("privacy.trackingprotection.enabled", true);', + 'user_pref("privacy.trackingprotection.pbmode.enabled", true);', + ] + ) + + # Persona spoofing (UA, accept-language, screen, platform) + if hard.persona is not None: + p = hard.persona + lines.extend( + [ + f'user_pref("general.useragent.override", "{p.user_agent}");', + f'user_pref("intl.accept_languages", "{p.accept_language}");', + f'user_pref("general.useragent.locale", "{p.locale}");', + 'user_pref("javascript.use_us_english_locale", true);', + f'user_pref("privacy.window.maxInnerWidth", {int(p.screen_w)});', + f'user_pref("privacy.window.maxInnerHeight", {int(p.screen_h)});', + # Hint timezone — Firefox honors TZ env var at launch (set by + # the launcher) but this pref nudges some site detection too. + f'user_pref("intl.locale.requested", "{p.locale}");', + ] + ) + elif hard.user_agent_override: + lines.append( + f'user_pref("general.useragent.override", "{hard.user_agent_override}");' + ) + + # ── Cookie policy ──────────────────────────────────────────────────────── + cp: CookiePolicy = get_cookie_policy(hard.cookie_mode) + + lines.extend([ + f'user_pref("network.cookie.cookieBehavior", {cp.behavior});', + # pbmode = private-browsing mode uses same behavior + f'user_pref("network.cookie.cookieBehavior.pbmode", {cp.behavior});', + # lifetime: 0 = normal, 2 = session-only + f'user_pref("network.cookie.lifetimePolicy", {cp.lifetime});', + ]) + + # Total Cookie Protection (TCP / Firefox partitioning) + if cp.partition: + lines.extend([ + 'user_pref("network.cookie.cookieBehavior.optInPartitioning", true);', + # Also isolate cache, localStorage, IndexedDB per top-level site + 'user_pref("privacy.partition.serviceWorkers", true);', + 'user_pref("privacy.partition.network_state", true);', + 'user_pref("privacy.partition.bloburl_by_default", true);', + ]) + else: + lines.extend([ + 'user_pref("network.cookie.cookieBehavior.optInPartitioning", false);', + ]) + + # SameSite=None cookies must be Secure (good hygiene regardless of mode) + lines.append('user_pref("network.cookie.sameSite.noneRequiresSecure", true);') + + # ── Sanitize-on-shutdown ───────────────────────────────────────────────── + # The CookiePolicy sanitize flag takes priority; the hard.clear_on_shutdown + # toggle acts as an additional override for cookies+cache even when the + # cookie mode doesn't request it. + do_sanitize = cp.sanitize or hard.clear_on_shutdown + if do_sanitize: + lines.append('user_pref("privacy.sanitize.sanitizeOnShutdown", true);') + # Cookies + lines.append(f'user_pref("privacy.clearOnShutdown.cookies", {_bool(cp.clear_cookies or hard.clear_on_shutdown)});') + # Cache + lines.append(f'user_pref("privacy.clearOnShutdown.cache", {_bool(cp.clear_cache or hard.clear_on_shutdown)});') + # History + lines.append(f'user_pref("privacy.clearOnShutdown.history", {_bool(cp.clear_history or hard.clear_on_shutdown)});') + # Downloads list + lines.append('user_pref("privacy.clearOnShutdown.downloads", true);') + # Form / search bar autofill data + lines.append(f'user_pref("privacy.clearOnShutdown.formdata", {_bool(cp.clear_formdata or hard.clear_on_shutdown)});') + # Active sessions (HTTP auth, logged-in sites) + lines.append('user_pref("privacy.clearOnShutdown.sessions", true);') + # localStorage + IndexedDB + lines.append(f'user_pref("privacy.clearOnShutdown.localStorage", {_bool(cp.clear_ls or hard.clear_on_shutdown)});') + lines.append(f'user_pref("privacy.clearOnShutdown.indexedDB", {_bool(cp.clear_ls or hard.clear_on_shutdown)});') + # Service worker registrations + lines.append('user_pref("privacy.clearOnShutdown.serviceWorkers", true);') + # Offline web app cache + lines.append('user_pref("privacy.clearOnShutdown.offlineApps", true);') + else: + lines.append('user_pref("privacy.sanitize.sanitizeOnShutdown", false);') + + if hard.timezone_utc: + lines.append('user_pref("privacy.resistFingerprinting.reduceTimerPrecision", true);') + + lines.append("") + return "\n".join(lines) + + +def persona_env(hard: FirefoxHardening) -> dict[str, str]: + """Environment overrides applied when launching Firefox under a persona. + + Only the timezone needs the OS-level env var (TZ) — Firefox reads it + even when running under Windows. + """ + hard = apply_persona(hard) + if hard.persona is None: + return {} + return {"TZ": hard.persona.timezone} + + +def ensure_firefox_profile( + profile_dir: Path, + proxy_host: str, + proxy_port: int, + hard: FirefoxHardening, +) -> None: + profile_dir.mkdir(parents=True, exist_ok=True) + (profile_dir / "user.js").write_text( + build_user_js(proxy_host, proxy_port, hard), + encoding="utf-8", + ) diff --git a/proxy_chain_manager/chain_map.py b/proxy_chain_manager/chain_map.py new file mode 100644 index 0000000..ccadbf8 --- /dev/null +++ b/proxy_chain_manager/chain_map.py @@ -0,0 +1,367 @@ +"""World-map visualization for proxy chains. + +Renders hop arcs over the bundled neon world map image. Falls back to a +drawn landmass map if the image file is absent. +""" +from __future__ import annotations + +import ipaddress +import logging +import math +import socket +from dataclasses import dataclass +from pathlib import Path +from typing import Literal +from urllib.parse import urlparse + +from PIL import Image, ImageDraw, ImageFilter, ImageFont + +try: + _RESAMPLE = Image.Resampling.LANCZOS +except AttributeError: + _RESAMPLE = Image.LANCZOS # type: ignore[attr-defined] + +from .config import normalize_proxy_url, redact_proxy_url +from .exit_intel import ExitIntel, fetch_ip_geo + +log = logging.getLogger(__name__) + +ChainStatus = Literal["healthy", "connecting", "dead", "idle"] + +# Theme palette +_BG = "#070713" +_GRID = "#1a1f3a" +_LAND = "#12182e" +_LAND_EDGE = "#1e2a4a" +_LINE_HEALTHY = "#00e5ff" +_LINE_CONNECTING = "#ffd000" +_LINE_DEAD = "#ff2a6d" +_LINE_IDLE = "#3052ff" +_YOU = "#00ff9c" +_HOP = "#00e5ff" +_EXIT = "#ffd000" +_LABEL = "#eaf2ff" +_LABEL_DIM = "#7a8aab" +_UNKNOWN = "#2a2f4a" + +def _map_img_path() -> Path: + """Locate world_map.png whether running from source or a frozen PyInstaller bundle.""" + import sys + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + return Path(sys._MEIPASS) / "proxy_chain_manager" / "world_map.png" + return Path(__file__).resolve().parent / "world_map.png" + +_cached_map: Image.Image | None = None + + +def _load_world_map(width: int, height: int) -> Image.Image | None: + """Return the neon world map resized to (width × height), or None if unavailable.""" + global _cached_map + try: + if _cached_map is None: + _cached_map = Image.open(_map_img_path()).convert("RGB") + return _cached_map.resize((width, height), _RESAMPLE) + except Exception as exc: + log.debug("world_map load failed: %s", exc) + return None + + +@dataclass(frozen=True) +class MapPoint: + role: Literal["you", "hop", "exit"] + label: str + lat: float + lon: float + detail: str = "" + hop_index: int = -1 + + +def extract_hop_host(proxy_url: str) -> str | None: + """Return hostname or IP from a proxy URL.""" + p = urlparse(normalize_proxy_url(proxy_url)) + host = (p.hostname or "").strip() + return host or None + + +def resolve_host_ip(host: str, timeout_seconds: float = 4.0) -> str | None: + """Resolve host to IPv4/IPv6 string. Never raises.""" + h = (host or "").strip() + if not h: + return None + try: + ipaddress.ip_address(h) + return h + except ValueError: + pass + try: + prev = socket.getdefaulttimeout() + socket.setdefaulttimeout(max(1.0, float(timeout_seconds))) + try: + infos = socket.getaddrinfo(h, None, type=socket.SOCK_STREAM) + finally: + socket.setdefaulttimeout(prev) + for info in infos: + addr = info[4][0] + try: + ipaddress.ip_address(addr) + return addr + except ValueError: + continue + except Exception as e: + log.debug("resolve_host_ip(%s): %s", h, e) + return None + + +def _loc_label(intel: ExitIntel) -> str: + bits = [b for b in (intel.city, intel.region, intel.country) if b] + return ", ".join(bits) if bits else intel.country_code or intel.ip or "—" + + +def _intel_to_point( + role: Literal["you", "hop", "exit"], + label: str, + intel: ExitIntel, + *, + hop_index: int = -1, +) -> MapPoint | None: + if not intel.ok or intel.lat is None or intel.lon is None: + return None + return MapPoint( + role=role, + label=label, + lat=float(intel.lat), + lon=float(intel.lon), + detail=_loc_label(intel), + hop_index=hop_index, + ) + + +def build_chain_map_points( + hops: list[str], + *, + direct_ip: str = "", + exit_ip: str = "", + exit_intel: ExitIntel | None = None, + timeout_seconds: float = 8.0, +) -> list[MapPoint]: + """Resolve geo for YOU → hop₁ → … → EXIT. Never raises.""" + points: list[MapPoint] = [] + + origin_ip = (direct_ip or "").strip() + if origin_ip and origin_ip != "—": + you = _intel_to_point("you", "YOU", fetch_ip_geo(origin_ip, timeout_seconds)) + if you: + points.append(you) + + for i, hop in enumerate(hops): + host = extract_hop_host(hop) + if not host: + continue + ip = resolve_host_ip(host, timeout_seconds=min(4.0, timeout_seconds)) + if not ip: + continue + intel = fetch_ip_geo(ip, timeout_seconds) + pt = _intel_to_point("hop", f"H{i + 1}", intel, hop_index=i) + if pt: + points.append(pt) + + exit_str = (exit_ip or "").strip() + if exit_str and exit_str != "—": + if ( + exit_intel + and exit_intel.ok + and exit_intel.ip == exit_str + and exit_intel.lat is not None + and exit_intel.lon is not None + ): + pt = _intel_to_point("exit", "EXIT", exit_intel) + else: + pt = _intel_to_point("exit", "EXIT", fetch_ip_geo(exit_str, timeout_seconds)) + if pt: + points.append(pt) + + return points + + +def _project(lat: float, lon: float, width: int, height: int, margin: int) -> tuple[float, float]: + inner_w = max(1, width - margin * 2) + inner_h = max(1, height - margin * 2) + x = margin + (float(lon) + 180.0) / 360.0 * inner_w + y = margin + (90.0 - float(lat)) / 180.0 * inner_h + return x, y + + +def _great_circle_points( + lat1: float, + lon1: float, + lat2: float, + lon2: float, + steps: int = 32, +) -> list[tuple[float, float]]: + """Interpolate along a great circle for curved hop arcs.""" + phi1, lam1 = math.radians(lat1), math.radians(lon1) + phi2, lam2 = math.radians(lat2), math.radians(lon2) + d = 2 * math.asin( + math.sqrt( + math.sin((phi2 - phi1) / 2) ** 2 + + math.cos(phi1) * math.cos(phi2) * math.sin((lam2 - lam1) / 2) ** 2 + ) + ) + if d < 1e-9: + return [(lat1, lon1), (lat2, lon2)] + out: list[tuple[float, float]] = [] + for i in range(steps + 1): + f = i / steps + a = math.sin((1 - f) * d) / math.sin(d) + b = math.sin(f * d) / math.sin(d) + x = a * math.cos(phi1) * math.cos(lam1) + b * math.cos(phi2) * math.cos(lam2) + y = a * math.cos(phi1) * math.sin(lam1) + b * math.cos(phi2) * math.sin(lam2) + z = a * math.sin(phi1) + b * math.sin(phi2) + out.append(( + math.degrees(math.atan2(z, math.sqrt(x * x + y * y))), + math.degrees(math.atan2(y, x)), + )) + return out + + +def _line_color(status: ChainStatus) -> tuple[int, int, int]: + table = { + "healthy": (0, 229, 255), + "connecting": (255, 208, 0), + "dead": (255, 42, 109), + "idle": (48, 82, 255), + } + return table.get(status, (48, 82, 255)) + + +def _node_color(role: str) -> tuple[int, int, int]: + table = {"you": (0, 255, 156), "hop": (0, 229, 255), "exit": (255, 208, 0)} + return table.get(role, (0, 229, 255)) + + +def _hex_to_rgb(h: str) -> tuple[int, int, int]: + h = h.lstrip("#") + return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) + + +def _draw_glow_line( + draw: ImageDraw.ImageDraw, + pts: list[tuple[float, float]], + color: tuple[int, int, int], +) -> None: + """Draw neon glow arc: fat dim outer halo → bright core line.""" + if len(pts) < 2: + return + r, g, b = color + # outer halo — wider, dim + halo = (r // 4, g // 4, b // 4) + draw.line(pts, fill=halo, width=7) + # mid glow + mid = (r // 2, g // 2, b // 2) + draw.line(pts, fill=mid, width=4) + # bright core + draw.line(pts, fill=color, width=2) + + +def _load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + for name in ("segoeui.ttf", "arial.ttf", "DejaVuSans.ttf"): + try: + return ImageFont.truetype(name, size) + except OSError: + continue + return ImageFont.load_default() + + +def _draw_landmasses(draw: ImageDraw.ImageDraw, width: int, height: int, margin: int) -> None: + """Fallback minimal continent silhouettes.""" + blobs: list[list[tuple[float, float]]] = [ + [(-168, 72), (-140, 75), (-100, 72), (-80, 50), (-75, 25), (-95, 15), (-110, 20), (-125, 48), (-168, 72)], + [(-82, 12), (-72, -5), (-55, -35), (-65, -55), (-75, -18), (-82, 12)], + [(-10, 72), (30, 70), (40, 55), (35, 30), (20, 5), (-5, 5), (-18, 28), (-10, 72)], + [(15, 35), (35, 32), (50, 12), (42, -5), (18, -35), (15, 35)], + [(40, 72), (100, 75), (140, 55), (130, 35), (110, 10), (80, 8), (60, 25), (40, 45), (40, 72)], + [(115, -12), (135, -12), (150, -25), (145, -38), (115, -38), (115, -12)], + ] + for poly in blobs: + pts = [_project(lat, lon, width, height, margin) for lat, lon in poly] + draw.polygon(pts, fill=_LAND, outline=_LAND_EDGE) + + +def render_chain_map( + points: list[MapPoint], + *, + width: int = 960, + height: int = 220, + status: ChainStatus = "idle", + margin: int = 12, +) -> Image.Image: + """Render chain hops over the neon world map (or a drawn fallback).""" + w = max(320, width) + h = max(120, height) + + # ── background ────────────────────────────────────────────────────────── + world = _load_world_map(w, h) + if world is not None: + # Slightly darken so overlaid lines pop + bg = Image.new("RGB", (w, h), (0, 0, 0)) + img = Image.blend(world, bg, alpha=0.22) + else: + img = Image.new("RGB", (w, h), _BG) + draw_bg = ImageDraw.Draw(img) + _draw_landmasses(draw_bg, w, h, margin) + # grid + for lat in range(-60, 91, 30): + y = _project(lat, 0, w, h, margin)[1] + draw_bg.line([(margin, y), (w - margin, y)], fill=_GRID, width=1) + for lon in range(-150, 181, 30): + x = _project(0, lon, w, h, margin)[0] + draw_bg.line([(x, margin), (x, h - margin)], fill=_GRID, width=1) + + draw = ImageDraw.Draw(img) + + if len(points) < 2: + font = _load_font(12) + msg = "Start the chain to plot hops on the map." if not points else "Need 2+ geo points to draw path." + # drop shadow + draw.text((margin + 1, h // 2 - 7), msg, fill=(0, 0, 0), font=font) + draw.text((margin, h // 2 - 8), msg, fill=_hex_to_rgb(_LABEL_DIM), font=font) + return img + + line_rgb = _line_color(status) + projected = [_project(p.lat, p.lon, w, h, margin) for p in points] + + # ── arc lines between consecutive hops ───────────────────────────────── + for i in range(len(projected) - 1): + arc = _great_circle_points( + points[i].lat, points[i].lon, + points[i + 1].lat, points[i + 1].lon, + ) + arc_xy = [_project(lat, lon, w, h, margin) for lat, lon in arc] + _draw_glow_line(draw, arc_xy, line_rgb) + + # ── nodes ─────────────────────────────────────────────────────────────── + label_font = _load_font(11) + detail_font = _load_font(9) + for pt, (x, y) in zip(points, projected): + nc = _node_color(pt.role) + r = 7 if pt.role == "exit" else (6 if pt.role == "you" else 5) + # outer glow ring + glow = (nc[0] // 3, nc[1] // 3, nc[2] // 3) + draw.ellipse((x - r - 4, y - r - 4, x + r + 4, y + r + 4), fill=glow) + # filled node with dark center + draw.ellipse((x - r, y - r, x + r, y + r), fill=nc) + draw.ellipse((x - r + 2, y - r + 2, x + r - 2, y + r - 2), fill=(5, 5, 20)) + + tx, ty = int(x + r + 5), int(y - 8) + # text drop-shadow + draw.text((tx + 1, ty + 1), pt.label, fill=(0, 0, 0), font=label_font) + draw.text((tx, ty), pt.label, fill=_hex_to_rgb(_LABEL), font=label_font) + if pt.detail: + draw.text((tx, ty + 12), pt.detail[:30], fill=_hex_to_rgb(_LABEL_DIM), font=detail_font) + + return img + + +def resize_map_display(pil_img: Image.Image, width: int, height: int) -> Image.Image: + """Resize rendered map for the GUI widget.""" + return pil_img.resize((max(320, int(width)), max(120, int(height))), _RESAMPLE) diff --git a/proxy_chain_manager/config.py b/proxy_chain_manager/config.py new file mode 100644 index 0000000..154aefc --- /dev/null +++ b/proxy_chain_manager/config.py @@ -0,0 +1,470 @@ +from __future__ import annotations + +import json +import logging +import shutil +import sys +from dataclasses import asdict, dataclass, field +from pathlib import Path +from urllib.parse import quote, unquote, urlparse, urlunparse + +from .paths import app_data_dir + +log = logging.getLogger(__name__) + +# ── Settings schema version ─────────────────────────────────────────────────── +# Bump this whenever a breaking field rename / type change is made so +# migrate() can upgrade old settings.json files safely. +SETTINGS_SCHEMA_VERSION = 1 + +# !! SECURITY NOTE !! +# settings.json is stored in plaintext at %LOCALAPPDATA%\ProxyChainManager\. +# It may contain proxy credentials (URLs with user:password). Do NOT log +# raw settings dicts — use _mask() to redact credential fields before logging. + + +def _mask(d: dict) -> dict: + """Return a copy of settings dict with credential-bearing fields redacted.""" + redact_keys = {"manual_exit_proxy", "pinned_chain", "sources"} + out: dict = {} + for k, v in d.items(): + if k in redact_keys: + if isinstance(v, list): + out[k] = [redact_proxy_url(str(x)) for x in v] + else: + out[k] = redact_proxy_url(str(v)) + else: + out[k] = v + return out + + +def _host_port_for_url(host: str, port: int | None) -> str: + """``host`` and optional ``port`` as used in proxy URLs (bracket IPv6 literals).""" + if not host: + return "" + if ":" in host and "." not in host: + inner = f"[{host}]" + else: + inner = host + return f"{inner}:{port}" if port else inner + + +# Always bind the local forwarder to loopback. VM clones and NIC/IP changes (DHCP) +# do not affect 127.0.0.1; avoid storing LAN IPs or hostnames for the listener. +LISTEN_HOST = "127.0.0.1" + + +def normalize_proxy_url(raw: str) -> str: + """Strip, add http:// if missing. Empty input → empty string.""" + t = (raw or "").strip() + if not t: + return "" + if "://" not in t: + t = "http://" + t + return t.rstrip("/") + + +def split_proxy_for_edit(raw: str) -> tuple[str, str, str]: + """Split stored proxy URL into (url_without_credentials, username, password). + + Used to populate fixed-exit / form fields. Credentials are percent-decoded. + """ + t = normalize_proxy_url((raw or "").strip()) + if not t: + return "", "", "" + p = urlparse(t) + if not p.hostname: + return t, "", "" + host = p.hostname + port = p.port + scheme = p.scheme or "http" + base = f"{scheme}://{_host_port_for_url(host, port)}" + u = unquote(p.username) if p.username else "" + pw = unquote(p.password) if p.password else "" + return base, u, pw + + +def merge_proxy_credentials(raw_url: str, username: str = "", password: str = "") -> str: + """Build a single proxy URL: host/port from ``raw_url`` (any embedded auth ignored) plus optional auth. + + User and password are URL-encoded for ``@`` and ``:`` inside values. + """ + base = normalize_proxy_url((raw_url or "").strip()) + if not base: + return "" + p = urlparse(base) + if not p.hostname: + return base + host = p.hostname + port = p.port + scheme = p.scheme or "http" + user = (username or "").strip() + pw = (password or "").strip() + if not user and not pw: + # Leave credentials embedded in URL when form fields are blank (paste user:pass@host). + return base + hp = _host_port_for_url(host, port) + netloc = f"{quote(user, safe='')}:{quote(pw, safe='')}@{hp}" + return urlunparse((scheme, netloc, "", "", "", "")).rstrip("/") + + +def parse_exit_proxy_input( + raw: str, default_scheme: str = "socks5" +) -> tuple[str, str, str]: + """Parse any sane paste format into ``(base_url, user, password)``. + + Accepts (whitespace-trimmed, default scheme = SOCKS5 when none given): + + ``host:port`` + ``host:port:user:password`` + ``host:port:user:password:session_or_extra`` (extras folded into password) + ``host:port user password`` (tab/space-separated) + ``user:password@host:port`` + ``scheme://host:port`` + ``scheme://user:password@host:port`` + + User and password may contain ``@``, ``:`` and arbitrary "words" — those + are URL-encoded automatically when ``merge_proxy_credentials`` rebuilds + the URL, so pasting plain text is safe. + """ + t = (raw or "").strip() + if not t: + return "", "", "" + + if "://" in t: + # Already a full URL — defer to the existing splitter. + return split_proxy_for_edit(t) + + if "@" in t: + return split_proxy_for_edit(f"{default_scheme}://{t}") + + if "\t" in t or any(c in t for c in (" ",)): + # "host:port user pass" or "host:port\tuser\tpass" + parts = [p for p in t.replace("\t", " ").split() if p] + if len(parts) >= 3: + hp, user, pw = parts[0], parts[1], " ".join(parts[2:]) + return f"{default_scheme}://{hp}".rstrip("/"), user, pw + if len(parts) == 2: + return f"{default_scheme}://{parts[0]}".rstrip("/"), "", "" + t = parts[0] + + bits = t.split(":") + if len(bits) >= 4: + host, port, user = bits[0], bits[1], bits[2] + password = ":".join(bits[3:]) # passwords containing ":" survive + return f"{default_scheme}://{host}:{port}", user, password + if len(bits) == 2: + return f"{default_scheme}://{bits[0]}:{bits[1]}", "", "" + # 1 token, or 3-token oddity — best-effort; treat as host[:port] only. + return f"{default_scheme}://{t}".rstrip("/"), "", "" + + +def redact_proxy_url(url: str) -> str: + """Same URL shape for logs/UI, with credentials replaced by ``***``.""" + t = (url or "").strip() + if not t: + return "" + if "://" not in t: + t = "http://" + t + p = urlparse(t.rstrip("/")) + if not p.hostname: + return t + has_auth = bool(p.username) or bool(p.password) + if not has_auth: + return t.rstrip("/") + scheme = p.scheme or "http" + hp = _host_port_for_url(p.hostname, p.port) + netloc = f"***:***@{hp}" + return urlunparse((scheme, netloc, "", "", "", "")).rstrip("/") + + +OBFUSCATION_MODES = ["auto", "http_only", "socks5_only", "random_mix"] +OBFUSCATION_LABELS = { + "auto": "Auto (best available)", + "http_only": "HTTP only", + "socks5_only": "SOCKS5 only", + "random_mix": "Random Mix (max noise)", +} + + +@dataclass +class Settings: + # Schema version — used by migrate() to upgrade old settings.json files. + settings_version: int = SETTINGS_SCHEMA_VERSION + + # ── network ────────────────────────────────────────────────────────── + local_host: str = LISTEN_HOST + local_port: int = 18888 + + # ── chain ───────────────────────────────────────────────────────────── + chain_length: int = 3 # number of hops (2-8) + obfuscation_mode: str = "auto" # see OBFUSCATION_MODES + use_pinned_chain: bool = True # use manually ordered chain from Chain Builder + pinned_chain: list[str] = field(default_factory=list) # user-ordered hop list + validate_pinned_on_start: bool = True # quick TCP probe of each pinned hop before chaining + max_browser_relaunches: int = 5 # cap auto-relaunch attempts so a broken profile doesn't loop + # Fixed last hop only (ignored when use_pinned_chain is True — full manual chain wins) + manual_exit_proxy: str = "" + + # ── timing ──────────────────────────────────────────────────────────── + health_check_seconds: int = 180 # how often to re-verify exit IP + full_refresh_seconds: int = 1800 # how often to re-fetch/re-validate pool + + # ── validation ──────────────────────────────────────────────────────── + validation_concurrency: int = 64 + max_candidates: int = 200 # cap before validation (shuffled, so random sample) + min_pool_size: int = 20 # stop validating once this many good proxies found + validation_timeout_seconds: float = 12.0 + prefer_elite: bool = False + + # ── security ────────────────────────────────────────────────────────── + kill_switch_enabled: bool = sys.platform == "win32" # Windows Firewall kill-switch when available + proxy_bypass: str = "localhost;127.*;10.*;192.168.*;" + + # ── privacy / device hardening (Privacy tab) ─────────────────────────── + mac_spoof_enabled: bool = False # randomize NIC MAC while chain runs (Admin) + mac_rotate_on_chain_rotate: bool = False # re-randomize MAC on every chain rotation + spoof_hostname_enabled: bool = False # temporary computer name while chain runs (Admin) + flush_dns_on_rotate: bool = True # ipconfig /flushdns on each rotation + disable_ipv6_while_active: bool = False # disable IPv6 bindings while chain runs (Admin) + harden_webrtc_enabled: bool = False # Chrome/Edge WebRTC policy (Admin) + lan_lockdown_enabled: bool = False # kill LLMNR/NetBIOS/mDNS while chain runs + telemetry_kill_enabled: bool = False # disable DiagTrack + Activity History (Admin) + + # ── hardened browser ──────────────────────────────────────────────────── + firefox_path: str = "" + firefox_profile_dir: str = "" + browser_clear_on_close: bool = True + browser_disposable_profile: bool = False + browser_kill_on_chain_drop: bool = True + browser_auto_relaunch: bool = False + browser_lock_managed_profile: bool = True + browser_force_proxy: bool = True + browser_disable_webrtc: bool = True + browser_resist_fingerprinting: bool = True + browser_disable_telemetry: bool = True + browser_first_party_isolation: bool = True + browser_strict_tracking_protection: bool = True + browser_timezone_utc: bool = True + browser_persona: str = "blend_windows_chrome" + browser_cookie_mode: str = "block_third_party" + + # ── sources ─────────────────────────────────────────────────────────── + sources: list[str] = field( + default_factory=lambda: [ + "https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/protocols/http/data.json", + "https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/protocols/socks5/data.json", + "https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/protocols/https/data.json", + ] + ) + ip_check_url: str = "https://api.ipify.org?format=json" + + def listen_addr(self) -> str: + return f"{self.local_host}:{self.local_port}" + + def settings_path(self) -> Path: + return app_data_dir() / "settings.json" + + +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 != "https": + return False + host = p.hostname or "" + if not host: + return False + import ipaddress as _ip + try: + addr = _ip.ip_address(host) + return not (addr.is_private or addr.is_loopback or addr.is_link_local) + except ValueError: + pass + return True + except Exception: + return False + + +def migrate(raw: dict) -> dict: + """Upgrade a raw settings dict from any schema version to the current one. + + Each version block transforms ``raw`` in-place and bumps + ``settings_version``. New fields are left absent so the dataclass + default takes effect. + """ + ver = int(raw.get("settings_version", 0)) + # v0 → v1: no structural changes; just stamp the version + if ver < 1: + raw["settings_version"] = 1 + return raw + + +def sanitize_settings(s: Settings) -> tuple[Settings, bool]: + """Clamp invalid values from hand-edited JSON. Returns (settings, changed).""" + changed = False + try: + cl = int(s.chain_length) + if not (1 <= cl <= 8): + s.chain_length = max(1, min(8, cl)) + changed = True + except (TypeError, ValueError): + s.chain_length = 3 + changed = True + try: + p = int(s.local_port) + if not (1 <= p <= 65535): + s.local_port = max(1, min(65535, p)) + changed = True + except (TypeError, ValueError): + s.local_port = 18888 + changed = True + try: + hc = int(s.health_check_seconds) + if hc < 10: + s.health_check_seconds = 10 + changed = True + elif hc > 3600: + s.health_check_seconds = 3600 + changed = True + except (TypeError, ValueError): + s.health_check_seconds = 180 + changed = True + try: + fr = int(s.full_refresh_seconds) + if fr < 60: + s.full_refresh_seconds = 60 + changed = True + elif fr > 86400: + s.full_refresh_seconds = 86400 + changed = True + except (TypeError, ValueError): + s.full_refresh_seconds = 1800 + changed = True + try: + if int(s.validation_concurrency) < 1: + s.validation_concurrency = 1 + changed = True + except (TypeError, ValueError): + s.validation_concurrency = 64 + changed = True + try: + if int(s.max_candidates) < 10: + s.max_candidates = 10 + changed = True + except (TypeError, ValueError): + s.max_candidates = 200 + changed = True + try: + mps = int(s.min_pool_size) + if mps < 3: + s.min_pool_size = 3 + changed = True + elif mps > 500: + s.min_pool_size = 500 + changed = True + except (TypeError, ValueError): + s.min_pool_size = 20 + changed = True + if not hasattr(s, "min_pool_size"): + s.min_pool_size = 20 + changed = True + try: + vt = float(s.validation_timeout_seconds) + if vt < 2.0: + s.validation_timeout_seconds = 2.0 + changed = True + elif vt > 120.0: + s.validation_timeout_seconds = 120.0 + changed = True + except (TypeError, ValueError): + s.validation_timeout_seconds = 12.0 + changed = True + # Empty list survives "all(isinstance…)" — would skip all fetches and yield an empty pool. + if not isinstance(s.sources, list) or not all(isinstance(x, str) for x in s.sources) or len(s.sources) == 0: + s.sources = list(Settings().sources) + changed = True + else: + safe = [u for u in s.sources if _is_safe_https_url(u)] + if len(safe) < len(s.sources): + log.warning("Removed %d unsafe source URL(s) from settings.", len(s.sources) - len(safe)) + s.sources = safe if safe else list(Settings().sources) + changed = True + if s.ip_check_url and not _is_safe_https_url(s.ip_check_url): + log.warning("ip_check_url looks unsafe (%s) — reverting to default.", s.ip_check_url) + s.ip_check_url = Settings().ip_check_url + changed = True + return s, changed + + +def normalize_listen_host(s: Settings) -> tuple[Settings, bool]: + """Force loopback bind so LAN/DHCP IP changes (e.g. cloned VMs) never break the listener.""" + if s.local_host == LISTEN_HOST: + return s, False + s.local_host = LISTEN_HOST + return s, True + + +def load_settings() -> Settings: + p = app_data_dir() / "settings.json" + if not p.is_file(): + s = Settings() + if sys.platform != "win32": + s.kill_switch_enabled = False + s.mac_spoof_enabled = False + s.mac_rotate_on_chain_rotate = False + s.spoof_hostname_enabled = False + s.disable_ipv6_while_active = False + s.harden_webrtc_enabled = False + s.lan_lockdown_enabled = False + s.telemetry_kill_enabled = False + return s + try: + raw = json.loads(p.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + corrupt = p.with_suffix(".json.corrupt") + log.warning("settings.json unreadable (%s) — backing up to %s and using defaults.", exc, corrupt) + try: + shutil.copy2(p, corrupt) + except OSError: + pass + return Settings() + raw = migrate(raw) + s = Settings() + for k, v in raw.items(): + if hasattr(s, k): + setattr(s, k, v) + if ( + not isinstance(s.sources, list) + or not all(isinstance(x, str) for x in s.sources) + or len(s.sources) == 0 + ): + s.sources = list(Settings().sources) + if s.obfuscation_mode not in OBFUSCATION_MODES: + s.obfuscation_mode = "auto" + if not isinstance(s.pinned_chain, list): + s.pinned_chain = [] + if not hasattr(s, "manual_exit_proxy") or not isinstance(s.manual_exit_proxy, str): + s.manual_exit_proxy = "" + s, ch1 = sanitize_settings(s) + s, ch2 = normalize_listen_host(s) + changed = ch1 or ch2 + if changed: + try: + p.write_text(json.dumps(asdict(s), indent=2), encoding="utf-8") + except OSError: + pass + return s + + +def save_settings(s: Settings) -> None: + s, _ = sanitize_settings(s) + s, _ = normalize_listen_host(s) + p = app_data_dir() / "settings.json" + # Backup before overwriting so a crash mid-write doesn't corrupt settings. + if p.is_file(): + try: + shutil.copy2(p, p.with_suffix(".json.bak")) + except OSError as exc: + log.warning("Could not back up settings.json: %s", exc) + p.write_text(json.dumps(asdict(s), indent=2), encoding="utf-8") diff --git a/proxy_chain_manager/dns_leak.py b/proxy_chain_manager/dns_leak.py new file mode 100644 index 0000000..20d1b63 --- /dev/null +++ b/proxy_chain_manager/dns_leak.py @@ -0,0 +1,335 @@ +"""DNS leak detection and cache flushing. + +Two levels of testing: + 1. check_dns_leak_hint() — fast, no network: inspect configured DNS resolvers + 2. run_dns_leak_test() — real network test: compare DNS answers seen through + the proxy vs direct, and probe resolver identity via + a dedicated leak-test API. +""" +from __future__ import annotations + +import concurrent.futures +import ipaddress +import logging +import socket +import subprocess +import sys +from dataclasses import dataclass, field + +import httpx + +log = logging.getLogger(__name__) + +# ─── DoH endpoint used to ask "what is my apparent source IP?" ────────────── +_DOH_IP_CHECK = "https://api.ipify.org?format=json" +_DNS_LEAK_API = "https://dnsleaktest.com/api/v1/start" +_DNS_LEAK_RESULT = "https://dnsleaktest.com/api/v1/results" + +# Well-known public resolver IP ranges (prefix → label) +_PUBLIC_DNS_LABELS: dict[str, str] = { + "8.8.8.8": "Google", "8.8.4.4": "Google", + "1.1.1.1": "Cloudflare", "1.0.0.1": "Cloudflare", + "9.9.9.9": "Quad9", "149.112.112.112": "Quad9", + "208.67.222.222": "OpenDNS", "208.67.220.220": "OpenDNS", + "76.76.2.0": "Alternate DNS", + "94.140.14.14": "AdGuard", +} + +# ───────────────────────────────────────────────────────────────────────────── + +@dataclass +class DnsLeakResult: + """Legacy single-check result kept for API compatibility.""" + ok: bool + system_resolvers: list[str] + message: str + doh_ip: str | None = None + + +@dataclass +class DnsResolverInfo: + ip: str + hostname: str = "" + country: str = "" + isp: str = "" + label: str = "" # "Google", "Cloudflare", or "" + is_public_known: bool = False + + +@dataclass +class FullDnsLeakReport: + """Comprehensive DNS leak report from run_dns_leak_test().""" + # Resolvers that answered DNS queries in this session + resolvers_seen: list[DnsResolverInfo] = field(default_factory=list) + # Resolvers configured by the OS + system_resolvers: list[str] = field(default_factory=list) + # IPs resolved for test hostnames — via proxy vs direct + proxy_resolution: dict[str, list[str]] = field(default_factory=dict) + direct_resolution: dict[str, list[str]] = field(default_factory=dict) + # True when proxy and direct give the same answers (possible leak) + resolution_matches_direct: bool = False + # Whether any configured resolver is a known public server + has_public_resolver: bool = False + # Whether any configured resolver is outside local network + has_external_resolver: bool = False + # Overall verdict + leaked: bool = False + summary: str = "" + errors: list[str] = field(default_factory=list) + + +# ───────────────────────────────────────────────────────────────────────────── +# System resolver detection +# ───────────────────────────────────────────────────────────────────────────── + +def get_system_dns_servers() -> list[str]: + """Return configured IPv4 DNS resolvers via PowerShell (falls back to ipconfig).""" + if sys.platform == "darwin": + try: + r = subprocess.run(["scutil", "--dns"], capture_output=True, text=True, timeout=10) + servers: list[str] = [] + for line in (r.stdout or "").splitlines(): + line = line.strip() + if line.startswith("nameserver["): + ip = line.split(":", 1)[-1].strip() + if ip and ip not in servers: + servers.append(ip) + return servers + except Exception: + return [] + try: + r = subprocess.run( + ["powershell", "-NoProfile", "-Command", + "(Get-DnsClientServerAddress -AddressFamily IPv4 | " + "Where-Object { $_.ServerAddresses } | " + "Select-Object -ExpandProperty ServerAddresses) -join ','"], + capture_output=True, text=True, timeout=12, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + raw = (r.stdout or "").strip() + if raw: + return [x.strip() for x in raw.replace(";", ",").split(",") if x.strip()] + except Exception as exc: + log.debug("PowerShell DNS server query failed (will try ipconfig): %s", exc) + + # ipconfig fallback + try: + r = subprocess.run( + ["ipconfig", "/all"], capture_output=True, text=True, timeout=10, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + servers: list[str] = [] + for line in (r.stdout or "").splitlines(): + if "DNS Servers" in line or "DNS Server" in line: + parts = line.split(":", 1) + if len(parts) == 2: + ip = parts[1].strip() + if ip: + servers.append(ip) + return servers + except Exception: + return [] + + +def flush_dns_cache() -> tuple[bool, str]: + if sys.platform == "darwin": + try: + subprocess.run(["dscacheutil", "-flushcache"], capture_output=True, timeout=15) + subprocess.run(["killall", "-HUP", "mDNSResponder"], capture_output=True, timeout=15) + return True, "macOS DNS cache flushed" + except Exception as e: + return False, str(e) + try: + r = subprocess.run( + ["ipconfig", "/flushdns"], capture_output=True, text=True, timeout=15, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + lines = (r.stdout or r.stderr or "").strip().splitlines() + msg = lines[-1] if lines else "flushed" + return r.returncode == 0, msg + except Exception as e: + return False, str(e) + + +# ───────────────────────────────────────────────────────────────────────────── +# Hostname resolution helpers +# ───────────────────────────────────────────────────────────────────────────── + +def _resolve_direct(hostname: str, timeout: float = 4.0) -> list[str]: + """Resolve hostname using system DNS (direct, not through proxy).""" + _prev = socket.getdefaulttimeout() + try: + socket.setdefaulttimeout(timeout) + infos = socket.getaddrinfo(hostname, None) + seen: list[str] = [] + for info in infos: + addr = info[4][0] + if addr and addr not in seen: + seen.append(addr) + return seen + except Exception: + return [] + finally: + socket.setdefaulttimeout(_prev) + + +def _resolve_via_proxy(hostname: str, proxy_url: str, timeout: float = 8.0) -> list[str]: + """Resolve hostname by fetching a DNS-over-HTTPS endpoint through the proxy.""" + # Use Google DoH JSON API through the proxy so DNS is resolved on the exit node + doh = f"https://dns.google/resolve?name={hostname}&type=A" + try: + with httpx.Client(proxy=proxy_url, timeout=timeout, verify=False, follow_redirects=True) as c: + r = c.get(doh) + if r.status_code == 200: + data = r.json() + return [ans["data"] for ans in (data.get("Answer") or []) + if ans.get("type") == 1] + except Exception as exc: + log.debug("DoH DNS leak query failed: %s", exc) + return [] + + +def _is_private(ip: str) -> bool: + try: + return ipaddress.ip_address(ip).is_private + except Exception: + return False + + +# ───────────────────────────────────────────────────────────────────────────── +# Main leak test +# ───────────────────────────────────────────────────────────────────────────── + +_LEAK_TEST_HOSTS = [ + "google.com", + "cloudflare.com", + "github.com", + "amazon.com", +] + + +def run_dns_leak_test( + proxy_url: str | None = None, + timeout: float = 10.0, +) -> FullDnsLeakReport: + """ + Comprehensive DNS leak test: + - Collect system DNS resolver configuration + - Resolve test hostnames both directly (system DNS) and via proxy (DoH through chain) + - Compare answers: matching answers suggest DNS is NOT going through the proxy + - Flag known public resolvers that would bypass the chain + """ + rep = FullDnsLeakReport() + rep.system_resolvers = get_system_dns_servers() + + _PRIVATE_PREFIXES = ( + "127.", "10.", "192.168.", "172.16.", "172.17.", "172.18.", + "172.19.", "172.2", "172.30.", "172.31.", "169.254.", + ) + + for r_ip in rep.system_resolvers: + is_public_known = r_ip in _PUBLIC_DNS_LABELS + is_external = not any(r_ip.startswith(p) for p in _PRIVATE_PREFIXES) + label = _PUBLIC_DNS_LABELS.get(r_ip, "") + rep.has_public_resolver = rep.has_public_resolver or is_public_known + rep.has_external_resolver = rep.has_external_resolver or is_external + rep.resolvers_seen.append(DnsResolverInfo( + ip=r_ip, + label=label, + is_public_known=is_public_known, + )) + + # Resolve test hosts in parallel + all_match = True + any_resolved_via_proxy = False + + def _test_host(host: str) -> tuple[str, list[str], list[str]]: + direct = _resolve_direct(host, timeout=min(4.0, timeout)) + proxy = _resolve_via_proxy(host, proxy_url, timeout=timeout) if proxy_url else [] + return host, direct, proxy + + try: + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool: + futs = {pool.submit(_test_host, h): h for h in _LEAK_TEST_HOSTS} + for fut in concurrent.futures.as_completed(futs, timeout=timeout + 2): + try: + host, direct, proxy = fut.result() + rep.direct_resolution[host] = direct + rep.proxy_resolution[host] = proxy + if proxy: + any_resolved_via_proxy = True + # Overlap: if proxy and direct gave the same IPs, DNS + # might not be going through the proxy (same resolver path) + direct_set = set(direct) + proxy_set = set(proxy) + if direct_set and proxy_set and direct_set == proxy_set: + pass # this host matches + else: + all_match = False + except Exception as e: + rep.errors.append(str(e)) + except Exception as e: + rep.errors.append(f"Thread pool error: {e}") + + rep.resolution_matches_direct = all_match and any_resolved_via_proxy + + # ── Build verdict ────────────────────────────────────────────────────── + problems: list[str] = [] + + if rep.has_external_resolver: + external = [r for r in rep.system_resolvers + if not any(r.startswith(p) for p in _PRIVATE_PREFIXES)] + names = [f"{ip} ({_PUBLIC_DNS_LABELS[ip]})" if ip in _PUBLIC_DNS_LABELS else ip + for ip in external[:4]] + problems.append(f"OS DNS: {', '.join(names)} — DNS may bypass proxy chain") + + if rep.resolution_matches_direct and proxy_url: + problems.append("Proxy DNS resolution matches direct — possible DNS leak (same upstream resolver)") + + if problems: + rep.leaked = True + rep.summary = " · ".join(problems) + else: + rep.leaked = False + if proxy_url: + rep.summary = "DNS routing looks clean — proxy resolves differently from direct." + else: + resolver_str = ", ".join(rep.system_resolvers[:3]) if rep.system_resolvers else "none detected" + rep.summary = f"System resolvers: {resolver_str}" + + return rep + + +# ───────────────────────────────────────────────────────────────────────────── +# Legacy quick hint (used by Privacy tab DNS section) +# ───────────────────────────────────────────────────────────────────────────── + +def check_dns_leak_hint(local_proxy: str | None = None) -> DnsLeakResult: + """Fast heuristic: inspect configured DNS resolvers, flag public ones.""" + resolvers = get_system_dns_servers() + private_prefixes = ( + "127.", "10.", "192.168.", "172.16.", "172.17.", "172.18.", + "172.19.", "172.2", "172.30.", "172.31.", + ) + public = [r for r in resolvers if not any(r.startswith(p) for p in private_prefixes)] + + if not resolvers: + return DnsLeakResult(ok=True, system_resolvers=[], message="No IPv4 DNS servers reported (DHCP).") + + if public and local_proxy: + return DnsLeakResult( + ok=False, system_resolvers=resolvers, + message=( + f"DNS may bypass proxy chain: public resolvers {', '.join(public)}. " + "Use kill-switch + VPN, or set DNS to localhost when hardened." + ), + ) + if public: + return DnsLeakResult( + ok=False, system_resolvers=resolvers, + message=f"Public DNS resolvers active: {', '.join(public)}", + ) + return DnsLeakResult( + ok=True, system_resolvers=resolvers, + message=f"DNS servers: {', '.join(resolvers)}", + ) diff --git a/proxy_chain_manager/exit_intel.py b/proxy_chain_manager/exit_intel.py new file mode 100644 index 0000000..857d9d3 --- /dev/null +++ b/proxy_chain_manager/exit_intel.py @@ -0,0 +1,226 @@ +"""Exit IP intelligence: geo, ASN, datacenter heuristics. + +Designed to be called through the local chain proxy so the lookup itself +flows over the active path (no out-of-band leak). Uses ip-api.com (free, +no key) with a fallback to ipwho.is. Both return geo + ASN. +""" +from __future__ import annotations + +import logging +from dataclasses import dataclass +from typing import Any + +import httpx + +log = logging.getLogger(__name__) + +# Common datacenter / hosting ASN keywords. Used as a quick heuristic to +# warn the operator before signup (residential ASNs are far less likely to +# trip Google/Proton anti-fraud than DC ranges). +_DC_KEYWORDS = ( + "amazon", "aws", "google", "microsoft", "azure", "digitalocean", + "linode", "ovh", "hetzner", "vultr", "choopa", "scaleway", + "leaseweb", "contabo", "hostinger", "godaddy", "namecheap", + "cloudflare", "fastly", "akamai", "datacamp", "m247", + "psychz", "quadranet", "colocrossing", "rackspace", + "alibaba", "tencent", "huawei", "online s.a.s", + "wholesale", "datacenter", "data center", "hosting", + "server", "cloud", "vps", +) + + +@dataclass +class ExitIntel: + ok: bool + ip: str = "" + country: str = "" + country_code: str = "" + city: str = "" + region: str = "" + timezone: str = "" + lat: float | None = None + lon: float | None = None + asn: str = "" + org: str = "" + isp: str = "" + is_datacenter: bool = False + is_mobile: bool = False + is_proxy_flagged: bool = False + source: str = "" + detail: str = "" + + def summary(self) -> str: + if not self.ok: + return f"unknown — {self.detail}" + loc_bits = [b for b in (self.city, self.region, self.country) if b] + loc = ", ".join(loc_bits) if loc_bits else "—" + tags: list[str] = [] + if self.is_datacenter: + tags.append("DATACENTER") + if self.is_mobile: + tags.append("MOBILE") + if self.is_proxy_flagged: + tags.append("PROXY-FLAGGED") + tag_str = f" [{' · '.join(tags)}]" if tags else "" + asn_str = f" AS{self.asn}" if self.asn else "" + return f"{self.ip} {loc}{asn_str}{tag_str}" + + +def _looks_dc(org: str, isp: str) -> bool: + haystack = f"{org} {isp}".lower() + return any(k in haystack for k in _DC_KEYWORDS) + + +def _parse_ipapi(payload: dict[str, Any]) -> ExitIntel: + if (payload.get("status") or "").lower() != "success": + return ExitIntel(ok=False, detail=str(payload.get("message") or "ip-api error")) + asn_raw = str(payload.get("as") or "") + asn = asn_raw.split()[0].lstrip("AS").strip() if asn_raw else "" + org = str(payload.get("org") or payload.get("isp") or "") + isp = str(payload.get("isp") or "") + lat_raw, lon_raw = payload.get("lat"), payload.get("lon") + return ExitIntel( + ok=True, + ip=str(payload.get("query") or ""), + country=str(payload.get("country") or ""), + country_code=str(payload.get("countryCode") or ""), + city=str(payload.get("city") or ""), + region=str(payload.get("regionName") or ""), + timezone=str(payload.get("timezone") or ""), + lat=float(lat_raw) if lat_raw is not None else None, + lon=float(lon_raw) if lon_raw is not None else None, + asn=asn, + org=org, + isp=isp, + is_datacenter=bool(payload.get("hosting")) or _looks_dc(org, isp), + is_mobile=bool(payload.get("mobile")), + is_proxy_flagged=bool(payload.get("proxy")), + source="ip-api.com", + detail="ok", + ) + + +def _parse_ipwhois(payload: dict[str, Any]) -> ExitIntel: + if not payload.get("success", True): + return ExitIntel(ok=False, detail=str(payload.get("message") or "ipwho.is error")) + conn = payload.get("connection") or {} + asn_val = conn.get("asn") + asn = str(asn_val) if asn_val is not None else "" + org = str(conn.get("org") or "") + isp = str(conn.get("isp") or "") + lat_raw, lon_raw = payload.get("latitude"), payload.get("longitude") + return ExitIntel( + ok=True, + ip=str(payload.get("ip") or ""), + country=str(payload.get("country") or ""), + country_code=str(payload.get("country_code") or ""), + city=str(payload.get("city") or ""), + region=str(payload.get("region") or ""), + timezone=str((payload.get("timezone") or {}).get("id") or ""), + lat=float(lat_raw) if lat_raw is not None else None, + lon=float(lon_raw) if lon_raw is not None else None, + asn=asn, + org=org, + isp=isp, + is_datacenter=_looks_dc(org, isp), + is_mobile=False, + is_proxy_flagged=False, + source="ipwho.is", + detail="ok", + ) + + +_IPAPI_FIELDS = ( + "status,message,country,countryCode,regionName,city,lat,lon,timezone," + "isp,org,as,mobile,proxy,hosting,query" +) + + +def _fetch_intel( + *, + proxy_url: str | None, + timeout_seconds: float, +) -> ExitIntel: + """Shared ip-api / ipwho.is lookup. ``proxy_url=None`` uses a direct connection.""" + timeout = httpx.Timeout(timeout_seconds, connect=min(6.0, timeout_seconds)) + sources = [ + (f"http://ip-api.com/json/?fields={_IPAPI_FIELDS}", _parse_ipapi), + ("https://ipwho.is/", _parse_ipwhois), + ] + last_err = "" + try: + with httpx.Client( + proxy=proxy_url, + timeout=timeout, + verify=False, + follow_redirects=True, + headers={"User-Agent": "Mozilla/5.0"}, + ) as c: + for url, parser in sources: + try: + r = c.get(url) + if r.status_code != 200 or not r.content: + last_err = f"{url} -> HTTP {r.status_code}" + continue + data = r.json() + out = parser(data) + if out.ok: + return out + last_err = out.detail + except Exception as e: + last_err = f"{type(e).__name__}: {e}" + continue + except Exception as e: + return ExitIntel(ok=False, detail=f"{type(e).__name__}: {e}") + return ExitIntel(ok=False, detail=last_err or "no source responded") + + +def fetch_exit_intel(proxy_url: str, timeout_seconds: float = 12.0) -> ExitIntel: + """Look up exit IP geo/ASN/datacenter through the given proxy. + + Returns an ExitIntel with ok=False and detail set on failure. Never raises. + """ + return _fetch_intel(proxy_url=proxy_url, timeout_seconds=timeout_seconds) + + +def fetch_ip_geo(ip: str, timeout_seconds: float = 8.0) -> ExitIntel: + """Direct geo lookup for a specific IP (used for hop / origin mapping). + + Never raises. + """ + target = (ip or "").strip() + if not target: + return ExitIntel(ok=False, detail="empty ip") + timeout = httpx.Timeout(timeout_seconds, connect=min(5.0, timeout_seconds)) + sources = [ + ( + f"http://ip-api.com/json/{target}?fields={_IPAPI_FIELDS}", + _parse_ipapi, + ), + (f"https://ipwho.is/{target}", _parse_ipwhois), + ] + last_err = "" + try: + with httpx.Client( + timeout=timeout, + verify=False, + follow_redirects=True, + headers={"User-Agent": "Mozilla/5.0"}, + ) as c: + for url, parser in sources: + try: + r = c.get(url) + if r.status_code != 200 or not r.content: + last_err = f"{url} -> HTTP {r.status_code}" + continue + data = r.json() + out = parser(data) + if out.ok: + return out + last_err = out.detail + except Exception as e: + last_err = f"{type(e).__name__}: {e}" + continue + except Exception as e: + return ExitIntel(ok=False, detail=f"{type(e).__name__}: {e}") + return ExitIntel(ok=False, detail=last_err or "no source responded") diff --git a/proxy_chain_manager/fetcher.py b/proxy_chain_manager/fetcher.py new file mode 100644 index 0000000..5791819 --- /dev/null +++ b/proxy_chain_manager/fetcher.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import logging +from typing import Any + +import httpx + +log = logging.getLogger(__name__) + + +def fetch_proxy_json(url: str, timeout: float = 45.0) -> list[dict[str, Any]]: + log.debug("fetch_proxy_json: GET %s", (url[:100] + "…") if len(url) > 100 else url) + with httpx.Client(timeout=timeout, follow_redirects=True) as c: + r = c.get(url) + r.raise_for_status() + data = r.json() + if not isinstance(data, list): + return [] + return [x for x in data if isinstance(x, dict)] + + +def normalize_entries(rows: list[dict[str, Any]], prefer_elite: bool) -> list[str]: + out: list[str] = [] + for row in rows: + if prefer_elite and str(row.get("anonymity", "")).lower() != "elite": + continue + p = row.get("proxy") + if isinstance(p, str) and "://" in p: + out.append(p.strip()) + # de-dupe preserving order + seen: set[str] = set() + uniq: list[str] = [] + for u in out: + if u not in seen: + seen.add(u) + uniq.append(u) + return uniq diff --git a/proxy_chain_manager/fingerprint.py b/proxy_chain_manager/fingerprint.py new file mode 100644 index 0000000..05ffcf7 --- /dev/null +++ b/proxy_chain_manager/fingerprint.py @@ -0,0 +1,416 @@ +"""Device fingerprint audit and OS-level hardening helpers.""" +from __future__ import annotations + +import logging +import platform +import random +import re +import string +import subprocess +import winreg +from dataclasses import dataclass, field + +from .firewall import is_admin +from .mac_spoof import list_nics +from .win_compat import probe as _win_probe + +log = logging.getLogger(__name__) + +_WEBRTC_CHROME = r"SOFTWARE\Policies\Google\Chrome" +_WEBRTC_EDGE = r"SOFTWARE\Policies\Microsoft\Edge" +# Legacy DWORD key — value 3 = disable_non_proxied_udp (value 2 was wrong: public+private only) +_WEBRTC_VALUE = "DefaultWebRtcIpHandlingPolicy" +_WEBRTC_DISABLE = 3 # disable_non_proxied_udp (correct Chrome/Edge DWORD) +# Modern REG_SZ key required by Chrome 114+ Group Policy +_WEBRTC_VALUE_STR = "WebRtcIPHandling" +_WEBRTC_DISABLE_STR = "disable_non_proxied_udp" + + +@dataclass +class FingerprintAudit: + lines: list[str] = field(default_factory=list) + hostname: str = "" + machine_guid: str = "" + username: str = "" + macs: list[str] = field(default_factory=list) + + +def _run_ps(script: str, timeout: float = 15.0) -> str: + try: + r = subprocess.run( + ["powershell", "-NoProfile", "-Command", script], + capture_output=True, + text=True, + timeout=timeout, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + return (r.stdout or "").strip() + except Exception as e: + log.debug("fingerprint ps: %s", e) + return "" + + +def get_computer_name() -> str: + try: + import os + return os.environ.get("COMPUTERNAME", "") or platform.node() or "" + except Exception: + return "" + + +def get_machine_guid() -> str: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\Cryptography", + ) as key: + val, _ = winreg.QueryValueEx(key, "MachineGuid") + return str(val) + except OSError: + return "" + + +def random_hostname(prefix: str = "PC") -> str: + suffix = "".join(random.choices(string.ascii_uppercase + string.digits, k=7)) + return f"{prefix}-{suffix}"[:15] + + +def set_computer_name(name: str) -> tuple[bool, str]: + """Set NetBIOS / computer name (Admin). Reboot may be required for all apps.""" + if not is_admin(): + return False, "Administrator required to change computer name." + name = re.sub(r"[^A-Za-z0-9\-]", "", name)[:15] + if len(name) < 1: + return False, "Invalid hostname." + code = subprocess.run( + ["powershell", "-NoProfile", "-Command", + f'Rename-Computer -NewName "{name}" -Force -ErrorAction Stop'], + capture_output=True, + text=True, + timeout=30, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ).returncode + if code != 0: + # NetBIOS name via WMI (often works without full rename) + wmi = ( + f'$n = Get-WmiObject Win32_ComputerSystem; ' + f'$r = $n.Rename("{name}"); if ($r.ReturnValue -ne 0) {{ exit $r.ReturnValue }}' + ) + code = subprocess.run( + ["powershell", "-NoProfile", "-Command", wmi], + capture_output=True, + text=True, + timeout=30, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ).returncode + if code == 0: + return True, f"Computer name set to {name} (some apps need reconnect/reboot)." + return False, "Could not change computer name." + + +def disable_ipv6_on_adapters() -> tuple[list[str], list[str]]: + """Disable IPv6 binding on up physical adapters. Returns (adapter names, log lines).""" + if not is_admin(): + return [], ["IPv6 disable skipped (not Admin)."] + if not _win_probe().has_net_cmdlets: + return [], [ + "IPv6 disable skipped — requires PowerShell 3.0+ " + "(Windows 8 / Server 2012+). Detected legacy PowerShell." + ] + script = ( + "Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | " + "ForEach-Object { $_.Name }" + ) + names = [n.strip() for n in _run_ps(script).splitlines() if n.strip()] + logs: list[str] = [] + changed: list[str] = [] + skip = ("virtual", "vmware", "hyper-v", "loopback", "bluetooth") + for name in names: + if any(h in name.lower() for h in skip): + continue + cmd = ( + f'Disable-NetAdapterBinding -Name "{name}" -ComponentID ms_tcpip6 -Confirm:$false ' + f'-ErrorAction SilentlyContinue' + ) + subprocess.run( + ["powershell", "-NoProfile", "-Command", cmd], + capture_output=True, + timeout=20, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + changed.append(name) + logs.append(f"IPv6 disabled on {name}") + return changed, logs + + +def enable_ipv6_on_adapters(adapters: list[str]) -> list[str]: + if not is_admin() or not adapters: + return [] + if not _win_probe().has_net_cmdlets: + return [] + logs: list[str] = [] + for name in adapters: + cmd = ( + f'Enable-NetAdapterBinding -Name "{name}" -ComponentID ms_tcpip6 -Confirm:$false ' + f'-ErrorAction SilentlyContinue' + ) + subprocess.run( + ["powershell", "-NoProfile", "-Command", cmd], + capture_output=True, + timeout=20, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + logs.append(f"IPv6 re-enabled on {name}") + return logs + + +def apply_webrtc_hardening(enable: bool) -> tuple[bool, str]: + """Chrome/Edge: disable WebRTC non-proxied UDP (Admin, HKLM policies). + + Writes both the legacy DWORD key (DefaultWebRtcIpHandlingPolicy=3) and + the modern REG_SZ key (WebRtcIPHandling=disable_non_proxied_udp) so that + all Chrome/Edge versions are covered. + """ + if not is_admin(): + return False, "Administrator required for browser WebRTC policy." + paths = [_WEBRTC_CHROME, _WEBRTC_EDGE] + try: + for path in paths: + if enable: + try: + key = winreg.CreateKeyEx( + winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_SET_VALUE + ) + except OSError: + continue + with key: + winreg.SetValueEx(key, _WEBRTC_VALUE, 0, winreg.REG_DWORD, _WEBRTC_DISABLE) + winreg.SetValueEx(key, _WEBRTC_VALUE_STR, 0, winreg.REG_SZ, _WEBRTC_DISABLE_STR) + else: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_SET_VALUE + ) as key: + for val_name in (_WEBRTC_VALUE, _WEBRTC_VALUE_STR): + try: + winreg.DeleteValue(key, val_name) + except OSError: + pass + except OSError: + pass + return True, ( + "WebRTC hardened (Chrome/Edge: non-proxied UDP disabled)." + if enable + else "WebRTC policy removed." + ) + except OSError as e: + return False, str(e) + + +def _get_os_info() -> str: + try: + return f"{platform.system()} {platform.release()} {platform.machine()}" + except Exception: + return "unknown" + + +def _get_timezone() -> str: + try: + import datetime + tz = datetime.datetime.now(datetime.timezone.utc).astimezone() + name = str(tz.tzname() or "") + offset = tz.utcoffset() + h = int(offset.total_seconds() // 3600) if offset else 0 + return f"{name} (UTC{h:+d})" if name else f"UTC{h:+d}" + except Exception: + return "unknown" + + +def _get_screen_resolution() -> str: + if platform.system() == "Darwin": + try: + r = subprocess.run( + ["system_profiler", "SPDisplaysDataType"], + capture_output=True, + text=True, + timeout=8, + ) + for line in (r.stdout or "").splitlines(): + if "Resolution:" in line: + return line.split("Resolution:", 1)[1].strip() + except Exception: + return "unknown" + try: + r = subprocess.run( + ["powershell", "-NoProfile", "-Command", + "Add-Type -AssemblyName System.Windows.Forms;" + "[System.Windows.Forms.Screen]::PrimaryScreen.Bounds | " + "ForEach-Object { \"$($_.Width)x$($_.Height)\" }"], + capture_output=True, text=True, timeout=8, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + return (r.stdout or "").strip() or "unknown" + except Exception: + return "unknown" + + +def check_browser_fingerprint_consistency(profile_dir: "Path") -> list[str]: + """ + Verify that the managed Firefox user.js is internally consistent and + doesn't mix prefs that would expose the browser as fingerprint-hardened + while still advertising a normal user-agent. + + Returns a list of warning strings (empty = consistent). + """ + from pathlib import Path + user_js = Path(profile_dir) / "user.js" + if not user_js.is_file(): + return ["user.js not found — profile not yet initialized"] + + try: + content = user_js.read_text(encoding="utf-8", errors="replace") + except Exception as e: + return [f"Cannot read user.js: {e}"] + + warnings: list[str] = [] + + def _has(pref: str, value: str) -> bool: + return f'"{pref}", {value}' in content + + # 1. RFP + custom UA is a contradiction (RFP overrides UA) + if _has("privacy.resistFingerprinting", "true") and 'general.useragent.override' in content: + warnings.append( + "RFP + UA override conflict: privacy.resistFingerprinting=true overrides " + "general.useragent.override — the spoofed UA is ignored" + ) + + # 2. proxy not set but force_proxy claimed + if _has("network.proxy.type", "1"): + if 'network.proxy.http",' not in content: + warnings.append("proxy.type=1 but network.proxy.http is missing — browsers will error") + + # 3. FPI on + third-party cookies allowed = inconsistent privacy posture + if _has("privacy.firstparty.isolate", "true") and _has("network.cookie.cookieBehavior", "0"): + warnings.append( + "FPI enabled but cookieBehavior=0 (accept all) — cookies are isolated but not blocked" + ) + + # 4. sanitizeOnShutdown without clearing history = incomplete wipe + if _has("privacy.sanitize.sanitizeOnShutdown", "true"): + if not _has("privacy.clearOnShutdown.history", "true"): + warnings.append( + "sanitizeOnShutdown=true but clearOnShutdown.history not set — " + "history may survive session" + ) + + # 5. WebRTC peerconnection disabled is good — flag if missing + if not _has("media.peerconnection.enabled", "false"): + warnings.append("WebRTC not disabled in profile — real IP can leak via STUN") + + # 6. network.trr.mode=5 (DoH off) is expected when using proxy DNS + if not _has("network.trr.mode", "5"): + warnings.append( + "network.trr.mode is not 5 — Firefox may use its own DoH resolver, " + "bypassing the proxy chain DNS path" + ) + + return warnings + + +def audit_device() -> FingerprintAudit: + """Collect OS-level identifiers and consistency notes.""" + import getpass + import os + from pathlib import Path + + host = get_computer_name() + guid = get_machine_guid() + user = getpass.getuser() + nics = list_nics() + macs = [f"{n.name}: {n.mac}" for n in nics] + os_ = _get_os_info() + tz_ = _get_timezone() + res_ = _get_screen_resolution() + + lines: list[str] = [ + "── OS / Identity ──────────────────────────────────", + f" OS : {os_}", + f" Computer name: {host or '—'}", + f" Username : {user}", + f" MachineGuid : " + (f"{guid[:8]}…{guid[-4:]}" if len(guid) > 12 else guid or "—"), + f" Timezone : {tz_}", + f" Screen res : {res_}", + "", + "── Network adapters ───────────────────────────────", + ] + lines.extend([f" • {m}" for m in macs[:10]] or [" • (none detected)"]) + if len(macs) > 10: + lines.append(f" … and {len(macs) - 10} more") + + lines += [ + "", + "── Browser fingerprint note ───────────────────────", + " Canvas, WebGL, font metrics, audio context, and screen dimensions", + " are NOT changed at the OS level by this app.", + " → Use 'blend_windows_chrome' persona OR 'hardened' mode to control", + " what the browser reports to sites.", + " → RFP (Resist Fingerprinting) makes Firefox stand out as hardened.", + " → Blend persona spoofs UA/platform to look like a normal Windows Chrome.", + "", + "── Recommendations ────────────────────────────────", + ] + + # Quick consistency checks — WebRTC IP-handling policy (Chrome / Edge) + # Accept either: DWORD DefaultWebRtcIpHandlingPolicy==3 + # or REG_SZ WebRtcIPHandling=="disable_non_proxied_udp" + webrtc_ok = False + for hive_root in (_WEBRTC_CHROME, _WEBRTC_EDGE): + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, hive_root, 0, winreg.KEY_QUERY_VALUE + ) as k: + try: + v = int(winreg.QueryValueEx(k, _WEBRTC_VALUE)[0]) + if v == _WEBRTC_DISABLE: + webrtc_ok = True + break + except OSError: + pass + try: + v_str = str(winreg.QueryValueEx(k, _WEBRTC_VALUE_STR)[0]) + if v_str == _WEBRTC_DISABLE_STR: + webrtc_ok = True + break + except OSError: + pass + except OSError: + continue + + if webrtc_ok: + lines.append(" ✓ Chrome/Edge WebRTC IP-handling policy is set (disable_non_proxied_udp)") + else: + lines.append(" ⚠ Chrome/Edge WebRTC policy not set — enable in Privacy tab") + + # Check IPv6 + try: + r = subprocess.run( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", + "Get-NetAdapterBinding -ComponentID ms_tcpip6 | Where-Object { $_.Enabled } | Measure-Object | Select-Object -ExpandProperty Count"], + capture_output=True, text=True, timeout=8, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + count = int((r.stdout or "0").strip() or "0") + if count > 0: + lines.append(f" ⚠ IPv6 active on {count} adapter(s) — disable in Privacy tab for full v4-only chain") + else: + lines.append(" ✓ IPv6 disabled on all adapters") + except Exception: + lines.append(" ? IPv6 status unknown") + + return FingerprintAudit( + lines=lines, + hostname=host, + machine_guid=guid, + username=user, + macs=[n.mac for n in nics], + ) diff --git a/proxy_chain_manager/firewall.py b/proxy_chain_manager/firewall.py new file mode 100644 index 0000000..ab4a73b --- /dev/null +++ b/proxy_chain_manager/firewall.py @@ -0,0 +1,191 @@ +""" +Windows Firewall enforcement — kill-switch style. + +When engaged: + - Default outbound policy → BLOCK (all profiles) + - Allow rules for: GOST, NordVPN stack, this app, loopback, DHCP + - Everything else is denied outbound — apps that don't go through the + local proxy simply can't reach the internet. + +When disengaged: + - Remove our rules, restore default outbound → ALLOW +""" +from __future__ import annotations + +import ctypes +import logging +import subprocess +import sys +from pathlib import Path + +from .paths import gost_exe_path +from .vpn_detect import expand_vpn_executables + +log = logging.getLogger(__name__) + +RULE_PREFIX = "PCM_" + + +def is_admin() -> bool: + try: + return ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[attr-defined] + except Exception: + return False + + +def request_admin_relaunch() -> bool: + """Re-launch the current process elevated. Returns True if launched, False on error.""" + if is_admin(): + return False + try: + exe = sys.executable + args = " ".join(f'"{a}"' for a in sys.argv) + ret = ctypes.windll.shell32.ShellExecuteW( # type: ignore[attr-defined] + None, "runas", exe, args, None, 1 + ) + return ret > 32 + except Exception: + return False + + +def _run(args: list[str], check: bool = False) -> subprocess.CompletedProcess[str]: + try: + return subprocess.run( + args, capture_output=True, text=True, timeout=30, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + except FileNotFoundError as exc: + return subprocess.CompletedProcess(args, 127, "", str(exc)) + + +def _add_rule(name: str, **kw: str) -> bool: + full = RULE_PREFIX + name + args = ["netsh", "advfirewall", "firewall", "add", "rule", f"name={full}"] + for k, v in kw.items(): + args.append(f"{k}={v}") + r = _run(args) + ok = r.returncode == 0 + if not ok: + log.warning("Firewall rule %s failed: %s", full, (r.stdout or "") + (r.stderr or "")) + return ok + + +def _delete_rules() -> None: + r = _run(["netsh", "advfirewall", "firewall", "show", "rule", "name=all", "dir=out"]) + lines = (r.stdout or "").splitlines() + names: set[str] = set() + for ln in lines: + if ln.startswith("Rule Name:"): + n = ln.split(":", 1)[1].strip() + if n.startswith(RULE_PREFIX): + names.add(n) + for n in names: + _run(["netsh", "advfirewall", "firewall", "delete", "rule", f"name={n}"]) + log.info("Deleted %d PCM firewall rules", len(names)) + + +def _set_outbound_policy(action: str) -> None: + """action = 'blockinbound,blockoutbound' or 'blockinbound,allowoutbound'.""" + for profile in ("domainprofile", "privateprofile", "publicprofile"): + _run(["netsh", "advfirewall", "set", profile, "firewallpolicy", action]) + + +def _resolve_self_exe() -> Path: + if getattr(sys, "frozen", False): + return Path(sys.executable).resolve() + return Path(sys.executable).resolve() + + +def engage(gost_path: Path | None = None) -> tuple[bool, str]: + """Activate kill-switch firewall. Returns (success, message).""" + if sys.platform != "win32": + return False, "Kill-switch is Windows-only; skipped on macOS." + if not is_admin(): + return False, "Need admin privileges for firewall enforcement." + + gost = gost_path or gost_exe_path() + self_exe = _resolve_self_exe() + vpn_exes = expand_vpn_executables() + + _delete_rules() + + # Allow loopback (apps → GOST on 127.0.0.1) + _add_rule("Loopback", dir="out", action="allow", + remoteip="127.0.0.0/8", protocol="any") + + # Allow GOST outbound (it connects to the proxy hops) + _add_rule("GOST", dir="out", action="allow", + program=f'"{gost}"', protocol="any") + + # Allow this application outbound (fetches proxy lists, validates) + _add_rule("Self", dir="out", action="allow", + program=f'"{self_exe}"', protocol="any") + + # If running from python.exe, also allow pythonw.exe + if self_exe.name.lower() in ("python.exe", "pythonw.exe"): + for sibling in ("python.exe", "pythonw.exe"): + p = self_exe.with_name(sibling) + if p.is_file(): + _add_rule(f"Python_{sibling}", dir="out", action="allow", + program=f'"{p}"', protocol="any") + + # Allow VPN client executables (Nord, WireGuard, OpenVPN, etc.) + for i, vpath in enumerate(vpn_exes): + _add_rule(f"VPN_{i}", dir="out", action="allow", + program=f'"{vpath}"', protocol="any") + + # Allow DHCP (or you lose your adapter) + _add_rule("DHCP", dir="out", action="allow", + protocol="udp", remoteport="67,68") + + # Allow DNS (GOST needs to resolve proxy hostnames) + _add_rule("DNS_UDP", dir="out", action="allow", + protocol="udp", remoteport="53") + _add_rule("DNS_TCP", dir="out", action="allow", + protocol="tcp", remoteport="53") + + # Set default outbound to BLOCK + _set_outbound_policy("blockinbound,blockoutbound") + + log.info("Firewall kill-switch engaged. %d VPN exes whitelisted.", len(vpn_exes)) + return True, f"Kill-switch ON. {len(vpn_exes)} VPN client(s) whitelisted." + + +def disengage() -> tuple[bool, str]: + """Remove kill-switch, restore normal outbound.""" + if sys.platform != "win32": + return True, "Kill-switch is Windows-only; nothing to remove on macOS." + if not is_admin(): + return False, "Need admin to remove firewall rules." + _set_outbound_policy("blockinbound,allowoutbound") + _delete_rules() + log.info("Firewall kill-switch disengaged.") + return True, "Kill-switch OFF. Normal outbound restored." + + +def is_engaged() -> bool: + """Quick check: are our rules present?""" + if sys.platform != "win32": + return False + r = _run(["netsh", "advfirewall", "firewall", "show", "rule", f"name={RULE_PREFIX}GOST", "dir=out"]) + return RULE_PREFIX in (r.stdout or "") + + +def emergency_disengage() -> None: + """Best-effort kill-switch removal for atexit/signal handlers. + + 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() + log.info("emergency_disengage: firewall rules removed.") + except Exception as exc: # noqa: BLE001 + log.warning("emergency_disengage failed: %s", exc) diff --git a/proxy_chain_manager/gost_util.py b/proxy_chain_manager/gost_util.py new file mode 100644 index 0000000..794e1e4 --- /dev/null +++ b/proxy_chain_manager/gost_util.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +import hashlib +import io +import logging +import platform +import shutil +import subprocess +import sys +import tarfile +import zipfile +from pathlib import Path +from typing import Any + +import httpx + +from .paths import app_data_dir, gost_exe_path + +log = logging.getLogger(__name__) + +GOST_VERSION = "3.2.6" +GOST_RELEASE_ZIP = ( + "https://github.com/go-gost/gost/releases/download/v3.2.6/" + "gost_3.2.6_windows_amd64.zip" +) + +# Pinned SHA256 of the release zip. Update this constant when bumping GOST_RELEASE_ZIP. +# Verified 2026-05-21 against https://github.com/go-gost/gost/releases/download/v3.2.6/ +GOST_RELEASE_ZIP_SHA256 = "32f4edf3d94b622e67f1979f6f5de82dac62abc0977772cf96215dd199ef7e7b" + +# Pinned SHA256 of the extracted ``gost.exe`` binary inside the v3.2.6 zip. +# Recorded the first time the zip is unpacked and verified on every reuse, so +# a tampered or partially-overwritten exe on disk forces a clean re-download. +GOST_EXE_SHA256_FILE = "gost.sha256" + +_DARWIN_ARCHIVE_SHA256 = { + "arm64": "e54f6c22e81c00650adfbbb23317c74a4dca9b9b73fa28cfa150f5559cc3ff2e", + "x86_64": "0892485bd94e37b67a1f1d0d2372ed12d7dc0f1bc763d56177a0c0ee734855e6", + "amd64": "0892485bd94e37b67a1f1d0d2372ed12d7dc0f1bc763d56177a0c0ee734855e6", +} + + +def _release_asset() -> tuple[str, str, str]: + """Return (url, archive_sha256, binary_name) for this platform.""" + if sys.platform == "darwin": + machine = platform.machine().lower() + asset_arch = "arm64" if machine == "arm64" else "amd64" + digest = _DARWIN_ARCHIVE_SHA256["arm64" if asset_arch == "arm64" else "amd64"] + name = f"gost_{GOST_VERSION}_darwin_{asset_arch}.tar.gz" + return f"https://github.com/go-gost/gost/releases/download/v{GOST_VERSION}/{name}", digest, "gost" + return GOST_RELEASE_ZIP, GOST_RELEASE_ZIP_SHA256, "gost.exe" + + + +def _add_defender_exclusion(path: Path) -> None: + """Add Windows Defender exclusion so GOST is not quarantined.""" + if sys.platform != "win32": + return + try: + # ExclusionPath covers gost.exe under this folder; avoid invalid combined params on older builds. + folder = str(path.parent).replace("'", "''") + subprocess.run( + [ + "powershell", "-NoProfile", "-NonInteractive", "-Command", + f"Add-MpPreference -ExclusionPath '{folder}' -ErrorAction SilentlyContinue", + ], + capture_output=True, + timeout=30, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + log.info("Defender exclusion added for %s", path.parent) + except Exception as exc: + log.warning("Defender exclusion failed (non-fatal): %s", exc) + + +def _verify_zip_sha256(data: bytes, expected: str) -> None: + """Raise RuntimeError if SHA256 of *data* does not match *expected* (hex).""" + actual = hashlib.sha256(data).hexdigest().lower() + if actual != expected.lower(): + raise RuntimeError( + f"GOST zip SHA256 mismatch — possible supply-chain attack!\n" + f" expected: {expected.lower()}\n" + f" actual: {actual}\n" + "Delete the downloaded zip and retry, or update GOST_RELEASE_ZIP_SHA256." + ) + log.info("GOST zip SHA256 OK: %s", actual) + + +def _hash_file(path: Path) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(64 * 1024), b""): + h.update(chunk) + return h.hexdigest().lower() + + +def _read_pinned_exe_hash(exe: Path) -> str | None: + sidecar = exe.with_suffix(exe.suffix + ".sha256") + try: + return sidecar.read_text(encoding="utf-8").strip().lower() or None + except OSError: + return None + + +def _write_pinned_exe_hash(exe: Path, digest: str) -> None: + sidecar = exe.with_suffix(exe.suffix + ".sha256") + try: + sidecar.write_text(digest, encoding="utf-8") + except OSError as exc: + log.warning("Could not write gost.exe hash sidecar: %s", exc) + + +def _bundled_gost_paths() -> tuple[Path | None, Path | None]: + """Return (exe_path, sha_path) inside the PyInstaller bundle, or (None, None).""" + base = getattr(sys, "_MEIPASS", None) + if not base: + # Source checkout — look next to the package + base = str(Path(__file__).resolve().parent.parent) + binary_name = "gost.exe" if sys.platform == "win32" else "gost" + bundle_exe = Path(base) / "proxy_chain_manager" / "_bundled" / binary_name + bundle_sha = bundle_exe.with_suffix(bundle_exe.suffix + ".sha256") + if bundle_exe.is_file(): + return bundle_exe, (bundle_sha if bundle_sha.is_file() else None) + return None, None + + +def _install_from_bundle(exe: Path) -> bool: + """Copy the bundled gost.exe into *exe* and pin its hash. Returns True + on success. Verifies the bundle hash if a sidecar shipped with it.""" + src, sha_src = _bundled_gost_paths() + if not src: + return False + try: + exe.parent.mkdir(parents=True, exist_ok=True) + _add_defender_exclusion(exe) + shutil.copy2(src, exe) + _add_defender_exclusion(exe) + digest = _hash_file(exe) + if sha_src: + expected = sha_src.read_text(encoding="utf-8").strip().lower() + if expected and expected != digest: + exe.unlink(missing_ok=True) + log.warning( + "Bundled gost.exe SHA mismatch (bundle=%s actual=%s) — " + "falling back to network download.", expected, digest, + ) + return False + _write_pinned_exe_hash(exe, digest) + log.info("GOST installed from bundle at %s", exe) + return True + except Exception as exc: # noqa: BLE001 + log.warning("Bundle install failed: %s", exc) + return False + + +def ensure_gost(target: Path | None = None) -> Path: + exe = target or gost_exe_path() + if exe.is_file() and exe.stat().st_size > 10_000: + # Re-verify the on-disk binary against the sidecar pin so a tampered + # exe cannot persist across launches. + pinned = _read_pinned_exe_hash(exe) + if pinned: + actual = _hash_file(exe) + if actual == pinned: + return exe + log.warning( + "gost.exe SHA256 mismatch on reuse — re-installing.\n" + " pinned: %s\n actual: %s", + pinned, actual, + ) + try: + exe.unlink() + except OSError as exc: + log.warning("Could not remove tampered gost.exe: %s", exc) + else: + # First reuse after older versions: record the current hash so the + # next run can verify. Mark as trusted-on-first-use only when zip + # SHA verification has already happened earlier in the session. + _write_pinned_exe_hash(exe, _hash_file(exe)) + return exe + + # Prefer the bundled binary so the first run works fully offline. + if _install_from_bundle(exe): + return exe + + exe.parent.mkdir(parents=True, exist_ok=True) + # Add exclusion BEFORE downloading so Defender doesn't nuke it on write + _add_defender_exclusion(exe) + url, archive_sha, binary_name = _release_asset() + log.info("Downloading GOST %s", url) + with httpx.Client(timeout=120.0, follow_redirects=True) as c: + r = c.get(url) + r.raise_for_status() + data = r.content + _verify_zip_sha256(data, archive_sha) + if url.endswith(".zip"): + if len(data) < 64 or data[:2] != b"PK": + raise RuntimeError("Downloaded GOST zip looks invalid (not a zip).") + with zipfile.ZipFile(io.BytesIO(data), "r") as z: + names = [n for n in z.namelist() if n.lower().endswith(binary_name)] + if not names: + raise RuntimeError(f"{binary_name} not found in release zip") + with z.open(names[0]) as src, open(exe, "wb") as dst: + shutil.copyfileobj(src, dst) + else: + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tar: + members = [m for m in tar.getmembers() if Path(m.name).name == binary_name and m.isfile()] + if not members: + raise RuntimeError(f"{binary_name} not found in release tarball") + src = tar.extractfile(members[0]) + if src is None: + raise RuntimeError(f"{binary_name} could not be extracted") + with src, open(exe, "wb") as dst: + shutil.copyfileobj(src, dst) + try: + exe.chmod(0o755) + except OSError: + pass + # Add exclusion again after write in case Defender scanned during extraction + _add_defender_exclusion(exe) + # Pin the freshly-extracted exe so subsequent launches can re-verify it. + _write_pinned_exe_hash(exe, _hash_file(exe)) + log.info("GOST installed at %s", exe) + return exe + + +def build_gost_cmd(gost: Path, listen_http: str, forwards: list[str]) -> list[str]: + args = [str(gost), "-L", f"http://{listen_http}"] + for f in forwards: + args.extend(["-F", f]) + return args + + +def gost_log_path() -> Path: + return app_data_dir() / "gost.log" + + +def popen_no_window(args: list[str]) -> subprocess.Popen: + # stderr → rolling log file (not a pipe — pipes deadlock if unread; files never block). + # stdout is also captured to the same file since GOST v3 mixes output channels. + cr = getattr(subprocess, "CREATE_NO_WINDOW", 0) + log_path = gost_log_path() + try: + _rotate_gost_log(log_path) + stderr_dest: Any = open(log_path, "ab") # noqa: WPS515 — intentional long-lived file handle + except OSError: + stderr_dest = subprocess.DEVNULL + return subprocess.Popen( + args, + stdin=subprocess.DEVNULL, + stdout=stderr_dest, + stderr=stderr_dest, + creationflags=cr, + ) + + +def _rotate_gost_log(path: Path, max_bytes: int = 512 * 1024) -> None: + """Keep gost.log under max_bytes by truncating the oldest half when it grows too large.""" + try: + if path.is_file() and path.stat().st_size > max_bytes: + data = path.read_bytes() + path.write_bytes(data[len(data) // 2:]) + except OSError: + pass + + +def read_gost_log_tail(lines: int = 40) -> str: + """Return the last N lines of gost.log for display in the UI.""" + try: + text = gost_log_path().read_text(encoding="utf-8", errors="replace") + return "\n".join(text.splitlines()[-lines:]) + except OSError: + return "(gost.log not found)" + + +def terminate_process(proc: subprocess.Popen | None) -> None: + if proc is None: + return + if proc.poll() is not None: + return + try: + proc.terminate() + proc.wait(timeout=5) + except Exception as exc: + log.debug("GOST terminate failed (%s) — escalating to kill.", exc) + try: + proc.kill() + except Exception as kill_exc: + log.warning("GOST kill also failed: %s", kill_exc) diff --git a/proxy_chain_manager/gui_validate.py b/proxy_chain_manager/gui_validate.py new file mode 100644 index 0000000..d273bdb --- /dev/null +++ b/proxy_chain_manager/gui_validate.py @@ -0,0 +1,41 @@ +"""Fast parallel proxy checks for the GUI (per-hop callbacks).""" +from __future__ import annotations + +import asyncio +from typing import Callable + +from .validator import _check_one + +# High concurrency for interactive chain tests +GUI_VALIDATE_CONCURRENCY = 48 + + +async def validate_each_parallel( + urls: list[str], + check_url: str, + concurrency: int, + timeout_seconds: float, + on_result: Callable[[str, bool], None], +) -> None: + if not urls: + return + sem = asyncio.Semaphore(max(1, concurrency)) + + async def one(u: str) -> None: + async with sem: + ok = await _check_one(u, check_url, timeout_seconds) + on_result(u, ok) + + await asyncio.gather(*(one(u) for u in urls)) + + +def run_validate_each_sync( + urls: list[str], + check_url: str, + timeout_seconds: float, + on_result: Callable[[str, bool], None], + concurrency: int = GUI_VALIDATE_CONCURRENCY, +) -> None: + asyncio.run( + validate_each_parallel(urls, check_url, concurrency, timeout_seconds, on_result) + ) diff --git a/proxy_chain_manager/leak_audit.py b/proxy_chain_manager/leak_audit.py new file mode 100644 index 0000000..947bbdc --- /dev/null +++ b/proxy_chain_manager/leak_audit.py @@ -0,0 +1,313 @@ +"""Consolidated live leak audit. + +Used by the Privacy tab "Run audit" button. Probes every surface that can +deanonymize the host even when the chain is healthy: + + • IP — exit IP through chain vs direct IP (subnet leak) + • DNS — system resolvers, whether they're public + • IPv6 binding — adapters with IPv6 active + • WPAD/PAC — leftover AutoConfigURL or AutoDetect + • Policy locks — Group Policy proxy keys that beat our settings + • PerUser flag — Windows Server ProxySettingsPerUser + • LAN broadcast — LLMNR / NetBIOS / mDNS status + • VPN — adapter presence + • WebRTC — Chrome/Edge policy presence + • System proxy — what registry says we're set to + • TLS exit — quick categorisation hint (datacenter / residential) + +Each check has its own short timeout so a slow probe never blocks the rest. +""" +from __future__ import annotations + +import asyncio +import logging +import subprocess +import winreg +from dataclasses import dataclass, field + +from .dns_leak import get_system_dns_servers +from .firewall import is_admin +from .privacy_lan import lan_status +from .sysproxy import detect_policy_overrides, is_system_proxy_set +from .validator import check_chain_exit_ip, get_direct_ip +from .vpn_detect import detect_vpn + +log = logging.getLogger(__name__) + + +@dataclass +class AuditFinding: + name: str + ok: bool + value: str + note: str = "" + + +@dataclass +class AuditReport: + findings: list[AuditFinding] = field(default_factory=list) + overall_ok: bool = True + + def add(self, name: str, ok: bool, value: str, note: str = "") -> None: + self.findings.append(AuditFinding(name, ok, value, note)) + if not ok: + self.overall_ok = False + + +def _check_ipv6_active() -> tuple[bool, str]: + """True/'string' if any 'Up' adapter has IPv6 binding enabled. + + PowerShell 3.0+ path; falls back to ipconfig parsing (locale-tolerant + enough — looks for hex colons). + """ + try: + r = subprocess.run( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", + "Get-NetAdapterBinding -ComponentID ms_tcpip6 | " + "Where-Object { $_.Enabled } | " + "Select-Object -ExpandProperty Name"], + capture_output=True, text=True, timeout=10, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + names = [n.strip() for n in (r.stdout or "").splitlines() if n.strip()] + if names: + return True, ", ".join(names[:3]) + (f" +{len(names)-3}" if len(names) > 3 else "") + # Empty stdout could mean PS3 cmdlet missing — try ipconfig fallback. + if r.returncode != 0: + raise RuntimeError("PS3 net cmdlets unavailable") + return False, "no adapters bound to IPv6" + except Exception: + try: + r = subprocess.run( + ["ipconfig"], capture_output=True, text=True, timeout=8, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + has_v6 = any( + "IPv6" in ln and ":" in ln.split(":", 1)[1] + for ln in (r.stdout or "").splitlines() + ) + return has_v6, ("IPv6 address present" if has_v6 else "no IPv6 address") + except Exception as e: + return False, f"unknown ({e})" + + +def _check_wpad() -> tuple[bool, str]: + path = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings" + autoconf = "" + autodet = 0 + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_QUERY_VALUE + ) as key: + try: + autoconf = str(winreg.QueryValueEx(key, "AutoConfigURL")[0] or "") + except OSError: + pass + try: + autodet = int(winreg.QueryValueEx(key, "AutoDetect")[0]) + except OSError: + autodet = 0 + except OSError: + pass + leaking = bool(autoconf) or autodet == 1 + if leaking: + bits = [] + if autoconf: + bits.append(f"AutoConfigURL={autoconf}") + if autodet: + bits.append(f"AutoDetect={autodet}") + return False, ", ".join(bits) + return True, "no WPAD/PAC override" + + +def _check_per_user_flag() -> tuple[bool, str]: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings", + 0, winreg.KEY_QUERY_VALUE, + ) as key: + v = int(winreg.QueryValueEx(key, "ProxySettingsPerUser")[0]) + if v == 0: + return False, "ProxySettingsPerUser=0 (HKCU IGNORED)" + return True, "ProxySettingsPerUser=1" + except OSError: + return True, "unset (default → HKCU honored)" + + +def _check_webrtc_policy() -> tuple[bool, str]: + """OK means the policy IS set (browsers won't leak non-proxied UDP).""" + paths = ( + 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: + 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 + if found: + return True, "policy set: " + ", ".join(found) + return False, "no WebRTC policy — Chrome/Edge may leak local IPs" + + +def _categorize_exit(ip: str | None) -> str: + """Cheap categorization hint based on common RIR / ASN heuristics. + + No external lookup — just shape-based hints. Real classification can be + added later via offline GeoIP DBs. + """ + if not ip: + return "unknown" + try: + a = int(ip.split(".")[0]) + except Exception: + return "non-IPv4" + if a in (10,) or ip.startswith(("192.168.", "172.16.", "172.17.", "172.18.", + "172.19.", "172.2", "172.30.", "172.31.")): + return "PRIVATE — chain broken" + if a == 127: + return "LOOPBACK — chain broken" + return "public" + + +async def run_audit( + listen_proxy: str, + ip_check_url: str, + timeout_seconds: float = 10.0, +) -> AuditReport: + """Run every check in parallel where safe; return structured report.""" + rep = AuditReport() + chain_running = is_system_proxy_set() + + # Network probes in parallel + direct_task = asyncio.create_task(get_direct_ip(ip_check_url, timeout_seconds)) + chain_task = ( + asyncio.create_task( + check_chain_exit_ip(listen_proxy, ip_check_url, timeout_seconds, chain_hops=1) + ) + if chain_running else None + ) + + direct_ip = await direct_task + exit_ip = await chain_task if chain_task else None + + if direct_ip: + rep.add("Direct IP (no chain)", True, direct_ip, "") + else: + rep.add("Direct IP (no chain)", False, "unreachable", "no internet?") + + if chain_running: + if exit_ip: + cat = _categorize_exit(exit_ip) + same = bool(direct_ip and exit_ip == direct_ip) + rep.add( + "Chain exit IP", + not same and cat == "public", + f"{exit_ip} ({cat})", + "matches direct IP — chain not forwarding" if same else "", + ) + else: + rep.add("Chain exit IP", False, "unreachable through chain", "") + else: + rep.add("Chain exit IP", True, "chain not running", "skipped") + + # DNS resolvers + dns_servers = get_system_dns_servers() + private_prefixes = ("127.", "10.", "192.168.", "172.16.", "172.17.", "172.18.", + "172.19.", "172.2", "172.30.", "172.31.") + public_dns = [d for d in dns_servers if not d.startswith(private_prefixes)] + if not dns_servers: + rep.add("DNS resolvers", True, "none reported (DHCP)", "") + elif public_dns: + rep.add( + "DNS resolvers", + False, + ", ".join(dns_servers), + f"{len(public_dns)} public — DNS may bypass chain", + ) + else: + rep.add("DNS resolvers", True, ", ".join(dns_servers), "all private/local") + + # IPv6 binding + v6_on, v6_msg = _check_ipv6_active() + rep.add("IPv6 binding", not v6_on, v6_msg, "IPv6 active = potential leak past v4 proxies" if v6_on else "") + + # WPAD + wpad_ok, wpad_msg = _check_wpad() + rep.add("WPAD / PAC", wpad_ok, wpad_msg) + + # Per-User flag + pu_ok, pu_msg = _check_per_user_flag() + rep.add("ProxySettingsPerUser", pu_ok, pu_msg, + "needs Admin fix on this box" if not pu_ok else "") + + # Policy locks + pol = detect_policy_overrides() + if pol: + rep.add("Group Policy proxy locks", False, f"{len(pol)} entries", + "policy keys override our proxy") + else: + rep.add("Group Policy proxy locks", True, "none") + + # LAN-scope broadcasts + lan = lan_status() + lan_clean = "OFF" in lan["llmnr"] and "OFF" in lan["mdns"] and "NICs with NetBIOS disabled" in lan["netbios"] + rep.add( + "LAN broadcast (LLMNR/NetBIOS/mDNS)", + lan_clean, + f"LLMNR={lan['llmnr']} NetBIOS={lan['netbios']} mDNS={lan['mdns']}", + "hostname leaks to local network" if not lan_clean else "", + ) + + # VPN + vpn = detect_vpn() + rep.add( + "VPN adapter", + True, + f"{vpn.label}" + (f" ({vpn.adapter})" if vpn.adapter else ""), + "informational", + ) + + # WebRTC policy + rtc_ok, rtc_msg = _check_webrtc_policy() + rep.add("Browser WebRTC policy", rtc_ok, rtc_msg) + + # Admin status (a lot of fixes require it) + rep.add( + "Administrator privileges", + is_admin(), + "Yes" if is_admin() else "No", + "MAC/IPv6/LAN/HKLM fixes need elevation" if not is_admin() else "", + ) + + return rep + + +def run_audit_sync( + listen_proxy: str, + ip_check_url: str, + timeout_seconds: float = 10.0, +) -> AuditReport: + return asyncio.run(run_audit(listen_proxy, ip_check_url, timeout_seconds)) diff --git a/proxy_chain_manager/leak_detect.py b/proxy_chain_manager/leak_detect.py new file mode 100644 index 0000000..3945736 --- /dev/null +++ b/proxy_chain_manager/leak_detect.py @@ -0,0 +1,64 @@ +"""VPN-aware chain leak detection.""" +from __future__ import annotations + + +def is_same_subnet(ip_a: str | None, ip_b: str | None, prefix_len: int = 16) -> bool: + """True if two IPv4 addresses share the same /prefix_len subnet.""" + if not ip_a or not ip_b: + return False + try: + a_parts = [int(x) for x in ip_a.split(".")] + b_parts = [int(x) for x in ip_b.split(".")] + if len(a_parts) != 4 or len(b_parts) != 4: + return False + + def to_int(parts: list[int]) -> int: + return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3] + + mask = (0xFFFFFFFF << (32 - prefix_len)) & 0xFFFFFFFF + return (to_int(a_parts) & mask) == (to_int(b_parts) & mask) + except Exception: + return False + + +def is_chain_leak(exit_ip: str | None, real_ip: str | None, vpn_active: bool) -> bool: + """True when the chain is not forwarding (traffic still looks like direct/VPN exit). + + With VPN: compare /16 — VPN IPs rotate but stay in-provider ranges. + Without VPN: exact IP match only (avoid false positives on same ISP /16). + + **Fail-closed**: when ``real_ip`` is unknown we cannot prove the chain is + safe, so we conservatively return ``True``. The service loop applies a + short warm-up grace period before this verdict kicks in so a transient + direct-IP lookup failure doesn't cause perpetual rotation. + """ + if not exit_ip: + return True + if not real_ip: + # Fail-closed: README promises "fail closed" when trust dies. + return True + if exit_ip == real_ip: + return True + if vpn_active: + return is_same_subnet(exit_ip, real_ip) + return False + + +def leak_reason(exit_ip: str | None, real_ip: str | None, vpn_active: bool) -> str: + if not exit_ip: + return "exit IP unreachable" + if not real_ip: + return ( + "direct IP unknown — cannot prove the chain is forwarding " + "(fail-closed). Check internet connectivity and ip_check_url." + ) + if exit_ip == real_ip: + if vpn_active: + return f"exit {exit_ip} equals VPN/direct IP (chain not forwarding)" + return f"exit {exit_ip} equals your real IP (no anonymization)" + if vpn_active and is_same_subnet(exit_ip, real_ip): + return ( + f"exit {exit_ip} shares /16 with direct {real_ip} " + "(likely exiting via VPN tunnel, not proxy chain)" + ) + return "ok" diff --git a/proxy_chain_manager/mac_spoof.py b/proxy_chain_manager/mac_spoof.py new file mode 100644 index 0000000..e6d9872 --- /dev/null +++ b/proxy_chain_manager/mac_spoof.py @@ -0,0 +1,158 @@ +"""Randomize and restore NIC MAC addresses (Admin required).""" +from __future__ import annotations + +import logging +import random +import re +import subprocess +from dataclasses import dataclass + +from .firewall import is_admin +from .win_compat import probe as _win_probe + +log = logging.getLogger(__name__) + +_MAC_RE = re.compile(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$") + + +@dataclass +class NicMac: + name: str + mac: str + description: str = "" + + +def _run(args: list[str], timeout: float = 20.0) -> tuple[int, str, str]: + try: + r = subprocess.run( + args, + capture_output=True, + text=True, + timeout=timeout, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + return r.returncode, r.stdout or "", r.stderr or "" + except Exception as e: + return 1, "", str(e) + + +def list_nics() -> list[NicMac]: + """Physical/up adapters with current MAC. + + Falls back to ``wmic nic`` for hosts without PowerShell 3.0 (Server 2008 R2). + """ + compat = _win_probe() + if not compat.has_net_cmdlets: + return _list_nics_wmic() + + script = ( + "Get-NetAdapter | Where-Object { $_.Status -ne 'Disabled' } | " + "Select-Object Name, MacAddress, InterfaceDescription | " + "ConvertTo-Json -Compress" + ) + code, out, _ = _run(["powershell", "-NoProfile", "-Command", script]) + if code != 0 or not out.strip(): + return _list_nics_wmic() + import json + + try: + data = json.loads(out) + except json.JSONDecodeError: + return [] + rows = data if isinstance(data, list) else [data] + nics: list[NicMac] = [] + for row in rows: + if not isinstance(row, dict): + continue + name = str(row.get("Name", "")).strip() + mac = str(row.get("MacAddress", "")).strip().replace("-", ":") + desc = str(row.get("InterfaceDescription", "")).strip() + if name and mac and mac != "00:00:00:00:00:00": + nics.append(NicMac(name=name, mac=mac, description=desc)) + return nics + + +def random_mac() -> str: + """Locally administered unicast MAC.""" + b = [random.randint(0, 255) for _ in range(6)] + b[0] = (b[0] | 0x02) & 0xFE + return ":".join(f"{x:02X}" for x in b) + + +def _list_nics_wmic() -> list[NicMac]: + """Server 2008 R2 / PowerShell 2.0 fallback via wmic.""" + code, out, _ = _run([ + "wmic", "nic", "where", "NetEnabled=true", + "get", "NetConnectionID,MACAddress,Name", "/format:list", + ]) + if code != 0 or not out: + return [] + nics: list[NicMac] = [] + cur = {"id": "", "mac": "", "name": ""} + for ln in out.splitlines(): + s = ln.strip() + if not s: + if cur["id"] and cur["mac"]: + mac = cur["mac"].replace("-", ":") + if mac and mac != "00:00:00:00:00:00": + nics.append(NicMac(name=cur["id"], mac=mac, description=cur["name"])) + cur = {"id": "", "mac": "", "name": ""} + continue + if s.startswith("NetConnectionID="): + cur["id"] = s.split("=", 1)[1].strip() + elif s.startswith("MACAddress="): + cur["mac"] = s.split("=", 1)[1].strip() + elif s.startswith("Name="): + cur["name"] = s.split("=", 1)[1].strip() + if cur["id"] and cur["mac"]: + mac = cur["mac"].replace("-", ":") + if mac and mac != "00:00:00:00:00:00": + nics.append(NicMac(name=cur["id"], mac=mac, description=cur["name"])) + return nics + + +def set_mac(adapter: str, mac: str) -> tuple[bool, str]: + if not is_admin(): + return False, "Administrator required to change MAC." + if not _win_probe().has_net_cmdlets: + return False, ( + "MAC change requires PowerShell 3.0+ (Windows 8 / Server 2012+). " + "Detected legacy PowerShell — feature unavailable on this host." + ) + mac = mac.replace("-", ":").upper() + if not _MAC_RE.match(mac.replace(":", "-")): + return False, f"Invalid MAC: {mac}" + ps = ( + f'$a = Get-NetAdapter -Name "{adapter}" -ErrorAction Stop; ' + f'Set-NetAdapter -Name $a.Name -MacAddress "{mac}" -Confirm:$false' + ) + code, _, err = _run(["powershell", "-NoProfile", "-Command", ps]) + if code != 0: + return False, err or "Set-NetAdapter failed" + log.info("MAC set %s → %s", adapter, mac) + return True, f"{adapter} → {mac}" + + +def spoof_all_physical(snapshot: dict[str, str] | None = None) -> tuple[dict[str, str], list[str]]: + """Randomize MAC on non-virtual adapters. Returns (original_map, log lines).""" + originals: dict[str, str] = dict(snapshot or {}) + logs: list[str] = [] + skip_hints = ("virtual", "vmware", "hyper-v", "loopback", "bluetooth", "wan miniport") + for nic in list_nics(): + low = (nic.description + nic.name).lower() + if any(h in low for h in skip_hints): + continue + if nic.name not in originals: + originals[nic.name] = nic.mac + new_mac = random_mac() + ok, msg = set_mac(nic.name, new_mac) + logs.append(msg if ok else f"{nic.name}: {msg}") + return originals, logs + + +def restore_macs(originals: dict[str, str]) -> list[str]: + logs: list[str] = [] + for name, mac in originals.items(): + ok, msg = set_mac(name, mac) + logs.append(msg if ok else f"{name}: {msg}") + return logs diff --git a/proxy_chain_manager/paths.py b/proxy_chain_manager/paths.py new file mode 100644 index 0000000..a43895d --- /dev/null +++ b/proxy_chain_manager/paths.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import os +import sys +from pathlib import Path + + +def app_data_dir() -> Path: + if sys.platform == "darwin": + base = Path.home() / "Library" / "Application Support" + elif os.environ.get("LOCALAPPDATA"): + base = Path(os.environ["LOCALAPPDATA"]) + else: + base = Path.home() / "AppData" / "Local" + d = base / "ProxyChainManager" + d.mkdir(parents=True, exist_ok=True) + return d + + +def gost_exe_path() -> Path: + suffix = ".exe" if sys.platform == "win32" else "" + return app_data_dir() / f"gost{suffix}" diff --git a/proxy_chain_manager/pool_ops.py b/proxy_chain_manager/pool_ops.py new file mode 100644 index 0000000..2989a83 --- /dev/null +++ b/proxy_chain_manager/pool_ops.py @@ -0,0 +1,106 @@ +"""Fetch proxy lists for UI picker and shared pool building.""" +from __future__ import annotations + +import asyncio +import logging +import random +from concurrent.futures import ThreadPoolExecutor +from typing import Callable + +from .config import Settings +from .fetcher import fetch_proxy_json, normalize_entries +from .validator import validate_proxies + +log = logging.getLogger(__name__) +_POOL_EXEC = ThreadPoolExecutor(max_workers=6, thread_name_prefix="pool_ops") + +PICKER_DISPLAY_CAP = 200 + + +class ProxySourceCache: + """Cache full source lists; each UI pull shows a new random batch.""" + + def __init__(self) -> None: + self._full: list[str] = [] + self.total_cached: int = 0 + + def clear(self) -> None: + self._full.clear() + self.total_cached = 0 + + @property + def loaded(self) -> bool: + return bool(self._full) + + def load(self, sources: list[str], prefer_elite: bool, + on_status: Callable[[str], None] | None = None) -> int: + self._full = fetch_raw_proxies(sources, prefer_elite, on_status=on_status) + self.total_cached = len(self._full) + return self.total_cached + + def random_batch(self, cap: int = PICKER_DISPLAY_CAP) -> list[str]: + if not self._full: + return [] + k = min(cap, len(self._full)) + return random.sample(self._full, k) + + +def fetch_raw_proxies( + sources: list[str], + prefer_elite: bool, + on_status: Callable[[str], None] | None = None, +) -> list[str]: + """Blocking fetch from all JSON sources; deduped, not validated.""" + if not sources: + sources = list(Settings().sources) + seen: set[str] = set() + out: list[str] = [] + for url in sources: + if on_status: + on_status(f"Fetching {url[:70]}…") + try: + rows = fetch_proxy_json(url, timeout=45.0) + entries = normalize_entries(rows, prefer_elite) + if on_status: + on_status(f" → {len(entries)} proxies") + for u in entries: + if u not in seen: + seen.add(u) + out.append(u) + except Exception as e: + log.warning("fetch_raw_proxies %s: %s", url[:60], e) + if on_status: + on_status(f" → error: {e!s}") + return out + + +async def validate_proxy_subset( + urls: list[str], + settings: Settings, + target: int = 0, + on_progress: Callable[[int, int], None] | None = None, +) -> list[str]: + if not urls: + return [] + sample = list(urls) + random.shuffle(sample) + if len(sample) > settings.max_candidates: + sample = sample[: settings.max_candidates] + tgt = target or settings.min_pool_size + return await validate_proxies( + sample, + settings.ip_check_url, + settings.validation_concurrency, + settings.validation_timeout_seconds, + on_progress=on_progress, + target=tgt, + ) + + +def run_validate_sync( + urls: list[str], + settings: Settings, + target: int = 0, + on_progress: Callable[[int, int], None] | None = None, +) -> list[str]: + return asyncio.run(validate_proxy_subset(urls, settings, target=target, on_progress=on_progress)) diff --git a/proxy_chain_manager/privacy_lan.py b/proxy_chain_manager/privacy_lan.py new file mode 100644 index 0000000..734c908 --- /dev/null +++ b/proxy_chain_manager/privacy_lan.py @@ -0,0 +1,291 @@ +"""LAN-scope hostname leak lockdown. + +Windows by default broadcasts the local hostname on every adapter via: + - LLMNR (UDP 5355) — link-local multicast name resolution + - NetBIOS over TCP/IP — broadcast name registration on the subnet + - mDNS (UDP 5353) — Bonjour-style name advertising on newer Windows + +Anyone on the same Wi-Fi / VLAN can grab the hostname and tie it to the MAC +address. Killing these is a paranoid-mode no-brainer. + +All three are reversible. We snapshot original state on engage and restore on +disengage. +""" +from __future__ import annotations + +import json +import logging +import subprocess +from dataclasses import dataclass, field + +import winreg + +from .firewall import is_admin +from .win_compat import probe as _win_probe + +log = logging.getLogger(__name__) + +_LLMNR_POLICY_PATH = r"SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" +_LLMNR_VALUE = "EnableMulticast" +_MDNS_DNSCLIENT = r"SYSTEM\CurrentControlSet\Services\Dnscache\Parameters" +_MDNS_VALUE = "EnableMDNS" + + +@dataclass +class LanSnapshot: + netbios_per_interface: dict[str, int] = field(default_factory=dict) + llmnr_present: bool = False + llmnr_prev_value: int | None = None + mdns_present: bool = False + mdns_prev_value: int | None = None + + +def _run(args: list[str], timeout: float = 12.0) -> tuple[int, str, str]: + try: + r = subprocess.run( + args, + capture_output=True, + text=True, + timeout=timeout, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + return r.returncode, r.stdout or "", r.stderr or "" + except Exception as e: + return 1, "", str(e) + + +def _list_netbios_via_wmic() -> dict[str, int]: + """Per-NIC NetBIOS setting via wmic (works back to Server 2008 R2). + + Returns: { settings_id : tcpip_netbios_options } + 0 = use DHCP, 1 = enabled, 2 = disabled + """ + code, out, _ = _run([ + "wmic", "nicconfig", "where", "IPEnabled=true", + "get", "SettingID,TcpipNetbiosOptions", "/format:list", + ]) + if code != 0 or not out: + return {} + settings: dict[str, int] = {} + cur_id = "" + cur_val: int | None = None + for raw in out.splitlines(): + line = raw.strip() + if not line: + if cur_id and cur_val is not None: + settings[cur_id] = cur_val + cur_id, cur_val = "", None + continue + if line.startswith("SettingID="): + cur_id = line.split("=", 1)[1].strip() + elif line.startswith("TcpipNetbiosOptions="): + try: + cur_val = int(line.split("=", 1)[1].strip()) + except ValueError: + cur_val = None + if cur_id and cur_val is not None: + settings[cur_id] = cur_val + return settings + + +def _set_netbios_via_wmic(settings_id: str, mode: int) -> bool: + """mode: 0=DHCP, 1=enabled, 2=disabled.""" + code, _, _ = _run([ + "wmic", "nicconfig", "where", + f"SettingID='{settings_id}'", + "call", "SetTcpipNetbios", str(mode), + ]) + return code == 0 + + +def _llmnr_read() -> tuple[bool, int | None]: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, _LLMNR_POLICY_PATH, 0, winreg.KEY_QUERY_VALUE + ) as key: + val, _ = winreg.QueryValueEx(key, _LLMNR_VALUE) + return True, int(val) + except OSError: + return False, None + + +def _llmnr_write(value: int) -> bool: + try: + with winreg.CreateKeyEx( + winreg.HKEY_LOCAL_MACHINE, _LLMNR_POLICY_PATH, 0, winreg.KEY_SET_VALUE + ) as key: + winreg.SetValueEx(key, _LLMNR_VALUE, 0, winreg.REG_DWORD, value) + return True + except OSError: + return False + + +def _llmnr_delete() -> None: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, _LLMNR_POLICY_PATH, 0, winreg.KEY_SET_VALUE + ) as key: + winreg.DeleteValue(key, _LLMNR_VALUE) + except OSError: + pass + + +def _mdns_read() -> tuple[bool, int | None]: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, _MDNS_DNSCLIENT, 0, winreg.KEY_QUERY_VALUE + ) as key: + val, _ = winreg.QueryValueEx(key, _MDNS_VALUE) + return True, int(val) + except OSError: + return False, None + + +def _mdns_write(value: int) -> bool: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, _MDNS_DNSCLIENT, 0, winreg.KEY_SET_VALUE + ) as key: + winreg.SetValueEx(key, _MDNS_VALUE, 0, winreg.REG_DWORD, value) + return True + except OSError: + return False + + +def _restart_dnscache() -> None: + """Bounce the DNS Client service so the LLMNR/mDNS toggles take effect. + + Server hardening baselines sometimes mark this service "denied" — failure + is logged and ignored; the change still applies on next reboot. + """ + _run(["net", "stop", "Dnscache", "/y"], timeout=20) + _run(["net", "start", "Dnscache"], timeout=20) + + +def lan_status() -> dict[str, str]: + """Human-readable current state. Returned even when not Admin.""" + nb = _list_netbios_via_wmic() + if nb: + disabled = sum(1 for v in nb.values() if v == 2) + nb_summary = f"{disabled}/{len(nb)} NICs with NetBIOS disabled" + else: + nb_summary = "unavailable" + llmnr_present, llmnr_v = _llmnr_read() + if not llmnr_present: + llmnr_summary = "default (ON)" + else: + llmnr_summary = "OFF" if llmnr_v == 0 else f"ON (policy={llmnr_v})" + mdns_present, mdns_v = _mdns_read() + if not mdns_present: + mdns_summary = "default (ON on Win10 1903+)" + else: + mdns_summary = "OFF" if mdns_v == 0 else f"ON (EnableMDNS={mdns_v})" + return { + "netbios": nb_summary, + "llmnr": llmnr_summary, + "mdns": mdns_summary, + } + + +def engage_lan_lockdown() -> tuple[LanSnapshot | None, list[str]]: + """Disable LLMNR / NetBIOS / mDNS. Returns (snapshot for restore, log lines). + + snapshot is None when the operation was refused (not Admin, etc.). + """ + logs: list[str] = [] + if not is_admin(): + return None, ["LAN privacy lockdown skipped — needs Administrator."] + + snap = LanSnapshot() + snap.netbios_per_interface = _list_netbios_via_wmic() + snap.llmnr_present, snap.llmnr_prev_value = _llmnr_read() + snap.mdns_present, snap.mdns_prev_value = _mdns_read() + + if snap.netbios_per_interface: + changed = 0 + for sid in snap.netbios_per_interface: + if _set_netbios_via_wmic(sid, 2): + changed += 1 + logs.append(f"NetBIOS over TCP/IP disabled on {changed}/{len(snap.netbios_per_interface)} NICs.") + else: + logs.append("NetBIOS: could not enumerate adapters (wmic missing?).") + + if _llmnr_write(0): + logs.append("LLMNR (UDP 5355) disabled via DNSClient policy.") + else: + logs.append("LLMNR disable failed (policy write rejected).") + + if _mdns_write(0): + logs.append("mDNS (UDP 5353) disabled via Dnscache parameters.") + else: + logs.append("mDNS disable failed (Dnscache parameters not writable).") + + _restart_dnscache() + logs.append("DNS Client service bounced — LAN lockdown active.") + return snap, logs + + +def restore_lan(snap: LanSnapshot | None) -> list[str]: + """Roll back to the snapshot captured by engage_lan_lockdown.""" + if snap is None or not is_admin(): + return [] + logs: list[str] = [] + + if snap.netbios_per_interface: + restored = 0 + for sid, prev in snap.netbios_per_interface.items(): + if _set_netbios_via_wmic(sid, prev): + restored += 1 + logs.append(f"NetBIOS restored on {restored}/{len(snap.netbios_per_interface)} NICs.") + + if snap.llmnr_present and snap.llmnr_prev_value is not None: + _llmnr_write(snap.llmnr_prev_value) + logs.append(f"LLMNR policy restored to {snap.llmnr_prev_value}.") + else: + _llmnr_delete() + logs.append("LLMNR policy removed (was default).") + + if snap.mdns_present and snap.mdns_prev_value is not None: + _mdns_write(snap.mdns_prev_value) + logs.append(f"mDNS restored (EnableMDNS={snap.mdns_prev_value}).") + else: + # If mDNS key was absent originally, delete the value we created. + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, _MDNS_DNSCLIENT, 0, winreg.KEY_SET_VALUE + ) as key: + winreg.DeleteValue(key, _MDNS_VALUE) + logs.append("mDNS toggle removed (was default).") + except OSError: + pass + + _restart_dnscache() + return logs + + +def snapshot_to_json(snap: LanSnapshot | None) -> str: + if snap is None: + return "" + return json.dumps({ + "netbios": snap.netbios_per_interface, + "llmnr_present": snap.llmnr_present, + "llmnr_prev_value": snap.llmnr_prev_value, + "mdns_present": snap.mdns_present, + "mdns_prev_value": snap.mdns_prev_value, + }) + + +def snapshot_from_json(raw: str) -> LanSnapshot | None: + if not raw: + return None + try: + d = json.loads(raw) + return LanSnapshot( + netbios_per_interface={k: int(v) for k, v in (d.get("netbios") or {}).items()}, + llmnr_present=bool(d.get("llmnr_present")), + llmnr_prev_value=d.get("llmnr_prev_value"), + mdns_present=bool(d.get("mdns_present")), + mdns_prev_value=d.get("mdns_prev_value"), + ) + except Exception: + return None diff --git a/proxy_chain_manager/proxy_picker.py b/proxy_chain_manager/proxy_picker.py new file mode 100644 index 0000000..1c9b007 --- /dev/null +++ b/proxy_chain_manager/proxy_picker.py @@ -0,0 +1,201 @@ +"""Modal dialog: random batch from cached sources, multi-select.""" +from __future__ import annotations + +import threading +from typing import Any, Callable + +import customtkinter as ctk + +from .config import Settings, redact_proxy_url +from .pool_ops import PICKER_DISPLAY_CAP, ProxySourceCache + +BG = "#0d0d1a" +CARD = "#12122a" +PANEL = "#1a1a3e" +ACCENT = "#1e3a8a" +ACCENT2 = "#2563eb" +GREEN = "#00e676" +RED = "#ff1744" +YELLOW = "#ffab00" +DIM = "#4a5568" +TEXT = "#e2e8f0" +TEXT2 = "#94a3b8" +FONT = "Segoe UI" +_UI_CHUNK = 50 + +# One cache per app session — avoids re-downloading 800+ URLs every open +_SESSION_CACHE = ProxySourceCache() + + +def show_proxy_picker( + parent: Any, + settings: Settings, + prefer_elite: bool, + on_done: Callable[[list[str], str], None], +) -> None: + """``on_done(urls, mode)`` where mode is ``append`` or ``replace``.""" + win = ctk.CTkToplevel(parent) + win.title("Browse proxy lists") + win.geometry("700x500") + win.configure(fg_color=BG) + win.transient(parent) + win.grab_set() + + status = ctk.CTkLabel( + win, + text=f"Pull loads sources once, then shows {PICKER_DISPLAY_CAP} random proxies per batch.", + font=(FONT, 11), + text_color=TEXT2, + ) + status.pack(fill="x", padx=12, pady=(10, 4)) + + filter_var = ctk.StringVar(value="") + filt_row = ctk.CTkFrame(win, fg_color="transparent") + filt_row.pack(fill="x", padx=12, pady=4) + ctk.CTkLabel(filt_row, text="Filter:", font=(FONT, 10), text_color=TEXT2).pack(side="left") + ctk.CTkEntry( + filt_row, textvariable=filter_var, width=180, height=28, + fg_color=BG, border_color=ACCENT, + ).pack(side="left", padx=6) + + proto_var = ctk.StringVar(value="all") + for label, val in [("All", "all"), ("HTTP", "http"), ("SOCKS5", "socks5")]: + ctk.CTkRadioButton( + filt_row, text=label, variable=proto_var, value=val, + font=(FONT, 10), fg_color=ACCENT2, text_color=TEXT, + ).pack(side="left", padx=4) + + scroll = ctk.CTkScrollableFrame(win, fg_color=CARD, height=300, + scrollbar_button_color=ACCENT) + scroll.pack(fill="both", expand=True, padx=12, pady=6) + + display_proxies: list[str] = [] + check_vars: dict[str, ctk.BooleanVar] = {} + _build_gen = [0] + + def _filtered_urls() -> list[str]: + q = filter_var.get().strip().lower() + pv = proto_var.get() + out: list[str] = [] + for u in display_proxies: + if pv == "http" and not u.startswith("http://"): + continue + if pv == "socks5" and not u.startswith("socks5://"): + continue + if q and q not in u.lower(): + continue + out.append(u) + return out + + def _rebuild_list() -> None: + _build_gen[0] += 1 + gen = _build_gen[0] + for w in scroll.winfo_children(): + w.destroy() + urls = _filtered_urls() + if not urls: + status.configure( + text="No proxies match filter — pull a batch or change filter.", + text_color=TEXT2, + ) + return + + def _chunk(start: int) -> None: + if gen != _build_gen[0]: + return + end = min(start + _UI_CHUNK, len(urls)) + for u in urls[start:end]: + if u not in check_vars: + check_vars[u] = ctk.BooleanVar(value=False) + fr = ctk.CTkFrame(scroll, fg_color=PANEL, corner_radius=4) + fr.pack(fill="x", pady=1) + ctk.CTkCheckBox( + fr, text=redact_proxy_url(u), variable=check_vars[u], + font=("Consolas", 10), fg_color=ACCENT2, text_color=TEXT, + ).pack(anchor="w", padx=8, pady=2) + if end < len(urls): + win.after(1, lambda: _chunk(end)) + else: + cached = _SESSION_CACHE.total_cached + status.configure( + text=f"Showing {len(urls)} of batch ({cached} cached in memory). Select → OK.", + text_color=GREEN, + ) + + _chunk(0) + + def _select_all(on: bool) -> None: + for u in _filtered_urls(): + if u in check_vars: + check_vars[u].set(on) + + def _pull(force_reload: bool = False) -> None: + pull_btn.configure(state="disabled") + reload_btn.configure(state="disabled") + status.configure(text="Loading sources…", text_color=YELLOW) + + def work() -> None: + def stat(msg: str) -> None: + win.after(0, lambda m=msg: status.configure(text=m, text_color=TEXT2)) + + if force_reload: + _SESSION_CACHE.clear() + if not _SESSION_CACHE.loaded: + _SESSION_CACHE.load(list(settings.sources), prefer_elite, on_status=stat) + batch = _SESSION_CACHE.random_batch(PICKER_DISPLAY_CAP) + + def finish() -> None: + nonlocal display_proxies + display_proxies = batch + check_vars.clear() + for u in batch: + check_vars[u] = ctk.BooleanVar(value=False) + pull_btn.configure(state="normal") + reload_btn.configure(state="normal") + _rebuild_list() + + win.after(0, finish) + + threading.Thread(target=work, daemon=True).start() + + def _selected() -> list[str]: + return [u for u, v in check_vars.items() if v.get()] + + def _ok(mode: str) -> None: + sel = _selected() + if not sel: + status.configure(text="Select at least one proxy.", text_color=RED) + return + win.grab_release() + win.destroy() + on_done(sel, mode) + + btn_row = ctk.CTkFrame(win, fg_color="transparent") + btn_row.pack(fill="x", padx=12, pady=(4, 12)) + + pull_btn = ctk.CTkButton( + btn_row, text="↻ Random batch", command=lambda: _pull(False), + width=120, fg_color=ACCENT, hover_color=ACCENT2, + ) + pull_btn.pack(side="left", padx=2) + reload_btn = ctk.CTkButton( + btn_row, text="Reload sources", command=lambda: _pull(True), + width=110, fg_color=DIM, hover_color=ACCENT, + ) + reload_btn.pack(side="left", padx=2) + ctk.CTkButton(btn_row, text="Select all", command=lambda: _select_all(True), + width=72, fg_color=DIM).pack(side="left", padx=2) + ctk.CTkButton(btn_row, text="Clear", command=lambda: _select_all(False), + width=52, fg_color=DIM).pack(side="left", padx=2) + ctk.CTkButton(btn_row, text="Cancel", + command=lambda: (win.grab_release(), win.destroy()), + width=64, fg_color="#7f1d1d").pack(side="right", padx=2) + ctk.CTkButton(btn_row, text="Add to chain", command=lambda: _ok("append"), + width=100, fg_color=GREEN, hover_color=ACCENT2).pack(side="right", padx=2) + ctk.CTkButton(btn_row, text="Replace chain", command=lambda: _ok("replace"), + width=100, fg_color=ACCENT2).pack(side="right", padx=2) + + filter_var.trace_add("write", lambda *_: _rebuild_list()) + proto_var.trace_add("write", lambda *_: _rebuild_list()) + + _pull(False) diff --git a/proxy_chain_manager/secrets_store.py b/proxy_chain_manager/secrets_store.py new file mode 100644 index 0000000..0490881 --- /dev/null +++ b/proxy_chain_manager/secrets_store.py @@ -0,0 +1,160 @@ +"""Windows DPAPI-based secret storage. + +Provides ``encrypt(plaintext: str) -> str`` and ``decrypt(token: str) -> str`` +backed by ``CryptProtectData`` / ``CryptUnprotectData``. + +Why DPAPI: + - Built into Windows; no third-party crypto dependency. + - Key material is tied to the *current user account* (or the machine, when + ``CRYPTPROTECT_LOCAL_MACHINE`` is used) — no passphrase to remember. + - Data encrypted on disk cannot be decrypted by another user / machine. + +Token format: ``DPAPI:v1:`` + +Plain strings that don't start with ``DPAPI:`` are returned unchanged by +``decrypt()`` so the loader can safely walk old plaintext files and re-encrypt +them on the next save (migration is free). + +Falls back gracefully on non-Windows or when DPAPI is unavailable: ``encrypt`` +returns the original string verbatim (logged once). +""" +from __future__ import annotations + +import base64 +import ctypes +import logging +import sys +from ctypes import wintypes + +log = logging.getLogger(__name__) + +_TOKEN_PREFIX = "DPAPI:v1:" + + +# ── Windows API binding ────────────────────────────────────────────────────── + +class _DATA_BLOB(ctypes.Structure): + _fields_ = [ + ("cbData", wintypes.DWORD), + ("pbData", ctypes.POINTER(ctypes.c_char)), + ] + + +def _blob(data: bytes) -> _DATA_BLOB: + buf = ctypes.create_string_buffer(data, len(data)) + return _DATA_BLOB(len(data), ctypes.cast(buf, ctypes.POINTER(ctypes.c_char))) + + +def _is_windows() -> bool: + return sys.platform == "win32" + + +_warned_unavailable = False + + +def _warn_unavailable(reason: str) -> None: + global _warned_unavailable + if not _warned_unavailable: + log.warning("DPAPI secret store unavailable: %s — secrets will be stored in plaintext.", reason) + _warned_unavailable = True + + +# ── Public API ─────────────────────────────────────────────────────────────── + +def is_available() -> bool: + """Return True if the current process can use DPAPI.""" + if not _is_windows(): + return False + try: + ctypes.windll.crypt32 # noqa: B018 — just confirm the DLL is importable + return True + except Exception: # noqa: BLE001 + return False + + +def encrypt(plaintext: str) -> str: + """Encrypt *plaintext* with current-user DPAPI. + + Returns ``DPAPI:v1:`` on success. Returns the original plaintext + on any failure (callers can therefore always trust the return value as the + string to persist). + """ + if not plaintext: + return "" + # Already encrypted — pass through. + if plaintext.startswith(_TOKEN_PREFIX): + return plaintext + if not is_available(): + _warn_unavailable("not running on Windows") + return plaintext + try: + data = plaintext.encode("utf-8") + in_blob = _blob(data) + out_blob = _DATA_BLOB() + ok = ctypes.windll.crypt32.CryptProtectData( + ctypes.byref(in_blob), + "ProxyGodSecret", # description (ignored by us) + None, # optional entropy + None, # reserved + None, # prompt struct + 0, # flags + ctypes.byref(out_blob), + ) + if not ok: + err = ctypes.get_last_error() + _warn_unavailable(f"CryptProtectData failed (err={err})") + return plaintext + try: + enc = ctypes.string_at(out_blob.pbData, out_blob.cbData) + finally: + ctypes.windll.kernel32.LocalFree(out_blob.pbData) + return _TOKEN_PREFIX + base64.b64encode(enc).decode("ascii") + except Exception as exc: # noqa: BLE001 + _warn_unavailable(f"unexpected error: {exc}") + return plaintext + + +def decrypt(token: str) -> str: + """Decrypt a token produced by :func:`encrypt`. + + Plain strings (not starting with the ``DPAPI:`` prefix) are returned + verbatim — this is intentional, it lets the loader transparently read + legacy plaintext files and re-encrypt on save. + """ + if not token: + return "" + if not token.startswith(_TOKEN_PREFIX): + return token # legacy plaintext + if not is_available(): + _warn_unavailable("not running on Windows — cannot decrypt") + return "" + try: + b64 = token[len(_TOKEN_PREFIX):] + data = base64.b64decode(b64.encode("ascii")) + in_blob = _blob(data) + out_blob = _DATA_BLOB() + ok = ctypes.windll.crypt32.CryptUnprotectData( + ctypes.byref(in_blob), + None, # ppszDataDescr + None, # entropy + None, # reserved + None, # prompt struct + 0, # flags + ctypes.byref(out_blob), + ) + if not ok: + err = ctypes.get_last_error() + log.warning("CryptUnprotectData failed (err=%s) — secret stayed encrypted.", err) + return "" + try: + plain = ctypes.string_at(out_blob.pbData, out_blob.cbData) + finally: + ctypes.windll.kernel32.LocalFree(out_blob.pbData) + return plain.decode("utf-8", errors="replace") + except Exception as exc: # noqa: BLE001 + log.warning("DPAPI decrypt error: %s", exc) + return "" + + +def is_encrypted(token: str) -> bool: + return bool(token) and token.startswith(_TOKEN_PREFIX) diff --git a/proxy_chain_manager/service.py b/proxy_chain_manager/service.py new file mode 100644 index 0000000..3ff6fc4 --- /dev/null +++ b/proxy_chain_manager/service.py @@ -0,0 +1,949 @@ +from __future__ import annotations + +import atexit +import asyncio +import logging +import random +import signal +import threading +import time +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor +from typing import Any + +from .config import ( + Settings, + load_settings, + normalize_proxy_url, + redact_proxy_url, + save_settings, +) +from .dns_leak import flush_dns_cache +from .fetcher import fetch_proxy_json, normalize_entries +from .fingerprint import ( + apply_webrtc_hardening, + disable_ipv6_on_adapters, + enable_ipv6_on_adapters, + get_computer_name, + random_hostname, + set_computer_name, +) +from .firewall import disengage as fw_disengage, emergency_disengage as fw_emergency_disengage, engage as fw_engage, is_admin +from .gost_util import build_gost_cmd, ensure_gost, popen_no_window, read_gost_log_tail, terminate_process +from .leak_detect import is_chain_leak, leak_reason +from .mac_spoof import restore_macs, spoof_all_physical +from .privacy_lan import ( + LanSnapshot, + engage_lan_lockdown, + restore_lan, +) +from .telemetry_kill import ( + TelemetrySnapshot, + engage_telemetry_kill, + restore_telemetry, +) +from .sysproxy import ( + clear_system_proxy, + detect_policy_overrides, + diagnose_system_proxy, + is_system_proxy_set, + set_system_proxy, +) +from .validator import ( + check_chain_exit_ip, + check_https_tunnel, + get_direct_ip, + validate_proxies, +) +from .vpn_detect import VpnStatus, detect_vpn +from .win_compat import probe as _win_probe + +log = logging.getLogger(__name__) + +Notify = Callable[[dict[str, Any]], None] + +# Thread pool for blocking I/O (proxy list fetching) so we don't stall asyncio +_FETCH_POOL = ThreadPoolExecutor(max_workers=8, thread_name_prefix="fetcher") + + +# Back-compat for tests +from .leak_detect import is_same_subnet as _is_same_network # noqa: F401 + + +async def _probe_tcp(host: str, port: int, timeout: float = 6.0) -> bool: + """Open and close a TCP socket; True if the handshake completes within *timeout*.""" + try: + reader, writer = await asyncio.wait_for( + asyncio.open_connection(host, port), timeout=timeout, + ) + writer.close() + try: + await writer.wait_closed() + except Exception: # noqa: BLE001 + pass + return True + except Exception: # noqa: BLE001 + return False + + +class ChainService: + """Background rotating proxy chain using GOST.""" + + def __init__(self, notify: Notify) -> None: + self._notify = notify + self._stop = threading.Event() + self._force_rotate = threading.Event() + self._thread: threading.Thread | None = None + self._proc = None + self._settings_lock = threading.Lock() + self._settings = load_settings() + self._current_chain: list[str] = [] + # Sticky-exit: while time.monotonic() < self._sticky_until, leak-based + # auto-rotation is skipped so signup flows keep the same egress IP. + self._sticky_until: float = 0.0 + + # Shuffle-and-drain pool state — never repeat a proxy within a cycle + self._available: list[str] = [] # proxies not yet used this cycle + self._used: set[str] = set() # proxies used this cycle + # Per-session blacklist: proxies that crashed GOST immediately + self._blacklist: set[str] = set() + + self._vpn: VpnStatus = VpnStatus() + self._mac_originals: dict[str, str] = {} + self._hostname_original: str | None = None + self._ipv6_adapters: list[str] = [] + self._webrtc_was_applied: bool = False + self._lan_snap: LanSnapshot | None = None + self._telemetry_snap: TelemetrySnapshot | None = None + + # Register emergency firewall disengage so a crash can't leave the + # kill-switch permanently engaged. + atexit.register(fw_emergency_disengage) + for _sig in (signal.SIGTERM, signal.SIGINT): + try: + signal.signal(_sig, self._signal_handler) + except (OSError, ValueError): + pass # not on main thread or unsupported on this platform + + def _signal_handler(self, signum: int, frame: Any) -> None: + """Received SIGTERM/SIGINT — stop cleanly and ensure firewall is disengaged.""" + log.warning("Signal %s received — stopping ChainService.", signum) + self._stop.set() + terminate_process(self._proc) + fw_emergency_disengage() + + def _manual_exit_url(self) -> str | None: + with self._settings_lock: + u = normalize_proxy_url(self._settings.manual_exit_proxy) + return u if u else None + + @property + def settings(self) -> Settings: + with self._settings_lock: + return self._settings + + @property + def current_chain(self) -> list[str]: + return list(self._current_chain) + + def update_settings(self, s: Settings) -> None: + with self._settings_lock: + self._settings = s + save_settings(s) + + def start(self) -> None: + if self._thread and self._thread.is_alive(): + return + compat = _win_probe() + self._notify({"type": "log", "text": f"Host: {compat.summary()}"}) + if not compat.has_net_cmdlets: + self._notify({ + "type": "log", + "text": ( + "Legacy PowerShell detected — MAC spoof / IPv6 disable / " + "hostname spoof unavailable on this host. Proxy chain + " + "kill-switch + WebRTC policy still work." + ), + }) + self._stop.clear() + self._thread = threading.Thread( + target=self._run_thread, name="ChainService", daemon=True + ) + self._thread.start() + + def stop(self) -> None: + self._stop.set() + terminate_process(self._proc) + self._proc = None + if self._thread: + self._thread.join(timeout=15) + if self._thread.is_alive(): + log.warning("ChainService thread did not exit in 15s — forcing firewall disengage.") + fw_emergency_disengage() + self._teardown_network() + self._notify({"type": "state", "running": False}) + + def rotate_now(self) -> None: + self._force_rotate.set() + + def set_sticky(self, seconds: float) -> None: + """Hold current exit for ``seconds`` (suppresses leak-driven rotation). + + Manual rotation still works. Pass 0 to clear sticky mode immediately. + """ + if seconds <= 0: + self._sticky_until = 0.0 + return + self._sticky_until = time.monotonic() + float(seconds) + + def sticky_remaining(self) -> float: + """Seconds remaining on sticky-exit hold (0.0 if not sticky).""" + rem = self._sticky_until - time.monotonic() + return rem if rem > 0 else 0.0 + + # ───────────────────────────────────────────────────────────────────────── + + def _teardown_network(self) -> None: + clear_system_proxy() + self._notify({"type": "log", "text": "System proxy cleared."}) + self._restore_privacy() + with self._settings_lock: + ks_enabled = self._settings.kill_switch_enabled + if is_admin() and ks_enabled: + ok, msg = fw_disengage() + self._notify({"type": "log", "text": msg}) + self._notify({"type": "firewall", "engaged": False}) + + def _apply_privacy(self) -> None: + s = self._settings + if s.mac_spoof_enabled and is_admin(): + self._mac_originals, logs = spoof_all_physical(self._mac_originals or None) + for ln in logs: + self._notify({"type": "log", "text": f"MAC: {ln}"}) + elif s.mac_spoof_enabled: + self._notify({"type": "log", "text": "MAC spoof enabled but not Admin — skipped."}) + + if s.spoof_hostname_enabled and is_admin(): + self._hostname_original = get_computer_name() + new_name = random_hostname() + ok, msg = set_computer_name(new_name) + self._notify({"type": "log", "text": f"Hostname: {msg}"}) + elif s.spoof_hostname_enabled: + self._notify({"type": "log", "text": "Hostname spoof enabled but not Admin — skipped."}) + + if s.disable_ipv6_while_active and is_admin(): + self._ipv6_adapters, logs = disable_ipv6_on_adapters() + for ln in logs: + self._notify({"type": "log", "text": ln}) + elif s.disable_ipv6_while_active: + self._notify({"type": "log", "text": "IPv6 disable enabled but not Admin — skipped."}) + + if s.harden_webrtc_enabled and is_admin(): + ok, msg = apply_webrtc_hardening(True) + self._webrtc_was_applied = ok + self._notify({"type": "log", "text": msg}) + elif s.harden_webrtc_enabled: + self._notify({"type": "log", "text": "WebRTC hardening enabled but not Admin — skipped."}) + + if s.lan_lockdown_enabled and is_admin(): + snap, logs = engage_lan_lockdown() + self._lan_snap = snap + for ln in logs: + self._notify({"type": "log", "text": f"LAN: {ln}"}) + elif s.lan_lockdown_enabled: + self._notify({"type": "log", "text": "LAN lockdown enabled but not Admin — skipped."}) + + if s.telemetry_kill_enabled and is_admin(): + tsnap, tlogs = engage_telemetry_kill() + self._telemetry_snap = tsnap + for ln in tlogs[:8]: + self._notify({"type": "log", "text": f"Telemetry: {ln}"}) + if len(tlogs) > 8: + self._notify({"type": "log", "text": f"Telemetry: … +{len(tlogs) - 8} more changes."}) + elif s.telemetry_kill_enabled: + self._notify({"type": "log", "text": "Telemetry kill enabled but not Admin — skipped."}) + + def _restore_privacy(self) -> None: + if self._mac_originals: + for ln in restore_macs(self._mac_originals): + self._notify({"type": "log", "text": f"MAC restore: {ln}"}) + self._mac_originals.clear() + if self._hostname_original and is_admin(): + ok, msg = set_computer_name(self._hostname_original) + self._notify({"type": "log", "text": f"Hostname restore: {msg}"}) + self._hostname_original = None + if self._ipv6_adapters: + for ln in enable_ipv6_on_adapters(self._ipv6_adapters): + self._notify({"type": "log", "text": ln}) + self._ipv6_adapters.clear() + if self._webrtc_was_applied and is_admin(): + apply_webrtc_hardening(False) + self._webrtc_was_applied = False + self._notify({"type": "log", "text": "WebRTC policy restored."}) + if self._lan_snap is not None and is_admin(): + for ln in restore_lan(self._lan_snap): + self._notify({"type": "log", "text": f"LAN: {ln}"}) + self._lan_snap = None + if self._telemetry_snap is not None and is_admin(): + for ln in restore_telemetry(self._telemetry_snap)[:8]: + self._notify({"type": "log", "text": f"Telemetry: {ln}"}) + self._telemetry_snap = None + + def _run_thread(self) -> None: + try: + asyncio.run(self._async_main()) + except Exception: + log.exception("service thread failed") + self._notify({"type": "log", "text": "Fatal error in service thread (see log)."}) + finally: + terminate_process(self._proc) + self._proc = None + self._teardown_network() + self._notify({"type": "state", "running": False}) + + # ───────────────────────────────────────────────────────────────────────── + # MAIN ASYNC LOOP + # ───────────────────────────────────────────────────────────────────────── + + async def _async_main(self) -> None: + self._notify({"type": "state", "running": True}) + log.info( + "Service start: chain_length=%d mode=%s sources=%d pinned=%s kill_switch=%s", + self._settings.chain_length, + self._settings.obfuscation_mode, + len(self._settings.sources), + self._settings.use_pinned_chain, + self._settings.kill_switch_enabled, + ) + + # ── GOST setup ─────────────────────────────────────────────────────── + try: + gost = ensure_gost() + self._notify({"type": "log", "text": f"GOST ready: {gost}"}) + except Exception as e: + self._notify({"type": "log", "text": f"GOST setup failed: {e}"}) + return + + if gost.stat().st_size < 10_000: + self._notify({"type": "log", "text": "GOST appears quarantined. Re-downloading..."}) + gost.unlink(missing_ok=True) + try: + gost = ensure_gost() + except Exception as e: + self._notify({"type": "log", "text": f"GOST re-download failed: {e}"}) + return + + # ── VPN + direct IP ─────────────────────────────────────────────────── + self._vpn = detect_vpn() + mode = "VPN-aware (/16)" if self._vpn.active else "strict (exact IP)" + self._notify({ + "type": "vpn", + "active": self._vpn.active, + "label": self._vpn.label, + "adapter": self._vpn.adapter, + "leak_mode": mode, + }) + self._notify({ + "type": "log", + "text": ( + f"VPN: {self._vpn.label}" + + (f" ({self._vpn.adapter})" if self._vpn.adapter else "") + + f" — leak check: {mode}" + ), + }) + + # Warm-up: retry the direct-IP lookup a few times so a transient + # network blip during startup doesn't put us in permanent fail-closed. + real_ip = None + for attempt in range(3): + real_ip = await get_direct_ip(self._settings.ip_check_url) + if real_ip: + break + if attempt < 2: + await asyncio.sleep(2.0) + if real_ip: + self._notify({"type": "real_ip", "ip": real_ip}) + label = "direct/VPN IP" if self._vpn.active else "your real IP" + self._notify({"type": "log", "text": f"{label.capitalize()}: {real_ip}"}) + else: + # Leak detection now FAILS CLOSED on unknown real_ip — any chain + # whose exit IP can't be compared against a baseline will be + # treated as a leak and rotated. Make the operator aware up front. + self._notify({"type": "log", "text": ( + "Could not determine direct IP after 3 attempts — leak detection " + "will FAIL CLOSED (rotate on any chain whose exit IP cannot be " + "verified). Fix internet / ip_check_url to restore." + )}) + + self._apply_privacy() + + # ── Firewall kill-switch ────────────────────────────────────────────── + if self._settings.kill_switch_enabled: + if is_admin(): + ok, msg = fw_engage(gost) + self._notify({"type": "log", "text": msg}) + self._notify({"type": "firewall", "engaged": ok}) + else: + self._notify({"type": "log", "text": "Kill-switch skipped (not Admin)."}) + self._notify({"type": "firewall", "engaged": False}) + else: + self._notify({"type": "log", "text": "Kill-switch disabled in settings."}) + self._notify({"type": "firewall", "engaged": False}) + + log.debug( + "Listener %s | refresh=%ds health=%ds max_candidates=%d concurrency=%d", + self._settings.listen_addr(), + int(self._settings.full_refresh_seconds), + int(self._settings.health_check_seconds), + int(self._settings.max_candidates), + int(self._settings.validation_concurrency), + ) + + # ── Main rotation loop ──────────────────────────────────────────────── + full_pool: list[str] = [] + last_full = 0.0 + rotation_num = 0 + + while not self._stop.is_set(): + now = time.monotonic() + need_refresh = ( + not full_pool + or now - last_full >= float(self._settings.full_refresh_seconds) + ) + log.debug( + "Loop tick: full_pool=%d available=%d used=%d blacklist=%d need_refresh=%s age=%.0fs", + len(full_pool), + len(self._available), + len(self._used), + len(self._blacklist), + need_refresh, + (now - last_full) if full_pool else 0.0, + ) + + # 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) + + # Optional per-hop TCP probe so a known-dead pinned chain + # doesn't loop forever on the 10 s retry below. + if self._settings.validate_pinned_on_start: + from urllib.parse import urlparse as _urlparse + dead = [] + for hop in chain: + try: + p = _urlparse(hop if "://" in hop else "http://" + hop) + if p.hostname and p.port: + if not await _probe_tcp(p.hostname, int(p.port), 6.0): + dead.append(hop) + except Exception: # noqa: BLE001 + dead.append(hop) + if dead: + self._notify({"type": "log", "text": ( + f"Pinned chain: {len(dead)}/{len(chain)} hop(s) failed TCP " + f"probe — proceeding anyway (will retry on failure)." + )}) + + rotation_num += 1 + self._notify({"type": "rotation", "n": rotation_num}) + log.info("Pinned chain run: %d hops", len(chain)) + ok = await self._run_chain(gost, chain, real_ip) + if not ok: + await self._sleep_interruptible(10) + if self._stop.is_set(): + break + continue + + # Fixed exit only: hop count = 1 → chain is only the manual exit (no pool) + mex = self._manual_exit_url() + if mex and not self._settings.use_pinned_chain and self._settings.chain_length == 1: + rotation_num += 1 + self._notify({"type": "rotation", "n": rotation_num}) + ok = await self._run_chain(gost, [mex], real_ip) + if not ok: + await self._sleep_interruptible(15) + if self._stop.is_set(): + break + continue + + # ── Pool refresh ───────────────────────────────────────────────── + if need_refresh: + self._notify({"type": "phase", "phase": "fetch"}) + log.info("Pool refresh: fetching %d source(s)…", len(self._settings.sources)) + full_pool = await self._build_pool() + last_full = time.monotonic() + self._available = list(full_pool) + random.shuffle(self._available) + self._used.clear() + self._notify({"type": "pool", "count": len(full_pool)}) + + if not full_pool: + self._notify({"type": "log", "text": "Empty pool — retrying in 60s..."}) + await self._sleep_interruptible(60) + last_full = 0.0 + continue + + # ── Pick next chain (shuffle-and-drain, no repeats per cycle) ──── + chain = self._pick_chain() + if not chain: + # Pool exhausted for this cycle — reshuffle and restart + self._notify({"type": "log", "text": "Pool cycle complete — reshuffling for next round."}) + self._available = list(full_pool) + random.shuffle(self._available) + self._used.clear() + chain = self._pick_chain() + if not chain: + await self._sleep_interruptible(15) + continue + + rotation_num += 1 + self._notify({"type": "rotation", "n": rotation_num}) + log.info( + "Auto chain #%d: %d hops | obfuscation=%s | pool_remain=%d", + rotation_num, + len(chain), + self._settings.obfuscation_mode, + len(self._available), + ) + await self._run_chain(gost, chain, real_ip) + + if self._stop.is_set(): + break + + log.info("Main loop exit (stop requested or fatal).") + terminate_process(self._proc) + self._proc = None + + # ───────────────────────────────────────────────────────────────────────── + # CHAIN RUNNER + # ───────────────────────────────────────────────────────────────────────── + + async def _run_chain( + self, gost: Any, chain: list[str], real_ip: str | None + ) -> bool: + """Start GOST with chain, verify exit IP, monitor until rotation/stop. + Returns True if chain ran successfully, False if it immediately failed.""" + self._current_chain = list(chain) + self._notify({"type": "hops", "hops": chain, "status": "connecting"}) + self._notify({"type": "phase", "phase": "gost_start"}) + if self._settings.flush_dns_on_rotate: + ok, msg = flush_dns_cache() + if ok: + log.debug("DNS cache flushed before chain run") + # Per-rotation MAC re-randomization (only if mac_spoof is also on, since + # without spoof there's no original snapshot we own to mutate). + if ( + self._settings.mac_rotate_on_chain_rotate + and self._settings.mac_spoof_enabled + and is_admin() + and self._mac_originals + ): + _, logs = spoof_all_physical(self._mac_originals) + for ln in logs: + self._notify({"type": "log", "text": f"MAC rotate: {ln}"}) + listen = self._settings.listen_addr() + cmd = build_gost_cmd(gost, listen, chain) + self._notify({ + "type": "log", + "text": f"Chain #{len(self._used) // max(1, self._settings.chain_length)}: " + + " → ".join(self._short(h) for h in chain), + }) + red = " | ".join(redact_proxy_url(h) for h in chain) + log.debug("GOST listen=http://%s | forwards (redacted): %s", listen, red) + log.debug("GOST argv: %s … (%d args)", cmd[0], len(cmd)) + + terminate_process(self._proc) + self._proc = popen_no_window(cmd) + self._notify({"type": "log", "text": "GOST started — warming up (2s)…"}) + + for _ in range(4): + if self._stop.is_set(): + terminate_process(self._proc) + self._proc = None + return False + await asyncio.sleep(0.5) + + if self._proc.poll() is not None: + # GOST died immediately — blacklist pool proxies (never blacklist user fixed exit) + 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] + self._notify({"type": "hops", "hops": chain, "status": "dead", "exit_ip": None}) + self._notify({"type": "log", "text": "GOST exited immediately — proxies blacklisted."}) + tail = read_gost_log_tail(20) + if tail.strip(): + for ln in tail.splitlines()[-8:]: + if ln.strip(): + self._notify({"type": "log", "text": f" GOST> {ln.rstrip()}"}) + return False + + local_proxy = f"http://{listen}" + timeout = min(30.0, self._settings.validation_timeout_seconds + 12.0) + self._notify({"type": "phase", "phase": "verify_chain"}) + self._notify({ + "type": "log", + "text": f"Exit IP check through local proxy (≤{int(timeout * 2 + 5)}s)…", + }) + t0 = time.monotonic() + exit_ip = await check_chain_exit_ip( + local_proxy, self._settings.ip_check_url, timeout, chain_hops=len(chain) + ) + log.debug("Initial exit IP check took %.2fs → %s", time.monotonic() - t0, exit_ip or "none") + + if not exit_ip: + self._notify({"type": "hops", "hops": chain, "status": "dead", "exit_ip": None}) + self._notify({"type": "log", "text": "Chain IP check failed. Rotating."}) + terminate_process(self._proc) + self._proc = None + return False + + if is_chain_leak(exit_ip, real_ip, self._vpn.active): + reason = leak_reason(exit_ip, real_ip, self._vpn.active) + self._notify({"type": "hops", "hops": chain, "status": "dead", "exit_ip": exit_ip}) + self._notify({"type": "log", "text": f"Leak detected — {reason}. Rotating."}) + log.warning("Chain leak: exit=%s real=%s vpn=%s — %s", exit_ip, real_ip, self._vpn.active, reason) + # Blacklist the whole chain so we don't reuse broken proxies + 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 + + # 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}"}) + 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). + pol_before = detect_policy_overrides() + if pol_before: + self._notify({"type": "log", "text": ( + f"Group Policy proxy lock detected ({len(pol_before)} entries) — " + "these BEAT our settings. Browsers will keep the policy proxy " + "(or DIRECT) until those keys are removed." + )}) + for p in pol_before[:3]: + self._notify({"type": "log", "text": f" ! {p}"}) + + set_system_proxy( + self._settings.local_host, + self._settings.local_port, + self._settings.proxy_bypass, + ) + applied = is_system_proxy_set() + self._notify({ + "type": "log", + "text": ( + f"System proxy → {self._settings.listen_addr()} " + f"(HKCU + HKLM(adm) + Connections + WinHTTP) " + f"{'OK' if applied else 'FAILED — registry write rejected'}" + ), + }) + # Post-flight diagnostic — every layer's actual state. + for ln in diagnose_system_proxy(): + self._notify({"type": "log", "text": f" proxy: {ln}"}) + + # ── Health monitor loop ─────────────────────────────────────────────── + hc = int(self._settings.health_check_seconds) + while not self._stop.is_set(): + log.debug("Health sleep: %ds until next exit check", hc) + result = await self._wait_health_interval() + if result in ("stop", "rotate"): + log.debug("Health loop break: %s", result) + break + + if self._proc is None or self._proc.poll() is not None: + self._notify({"type": "log", "text": "GOST process died — rebuilding."}) + break + + self._notify({"type": "log", "text": "Health check..."}) + t1 = time.monotonic() + exit_ip = await check_chain_exit_ip( + local_proxy, self._settings.ip_check_url, timeout, chain_hops=len(chain) + ) + log.debug("Periodic exit IP check %.2fs → %s", time.monotonic() - t1, exit_ip or "none") + + if is_chain_leak(exit_ip, real_ip, self._vpn.active): + reason = leak_reason(exit_ip, real_ip, self._vpn.active) + rem = self.sticky_remaining() + if rem > 0: + self._notify({ + "type": "log", + "text": ( + f"Health check flagged ({reason}) — STICKY EXIT active " + f"({int(rem)}s left), skipping rotation." + ), + }) + self._notify({"type": "hops", "hops": chain, "status": "healthy", "exit_ip": exit_ip}) + continue + self._notify({"type": "hops", "hops": chain, "status": "dead", "exit_ip": exit_ip}) + self._notify({"type": "log", "text": f"Health check failed ({reason}) — rotating."}) + break + + self._notify({"type": "hops", "hops": chain, "status": "healthy", "exit_ip": exit_ip}) + self._notify({"type": "log", "text": f"✓ Still healthy — Exit IP: {exit_ip}"}) + + terminate_process(self._proc) + self._proc = None + return True + + # ───────────────────────────────────────────────────────────────────────── + # POOL MANAGEMENT + # ───────────────────────────────────────────────────────────────────────── + + @staticmethod + def _apply_mode_filter(candidates: list[str], mode: str) -> list[str]: + if mode == "http_only": + return [u for u in candidates if u.startswith("http://")] + if mode == "socks5_only": + return [u for u in candidates if u.startswith("socks5://")] + if mode == "random_mix": + out = list(candidates) + random.shuffle(out) + return out + return list(candidates) + + def _pick_chain(self) -> list[str]: + """Pick chain_length hops: optional fixed last hop + random prefix from pool.""" + s = self._settings + chain = self._pick_chain_for_mode(s.obfuscation_mode) + if chain: + log.debug( + "Picked chain len=%d mode=%s available_left=%d", + len(chain), + s.obfuscation_mode, + len(self._available), + ) + return chain + if s.obfuscation_mode != "auto" and not s.use_pinned_chain: + log.debug("Pick empty under mode=%s — retrying as auto", s.obfuscation_mode) + self._notify({ + "type": "log", + "text": "Obfuscation filter left nothing usable — retrying this pick with ALL protocols (auto).", + }) + ch2 = self._pick_chain_for_mode("auto") + if ch2: + log.debug("Auto retry picked len=%d", len(ch2)) + return ch2 + log.debug("Pick chain returned empty (pool exhausted?)") + return [] + + def _pick_chain_for_mode(self, mode: str) -> list[str]: + """Build one chain using given mode (http_only / socks5_only / random_mix / auto).""" + s = self._settings + k = max(1, s.chain_length) + manual = self._manual_exit_url() + if manual and not s.use_pinned_chain: + mid_need = k - 1 + base = [ + u for u in self._available + if u not in self._blacklist and u != manual + ] + candidates = self._apply_mode_filter(base, mode) + + if mid_need == 0: + return [manual] + if len(candidates) < mid_need: + return [] + + prefix = candidates[:mid_need] + pset = set(prefix) + self._available = [u for u in self._available if u not in pset] + self._used.update(prefix) + self._used.add(manual) + return prefix + [manual] + + base = [u for u in self._available if u not in self._blacklist] + candidates = self._apply_mode_filter(base, mode) + + if len(candidates) < k: + return [] + + picked = candidates[:k] + picked_set = set(picked) + self._available = [u for u in self._available if u not in picked_set] + self._used.update(picked) + return picked + + async def _build_pool(self) -> list[str]: + """Fetch and validate proxies. Runs blocking I/O in thread pool.""" + s = self._settings + loop = asyncio.get_running_loop() + log.info("_build_pool: %d source URL(s), prefer_elite=%s", len(s.sources), s.prefer_elite) + + # Fetch all sources concurrently in thread pool (they are blocking) + async def _fetch_one(url: str) -> list[str]: + try: + rows = await asyncio.wait_for( + loop.run_in_executor( + _FETCH_POOL, + lambda u=url: fetch_proxy_json(u, timeout=50.0), + ), + timeout=55.0, + ) + entries = normalize_entries(rows, s.prefer_elite) + if s.prefer_elite and rows and not entries: + self._notify({ + "type": "log", + "text": ( + f"Source returned {len(rows)} proxies but none marked “elite” — all skipped. " + "Turn off “Elite proxies only” in Chain Builder if every fetch is empty." + ), + }) + self._notify({"type": "log", "text": f"Fetched {len(entries)} from source."}) + return entries + except asyncio.TimeoutError: + self._notify({"type": "log", "text": f"Fetch timed out (55s): {url[:60]}..."}) + return [] + except Exception as e: + self._notify({"type": "log", "text": f"Fetch error: {e!s}"}) + return [] + + src_urls = list(s.sources) if s.sources else list(Settings().sources) + if not s.sources: + self._notify({ + "type": "log", + "text": "Settings had no proxy sources — using built-in defaults (save Settings to persist).", + }) + if not src_urls: + self._notify({"type": "log", "text": "No proxy source URLs configured — cannot build a pool."}) + return [] + + results = await asyncio.gather(*(_fetch_one(u) for u in src_urls)) + raw_urls: list[str] = [] + for chunk in results: + raw_urls.extend(chunk) + + if not raw_urls: + self._notify({ + "type": "log", + "text": "No proxies fetched from any source (check network, URLs, or turn off “Elite only” if every list is filtered empty).", + }) + return [] + + # Deduplicate, remove blacklisted, shuffle before capping + seen: set[str] = set() + unique: list[str] = [] + for u in raw_urls: + if u not in seen and u not in self._blacklist: + seen.add(u) + unique.append(u) + + random.shuffle(unique) + if len(unique) > s.max_candidates: + unique = unique[: s.max_candidates] + log.debug("Unique candidates after dedupe/cap: %d (max_candidates=%d)", len(unique), s.max_candidates) + + self._notify({"type": "phase", "phase": "validate"}) + self._notify({"type": "log", "text": f"Validating {len(unique)} candidates..."}) + + def on_prog(done: int, total: int) -> None: + self._notify({"type": "validate_progress", "done": done, "total": total}) + + target = max(s.min_pool_size, s.chain_length * 3) + good = await validate_proxies( + unique, + s.ip_check_url, + s.validation_concurrency, + s.validation_timeout_seconds, + on_progress=on_prog, + target=target, + ) + random.shuffle(good) + + mex = normalize_proxy_url(s.manual_exit_proxy) + if mex and not s.use_pinned_chain: + self._notify({"type": "phase", "phase": "exit_check"}) + self._notify({"type": "log", "text": "Validating fixed exit proxy…"}) + v = await validate_proxies( + [mex], + s.ip_check_url, + 1, + s.validation_timeout_seconds, + on_progress=None, + ) + if v: + self._notify({"type": "log", "text": "Fixed exit proxy: OK (reachable)."}) + else: + self._notify({ + "type": "log", + "text": "Fixed exit proxy: validation failed — will still attempt; check URL/credentials.", + }) + + self._notify({"type": "log", "text": f"Valid proxies: {len(good)} / {len(unique)}"}) + log.info("_build_pool done: valid=%d / tested=%d", len(good), len(unique)) + return good + + # ───────────────────────────────────────────────────────────────────────── + # UTILITIES + # ───────────────────────────────────────────────────────────────────────── + + async def _wait_health_interval(self) -> str | None: + """Sleep for health_check_seconds. Returns 'stop', 'rotate', or None.""" + total = float(self._settings.health_check_seconds) + end = time.monotonic() + total + while time.monotonic() < end: + if self._stop.is_set(): + return "stop" + if self._force_rotate.is_set(): + self._force_rotate.clear() + self._notify({"type": "log", "text": "Manual rotate triggered."}) + if self._settings.flush_dns_on_rotate: + ok, msg = flush_dns_cache() + self._notify({"type": "log", "text": f"DNS flush: {msg}" if ok else f"DNS flush failed: {msg}"}) + return "rotate" + remaining = end - time.monotonic() + self._notify({"type": "countdown", "secs": max(0, int(remaining))}) + await asyncio.sleep(1.0) + self._notify({"type": "countdown", "secs": 0}) + return None + + async def _sleep_interruptible(self, seconds: float) -> None: + end = time.monotonic() + seconds + while time.monotonic() < end: + if self._stop.is_set(): + return + await asyncio.sleep(0.2) + + @staticmethod + def _short(url: str) -> str: + url = redact_proxy_url(url) + url = ( + url.replace("http://", "") + .replace("socks5://", "s5://") + .replace("socks4://", "s4://") + .replace("https://", "") + ) + return url[:30] + "…" if len(url) > 32 else url diff --git a/proxy_chain_manager/signup_extension/content.js b/proxy_chain_manager/signup_extension/content.js new file mode 100644 index 0000000..2132e37 --- /dev/null +++ b/proxy_chain_manager/signup_extension/content.js @@ -0,0 +1,153 @@ +(function () { + "use strict"; + + var cfg = null; + var filledKeys = {}; + var startedAt = Date.now(); + var MAX_MS = 120000; + + function loadConfig(cb) { + try { + var rt = typeof browser !== "undefined" ? browser : chrome; + var url = rt.runtime.getURL("autofill_config.json"); + fetch(url, { cache: "no-store" }) + .then(function (r) { return r.json(); }) + .then(function (j) { cb(j || null); }) + .catch(function () { cb(null); }); + } catch (e) { + cb(null); + } + } + + function hostMatches(host, patterns) { + if (!patterns || !patterns.length) return true; + host = (host || "").toLowerCase(); + for (var i = 0; i < patterns.length; i++) { + var p = String(patterns[i] || "").toLowerCase(); + if (!p) continue; + if (p.indexOf("*") >= 0) { + var re = new RegExp("^" + p.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"); + if (re.test(host)) return true; + } else if (host === p || host.endsWith("." + p)) { + return true; + } + } + return false; + } + + function setValue(el, value) { + if (!el || value == null || value === "") return false; + try { + el.focus(); + el.value = value; + el.dispatchEvent(new Event("input", { bubbles: true })); + el.dispatchEvent(new Event("change", { bubbles: true })); + return true; + } catch (e) { + return false; + } + } + + function trySelectors(selectors, value) { + if (!selectors || !value) return false; + for (var i = 0; i < selectors.length; i++) { + var nodes = document.querySelectorAll(selectors[i]); + for (var j = 0; j < nodes.length; j++) { + var el = nodes[j]; + if (!el || el.disabled || el.readOnly) continue; + if (el.type === "hidden") continue; + if (setValue(el, value)) return true; + } + } + return false; + } + + function runFill() { + if (!cfg || !cfg.fields) return; + if (Date.now() - startedAt > MAX_MS) return; + var host = location.hostname || ""; + if (cfg.hosts && cfg.hosts.length && !hostMatches(host, cfg.hosts)) return; + + var fields = cfg.fields; + var keys = Object.keys(fields); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var val = fields[key]; + if (!val) continue; + if (filledKeys[key]) continue; + if (trySelectors(fields[key + "_selectors"] || defaultSelectors(key), val)) { + filledKeys[key] = true; + } + } + } + + function defaultSelectors(key) { + var map = { + email: [ + 'input[type="email"]', + 'input[name="email"]', + 'input[name="Email"]', + 'input[id="email"]', + 'input[autocomplete="email"]', + 'input[autocomplete="username"]', + 'input[name="identifier"]' + ], + password: [ + 'input[type="password"]:not([name*="Confirm"]):not([name*="confirm"]):not([name*="Again"]):not([name*="again"])', + 'input[name="password"]', + 'input[name="Passwd"]', + 'input[id="password"]', + 'input[autocomplete="new-password"]' + ], + password_confirm: [ + 'input[name="PasswdAgain"]', + 'input[name="password_confirm"]', + 'input[name="confirmPassword"]', + 'input[name="passwordConfirmation"]', + 'input[autocomplete="new-password"]:nth-of-type(2)' + ], + first_name: [ + 'input[name="firstName"]', + 'input[name="firstname"]', + 'input[name="first_name"]', + 'input[autocomplete="given-name"]' + ], + last_name: [ + 'input[name="lastName"]', + 'input[name="lastname"]', + 'input[name="last_name"]', + 'input[autocomplete="family-name"]' + ], + username: [ + 'input[name="username"]', + 'input[name="Username"]', + 'input[id="username"]', + 'input[autocomplete="username"]' + ] + }; + return map[key] || []; + } + + function boot() { + loadConfig(function (j) { + cfg = j; + if (!cfg) return; + runFill(); + var obs = new MutationObserver(function () { runFill(); }); + obs.observe(document.documentElement || document.body, { + childList: true, + subtree: true + }); + var timer = setInterval(function () { + runFill(); + if (Date.now() - startedAt > MAX_MS) clearInterval(timer); + }, 900); + }); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", boot); + } else { + boot(); + } +})(); diff --git a/proxy_chain_manager/signup_extension/manifest.json b/proxy_chain_manager/signup_extension/manifest.json new file mode 100644 index 0000000..d6563b6 --- /dev/null +++ b/proxy_chain_manager/signup_extension/manifest.json @@ -0,0 +1,21 @@ +{ + "manifest_version": 2, + "name": "Proxy God Signup Autofill", + "version": "1.0.0", + "description": "Autofills signup forms from Proxy God (manual submit + captcha).", + "applications": { + "gecko": { + "id": "signup-autofill@proxygod" + } + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_idle", + "all_frames": true + } + ], + "permissions": [""], + "web_accessible_resources": ["autofill_config.json"] +} diff --git a/proxy_chain_manager/signup_prep.py b/proxy_chain_manager/signup_prep.py new file mode 100644 index 0000000..6fdd85c --- /dev/null +++ b/proxy_chain_manager/signup_prep.py @@ -0,0 +1,444 @@ +from __future__ import annotations + +import json +import secrets +import shutil +import string +import uuid +from dataclasses import asdict, dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from .paths import app_data_dir + +_EXT_ID = "signup-autofill@proxygod" + + +def _ext_src_path() -> Path: + """Locate the signup_extension dir whether running from source or frozen.""" + import sys + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + return Path(sys._MEIPASS) / "proxy_chain_manager" / "signup_extension" + return Path(__file__).resolve().parent / "signup_extension" + + + + +@dataclass(frozen=True) +class SignupSitePreset: + key: str + label: str + url: str + hosts: tuple[str, ...] + extra_selectors: dict[str, list[str]] = field(default_factory=dict) + + +SIGNUP_PRESETS: dict[str, SignupSitePreset] = { + "google": SignupSitePreset( + key="google", + label="Google", + url="https://accounts.google.com/signup/v2/createaccount?flowName=GlifWebSignIn&flowEntry=SignUp", + hosts=("accounts.google.com", "google.com"), + extra_selectors={ + "first_name": ['input[name="firstName"]'], + "last_name": ['input[name="lastName"]'], + "email": ['input[name="Username"]', 'input[type="email"]'], + "password": ['input[name="Passwd"]'], + "password_confirm": ['input[name="PasswdAgain"]'], + }, + ), + "protonmail": SignupSitePreset( + key="protonmail", + label="Proton Mail", + url="https://account.proton.me/signup", + hosts=("account.proton.me", "proton.me"), + extra_selectors={ + "email": ['input[id="email"]', 'input[name="email"]', 'input[type="email"]'], + "password": ['input[id="password"]', 'input[name="password"]'], + "password_confirm": ['input[id="password-confirm"]', 'input[name="passwordConfirmation"]'], + }, + ), + "hydraproxy": SignupSitePreset( + key="hydraproxy", + label="HydraProxy", + url="https://dashboard.hydraproxy.com/register", + hosts=("dashboard.hydraproxy.com", "hydraproxy.com"), + extra_selectors={ + "email": ['input[name="email"]', 'input[type="email"]', 'input[id="email"]'], + "password": ['input[name="password"]', 'input[type="password"]'], + "password_confirm": ['input[name="password_confirmation"]', 'input[name="confirmPassword"]'], + "username": ['input[name="username"]', 'input[name="name"]'], + }, + ), + "outlook": SignupSitePreset( + key="outlook", + label="Outlook / Microsoft", + url="https://signup.live.com/signup", + hosts=("signup.live.com", "login.live.com", "account.microsoft.com"), + extra_selectors={ + "email": ['input[name="MemberName"]', 'input[type="email"]'], + "password": ['input[name="Password"]', 'input[name="PasswordInput"]'], + "first_name": ['input[name="FirstName"]'], + "last_name": ['input[name="LastName"]'], + }, + ), + "github": SignupSitePreset( + key="github", + label="GitHub", + url="https://github.com/signup", + hosts=("github.com",), + extra_selectors={ + "email": ['input[name="user[email]"]', 'input[autocomplete="email"]'], + "password": ['input[name="user[password]"]', 'input[autocomplete="new-password"]'], + "username": ['input[name="user[login]"]'], + }, + ), + "reddit": SignupSitePreset( + key="reddit", + label="Reddit", + url="https://www.reddit.com/register", + hosts=("www.reddit.com", "reddit.com"), + extra_selectors={ + "email": ['input[name="email"]', 'input[type="email"]'], + "username": ['input[name="username"]'], + "password": ['input[name="password"]'], + }, + ), + "discord": SignupSitePreset( + key="discord", + label="Discord", + url="https://discord.com/register", + hosts=("discord.com",), + extra_selectors={ + "email": ['input[name="email"]', 'input[type="email"]'], + "username": ['input[name="username"]'], + "password": ['input[name="password"]'], + }, + ), + "x_twitter": SignupSitePreset( + key="x_twitter", + label="X / Twitter", + url="https://x.com/i/flow/signup", + hosts=("x.com", "twitter.com"), + extra_selectors={ + "email": ['input[name="email"]', 'input[type="email"]'], + "username": ['input[name="username"]'], + "password": ['input[name="password"]'], + "first_name": ['input[name="name"]'], + }, + ), + "telegram": SignupSitePreset( + key="telegram", + label="Telegram Web", + url="https://web.telegram.org/", + hosts=("web.telegram.org", "telegram.org"), + ), + "tutanota": SignupSitePreset( + key="tutanota", + label="Tuta Mail", + url="https://app.tuta.com/signup", + hosts=("app.tuta.com", "tutanota.com", "tuta.com"), + extra_selectors={ + "email": ['input[id="mailAddress"]', 'input[name="mailAddress"]'], + "password": ['input[id="password1"]', 'input[type="password"]'], + "password_confirm": ['input[id="password2"]'], + }, + ), + "mailcom": SignupSitePreset( + key="mailcom", + label="mail.com", + url="https://signup.mail.com/", + hosts=("signup.mail.com", "mail.com"), + extra_selectors={ + "username": ['input[name="email_user"]', 'input[name="username"]'], + "password": ['input[name="password"]'], + "first_name": ['input[name="firstName"]'], + "last_name": ['input[name="lastName"]'], + }, + ), +} + + +# Public site-key list in display order. Useful for building dropdowns. +SIGNUP_PRESET_KEYS: tuple[str, ...] = ( + "google", "protonmail", "outlook", "tutanota", "mailcom", + "github", "reddit", "discord", "x_twitter", "telegram", + "hydraproxy", +) + + +_FIRST_NAMES: tuple[str, ...] = ( + "Alex", "Jordan", "Casey", "Morgan", "Taylor", "Riley", "Cameron", + "Sam", "Jamie", "Quinn", "Avery", "Drew", "Reese", "Skyler", "Logan", + "Hayden", "Parker", "Rowan", "Sage", "Emerson", "Finley", "Harper", + "Kai", "Micah", "Nico", "Phoenix", "River", "Shawn", "Sutton", "Wren", +) + +_LAST_NAMES: tuple[str, ...] = ( + "Reed", "Hayes", "Brooks", "Bennett", "Carter", "Foster", "Gray", + "Hughes", "Jensen", "Knight", "Lambert", "Mason", "Nash", "Owens", + "Porter", "Quincy", "Reyes", "Sutton", "Tate", "Underwood", "Vance", + "Walsh", "Yates", "Zimmerman", "Abbott", "Barker", "Cohen", "Dixon", + "Ellis", "Fitch", +) + + +def random_first_name() -> str: + return secrets.choice(_FIRST_NAMES) + + +def random_last_name() -> str: + return secrets.choice(_LAST_NAMES) + + +def random_full_name() -> tuple[str, str]: + return random_first_name(), random_last_name() + + +def random_username(first: str = "", last: str = "") -> str: + """Generate a plausible-looking username. + + If a name is provided we mix it with digits; otherwise we use two short + name fragments. Always lowercase, always 8-18 chars. + """ + f = (first or random_first_name()).lower() + l = (last or random_last_name()).lower() + seps = ("", ".", "_", "") + sep = secrets.choice(seps) + digits = "".join(secrets.choice(string.digits) for _ in range(secrets.choice([2, 3, 4]))) + style = secrets.randbelow(4) + if style == 0: + base = f + sep + l + digits + elif style == 1: + base = f + digits + elif style == 2: + base = l + sep + f[0] + digits + else: + base = f[0] + sep + l + digits + base = base.strip(".-_") + return base[:18] + + +@dataclass +class SignupDraft: + site_key: str = "google" + custom_url: str = "" + email: str = "" + password: str = "" + username: str = "" + first_name: str = "" + last_name: str = "" + notes: str = "" + + +@dataclass +class AccountRecord: + id: str + site: str + url: str + email: str + password: str + username: str = "" + first_name: str = "" + last_name: str = "" + exit_ip: str = "" + exit_hop: str = "" + notes: str = "" + status: str = "created" # pending | created + created_at: str = "" + + +def accounts_path() -> Path: + return app_data_dir() / "signup_accounts.json" + + +def draft_path() -> Path: + return app_data_dir() / "signup_draft.json" + + +def generate_password(length: int = 18) -> str: + n = max(12, min(64, int(length))) + alphabet = string.ascii_letters + string.digits + "!@#$%^&*-_=+" + while True: + pw = "".join(secrets.choice(alphabet) for _ in range(n)) + if ( + any(c.islower() for c in pw) + and any(c.isupper() for c in pw) + and any(c.isdigit() for c in pw) + ): + return pw + + +def preset_for(key: str) -> SignupSitePreset | None: + return SIGNUP_PRESETS.get(key) + + +def resolve_signup_url(draft: SignupDraft) -> str: + if draft.site_key == "custom": + return (draft.custom_url or "").strip() + p = preset_for(draft.site_key) + return p.url if p else "" + + +def load_draft() -> SignupDraft: + from . import secrets_store as _ss + p = draft_path() + if not p.is_file(): + return SignupDraft() + try: + raw = json.loads(p.read_text(encoding="utf-8")) + kwargs = {k: raw.get(k, "") for k in SignupDraft.__dataclass_fields__} + if kwargs.get("password"): + kwargs["password"] = _ss.decrypt(kwargs["password"]) + return SignupDraft(**kwargs) + except (OSError, json.JSONDecodeError, TypeError): + return SignupDraft() + + +def save_draft(draft: SignupDraft) -> None: + from . import secrets_store as _ss + d = asdict(draft) + if d.get("password"): + d["password"] = _ss.encrypt(d["password"]) + draft_path().write_text(json.dumps(d, indent=2), encoding="utf-8") + + +def load_accounts() -> list[AccountRecord]: + from . import secrets_store as _ss + p = accounts_path() + if not p.is_file(): + return [] + try: + raw = json.loads(p.read_text(encoding="utf-8")) + if not isinstance(raw, list): + return [] + out: list[AccountRecord] = [] + for item in raw: + if not isinstance(item, dict): + continue + fields = AccountRecord.__dataclass_fields__ + kwargs = {k: item.get(k, "") for k in fields} + if not kwargs.get("id"): + kwargs["id"] = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S%f") + # Transparent DPAPI decrypt; plaintext passes through. + if kwargs.get("password"): + kwargs["password"] = _ss.decrypt(kwargs["password"]) + out.append(AccountRecord(**kwargs)) + return out + except (OSError, json.JSONDecodeError, TypeError): + return [] + + +def save_accounts(rows: list[AccountRecord]) -> None: + from . import secrets_store as _ss + serialized = [] + for r in rows: + d = asdict(r) + # Encrypt sensitive fields at rest using Windows DPAPI when available. + # Legacy plaintext entries are migrated transparently on first save. + if d.get("password"): + d["password"] = _ss.encrypt(d["password"]) + serialized.append(d) + accounts_path().write_text( + json.dumps(serialized, indent=2), + encoding="utf-8", + ) + + +def append_account(record: AccountRecord) -> None: + rows = load_accounts() + rows.insert(0, record) + save_accounts(rows) + + +def build_autofill_config(draft: SignupDraft) -> dict[str, Any]: + preset = preset_for(draft.site_key) + hosts = list(preset.hosts) if preset else [] + selectors: dict[str, list[str]] = {} + if preset: + selectors.update(preset.extra_selectors) + + fields: dict[str, str] = {} + if draft.first_name: + fields["first_name"] = draft.first_name + if draft.last_name: + fields["last_name"] = draft.last_name + if draft.username: + fields["username"] = draft.username + if draft.email: + fields["email"] = draft.email + if draft.password: + fields["password"] = draft.password + fields["password_confirm"] = draft.password + + out_fields: dict[str, Any] = {} + for key, val in fields.items(): + out_fields[key] = val + sel_key = f"{key}_selectors" + if key in selectors: + out_fields[sel_key] = selectors[key] + + return { + "site": draft.site_key, + "hosts": hosts, + "fields": out_fields, + "updated_at": datetime.now(timezone.utc).isoformat(), + } + + +def _extensions_json_entry(ext_dir_name: str) -> dict[str, Any]: + now = int(datetime.now(timezone.utc).timestamp()) * 1000 + return { + "id": _EXT_ID, + "syncGUID": str(uuid.uuid4()), + "version": "1.0.0", + "type": "extension", + "location": { + "appProfile": { + "root": "app-profile", + "path": f"extensions/{ext_dir_name}", + } + }, + "internalName": None, + "updateURL": None, + "webExtensionConverted": False, + "webExtensionId": _EXT_ID, + "active": True, + "userDisabled": False, + "appDisabled": False, + "installDate": now, + "scope": 1, + } + + +def install_signup_extension(profile_dir: Path, draft: SignupDraft) -> None: + """Copy autofill WebExtension into the Firefox profile and write current field values.""" + profile_dir.mkdir(parents=True, exist_ok=True) + ext_name = _EXT_ID + dest = profile_dir / "extensions" / ext_name + if dest.exists(): + shutil.rmtree(dest, ignore_errors=True) + shutil.copytree(_ext_src_path(), dest) + + cfg = build_autofill_config(draft) + (dest / "autofill_config.json").write_text(json.dumps(cfg, indent=2), encoding="utf-8") + + ext_json_path = profile_dir / "extensions.json" + entry = _extensions_json_entry(ext_name) + data: dict[str, Any] + if ext_json_path.is_file(): + try: + data = json.loads(ext_json_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + data = {"schemaVersion": 35, "addons": []} + else: + data = {"schemaVersion": 35, "addons": []} + + addons = data.get("addons") + if not isinstance(addons, list): + addons = [] + addons = [a for a in addons if isinstance(a, dict) and a.get("id") != _EXT_ID] + addons.append(entry) + data["addons"] = addons + ext_json_path.write_text(json.dumps(data, indent=2), encoding="utf-8") diff --git a/proxy_chain_manager/sysproxy.py b/proxy_chain_manager/sysproxy.py new file mode 100644 index 0000000..0c525b5 --- /dev/null +++ b/proxy_chain_manager/sysproxy.py @@ -0,0 +1,574 @@ +"""Set / clear the Windows system-wide HTTP proxy. + +Layers configured so the proxy actually applies system-wide: + +1. ``HKCU\\…\\Internet Settings`` — simple values WinINet reads first +2. ``Internet Settings\\Connections\\DefaultConnectionSettings`` binary blob — + overrides #1 for any process that opens the per-connection structure. +3. ``HKLM\\…\\Internet Settings`` mirror (Admin only) — required on Server when + ``ProxySettingsPerUser=0`` is set by Group Policy / baseline image. +4. ``netsh winhttp set proxy`` — .NET, Windows Update, services and + headless browser components that bypass WinINet. + +We also actively detect and warn about: +- ``ProxySettingsPerUser=0`` → HKCU is ignored; we fix it when Admin. +- Group Policy proxy keys → those override us; we surface the path so the + user can verify or remove them. +- ``AutoConfigURL`` / ``AutoDetect`` (WPAD) → wiped on engage. +""" +from __future__ import annotations + +import ctypes +import logging +import sys +import struct +import subprocess +import winreg + +log = logging.getLogger(__name__) + +_KEY_PATH = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings" +_CONN_KEY_PATH = ( + r"Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" +) +_HKLM_INET_PATH = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings" +_HKLM_CONN_PATH = ( + r"SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" +) +_POLICY_PATHS = ( + (winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings"), + (winreg.HKEY_CURRENT_USER, + r"SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings"), + (winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Policies\Microsoft\Internet Explorer\Control Panel"), +) +_SETTINGS_CHANGED = 39 +_REFRESH = 37 + +_FLAG_DIRECT = 0x01 +_FLAG_MANUAL_PROXY = 0x02 +_FLAG_AUTO_CONFIG = 0x04 +_FLAG_AUTO_DETECT = 0x08 + + +def _mac_network_services() -> list[str]: + if sys.platform != "darwin": + return [] + try: + r = subprocess.run( + ["networksetup", "-listallnetworkservices"], + capture_output=True, + text=True, + timeout=12, + ) + except Exception: + return [] + services: list[str] = [] + for line in (r.stdout or "").splitlines(): + name = line.strip() + if not name or name.startswith("An asterisk"): + continue + services.append(name.lstrip("*").strip()) + return services + + +def _mac_run_networksetup(args: list[str]) -> None: + try: + subprocess.run(["networksetup", *args], capture_output=True, text=True, timeout=20) + except Exception as exc: + log.debug("networksetup failed (%s): %s", args, exc) + + +def _mac_set_system_proxy(host: str, port: int, bypass: str) -> None: + bypass_hosts = [ + h.strip().replace("*", "") + for h in bypass.replace(";", ",").split(",") + if h.strip() and h.strip() != "" + ] + for service in _mac_network_services(): + _mac_run_networksetup(["-setwebproxy", service, host, str(port)]) + _mac_run_networksetup(["-setsecurewebproxy", service, host, str(port)]) + _mac_run_networksetup(["-setwebproxystate", service, "on"]) + _mac_run_networksetup(["-setsecurewebproxystate", service, "on"]) + if bypass_hosts: + _mac_run_networksetup(["-setproxybypassdomains", service, *bypass_hosts]) + log.info("macOS system proxy set to %s:%s for %d service(s)", host, port, len(_mac_network_services())) + + +def _mac_clear_system_proxy() -> None: + for service in _mac_network_services(): + _mac_run_networksetup(["-setwebproxystate", service, "off"]) + _mac_run_networksetup(["-setsecurewebproxystate", service, "off"]) + log.info("macOS system proxy cleared for %d service(s)", len(_mac_network_services())) + + +def _mac_system_proxy_set() -> bool: + for service in _mac_network_services(): + try: + r = subprocess.run( + ["networksetup", "-getwebproxy", service], + capture_output=True, + text=True, + timeout=8, + ) + if "Enabled: Yes" in (r.stdout or ""): + return True + except Exception: + continue + return False + + +def _is_admin() -> bool: + try: + return ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[attr-defined] + except Exception: + return False + + +def _broadcast() -> None: + """Tell running apps (browsers, etc.) that proxy settings changed.""" + try: + inet = ctypes.windll.wininet # type: ignore[attr-defined] + inet.InternetSetOptionW(0, _SETTINGS_CHANGED, 0, 0) + inet.InternetSetOptionW(0, _REFRESH, 0, 0) + except Exception: + pass + + +def _bump_counter(prev: bytes | None) -> int: + if not prev or len(prev) < 8: + return 1 + try: + return int.from_bytes(prev[4:8], "little") + 1 + except Exception: + return 1 + + +def _build_blob( + proxy: str, + bypass: str, + auto_config_url: str = "", + counter: int = 1, +) -> bytes: + """REG_BINARY layout for DefaultConnectionSettings (little-endian).""" + flags = _FLAG_DIRECT + if proxy: + flags |= _FLAG_MANUAL_PROXY + if auto_config_url: + flags |= _FLAG_AUTO_CONFIG + + p = proxy.encode("utf-8") + b = bypass.encode("utf-8") + a = auto_config_url.encode("utf-8") + + return ( + struct.pack(" int: + try: + with winreg.OpenKey(hive, path, 0, winreg.KEY_QUERY_VALUE) as key: + val, _ = winreg.QueryValueEx(key, "DefaultConnectionSettings") + return _bump_counter(val) + except OSError: + return 1 + + +def _write_conn_blob(hive: int, path: str, proxy: str, bypass: str) -> bool: + counter = _read_blob_counter(hive, path) + blob = _build_blob(proxy, bypass, "", counter) + try: + key = winreg.CreateKeyEx(hive, path, 0, winreg.KEY_SET_VALUE) + with key: + winreg.SetValueEx(key, "DefaultConnectionSettings", 0, winreg.REG_BINARY, blob) + winreg.SetValueEx(key, "SavedLegacySettings", 0, winreg.REG_BINARY, blob) + return True + except OSError as e: + log.debug("Connections blob write failed (%s): %s", path, e) + return False + + +def _clear_conn_blob(hive: int, path: str) -> None: + counter = _read_blob_counter(hive, path) + blob = _build_blob("", "", "", counter) + try: + with winreg.OpenKey(hive, path, 0, winreg.KEY_SET_VALUE) as key: + winreg.SetValueEx(key, "DefaultConnectionSettings", 0, winreg.REG_BINARY, blob) + winreg.SetValueEx(key, "SavedLegacySettings", 0, winreg.REG_BINARY, blob) + except OSError: + pass + + +def _wipe_autoconfig(key: winreg.HKEYType) -> None: + for name in ("AutoConfigURL", "AutoDetect"): + try: + winreg.DeleteValue(key, name) + except OSError: + pass + + +def _set_winhttp_proxy(host: str, port: int, bypass: str) -> None: + try: + bypass_list = ( + bypass.replace(";", " ").replace("", "").strip() + ) or "" + subprocess.run( + ["netsh", "winhttp", "set", "proxy", + f"{host}:{port}", f"bypass-list={bypass_list}"], + capture_output=True, text=True, timeout=15, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + except Exception as e: + log.debug("netsh winhttp set proxy failed: %s", e) + + +def _reset_winhttp_proxy() -> None: + try: + subprocess.run( + ["netsh", "winhttp", "reset", "proxy"], + capture_output=True, text=True, timeout=15, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + except Exception: + pass + + +# ── browser-policy proxy (Chrome / Edge) ───────────────────────────────────── +# +# Why this exists: when you set WebRTC policy on Chrome/Edge, the browser sees +# itself as "managed". Some managed-Chrome builds then *ignore* the system +# proxy unless an explicit ``ProxyServer`` policy is also set. They also keep +# trying HTTP/3 over QUIC (UDP/443), which bypasses HTTP proxies entirely — +# under the kill-switch those UDP packets get dropped and pages hang forever. +# +# Setting these policy values pins the browser to our chain (HTTP proxy) and +# forces it off QUIC. Cleared cleanly on disengage. +_BROWSER_POLICY_PATHS = ( + r"SOFTWARE\Policies\Google\Chrome", + r"SOFTWARE\Policies\Microsoft\Edge", + r"SOFTWARE\Policies\Chromium", +) + +def _set_browser_proxy_policy(host: str, port: int, bypass: str) -> int: + """Pin Chromium-based browsers (Chrome, Edge, Chromium) to our local proxy + via Group Policy registry, and disable QUIC. Returns count of keys updated. + + Requires Admin to write under HKLM\\SOFTWARE\\Policies. Silently skips when + not elevated — the user gets a warning in the service log instead. + """ + if not _is_admin(): + return 0 + proxy_value = f"http={host}:{port};https={host}:{port}" + bypass_value = bypass.replace(";", ",") + written = 0 + for path in _BROWSER_POLICY_PATHS: + try: + with winreg.CreateKeyEx( + winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_SET_VALUE + ) as key: + winreg.SetValueEx(key, "ProxyMode", 0, winreg.REG_SZ, "fixed_servers") + winreg.SetValueEx(key, "ProxyServer", 0, winreg.REG_SZ, proxy_value) + winreg.SetValueEx(key, "ProxyBypassList", 0, winreg.REG_SZ, bypass_value) + winreg.SetValueEx(key, "QuicAllowed", 0, winreg.REG_DWORD, 0) + written += 1 + except OSError: + continue + return written + + +def _clear_browser_proxy_policy() -> None: + if not _is_admin(): + return + for path in _BROWSER_POLICY_PATHS: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_SET_VALUE + ) as key: + for name in ("ProxyMode", "ProxyServer", "ProxyBypassList", "QuicAllowed"): + try: + winreg.DeleteValue(key, name) + except OSError: + pass + except OSError: + continue + + +def _read_proxy_settings_per_user() -> int | None: + """``HKLM\\…\\Internet Settings\\ProxySettingsPerUser``. + + When 0, Windows ignores HKCU proxy values and reads only HKLM. Set on many + Server images and AD policy baselines — the #1 cause of "set the proxy but + the browser still goes direct" on Server. + """ + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_PATH, 0, winreg.KEY_QUERY_VALUE + ) as key: + val, _ = winreg.QueryValueEx(key, "ProxySettingsPerUser") + return int(val) + except OSError: + return None + + +def _fix_proxy_settings_per_user() -> bool: + """Force ``ProxySettingsPerUser=1`` (per-user proxy honored). Admin only.""" + if not _is_admin(): + return False + try: + with winreg.CreateKeyEx( + winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_PATH, 0, winreg.KEY_SET_VALUE + ) as key: + winreg.SetValueEx(key, "ProxySettingsPerUser", 0, winreg.REG_DWORD, 1) + log.info("HKLM\\…\\ProxySettingsPerUser → 1 (was %s)", _read_proxy_settings_per_user()) + return True + except OSError as e: + log.warning("Could not fix ProxySettingsPerUser: %s", e) + return False + + +def _set_hklm_root(proxy: str, bypass: str) -> bool: + """Mirror the per-user keys at HKLM so machine-scope WinINet reads honor us.""" + if not _is_admin(): + return False + try: + with winreg.CreateKeyEx( + winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_PATH, 0, winreg.KEY_SET_VALUE + ) as key: + winreg.SetValueEx(key, "ProxyEnable", 0, winreg.REG_DWORD, 1) + winreg.SetValueEx(key, "ProxyServer", 0, winreg.REG_SZ, proxy) + winreg.SetValueEx(key, "ProxyOverride", 0, winreg.REG_SZ, bypass) + _wipe_autoconfig(key) + return True + except OSError as e: + log.warning("HKLM root proxy write failed: %s", e) + return False + + +def _clear_hklm_root() -> None: + if not _is_admin(): + return + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_PATH, 0, winreg.KEY_SET_VALUE + ) as key: + winreg.SetValueEx(key, "ProxyEnable", 0, winreg.REG_DWORD, 0) + _wipe_autoconfig(key) + except OSError: + pass + + +def detect_policy_overrides() -> list[str]: + if sys.platform != "win32": + return [] + """Return human-readable paths of Group Policy proxy locks that beat us. + + Anything we find here will silently override our manual proxy. We do *not* + delete policy keys (admins set those deliberately) — we just surface them. + """ + hits: list[str] = [] + proxy_value_names = ("ProxyServer", "ProxyEnable", "AutoConfigURL", "ProxyOverride") + for hive, path in _POLICY_PATHS: + try: + with winreg.OpenKey(hive, path, 0, winreg.KEY_QUERY_VALUE) as key: + for n in proxy_value_names: + try: + winreg.QueryValueEx(key, n) + hive_name = "HKLM" if hive == winreg.HKEY_LOCAL_MACHINE else "HKCU" + hits.append(f"{hive_name}\\{path}\\{n}") + except OSError: + continue + except OSError: + continue + return hits + + +def _read_str(hive: int, path: str, name: str) -> str: + try: + with winreg.OpenKey(hive, path, 0, winreg.KEY_QUERY_VALUE) as key: + val, _ = winreg.QueryValueEx(key, name) + return str(val) + except OSError: + return "" + + +def _read_dword(hive: int, path: str, name: str) -> int | None: + try: + with winreg.OpenKey(hive, path, 0, winreg.KEY_QUERY_VALUE) as key: + val, _ = winreg.QueryValueEx(key, name) + return int(val) + except OSError: + return None + + +def _read_winhttp_proxy() -> str: + try: + r = subprocess.run( + ["netsh", "winhttp", "show", "proxy"], + capture_output=True, text=True, timeout=10, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + for ln in (r.stdout or "").splitlines(): + if "proxy" in ln.lower() and ":" in ln: + return ln.strip() + return (r.stdout or "").strip().splitlines()[-1] if r.stdout else "" + except Exception: + return "" + + +def diagnose_system_proxy() -> list[str]: + if sys.platform == "darwin": + out = [] + for service in _mac_network_services()[:8]: + try: + r = subprocess.run( + ["networksetup", "-getwebproxy", service], + capture_output=True, + text=True, + timeout=8, + ) + first = " ".join((r.stdout or "").splitlines()[:3]) + except Exception as exc: + first = str(exc) + out.append(f"macOS {service}: {first}") + return out or ["macOS network services not detected"] + """Return a list of human-readable lines covering every place proxy can + be configured. Logged on each engage so "chain green but browser leaks" + is debuggable without RegEdit gymnastics. + """ + lines: list[str] = [] + per_user = _read_proxy_settings_per_user() + per_user_msg = ( + "1 (HKCU honored)" if per_user == 1 + else "0 (HKCU IGNORED — Server lock!)" if per_user == 0 + else "(unset → defaults to HKCU)" + ) + lines.append(f"ProxySettingsPerUser = {per_user_msg}") + + en_u = _read_dword(winreg.HKEY_CURRENT_USER, _KEY_PATH, "ProxyEnable") + ps_u = _read_str(winreg.HKEY_CURRENT_USER, _KEY_PATH, "ProxyServer") + lines.append(f"HKCU Enable={en_u} Server='{ps_u}'") + + en_m = _read_dword(winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_PATH, "ProxyEnable") + ps_m = _read_str(winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_PATH, "ProxyServer") + lines.append(f"HKLM Enable={en_m} Server='{ps_m}'") + + ac_u = _read_str(winreg.HKEY_CURRENT_USER, _KEY_PATH, "AutoConfigURL") + ad_u = _read_dword(winreg.HKEY_CURRENT_USER, _KEY_PATH, "AutoDetect") + if ac_u or ad_u: + lines.append(f"WPAD/PAC: AutoConfigURL='{ac_u}' AutoDetect={ad_u}") + + winhttp = _read_winhttp_proxy() + if winhttp: + lines.append(f"WinHTTP: {winhttp}") + + pol = detect_policy_overrides() + if pol: + lines.append(f"Policy overrides (these BEAT us): {len(pol)} entries") + for p in pol[:5]: + lines.append(f" ! {p}") + if len(pol) > 5: + lines.append(f" ! …and {len(pol) - 5} more") + else: + lines.append("Policy overrides: none") + + return lines + + +def set_system_proxy( + host: str, + port: int, + bypass: str = "localhost;127.*;10.*;192.168.*;", +) -> None: + if sys.platform == "darwin": + _mac_set_system_proxy(host, port, bypass) + return + proxy = f"{host}:{port}" + + # HKCU root + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, _KEY_PATH, 0, winreg.KEY_SET_VALUE + ) as key: + winreg.SetValueEx(key, "ProxyEnable", 0, winreg.REG_DWORD, 1) + winreg.SetValueEx(key, "ProxyServer", 0, winreg.REG_SZ, proxy) + winreg.SetValueEx(key, "ProxyOverride", 0, winreg.REG_SZ, bypass) + _wipe_autoconfig(key) + except OSError: + log.exception("Failed to set system proxy (HKCU root)") + return + + _write_conn_blob(winreg.HKEY_CURRENT_USER, _CONN_KEY_PATH, proxy, bypass) + + # If ProxySettingsPerUser=0 (Server / GPO baseline), HKCU is ignored. + # When Admin, fix the flag AND mirror to HKLM so the proxy actually applies. + per_user = _read_proxy_settings_per_user() + if per_user == 0: + log.warning( + "ProxySettingsPerUser=0 detected — Windows is ignoring per-user " + "proxy. Attempting Admin fix (HKLM mirror)." + ) + if _is_admin(): + _fix_proxy_settings_per_user() + _set_hklm_root(proxy, bypass) + _write_conn_blob(winreg.HKEY_LOCAL_MACHINE, _HKLM_CONN_PATH, proxy, bypass) + else: + log.warning( + "Not running as Admin — cannot fix ProxySettingsPerUser. " + "Re-launch elevated for proxy to apply on this Server image." + ) + + # On Admin runs always mirror HKLM root too. Harmless when PerUser=1 and + # correct when PerUser=0 or absent. + if _is_admin(): + _set_hklm_root(proxy, bypass) + _write_conn_blob(winreg.HKEY_LOCAL_MACHINE, _HKLM_CONN_PATH, proxy, bypass) + + _set_winhttp_proxy(host, port, bypass) + n_browser = _set_browser_proxy_policy(host, port, bypass) + _broadcast() + log.info( + "System proxy set %s (bypass=%s) — HKCU + HKLM(adm) + Connections + WinHTTP + %d browser policy", + proxy, bypass, n_browser, + ) + + +def clear_system_proxy() -> None: + if sys.platform == "darwin": + _mac_clear_system_proxy() + return + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, _KEY_PATH, 0, winreg.KEY_SET_VALUE + ) as key: + winreg.SetValueEx(key, "ProxyEnable", 0, winreg.REG_DWORD, 0) + _wipe_autoconfig(key) + except OSError: + log.exception("Failed to clear system proxy") + + _clear_conn_blob(winreg.HKEY_CURRENT_USER, _CONN_KEY_PATH) + if _is_admin(): + _clear_hklm_root() + _clear_conn_blob(winreg.HKEY_LOCAL_MACHINE, _HKLM_CONN_PATH) + _reset_winhttp_proxy() + _clear_browser_proxy_policy() + _broadcast() + log.info("System proxy cleared — HKCU + HKLM(adm) + Connections + WinHTTP + browser policy") + + +def is_system_proxy_set() -> bool: + if sys.platform == "darwin": + return _mac_system_proxy_set() + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, _KEY_PATH, 0, winreg.KEY_QUERY_VALUE + ) as key: + val, _ = winreg.QueryValueEx(key, "ProxyEnable") + return val == 1 + except OSError: + return False diff --git a/proxy_chain_manager/telemetry_kill.py b/proxy_chain_manager/telemetry_kill.py new file mode 100644 index 0000000..6ebe836 --- /dev/null +++ b/proxy_chain_manager/telemetry_kill.py @@ -0,0 +1,292 @@ +"""Reversibly disable Windows telemetry / data collection. + +Targets, in order of leak severity: + + • DiagTrack service — primary telemetry pipeline ("Connected User + Experiences and Telemetry"). Phones home every + few minutes. + • dmwappushservice — WAP Push routing (DSMS), historically tied to + DiagTrack. Disabled when present. + • AllowTelemetry (HKLM) — DataCollection policy. 0 = lowest tier. + • Activity History — EnableActivityFeed / Upload / PublishUserActivities. + • Advertising ID — HKCU\\…\\AdvertisingInfo\\Enabled. + • Cortana Bing search — HKCU\\…\\Search\\BingSearchEnabled. + • Scheduled tasks — Compatibility Appraiser + CEIP Consolidator + + UsbCeip. + +Every change is snapshotted before mutation; ``restore_telemetry`` rolls back. +""" +from __future__ import annotations + +import json +import logging +import subprocess +import winreg +from dataclasses import dataclass, field + +from .firewall import is_admin + +log = logging.getLogger(__name__) + +_SERVICES = ("DiagTrack", "dmwappushservice") + +_SCHED_TASKS = ( + r"\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser", + r"\Microsoft\Windows\Customer Experience Improvement Program\Consolidator", + r"\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip", +) + +# (hive, path, value_name, dword_when_killed) +_REG_KILLS = ( + (winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Policies\Microsoft\Windows\DataCollection", + "AllowTelemetry", 0), + (winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Policies\Microsoft\Windows\System", + "EnableActivityFeed", 0), + (winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Policies\Microsoft\Windows\System", + "PublishUserActivities", 0), + (winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Policies\Microsoft\Windows\System", + "UploadUserActivities", 0), + (winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Policies\Microsoft\Windows\CloudContent", + "DisableWindowsConsumerFeatures", 1), + (winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo", + "Enabled", 0), + (winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Search", + "BingSearchEnabled", 0), + (winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Search", + "CortanaConsent", 0), +) + + +@dataclass +class TelemetrySnapshot: + service_state: dict[str, str] = field(default_factory=dict) # name -> 'auto'|'manual'|'disabled'|'absent' + sched_tasks_state: dict[str, bool] = field(default_factory=dict) # task -> was_enabled + reg_values: list[dict] = field(default_factory=list) + + +def _run(args: list[str], timeout: float = 12.0) -> tuple[int, str, str]: + try: + r = subprocess.run( + args, capture_output=True, text=True, timeout=timeout, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + return r.returncode, r.stdout or "", r.stderr or "" + except Exception as e: + return 1, "", str(e) + + +# ── service helpers ────────────────────────────────────────────────────────── + +def _service_start_type(name: str) -> str: + code, out, _ = _run(["sc", "qc", name]) + if code != 0: + return "absent" + for ln in out.splitlines(): + s = ln.strip().lower() + if "start_type" in s: + if "2" in s or "auto" in s: + return "auto" + if "3" in s or "demand" in s or "manual" in s: + return "manual" + if "4" in s or "disabled" in s: + return "disabled" + return "absent" + + +def _set_service_disabled(name: str) -> bool: + _run(["sc", "stop", name], timeout=15) + code, _, _ = _run(["sc", "config", name, "start=", "disabled"]) + return code == 0 + + +def _set_service_start(name: str, want: str) -> bool: + start_value = {"auto": "auto", "manual": "demand", "disabled": "disabled"}.get(want) + if not start_value: + return False + _run(["sc", "stop", name], timeout=10) + code, _, _ = _run(["sc", "config", name, "start=", start_value]) + if want == "auto": + _run(["sc", "start", name], timeout=15) + return code == 0 + + +# ── scheduled-task helpers ────────────────────────────────────────────────── + +def _task_is_enabled(name: str) -> bool | None: + code, out, _ = _run(["schtasks", "/Query", "/TN", name]) + if code != 0: + return None + for ln in out.splitlines(): + s = ln.strip().lower() + if s.startswith("status:") or "ready" in s or "disabled" in s: + if "disabled" in s: + return False + if "ready" in s or "running" in s: + return True + return True # assume ready when query succeeded + + +def _set_task_state(name: str, enable: bool) -> bool: + code, _, _ = _run([ + "schtasks", "/Change", "/TN", name, + "/Enable" if enable else "/Disable", + ]) + return code == 0 + + +# ── registry helpers ──────────────────────────────────────────────────────── + +def _read_reg_dword(hive: int, path: str, name: str) -> tuple[bool, int | None]: + """Returns (present, value).""" + try: + with winreg.OpenKey(hive, path, 0, winreg.KEY_QUERY_VALUE) as key: + v, _ = winreg.QueryValueEx(key, name) + return True, int(v) + except OSError: + return False, None + + +def _write_reg_dword(hive: int, path: str, name: str, value: int) -> bool: + try: + with winreg.CreateKeyEx(hive, path, 0, winreg.KEY_SET_VALUE) as key: + winreg.SetValueEx(key, name, 0, winreg.REG_DWORD, value) + return True + except OSError: + return False + + +def _delete_reg_value(hive: int, path: str, name: str) -> None: + try: + with winreg.OpenKey(hive, path, 0, winreg.KEY_SET_VALUE) as key: + winreg.DeleteValue(key, name) + except OSError: + pass + + +# ── public API ────────────────────────────────────────────────────────────── + +def telemetry_status() -> dict[str, str]: + """Read-only snapshot for the audit panel.""" + out: dict[str, str] = {} + for svc in _SERVICES: + out[f"svc.{svc}"] = _service_start_type(svc) + pres, val = _read_reg_dword( + winreg.HKEY_LOCAL_MACHINE, + r"SOFTWARE\Policies\Microsoft\Windows\DataCollection", + "AllowTelemetry", + ) + out["AllowTelemetry"] = "0 (killed)" if pres and val == 0 else ( + f"{val}" if pres else "unset" + ) + pres, val = _read_reg_dword( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo", + "Enabled", + ) + out["AdvertisingID"] = "off" if pres and val == 0 else ( + "on" if pres and val == 1 else "unset" + ) + return out + + +def engage_telemetry_kill() -> tuple[TelemetrySnapshot | None, list[str]]: + logs: list[str] = [] + if not is_admin(): + return None, ["Telemetry kill skipped — needs Administrator."] + snap = TelemetrySnapshot() + + for svc in _SERVICES: + st = _service_start_type(svc) + snap.service_state[svc] = st + if st == "absent": + continue + if _set_service_disabled(svc): + logs.append(f"Service '{svc}' stopped + disabled (was {st}).") + else: + logs.append(f"Service '{svc}' could not be disabled.") + + for task in _SCHED_TASKS: + was = _task_is_enabled(task) + if was is None: + continue + snap.sched_tasks_state[task] = was + if was: + if _set_task_state(task, enable=False): + logs.append(f"Scheduled task disabled: {task}") + + for hive, path, name, kill_val in _REG_KILLS: + present, prev = _read_reg_dword(hive, path, name) + snap.reg_values.append({ + "hive": "HKLM" if hive == winreg.HKEY_LOCAL_MACHINE else "HKCU", + "path": path, + "name": name, + "present": present, + "prev": prev, + }) + if _write_reg_dword(hive, path, name, kill_val): + logs.append(f"reg: {('HKLM' if hive == winreg.HKEY_LOCAL_MACHINE else 'HKCU')}\\{path}\\{name} → {kill_val}") + else: + logs.append(f"reg write failed: {path}\\{name}") + + return snap, logs + + +def restore_telemetry(snap: TelemetrySnapshot | None) -> list[str]: + if snap is None or not is_admin(): + return [] + logs: list[str] = [] + + for svc, prev in snap.service_state.items(): + if prev == "absent": + continue + if _set_service_start(svc, prev): + logs.append(f"Service '{svc}' restored → {prev}.") + + for task, was in snap.sched_tasks_state.items(): + if was: + _set_task_state(task, enable=True) + logs.append(f"Scheduled task re-enabled: {task}") + + for entry in snap.reg_values: + hive = winreg.HKEY_LOCAL_MACHINE if entry["hive"] == "HKLM" else winreg.HKEY_CURRENT_USER + path = entry["path"] + name = entry["name"] + if entry["present"]: + _write_reg_dword(hive, path, name, int(entry["prev"])) + logs.append(f"reg restore: {entry['hive']}\\{path}\\{name} ← {entry['prev']}") + else: + _delete_reg_value(hive, path, name) + logs.append(f"reg removed (was absent): {entry['hive']}\\{path}\\{name}") + return logs + + +def snapshot_to_json(snap: TelemetrySnapshot | None) -> str: + if snap is None: + return "" + return json.dumps({ + "services": snap.service_state, + "tasks": snap.sched_tasks_state, + "reg": snap.reg_values, + }) + + +def snapshot_from_json(raw: str) -> TelemetrySnapshot | None: + if not raw: + return None + try: + d = json.loads(raw) + return TelemetrySnapshot( + service_state=dict(d.get("services") or {}), + sched_tasks_state={k: bool(v) for k, v in (d.get("tasks") or {}).items()}, + reg_values=list(d.get("reg") or []), + ) + except Exception: + return None diff --git a/proxy_chain_manager/tray.py b/proxy_chain_manager/tray.py new file mode 100644 index 0000000..1b57471 --- /dev/null +++ b/proxy_chain_manager/tray.py @@ -0,0 +1,160 @@ +"""System tray: LED-style proxy on/off + hover tooltip with exit IP.""" +from __future__ import annotations + +import logging +import sys +import threading +from typing import Any, Callable + +log = logging.getLogger(__name__) + +try: + import pystray +except Exception as exc: # noqa: BLE001 + pystray = None # type: ignore[assignment] + log.warning("System tray unavailable: %s", exc) +from PIL import Image, ImageDraw, ImageFilter + +_SIZE = 64 + + +def _hex_rgb(h: str) -> tuple[int, int, int]: + h = h.lstrip("#") + return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) + + +def _led_icon(led: str, glow: str, rim: str = "#2d3748") -> Image.Image: + """Traffic-light style icon: dark housing + glowing LED.""" + base = Image.new("RGBA", (_SIZE, _SIZE), (0, 0, 0, 0)) + draw = ImageDraw.Draw(base) + cx, cy = _SIZE // 2, _SIZE // 2 + + # Outer housing (rounded rect feel via ellipse) + draw.ellipse([4, 4, _SIZE - 4, _SIZE - 4], fill="#0d0d1a", outline=rim, width=2) + draw.ellipse([10, 10, _SIZE - 10, _SIZE - 10], fill="#12122a", outline="#1a1a3e", width=1) + + glow_layer = Image.new("RGBA", (_SIZE, _SIZE), (0, 0, 0, 0)) + g = ImageDraw.Draw(glow_layer) + lr, lg, lb = _hex_rgb(glow) + for radius, alpha in ((22, 35), (16, 70), (11, 120)): + g.ellipse( + [cx - radius, cy - radius, cx + radius, cy + radius], + fill=(lr, lg, lb, alpha), + ) + glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(radius=2)) + base = Image.alpha_composite(base, glow_layer) + + draw = ImageDraw.Draw(base) + dr, dg, db = _hex_rgb(led) + draw.ellipse([cx - 9, cy - 9, cx + 9, cy + 9], fill=(dr, dg, db, 255)) + draw.ellipse([cx - 5, cy - 6, cx + 2, cy - 1], fill=(255, 255, 255, 90)) + + return base + + +ICONS = { + "green": _led_icon("#00e676", "#00e676"), + "red": _led_icon("#ff1744", "#ff1744"), + "yellow": _led_icon("#ffab00", "#ffab00"), + "gray": _led_icon("#4a5568", "#3d4a5c", rim="#374151"), +} + +_STATE_LABEL = { + "green": "PROXY ON", + "red": "PROXY OFF / ERROR", + "yellow": "CONNECTING…", + "gray": "STOPPED", +} + + +def _tooltip(state: str, exit_ip: str | None) -> str: + label = _STATE_LABEL.get(state, "Proxy God") + if exit_ip: + return f"Proxy God — {label}\nExit IP: {exit_ip}" + if state == "green": + return f"Proxy God — {label}\nExit IP: (checking…)" + return f"Proxy God — {label}\nExit IP: —" + + +class TrayIcon: + def __init__( + self, + on_show: Callable[[], None], + on_quit: Callable[[], None], + on_rotate: Callable[[], None], + ) -> None: + self._on_show = on_show + self._on_quit = on_quit + self._on_rotate = on_rotate + self._icon: pystray.Icon | None = None + self._thread: threading.Thread | None = None + self._state = "gray" + self._exit_ip: str | None = None + self._lock = threading.Lock() + + def start(self) -> None: + if sys.platform == "darwin": + log.info("Tray icon disabled on macOS; use the main window controls.") + return + if pystray is None: + return + if self._thread and self._thread.is_alive(): + return + menu = pystray.Menu( + pystray.MenuItem("Show Proxy God", self._show, default=True), + pystray.MenuItem("Rotate chain now", self._rotate), + pystray.Menu.SEPARATOR, + pystray.MenuItem("Quit", self._quit), + ) + with self._lock: + tip = _tooltip(self._state, self._exit_ip) + self._icon = pystray.Icon( + "ProxyGod", + icon=ICONS[self._state], + title=tip, + menu=menu, + ) + self._thread = threading.Thread(target=self._icon.run, daemon=True) + self._thread.start() + + def stop(self) -> None: + if sys.platform == "darwin": + return + if pystray is None: + return + if self._icon: + try: + self._icon.stop() + except Exception as exc: + log.warning("Tray icon stop failed: %s", exc) + + def set_state(self, state: str, exit_ip: str | None = None) -> None: + """state: green (on), red (error), yellow (connecting), gray (stopped). + + Pass exit_ip to refresh the hover tooltip. Use exit_ip=None to keep the last IP. + """ + if state not in ICONS: + state = "gray" + with self._lock: + self._state = state + if exit_ip is not None: + self._exit_ip = exit_ip.strip() if exit_ip else None + if self._icon: + self._icon.icon = ICONS[state] + self._icon.title = _tooltip(state, self._exit_ip) + + def set_exit_ip(self, exit_ip: str | None) -> None: + """Update tooltip only (e.g. after health check, same LED color).""" + with self._lock: + self._exit_ip = exit_ip.strip() if exit_ip else None + if self._icon: + self._icon.title = _tooltip(self._state, self._exit_ip) + + def _show(self, icon: Any = None, item: Any = None) -> None: + self._on_show() + + def _rotate(self, icon: Any = None, item: Any = None) -> None: + self._on_rotate() + + def _quit(self, icon: Any = None, item: Any = None) -> None: + self._on_quit() diff --git a/proxy_chain_manager/validator.py b/proxy_chain_manager/validator.py new file mode 100644 index 0000000..41540fa --- /dev/null +++ b/proxy_chain_manager/validator.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +import asyncio +import json +import logging +import time +from typing import Callable + +import httpx + +log = logging.getLogger(__name__) + +# Secondary fallback endpoints for IP resolution. +# HTTP endpoints come first: many free proxies and older Windows Server +# environments fail HTTPS CONNECT through chained hops. Plain HTTP IP-check +# succeeds even when only forward-proxying (no CONNECT) works, so the chain's +# "last leg" exit-IP verification doesn't fail just because TLS can't tunnel. +_IP_FALLBACKS = [ + "http://api.ipify.org/?format=json", + "http://checkip.amazonaws.com/", + "http://ip-api.com/json/", + "http://ifconfig.me/ip", + "https://api.ipify.org?format=json", + "https://httpbin.org/ip", + "https://ifconfig.me/ip", +] + + +def _parse_ip(text: str) -> str | None: + """Parse an IP string from JSON or plain-text response body.""" + body = text.strip() + if not body: + return None + if body.startswith("{"): + try: + d = json.loads(body) + ip = d.get("ip") or d.get("origin") or d.get("query") + return str(ip).split(",")[0].strip() if ip else None + except Exception: + return None + # Plain-text: must look like an IP (short, no HTML) + if len(body) < 50 and "<" not in body: + candidate = body.split()[0].strip() + if candidate.count(".") >= 3 or ":" in candidate: + return candidate + return None + + +async def validate_proxies( + proxy_urls: list[str], + check_url: str, + concurrency: int, + timeout_seconds: float, + on_progress: Callable[[int, int], None] | None = None, + target: int = 0, +) -> list[str]: + """Validate proxies concurrently. + + Args: + target: Stop as soon as this many valid proxies are found (0 = test all). + """ + if not proxy_urls: + return [] + sem = asyncio.Semaphore(max(1, concurrency)) + ok: list[str] = [] + lock = asyncio.Lock() + total = len(proxy_urls) + done = 0 + c = max(1, concurrency) + waves = (total + c - 1) // c + cap = min(600.0, float(waves) * float(timeout_seconds) + 45.0) + cap = max(90.0, cap) + log.info( + "validate_proxies: n=%d concurrency=%d per_timeout=%.1fs wall_cap~=%.0fs target=%s check=%s", + total, + c, + timeout_seconds, + cap, + target if target else "all", + (check_url[:70] + "…") if len(check_url) > 70 else check_url, + ) + + last_prog_t = 0.0 + prog_step = max(1, total // 120) + enough = False # set True once target reached; tasks skip HTTP call and drain fast + + async def one(u: str) -> None: + nonlocal done, last_prog_t, enough + # Skip cost of the HTTP check once we already have enough + if enough: + async with lock: + done += 1 + if on_progress and done == total: + on_progress(done, total) + return + async with sem: + if enough: # re-check after potentially waiting on semaphore + good = False + else: + good = await _check_one(u, check_url, timeout_seconds) + async with lock: + done += 1 + if good: + ok.append(u) + if target and len(ok) >= target: + enough = True + log.info("validate_proxies: target %d reached after %d tested", target, done) + if on_progress: + now = time.monotonic() + if ( + done == total + or done == 1 + or enough + or done % prog_step == 0 + or (now - last_prog_t) >= 0.1 + ): + last_prog_t = now + on_progress(done, total) + + async def _run_all() -> None: + await asyncio.gather(*(one(u) for u in proxy_urls)) + + try: + await asyncio.wait_for(_run_all(), timeout=cap) + except asyncio.TimeoutError: + log.warning( + "validate_proxies wall-clock cap %.0fs exceeded (%d URLs) — returning partial results", + cap, + total, + ) + log.info("validate_proxies: finished ok=%d / %d tested", len(ok), done) + return ok + + +async def _check_one(proxy_url: str, check_url: str, timeout_seconds: float) -> bool: + """Check a single proxy by fetching check_url through it. True = live and returns 200.""" + t = httpx.Timeout(timeout_seconds, connect=min(8.0, timeout_seconds)) + try: + async with httpx.AsyncClient( + proxy=proxy_url, + timeout=t, + verify=False, + follow_redirects=True, + ) as c: + r = await c.get(check_url) + return r.status_code == 200 and len(r.content) > 0 + except Exception: + return False + + +async def check_chain_exit_ip( + listen_proxy: str, + check_url: str, + timeout_seconds: float, + chain_hops: int = 3, +) -> str | None: + """Query the IP-check URL through the local chain proxy. + + Bounded total time — never stacks one slow request per fallback URL forever. + Per-request timeout scales mildly with chain length so 5+ hop chains on + slower hardware (older Windows Server, low-spec VPS) don't time out on + cumulative TLS/CONNECT handshakes. + """ + hops = max(1, int(chain_hops)) + scale = 1.0 + max(0, hops - 3) * 0.35 + per = max(5.0, min(30.0, float(timeout_seconds) * scale)) + budget = max(20.0, min(90.0, float(timeout_seconds) * 2 * scale + 5.0)) + # Try more endpoints with HTTP first; if any hop blocks CONNECT, HTTPS + # IP-check would fail and the whole chain looks "dead" at the last step. + urls = [check_url] + [u for u in _IP_FALLBACKS if u != check_url][:4] + log.debug( + "check_chain_exit_ip: hops=%d budget=%.1fs per_req=%.1fs fallback_count=%d", + hops, + budget, + per, + len(urls), + ) + + async def _run() -> str | None: + t = httpx.Timeout(per, connect=min(8.0, per)) + try: + async with httpx.AsyncClient( + proxy=listen_proxy, + timeout=t, + verify=False, + follow_redirects=True, + ) as c: + for url in urls: + try: + r = await c.get(url) + if r.status_code == 200: + ip = _parse_ip(r.text) + if ip: + return ip + except Exception: + continue + except Exception: + pass + return None + + try: + return await asyncio.wait_for(_run(), timeout=budget) + except asyncio.TimeoutError: + log.debug("check_chain_exit_ip timed out after %.1fs", budget) + return None + + +async def get_direct_ip(check_url: str, timeout_seconds: float = 15.0) -> str | None: + """Get our own exit IP without the proxy chain (so we can compare).""" + per = max(5.0, min(15.0, float(timeout_seconds))) + budget = max(12.0, min(45.0, float(timeout_seconds) * 2)) + urls = [check_url] + [u for u in _IP_FALLBACKS if u != check_url][:2] + + async def _run() -> str | None: + t = httpx.Timeout(per) + try: + async with httpx.AsyncClient( + timeout=t, + verify=False, + follow_redirects=True, + ) as c: + for url in urls: + try: + r = await c.get(url) + if r.status_code == 200: + ip = _parse_ip(r.text) + if ip: + return ip + except Exception: + continue + except Exception: + pass + return None + + try: + return await asyncio.wait_for(_run(), timeout=budget) + except asyncio.TimeoutError: + log.debug("get_direct_ip timed out after %.1fs", budget) + return None + + +async def check_https_tunnel( + listen_proxy: str, + timeout_seconds: float = 12.0, +) -> tuple[bool, str]: + """Return (ok, message). True means the chain successfully tunneled HTTPS. + + Many free proxies pass plain HTTP but refuse HTTP ``CONNECT`` (the method + required to tunnel TLS). The chain can look healthy on an IP-check via + plain HTTP yet break every HTTPS page a browser tries to load. This probe + catches that case at start time so the user gets a clear warning instead + of a mystery "browser broken". + """ + per = max(5.0, min(20.0, float(timeout_seconds))) + t = httpx.Timeout(per, connect=min(8.0, per)) + # Use a tiny, fast, very-common HTTPS endpoint. + targets = [ + "https://www.google.com/generate_204", + "https://www.cloudflare.com/cdn-cgi/trace", + "https://duckduckgo.com/", + ] + last_err = "" + try: + async with httpx.AsyncClient( + proxy=listen_proxy, timeout=t, verify=False, follow_redirects=True, + ) as c: + for url in targets: + try: + r = await c.get(url) + if 200 <= r.status_code < 400: + return True, f"HTTPS OK via {url} ({r.status_code})" + except Exception as e: + last_err = f"{type(e).__name__}: {e}" + continue + except Exception as e: + last_err = f"{type(e).__name__}: {e}" + return False, last_err or "all HTTPS targets failed" diff --git a/proxy_chain_manager/vpn_detect.py b/proxy_chain_manager/vpn_detect.py new file mode 100644 index 0000000..56bbd72 --- /dev/null +++ b/proxy_chain_manager/vpn_detect.py @@ -0,0 +1,233 @@ +"""Detect whether a VPN tunnel is active on Windows (any provider).""" +from __future__ import annotations + +import glob +import logging +import re +import subprocess +import sys +from dataclasses import dataclass, field +from pathlib import Path + +log = logging.getLogger(__name__) + +# Adapter description / name substrings (case-insensitive) +_VPN_ADAPTER_HINTS = ( + "nordlynx", "nordvpn", "openvpn", "wireguard", "wintun", "tap-windows", + "tailscale", "zerotier", "cisco anyconnect", "fortinet", "pulse secure", + "globalprotect", "softether", "proton", "mullvad", "expressvpn", + "surfshark", "private internet", "pia ", "windscribe", "hotspot shield", +) + +_NON_VPN_HINTS = ( + "wan miniport", + "microsoft kernel debug", + "isatap", + "teredo", + "loopback", + "pseudo-interface", +) + +# Executables to whitelist in kill-switch when present +_VPN_EXE_GLOBS: list[str] = [ + r"C:\Program Files\NordVPN\*.exe", + r"C:\Program Files\NordUpdater\*.exe", + r"C:\Program Files\NordVPN\NordSec ThreatProtection\*.exe", + r"C:\Program Files\OpenVPN\bin\*.exe", + r"C:\Program Files\OpenVPN Connect\*.exe", + r"C:\Program Files\WireGuard\*.exe", + r"C:\Program Files\Proton\VPN\*.exe", + r"C:\Program Files\Mullvad VPN\*.exe", + r"C:\Program Files\ExpressVPN\*.exe", + r"C:\Program Files\Surfshark\*.exe", + r"C:\Program Files\Private Internet Access\*.exe", + r"C:\Program Files\Tailscale\*.exe", + r"C:\Program Files\ZeroTier\One\*.exe", +] + +_PROVIDER_FROM_ADAPTER: list[tuple[str, str]] = [ + ("nordlynx", "NordVPN"), + ("nordvpn", "NordVPN"), + ("wireguard", "WireGuard"), + ("wintun", "WireGuard"), + ("openvpn", "OpenVPN"), + ("proton", "Proton VPN"), + ("mullvad", "Mullvad"), + ("expressvpn", "ExpressVPN"), + ("surfshark", "Surfshark"), + ("tailscale", "Tailscale"), + ("zerotier", "ZeroTier"), + ("tap-windows", "OpenVPN/TAP"), + ("globalprotect", "GlobalProtect"), + ("fortinet", "FortiClient"), + ("cisco", "Cisco VPN"), +] + + +@dataclass +class VpnStatus: + active: bool = False + label: str = "Direct (no VPN)" + adapter: str = "" + adapters: list[str] = field(default_factory=list) + + def short_label(self) -> str: + if not self.active: + return "Direct" + return self.label + + +def _run_ps(script: str, timeout: float = 12.0) -> str: + try: + r = subprocess.run( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", script], + capture_output=True, + text=True, + timeout=timeout, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + return (r.stdout or "").strip() + except Exception as e: + log.debug("vpn_detect powershell failed: %s", e) + return "" + + +def _match_provider(name: str) -> str: + low = name.lower() + for hint, label in _PROVIDER_FROM_ADAPTER: + if hint in low: + return label + if "vpn" in low or "tunnel" in low: + return "VPN" + return "VPN" + + +def detect_vpn() -> VpnStatus: + """Inspect up network adapters for VPN/tunnel interfaces.""" + if sys.platform == "darwin": + try: + r = subprocess.run(["ifconfig"], capture_output=True, text=True, timeout=8) + names: list[str] = [] + for line in (r.stdout or "").splitlines(): + if line and not line.startswith("\t") and ":" in line: + name = line.split(":", 1)[0].strip() + if name: + names.append(name) + hits = [n for n in names if n.startswith(("utun", "tun", "tap", "wg"))] + if not hits: + return VpnStatus(active=False, label="Direct (no VPN)", adapters=names) + primary = hits[0] + return VpnStatus(active=True, label=_match_provider(primary), adapter=primary, adapters=names) + except Exception: + return VpnStatus() + out = _run_ps( + "Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | " + "ForEach-Object { \"$($_.Name)|$($_.InterfaceDescription)\" }" + ) + if not out: + # Fallback for PowerShell 2.0 / older Windows Server: parse "wmic nic" + # then "netsh interface show interface". Both are locale-tolerant. + try: + r = subprocess.run( + ["wmic", "nic", "where", "NetEnabled=true", + "get", "NetConnectionID,Name", "/format:list"], + capture_output=True, + text=True, + timeout=12, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + blob = (r.stdout or "") + names: list[str] = [] + cur_id = "" + cur_name = "" + for ln in blob.splitlines(): + ln = ln.strip() + if not ln: + if cur_id: + names.append(f"{cur_id}|{cur_name}") + cur_id, cur_name = "", "" + continue + if ln.startswith("NetConnectionID="): + cur_id = ln.split("=", 1)[1].strip() + elif ln.startswith("Name="): + cur_name = ln.split("=", 1)[1].strip() + if cur_id: + names.append(f"{cur_id}|{cur_name}") + out = "\n".join(names) + except Exception: + out = "" + + if not out: + # Last-ditch: netsh. Locale-tolerant — match the connected/enabled state + # by extracting the interface name from the rightmost column. Older + # localized Server SKUs (es, de, fr, etc.) don't print literal "Enabled". + try: + r = subprocess.run( + ["netsh", "interface", "show", "interface"], + capture_output=True, + text=True, + timeout=10, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + raw = (r.stdout or "") + names = [] + for ln in raw.splitlines(): + s = ln.rstrip() + if not s or s.startswith("-") or ":" in s.split(" ")[0]: + continue + parts = re.split(r"\s{2,}", s.strip()) + if len(parts) >= 4: + nm = parts[-1].strip() + if nm and nm.lower() not in ("interface name", "nombre de interfaz"): + names.append(f"{nm}|") + out = "\n".join(names) + except Exception: + return VpnStatus() + + adapters: list[str] = [] + adapter_blob: list[str] = [] + for line in out.splitlines(): + raw = line.strip() + if not raw: + continue + if "|" in raw: + name, desc = raw.split("|", 1) + else: + name, desc = raw, "" + name = name.strip() + desc = desc.strip() + if name: + adapters.append(name) + adapter_blob.append((name + " " + desc).strip()) + + hits: list[str] = [] + for i, name in enumerate(adapters): + low = adapter_blob[i].lower() + if any(h in low for h in _NON_VPN_HINTS): + continue + if any(h in low for h in _VPN_ADAPTER_HINTS): + hits.append(name) + + if not hits: + return VpnStatus(active=False, label="Direct (no VPN)", adapters=adapters) + + primary = hits[0] + return VpnStatus( + active=True, + label=_match_provider(primary), + adapter=primary, + adapters=adapters, + ) + + +def expand_vpn_executables() -> list[str]: + """Paths to VPN client binaries for firewall allow rules.""" + seen: set[str] = set() + out: list[str] = [] + for pattern in _VPN_EXE_GLOBS: + for p in glob.glob(pattern): + rp = str(Path(p).resolve()) + if rp not in seen: + seen.add(rp) + out.append(rp) + return out diff --git a/proxy_chain_manager/webrtc_check.py b/proxy_chain_manager/webrtc_check.py new file mode 100644 index 0000000..fe799d9 --- /dev/null +++ b/proxy_chain_manager/webrtc_check.py @@ -0,0 +1,231 @@ +"""WebRTC leak detection and hardening checks. + +WebRTC leaks expose your real IP through STUN/ICE even when a proxy is set. +This module checks every controllable surface: + + 1. OS-level Chrome / Edge Group Policy (prevents non-proxied UDP) + 2. Firefox managed profile prefs (user.js must disable PeerConnection) + 3. STUN UDP reachability (if STUN is reachable, WebRTC CAN leak) + 4. Firefox process prefs (confirm our user.js was accepted) +""" +from __future__ import annotations + +import logging +import socket +import subprocess +import winreg +from dataclasses import dataclass, field +from pathlib import Path + +log = logging.getLogger(__name__) + +# STUN servers commonly used by WebRTC +_STUN_HOSTS: list[tuple[str, int]] = [ + ("stun.l.google.com", 19302), + ("stun1.l.google.com", 19302), + ("stun.cloudflare.com", 3478), + ("stun.stunprotocol.org", 3478), +] + +# Firefox user.js prefs that block WebRTC +_REQUIRED_WEBRTC_PREFS: dict[str, str] = { + "media.peerconnection.enabled": "false", + "media.peerconnection.ice.default_address_only": "true", + "media.peerconnection.ice.no_host": "true", +} + +# Chrome / Edge HKLM policy keys +_CHROMIUM_POLICY_PATHS = ( + r"SOFTWARE\Policies\Google\Chrome", + r"SOFTWARE\Policies\Microsoft\Edge", + r"SOFTWARE\Policies\Chromium", +) +_WEBRTC_POLICY_VALUE = "DefaultWebRtcIpHandlingPolicy" +_WEBRTC_BLOCK_VALUE = 3 # disable_non_proxied_udp (was 2 = public+private only — wrong) +_WEBRTC_POLICY_STR_KEY = "WebRtcIPHandling" +_WEBRTC_BLOCK_STR_VALUE = "disable_non_proxied_udp" + + +@dataclass +class WebRtcCheckResult: + """Aggregated WebRTC leak surface scan.""" + # Policy / config checks + chrome_edge_policy_set: bool = False + chrome_edge_policy_detail: str = "" + firefox_prefs_ok: bool = False + firefox_prefs_detail: str = "" + # Network reachability + stun_reachable: bool = False + stun_detail: str = "" + # Convenience flags + any_leak_risk: bool = True + issues: list[str] = field(default_factory=list) + checks: list[tuple[str, bool, str]] = field(default_factory=list) # (name, ok, detail) + + def add(self, name: str, ok: bool, detail: str) -> None: + self.checks.append((name, ok, detail)) + if not ok: + self.issues.append(f"{name}: {detail}") + + +# ───────────────────────────────────────────────────────────────────────────── +# 1. Chrome / Edge Group Policy +# ───────────────────────────────────────────────────────────────────────────── + +def check_chromium_webrtc_policy() -> tuple[bool, str]: + """Return (policy_set, detail_string). + + Accepts either the legacy DWORD DefaultWebRtcIpHandlingPolicy==3 + OR the modern REG_SZ WebRtcIPHandling=="disable_non_proxied_udp". + """ + found: list[str] = [] + missing: list[str] = [] + for path in _CHROMIUM_POLICY_PATHS: + try: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_QUERY_VALUE) as k: + name = path.split("\\")[-1] + dword_ok = False + str_ok = False + # Check legacy DWORD + try: + val = int(winreg.QueryValueEx(k, _WEBRTC_POLICY_VALUE)[0]) + dword_ok = val == _WEBRTC_BLOCK_VALUE + if not dword_ok: + missing.append(f"{name} DWORD={val} (need {_WEBRTC_BLOCK_VALUE})") + except OSError: + pass + # Check modern REG_SZ + try: + val_str = str(winreg.QueryValueEx(k, _WEBRTC_POLICY_STR_KEY)[0]) + str_ok = val_str == _WEBRTC_BLOCK_STR_VALUE + if not str_ok: + missing.append(f"{name} REG_SZ={val_str!r} (need {_WEBRTC_BLOCK_STR_VALUE!r})") + except OSError: + pass + if dword_ok or str_ok: + found.append(name) + elif not dword_ok and not str_ok: + missing.append(f"{name}: no WebRTC policy keys present") + except OSError: + continue # key not present at all — browser not installed or not policy-managed + + if found: + return True, "Policy set: " + ", ".join(found) + if missing: + return False, "Not set: " + "; ".join(missing) + return False, "No Chrome/Edge/Chromium policy keys found (browsers may not be installed)" + + +# ───────────────────────────────────────────────────────────────────────────── +# 2. Firefox managed profile prefs +# ───────────────────────────────────────────────────────────────────────────── + +def check_firefox_webrtc_prefs(profile_dir: Path) -> tuple[bool, str]: + """Verify our written user.js contains the required WebRTC-disabling prefs.""" + user_js = profile_dir / "user.js" + if not user_js.is_file(): + return False, f"user.js not found at {profile_dir} — profile not yet initialized" + + try: + content = user_js.read_text(encoding="utf-8", errors="replace") + except Exception as e: + return False, f"Cannot read user.js: {e}" + + missing: list[str] = [] + for pref, expected_val in _REQUIRED_WEBRTC_PREFS.items(): + # Look for: user_pref("pref.name", value); + needle = f'"{pref}", {expected_val}' + if needle not in content: + missing.append(pref) + + if missing: + return False, "Missing in user.js: " + ", ".join(missing) + return True, "All WebRTC prefs present in managed profile" + + +# ───────────────────────────────────────────────────────────────────────────── +# 3. STUN server UDP reachability +# ───────────────────────────────────────────────────────────────────────────── + +def _probe_stun_udp(host: str, port: int, timeout: float = 2.5) -> bool: + """ + Send a minimal STUN Binding Request over UDP and wait for a response. + Returns True if we get *any* response (STUN or ICMP unreachable). + """ + # Minimal STUN Binding Request (RFC 5389) + msg = ( + b"\x00\x01" # Message Type: Binding Request + b"\x00\x00" # Message Length: 0 + b"\x21\x12\xa4\x42" # Magic Cookie + + b"\x00" * 12 # Transaction ID (12 bytes) + ) + try: + ip = socket.gethostbyname(host) + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(timeout) + s.sendto(msg, (ip, port)) + data, _ = s.recvfrom(512) + s.close() + return len(data) > 0 + except socket.timeout: + return False + except Exception: + return False + + +def check_stun_reachability(timeout: float = 3.0) -> tuple[bool, str]: + """ + Returns (reachable, detail). + reachable = True means STUN servers ARE reachable → WebRTC COULD leak. + """ + reachable: list[str] = [] + for host, port in _STUN_HOSTS[:3]: + try: + if _probe_stun_udp(host, port, timeout): + reachable.append(f"{host}:{port}") + except Exception: + continue + + if reachable: + return True, "STUN reachable: " + ", ".join(reachable) + " — WebRTC UDP is NOT blocked" + return False, "STUN servers unreachable — WebRTC UDP appears blocked (good)" + + +# ───────────────────────────────────────────────────────────────────────────── +# Composite scan +# ───────────────────────────────────────────────────────────────────────────── + +def run_webrtc_check(profile_dir: Path | None = None) -> WebRtcCheckResult: + """ + Run all WebRTC leak surface checks. + + profile_dir: path to the managed Firefox profile (to verify user.js). + """ + res = WebRtcCheckResult() + + # 1. Chrome/Edge policy + policy_ok, policy_detail = check_chromium_webrtc_policy() + res.chrome_edge_policy_set = policy_ok + res.chrome_edge_policy_detail = policy_detail + res.add("Chrome/Edge WebRTC policy (HKLM)", policy_ok, policy_detail) + + # 2. Firefox profile prefs + if profile_dir is not None: + fx_ok, fx_detail = check_firefox_webrtc_prefs(profile_dir) + res.firefox_prefs_ok = fx_ok + res.firefox_prefs_detail = fx_detail + res.add("Firefox managed profile WebRTC prefs", fx_ok, fx_detail) + else: + res.add("Firefox managed profile WebRTC prefs", False, + "Profile not specified — not checked") + + # 3. STUN UDP reachability + stun_reachable, stun_detail = check_stun_reachability() + res.stun_reachable = stun_reachable + res.stun_detail = stun_detail + # STUN reachable = risk; NOT reachable = good (firewall is blocking it) + res.add("STUN UDP reachability", not stun_reachable, stun_detail) + + # Overall verdict + res.any_leak_risk = bool(res.issues) + return res diff --git a/proxy_chain_manager/win_compat.py b/proxy_chain_manager/win_compat.py new file mode 100644 index 0000000..f9220a4 --- /dev/null +++ b/proxy_chain_manager/win_compat.py @@ -0,0 +1,107 @@ +"""Windows version / PowerShell capability probe. + +Used by privacy features to log a single clear reason when a function silently +no-ops on older Windows Server hosts (Server 2008 R2 ships PowerShell 2.0 which +does not have ``Get-NetAdapter`` / ``Set-NetAdapter`` / ``Disable-NetAdapterBinding`` +/ ``Get-DnsClientServerAddress`` / ``Rename-Computer``). +""" +from __future__ import annotations + +import logging +import platform +import subprocess +import sys +from dataclasses import dataclass +from functools import lru_cache + +log = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class WinCompat: + release: str # e.g. "10", "2012ServerR2" + build: int # major build number + powershell_major: int # 0 = PowerShell not detected + has_net_cmdlets: bool # Get-NetAdapter etc. (PS 3.0+ on Server 2012+) + has_defender: bool # Add-MpPreference cmdlet exists + + def summary(self) -> str: + if sys.platform != "win32": + return ( + f"{platform.system()} release={self.release} " + f"PowerShell={self.powershell_major}.x " + "Windows-only privacy controls disabled" + ) + return ( + f"Windows release={self.release} build={self.build} " + f"PowerShell={self.powershell_major}.x " + f"NetAdapter cmdlets={'yes' if self.has_net_cmdlets else 'no'} " + f"Defender={'yes' if self.has_defender else 'no'}" + ) + + +def _powershell_major() -> int: + try: + r = subprocess.run( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", + "$PSVersionTable.PSVersion.Major"], + capture_output=True, + text=True, + timeout=12, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + out = (r.stdout or "").strip().splitlines()[-1] if r.stdout else "" + return int(out) if out.isdigit() else 0 + except Exception: + return 0 + + +def _has_cmdlet(name: str) -> bool: + try: + r = subprocess.run( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", + f"if (Get-Command {name} -ErrorAction SilentlyContinue) " + f"{{ 'yes' }} else {{ 'no' }}"], + capture_output=True, + text=True, + timeout=10, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + return "yes" in (r.stdout or "").lower() + except Exception: + return False + + +@lru_cache(maxsize=1) +def probe() -> WinCompat: + """Cached host probe. Cheap on subsequent calls.""" + if sys.platform != "win32": + info = WinCompat( + release=platform.release(), + build=0, + powershell_major=0, + has_net_cmdlets=False, + has_defender=False, + ) + log.info("Compat probe: %s", info.summary()) + return info + release = platform.release() + try: + build = int(platform.version().split(".")[-1]) + except Exception: + build = 0 + ps = _powershell_major() + # PowerShell >= 3.0 is the gate for the modern Net* cmdlets that all the + # privacy features rely on. Anything older (Server 2008 R2 RTM) only has + # PS 2.0 and needs WMI / netsh / ipconfig fallbacks. + has_net = ps >= 3 and _has_cmdlet("Get-NetAdapter") + has_def = _has_cmdlet("Add-MpPreference") + info = WinCompat( + release=release, + build=build, + powershell_major=ps, + has_net_cmdlets=has_net, + has_defender=has_def, + ) + log.info("WinCompat probe: %s", info.summary()) + return info diff --git a/proxy_chain_manager/windows_task.py b/proxy_chain_manager/windows_task.py new file mode 100644 index 0000000..aafa3a8 --- /dev/null +++ b/proxy_chain_manager/windows_task.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +from .paths import app_data_dir + +TASK_NAME = "ProxyChainManagerAutoStart" + + +def _write_launcher_cmd() -> Path: + """Create a stable launcher script so Task Scheduler does not need fragile quoting.""" + if getattr(sys, "frozen", False): + exe = Path(sys.executable).resolve() + body = f'@echo off\r\nstart "" /B "{exe}"\r\n' + else: + py = Path(sys.executable).resolve() + if py.name.lower() == "python.exe": + cand = py.with_name("pythonw.exe") + if cand.is_file(): + py = cand + root = Path(__file__).resolve().parent.parent + run_py = (root / "run.py").resolve() + body = f'@echo off\r\n"{py}" "{run_py}"\r\n' + + p = app_data_dir() / "launch_proxy_chain_manager.cmd" + p.write_text(body, encoding="utf-8") + return p + + +def install_logon_task() -> tuple[bool, str]: + launcher = _write_launcher_cmd() + tr = str(launcher) + try: + r = subprocess.run( + [ + "schtasks", + "/Create", + "/F", + "/TN", + TASK_NAME, + "/TR", + tr, + "/SC", + "ONLOGON", + "/RL", + "LIMITED", + ], + capture_output=True, + text=True, + timeout=60, + ) + out = (r.stdout or "") + (r.stderr or "") + return r.returncode == 0, out.strip() or ("OK" if r.returncode == 0 else "Failed") + except Exception as e: + return False, str(e) + + +def uninstall_logon_task() -> tuple[bool, str]: + try: + r = subprocess.run( + ["schtasks", "/Delete", "/F", "/TN", TASK_NAME], + capture_output=True, + text=True, + timeout=60, + ) + out = (r.stdout or "") + (r.stderr or "") + return r.returncode == 0, out.strip() or ("OK" if r.returncode == 0 else "Failed") + except Exception as e: + return False, str(e) + + +def task_exists() -> bool: + try: + r = subprocess.run( + ["schtasks", "/Query", "/TN", TASK_NAME], + capture_output=True, + text=True, + timeout=30, + ) + return r.returncode == 0 + except Exception: + return False diff --git a/proxy_chain_manager/world_map.png b/proxy_chain_manager/world_map.png new file mode 100644 index 0000000000000000000000000000000000000000..9634198cebda71a8f3b34a27c63237f64ffa428a GIT binary patch literal 154826 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