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:
Dr Jones
2026-05-21 16:47:22 -07:00
parent ac6bde8a08
commit 01c19349ee
12 changed files with 1913 additions and 400 deletions

View File

@@ -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.",