mirror of
https://github.com/by-sonic/tglock.git
synced 2026-08-13 10:13:10 +03:00
feat(cli): headless tglock-cli, GUI behind a feature, deep test coverage (#25)
Реализует направление PR #15 поверх архитектуры 2.0. Сам PR смерджить нельзя: он патчит src/bypass.rs, src/network.rs и src/ws_proxy.rs, которых больше нет, и правит системный DNS — в 2.0 это не нужно, потому что адреса Telegram зашиты в маршрутах, а SNI остаётся настоящим. Взято разделение GUI/CLI и произвольный bind-адрес, отброшены DNS-менеджмент и проверка root: CLI не требует прав. Closes #10, #17 — GUI не создаёт окно без 3D-ускорения, на машине без монитора и в виртуалке. Причина в WebView под Tauri, поэтому лечится не программным рендером, а бинарём, в котором WebView нет вовсе: при выключенной фиче gui Tauri и фронтенд в сборку не попадают. Отдельная задача CI собирает и гоняет CLI на голом ubuntu без Node.js и без libwebkit2gtk. Структура: - src/lib.rs — ядро (mtproto, proxy, transport, config), без Tauri - src/main.rs — GUI, required-features = ["gui"] - src/bin/cli.rs — headless-бинарь на clap - build.rs вызывает tauri_build только при включённой фиче gui Исправлено по пути: - Определение протокола: SOCKS5 и MTProto различались по первому байту, но is_reserved_init не исключает 0x05, поэтому примерно одно соединение из 256 уезжало в SOCKS5-ветку и умирало. Теперь неоднозначный первый байт решается по полному 64-байтному init и секрету. - Ярлык маршрута в UI: код 2 подписывался как «Cloudflare Worker», хотя это запасной Telegram IP, а системный DNS и настоящий Worker оба показывались как «Автоматический маршрут». Метки переехали в transport::route_label, общий для обоих интерфейсов. - Секрет прокси: под DynamicUser и ProtectHome домашней папки нет, secret_path возвращает None и секрет генерировался заново при каждом старте, ломая всем настроенным клиентам tg://-ссылку. Добавлен --secret-file. - README обещал Rust 1.75+, тогда как Cargo.toml требует 1.88 и CI это проверяет. Это и есть первопричина #3. Политика доступа: прямые не-Telegram соединения разрешены только на loopback, на любом сетевом адресе нужен явный --allow-direct. Правило из ISSUE_AUDIT о том, что LAN не должен становиться открытым SOCKS5, теперь выражено в типе ListenConfig и покрыто тестами. Тесты: 46 в библиотеке + 12 в CLI. Появился сквозной тест туннеля против мок-релея, который реализует сторону Telegram по obfuscated2 — проверяется не внутренняя консистентность, а что реле получает ровно тот открытый текст, который отправил клиент, и обратно. Плюс расписание backoff, фолбэк при всех маршрутах в cooldown, валидация Worker-доменов, отказы SOCKS5, устойчивость секрета к перезапуску и корректная остановка по SIGTERM. Документация: секция CLI в README с юнитом systemd и Dockerfile. Co-authored-by: by-sonic <171230345+by-sonic@users.noreply.github.com>
This commit is contained in:
+12
-40
@@ -1,15 +1,13 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod mtproto;
|
||||
mod proxy;
|
||||
mod transport;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
||||
use tauri::{Manager, State};
|
||||
use tglock::config::ListenConfig;
|
||||
use tglock::{proxy, transport};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -90,11 +88,7 @@ impl AppState {
|
||||
|
||||
fn snapshot(&self) -> StatusSnapshot {
|
||||
let data_center = self.stats.last_dc.load(Ordering::Relaxed);
|
||||
let route = match self.stats.last_route.load(Ordering::Relaxed) {
|
||||
1 => "Telegram WebSocket",
|
||||
2 => "Cloudflare Worker",
|
||||
_ => "Автоматический маршрут",
|
||||
};
|
||||
let route = transport::route_label(self.stats.last_route.load(Ordering::Relaxed));
|
||||
StatusSnapshot {
|
||||
running: self.stats.running.load(Ordering::SeqCst),
|
||||
active_connections: self.stats.active.load(Ordering::Relaxed),
|
||||
@@ -173,10 +167,14 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
||||
*state.started_at.lock().unwrap() = Some(Instant::now());
|
||||
state.log("Запускаю защищённый маршрут…", false);
|
||||
|
||||
let listen = if settings.lan_mode {
|
||||
ListenConfig::lan(settings.port)
|
||||
} else {
|
||||
ListenConfig::loopback(settings.port)
|
||||
};
|
||||
|
||||
let stats = state.stats.clone();
|
||||
let logs = state.logs.clone();
|
||||
let lan_mode = settings.lan_mode;
|
||||
let port = settings.port;
|
||||
std::thread::spawn(move || {
|
||||
let runtime = match tokio::runtime::Runtime::new() {
|
||||
Ok(runtime) => runtime,
|
||||
@@ -185,7 +183,7 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(error) = runtime.block_on(proxy::run(stats, lan_mode, port)) {
|
||||
if let Err(error) = runtime.block_on(proxy::run(stats, listen)) {
|
||||
push_log(&logs, format!("Ошибка подключения: {error}"), true);
|
||||
}
|
||||
});
|
||||
@@ -202,28 +200,8 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
||||
.unwrap_or_else(|| "Не удалось запустить прокси".into()));
|
||||
}
|
||||
|
||||
state.log(
|
||||
format!(
|
||||
"Прокси запущен на {}:{}",
|
||||
if settings.lan_mode {
|
||||
"0.0.0.0"
|
||||
} else {
|
||||
"127.0.0.1"
|
||||
},
|
||||
settings.port
|
||||
),
|
||||
false,
|
||||
);
|
||||
let host = if settings.lan_mode {
|
||||
local_ip().unwrap_or_else(|| "127.0.0.1".into())
|
||||
} else {
|
||||
"127.0.0.1".into()
|
||||
};
|
||||
let _ = open::that(format!(
|
||||
"tg://proxy?server={host}&port={}&secret={}",
|
||||
settings.port,
|
||||
state.stats.telegram_secret()
|
||||
));
|
||||
state.log(format!("Прокси запущен на {}", listen.addr), false);
|
||||
let _ = open::that(listen.telegram_link(&state.stats.telegram_secret()));
|
||||
state.log("Открываю подключение в Telegram…", false);
|
||||
Ok(state.snapshot())
|
||||
}
|
||||
@@ -244,12 +222,6 @@ fn push_log(logs: &Arc<Mutex<Vec<LogLine>>>, message: String, error: bool) {
|
||||
});
|
||||
}
|
||||
|
||||
fn local_ip() -> Option<String> {
|
||||
let socket = std::net::UdpSocket::bind("0.0.0.0:0").ok()?;
|
||||
socket.connect("8.8.8.8:80").ok()?;
|
||||
Some(socket.local_addr().ok()?.ip().to_string())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
|
||||
Reference in New Issue
Block a user