856 lines
36 KiB
Python
856 lines
36 KiB
Python
"""
|
||
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:
|
||
# Merge styling; pass font / corner_radius explicitly so nothing duplicates
|
||
# inside CTkButton (font must not appear in **kwargs passed to the base class).
|
||
merged: dict[str, Any] = {
|
||
"fg_color": ACCENT,
|
||
"hover_color": ACCENT2,
|
||
"text_color": TEXT,
|
||
**kw,
|
||
}
|
||
font = merged.pop("font", (FONT, 11))
|
||
corner_radius = merged.pop("corner_radius", 6)
|
||
return ctk.CTkButton(
|
||
parent,
|
||
text=label,
|
||
command=cmd,
|
||
width=w,
|
||
height=h,
|
||
corner_radius=corner_radius,
|
||
font=font,
|
||
**merged,
|
||
)
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 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()
|