mirror of
https://github.com/by-sonic/tglock.git
synced 2026-09-10 04:24:11 +03:00
55653ed0bc
Реализует направление 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>
136 lines
4.3 KiB
Rust
136 lines
4.3 KiB
Rust
//! Listener configuration shared by the GUI and the CLI.
|
|
|
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
|
|
/// Where the local proxy listens and whether it is allowed to relay anything
|
|
/// other than Telegram.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct ListenConfig {
|
|
pub addr: SocketAddr,
|
|
/// Relay non-Telegram destinations as a plain SOCKS5 proxy.
|
|
///
|
|
/// Loopback listeners get this for free because only local processes can
|
|
/// reach them. A listener the network can reach must opt in explicitly, so
|
|
/// that sharing TGLock across a flat never silently turns the machine into
|
|
/// an open SOCKS5 relay.
|
|
pub allow_direct: bool,
|
|
}
|
|
|
|
impl ListenConfig {
|
|
/// Listener with the default policy for the given address.
|
|
pub fn new(ip: IpAddr, port: u16) -> Self {
|
|
Self {
|
|
addr: SocketAddr::new(ip, port),
|
|
allow_direct: ip.is_loopback(),
|
|
}
|
|
}
|
|
|
|
/// `127.0.0.1` — only this machine, non-Telegram traffic relayed.
|
|
pub fn loopback(port: u16) -> Self {
|
|
Self::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)
|
|
}
|
|
|
|
/// `0.0.0.0` — reachable from the local network, Telegram destinations only.
|
|
pub fn lan(port: u16) -> Self {
|
|
Self::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port)
|
|
}
|
|
|
|
/// Override the direct-relay policy. Used by `--allow-direct`.
|
|
pub fn with_allow_direct(mut self, allow_direct: bool) -> Self {
|
|
self.allow_direct = allow_direct;
|
|
self
|
|
}
|
|
|
|
/// Host to advertise in a `tg://proxy` link for this listener.
|
|
///
|
|
/// A wildcard bind is not a usable destination, so it is resolved to the
|
|
/// address this machine uses to reach the network.
|
|
pub fn advertised_host(&self) -> String {
|
|
let ip = self.addr.ip();
|
|
if ip.is_unspecified() {
|
|
outbound_ip().unwrap_or_else(|| Ipv4Addr::LOCALHOST.to_string())
|
|
} else {
|
|
ip.to_string()
|
|
}
|
|
}
|
|
|
|
/// `tg://proxy` link that points Telegram at this listener.
|
|
pub fn telegram_link(&self, secret: &str) -> String {
|
|
format!(
|
|
"tg://proxy?server={}&port={}&secret={}",
|
|
self.advertised_host(),
|
|
self.addr.port(),
|
|
secret
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Local address of the interface that reaches the default route.
|
|
///
|
|
/// No packet is sent: connecting a UDP socket only makes the OS pick a route.
|
|
fn outbound_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())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn loopback_relays_direct_traffic() {
|
|
let config = ListenConfig::loopback(1080);
|
|
assert_eq!(config.addr.to_string(), "127.0.0.1:1080");
|
|
assert!(config.allow_direct);
|
|
}
|
|
|
|
#[test]
|
|
fn lan_restricts_to_telegram_by_default() {
|
|
let config = ListenConfig::lan(1080);
|
|
assert_eq!(config.addr.to_string(), "0.0.0.0:1080");
|
|
assert!(!config.allow_direct);
|
|
}
|
|
|
|
#[test]
|
|
fn any_routable_address_restricts_to_telegram() {
|
|
for ip in ["192.168.1.10", "10.0.0.5", "::"] {
|
|
let config = ListenConfig::new(ip.parse().unwrap(), 1080);
|
|
assert!(
|
|
!config.allow_direct,
|
|
"{ip} must not relay non-Telegram traffic without an explicit opt-in"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ipv6_loopback_is_treated_as_local() {
|
|
let config = ListenConfig::new("::1".parse().unwrap(), 1080);
|
|
assert!(config.allow_direct);
|
|
assert_eq!(config.addr.to_string(), "[::1]:1080");
|
|
}
|
|
|
|
#[test]
|
|
fn allow_direct_override_is_explicit_in_both_directions() {
|
|
assert!(ListenConfig::lan(1080).with_allow_direct(true).allow_direct);
|
|
assert!(
|
|
!ListenConfig::loopback(1080)
|
|
.with_allow_direct(false)
|
|
.allow_direct
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn link_uses_concrete_host_and_port() {
|
|
let link = ListenConfig::new("192.168.1.10".parse().unwrap(), 1443).telegram_link("ddaa");
|
|
assert_eq!(link, "tg://proxy?server=192.168.1.10&port=1443&secret=ddaa");
|
|
}
|
|
|
|
#[test]
|
|
fn wildcard_bind_never_advertises_itself() {
|
|
let host = ListenConfig::lan(1080).advertised_host();
|
|
assert_ne!(host, "0.0.0.0");
|
|
assert!(!host.is_empty());
|
|
}
|
|
}
|