""" 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, normalize_proxy_url, save_settings, ) 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), ], ) # ── 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 = 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", 11), fg_color=BG, text_color=TEXT, scrollbar_button_color=ACCENT, ) log_box.pack(fill="both", expand=True, padx=4, pady=4) def _log(line: str) -> None: log_box.insert("end", f"[{_ts()}] {line}\n") log_box.see("end") # ─── 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)) if s.manual_exit_proxy: exit_proxy_entry.insert(0, s.manual_exit_proxy) ctk.CTkLabel( exit_fix_frame, text="Earlier hops still rotate randomly. Empty = fully automatic exit.\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://ip:port or socks5://ip:port", font=(FONT, 10), fg_color=BG, border_color=ACCENT, height=28, ) add_entry.pack(side="left", fill="x", expand=True, padx=(0, 4)) def _add_proxy() -> None: v = add_entry.get().strip() if not v: return if "://" not in v: v = "http://" + v manual_chain.append(v) _rebuild_chain_ui() add_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=normalize_proxy_url(exit_proxy_entry.get()), health_check_seconds=max(10, int(entries["health"].get().strip())), full_refresh_seconds=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=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_lbl.configure(text=ph) 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 == "running": prog_bar.configure(progress_color=GREEN) prog_bar.set(1.0) else: 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("─" * 60) _pump() root.mainloop() if __name__ == "__main__": main()