diff --git a/Cargo.lock b/Cargo.lock index 6054188..38047bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1066,6 +1066,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1723,7 +1742,7 @@ dependencies = [ [[package]] name = "telemt" -version = "1.2.0" +version = "3.0.0" dependencies = [ "aes", "base64", @@ -1741,6 +1760,8 @@ dependencies = [ "libc", "lru", "md-5", + "num-bigint", + "num-traits", "parking_lot", "proptest", "rand", diff --git a/README.md b/README.md index b1a20e2..de744a3 100644 --- a/README.md +++ b/README.md @@ -208,10 +208,6 @@ then Ctrl+X -> Y -> Enter to save ## Configuration ### Minimal Configuration for First Start ```toml -# === UI === -# Users to show in the startup log (tg:// links) -show_link = ["hello"] - # === General Settings === [general] prefer_ipv6 = false @@ -240,6 +236,12 @@ ip = "0.0.0.0" [[server.listeners]] ip = "::" +# Users to show in the startup log (tg:// links) +[general.links] +show = ["hello"] # Users to show in the startup log (tg:// links) +# public_host = "proxy.example.com" # Host (IP or domain) for tg:// links +# public_port = 443 # Port for tg:// links (default: server.port) + # === Timeouts (in seconds) === [timeouts] client_handshake = 15 @@ -287,6 +289,10 @@ weight = 10 # address = "127.0.0.1:9050" # enabled = false # weight = 1 + +# === DC Address Overrides === +# [dc_overrides] +# "203" = "91.105.192.100:443" ``` ### Advanced #### Adtag diff --git a/config.toml b/config.toml index 0f5d438..0cddaac 100644 --- a/config.toml +++ b/config.toml @@ -1,7 +1,3 @@ -# === UI === -# Users to show in the startup log (tg:// links) -show_link = ["hello"] - # === General Settings === [general] prefer_ipv6 = true @@ -24,6 +20,8 @@ tls = true port = 443 listen_addr_ipv4 = "0.0.0.0" listen_addr_ipv6 = "::" +# listen_unix_sock = "/var/run/telemt.sock" # Unix socket +# listen_unix_sock_perm = "0666" # Socket file permissions # metrics_port = 9090 # metrics_whitelist = ["127.0.0.1", "::1"] @@ -35,6 +33,12 @@ ip = "0.0.0.0" [[server.listeners]] ip = "::" +# Users to show in the startup log (tg:// links) +[general.links] +show = ["hello"] # Users to show in the startup log (tg:// links) +# public_host = "proxy.example.com" # Host (IP or domain) for tg:// links +# public_port = 443 # Port for tg:// links (default: server.port) + # === Timeouts (in seconds) === [timeouts] client_handshake = 15 @@ -65,7 +69,7 @@ hello = "00000000000000000000000000000000" # hello = 50 # [access.user_max_unique_ips] -# hello = 5 +# hello = 5 # [access.user_data_quota] # hello = 1073741824 # 1 GB @@ -80,4 +84,8 @@ weight = 10 # type = "socks5" # address = "127.0.0.1:1080" # enabled = false -# weight = 1 \ No newline at end of file +# weight = 1 + +# === DC Address Overrides === +# [dc_overrides] +# "203" = "91.105.192.100:443" diff --git a/src/config/mod.rs b/src/config/mod.rs index 4b4d7e6..42190e6 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -256,6 +256,27 @@ pub struct GeneralConfig { /// Disable colored output in logs (useful for files/systemd) #[serde(default)] pub disable_colors: bool, + + /// [general.links] — proxy link generation overrides + #[serde(default)] + pub links: LinksConfig, +} + +/// `[general.links]` — proxy link generation settings. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct LinksConfig { + /// List of usernames whose tg:// links to display at startup. + /// `"*"` = all users, `["alice", "bob"]` = specific users. + #[serde(default)] + pub show: ShowLink, + + /// Public hostname/IP for tg:// link generation (overrides detected IP). + #[serde(default)] + pub public_host: Option, + + /// Public port for tg:// link generation (overrides server.port). + #[serde(default)] + pub public_port: Option, } impl Default for GeneralConfig { @@ -274,6 +295,7 @@ impl Default for GeneralConfig { unknown_dc_log_path: default_unknown_dc_log_path(), log_level: LogLevel::Normal, disable_colors: false, + links: LinksConfig::default(), } } } @@ -283,8 +305,8 @@ pub struct ServerConfig { #[serde(default = "default_port")] pub port: u16, - #[serde(default = "default_listen_addr")] - pub listen_addr_ipv4: String, + #[serde(default)] + pub listen_addr_ipv4: Option, #[serde(default)] pub listen_addr_ipv6: Option, @@ -292,6 +314,16 @@ pub struct ServerConfig { #[serde(default)] pub listen_unix_sock: Option, + /// Unix socket file permissions (octal, e.g. "0666" or "0777"). + /// Applied via chmod after bind. Default: no change (inherits umask). + #[serde(default)] + pub listen_unix_sock_perm: Option, + + /// Enable TCP listening. Default: true when no unix socket, false when + /// listen_unix_sock is set. Set explicitly to override auto-detection. + #[serde(default)] + pub listen_tcp: Option, + #[serde(default)] pub metrics_port: Option, @@ -306,9 +338,11 @@ impl Default for ServerConfig { fn default() -> Self { Self { port: default_port(), - listen_addr_ipv4: default_listen_addr(), + listen_addr_ipv4: Some(default_listen_addr()), listen_addr_ipv6: Some("::".to_string()), listen_unix_sock: None, + listen_unix_sock_perm: None, + listen_tcp: None, metrics_port: None, metrics_whitelist: default_metrics_whitelist(), listeners: Vec::new(), @@ -661,9 +695,25 @@ impl ProxyConfig { use rand::Rng; config.censorship.fake_cert_len = rand::rng().gen_range(1024..4096); - // Migration: Populate listeners if empty - if config.server.listeners.is_empty() { - if let Ok(ipv4) = config.server.listen_addr_ipv4.parse::() { + // Resolve listen_tcp: explicit value wins, otherwise auto-detect. + // If unix socket is set → TCP only when listen_addr_ipv4 or listeners are explicitly provided. + // If no unix socket → TCP always (backward compat). + let listen_tcp = config.server.listen_tcp.unwrap_or_else(|| { + if config.server.listen_unix_sock.is_some() { + // Unix socket present: TCP only if user explicitly set addresses or listeners + config.server.listen_addr_ipv4.is_some() + || !config.server.listeners.is_empty() + } else { + true + } + }); + + // Migration: Populate listeners if empty (skip when listen_tcp = false) + if config.server.listeners.is_empty() && listen_tcp { + let ipv4_str = config.server.listen_addr_ipv4 + .as_deref() + .unwrap_or("0.0.0.0"); + if let Ok(ipv4) = ipv4_str.parse::() { config.server.listeners.push(ListenerConfig { ip: ipv4, announce_ip: None, @@ -679,6 +729,11 @@ impl ProxyConfig { } } + // Migration: show_link (top-level) → general.links.show + if !config.show_link.is_empty() && config.general.links.show.is_empty() { + config.general.links.show = config.show_link.clone(); + } + // Migration: Populate upstreams if empty (Default Direct) if config.upstreams.is_empty() { config.upstreams.push(UpstreamConfig { diff --git a/src/main.rs b/src/main.rs index 0691bb2..48bd45f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,8 @@ use tokio::signal; use tokio::sync::Semaphore; use tracing::{debug, error, info, warn}; use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload}; +#[cfg(unix)] +use tokio::net::UnixListener; mod cli; mod config; @@ -100,6 +102,37 @@ fn parse_cli() -> (String, bool, Option) { (config_path, silent, log_level) } +fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) { + info!("--- Proxy Links ({}) ---", host); + for user_name in config.general.links.show.resolve_users(&config.access.users) { + if let Some(secret) = config.access.users.get(user_name) { + info!("User: {}", user_name); + if config.general.modes.classic { + info!( + " Classic: tg://proxy?server={}&port={}&secret={}", + host, port, secret + ); + } + if config.general.modes.secure { + info!( + " DD: tg://proxy?server={}&port={}&secret=dd{}", + host, port, secret + ); + } + if config.general.modes.tls { + let domain_hex = hex::encode(&config.censorship.tls_domain); + info!( + " EE-TLS: tg://proxy?server={}&port={}&secret=ee{}{}", + host, port, secret, domain_hex + ); + } + } else { + warn!("User '{}' in show_link not found", user_name); + } + } + info!("------------------------"); +} + #[tokio::main] async fn main() -> std::result::Result<(), Box> { let (config_path, cli_silent, cli_log_level) = parse_cli(); @@ -476,35 +509,10 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai listener_conf.ip }; - if !config.show_link.is_empty() { - info!("--- Proxy Links ({}) ---", public_ip); - for user_name in config.show_link.resolve_users(&config.access.users) { - if let Some(secret) = config.access.users.get(user_name) { - info!("User: {}", user_name); - if config.general.modes.classic { - info!( - " Classic: tg://proxy?server={}&port={}&secret={}", - public_ip, config.server.port, secret - ); - } - if config.general.modes.secure { - info!( - " DD: tg://proxy?server={}&port={}&secret=dd{}", - public_ip, config.server.port, secret - ); - } - if config.general.modes.tls { - let domain_hex = hex::encode(&config.censorship.tls_domain); - info!( - " EE-TLS: tg://proxy?server={}&port={}&secret=ee{}{}", - public_ip, config.server.port, secret, domain_hex - ); - } - } else { - warn!("User '{}' in show_link not found", user_name); - } - } - info!("------------------------"); + // Show per-listener proxy links only when public_host is not set + if config.general.links.public_host.is_none() && !config.general.links.show.is_empty() { + let link_port = config.general.links.public_port.unwrap_or(config.server.port); + print_proxy_links(&public_ip.to_string(), link_port, &config); } listeners.push(listener); @@ -515,7 +523,104 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai } } - if listeners.is_empty() { + // Show proxy links once when public_host is set, OR when there are no TCP listeners + // (unix-only mode) — use detected IP as fallback + if !config.general.links.show.is_empty() && (config.general.links.public_host.is_some() || listeners.is_empty()) { + let (host, port) = if let Some(ref h) = config.general.links.public_host { + (h.clone(), config.general.links.public_port.unwrap_or(config.server.port)) + } else { + let ip = detected_ip + .ipv4 + .or(detected_ip.ipv6) + .map(|ip| ip.to_string()); + if ip.is_none() { + warn!("show_link is configured but public IP could not be detected. Set public_host in config."); + } + (ip.unwrap_or_else(|| "UNKNOWN".to_string()), config.general.links.public_port.unwrap_or(config.server.port)) + }; + + print_proxy_links(&host, port, &config); + } + + // Unix socket setup (before listeners check so unix-only config works) + let mut has_unix_listener = false; + #[cfg(unix)] + if let Some(ref unix_path) = config.server.listen_unix_sock { + // Remove stale socket file if present (standard practice) + let _ = tokio::fs::remove_file(unix_path).await; + + let unix_listener = UnixListener::bind(unix_path)?; + + // Apply socket permissions if configured + if let Some(ref perm_str) = config.server.listen_unix_sock_perm { + match u32::from_str_radix(perm_str.trim_start_matches('0'), 8) { + Ok(mode) => { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::Permissions::from_mode(mode); + if let Err(e) = std::fs::set_permissions(unix_path, perms) { + error!("Failed to set unix socket permissions to {}: {}", perm_str, e); + } else { + info!("Listening on unix:{} (mode {})", unix_path, perm_str); + } + } + Err(e) => { + warn!("Invalid listen_unix_sock_perm '{}': {}. Ignoring.", perm_str, e); + info!("Listening on unix:{}", unix_path); + } + } + } else { + info!("Listening on unix:{}", unix_path); + } + + has_unix_listener = true; + + let config = config.clone(); + let stats = stats.clone(); + let upstream_manager = upstream_manager.clone(); + let replay_checker = replay_checker.clone(); + let buffer_pool = buffer_pool.clone(); + let rng = rng.clone(); + let me_pool = me_pool.clone(); + let ip_tracker = ip_tracker.clone(); + + tokio::spawn(async move { + let unix_conn_counter = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)); + + loop { + match unix_listener.accept().await { + Ok((stream, _)) => { + let conn_id = unix_conn_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let fake_peer = SocketAddr::from(([127, 0, 0, 1], (conn_id % 65535) as u16)); + + let config = config.clone(); + let stats = stats.clone(); + let upstream_manager = upstream_manager.clone(); + let replay_checker = replay_checker.clone(); + let buffer_pool = buffer_pool.clone(); + let rng = rng.clone(); + let me_pool = me_pool.clone(); + let ip_tracker = ip_tracker.clone(); + + tokio::spawn(async move { + if let Err(e) = crate::proxy::client::handle_client_stream( + stream, fake_peer, config, stats, + upstream_manager, replay_checker, buffer_pool, rng, + me_pool, ip_tracker, + ).await { + debug!(error = %e, "Unix socket connection error"); + } + }); + } + Err(e) => { + error!("Unix socket accept error: {}", e); + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + } + }); + } + + if listeners.is_empty() && !has_unix_listener { error!("No listeners. Exiting."); std::process::exit(1); } @@ -582,62 +687,6 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai }); } - #[cfg(unix)] - if let Some(ref unix_path) = config.server.listen_unix_sock { - use tokio::net::UnixListener; // ← добавь импорт, если его нет выше - - // Удаляем старые файлы сокета, если они есть (стандартная практика) - let _ = tokio::fs::remove_file(unix_path).await; - - let unix_listener = UnixListener::bind(unix_path)?; - info!("Listening on unix:{}", unix_path); - - let config = config.clone(); - let stats = stats.clone(); - let upstream_manager = upstream_manager.clone(); - let replay_checker = replay_checker.clone(); - let buffer_pool = buffer_pool.clone(); - let rng = rng.clone(); - let me_pool = me_pool.clone(); - let ip_tracker = ip_tracker.clone(); - - tokio::spawn(async move { - let unix_conn_counter = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)); - - loop { - match unix_listener.accept().await { - Ok((stream, _)) => { - let conn_id = unix_conn_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let fake_peer = SocketAddr::from(([127, 0, 0, 1], (conn_id % 65535) as u16)); // безопасный порт - - let config = config.clone(); - let stats = stats.clone(); - let upstream_manager = upstream_manager.clone(); - let replay_checker = replay_checker.clone(); - let buffer_pool = buffer_pool.clone(); - let rng = rng.clone(); - let me_pool = me_pool.clone(); - let ip_tracker = ip_tracker.clone(); - - tokio::spawn(async move { - if let Err(e) = crate::proxy::client::handle_client_stream( - stream, fake_peer, config, stats, - upstream_manager, replay_checker, buffer_pool, rng, - me_pool, ip_tracker, - ).await { - debug!(error = %e, "Unix socket connection error"); - } - }); - } - Err(e) => { - error!("Unix socket accept error: {}", e); - tokio::time::sleep(Duration::from_millis(100)).await; - } - } - } - }); - } - match signal::ctrl_c().await { Ok(()) => info!("Shutting down..."), Err(e) => error!("Signal error: {}", e),