v2: tabs, chain builder, obfuscation modes, hop slider, Defender exclusion, firewall kept

Made-with: Cursor
This commit is contained in:
Dr Jones
2026-04-14 17:13:02 -07:00
commit 4bb626498f
45 changed files with 34053 additions and 0 deletions

664
proxy_chain_manager/app.py Normal file
View File

@@ -0,0 +1,664 @@
"""
Proxy Chain Manager — GUI
Tabs: Live | Chain Builder | Settings
"""
from __future__ import annotations
import logging
import queue
import sys
from typing import Any
import customtkinter as ctk
from .config import (
LISTEN_HOST,
OBFUSCATION_LABELS,
OBFUSCATION_MODES,
Settings,
load_settings,
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"
PURPLE = "#bb86fc"
BLUE = "#448aff"
CYAN = "#00bcd4"
DIM = "#4a5568"
TEXT = "#e2e8f0"
TEXT2 = "#94a3b8"
FONT = "Segoe UI"
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("1060x720")
root.minsize(900, 600)
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 ─────────────────────────────────────────────────────────────────
def _tray_show() -> None:
root.after(0, lambda: (root.deiconify(), root.lift()))
def _tray_quit() -> None:
root.after(0, _close)
tray = TrayIcon(on_show=_tray_show, on_quit=_tray_quit, on_rotate=svc.rotate_now)
tray.start()
# ─────────────────────────────────────────────────────────────────────────
# HELPERS
# ─────────────────────────────────────────────────────────────────────────
def _btn(parent: Any, label: str, cmd: Any, w: int = 80, h: int = 28, **kw: Any) -> ctk.CTkButton:
cfg = {"fg_color": ACCENT, "hover_color": ACCENT2, "text_color": TEXT}
cfg.update(kw)
return ctk.CTkButton(
parent, text=label, command=cmd, width=w, height=h,
font=(FONT, 11), corner_radius=6, **cfg,
)
pulse_job: dict[str, Any] = {"id": None}
def _cancel_pulse() -> None:
jid = pulse_job.get("id")
if jid:
try:
root.after_cancel(jid)
except Exception:
pass
pulse_job["id"] = None
def _short(u: str) -> str:
u = u.replace("http://", "").replace("socks5://", "s5://").replace("socks4://", "s4://")
return u[:28] + "" if len(u) > 30 else u
# ─────────────────────────────────────────────────────────────────────────
# TOP BAR
# ─────────────────────────────────────────────────────────────────────────
topbar = ctk.CTkFrame(root, fg_color=CARD, corner_radius=0, height=46)
topbar.pack(fill="x", side="top")
topbar.pack_propagate(False)
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, 10))
def _start() -> None:
_save_settings()
svc.start()
_btn(topbar, "▶ Start", _start, w=90).pack(side="left", padx=2)
_btn(topbar, "■ Stop", svc.stop, w=80).pack(side="left", padx=2)
_btn(topbar, "↻ Rotate", svc.rotate_now, w=80).pack(side="left", padx=2)
_btn(topbar, "Quit", lambda: root.after(0, _close), w=55,
fg_color="#7f1d1d", hover_color="#991b1b").pack(side="left", padx=(8, 2))
ctk.CTkLabel(topbar, text="", text_color=DIM).pack(side="left", padx=6)
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 fail: {msg}")
_refresh_boot()
def _rm_boot() -> None:
ok, msg = uninstall_logon_task()
_log("Boot task removed." if ok else f"Boot remove fail: {msg}")
_refresh_boot()
_btn(topbar, "Boot+", _inst_boot, w=55).pack(side="left", padx=2)
_btn(topbar, "Boot-", _rm_boot, w=55).pack(side="left", padx=2)
# Right side status indicators
proxy_lbl = ctk.CTkLabel(topbar, text=f"proxy: {LISTEN_HOST}:{s.local_port}",
font=(FONT, 11, "bold"), text_color=BLUE)
proxy_lbl.pack(side="right", padx=(4, 12))
fw_lbl = ctk.CTkLabel(topbar, text="FW:—", font=(FONT, 10), text_color=DIM)
fw_lbl.pack(side="right", padx=4)
sys_lbl = ctk.CTkLabel(topbar, text="SYS:—", font=(FONT, 10), text_color=DIM)
sys_lbl.pack(side="right", padx=4)
admin_lbl = ctk.CTkLabel(topbar,
text="⚡ADMIN" if is_admin() else "👤USER",
font=(FONT, 10, "bold"),
text_color=GREEN if is_admin() else YELLOW)
admin_lbl.pack(side="right", padx=4)
if not is_admin():
def _elevate() -> None:
if request_admin_relaunch():
_close()
_btn(topbar, "Run as Admin", _elevate, w=100,
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=54)
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, 6), pady=4)
chain_canvas = ctk.CTkFrame(viz_bar, fg_color="transparent")
chain_canvas.pack(side="left", fill="both", expand=True, padx=4)
exit_ip_lbl = ctk.CTkLabel(viz_bar, text="Exit: —",
font=(FONT, 13, "bold"), text_color=YELLOW)
exit_ip_lbl.pack(side="right", padx=(6, 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: _pulse(step + 1))
def _render_chain(hops: list[str], status: str, exit_ip: str | None) -> None:
_cancel_pulse()
for w in list(chain_canvas.winfo_children()):
w.destroy()
hop_dot_labels.clear()
color_map = {"healthy": GREEN, "dead": RED, "connecting": YELLOW}
hop_c = color_map.get(status, DIM)
def _node(text: str, color: str, bold: bool = False) -> None:
f = (FONT, 11, "bold") if bold else (FONT, 10)
ctk.CTkLabel(chain_canvas, text=text, font=f, 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()
for i, h in enumerate(hops):
dot = ctk.CTkLabel(chain_canvas, text="", font=(FONT, 12), text_color=hop_c)
dot.pack(side="left", padx=1)
hop_dot_labels.append(dot)
ctk.CTkLabel(chain_canvas, text=_short(h), font=(FONT, 10), text_color=hop_c).pack(side="left", padx=1)
if i < len(hops) - 1:
_arr()
_arr()
_node("WEB", BLUE, bold=True)
if exit_ip:
exit_ip_lbl.configure(
text=f"Exit: {exit_ip}",
text_color=GREEN if status == "healthy" else RED,
)
else:
exit_ip_lbl.configure(text="Exit: —",
text_color=YELLOW if status == "connecting" else DIM)
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))
phase_lbl = ctk.CTkLabel(prog_strip, text="idle", font=(FONT, 10), text_color=TEXT2)
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)
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 ")
# ─── TAB: LIVE ───────────────────────────────────────────────────────────
tab_live.configure(fg_color=BG)
# Stats 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) -> 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, 15, "bold"), text_color=TEXT).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_hops = ctk.StringVar(value="")
_stat_card(stats_row, "Your IP (Nord/ISP)", v_real_ip)
_stat_card(stats_row, "Exit IP (last proxy)", v_exit_ip)
_stat_card(stats_row, "Valid proxy pool", v_pool)
_stat_card(stats_row, "Active hops", v_hops)
log_frame = ctk.CTkFrame(tab_live, fg_color=CARD, corner_radius=8)
log_frame.pack(fill="both", expand=True, padx=0)
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", line + "\n")
log_box.see("end")
# ─── TAB: CHAIN BUILDER ──────────────────────────────────────────────────
tab_chain.configure(fg_color=BG)
cb_top = ctk.CTkFrame(tab_chain, fg_color="transparent")
cb_top.pack(fill="x", pady=(4, 8))
# Left col — mode + hops
cb_left = ctk.CTkFrame(cb_top, 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=10, pady=(10, 4))
mode_var = ctk.StringVar(value=s.obfuscation_mode)
for mode_key in OBFUSCATION_MODES:
ctk.CTkRadioButton(
cb_left,
text=OBFUSCATION_LABELS[mode_key],
variable=mode_var,
value=mode_key,
font=(FONT, 11),
fg_color=ACCENT2,
hover_color=ACCENT,
text_color=TEXT,
).pack(anchor="w", padx=14, pady=2)
ctk.CTkLabel(cb_left, text="", height=6).pack()
ctk.CTkLabel(cb_left, text="Hop Count", font=(FONT, 12, "bold"), text_color=TEXT).pack(
anchor="w", padx=10)
hop_val_lbl = ctk.CTkLabel(cb_left, text=str(s.chain_length),
font=(FONT, 22, "bold"), text_color=CYAN)
hop_val_lbl.pack(pady=(2, 0))
def _on_hop_slider(val: float) -> None:
hop_val_lbl.configure(text=str(int(val)))
hop_slider = ctk.CTkSlider(cb_left, from_=2, to=8, number_of_steps=6,
command=_on_hop_slider,
fg_color=CARD, progress_color=ACCENT2,
button_color=CYAN, button_hover_color=BLUE)
hop_slider.set(s.chain_length)
hop_slider.pack(fill="x", padx=14, pady=(2, 12))
elite_var2 = ctk.BooleanVar(value=s.prefer_elite)
ctk.CTkCheckBox(cb_left, text="Elite proxies only",
variable=elite_var2, font=(FONT, 11),
fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT).pack(
anchor="w", padx=14, pady=(0, 10))
# Right col — manual chain editor
cb_right = ctk.CTkFrame(cb_top, fg_color=CARD, corner_radius=8, width=380)
cb_right.pack(side="left", fill="both", expand=True)
hdr_row = ctk.CTkFrame(cb_right, fg_color="transparent")
hdr_row.pack(fill="x", padx=10, pady=(10, 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="Use this chain",
variable=use_manual_var,
font=(FONT, 10), fg_color=ACCENT2,
hover_color=ACCENT, text_color=TEXT2).pack(side="right")
# Listbox-style chain using a scrollable frame with rows
chain_list_frame = ctk.CTkScrollableFrame(cb_right, fg_color=BG,
height=160, corner_radius=6,
scrollbar_button_color=ACCENT)
chain_list_frame.pack(fill="x", padx=10, pady=(0, 6))
manual_chain: list[str] = list(s.pinned_chain)
row_frames: list[ctk.CTkFrame] = []
def _rebuild_chain_ui() -> None:
for w in list(chain_list_frame.winfo_children()):
w.destroy()
row_frames.clear()
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=30)
fr.pack(fill="x", pady=2)
fr.pack_propagate(False)
row_frames.append(fr)
# Drag indicator + index
ctk.CTkLabel(fr, text=f" {idx+1}.", font=(FONT, 10, "bold"),
text_color=CYAN, width=24).pack(side="left")
# Protocol badge
proto = hop.split("://")[0].upper() if "://" in hop else "?"
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=24, h=22, fg_color=DIM, hover_color=ACCENT).pack(side="right", padx=1)
_btn(fr, "", _dn, w=24, h=22, fg_color=DIM, hover_color=ACCENT).pack(side="right", padx=1)
_btn(fr, "", _rm, w=24, h=22, fg_color="#7f1d1d", hover_color=RED).pack(side="right", padx=(1, 4))
_rebuild_chain_ui()
# Add proxy row
add_row = ctk.CTkFrame(cb_right, fg_color="transparent")
add_row.pack(fill="x", padx=10, pady=(0, 4))
add_entry = ctk.CTkEntry(add_row, placeholder_text="http://1.2.3.4:8080 or socks5://...",
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 v and ("://" in v or ":" in v):
if "://" not in v:
v = "http://" + v
manual_chain.append(v)
_rebuild_chain_ui()
add_entry.delete(0, "end")
else:
_log("Invalid proxy format — use http://ip:port or socks5://ip:port")
_btn(add_row, "+ Add", _add_proxy, w=70, h=28).pack(side="left")
def _paste_from_current() -> None:
for h in svc.current_chain:
if h not in manual_chain:
manual_chain.append(h)
_rebuild_chain_ui()
_log("Pasted active chain into manual chain editor.")
_btn(cb_right, "↙ Paste current chain", _paste_from_current, w=200, h=26,
fg_color=DIM, hover_color=ACCENT).pack(pady=(0, 10))
# Sources section
src_frame = ctk.CTkFrame(tab_chain, fg_color=CARD, corner_radius=8)
src_frame.pack(fill="x", pady=(0, 6))
src_hdr = ctk.CTkFrame(src_frame, fg_color="transparent")
src_hdr.pack(fill="x", padx=10, pady=(8, 4))
ctk.CTkLabel(src_hdr, text="Proxy Sources (one URL per line)",
font=(FONT, 12, "bold"), text_color=TEXT).pack(side="left")
sources_box = ctk.CTkTextbox(src_frame, height=80, font=("Consolas", 10),
fg_color=BG, text_color=TEXT,
scrollbar_button_color=ACCENT)
sources_box.pack(fill="x", padx=10, pady=(0, 8))
sources_box.insert("end", "\n".join(s.sources))
# ─── TAB: SETTINGS ───────────────────────────────────────────────────────
tab_settings.configure(fg_color=BG)
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=10, pady=4)
ctk.CTkLabel(row, text=label, font=(FONT, 11), text_color=TEXT, width=220,
anchor="w").pack(side="left")
e = ctk.CTkEntry(row, height=26, font=(FONT, 11), fg_color=BG,
border_color=ACCENT, width=120)
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 port", "port", str(s.local_port), "(apps point here)")
_fld(net, "Proxy bypass list", "bypass", s.proxy_bypass, "(semicolon separated)")
_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), "(0 = manual only)")
_fld(timing, "Full list refresh (sec)", "refresh", str(s.full_refresh_seconds))
validation = _section("Validation")
_fld(validation, "Concurrent proxy tests", "conc", str(s.validation_concurrency))
_fld(validation, "Max candidates per cycle", "maxc", str(s.max_candidates))
_fld(validation, "Per-proxy timeout (sec)", "timeout", str(s.validation_timeout_seconds))
security = _section("Security")
ks_row = ctk.CTkFrame(security, fg_color="transparent")
ks_row.pack(fill="x", padx=10, pady=6)
ks_var = ctk.BooleanVar(value=s.kill_switch_enabled)
ctk.CTkCheckBox(ks_row, text="Firewall kill-switch (block all traffic if proxy chain dies)",
variable=ks_var, font=(FONT, 11),
fg_color=ACCENT2, hover_color=ACCENT, text_color=TEXT).pack(side="left")
save_btn_frame = ctk.CTkFrame(sf_outer, fg_color="transparent")
save_btn_frame.pack(fill="x", padx=6, pady=8)
_btn(save_btn_frame, "💾 Save All Settings", lambda: _save_settings(verbose=True),
w=220, h=36, font=(FONT, 13)).pack(side="left")
# ─────────────────────────────────────────────────────────────────────────
# 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")]
ns = Settings(
local_host=LISTEN_HOST,
local_port=int(entries["port"].get().strip()),
chain_length=int(hop_slider.get()),
obfuscation_mode=mode_var.get(),
use_pinned_chain=bool(use_manual_var.get()),
pinned_chain=list(manual_chain),
health_check_seconds=int(entries["health"].get().strip()),
full_refresh_seconds=int(entries["refresh"].get().strip()),
validation_concurrency=int(entries["conc"].get().strip()),
max_candidates=int(entries["maxc"].get().strip()),
validation_timeout_seconds=float(entries["timeout"].get().strip()),
prefer_elite=bool(elite_var2.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"proxy: {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")
tray.set_state("gray")
v_exit_ip.set("")
v_hops.set("")
elif t == "phase":
phase_lbl.configure(text=str(m.get("phase", "")))
if m.get("phase") != "validate":
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(m.get("engaged"))
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")
_render_chain(hops, status, exit_ip)
_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 "")
v_hops.set(str(len(hops)))
def _pump() -> None:
try:
while True:
_handle(ui_q.get_nowait())
except queue.Empty:
pass
root.after(100, _pump)
# ─────────────────────────────────────────────────────────────────────────
# CLOSE / MINIMIZE
# ─────────────────────────────────────────────────────────────────────────
def _close() -> None:
_cancel_pulse()
tray.stop()
svc.stop()
clear_system_proxy()
if is_admin():
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' if is_admin() else 'NO (Run as Admin for kill-switch)'}")
_log(f"Log: {LOG_PATH}")
_log("Press START to fetch, validate and chain proxies.")
_log("" * 60)
_pump()
root.mainloop()
if __name__ == "__main__":
main()