""" Proxy God — GUI Tabs: Live | Chain Builder | Settings """ from __future__ import annotations import logging import queue import sys from datetime import datetime from typing import Any import customtkinter as ctk from .config import ( LISTEN_HOST, OBFUSCATION_LABELS, OBFUSCATION_MODES, Settings, load_settings, merge_proxy_credentials, normalize_proxy_url, redact_proxy_url, save_settings, split_proxy_for_edit, ) from .firewall import ( disengage as fw_disengage, is_admin, is_engaged as fw_is_engaged, request_admin_relaunch, ) from .paths import app_data_dir from .service import ChainService from .sysproxy import clear_system_proxy, is_system_proxy_set from .tray import TrayIcon from .windows_task import install_logon_task, task_exists, uninstall_logon_task LOG_PATH = app_data_dir() / "proxy_chain_manager.log" logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", handlers=[ logging.FileHandler(LOG_PATH, encoding="utf-8"), logging.StreamHandler(sys.stdout), ], ) class UILogHandler(logging.Handler): """Forward ``proxy_chain_manager`` log records to the Live tab (via ``ui_q``). Thread-safe.""" def __init__(self, q: queue.Queue[dict[str, Any]]) -> None: super().__init__(level=logging.DEBUG) self._q = q def emit(self, record: logging.LogRecord) -> None: try: text = self.format(record) self._q.put_nowait({"type": "log", "text": text}) except Exception: self.handleError(record) # ── palette ────────────────────────────────────────────────────────────────── BG = "#0d0d1a" CARD = "#12122a" PANEL = "#1a1a3e" ACCENT = "#1e3a8a" ACCENT2 = "#2563eb" GREEN = "#00e676" RED = "#ff1744" YELLOW = "#ffab00" ORANGE = "#ff6d00" PURPLE = "#bb86fc" BLUE = "#448aff" CYAN = "#00bcd4" DIM = "#4a5568" TEXT = "#e2e8f0" TEXT2 = "#94a3b8" FONT = "Segoe UI" def _ts() -> str: return datetime.now().strftime("%H:%M:%S") def main() -> None: ctk.set_appearance_mode("dark") ctk.set_default_color_theme("dark-blue") root = ctk.CTk() root.title("Proxy God v2") root.geometry("1080x740") root.minsize(900, 620) root.configure(fg_color=BG) ui_q: queue.Queue[dict[str, Any]] = queue.Queue() svc = ChainService(notify=lambda m: ui_q.put(m)) s = load_settings() # ── tray ───────────────────────────────────────────────────────────────── tray = TrayIcon( on_show=lambda: root.after(0, lambda: (root.deiconify(), root.lift())), on_quit=lambda: root.after(0, _close), on_rotate=svc.rotate_now, ) tray.start() # ───────────────────────────────────────────────────────────────────────── # SHARED STATE # ───────────────────────────────────────────────────────────────────────── _hop_count = [s.chain_length] # mutable ref so topbar buttons can mutate it pulse_job: dict[str, str | None] = {"id": None} def _cancel_pulse() -> None: if pulse_job["id"]: try: root.after_cancel(pulse_job["id"]) # type: ignore[arg-type] except Exception: pass pulse_job["id"] = None def _short(u: str) -> str: u = redact_proxy_url(u) u = u.replace("http://", "").replace("socks5://", "s5://").replace("socks4://", "s4://").replace("https://", "") return u[:28] + "…" if len(u) > 30 else u # ───────────────────────────────────────────────────────────────────────── # HELPERS # ───────────────────────────────────────────────────────────────────────── def _btn(parent: Any, label: str, cmd: Any, w: int = 80, h: int = 28, **kw: Any) -> ctk.CTkButton: # Pull known keys out first so **kw never duplicates CTkButton parameters (font, width, …). kw.pop("text", None) kw.pop("command", None) font = kw.pop("font", (FONT, 11)) fg_color = kw.pop("fg_color", ACCENT) hover_color = kw.pop("hover_color", ACCENT2) text_color = kw.pop("text_color", TEXT) corner_radius = kw.pop("corner_radius", 6) width = kw.pop("width", w) height = kw.pop("height", h) return ctk.CTkButton( parent, text=label, command=cmd, width=width, height=height, font=font, fg_color=fg_color, hover_color=hover_color, text_color=text_color, corner_radius=corner_radius, **kw, ) # ───────────────────────────────────────────────────────────────────────── # TOP BAR # ───────────────────────────────────────────────────────────────────────── topbar = ctk.CTkFrame(root, fg_color=CARD, corner_radius=0, height=48) topbar.pack(fill="x", side="top") topbar.pack_propagate(False) # Status status_dot = ctk.CTkLabel(topbar, text="●", font=(FONT, 20), text_color=RED) status_dot.pack(side="left", padx=(10, 2)) status_txt = ctk.CTkLabel(topbar, text="STOPPED", font=(FONT, 12, "bold"), text_color=TEXT) status_txt.pack(side="left", padx=(0, 8)) def _start() -> None: _save_settings() svc.start() _btn(topbar, "▶ Start", _start, w=86).pack(side="left", padx=2) _btn(topbar, "■ Stop", svc.stop, w=76).pack(side="left", padx=2) _btn(topbar, "↻ Rotate", svc.rotate_now, w=76).pack(side="left", padx=2) _btn(topbar, "Quit", lambda: root.after(0, _close), w=52, fg_color="#7f1d1d", hover_color="#991b1b").pack(side="left", padx=(10, 2)) # Hop count +/- control ctk.CTkLabel(topbar, text="│", text_color=DIM).pack(side="left", padx=6) ctk.CTkLabel(topbar, text="Hops:", font=(FONT, 11), text_color=TEXT2).pack(side="left", padx=(0, 2)) hop_count_lbl = ctk.CTkLabel(topbar, text=str(_hop_count[0]), font=(FONT, 15, "bold"), text_color=CYAN, width=24) hop_count_lbl.pack(side="left") def _hop_adjust(delta: int) -> None: new = max(1, min(8, _hop_count[0] + delta)) _hop_count[0] = new hop_count_lbl.configure(text=str(new)) # Update hop slider in Chain Builder if it exists try: hop_slider.set(new) hop_val_lbl.configure(text=str(new)) except Exception: pass # Immediately apply to settings _apply_hop_count(new) _btn(topbar, "−", lambda: _hop_adjust(-1), w=26, h=26, fg_color=PANEL, hover_color=ACCENT).pack(side="left", padx=1) _btn(topbar, "+", lambda: _hop_adjust(+1), w=26, h=26, fg_color=PANEL, hover_color=ACCENT).pack(side="left", padx=(1, 6)) def _apply_hop_count(n: int) -> None: """Save hop count change to live settings immediately.""" cur = svc.settings ns = Settings( local_host=cur.local_host, local_port=cur.local_port, chain_length=n, obfuscation_mode=cur.obfuscation_mode, use_pinned_chain=cur.use_pinned_chain, pinned_chain=cur.pinned_chain, manual_exit_proxy=cur.manual_exit_proxy, health_check_seconds=cur.health_check_seconds, full_refresh_seconds=cur.full_refresh_seconds, validation_concurrency=cur.validation_concurrency, max_candidates=cur.max_candidates, validation_timeout_seconds=cur.validation_timeout_seconds, prefer_elite=cur.prefer_elite, kill_switch_enabled=cur.kill_switch_enabled, proxy_bypass=cur.proxy_bypass, sources=cur.sources, ip_check_url=cur.ip_check_url, ) svc.update_settings(ns) # Boot buttons ctk.CTkLabel(topbar, text="│", text_color=DIM).pack(side="left", padx=4) boot_lbl = ctk.CTkLabel(topbar, text="Boot:?", font=(FONT, 10), text_color=TEXT2) boot_lbl.pack(side="left", padx=2) def _refresh_boot() -> None: on = task_exists() boot_lbl.configure(text="Boot:ON" if on else "Boot:OFF", text_color=GREEN if on else DIM) def _inst_boot() -> None: ok, msg = install_logon_task() _log("Boot task installed." if ok else f"Boot install: {msg}") _refresh_boot() def _rm_boot() -> None: ok, msg = uninstall_logon_task() _log("Boot task removed." if ok else f"Boot remove: {msg}") _refresh_boot() _btn(topbar, "Boot+", _inst_boot, w=54).pack(side="left", padx=2) _btn(topbar, "Boot−", _rm_boot, w=54).pack(side="left", padx=2) # Right side indicators rotation_lbl = ctk.CTkLabel(topbar, text="rot: 0", font=(FONT, 10), text_color=TEXT2) rotation_lbl.pack(side="right", padx=(4, 12)) countdown_lbl = ctk.CTkLabel(topbar, text="next: —", font=(FONT, 10), text_color=TEXT2) countdown_lbl.pack(side="right", padx=4) proxy_lbl = ctk.CTkLabel(topbar, text=f"{LISTEN_HOST}:{s.local_port}", font=(FONT, 11, "bold"), text_color=BLUE) proxy_lbl.pack(side="right", padx=(4, 4)) ctk.CTkLabel(topbar, text="proxy:", font=(FONT, 10), text_color=TEXT2).pack(side="right") fw_lbl = ctk.CTkLabel(topbar, text="FW:—", font=(FONT, 10), text_color=DIM) fw_lbl.pack(side="right", padx=6) sys_lbl = ctk.CTkLabel(topbar, text="SYS:—", font=(FONT, 10), text_color=DIM) sys_lbl.pack(side="right", padx=4) admin_badge = ctk.CTkLabel( topbar, text="⚡ ADMIN" if is_admin() else "👤 USER", font=(FONT, 10, "bold"), text_color=GREEN if is_admin() else YELLOW, ) admin_badge.pack(side="right", padx=4) if not is_admin(): def _elevate() -> None: if request_admin_relaunch(): _close() _btn(topbar, "Run as Admin", _elevate, w=104, fg_color="#7f1d1d", hover_color="#991b1b").pack(side="right", padx=4) # ───────────────────────────────────────────────────────────────────────── # CHAIN VIZ BAR # ───────────────────────────────────────────────────────────────────────── viz_bar = ctk.CTkFrame(root, fg_color=PANEL, corner_radius=0, height=52) viz_bar.pack(fill="x") viz_bar.pack_propagate(False) your_ip_lbl = ctk.CTkLabel(viz_bar, text="Your IP: —", font=(FONT, 10), text_color=TEXT2) your_ip_lbl.pack(side="left", padx=(12, 8)) chain_canvas = ctk.CTkFrame(viz_bar, fg_color="transparent") chain_canvas.pack(side="left", fill="both", expand=True) exit_ip_lbl = ctk.CTkLabel(viz_bar, text="Exit: —", font=(FONT, 13, "bold"), text_color=YELLOW) exit_ip_lbl.pack(side="right", padx=(8, 14)) hop_dot_labels: list[ctk.CTkLabel] = [] def _pulse(step: int = 0) -> None: if not hop_dot_labels: pulse_job["id"] = None return c = YELLOW if step % 2 == 0 else DIM for d in hop_dot_labels: d.configure(text_color=c) pulse_job["id"] = root.after(380, lambda s=step: _pulse(s + 1)) # type: ignore[arg-type] def _render_chain( hops: list[str], status: str, exit_ip: str | None, fixed_last: bool = False ) -> None: _cancel_pulse() for w in list(chain_canvas.winfo_children()): w.destroy() hop_dot_labels.clear() hop_c = {"healthy": GREEN, "dead": RED, "connecting": YELLOW}.get(status, DIM) def _node(text: str, color: str, bold: bool = False) -> None: ctk.CTkLabel( chain_canvas, text=text, font=(FONT, 11, "bold") if bold else (FONT, 10), text_color=color, ).pack(side="left", padx=1) def _arr() -> None: ctk.CTkLabel(chain_canvas, text="→", font=(FONT, 11), text_color=DIM).pack(side="left", padx=2) _node("YOU", GREEN, bold=True) _arr() _node("NORD", PURPLE, bold=True) _arr() last_i = len(hops) - 1 for i, h in enumerate(hops): is_fixed = fixed_last and i == last_i and last_i >= 0 lbl_c = ORANGE if is_fixed else hop_c dot = ctk.CTkLabel(chain_canvas, text="●", font=(FONT, 12), text_color=lbl_c) dot.pack(side="left", padx=1) hop_dot_labels.append(dot) label = _short(h) + (" ★" if is_fixed else "") ctk.CTkLabel(chain_canvas, text=label, font=(FONT, 10), text_color=lbl_c).pack(side="left", padx=1) if i < len(hops) - 1: _arr() _arr() _node("WEB", BLUE, bold=True) exit_ip_lbl.configure( text=f"Exit: {exit_ip}" if exit_ip else "Exit: —", text_color=GREEN if status == "healthy" else (RED if exit_ip else YELLOW), ) if status == "connecting" and hop_dot_labels: _pulse() # ───────────────────────────────────────────────────────────────────────── # PROGRESS STRIP # ───────────────────────────────────────────────────────────────────────── prog_strip = ctk.CTkFrame(root, fg_color=BG, height=22) prog_strip.pack(fill="x", padx=8, pady=(4, 0)) prog_strip.pack_propagate(False) phase_lbl = ctk.CTkLabel(prog_strip, text="idle", font=(FONT, 10), text_color=TEXT2, width=70) phase_lbl.pack(side="left", padx=4) prog_bar = ctk.CTkProgressBar(prog_strip, height=8, corner_radius=4, fg_color=CARD, progress_color=BLUE) prog_bar.pack(side="left", fill="x", expand=True, padx=4) prog_bar.set(0) pool_lbl = ctk.CTkLabel(prog_strip, text="pool: 0", font=(FONT, 10), text_color=TEXT2, width=70) pool_lbl.pack(side="right", padx=4) # ───────────────────────────────────────────────────────────────────────── # TABVIEW # ───────────────────────────────────────────────────────────────────────── tabs = ctk.CTkTabview( root, fg_color=BG, segmented_button_fg_color=CARD, segmented_button_selected_color=ACCENT2, segmented_button_unselected_color=CARD, segmented_button_selected_hover_color=ACCENT, border_width=0, ) tabs.pack(fill="both", expand=True, padx=8, pady=(4, 8)) tab_live = tabs.add(" Live ") tab_chain = tabs.add(" Chain Builder ") tab_settings = tabs.add(" Settings ") for tab in (tab_live, tab_chain, tab_settings): tab.configure(fg_color=BG) # ─── TAB: LIVE ─────────────────────────────────────────────────────────── # Stats cards row stats_row = ctk.CTkFrame(tab_live, fg_color="transparent") stats_row.pack(fill="x", pady=(4, 6)) def _stat_card(parent: Any, title: str, var: ctk.StringVar, color: str = TEXT) -> None: f = ctk.CTkFrame(parent, fg_color=CARD, corner_radius=8) f.pack(side="left", fill="x", expand=True, padx=4) ctk.CTkLabel(f, text=title, font=(FONT, 9), text_color=TEXT2).pack( anchor="w", padx=8, pady=(6, 0)) ctk.CTkLabel(f, textvariable=var, font=(FONT, 14, "bold"), text_color=color).pack( anchor="w", padx=8, pady=(0, 6)) v_real_ip = ctk.StringVar(value="—") v_exit_ip = ctk.StringVar(value="—") v_pool = ctk.StringVar(value="0") v_rotation = ctk.StringVar(value="0") _stat_card(stats_row, "Your IP (through Nord)", v_real_ip) _stat_card(stats_row, "Exit IP (last hop)", v_exit_ip) _stat_card(stats_row, "Valid pool size", v_pool) _stat_card(stats_row, "Rotations this session", v_rotation) # Log box log_frame = ctk.CTkFrame(tab_live, fg_color=CARD, corner_radius=8) log_frame.pack(fill="both", expand=True) log_box = ctk.CTkTextbox( log_frame, font=("Consolas", 10), fg_color=BG, text_color=TEXT, scrollbar_button_color=ACCENT, ) log_box.pack(fill="both", expand=True, padx=4, pady=(0, 4)) def _log(line: str) -> None: log_box.insert("end", f"[{_ts()}] {line}\n") log_box.see("end") log_tool = ctk.CTkFrame(log_frame, fg_color="transparent") log_tool.pack(fill="x", padx=8, pady=(6, 0), before=log_box) verbose_logs_var = ctk.BooleanVar(value=True) _pcm_logger = logging.getLogger("proxy_chain_manager") _ui_log_handler: UILogHandler | None = None for _h in _pcm_logger.handlers: if isinstance(_h, UILogHandler): _ui_log_handler = _h break if _ui_log_handler is None: _ui_log_handler = UILogHandler(ui_q) _ui_log_handler.setFormatter( logging.Formatter("%(levelname)-5s [%(name)s] %(message)s") ) _pcm_logger.addHandler(_ui_log_handler) logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) def _sync_ui_log_level() -> None: on = bool(verbose_logs_var.get()) lvl = logging.DEBUG if on else logging.INFO _pcm_logger.setLevel(lvl) if _ui_log_handler is not None: _ui_log_handler.setLevel(lvl) _log(f"UI log level: {'DEBUG (verbose)' if on else 'INFO'}") ctk.CTkCheckBox( log_tool, text="Verbose logs (DEBUG in UI — file stays INFO)", variable=verbose_logs_var, command=_sync_ui_log_level, font=(FONT, 11), fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT, ).pack(side="left", padx=(0, 12)) _btn(log_tool, "Clear log", lambda: log_box.delete("1.0", "end"), w=88, h=26, fg_color=DIM, hover_color=ACCENT).pack(side="left", padx=(0, 6)) _sync_ui_log_level() # ─── TAB: CHAIN BUILDER ────────────────────────────────────────────────── cb_split = ctk.CTkFrame(tab_chain, fg_color="transparent") cb_split.pack(fill="x", pady=(4, 8)) # Left — mode + hop slider cb_left = ctk.CTkFrame(cb_split, fg_color=CARD, corner_radius=8) cb_left.pack(side="left", fill="both", expand=True, padx=(0, 6)) ctk.CTkLabel(cb_left, text="Obfuscation Mode", font=(FONT, 12, "bold"), text_color=TEXT).pack(anchor="w", padx=12, pady=(12, 4)) mode_var = ctk.StringVar(value=s.obfuscation_mode) for mk in OBFUSCATION_MODES: ctk.CTkRadioButton( cb_left, text=OBFUSCATION_LABELS[mk], variable=mode_var, value=mk, font=(FONT, 11), fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT, ).pack(anchor="w", padx=16, pady=3) ctk.CTkFrame(cb_left, fg_color="transparent", height=8).pack() ctk.CTkLabel(cb_left, text="Hop Count (also: topbar +/−)", font=(FONT, 12, "bold"), text_color=TEXT).pack(anchor="w", padx=12) hop_val_lbl = ctk.CTkLabel(cb_left, text=str(_hop_count[0]), font=(FONT, 26, "bold"), text_color=CYAN) hop_val_lbl.pack(pady=(2, 0)) def _on_hop_slider(val: float) -> None: n = int(val) _hop_count[0] = n hop_val_lbl.configure(text=str(n)) hop_count_lbl.configure(text=str(n)) _apply_hop_count(n) hop_slider = ctk.CTkSlider( cb_left, from_=1, to=8, number_of_steps=7, command=_on_hop_slider, fg_color=PANEL, progress_color=ACCENT2, button_color=CYAN, button_hover_color=BLUE, ) hop_slider.set(s.chain_length) hop_slider.pack(fill="x", padx=16, pady=(2, 6)) hop_legend = ctk.CTkFrame(cb_left, fg_color="transparent") hop_legend.pack(fill="x", padx=16, pady=(0, 8)) ctk.CTkLabel(hop_legend, text="1", font=(FONT, 9), text_color=DIM).pack(side="left") ctk.CTkLabel(hop_legend, text="8", font=(FONT, 9), text_color=DIM).pack(side="right") elite_var = ctk.BooleanVar(value=s.prefer_elite) ctk.CTkCheckBox( cb_left, text="Elite proxies only (slower but more anonymous)", variable=elite_var, font=(FONT, 11), fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT, ).pack(anchor="w", padx=16, pady=(0, 8)) exit_fix_frame = ctk.CTkFrame(cb_left, fg_color=PANEL, corner_radius=8) exit_fix_frame.pack(fill="x", padx=12, pady=(0, 12)) ctk.CTkLabel( exit_fix_frame, text="Fixed exit — last hop only", font=(FONT, 11, "bold"), text_color=ORANGE, ).pack(anchor="w", padx=10, pady=(8, 2)) exit_proxy_entry = ctk.CTkEntry( exit_fix_frame, placeholder_text="http://host:port or socks5://host:port (optional)", font=("Consolas", 10), fg_color=BG, border_color=ACCENT, height=28, ) exit_proxy_entry.pack(fill="x", padx=10, pady=(0, 4)) exit_auth_row = ctk.CTkFrame(exit_fix_frame, fg_color="transparent") exit_auth_row.pack(fill="x", padx=10, pady=(0, 4)) ctk.CTkLabel(exit_auth_row, text="User", font=(FONT, 10), text_color=TEXT2, width=36).pack( side="left", padx=(0, 4) ) exit_user_entry = ctk.CTkEntry( exit_auth_row, width=120, height=26, font=("Consolas", 10), fg_color=BG, border_color=ACCENT, placeholder_text="optional", ) exit_user_entry.pack(side="left", padx=(0, 10)) ctk.CTkLabel(exit_auth_row, text="Pass", font=(FONT, 10), text_color=TEXT2, width=36).pack( side="left", padx=(0, 4) ) exit_pass_entry = ctk.CTkEntry( exit_auth_row, width=120, height=26, font=("Consolas", 10), fg_color=BG, border_color=ACCENT, placeholder_text="optional", show="*", ) exit_pass_entry.pack(side="left", padx=(0, 0)) _eb, _eu, _epw = split_proxy_for_edit(s.manual_exit_proxy) if _eb: exit_proxy_entry.insert(0, _eb) if _eu: exit_user_entry.insert(0, _eu) if _epw: exit_pass_entry.insert(0, _epw) ctk.CTkLabel( exit_fix_frame, text="Earlier hops still rotate randomly. Empty = fully automatic exit.\n" "Auth: use User/Pass (saved in settings) or paste user:pass@host in the URL field.\n" "Ignored while “Pin & use this chain” is enabled.", font=(FONT, 9), text_color=TEXT2, justify="left", ).pack(anchor="w", padx=10, pady=(0, 8)) # Right — manual chain editor cb_right = ctk.CTkFrame(cb_split, fg_color=CARD, corner_radius=8, width=400) cb_right.pack(side="left", fill="both", expand=True) hdr_row = ctk.CTkFrame(cb_right, fg_color="transparent") hdr_row.pack(fill="x", padx=12, pady=(12, 4)) ctk.CTkLabel(hdr_row, text="Manual Chain", font=(FONT, 12, "bold"), text_color=TEXT).pack(side="left") use_manual_var = ctk.BooleanVar(value=s.use_pinned_chain) ctk.CTkCheckBox( hdr_row, text="Pin & use this chain", variable=use_manual_var, font=(FONT, 10), fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT2, ).pack(side="right") manual_chain: list[str] = list(s.pinned_chain) chain_list_frame = ctk.CTkScrollableFrame( cb_right, fg_color=BG, height=170, corner_radius=6, scrollbar_button_color=ACCENT, ) chain_list_frame.pack(fill="x", padx=12, pady=(0, 6)) def _rebuild_chain_ui() -> None: for w in list(chain_list_frame.winfo_children()): w.destroy() for i, hop in enumerate(manual_chain): _chain_row(i, hop) def _chain_row(idx: int, hop: str) -> None: fr = ctk.CTkFrame(chain_list_frame, fg_color=PANEL, corner_radius=6, height=32) fr.pack(fill="x", pady=2) fr.pack_propagate(False) ctk.CTkLabel(fr, text=f" {idx+1}.", font=(FONT, 10, "bold"), text_color=CYAN, width=26).pack(side="left") proto = hop.split("://")[0].upper() if "://" in hop else "HTTP" badge_c = {"HTTP": "#1e3a8a", "SOCKS5": "#064e3b", "SOCKS4": "#3b1a64"}.get(proto, DIM) ctk.CTkLabel(fr, text=proto, font=(FONT, 9, "bold"), fg_color=badge_c, corner_radius=4, padx=4, text_color=TEXT).pack(side="left", padx=2) ctk.CTkLabel(fr, text=_short(hop), font=("Consolas", 10), text_color=TEXT, anchor="w").pack(side="left", fill="x", expand=True, padx=4) def _up(i: int = idx) -> None: if i > 0: manual_chain[i], manual_chain[i-1] = manual_chain[i-1], manual_chain[i] _rebuild_chain_ui() def _dn(i: int = idx) -> None: if i < len(manual_chain) - 1: manual_chain[i], manual_chain[i+1] = manual_chain[i+1], manual_chain[i] _rebuild_chain_ui() def _rm(i: int = idx) -> None: del manual_chain[i] _rebuild_chain_ui() _btn(fr, "↑", _up, w=26, h=24, fg_color=DIM, hover_color=ACCENT).pack(side="right", padx=1) _btn(fr, "↓", _dn, w=26, h=24, fg_color=DIM, hover_color=ACCENT).pack(side="right", padx=1) _btn(fr, "✕", _rm, w=26, h=24, fg_color="#7f1d1d", hover_color=RED).pack(side="right", padx=(1, 6)) _rebuild_chain_ui() # Add proxy input add_row = ctk.CTkFrame(cb_right, fg_color="transparent") add_row.pack(fill="x", padx=12, pady=(0, 4)) add_entry = ctk.CTkEntry( add_row, placeholder_text="http://host:port or user:pass@host:port", font=(FONT, 10), fg_color=BG, border_color=ACCENT, height=28, ) add_entry.pack(side="left", fill="x", expand=True, padx=(0, 4)) add_user_entry = ctk.CTkEntry( add_row, width=88, height=28, font=("Consolas", 10), fg_color=BG, border_color=ACCENT, placeholder_text="user", ) add_user_entry.pack(side="left", padx=(0, 2)) add_pass_entry = ctk.CTkEntry( add_row, width=88, height=28, font=("Consolas", 10), fg_color=BG, border_color=ACCENT, placeholder_text="pass", show="*", ) add_pass_entry.pack(side="left", padx=(0, 4)) def _add_proxy() -> None: v = add_entry.get().strip() if not v: return if "://" not in v: v = "http://" + v au = add_user_entry.get().strip() ap = add_pass_entry.get().strip() if au or ap: v = merge_proxy_credentials(v, au, ap) else: v = normalize_proxy_url(v) manual_chain.append(v) _rebuild_chain_ui() add_entry.delete(0, "end") add_user_entry.delete(0, "end") add_pass_entry.delete(0, "end") _btn(add_row, "+ Add", _add_proxy, w=68, h=28).pack(side="left") def _paste_current() -> None: for h in svc.current_chain: if h not in manual_chain: manual_chain.append(h) _rebuild_chain_ui() _log("Active chain pasted into manual editor.") _btn(cb_right, "↙ Paste active chain", _paste_current, w=180, h=26, fg_color=DIM, hover_color=ACCENT).pack(pady=(0, 10)) # Sources src_frame = ctk.CTkFrame(tab_chain, fg_color=CARD, corner_radius=8) src_frame.pack(fill="x", pady=(0, 6)) ctk.CTkLabel(src_frame, text="Proxy Sources (one JSON URL per line)", font=(FONT, 11, "bold"), text_color=TEXT).pack( anchor="w", padx=12, pady=(8, 4)) sources_box = ctk.CTkTextbox( src_frame, height=72, font=("Consolas", 10), fg_color=BG, text_color=TEXT, scrollbar_button_color=ACCENT, ) sources_box.pack(fill="x", padx=12, pady=(0, 8)) sources_box.insert("end", "\n".join(s.sources)) # ─── TAB: SETTINGS ─────────────────────────────────────────────────────── sf_outer = ctk.CTkScrollableFrame(tab_settings, fg_color=BG, scrollbar_button_color=ACCENT) sf_outer.pack(fill="both", expand=True) def _section(title: str) -> ctk.CTkFrame: ctk.CTkLabel(sf_outer, text=title, font=(FONT, 12, "bold"), text_color=CYAN).pack(anchor="w", padx=6, pady=(14, 4)) f = ctk.CTkFrame(sf_outer, fg_color=CARD, corner_radius=8) f.pack(fill="x", padx=6, pady=(0, 4)) return f entries: dict[str, ctk.CTkEntry] = {} def _fld(parent: Any, label: str, key: str, val: str, tip: str = "") -> None: row = ctk.CTkFrame(parent, fg_color="transparent") row.pack(fill="x", padx=12, pady=5) ctk.CTkLabel(row, text=label, font=(FONT, 11), text_color=TEXT, width=230, anchor="w").pack(side="left") e = ctk.CTkEntry(row, height=26, font=(FONT, 11), fg_color=BG, border_color=ACCENT, width=130) e.insert(0, val) e.pack(side="left") if tip: ctk.CTkLabel(row, text=f" {tip}", font=(FONT, 10), text_color=TEXT2).pack(side="left") entries[key] = e net = _section("Network") _fld(net, "Local listener port", "port", str(s.local_port), "(point apps here)") _fld(net, "Proxy bypass list", "bypass", s.proxy_bypass, "(semicolons)") _fld(net, "IP check URL", "check_url", s.ip_check_url) timing = _section("Timing") _fld(timing, "Health check interval (sec)", "health", str(s.health_check_seconds), "(how often exit IP is re-verified)") _fld(timing, "Full pool refresh (sec)", "refresh", str(s.full_refresh_seconds), "(re-fetch + re-validate)") val_sec = _section("Validation") _fld(val_sec, "Concurrent validators", "conc", str(s.validation_concurrency)) _fld(val_sec, "Max candidates per cycle", "maxc", str(s.max_candidates), "(random sample from fetch)") _fld(val_sec, "Per-proxy timeout (sec)", "timeout", str(s.validation_timeout_seconds)) sec_sec = _section("Security") ks_row = ctk.CTkFrame(sec_sec, fg_color="transparent") ks_row.pack(fill="x", padx=12, pady=8) ks_var = ctk.BooleanVar(value=s.kill_switch_enabled) ctk.CTkCheckBox( ks_row, text="Firewall kill-switch (block ALL traffic if chain is down — requires Admin)", variable=ks_var, font=(FONT, 11), fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT, ).pack(side="left") ctk.CTkFrame(sf_outer, fg_color="transparent", height=4).pack() _btn(sf_outer, "Save All Settings", lambda: _save_settings(verbose=True), w=220, h=36, font=(FONT, 13)).pack(anchor="w", padx=6, pady=8) # ───────────────────────────────────────────────────────────────────────── # SAVE SETTINGS # ───────────────────────────────────────────────────────────────────────── def _save_settings(verbose: bool = False) -> None: try: src_raw = sources_box.get("1.0", "end").strip() src_list = [ln.strip() for ln in src_raw.splitlines() if ln.strip().startswith("http")] n_hops = _hop_count[0] hop_slider.set(n_hops) hop_val_lbl.configure(text=str(n_hops)) hop_count_lbl.configure(text=str(n_hops)) ns = Settings( local_host=LISTEN_HOST, local_port=int(entries["port"].get().strip()), chain_length=n_hops, obfuscation_mode=mode_var.get(), use_pinned_chain=bool(use_manual_var.get()), pinned_chain=list(manual_chain), manual_exit_proxy=merge_proxy_credentials( exit_proxy_entry.get(), exit_user_entry.get(), exit_pass_entry.get(), ), health_check_seconds=min(3600, max(10, int(entries["health"].get().strip()))), full_refresh_seconds=min(86400, max(60, int(entries["refresh"].get().strip()))), validation_concurrency=max(1, int(entries["conc"].get().strip())), max_candidates=max(10, int(entries["maxc"].get().strip())), validation_timeout_seconds=min(120.0, max(2.0, float(entries["timeout"].get().strip()))), prefer_elite=bool(elite_var.get()), kill_switch_enabled=bool(ks_var.get()), proxy_bypass=entries["bypass"].get().strip() or Settings().proxy_bypass, sources=src_list or Settings().sources, ip_check_url=entries["check_url"].get().strip() or Settings().ip_check_url, ) if not (1 <= ns.local_port <= 65535): raise ValueError("Port must be 1-65535") svc.update_settings(ns) save_settings(ns) proxy_lbl.configure(text=f"{LISTEN_HOST}:{ns.local_port}") if verbose: _log("✓ Settings saved.") except Exception as e: _log(f"⚠ Settings error: {e!s}") # ───────────────────────────────────────────────────────────────────────── # STATUS REFRESHERS # ───────────────────────────────────────────────────────────────────────── def _refresh_sysproxy() -> None: on = is_system_proxy_set() sys_lbl.configure(text="SYS:ON" if on else "SYS:OFF", text_color=GREEN if on else DIM) def _refresh_fw(engaged: bool | None = None) -> None: if engaged is None: engaged = fw_is_engaged() fw_lbl.configure(text="FW:ON" if engaged else "FW:OFF", text_color=GREEN if engaged else DIM) # ───────────────────────────────────────────────────────────────────────── # MESSAGE PUMP # ───────────────────────────────────────────────────────────────────────── def _handle(m: dict[str, Any]) -> None: t = m.get("type") if t == "log": _log(str(m.get("text", ""))) elif t == "state": running = bool(m.get("running")) status_dot.configure(text_color=GREEN if running else RED) status_txt.configure(text="RUNNING" if running else "STOPPED") _refresh_sysproxy() if not running: prog_bar.set(0) phase_lbl.configure(text="idle") countdown_lbl.configure(text="next: —", text_color=TEXT2) tray.set_state("gray") v_exit_ip.set("—") v_rotation.set("0") elif t == "phase": ph = str(m.get("phase", "")) phase_labels = { "validate": "validate", "fetch": "fetch", "running": "running", "exit_check": "fixed exit", "verify_chain": "verify chain", "gost_start": "starting chain", } phase_lbl.configure(text=phase_labels.get(ph, ph or "idle")) if ph == "validate": prog_bar.configure(progress_color=YELLOW) elif ph == "fetch": prog_bar.configure(progress_color=ORANGE) prog_bar.set(0.15) elif ph == "exit_check": prog_bar.configure(progress_color=YELLOW) prog_bar.set(0.96) elif ph == "gost_start": prog_bar.configure(progress_color=GREEN) prog_bar.set(0.985) elif ph == "verify_chain": prog_bar.configure(progress_color=GREEN) prog_bar.set(0.98) elif ph == "running": prog_bar.configure(progress_color=GREEN) prog_bar.set(1.0) elif ph in ("idle", ""): prog_bar.set(0) elif t == "validate_progress": d, tot = int(m.get("done", 0)), max(1, int(m.get("total", 1))) prog_bar.set(d / tot) elif t == "pool": cnt = int(m.get("count", 0)) pool_lbl.configure(text=f"pool: {cnt}") v_pool.set(str(cnt)) elif t == "real_ip": ip = str(m.get("ip", "—")) your_ip_lbl.configure(text=f"Your IP: {ip}") v_real_ip.set(ip) elif t == "firewall": _refresh_fw(bool(m.get("engaged"))) elif t == "rotation": n = str(m.get("n", 0)) rotation_lbl.configure(text=f"rot: {n}") v_rotation.set(n) elif t == "countdown": secs = int(m.get("secs", 0)) if secs > 0: m_s, s_s = divmod(secs, 60) countdown_lbl.configure( text=f"next: {m_s}m{s_s:02d}s", text_color=YELLOW if secs < 30 else TEXT2, ) else: countdown_lbl.configure(text="next: checking…", text_color=YELLOW) elif t == "hops": hops = [str(x) for x in (m.get("hops") or [])] status = str(m.get("status", "connecting")) exit_ip = m.get("exit_ip") 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) _refresh_sysproxy() tray.set_state( {"healthy": "green", "dead": "red", "connecting": "yellow"}.get(status, "yellow") ) v_exit_ip.set(str(exit_ip) if exit_ip else "—") def _pump() -> None: try: while True: _handle(ui_q.get_nowait()) except queue.Empty: pass root.after(80, _pump) # ───────────────────────────────────────────────────────────────────────── # CLOSE / MINIMIZE # ───────────────────────────────────────────────────────────────────────── def _close() -> None: _cancel_pulse() tray.stop() svc.stop() clear_system_proxy() if is_admin() and fw_is_engaged(): fw_disengage() root.destroy() root.protocol("WM_DELETE_WINDOW", lambda: root.withdraw()) # ───────────────────────────────────────────────────────────────────────── # INIT # ───────────────────────────────────────────────────────────────────────── _refresh_boot() _refresh_sysproxy() _refresh_fw() _log("─" * 60) _log("Proxy God v2 — ready.") _log(f"Admin: {'YES — kill-switch available' if is_admin() else 'NO — run as Admin for kill-switch'}") _log(f"Log file: {LOG_PATH}") _log("Press START to fetch, validate and chain proxies.") _log("Use +/− in top bar to change hop count instantly.") _log("Chain Builder → Fixed exit: set the last hop (optional); rest still rotates.") _log("Verbose logs (Live tab): DEBUG lines from the engine + validator + fetcher (httpx kept quiet).") _log("─" * 60) _pump() root.mainloop() if __name__ == "__main__": main()