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.
This commit is contained in:
mistermelphin 2026-03-24 11:10:58 +04:00
parent c40ad94e11
commit b8987b666a
8 changed files with 389 additions and 58 deletions

View File

@ -3,24 +3,37 @@ name = "tg_unblock"
version = "0.3.1" version = "0.3.1"
edition = "2021" 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] [dependencies]
eframe = "0.31"
egui = "0.31"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["blocking"] } reqwest = { version = "0.12", features = ["blocking"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
open = "5"
tokio-tungstenite = { version = "0.24", features = ["native-tls"] } tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
native-tls = "0.2" native-tls = "0.2"
futures-util = "0.3" futures-util = "0.3"
aes = "0.8" aes = "0.8"
ctr = "0.9" ctr = "0.9"
cipher = "0.4" 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] [target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winuser"] } winapi = { version = "0.3", features = ["winuser"] }
[[bin]]
name = "tg_unblock"
path = "src/main.rs"

100
README.md
View File

@ -2,14 +2,14 @@
<h1 align="center">TG Unblock</h1> <h1 align="center">TG Unblock</h1>
<p align="center"> <p align="center">
<b>Обход блокировки Telegram через WebSocket-туннель</b><br> <b>Обход блокировки Telegram через WebSocket-туннель</b><br>
Без VPN. Без серверов. Без абонентки. Один клик. Без VPN. Без серверов. Без абонентки. CLI + GUI.
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/by-sonic/tglock/releases"><img src="https://img.shields.io/github/v/release/by-sonic/tglock?style=for-the-badge&color=blue" alt="Release"></a> <a href="https://github.com/by-sonic/tglock/releases"><img src="https://img.shields.io/github/v/release/by-sonic/tglock?style=for-the-badge&color=blue" alt="Release"></a>
<a href="https://github.com/by-sonic/tglock/blob/main/LICENSE"><img src="https://img.shields.io/github/license/by-sonic/tglock?style=for-the-badge" alt="License"></a> <a href="https://github.com/by-sonic/tglock/blob/main/LICENSE"><img src="https://img.shields.io/github/license/by-sonic/tglock?style=for-the-badge" alt="License"></a>
<a href="https://github.com/by-sonic/tglock/stargazers"><img src="https://img.shields.io/github/stars/by-sonic/tglock?style=for-the-badge&color=yellow" alt="Stars"></a> <a href="https://github.com/by-sonic/tglock/stargazers"><img src="https://img.shields.io/github/stars/by-sonic/tglock?style=for-the-badge&color=yellow" alt="Stars"></a>
<img src="https://img.shields.io/badge/rust-1.70%2B-orange?style=for-the-badge&logo=rust" alt="Rust"> <img src="https://img.shields.io/badge/rust-1.70%2B-orange?style=for-the-badge&logo=rust" alt="Rust">
<img src="https://img.shields.io/badge/platform-Windows-0078D6?style=for-the-badge&logo=windows" alt="Windows"> <img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-0078D6?style=for-the-badge" alt="Platform">
</p> </p>
</p> </p>
@ -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? ### Почему не GoodbyeDPI / Zapret?
@ -28,7 +32,7 @@
| IP-шейпинг обходит? | Нет | Нет | **Да** | | IP-шейпинг обходит? | Нет | Нет | **Да** |
| Скорость | Зависит от DPI | Зависит от DPI | **Полная** | | Скорость | Зависит от DPI | Зависит от DPI | **Полная** |
| Переподключения | Возможны | Возможны | **Нет** | | Переподключения | Возможны | Возможны | **Нет** |
| Настройка | Много параметров | Стратегии | **Один клик** | | Настройка | Много параметров | Стратегии | **Один клик / одна команда** |
## Скачать ## Скачать
@ -39,28 +43,62 @@
```bash ```bash
git clone https://github.com/by-sonic/tglock.git git clone https://github.com/by-sonic/tglock.git
cd tglock 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 <PORT> Порт SOCKS5-прокси [default: 1080]
-b, --bind <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. Нажмите **"Запустить обход"** 2. Нажмите **"Запустить обход"**
3. Нажмите **"Настроить автоматически"** — откроется Telegram, нажмите "Подключить" 3. Нажмите **"Настроить автоматически"** — откроется Telegram, нажмите "Подключить"
4. Готово. Telegram работает на полной скорости. 4. Готово. Telegram работает на полной скорости.
### Ручная настройка прокси ### Настройка прокси в Telegram
Если автонастройка не сработала:
**Telegram Desktop** → Настройки → Продвинутые → Тип соединения → **Использовать SOCKS5-прокси** **Telegram Desktop** → Настройки → Продвинутые → Тип соединения → **Использовать SOCKS5-прокси**
| Параметр | Значение | | Параметр | Значение |
|---|---| |---|---|
| Сервер | `127.0.0.1` | | Сервер | `127.0.0.1` |
| Порт | `1080` | | Порт | `1080` (или тот, что указали в `--port`) |
| Логин | *пусто* | | Логин | *пусто* |
| Пароль | *пусто* | | Пароль | *пусто* |
@ -89,39 +127,46 @@ Telegram Desktop
| DC | Подсеть | WebSocket | | DC | Подсеть | WebSocket |
|---|---|---| |---|---|---|
| DC1 | `149.154.160.0/22` | `wss://pluto.web.telegram.org/apiws` | | DC1 | `149.154.160.0/22` | `wss://kws1.web.telegram.org/apiws` |
| DC2 | `149.154.164.0/22` | `wss://venus.web.telegram.org/apiws` | | DC2 | `149.154.164.0/22` | `wss://kws2.web.telegram.org/apiws` |
| DC3 | `149.154.168.0/22` | `wss://aurora.web.telegram.org/apiws` | | DC3 | `149.154.168.0/22` | `wss://kws3.web.telegram.org/apiws` |
| DC4 | `91.108.12.0/22` | `wss://vesta.web.telegram.org/apiws` | | DC4 | `91.108.12.0/22` | `wss://kws4.web.telegram.org/apiws` |
| DC5 | `91.108.56.0/22` | `wss://flora.web.telegram.org/apiws` | | DC5 | `91.108.56.0/22` | `wss://kws5.web.telegram.org/apiws` |
Имена DC (`pluto`, `venus`, `aurora`, `vesta`, `flora`) — из [официальной документации MTProto](https://core.telegram.org/mtproto/transports).
## Стек ## Стек
| Что | Зачем | | Что | Зачем |
|---|---| |---|---|
| **Rust** | Скорость, безопасность, один бинарник без зависимостей | | **Rust** | Скорость, безопасность, один бинарник без зависимостей |
| **egui / eframe** | Нативный GUI без Electron, без браузера |
| **tokio** | Async I/O для высокопроизводительного проксирования | | **tokio** | Async I/O для высокопроизводительного проксирования |
| **tokio-tungstenite** | WebSocket-клиент с TLS | | **tokio-tungstenite** | WebSocket-клиент с TLS |
| **native-tls** | TLS через системные сертификаты Windows | | **native-tls** | TLS через системные сертификаты |
| **clap** | Парсинг аргументов CLI |
| **egui / eframe** | Нативный GUI (опционально, Windows) |
## Структура проекта ## Структура проекта
``` ```
tglock/ tglock/
├── Cargo.toml # Зависимости ├── Cargo.toml # Зависимости и таргеты
├── src/ ├── src/
│ ├── main.rs # GUI + управление прокси │ ├── lib.rs # Библиотечный крейт
│ ├── ws_proxy.rs # SOCKS5-сервер + WebSocket-туннель │ ├── ws_proxy.rs # SOCKS5-сервер + WebSocket-туннель
│ ├── bypass.rs # DNS-настройка, утилиты Windows │ ├── bypass.rs # DNS-настройка (Linux + Windows)
│ └── network.rs # Сетевая диагностика │ ├── network.rs # Сетевая диагностика (Linux + Windows)
└── tg_blacklist.txt # IP-подсети и домены Telegram │ └── 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 - Windows 10/11
- [Rust 1.70+](https://rustup.rs/) (для сборки из исходников) - [Rust 1.70+](https://rustup.rs/) (для сборки из исходников)
- Права администратора (для смены DNS, опционально) - Права администратора (для смены DNS, опционально)
@ -140,6 +185,9 @@ A: Пока только Telegram Desktop. Для мобильных устро
**Q: Замедляется ли интернет?** **Q: Замедляется ли интернет?**
A: Нет. Проксируется только трафик к серверам Telegram. Весь остальной трафик идёт напрямую. A: Нет. Проксируется только трафик к серверам Telegram. Весь остальной трафик идёт напрямую.
**Q: Работает ли на Linux?**
A: Да. CLI-версия полностью кроссплатформенная. На Linux для смены DNS используется `resolvectl` (systemd-resolved) или прямая запись в `/etc/resolv.conf`.
## VPN для полного обхода ## VPN для полного обхода
Если нужен обход блокировок для **всех** приложений (YouTube, Discord, Instagram и др.) — попробуйте **[by sonic VPN](https://t.me/bysonicvpn_bot)**. Быстрый, без ограничений скорости. Если нужен обход блокировок для **всех** приложений (YouTube, Discord, Instagram и др.) — попробуйте **[by sonic VPN](https://t.me/bysonicvpn_bot)**. Быстрый, без ограничений скорости.

86
src/bin/cli.rs Normal file
View File

@ -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<String> = 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));
}

View File

@ -1,13 +1,10 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod bypass;
mod network;
mod ws_proxy;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use eframe::egui; use eframe::egui;
use tg_unblock::{bypass, network, ws_proxy};
const PROXY_PORT: u16 = 1080; const PROXY_PORT: u16 = 1080;
@ -30,6 +27,7 @@ fn main() -> eframe::Result<()> {
) )
} }
#[cfg(target_os = "windows")]
fn setup_fonts(ctx: &egui::Context) { fn setup_fonts(ctx: &egui::Context) {
let mut fonts = egui::FontDefinitions::default(); let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert( fonts.font_data.insert(
@ -51,6 +49,9 @@ fn setup_fonts(ctx: &egui::Context) {
ctx.set_fonts(fonts); ctx.set_fonts(fonts);
} }
#[cfg(not(target_os = "windows"))]
fn setup_fonts(_ctx: &egui::Context) {}
#[derive(Clone)] #[derive(Clone)]
struct LogEntry { struct LogEntry {
text: String, text: String,

View File

@ -1,6 +1,8 @@
use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
// ===== Admin/root check =====
#[cfg(target_os = "windows")]
pub fn check_admin() -> bool { pub fn check_admin() -> bool {
let output = Command::new("net") let output = Command::new("net")
.args(["session"]) .args(["session"])
@ -8,6 +10,21 @@ pub fn check_admin() -> bool {
matches!(output, Ok(o) if o.status.success()) 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> { pub fn set_dns(adapter: &str, primary: &str, secondary: &str) -> Result<(), String> {
let out1 = Command::new("netsh") let out1 = Command::new("netsh")
.args([ .args([
@ -37,6 +54,31 @@ pub fn set_dns(adapter: &str, primary: &str, secondary: &str) -> Result<(), Stri
Ok(()) 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> { pub fn reset_dns(adapter: &str) -> Result<(), String> {
let out = Command::new("netsh") let out = Command::new("netsh")
.args([ .args([
@ -53,12 +95,79 @@ pub fn reset_dns(adapter: &str) -> Result<(), String> {
Ok(()) 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() { pub fn flush_dns() {
let _ = Command::new("ipconfig") let _ = Command::new("ipconfig")
.args(["/flushdns"]) .args(["/flushdns"])
.output(); .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<String> { pub fn find_goodbyedpi() -> Option<String> {
let exe_dir = std::env::current_exe() let exe_dir = std::env::current_exe()
.ok() .ok()
@ -75,7 +184,6 @@ pub fn find_goodbyedpi() -> Option<String> {
]; ];
for dir in &search_dirs { for dir in &search_dirs {
// Check common locations
for sub in &["x86_64", "x86", ""] { for sub in &["x86_64", "x86", ""] {
let candidate = if sub.is_empty() { let candidate = if sub.is_empty() {
dir.join("goodbyedpi.exe") dir.join("goodbyedpi.exe")
@ -88,7 +196,6 @@ pub fn find_goodbyedpi() -> Option<String> {
} }
} }
// Recursive search in tools/
if let Ok(entries) = find_file_recursive(Path::new("tools"), "goodbyedpi.exe") { if let Ok(entries) = find_file_recursive(Path::new("tools"), "goodbyedpi.exe") {
if !entries.is_empty() { if !entries.is_empty() {
return Some(entries[0].to_string_lossy().to_string()); return Some(entries[0].to_string_lossy().to_string());
@ -98,6 +205,7 @@ pub fn find_goodbyedpi() -> Option<String> {
None None
} }
#[cfg(target_os = "windows")]
fn find_file_recursive(dir: &Path, filename: &str) -> Result<Vec<PathBuf>, std::io::Error> { fn find_file_recursive(dir: &Path, filename: &str) -> Result<Vec<PathBuf>, std::io::Error> {
let mut results = Vec::new(); let mut results = Vec::new();
if !dir.exists() { if !dir.exists() {
@ -115,6 +223,7 @@ fn find_file_recursive(dir: &Path, filename: &str) -> Result<Vec<PathBuf>, std::
Ok(results) Ok(results)
} }
#[cfg(target_os = "windows")]
pub fn get_blacklist_path() -> Option<String> { pub fn get_blacklist_path() -> Option<String> {
let candidates = vec![ let candidates = vec![
PathBuf::from("tg_blacklist.txt"), PathBuf::from("tg_blacklist.txt"),
@ -133,6 +242,7 @@ pub fn get_blacklist_path() -> Option<String> {
None None
} }
#[cfg(target_os = "windows")]
pub fn start_goodbyedpi(exe_path: &str, args: &[&str], blacklist: Option<&str>) -> Result<(), String> { pub fn start_goodbyedpi(exe_path: &str, args: &[&str], blacklist: Option<&str>) -> Result<(), String> {
let mut cmd = Command::new(exe_path); let mut cmd = Command::new(exe_path);
cmd.args(args); cmd.args(args);
@ -145,12 +255,14 @@ pub fn start_goodbyedpi(exe_path: &str, args: &[&str], blacklist: Option<&str>)
Ok(()) Ok(())
} }
#[cfg(target_os = "windows")]
pub fn kill_goodbyedpi() { pub fn kill_goodbyedpi() {
let _ = Command::new("taskkill") let _ = Command::new("taskkill")
.args(["/f", "/im", "goodbyedpi.exe"]) .args(["/f", "/im", "goodbyedpi.exe"])
.output(); .output();
} }
#[cfg(target_os = "windows")]
pub fn download_goodbyedpi() -> Result<String, String> { pub fn download_goodbyedpi() -> Result<String, String> {
let tools_dir = PathBuf::from("tools"); let tools_dir = PathBuf::from("tools");
std::fs::create_dir_all(&tools_dir) std::fs::create_dir_all(&tools_dir)
@ -159,7 +271,6 @@ pub fn download_goodbyedpi() -> Result<String, String> {
let zip_path = tools_dir.join("goodbyedpi.zip"); 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"; 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!( let dl_script = format!(
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing", "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing",
url, url,
@ -176,7 +287,6 @@ pub fn download_goodbyedpi() -> Result<String, String> {
return Err(format!("Download failed: {}", stderr)); return Err(format!("Download failed: {}", stderr));
} }
// Extract
let extract_script = format!( let extract_script = format!(
"Expand-Archive -Path '{}' -DestinationPath '{}' -Force", "Expand-Archive -Path '{}' -DestinationPath '{}' -Force",
zip_path.to_string_lossy(), zip_path.to_string_lossy(),
@ -193,9 +303,7 @@ pub fn download_goodbyedpi() -> Result<String, String> {
return Err(format!("Extraction failed: {}", stderr)); return Err(format!("Extraction failed: {}", stderr));
} }
// Clean up zip
let _ = std::fs::remove_file(&zip_path); let _ = std::fs::remove_file(&zip_path);
// Find the exe
find_goodbyedpi().ok_or_else(|| "goodbyedpi.exe not found after extraction".to_string()) find_goodbyedpi().ok_or_else(|| "goodbyedpi.exe not found after extraction".to_string())
} }

3
src/lib.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod bypass;
pub mod network;
pub mod ws_proxy;

View File

@ -2,6 +2,9 @@ use std::net::{TcpStream, SocketAddr};
use std::process::Command; use std::process::Command;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
// ===== Adapter detection =====
#[cfg(target_os = "windows")]
pub fn detect_adapter() -> Option<String> { pub fn detect_adapter() -> Option<String> {
let output = Command::new("powershell") let output = Command::new("powershell")
.args([ .args([
@ -19,6 +22,28 @@ pub fn detect_adapter() -> Option<String> {
} }
} }
#[cfg(not(target_os = "windows"))]
pub fn detect_adapter() -> Option<String> {
// 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<String> { pub fn get_current_dns() -> Option<String> {
let output = Command::new("powershell") let output = Command::new("powershell")
.args([ .args([
@ -36,20 +61,56 @@ pub fn get_current_dns() -> Option<String> {
} }
} }
#[cfg(not(target_os = "windows"))]
pub fn get_current_dns() -> Option<String> {
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<u64>) { pub fn ping_host(ip: &str) -> (bool, Option<u64>) {
let start = Instant::now(); let start = Instant::now();
let output = Command::new("ping") let output = Command::new("ping")
.args(["-n", "1", "-w", "3000", ip]) .args(["-n", "1", "-w", "3000", ip])
.output(); .output();
parse_ping_output(output, start)
}
#[cfg(not(target_os = "windows"))]
pub fn ping_host(ip: &str) -> (bool, Option<u64>) {
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<std::process::Output, std::io::Error>,
start: Instant,
) -> (bool, Option<u64>) {
match output { match output {
Ok(out) => { Ok(out) => {
let elapsed = start.elapsed().as_millis() as u64; let elapsed = start.elapsed().as_millis() as u64;
let stdout = String::from_utf8_lossy(&out.stdout); 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 { if ok {
// Try to extract actual time from ping output
if let Some(time_str) = extract_ping_time(&stdout) { if let Some(time_str) = extract_ping_time(&stdout) {
(true, Some(time_str)) (true, Some(time_str))
} else { } else {
@ -64,7 +125,6 @@ pub fn ping_host(ip: &str) -> (bool, Option<u64>) {
} }
fn extract_ping_time(output: &str) -> Option<u64> { fn extract_ping_time(output: &str) -> Option<u64> {
// Match patterns like "time=46ms" or "time<1ms" or "время=46мс"
for line in output.lines() { for line in output.lines() {
let lower = line.to_lowercase(); let lower = line.to_lowercase();
if let Some(pos) = lower.find("time=").or_else(|| lower.find("time<")) { if let Some(pos) = lower.find("time=").or_else(|| lower.find("time<")) {
@ -88,6 +148,8 @@ fn extract_ping_time(output: &str) -> Option<u64> {
None None
} }
// ===== TCP / HTTPS checks (cross-platform) =====
pub fn tcp_check(ip: &str, port: u16) -> (bool, Option<u64>) { pub fn tcp_check(ip: &str, port: u16) -> (bool, Option<u64>) {
let addr: SocketAddr = format!("{}:{}", ip, port).parse().unwrap(); let addr: SocketAddr = format!("{}:{}", ip, port).parse().unwrap();
let start = Instant::now(); let start = Instant::now();
@ -119,9 +181,6 @@ pub fn https_check(url: &str) -> (bool, Option<u64>) {
} }
} }
/// 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) { pub fn benchmark_telegram() -> (bool, u64) {
let tcp_targets = [ let tcp_targets = [
("149.154.167.51", 443u16), ("149.154.167.51", 443u16),
@ -134,7 +193,6 @@ pub fn benchmark_telegram() -> (bool, u64) {
let mut ok_count: u64 = 0; let mut ok_count: u64 = 0;
let mut fail_count: u64 = 0; let mut fail_count: u64 = 0;
// TCP checks (x2 rounds for stability)
for _ in 0..2 { for _ in 0..2 {
for (ip, port) in &tcp_targets { for (ip, port) in &tcp_targets {
let (ok, latency) = tcp_check(ip, *port); 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 = [ let https_urls = [
"https://web.telegram.org", "https://web.telegram.org",
"https://t.me", "https://t.me",
@ -155,7 +212,6 @@ pub fn benchmark_telegram() -> (bool, u64) {
for url in &https_urls { for url in &https_urls {
let (ok, latency) = https_check(url); let (ok, latency) = https_check(url);
if ok { if ok {
// Weight HTTPS 3x heavier since it's closer to real usage
let ms = latency.unwrap_or(10000); let ms = latency.unwrap_or(10000);
total_ms += ms * 3; total_ms += ms * 3;
ok_count += 3; ok_count += 3;
@ -168,7 +224,6 @@ pub fn benchmark_telegram() -> (bool, u64) {
return (false, u64::MAX); return (false, u64::MAX);
} }
// Penalize failures: each fail adds 2000ms to the score
let penalty = fail_count * 2000; let penalty = fail_count * 2000;
let avg = (total_ms + penalty) / (ok_count + fail_count); let avg = (total_ms + penalty) / (ok_count + fail_count);

View File

@ -11,6 +11,7 @@ pub struct ProxyStats {
pub active_conn: AtomicU32, pub active_conn: AtomicU32,
pub total_conn: AtomicU32, pub total_conn: AtomicU32,
pub ws_active: AtomicU32, pub ws_active: AtomicU32,
pub verbose: AtomicBool,
} }
impl ProxyStats { impl ProxyStats {
@ -20,12 +21,17 @@ impl ProxyStats {
active_conn: AtomicU32::new(0), active_conn: AtomicU32::new(0),
total_conn: AtomicU32::new(0), total_conn: AtomicU32::new(0),
ws_active: AtomicU32::new(0), ws_active: AtomicU32::new(0),
verbose: AtomicBool::new(false),
}) })
} }
} }
pub async fn run_proxy(port: u16, stats: Arc<ProxyStats>) -> Result<(), String> { pub async fn run_proxy(port: u16, stats: Arc<ProxyStats>) -> 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<ProxyStats>) -> Result<(), String> {
let addr = format!("{}:{}", bind, port);
let listener = TcpListener::bind(&addr) let listener = TcpListener::bind(&addr)
.await .await
.map_err(|e| format!("Не удалось занять порт {}: {}", port, e))?; .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 (dest_addr, dest_port) = parse_dest(&buf[3..n])?;
let is_tg = is_telegram_ip(&dest_addr); let is_tg = is_telegram_ip(&dest_addr);
let verbose = stats.verbose.load(Ordering::Relaxed);
// SOCKS5 success (we handle the connection ourselves) // SOCKS5 success (we handle the connection ourselves)
stream stream
.write_all(&[0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 0x04, 0x38]) .write_all(&[0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 0x04, 0x38])
@ -163,6 +169,10 @@ async fn handle_socks5(
.unwrap_or(2) .unwrap_or(2)
}); });
if verbose {
eprintln!("[+] Telegram {}:{} -> WSS DC{}", dest_addr, dest_port, dc);
}
stats.ws_active.fetch_add(1, Ordering::Relaxed); stats.ws_active.fetch_add(1, Ordering::Relaxed);
// Try WebSocket tunnel; fall back to direct TCP on failure // 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); stats.ws_active.fetch_sub(1, Ordering::Relaxed);
if verbose {
eprintln!("[-] Telegram DC{} отключён", dc);
}
if let Err(e) = ws_result { if let Err(e) = ws_result {
return Err(format!("DC{} tunnel: {}", dc, e).into()); return Err(format!("DC{} tunnel: {}", dc, e).into());
} }
} else { } else {
// Non-Telegram — direct TCP passthrough // Non-Telegram — direct TCP passthrough
let target = format!("{}:{}", dest_addr, dest_port); let target = format!("{}:{}", dest_addr, dest_port);
if verbose {
eprintln!("[+] TCP {}:{}", dest_addr, dest_port);
}
match TcpStream::connect(&target).await { match TcpStream::connect(&target).await {
Ok(remote) => { Ok(remote) => {
let _ = remote.set_nodelay(true); let _ = remote.set_nodelay(true);