Add ban tester, signup prep, exit IP intel, and sticky-exit hold.
New GUI tabs probe popular sites for exit-IP bans and prep signup autofill through the chain; Live tab shows geo/ASN/datacenter flags, and sticky-exit keeps the same egress IP during signup flows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
Proxy God — GUI
|
||||
Tabs: Live | Chain Builder | Browser | Privacy | Settings
|
||||
Tabs: Live | Chain Builder | Browser | Ban Tester | Signup Prep | Privacy | Settings
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import ipaddress
|
||||
import queue
|
||||
import sys
|
||||
import threading
|
||||
@@ -13,6 +14,7 @@ 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
|
||||
|
||||
@@ -36,6 +38,24 @@ from .firewall import (
|
||||
request_admin_relaunch,
|
||||
)
|
||||
from .artifact_wipe import wipe_artifacts
|
||||
from .ban_tester import BanTestResult, run_ban_tests, test_single_site
|
||||
from .exit_intel import ExitIntel, fetch_exit_intel
|
||||
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,
|
||||
@@ -175,6 +195,7 @@ def main() -> None:
|
||||
browser = BrowserSession()
|
||||
browser_should_run = [False]
|
||||
browser_last_launch_ts = [0.0]
|
||||
service_running = [False]
|
||||
|
||||
# ── tray ─────────────────────────────────────────────────────────────────
|
||||
tray = TrayIcon(
|
||||
@@ -200,6 +221,17 @@ def main() -> None:
|
||||
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
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -422,9 +454,11 @@ def main() -> None:
|
||||
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_privacy, tab_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 ───────────────────────────────────────────────────────────
|
||||
@@ -450,6 +484,99 @@ def main() -> None:
|
||||
_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)
|
||||
|
||||
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")
|
||||
|
||||
# Log box
|
||||
log_frame = ctk.CTkFrame(tab_live, fg_color=CARD, corner_radius=8)
|
||||
log_frame.pack(fill="both", expand=True)
|
||||
@@ -1178,6 +1305,9 @@ def main() -> None:
|
||||
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)
|
||||
@@ -1189,6 +1319,568 @@ def main() -> None:
|
||||
)
|
||||
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 List 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))
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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) -> None:
|
||||
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)
|
||||
)
|
||||
|
||||
def _render_ban_results(results: list[BanTestResult]) -> None:
|
||||
for w in list(ban_rows_wrap.winfo_children()):
|
||||
w.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
|
||||
for r in results:
|
||||
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)
|
||||
|
||||
_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()}"
|
||||
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")
|
||||
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 ban 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],
|
||||
)
|
||||
)
|
||||
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))
|
||||
|
||||
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]
|
||||
|
||||
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 _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",
|
||||
"Verifies chain, target site reachability, and exit-IP intel before you sign up.",
|
||||
)
|
||||
pf_rows: dict[str, dict[str, ctk.StringVar | ctk.CTkLabel]] = {}
|
||||
|
||||
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")
|
||||
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")
|
||||
|
||||
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)
|
||||
|
||||
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 ("chain", "exit_ip", "intel", "target"):
|
||||
_pf_set(k, "running", "checking…")
|
||||
pf_summary_var.set("Running pre-flight…")
|
||||
|
||||
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")
|
||||
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)
|
||||
|
||||
def done() -> None:
|
||||
ip = (v_exit_ip.get() or "—").strip()
|
||||
if ip and ip != "—":
|
||||
_pf_set("exit_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}")
|
||||
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)
|
||||
|
||||
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))
|
||||
pf_running[0] = False
|
||||
|
||||
root.after(0, done)
|
||||
|
||||
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,
|
||||
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 w in list(accounts_list.winfo_children()):
|
||||
w.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[: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)
|
||||
|
||||
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}.")
|
||||
else:
|
||||
signup_status_var.set("Browser launch failed.")
|
||||
svc.set_sticky(0)
|
||||
|
||||
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)
|
||||
@@ -1635,6 +2327,7 @@ def main() -> None:
|
||||
|
||||
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()
|
||||
@@ -1740,15 +2433,24 @@ def main() -> None:
|
||||
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_ip, fixed_last=fixed_last)
|
||||
_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 "—")
|
||||
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()
|
||||
@@ -1800,6 +2502,7 @@ def main() -> None:
|
||||
_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)
|
||||
|
||||
Reference in New Issue
Block a user