feat: exhaustive feature expansion — cookie modes, DNS/WebRTC testers, map, Firefox fix
Cookie system: - Expand from 5 to 12 fully-specified CookiePolicy modes in browser_identity.py - Add CookiePolicy dataclass: behavior, lifetime, TCP partitioning, clearOnShutdown.* - browser_profile.py emits full cookie pref set from policy object - UI dropdown widened to 680px with live description label per mode Firefox launch fix: - Detect Firefox via Windows registry, AppData, and shutil.which - Use DETACHED_PROCESS|CREATE_NO_WINDOW|CREATE_NEW_PROCESS_GROUP flags - Wait up to 3s for firefox.exe in tasklist instead of polling parent pid - taskkill on stop() to terminate all firefox.exe processes DNS leak tester: - dns_leak.py: FullDnsLeakReport dataclass, run_dns_leak_test comparing proxy DoH resolution vs direct system DNS WebRTC tester: - webrtc_check.py: STUN UDP probe, registry policy check, user.js pref check Ban tester: - Added SITES_SHOPPING, SITES_CRYPTO, SITES_DNS categories - Parallel execution via ThreadPoolExecutor - Expanded banned-text hint keywords Fingerprint audit: - OS identity checks: hostname, MAC, GUID, OS version, timezone, screen res - Browser consistency analysis of user.js Neon world map: - world_map.png bundled; chain_map.py renders hop arcs over it with glow effect Signup prep: - Auto-save account on Open & Autofill; Copy Email / Copy Pass buttons - Auto-fill custom URL when preset site selected - PyInstaller-safe path resolution for signup_extension Spec: - Bundle signup_extension dir and world_map.png as PyInstaller data files Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -39,7 +39,7 @@ from .firewall import (
|
||||
request_admin_relaunch,
|
||||
)
|
||||
from .artifact_wipe import wipe_artifacts
|
||||
from .ban_tester import BanTestResult, run_ban_tests, test_single_site
|
||||
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 (
|
||||
@@ -64,8 +64,14 @@ from .browser_identity import (
|
||||
PERSONA_LABELS,
|
||||
PERSONAS,
|
||||
)
|
||||
from .dns_leak import check_dns_leak_hint, flush_dns_cache
|
||||
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,
|
||||
@@ -1240,20 +1246,49 @@ def main() -> None:
|
||||
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, 10))
|
||||
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")
|
||||
cookie_var = ctk.StringVar(
|
||||
value=COOKIE_LABELS.get(s.browser_cookie_mode, COOKIE_LABELS["block_third_party"])
|
||||
_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=500,
|
||||
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))
|
||||
|
||||
def _persona_key() -> str:
|
||||
for k in PERSONAS:
|
||||
@@ -1415,68 +1450,81 @@ def main() -> None:
|
||||
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 List Tester",
|
||||
font=(FONT, 13, "bold"),
|
||||
text_color=CYAN,
|
||||
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 high-traffic sites through your ACTIVE chain to detect likely exit-IP bans.",
|
||||
font=(FONT, 10),
|
||||
text_color=TEXT2,
|
||||
).pack(anchor="w", padx=12, pady=(0, 8))
|
||||
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, 8)
|
||||
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_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) -> None:
|
||||
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=140, anchor="w").pack(
|
||||
side="left", padx=(8, 4)
|
||||
)
|
||||
ctk.CTkLabel(row, text=status, font=(FONT, 10, "bold"), text_color=color, width=80).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)
|
||||
)
|
||||
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 w in list(ban_rows_wrap.winfo_children()):
|
||||
w.destroy()
|
||||
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)
|
||||
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)
|
||||
_ban_row(r.site, st, r.detail, color, cat_header=header)
|
||||
|
||||
_render_ban_results([])
|
||||
|
||||
@@ -1489,23 +1537,24 @@ def main() -> None:
|
||||
_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)
|
||||
ok_n = sum(1 for r in results if r.status == "ok")
|
||||
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
|
||||
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_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)
|
||||
@@ -1513,13 +1562,8 @@ def main() -> None:
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
ban_start_btn = _btn(
|
||||
ban_action,
|
||||
"Start ban test",
|
||||
_start_ban_test,
|
||||
w=120,
|
||||
h=30,
|
||||
fg_color=ACCENT2,
|
||||
hover_color=ACCENT,
|
||||
ban_action, "Start test", _start_ban_test,
|
||||
w=120, h=30, fg_color=ACCENT2, hover_color=ACCENT,
|
||||
)
|
||||
ban_start_btn.pack(side="left")
|
||||
|
||||
@@ -1561,11 +1605,12 @@ def main() -> None:
|
||||
signup_site_labels[0],
|
||||
)
|
||||
)
|
||||
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,
|
||||
)
|
||||
signup_site_menu.pack(side="left", padx=(0, 8))
|
||||
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()
|
||||
@@ -1574,12 +1619,22 @@ def main() -> None:
|
||||
return signup_site_keys[i]
|
||||
return signup_site_keys[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="Custom 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 _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")
|
||||
@@ -1695,34 +1750,48 @@ def main() -> None:
|
||||
# ── Pre-flight checklist ─────────────────────────────────────────────────
|
||||
preflight_card = _signup_card(
|
||||
"Pre-flight check",
|
||||
"Verifies chain, target site reachability, and exit-IP intel before you sign up.",
|
||||
"Exhaustive readiness check: chain, exit IP, geo/ASN, DNS leak, WebRTC, "
|
||||
"IPv6, fingerprint, 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=160, anchor="w").pack(side="left")
|
||||
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}
|
||||
|
||||
_pf_row("chain", "Chain running")
|
||||
_pf_row("exit_ip", "Exit IP")
|
||||
_pf_row("intel", "IP intel (geo/ASN)")
|
||||
_pf_row("target", "Target site reachable")
|
||||
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)
|
||||
r["var"].set(detail)
|
||||
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"),
|
||||
@@ -1739,72 +1808,189 @@ def main() -> None:
|
||||
pf_summary_var.set("Pick a site or enter a custom URL first.")
|
||||
return
|
||||
pf_running[0] = True
|
||||
for k in ("chain", "exit_ip", "intel", "target"):
|
||||
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 not chain_ok:
|
||||
_pf_set("chain", "fail", "chain not running")
|
||||
_pf_set("exit_ip", "fail", "n/a")
|
||||
_pf_set("intel", "fail", "n/a")
|
||||
_pf_set("target", "fail", "n/a")
|
||||
pf_summary_var.set("Start the chain, then re-run pre-flight.")
|
||||
pf_running[0] = False
|
||||
return
|
||||
_pf_set("chain", "ok", f"{len(svc.current_chain)} hop(s) up")
|
||||
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:
|
||||
timeout = min(15.0, max(8.0, float(svc.settings.validation_timeout_seconds) + 2.0))
|
||||
intel = fetch_exit_intel(proxy_url, timeout_seconds=timeout)
|
||||
site_res = test_single_site(proxy_url, target_host, url, timeout_seconds=timeout)
|
||||
warns: list[str] = []
|
||||
|
||||
def done() -> None:
|
||||
ip = (v_exit_ip.get() or "—").strip()
|
||||
if ip and ip != "—":
|
||||
_pf_set("exit_ip", "ok", ip)
|
||||
# ── 1. Direct IP (bypass chain) ──────────────────────────────────
|
||||
from .validator import get_direct_ip as _get_direct_ip
|
||||
import asyncio as _asyncio
|
||||
try:
|
||||
direct_ip = _asyncio.run(_get_direct_ip(svc.settings.ip_check_url, timeout / 2))
|
||||
except Exception:
|
||||
direct_ip = None
|
||||
|
||||
def _set_direct(ip: str | None) -> None:
|
||||
if ip:
|
||||
_pf_set("direct_ip", "ok", ip)
|
||||
else:
|
||||
_pf_set("exit_ip", "warn", "no IP reported yet")
|
||||
if intel.ok:
|
||||
_apply_intel(intel)
|
||||
if intel.is_proxy_flagged:
|
||||
_pf_set("intel", "fail", f"proxy-flagged · {intel.org or intel.isp}")
|
||||
elif intel.is_datacenter:
|
||||
_pf_set("intel", "warn", f"datacenter ASN · {intel.org or intel.isp}")
|
||||
_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:
|
||||
loc = ", ".join([b for b in (intel.city, intel.country) if b]) or "—"
|
||||
_pf_set("intel", "ok", f"{loc} · AS{intel.asn} {intel.org or intel.isp}")
|
||||
else:
|
||||
_pf_set("intel", "warn", intel.detail or "lookup failed")
|
||||
if site_res.status == "ok":
|
||||
_pf_set("target", "ok", f"{target_host} {site_res.detail}")
|
||||
elif site_res.status == "banned":
|
||||
_pf_set("target", "fail", f"likely banned · {site_res.detail}")
|
||||
else:
|
||||
_pf_set("target", "warn", site_res.detail)
|
||||
_pf_set("exit_ip", "warn", "no exit IP yet")
|
||||
# IP leak compare
|
||||
if direct_ip and ip_ and ip_ not in ("—", ""):
|
||||
if direct_ip == ip_:
|
||||
_pf_set("leak_compare", "fail",
|
||||
f"exit == direct ({ip_}) — chain not working!")
|
||||
warns.append("exit IP matches direct IP (chain not routing)")
|
||||
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")
|
||||
|
||||
warns: list[str] = []
|
||||
if intel.ok and intel.is_proxy_flagged:
|
||||
warns.append("exit IP is proxy-flagged")
|
||||
if intel.ok and intel.is_datacenter:
|
||||
warns.append("exit IP is a datacenter ASN")
|
||||
if site_res.status == "banned":
|
||||
warns.append("target site likely blocks this IP")
|
||||
if not warns:
|
||||
pf_summary_var.set("All checks passed — good to sign up.")
|
||||
else:
|
||||
pf_summary_var.set("Warnings: " + "; ".join(warns))
|
||||
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
|
||||
import asyncio as _as2
|
||||
tun_ok, tun_msg = _as2.run(
|
||||
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 ─────────────────────────────────────────────────
|
||||
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, done)
|
||||
root.after(200, _final)
|
||||
|
||||
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=130, h=28,
|
||||
_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.")
|
||||
@@ -1824,19 +2010,46 @@ def main() -> None:
|
||||
accounts_list.pack(fill="x", padx=12, pady=(8, 10))
|
||||
|
||||
def _refresh_accounts_ui() -> None:
|
||||
for w in list(accounts_list.winfo_children()):
|
||||
w.destroy()
|
||||
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
|
||||
)
|
||||
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[:40]:
|
||||
line = f"{rec.created_at[:16]} {rec.site} {rec.email} exit={rec.exit_ip or '—'}"
|
||||
ctk.CTkLabel(
|
||||
accounts_list, text=line, font=("Consolas", 10), text_color=TEXT, anchor="w",
|
||||
).pack(anchor="w", padx=8, pady=2)
|
||||
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()
|
||||
@@ -1909,6 +2122,8 @@ def main() -> None:
|
||||
tag = f" [{' · '.join(extras)}]" if extras else ""
|
||||
signup_status_var.set(f"Opened {label}{tag} — autofill active (you submit + captcha).")
|
||||
_log(f"Signup prep: opened {url} with autofill extension{tag}.")
|
||||
# Auto-save entry so the account is recorded even if user forgets to save manually
|
||||
_save_signup_account()
|
||||
else:
|
||||
signup_status_var.set("Browser launch failed.")
|
||||
svc.set_sticky(0)
|
||||
@@ -2100,33 +2315,124 @@ def main() -> None:
|
||||
_btn(fp_card, "Refresh fingerprint audit", _refresh_fingerprint, w=180, h=28).pack(
|
||||
anchor="w", padx=12, pady=(0, 10))
|
||||
|
||||
dns_card = _priv_section("DNS", "Checks whether public resolvers may bypass your chain.")
|
||||
dns_result_lbl = ctk.CTkLabel(dns_card, text="Run a check to see DNS configuration.",
|
||||
font=(FONT, 11), text_color=TEXT2, wraplength=880, justify="left")
|
||||
dns_result_lbl.pack(anchor="w", padx=12, pady=8)
|
||||
# ── 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 _run_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_result_lbl.configure(
|
||||
text=r.message + (f"\nResolvers: {', '.join(r.system_resolvers)}" if r.system_resolvers else ""),
|
||||
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,
|
||||
)
|
||||
|
||||
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, "DNS leak check", _run_dns_check, w=120, h=28).pack(side="left", padx=(0, 8))
|
||||
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_result_lbl.configure(
|
||||
text=msg, text_color=GREEN if ok else RED,
|
||||
)
|
||||
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.",
|
||||
|
||||
Reference in New Issue
Block a user