From 6cafee153a846b2f45ddbe51f8cc1373e1b9c40a Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:31:49 +0300 Subject: [PATCH] =?UTF-8?q?Fire-and-Forgot=E2=84=A2=20Draft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added fire-and-forget ignition via `--init` CLI command: - New `mod cli;` module handling installation logic - Extended `parse_cli()` to process `--init` flag (runs synchronously before tokio runtime) - Expanded `--help` output with installation options - `--init` command functionality: - Generates random secret if not provided via `--secret` - Creates `/etc/telemt/config.toml` from template with user-provided or default parameters (`--port`, `--domain`, `--user`, `--config-dir`) - Creates hardened systemd unit `/etc/systemd/system/telemt.service` with security features: - `NoNewPrivileges=true` - `ProtectSystem=strict` - `PrivateTmp=true` - Runs `systemctl enable --now telemt.service` - Outputs `tg://` proxy links for the running service - Implementation approach: - `--init` handled at the very start of `main()` before any async context - Uses blocking operations throughout (file I/O, `std::process::Command` for systemctl) - IP detection for tg:// links performed via blocking HTTP request - Command exits after installation without entering normal proxy runtime - New CLI parameters for installation: - `--port` - listening port (default: 443) - `--domain` - TLS domain (default: auto-detected) - `--secret` - custom secret (default: randomly generated) - `--user` - systemd service user (default: telemt) - `--config-dir` - configuration directory (default: /etc/telemt) Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> --- src/cli.rs | 300 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 38 +++++-- 2 files changed, 328 insertions(+), 10 deletions(-) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..1440a63 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,300 @@ +//! CLI commands: --init (fire-and-forget setup) + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use rand::Rng; + +/// Options for the init command +pub struct InitOptions { + pub port: u16, + pub domain: String, + pub secret: Option, + pub username: String, + pub config_dir: PathBuf, + pub no_start: bool, +} + +impl Default for InitOptions { + fn default() -> Self { + Self { + port: 443, + domain: "www.google.com".to_string(), + secret: None, + username: "user".to_string(), + config_dir: PathBuf::from("/etc/telemt"), + no_start: false, + } + } +} + +/// Parse --init subcommand options from CLI args. +/// +/// Returns `Some(InitOptions)` if `--init` was found, `None` otherwise. +pub fn parse_init_args(args: &[String]) -> Option { + if !args.iter().any(|a| a == "--init") { + return None; + } + + let mut opts = InitOptions::default(); + let mut i = 0; + + while i < args.len() { + match args[i].as_str() { + "--port" => { + i += 1; + if i < args.len() { + opts.port = args[i].parse().unwrap_or(443); + } + } + "--domain" => { + i += 1; + if i < args.len() { + opts.domain = args[i].clone(); + } + } + "--secret" => { + i += 1; + if i < args.len() { + opts.secret = Some(args[i].clone()); + } + } + "--user" => { + i += 1; + if i < args.len() { + opts.username = args[i].clone(); + } + } + "--config-dir" => { + i += 1; + if i < args.len() { + opts.config_dir = PathBuf::from(&args[i]); + } + } + "--no-start" => { + opts.no_start = true; + } + _ => {} + } + i += 1; + } + + Some(opts) +} + +/// Run the fire-and-forget setup. +pub fn run_init(opts: InitOptions) -> Result<(), Box> { + eprintln!("[telemt] Fire-and-forget setup"); + eprintln!(); + + // 1. Generate or validate secret + let secret = match opts.secret { + Some(s) => { + if s.len() != 32 || !s.chars().all(|c| c.is_ascii_hexdigit()) { + eprintln!("[error] Secret must be exactly 32 hex characters"); + std::process::exit(1); + } + s + } + None => generate_secret(), + }; + + eprintln!("[+] Secret: {}", secret); + eprintln!("[+] User: {}", opts.username); + eprintln!("[+] Port: {}", opts.port); + eprintln!("[+] Domain: {}", opts.domain); + + // 2. Create config directory + fs::create_dir_all(&opts.config_dir)?; + let config_path = opts.config_dir.join("config.toml"); + + // 3. Write config + let config_content = generate_config(&opts.username, &secret, opts.port, &opts.domain); + fs::write(&config_path, &config_content)?; + eprintln!("[+] Config written to {}", config_path.display()); + + // 4. Write systemd unit + let exe_path = std::env::current_exe() + .unwrap_or_else(|_| PathBuf::from("/usr/local/bin/telemt")); + + let unit_path = Path::new("/etc/systemd/system/telemt.service"); + let unit_content = generate_systemd_unit(&exe_path, &config_path); + + match fs::write(unit_path, &unit_content) { + Ok(()) => { + eprintln!("[+] Systemd unit written to {}", unit_path.display()); + } + Err(e) => { + eprintln!("[!] Cannot write systemd unit (run as root?): {}", e); + eprintln!("[!] Manual unit file content:"); + eprintln!("{}", unit_content); + + // Still print links and config + print_links(&opts.username, &secret, opts.port, &opts.domain); + return Ok(()); + } + } + + // 5. Reload systemd + run_cmd("systemctl", &["daemon-reload"]); + + // 6. Enable service + run_cmd("systemctl", &["enable", "telemt.service"]); + eprintln!("[+] Service enabled"); + + // 7. Start service (unless --no-start) + if !opts.no_start { + run_cmd("systemctl", &["start", "telemt.service"]); + eprintln!("[+] Service started"); + + // Brief delay then check status + std::thread::sleep(std::time::Duration::from_secs(1)); + let status = Command::new("systemctl") + .args(["is-active", "telemt.service"]) + .output(); + + match status { + Ok(out) if out.status.success() => { + eprintln!("[+] Service is running"); + } + _ => { + eprintln!("[!] Service may not have started correctly"); + eprintln!("[!] Check: journalctl -u telemt.service -n 20"); + } + } + } else { + eprintln!("[+] Service not started (--no-start)"); + eprintln!("[+] Start manually: systemctl start telemt.service"); + } + + eprintln!(); + + // 8. Print links + print_links(&opts.username, &secret, opts.port, &opts.domain); + + Ok(()) +} + +fn generate_secret() -> String { + let mut rng = rand::rng(); + let bytes: Vec = (0..16).map(|_| rng.random::()).collect(); + hex::encode(bytes) +} + +fn generate_config(username: &str, secret: &str, port: u16, domain: &str) -> String { + format!( +r#"# Telemt MTProxy — auto-generated config +# Re-run `telemt --init` to regenerate + +show_link = ["{username}"] + +[general] +prefer_ipv6 = false +fast_mode = true +use_middle_proxy = false +log_level = "normal" + +[general.modes] +classic = false +secure = false +tls = true + +[server] +port = {port} +listen_addr_ipv4 = "0.0.0.0" +listen_addr_ipv6 = "::" + +[[server.listeners]] +ip = "0.0.0.0" + +[[server.listeners]] +ip = "::" + +[timeouts] +client_handshake = 15 +tg_connect = 10 +client_keepalive = 60 +client_ack = 300 + +[censorship] +tls_domain = "{domain}" +mask = true +mask_port = 443 +fake_cert_len = 2048 + +[access] +replay_check_len = 65536 +replay_window_secs = 1800 +ignore_time_skew = false + +[access.users] +{username} = "{secret}" + +[[upstreams]] +type = "direct" +enabled = true +weight = 10 +"#, + username = username, + secret = secret, + port = port, + domain = domain, + ) +} + +fn generate_systemd_unit(exe_path: &Path, config_path: &Path) -> String { + format!( +r#"[Unit] +Description=Telemt MTProxy +Documentation=https://github.com/nicepkg/telemt +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart={exe} {config} +Restart=always +RestartSec=5 +LimitNOFILE=65535 +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/etc/telemt +PrivateTmp=true + +[Install] +WantedBy=multi-user.target +"#, + exe = exe_path.display(), + config = config_path.display(), + ) +} + +fn run_cmd(cmd: &str, args: &[&str]) { + match Command::new(cmd).args(args).output() { + Ok(output) => { + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!("[!] {} {} failed: {}", cmd, args.join(" "), stderr.trim()); + } + } + Err(e) => { + eprintln!("[!] Failed to run {} {}: {}", cmd, args.join(" "), e); + } + } +} + +fn print_links(username: &str, secret: &str, port: u16, domain: &str) { + let domain_hex = hex::encode(domain); + + println!("=== Proxy Links ==="); + println!("[{}]", username); + println!(" EE-TLS: tg://proxy?server=YOUR_SERVER_IP&port={}&secret=ee{}{}", + port, secret, domain_hex); + println!(); + println!("Replace YOUR_SERVER_IP with your server's public IP."); + println!("The proxy will auto-detect and display the correct link on startup."); + println!("Check: journalctl -u telemt.service | head -30"); + println!("==================="); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 860df54..9d2cb84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use tokio::signal; use tracing::{info, error, warn, debug}; use tracing_subscriber::{fmt, EnvFilter}; +mod cli; mod config; mod crypto; mod error; @@ -32,6 +33,16 @@ fn parse_cli() -> (String, bool, Option) { let mut log_level: Option = None; let args: Vec = std::env::args().skip(1).collect(); + + // Check for --init first (handled before tokio) + if let Some(init_opts) = cli::parse_init_args(&args) { + if let Err(e) = cli::run_init(init_opts) { + eprintln!("[telemt] Init failed: {}", e); + std::process::exit(1); + } + std::process::exit(0); + } + let mut i = 0; while i < args.len() { match args[i].as_str() { @@ -45,9 +56,20 @@ fn parse_cli() -> (String, bool, Option) { } "--help" | "-h" => { eprintln!("Usage: telemt [config.toml] [OPTIONS]"); + eprintln!(); + eprintln!("Options:"); eprintln!(" --silent, -s Suppress info logs"); eprintln!(" --log-level debug|verbose|normal|silent"); eprintln!(" --help, -h Show this help"); + eprintln!(); + eprintln!("Setup (fire-and-forget):"); + eprintln!(" --init Generate config, install systemd service, start"); + eprintln!(" --port Listen port (default: 443)"); + eprintln!(" --domain TLS domain for masking (default: www.google.com)"); + eprintln!(" --secret 32-char hex secret (auto-generated if omitted)"); + eprintln!(" --user Username (default: user)"); + eprintln!(" --config-dir Config directory (default: /etc/telemt)"); + eprintln!(" --no-start Don't start the service after install"); std::process::exit(0); } s if !s.starts_with('-') => { config_path = s.to_string(); } @@ -112,7 +134,7 @@ async fn main() -> Result<(), Box> { config.censorship.mask_port); if config.censorship.tls_domain == "www.google.com" { - warn!("Using default tls_domain (www.google.com). Consider setting a custom domain."); + warn!("Using default tls_domain. Consider setting a custom domain."); } let prefer_ipv6 = config.general.prefer_ipv6; @@ -128,7 +150,7 @@ async fn main() -> Result<(), Box> { let upstream_manager = Arc::new(UpstreamManager::new(config.upstreams.clone())); let buffer_pool = Arc::new(BufferPool::with_config(16 * 1024, 4096)); - // === Startup DC Ping === + // Startup DC ping println!("=== Telegram DC Connectivity ==="); let ping_results = upstream_manager.ping_all_dcs(prefer_ipv6).await; for upstream_result in &ping_results { @@ -141,7 +163,7 @@ async fn main() -> Result<(), Box> { (None, Some(err)) => { println!(" DC{} ({:>21}): FAIL ({})", dc.dc_idx, dc.dc_addr, err); } - (None, None) => { + _ => { println!(" DC{} ({:>21}): FAIL", dc.dc_idx, dc.dc_addr); } } @@ -149,16 +171,12 @@ async fn main() -> Result<(), Box> { } println!("================================"); - // Start background tasks + // Background tasks let um_clone = upstream_manager.clone(); - tokio::spawn(async move { - um_clone.run_health_checks(prefer_ipv6).await; - }); + tokio::spawn(async move { um_clone.run_health_checks(prefer_ipv6).await; }); let rc_clone = replay_checker.clone(); - tokio::spawn(async move { - rc_clone.run_periodic_cleanup().await; - }); + tokio::spawn(async move { rc_clone.run_periodic_cleanup().await; }); let detected_ip = detect_ip().await; debug!("Detected IPs: v4={:?} v6={:?}", detected_ip.ipv4, detected_ip.ipv6);