v1.0.0: Clean rewrite — cross-platform, dark UI, stable WS tunnel

Made-with: Cursor:
This commit is contained in:
by-sonic
2026-04-08 14:56:50 +03:00
parent c40ad94e11
commit 09b7a03a0a
13 changed files with 594 additions and 2733 deletions
+249 -274
View File
@@ -1,356 +1,331 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod bypass;
mod network;
mod ws_proxy;
mod proxy;
use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use eframe::egui;
const PROXY_PORT: u16 = 1080;
// -- Colors (GitHub Dark inspired) ------------------------------------------
const BG: egui::Color32 = egui::Color32::from_rgb(13, 17, 23);
const SURFACE: egui::Color32 = egui::Color32::from_rgb(22, 27, 34);
const BORDER: egui::Color32 = egui::Color32::from_rgb(48, 54, 61);
const ACCENT: egui::Color32 = egui::Color32::from_rgb(88, 166, 255);
const GREEN: egui::Color32 = egui::Color32::from_rgb(63, 185, 80);
const RED: egui::Color32 = egui::Color32::from_rgb(248, 81, 73);
const TEXT: egui::Color32 = egui::Color32::from_rgb(230, 237, 243);
const TEXT2: egui::Color32 = egui::Color32::from_rgb(139, 148, 158);
const AD_BG: egui::Color32 = egui::Color32::from_rgb(17, 21, 28);
fn main() -> eframe::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([680.0, 560.0])
.with_min_inner_size([580.0, 460.0])
.with_title("TG Unblock"),
..Default::default()
};
eframe::run_native(
"TG Unblock",
options,
"TGLock",
eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([520.0, 620.0])
.with_min_inner_size([420.0, 500.0])
.with_title("TGLock"),
..Default::default()
},
Box::new(|cc| {
setup_fonts(&cc.egui_ctx);
apply_theme(&cc.egui_ctx);
Ok(Box::new(App::new()))
}),
)
}
fn setup_fonts(ctx: &egui::Context) {
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"system".to_owned(),
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
"C:\\Windows\\Fonts\\segoeui.ttf"
))),
);
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "system".to_owned());
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.insert(0, "system".to_owned());
ctx.set_fonts(fonts);
fn apply_theme(ctx: &egui::Context) {
let mut v = egui::Visuals::dark();
v.panel_fill = BG;
v.window_fill = SURFACE;
v.extreme_bg_color = BG;
v.faint_bg_color = SURFACE;
v.override_text_color = Some(TEXT);
v.widgets.noninteractive.bg_fill = SURFACE;
v.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, TEXT2);
v.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, BORDER);
v.widgets.inactive.bg_fill = egui::Color32::from_rgb(33, 38, 45);
v.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, TEXT);
v.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, BORDER);
v.widgets.hovered.bg_fill = egui::Color32::from_rgb(48, 54, 61);
v.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, TEXT);
v.widgets.active.bg_fill = ACCENT;
v.widgets.active.fg_stroke = egui::Stroke::new(1.0, BG);
ctx.set_visuals(v);
}
// -- Log --------------------------------------------------------------------
#[derive(Clone)]
struct LogEntry {
text: String,
is_error: bool,
struct LogLine {
ts: String,
msg: String,
err: bool,
}
fn now_ts() -> String {
let s = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
format!("{:02}:{:02}:{:02}", (s / 3600) % 24, (s / 60) % 60, s % 60)
}
fn log(log: &Arc<Mutex<Vec<LogLine>>>, msg: &str, err: bool) {
log.lock().unwrap().push(LogLine {
ts: now_ts(),
msg: msg.into(),
err,
});
}
// -- App --------------------------------------------------------------------
struct App {
log: Arc<Mutex<Vec<LogEntry>>>,
proxy_stats: Arc<ws_proxy::ProxyStats>,
is_admin: bool,
adapter_name: Arc<Mutex<Option<String>>>,
dns_set: Arc<Mutex<bool>>,
stats: Arc<proxy::Stats>,
log: Arc<Mutex<Vec<LogLine>>>,
started_at: Option<Instant>,
}
impl App {
fn new() -> Self {
let is_admin = bypass::check_admin();
let app = Self {
Self {
stats: proxy::Stats::new(),
log: Arc::new(Mutex::new(Vec::new())),
proxy_stats: ws_proxy::ProxyStats::new(),
is_admin,
adapter_name: Arc::new(Mutex::new(None)),
dns_set: Arc::new(Mutex::new(false)),
};
log_msg(&app.log, "Запущено", false);
if !is_admin {
log_msg(&app.log, "Нет прав администратора — DNS менять не получится", true);
started_at: None,
}
{
let adapter = app.adapter_name.clone();
let log = app.log.clone();
std::thread::spawn(move || {
if let Some(name) = network::detect_adapter() {
log_msg(&log, &format!("Адаптер: {}", name), false);
*adapter.lock().unwrap() = Some(name);
}
});
}
app
}
fn proxy_running(&self) -> bool {
self.proxy_stats.running.load(Ordering::SeqCst)
fn running(&self) -> bool {
self.stats.running.load(Ordering::SeqCst)
}
fn start_proxy(&self) {
if self.proxy_running() {
return;
}
let stats = self.proxy_stats.clone();
let log = self.log.clone();
let adapter = self.adapter_name.clone();
let dns_set = self.dns_set.clone();
let is_admin = self.is_admin;
fn start(&mut self) {
if self.running() { return; }
self.started_at = Some(Instant::now());
let stats = self.stats.clone();
let lg = self.log.clone();
log(&lg, "Запускаю прокси...", false);
std::thread::spawn(move || {
// DNS
if is_admin {
let aname = adapter.lock().unwrap().clone().or_else(network::detect_adapter);
if let Some(ref name) = aname {
if bypass::set_dns(name, "1.1.1.1", "1.0.0.1").is_ok() {
bypass::flush_dns();
log_msg(&log, "DNS → Cloudflare 1.1.1.1", false);
*dns_set.lock().unwrap() = true;
}
}
}
log_msg(&log, &format!("Запускаю WS-прокси на 127.0.0.1:{}...", PROXY_PORT), false);
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(ws_proxy::run_proxy(PROXY_PORT, stats));
if let Err(e) = result {
log_msg(&log, &format!("Прокси остановлен: {}", e), true);
let r = rt.block_on(proxy::run(stats));
if let Err(e) = r {
log(&lg, &format!("Ошибка: {}", e), true);
}
});
std::thread::sleep(std::time::Duration::from_millis(300));
if self.proxy_running() {
log_msg(&self.log, "Прокси запущен! Настройте Telegram.", false);
std::thread::sleep(std::time::Duration::from_millis(250));
if self.running() {
log(&self.log, &format!("SOCKS5 на 127.0.0.1:{}", proxy::PORT), false);
}
}
fn stop_proxy(&self) {
self.proxy_stats.running.store(false, Ordering::SeqCst);
log_msg(&self.log, "Прокси остановлен", false);
fn stop(&mut self) {
self.stats.running.store(false, Ordering::SeqCst);
self.started_at = None;
log(&self.log, "Остановлен", false);
}
if *self.dns_set.lock().unwrap() {
let adapter = self.adapter_name.clone();
let log = self.log.clone();
let dns_set = self.dns_set.clone();
std::thread::spawn(move || {
let aname = adapter.lock().unwrap().clone().or_else(network::detect_adapter);
if let Some(ref name) = aname {
let _ = bypass::reset_dns(name);
bypass::flush_dns();
*dns_set.lock().unwrap() = false;
log_msg(&log, "DNS сброшен", false);
}
});
fn uptime_str(&self) -> String {
match self.started_at {
Some(t) => {
let s = t.elapsed().as_secs();
format!("{:02}:{:02}:{:02}", s / 3600, (s / 60) % 60, s % 60)
}
None => "--:--:--".into(),
}
}
fn open_tg_proxy_link(&self) {
let url = format!("tg://socks?server=127.0.0.1&port={}", PROXY_PORT);
log_msg(&self.log, "Открываю настройку прокси в Telegram...", false);
let _ = open::that(&url);
}
}
fn log_msg(log: &Arc<Mutex<Vec<LogEntry>>>, text: &str, err: bool) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let ts = format!("{:02}:{:02}:{:02}", (now % 86400) / 3600, (now % 3600) / 60, now % 60);
log.lock().unwrap().push(LogEntry {
text: text.to_string(),
is_error: err,
ts,
});
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ctx.request_repaint_after(std::time::Duration::from_millis(400));
ctx.request_repaint_after(std::time::Duration::from_millis(300));
let running = self.proxy_running();
let active = self.proxy_stats.active_conn.load(Ordering::Relaxed);
let total = self.proxy_stats.total_conn.load(Ordering::Relaxed);
let ws = self.proxy_stats.ws_active.load(Ordering::Relaxed);
let on = self.running();
let active = self.stats.active.load(Ordering::Relaxed);
let total = self.stats.total.load(Ordering::Relaxed);
let ws = self.stats.ws.load(Ordering::Relaxed);
let dc = self.stats.last_dc.load(Ordering::Relaxed);
// --- Top bar ---
egui::TopBottomPanel::top("top").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.heading("TG Unblock");
ui.separator();
if running {
ui.colored_label(
egui::Color32::from_rgb(80, 220, 120),
egui::RichText::new("ПРОКСИ РАБОТАЕТ").strong(),
);
ui.separator();
ui.label(format!("Соединений: {} (WS: {}) | Всего: {}", active, ws, total));
} else {
ui.label("Прокси не запущен");
}
});
// === Ad bar (top) ===
egui::TopBottomPanel::top("ad").show(ctx, |ui| {
egui::Frame::new()
.fill(AD_BG)
.inner_margin(egui::Margin::symmetric(12, 6))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.colored_label(ACCENT, egui::RichText::new("by sonic VPN").size(12.0).strong());
ui.colored_label(TEXT2, egui::RichText::new("Обход для всех приложений").size(11.0));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.add(egui::Button::new(
egui::RichText::new("@bysonicvpn_bot").size(11.0).strong().color(ACCENT)
).frame(false)).clicked() {
let _ = open::that("https://t.me/bysonicvpn_bot");
}
});
});
});
});
// --- Log panel ---
// === Log (bottom) ===
egui::TopBottomPanel::bottom("log")
.min_height(130.0)
.min_height(120.0)
.show(ctx, |ui| {
ui.label(egui::RichText::new("Лог").strong());
ui.add_space(4.0);
ui.colored_label(TEXT2, egui::RichText::new("LOG").size(11.0));
ui.separator();
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.stick_to_bottom(true)
.show(ui, |ui| {
let logs = self.log.lock().unwrap();
for e in logs.iter() {
let color = if e.is_error {
egui::Color32::from_rgb(255, 100, 100)
} else {
egui::Color32::from_rgb(170, 215, 170)
};
ui.colored_label(color, format!("[{}] {}", e.ts, e.text));
for e in self.log.lock().unwrap().iter() {
let c = if e.err { RED } else { TEXT2 };
ui.colored_label(c, egui::RichText::new(
format!("{} {}", e.ts, e.msg)
).size(11.5).monospace());
}
});
});
// --- Main panel ---
egui::CentralPanel::default().show(ctx, |ui| {
ui.add_space(10.0);
// --- VPN ad (top) ---
ui.vertical_centered(|ui| {
egui::Frame::new()
.fill(egui::Color32::from_rgb(25, 30, 42))
.corner_radius(8.0)
.inner_margin(egui::Margin::symmetric(14, 8))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.colored_label(
egui::Color32::from_rgb(100, 180, 255),
egui::RichText::new("by sonic VPN").size(13.0).strong(),
);
ui.label(
egui::RichText::new("Полный обход для всех приложений")
.size(12.0)
.color(egui::Color32::from_rgb(160, 165, 180)),
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.add(
egui::Button::new(
egui::RichText::new("@bysonicvpn_bot")
.size(12.0)
.strong()
.color(egui::Color32::from_rgb(100, 200, 255)),
)
.frame(false),
).clicked() {
let _ = open::that("https://t.me/bysonicvpn_bot");
}
});
// === Stats bar ===
egui::TopBottomPanel::bottom("stats").show(ctx, |ui| {
egui::Frame::new()
.fill(SURFACE)
.inner_margin(egui::Margin::symmetric(16, 8))
.show(ui, |ui| {
ui.horizontal(|ui| {
stat(ui, "Соединения", &active.to_string());
ui.add_space(20.0);
stat(ui, "WS-туннели", &ws.to_string());
ui.add_space(20.0);
stat(ui, "DC", &if dc > 0 { dc.to_string() } else { "".into() });
ui.add_space(20.0);
stat(ui, "Всего", &total.to_string());
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
stat(ui, "Аптайм", &self.uptime_str());
});
});
});
ui.add_space(12.0);
});
});
// === Main ===
egui::CentralPanel::default().show(ctx, |ui| {
ui.vertical_centered(|ui| {
if !running {
ui.label(egui::RichText::new("Обход блокировки Telegram через WebSocket-прокси").size(15.0));
ui.add_space(5.0);
ui.label("Трафик идёт через web.telegram.org — провайдер видит обычный HTTPS");
ui.add_space(15.0);
ui.add_space(30.0);
// Title
ui.colored_label(TEXT, egui::RichText::new("TGLock").size(32.0).strong());
ui.add_space(4.0);
ui.colored_label(TEXT2, egui::RichText::new("WebSocket-туннель для Telegram").size(13.0));
ui.add_space(24.0);
// Status indicator
let (dot_color, status_text) = if on {
(GREEN, "Подключено")
} else {
(egui::Color32::from_rgb(80, 80, 80), "Отключено")
};
ui.horizontal(|ui| {
let center = ui.available_width() / 2.0 - 50.0;
ui.add_space(center);
let (r, _) = ui.allocate_exact_size(egui::vec2(10.0, 10.0), egui::Sense::hover());
ui.painter().circle_filled(r.center(), 5.0, dot_color);
ui.colored_label(
if on { GREEN } else { TEXT2 },
egui::RichText::new(status_text).size(14.0).strong(),
);
});
ui.add_space(20.0);
// Big button
if !on {
let btn = ui.add_sized(
[340.0, 55.0],
egui::Button::new(egui::RichText::new("Запустить обход").size(20.0).strong()),
[260.0, 52.0],
egui::Button::new(
egui::RichText::new("ПОДКЛЮЧИТЬ").size(18.0).strong().color(BG)
).fill(ACCENT).corner_radius(8.0),
);
if btn.clicked() {
self.start_proxy();
self.start();
}
} else {
ui.colored_label(
egui::Color32::from_rgb(80, 220, 120),
egui::RichText::new("Обход работает").size(22.0).strong(),
let btn = ui.add_sized(
[260.0, 52.0],
egui::Button::new(
egui::RichText::new("ОТКЛЮЧИТЬ").size(18.0).strong().color(TEXT)
).fill(egui::Color32::from_rgb(40, 45, 52)).corner_radius(8.0),
);
ui.add_space(5.0);
ui.label(format!("SOCKS5 прокси на 127.0.0.1:{}", PROXY_PORT));
ui.label(format!("WebSocket-туннелей: {} | Соединений: {}", ws, active));
ui.add_space(12.0);
// Stop button
let stop = ui.add_sized(
[340.0, 42.0],
egui::Button::new(egui::RichText::new("Остановить").size(17.0)),
);
if stop.clicked() {
self.stop_proxy();
if btn.clicked() {
self.stop();
}
}
ui.add_space(24.0);
// Setup section
egui::Frame::new()
.fill(SURFACE)
.corner_radius(8.0)
.inner_margin(16.0)
.show(ui, |ui| {
ui.set_width(360.0);
ui.colored_label(TEXT, egui::RichText::new("Настройка Telegram").size(14.0).strong());
ui.add_space(6.0);
if on {
if ui.add(egui::Button::new(
egui::RichText::new("Настроить автоматически").size(13.0).color(ACCENT)
).frame(false)).clicked() {
let _ = open::that(format!("tg://socks?server=127.0.0.1&port={}", proxy::PORT));
log(&self.log, "Открываю настройку Telegram...", false);
}
ui.add_space(4.0);
}
ui.colored_label(TEXT2, egui::RichText::new("Настройки → Продвинутые → Тип соединения → SOCKS5").size(11.5));
ui.add_space(4.0);
egui::Grid::new("cfg").num_columns(2).spacing([12.0, 3.0]).show(ui, |ui| {
ui.colored_label(TEXT2, "Сервер");
ui.monospace("127.0.0.1");
ui.end_row();
ui.colored_label(TEXT2, "Порт");
ui.monospace(format!("{}", proxy::PORT));
ui.end_row();
});
});
ui.add_space(16.0);
// How it works (compact)
ui.colored_label(TEXT2, egui::RichText::new(
"Трафик Telegram → SOCKS5 → WSS → web.telegram.org → DC"
).size(11.0));
ui.colored_label(TEXT2, egui::RichText::new(
"Провайдер видит обычный HTTPS. Остальной трафик не затрагивается."
).size(11.0));
});
ui.add_space(20.0);
ui.separator();
ui.add_space(8.0);
// --- Telegram setup ---
ui.heading("Настройка Telegram Desktop");
ui.add_space(6.0);
if running {
ui.horizontal(|ui| {
if ui.button(" Настроить автоматически ").clicked() {
self.open_tg_proxy_link();
}
ui.label("(откроет Telegram, нажмите \"Подключить\")");
});
ui.add_space(8.0);
}
ui.label("Или вручную: Настройки → Продвинутые → Тип соединения → SOCKS5");
ui.add_space(4.0);
egui::Grid::new("manual_setup")
.num_columns(2)
.spacing([15.0, 4.0])
.show(ui, |ui| {
ui.label("Сервер:");
ui.monospace("127.0.0.1");
ui.end_row();
ui.label("Порт:");
ui.monospace(format!("{}", PROXY_PORT));
ui.end_row();
ui.label("Логин/Пароль:");
ui.label("оставить пустыми");
ui.end_row();
});
ui.add_space(15.0);
ui.separator();
ui.add_space(5.0);
// --- How it works ---
ui.heading("Как это работает");
ui.add_space(4.0);
ui.label("1. Локальный SOCKS5-прокси принимает соединения от Telegram");
ui.label("2. Трафик к серверам Telegram заворачивается в WebSocket (WSS)");
ui.label("3. Подключение идёт через web.telegram.org — обычный HTTPS");
ui.label("4. Провайдер/DPI не видит MTProto, не может замедлить");
ui.add_space(4.0);
ui.colored_label(
egui::Color32::from_rgb(170, 170, 170),
"Не-Telegram трафик проходит напрямую без изменений",
);
});
}
}
fn stat(ui: &mut egui::Ui, label: &str, value: &str) {
ui.vertical(|ui| {
ui.colored_label(TEXT2, egui::RichText::new(label).size(10.0));
ui.colored_label(TEXT, egui::RichText::new(value).size(13.0).strong().monospace());
});
}