Files
proxy-god/proxy_chain_manager/app.py

859 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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=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_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()