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.
-
+
@@ -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);