From b8987b666ab3b6fd39d1e668fbd1c5a781472836 Mon Sep 17 00:00:00 2001 From: mistermelphin Date: Tue, 24 Mar 2026 11:10:58 +0400 Subject: [PATCH] Split into lib + CLI/GUI, add cross-platform support Refactor project structure: extract library crate, separate CLI and GUI binaries. GUI is now an optional feature (Windows-only). CLI works on Linux, macOS, and Windows with clap for argument parsing. Add cross-platform DNS management (resolvectl / resolv.conf) and root check for non-Windows systems. Update README with CLI usage docs. --- Cargo.toml | 31 ++++++--- README.md | 100 +++++++++++++++++++++-------- src/bin/cli.rs | 86 +++++++++++++++++++++++++ src/{main.rs => bin/gui.rs} | 9 +-- src/bypass.rs | 122 +++++++++++++++++++++++++++++++++--- src/lib.rs | 3 + src/network.rs | 75 +++++++++++++++++++--- src/ws_proxy.rs | 21 ++++++- 8 files changed, 389 insertions(+), 58 deletions(-) create mode 100644 src/bin/cli.rs rename src/{main.rs => bin/gui.rs} (98%) create mode 100644 src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index aa28091..5d457f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,24 +3,37 @@ name = "tg_unblock" version = "0.3.1" edition = "2021" +[lib] +name = "tg_unblock" +path = "src/lib.rs" + +[[bin]] +name = "tg_unblock" +path = "src/bin/cli.rs" + +[[bin]] +name = "tg_unblock_gui" +path = "src/bin/gui.rs" +required-features = ["gui"] + +[features] +gui = ["dep:eframe", "dep:egui", "dep:open"] + [dependencies] -eframe = "0.31" -egui = "0.31" tokio = { version = "1", features = ["full"] } reqwest = { version = "0.12", features = ["blocking"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -open = "5" tokio-tungstenite = { version = "0.24", features = ["native-tls"] } native-tls = "0.2" futures-util = "0.3" aes = "0.8" ctr = "0.9" cipher = "0.4" +clap = { version = "4", features = ["derive"] } + +# GUI (optional) +eframe = { version = "0.31", optional = true } +egui = { version = "0.31", optional = true } +open = { version = "5", optional = true } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winuser"] } - -[[bin]] -name = "tg_unblock" -path = "src/main.rs" diff --git a/README.md b/README.md index 8cefc24..07266a0 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@

TG Unblock

Обход блокировки Telegram через WebSocket-туннель
- Без VPN. Без серверов. Без абонентки. Один клик. + Без VPN. Без серверов. Без абонентки. CLI + GUI.

Release License Stars Rust - Windows + Platform

@@ -17,7 +17,11 @@ ## Что это? -**TG Unblock** — десктопное приложение на Rust, которое обходит блокировку Telegram через локальный WebSocket-прокси. Провайдер видит обычный HTTPS к `web.telegram.org`, а не MTProto — DPI не может обнаружить и заблокировать трафик. +**TG Unblock** — кроссплатформенное приложение на Rust, которое обходит блокировку Telegram через локальный WebSocket-прокси. Провайдер видит обычный HTTPS к `web.telegram.org`, а не MTProto — DPI не может обнаружить и заблокировать трафик. + +Доступно в двух вариантах: +- **CLI** — кроссплатформенный (Linux, macOS, Windows), работает в терминале +- **GUI** — графический интерфейс для Windows ### Почему не GoodbyeDPI / Zapret? @@ -28,7 +32,7 @@ | IP-шейпинг обходит? | Нет | Нет | **Да** | | Скорость | Зависит от DPI | Зависит от DPI | **Полная** | | Переподключения | Возможны | Возможны | **Нет** | -| Настройка | Много параметров | Стратегии | **Один клик** | +| Настройка | Много параметров | Стратегии | **Один клик / одна команда** | ## Скачать @@ -39,28 +43,62 @@ ```bash git clone https://github.com/by-sonic/tglock.git cd tglock -cargo build --release + +# CLI (Linux / macOS / Windows): +cargo build --release --bin tg_unblock + +# GUI (Windows): +cargo build --release --bin tg_unblock_gui --features gui ``` -Готовый `.exe` будет в `target/release/tg_unblock.exe`. +Готовый бинарник будет в `target/release/`. ## Как пользоваться -1. Запустите `tg_unblock.exe` +### CLI (Linux / macOS / Windows) + +```bash +# Запустить прокси на порту по умолчанию (1080): +tg_unblock + +# Указать порт: +tg_unblock --port 9050 + +# Со сменой DNS на Cloudflare (нужен root/admin): +sudo tg_unblock --dns +``` + +Остановка — `Ctrl+C` (DNS автоматически сбросится). + +``` +$ tg_unblock --help +Обход блокировки Telegram через WebSocket-туннель + +Usage: tg_unblock [OPTIONS] + +Options: + -p, --port Порт SOCKS5-прокси [default: 1080] + -b, --bind Адрес привязки [default: 127.0.0.1] + --dns Сменить DNS на Cloudflare 1.1.1.1 (нужен root/admin) + -h, --help Print help + -V, --version Print version +``` + +### GUI (Windows) + +1. Запустите `tg_unblock_gui.exe` 2. Нажмите **"Запустить обход"** 3. Нажмите **"Настроить автоматически"** — откроется Telegram, нажмите "Подключить" 4. Готово. Telegram работает на полной скорости. -### Ручная настройка прокси - -Если автонастройка не сработала: +### Настройка прокси в Telegram **Telegram Desktop** → Настройки → Продвинутые → Тип соединения → **Использовать SOCKS5-прокси** | Параметр | Значение | |---|---| | Сервер | `127.0.0.1` | -| Порт | `1080` | +| Порт | `1080` (или тот, что указали в `--port`) | | Логин | *пусто* | | Пароль | *пусто* | @@ -89,39 +127,46 @@ Telegram Desktop | DC | Подсеть | WebSocket | |---|---|---| -| DC1 | `149.154.160.0/22` | `wss://pluto.web.telegram.org/apiws` | -| DC2 | `149.154.164.0/22` | `wss://venus.web.telegram.org/apiws` | -| DC3 | `149.154.168.0/22` | `wss://aurora.web.telegram.org/apiws` | -| DC4 | `91.108.12.0/22` | `wss://vesta.web.telegram.org/apiws` | -| DC5 | `91.108.56.0/22` | `wss://flora.web.telegram.org/apiws` | - -Имена DC (`pluto`, `venus`, `aurora`, `vesta`, `flora`) — из [официальной документации MTProto](https://core.telegram.org/mtproto/transports). +| DC1 | `149.154.160.0/22` | `wss://kws1.web.telegram.org/apiws` | +| DC2 | `149.154.164.0/22` | `wss://kws2.web.telegram.org/apiws` | +| DC3 | `149.154.168.0/22` | `wss://kws3.web.telegram.org/apiws` | +| DC4 | `91.108.12.0/22` | `wss://kws4.web.telegram.org/apiws` | +| DC5 | `91.108.56.0/22` | `wss://kws5.web.telegram.org/apiws` | ## Стек | Что | Зачем | |---|---| | **Rust** | Скорость, безопасность, один бинарник без зависимостей | -| **egui / eframe** | Нативный GUI без Electron, без браузера | | **tokio** | Async I/O для высокопроизводительного проксирования | | **tokio-tungstenite** | WebSocket-клиент с TLS | -| **native-tls** | TLS через системные сертификаты Windows | +| **native-tls** | TLS через системные сертификаты | +| **clap** | Парсинг аргументов CLI | +| **egui / eframe** | Нативный GUI (опционально, Windows) | ## Структура проекта ``` tglock/ -├── Cargo.toml # Зависимости +├── Cargo.toml # Зависимости и таргеты ├── src/ -│ ├── main.rs # GUI + управление прокси -│ ├── ws_proxy.rs # SOCKS5-сервер + WebSocket-туннель -│ ├── bypass.rs # DNS-настройка, утилиты Windows -│ └── network.rs # Сетевая диагностика -└── tg_blacklist.txt # IP-подсети и домены Telegram +│ ├── lib.rs # Библиотечный крейт +│ ├── ws_proxy.rs # SOCKS5-сервер + WebSocket-туннель +│ ├── bypass.rs # DNS-настройка (Linux + Windows) +│ ├── network.rs # Сетевая диагностика (Linux + Windows) +│ └── bin/ +│ ├── cli.rs # CLI-интерфейс (кроссплатформенный) +│ └── gui.rs # GUI-интерфейс (Windows) +└── tg_blacklist.txt # IP-подсети и домены Telegram ``` ## Требования +### CLI (Linux / macOS) +- [Rust 1.70+](https://rustup.rs/) (для сборки из исходников) +- Права root (для смены DNS с флагом `--dns`, опционально) + +### GUI (Windows) - Windows 10/11 - [Rust 1.70+](https://rustup.rs/) (для сборки из исходников) - Права администратора (для смены DNS, опционально) @@ -140,6 +185,9 @@ A: Пока только Telegram Desktop. Для мобильных устро **Q: Замедляется ли интернет?** A: Нет. Проксируется только трафик к серверам Telegram. Весь остальной трафик идёт напрямую. +**Q: Работает ли на Linux?** +A: Да. CLI-версия полностью кроссплатформенная. На Linux для смены DNS используется `resolvectl` (systemd-resolved) или прямая запись в `/etc/resolv.conf`. + ## VPN для полного обхода Если нужен обход блокировок для **всех** приложений (YouTube, Discord, Instagram и др.) — попробуйте **[by sonic VPN](https://t.me/bysonicvpn_bot)**. Быстрый, без ограничений скорости. diff --git a/src/bin/cli.rs b/src/bin/cli.rs new file mode 100644 index 0000000..8ee31b7 --- /dev/null +++ b/src/bin/cli.rs @@ -0,0 +1,86 @@ +use std::sync::atomic::Ordering; + +use clap::Parser; +use tg_unblock::{bypass, network, ws_proxy}; + +#[derive(Parser)] +#[command(name = "tg_unblock", version, about = "Обход блокировки Telegram через WebSocket-туннель")] +struct Args { + /// Порт SOCKS5-прокси + #[arg(short, long, default_value_t = 1080)] + port: u16, + + /// Адрес привязки + #[arg(short, long, default_value = "127.0.0.1")] + bind: String, + + /// Сменить DNS на Cloudflare 1.1.1.1 (нужен root/admin) + #[arg(long)] + dns: bool, +} + +fn main() { + let args = Args::parse(); + + let is_admin = bypass::check_admin(); + let mut dns_was_set = false; + let mut adapter_name: Option = None; + + // DNS setup + if args.dns { + if !is_admin { + eprintln!("[!] Для смены DNS нужны права root/администратора"); + } else { + adapter_name = network::detect_adapter(); + if let Some(ref name) = adapter_name { + match bypass::set_dns(name, "1.1.1.1", "1.0.0.1") { + Ok(()) => { + bypass::flush_dns(); + eprintln!("[+] DNS -> Cloudflare 1.1.1.1 (адаптер: {})", name); + dns_was_set = true; + } + Err(e) => eprintln!("[!] Не удалось сменить DNS: {}", e), + } + } else { + eprintln!("[!] Не удалось определить сетевой адаптер"); + } + } + } + + let stats = ws_proxy::ProxyStats::new(); + stats.verbose.store(true, std::sync::atomic::Ordering::Relaxed); + let stats_signal = stats.clone(); + + eprintln!("[*] Запускаю SOCKS5-прокси на {}:{}...", args.bind, args.port); + eprintln!("[*] Подключение прокси в Telegram:"); + eprintln!(" tg://socks?server={}&port={}", args.bind, args.port); + eprintln!("[*] Ctrl+C для остановки"); + + let rt = tokio::runtime::Runtime::new().expect("Не удалось создать tokio runtime"); + + rt.block_on(async { + // Graceful shutdown по Ctrl+C + let stats_ctrl = stats_signal.clone(); + tokio::spawn(async move { + tokio::signal::ctrl_c().await.ok(); + eprintln!("\n[*] Остановка..."); + stats_ctrl.running.store(false, Ordering::SeqCst); + }); + + if let Err(e) = ws_proxy::run_proxy_bind(&args.bind, args.port, stats_signal).await { + eprintln!("[!] Прокси остановлен с ошибкой: {}", e); + } + }); + + // Cleanup DNS + if dns_was_set { + let name = adapter_name.or_else(network::detect_adapter); + if let Some(ref name) = name { + let _ = bypass::reset_dns(name); + bypass::flush_dns(); + eprintln!("[+] DNS сброшен"); + } + } + + eprintln!("[*] Завершено. Всего соединений: {}", stats.total_conn.load(Ordering::Relaxed)); +} diff --git a/src/main.rs b/src/bin/gui.rs similarity index 98% rename from src/main.rs rename to src/bin/gui.rs index 2f28154..718df2f 100644 --- a/src/main.rs +++ b/src/bin/gui.rs @@ -1,13 +1,10 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -mod bypass; -mod network; -mod ws_proxy; - use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; use eframe::egui; +use tg_unblock::{bypass, network, ws_proxy}; const PROXY_PORT: u16 = 1080; @@ -30,6 +27,7 @@ fn main() -> eframe::Result<()> { ) } +#[cfg(target_os = "windows")] fn setup_fonts(ctx: &egui::Context) { let mut fonts = egui::FontDefinitions::default(); fonts.font_data.insert( @@ -51,6 +49,9 @@ fn setup_fonts(ctx: &egui::Context) { ctx.set_fonts(fonts); } +#[cfg(not(target_os = "windows"))] +fn setup_fonts(_ctx: &egui::Context) {} + #[derive(Clone)] struct LogEntry { text: String, diff --git a/src/bypass.rs b/src/bypass.rs index dde9913..4699bae 100644 --- a/src/bypass.rs +++ b/src/bypass.rs @@ -1,6 +1,8 @@ -use std::path::{Path, PathBuf}; use std::process::Command; +// ===== Admin/root check ===== + +#[cfg(target_os = "windows")] pub fn check_admin() -> bool { let output = Command::new("net") .args(["session"]) @@ -8,6 +10,21 @@ pub fn check_admin() -> bool { matches!(output, Ok(o) if o.status.success()) } +#[cfg(not(target_os = "windows"))] +pub fn check_admin() -> bool { + let output = Command::new("id").args(["-u"]).output(); + match output { + Ok(o) => { + let uid = String::from_utf8_lossy(&o.stdout).trim().to_string(); + uid == "0" + } + Err(_) => false, + } +} + +// ===== DNS management ===== + +#[cfg(target_os = "windows")] pub fn set_dns(adapter: &str, primary: &str, secondary: &str) -> Result<(), String> { let out1 = Command::new("netsh") .args([ @@ -37,6 +54,31 @@ pub fn set_dns(adapter: &str, primary: &str, secondary: &str) -> Result<(), Stri Ok(()) } +#[cfg(not(target_os = "windows"))] +pub fn set_dns(interface: &str, primary: &str, secondary: &str) -> Result<(), String> { + // Try resolvectl first (systemd-resolved) + if has_command("resolvectl") { + let out = Command::new("resolvectl") + .args(["dns", interface, primary, secondary]) + .output() + .map_err(|e| format!("resolvectl error: {}", e))?; + + if out.status.success() { + return Ok(()); + } + let stderr = String::from_utf8_lossy(&out.stderr); + return Err(format!("resolvectl failed: {}", stderr)); + } + + // Fallback: write /etc/resolv.conf directly + backup_resolv_conf()?; + let content = format!("# Set by tg_unblock\nnameserver {}\nnameserver {}\n", primary, secondary); + std::fs::write("/etc/resolv.conf", content) + .map_err(|e| format!("Failed to write /etc/resolv.conf: {}", e))?; + Ok(()) +} + +#[cfg(target_os = "windows")] pub fn reset_dns(adapter: &str) -> Result<(), String> { let out = Command::new("netsh") .args([ @@ -53,12 +95,79 @@ pub fn reset_dns(adapter: &str) -> Result<(), String> { Ok(()) } +#[cfg(not(target_os = "windows"))] +pub fn reset_dns(interface: &str) -> Result<(), String> { + if has_command("resolvectl") { + let out = Command::new("resolvectl") + .args(["revert", interface]) + .output() + .map_err(|e| format!("resolvectl error: {}", e))?; + + if out.status.success() { + return Ok(()); + } + } + + // Fallback: restore backup + restore_resolv_conf() +} + +#[cfg(target_os = "windows")] pub fn flush_dns() { let _ = Command::new("ipconfig") .args(["/flushdns"]) .output(); } +#[cfg(not(target_os = "windows"))] +pub fn flush_dns() { + if has_command("resolvectl") { + let _ = Command::new("resolvectl") + .args(["flush-caches"]) + .output(); + } +} + +// ===== Linux helpers ===== + +#[cfg(not(target_os = "windows"))] +fn has_command(name: &str) -> bool { + Command::new("which") + .arg(name) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +#[cfg(not(target_os = "windows"))] +fn backup_resolv_conf() -> Result<(), String> { + let backup = "/etc/resolv.conf.tg_unblock_bak"; + if !std::path::Path::new(backup).exists() { + std::fs::copy("/etc/resolv.conf", backup) + .map_err(|e| format!("Failed to backup resolv.conf: {}", e))?; + } + Ok(()) +} + +#[cfg(not(target_os = "windows"))] +fn restore_resolv_conf() -> Result<(), String> { + let backup = "/etc/resolv.conf.tg_unblock_bak"; + if std::path::Path::new(backup).exists() { + std::fs::copy(backup, "/etc/resolv.conf") + .map_err(|e| format!("Failed to restore resolv.conf: {}", e))?; + let _ = std::fs::remove_file(backup); + Ok(()) + } else { + Err("No resolv.conf backup found".to_string()) + } +} + +// ===== Windows-only: GoodbyeDPI ===== + +#[cfg(target_os = "windows")] +use std::path::{Path, PathBuf}; + +#[cfg(target_os = "windows")] pub fn find_goodbyedpi() -> Option { let exe_dir = std::env::current_exe() .ok() @@ -75,7 +184,6 @@ pub fn find_goodbyedpi() -> Option { ]; for dir in &search_dirs { - // Check common locations for sub in &["x86_64", "x86", ""] { let candidate = if sub.is_empty() { dir.join("goodbyedpi.exe") @@ -88,7 +196,6 @@ pub fn find_goodbyedpi() -> Option { } } - // Recursive search in tools/ if let Ok(entries) = find_file_recursive(Path::new("tools"), "goodbyedpi.exe") { if !entries.is_empty() { return Some(entries[0].to_string_lossy().to_string()); @@ -98,6 +205,7 @@ pub fn find_goodbyedpi() -> Option { None } +#[cfg(target_os = "windows")] fn find_file_recursive(dir: &Path, filename: &str) -> Result, std::io::Error> { let mut results = Vec::new(); if !dir.exists() { @@ -115,6 +223,7 @@ fn find_file_recursive(dir: &Path, filename: &str) -> Result, std:: Ok(results) } +#[cfg(target_os = "windows")] pub fn get_blacklist_path() -> Option { let candidates = vec![ PathBuf::from("tg_blacklist.txt"), @@ -133,6 +242,7 @@ pub fn get_blacklist_path() -> Option { None } +#[cfg(target_os = "windows")] pub fn start_goodbyedpi(exe_path: &str, args: &[&str], blacklist: Option<&str>) -> Result<(), String> { let mut cmd = Command::new(exe_path); cmd.args(args); @@ -145,12 +255,14 @@ pub fn start_goodbyedpi(exe_path: &str, args: &[&str], blacklist: Option<&str>) Ok(()) } +#[cfg(target_os = "windows")] pub fn kill_goodbyedpi() { let _ = Command::new("taskkill") .args(["/f", "/im", "goodbyedpi.exe"]) .output(); } +#[cfg(target_os = "windows")] pub fn download_goodbyedpi() -> Result { let tools_dir = PathBuf::from("tools"); std::fs::create_dir_all(&tools_dir) @@ -159,7 +271,6 @@ pub fn download_goodbyedpi() -> Result { let zip_path = tools_dir.join("goodbyedpi.zip"); let url = "https://github.com/ValdikSS/GoodbyeDPI/releases/download/0.2.3rc3/goodbyedpi-0.2.3rc3-2.zip"; - // Download using powershell let dl_script = format!( "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing", url, @@ -176,7 +287,6 @@ pub fn download_goodbyedpi() -> Result { return Err(format!("Download failed: {}", stderr)); } - // Extract let extract_script = format!( "Expand-Archive -Path '{}' -DestinationPath '{}' -Force", zip_path.to_string_lossy(), @@ -193,9 +303,7 @@ pub fn download_goodbyedpi() -> Result { return Err(format!("Extraction failed: {}", stderr)); } - // Clean up zip let _ = std::fs::remove_file(&zip_path); - // Find the exe find_goodbyedpi().ok_or_else(|| "goodbyedpi.exe not found after extraction".to_string()) } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..183436f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod bypass; +pub mod network; +pub mod ws_proxy; diff --git a/src/network.rs b/src/network.rs index 065bf33..b298103 100644 --- a/src/network.rs +++ b/src/network.rs @@ -2,6 +2,9 @@ use std::net::{TcpStream, SocketAddr}; use std::process::Command; use std::time::{Duration, Instant}; +// ===== Adapter detection ===== + +#[cfg(target_os = "windows")] pub fn detect_adapter() -> Option { let output = Command::new("powershell") .args([ @@ -19,6 +22,28 @@ pub fn detect_adapter() -> Option { } } +#[cfg(not(target_os = "windows"))] +pub fn detect_adapter() -> Option { + // Parse default route: "default via 10.0.0.1 dev eth0 ..." + let output = Command::new("ip") + .args(["route", "show", "default"]) + .output() + .ok()?; + + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + if let Some(pos) = line.find("dev ") { + let after = &line[pos + 4..]; + let iface = after.split_whitespace().next()?; + return Some(iface.to_string()); + } + } + None +} + +// ===== Current DNS ===== + +#[cfg(target_os = "windows")] pub fn get_current_dns() -> Option { let output = Command::new("powershell") .args([ @@ -36,20 +61,56 @@ pub fn get_current_dns() -> Option { } } +#[cfg(not(target_os = "windows"))] +pub fn get_current_dns() -> Option { + let content = std::fs::read_to_string("/etc/resolv.conf").ok()?; + let servers: Vec<&str> = content + .lines() + .filter(|l| l.starts_with("nameserver")) + .filter_map(|l| l.split_whitespace().nth(1)) + .collect(); + + if servers.is_empty() { + Some("Не определено".to_string()) + } else { + Some(servers.join(", ")) + } +} + +// ===== Ping ===== + +#[cfg(target_os = "windows")] pub fn ping_host(ip: &str) -> (bool, Option) { let start = Instant::now(); let output = Command::new("ping") .args(["-n", "1", "-w", "3000", ip]) .output(); + parse_ping_output(output, start) +} + +#[cfg(not(target_os = "windows"))] +pub fn ping_host(ip: &str) -> (bool, Option) { + let start = Instant::now(); + let output = Command::new("ping") + .args(["-c", "1", "-W", "3", ip]) + .output(); + + parse_ping_output(output, start) +} + +fn parse_ping_output( + output: Result, + start: Instant, +) -> (bool, Option) { match output { Ok(out) => { let elapsed = start.elapsed().as_millis() as u64; let stdout = String::from_utf8_lossy(&out.stdout); - let ok = out.status.success() && (stdout.contains("TTL=") || stdout.contains("ttl=")); + let ok = out.status.success() + && (stdout.contains("TTL=") || stdout.contains("ttl=")); if ok { - // Try to extract actual time from ping output if let Some(time_str) = extract_ping_time(&stdout) { (true, Some(time_str)) } else { @@ -64,7 +125,6 @@ pub fn ping_host(ip: &str) -> (bool, Option) { } fn extract_ping_time(output: &str) -> Option { - // Match patterns like "time=46ms" or "time<1ms" or "время=46мс" for line in output.lines() { let lower = line.to_lowercase(); if let Some(pos) = lower.find("time=").or_else(|| lower.find("time<")) { @@ -88,6 +148,8 @@ fn extract_ping_time(output: &str) -> Option { None } +// ===== TCP / HTTPS checks (cross-platform) ===== + pub fn tcp_check(ip: &str, port: u16) -> (bool, Option) { let addr: SocketAddr = format!("{}:{}", ip, port).parse().unwrap(); let start = Instant::now(); @@ -119,9 +181,6 @@ pub fn https_check(url: &str) -> (bool, Option) { } } -/// Benchmarks Telegram connectivity: runs multiple TCP+HTTPS checks, -/// returns (works: bool, score: u64) where lower score = faster connection. -/// Score is average latency across all successful checks. u64::MAX if nothing works. pub fn benchmark_telegram() -> (bool, u64) { let tcp_targets = [ ("149.154.167.51", 443u16), @@ -134,7 +193,6 @@ pub fn benchmark_telegram() -> (bool, u64) { let mut ok_count: u64 = 0; let mut fail_count: u64 = 0; - // TCP checks (x2 rounds for stability) for _ in 0..2 { for (ip, port) in &tcp_targets { let (ok, latency) = tcp_check(ip, *port); @@ -147,7 +205,6 @@ pub fn benchmark_telegram() -> (bool, u64) { } } - // HTTPS check — the real indicator of usable speed let https_urls = [ "https://web.telegram.org", "https://t.me", @@ -155,7 +212,6 @@ pub fn benchmark_telegram() -> (bool, u64) { for url in &https_urls { let (ok, latency) = https_check(url); if ok { - // Weight HTTPS 3x heavier since it's closer to real usage let ms = latency.unwrap_or(10000); total_ms += ms * 3; ok_count += 3; @@ -168,7 +224,6 @@ pub fn benchmark_telegram() -> (bool, u64) { return (false, u64::MAX); } - // Penalize failures: each fail adds 2000ms to the score let penalty = fail_count * 2000; let avg = (total_ms + penalty) / (ok_count + fail_count); diff --git a/src/ws_proxy.rs b/src/ws_proxy.rs index 681b1b0..4bb28b0 100644 --- a/src/ws_proxy.rs +++ b/src/ws_proxy.rs @@ -11,6 +11,7 @@ pub struct ProxyStats { pub active_conn: AtomicU32, pub total_conn: AtomicU32, pub ws_active: AtomicU32, + pub verbose: AtomicBool, } impl ProxyStats { @@ -20,12 +21,17 @@ impl ProxyStats { active_conn: AtomicU32::new(0), total_conn: AtomicU32::new(0), ws_active: AtomicU32::new(0), + verbose: AtomicBool::new(false), }) } } pub async fn run_proxy(port: u16, stats: Arc) -> Result<(), String> { - let addr = format!("127.0.0.1:{}", port); + run_proxy_bind("127.0.0.1", port, stats).await +} + +pub async fn run_proxy_bind(bind: &str, port: u16, stats: Arc) -> Result<(), String> { + let addr = format!("{}:{}", bind, port); let listener = TcpListener::bind(&addr) .await .map_err(|e| format!("Не удалось занять порт {}: {}", port, e))?; @@ -143,7 +149,7 @@ async fn handle_socks5( let (dest_addr, dest_port) = parse_dest(&buf[3..n])?; let is_tg = is_telegram_ip(&dest_addr); - + let verbose = stats.verbose.load(Ordering::Relaxed); // SOCKS5 success (we handle the connection ourselves) stream .write_all(&[0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 0x04, 0x38]) @@ -163,6 +169,10 @@ async fn handle_socks5( .unwrap_or(2) }); + if verbose { + eprintln!("[+] Telegram {}:{} -> WSS DC{}", dest_addr, dest_port, dc); + } + stats.ws_active.fetch_add(1, Ordering::Relaxed); // Try WebSocket tunnel; fall back to direct TCP on failure @@ -170,12 +180,19 @@ async fn handle_socks5( stats.ws_active.fetch_sub(1, Ordering::Relaxed); + if verbose { + eprintln!("[-] Telegram DC{} отключён", dc); + } + if let Err(e) = ws_result { return Err(format!("DC{} tunnel: {}", dc, e).into()); } } else { // Non-Telegram — direct TCP passthrough let target = format!("{}:{}", dest_addr, dest_port); + if verbose { + eprintln!("[+] TCP {}:{}", dest_addr, dest_port); + } match TcpStream::connect(&target).await { Ok(remote) => { let _ = remote.set_nodelay(true);