From 909714af318c45edb3b6714edc7428a364e8a553 Mon Sep 17 00:00:00 2001 From: Vladimir Krivopalov Date: Fri, 20 Mar 2026 22:13:21 +0200 Subject: [PATCH] Add multi-platform service manager integration Implement automatic init system detection and service file generation for systemd, OpenRC (Alpine/Gentoo), and FreeBSD rc.d: - Add src/service module with init system detection and generators - Auto-detect init system via filesystem probes - Generate platform-appropriate service files during --init systemd enhancements: - ExecReload for SIGHUP config reload - PIDFile directive - Comprehensive security hardening (ProtectKernelTunables, RestrictAddressFamilies, MemoryDenyWriteExecute, etc.) - CAP_NET_BIND_SERVICE for privileged ports OpenRC support: - Standard openrc-run script with depend/reload functions - Directory setup in start_pre FreeBSD rc.d support: - rc.subr integration with rc.conf variables - reload extra command The --init command now detects the init system and runs the appropriate enable/start commands (systemctl, rc-update, sysrc). Signed-off-by: Vladimir Krivopalov --- src/cli.rs | 169 +++++++++++--------- src/main.rs | 1 + src/service/mod.rs | 374 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 475 insertions(+), 69 deletions(-) create mode 100644 src/service/mod.rs diff --git a/src/cli.rs b/src/cli.rs index bdfb629..bc38909 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -401,10 +401,16 @@ pub fn parse_init_args(args: &[String]) -> Option { /// Run the fire-and-forget setup. pub fn run_init(opts: InitOptions) -> Result<(), Box> { + use crate::service::{self, InitSystem, ServiceOptions}; + eprintln!("[telemt] Fire-and-forget setup"); eprintln!(); - // 1. Generate or validate secret + // 1. Detect init system + let init_system = service::detect_init_system(); + eprintln!("[+] Detected init system: {}", init_system); + + // 2. Generate or validate secret let secret = match opts.secret { Some(s) => { if s.len() != 32 || !s.chars().all(|c| c.is_ascii_hexdigit()) { @@ -421,72 +427,126 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box> { eprintln!("[+] Port: {}", opts.port); eprintln!("[+] Domain: {}", opts.domain); - // 2. Create config directory + // 3. Create config directory fs::create_dir_all(&opts.config_dir)?; let config_path = opts.config_dir.join("config.toml"); - // 3. Write config + // 4. 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")); + // 5. Generate and write service file + 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); + let service_opts = ServiceOptions { + exe_path: &exe_path, + config_path: &config_path, + user: None, // Let systemd/init handle user + group: None, + pid_file: "/var/run/telemt.pid", + working_dir: Some("/var/lib/telemt"), + description: "Telemt MTProxy - Telegram MTProto Proxy", + }; - match fs::write(unit_path, &unit_content) { + let service_path = service::service_file_path(init_system); + let service_content = service::generate_service_file(init_system, &service_opts); + + // Ensure parent directory exists + if let Some(parent) = Path::new(service_path).parent() { + let _ = fs::create_dir_all(parent); + } + + match fs::write(service_path, &service_content) { Ok(()) => { - eprintln!("[+] Systemd unit written to {}", unit_path.display()); + eprintln!("[+] Service file written to {}", service_path); + + // Make script executable for OpenRC/FreeBSD + #[cfg(unix)] + if init_system == InitSystem::OpenRC || init_system == InitSystem::FreeBSDRc { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(service_path)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(service_path, perms)?; + } } Err(e) => { - eprintln!("[!] Cannot write systemd unit (run as root?): {}", e); - eprintln!("[!] Manual unit file content:"); - eprintln!("{}", unit_content); + eprintln!("[!] Cannot write service file (run as root?): {}", e); + eprintln!("[!] Manual service file content:"); + eprintln!("{}", service_content); - // Still print links and config + // Still print links and installation instructions + eprintln!(); + eprintln!("{}", service::installation_instructions(init_system)); print_links(&opts.username, &secret, opts.port, &opts.domain); return Ok(()); } } - // 5. Reload systemd - run_cmd("systemctl", &["daemon-reload"]); + // 6. Install and enable service based on init system + match init_system { + InitSystem::Systemd => { + run_cmd("systemctl", &["daemon-reload"]); + run_cmd("systemctl", &["enable", "telemt.service"]); + eprintln!("[+] Service enabled"); - // 6. Enable service - run_cmd("systemctl", &["enable", "telemt.service"]); - eprintln!("[+] Service enabled"); + if !opts.no_start { + run_cmd("systemctl", &["start", "telemt.service"]); + eprintln!("[+] Service started"); - // 7. Start service (unless --no-start) - if !opts.no_start { - run_cmd("systemctl", &["start", "telemt.service"]); - eprintln!("[+] Service started"); + std::thread::sleep(std::time::Duration::from_secs(1)); + let status = Command::new("systemctl") + .args(["is-active", "telemt.service"]) + .output(); - // 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"); + 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"); } } - } else { - eprintln!("[+] Service not started (--no-start)"); - eprintln!("[+] Start manually: systemctl start telemt.service"); + InitSystem::OpenRC => { + run_cmd("rc-update", &["add", "telemt", "default"]); + eprintln!("[+] Service enabled"); + + if !opts.no_start { + run_cmd("rc-service", &["telemt", "start"]); + eprintln!("[+] Service started"); + } else { + eprintln!("[+] Service not started (--no-start)"); + eprintln!("[+] Start manually: rc-service telemt start"); + } + } + InitSystem::FreeBSDRc => { + run_cmd("sysrc", &["telemt_enable=YES"]); + eprintln!("[+] Service enabled"); + + if !opts.no_start { + run_cmd("service", &["telemt", "start"]); + eprintln!("[+] Service started"); + } else { + eprintln!("[+] Service not started (--no-start)"); + eprintln!("[+] Start manually: service telemt start"); + } + } + InitSystem::Unknown => { + eprintln!("[!] Unknown init system - service file written but not installed"); + eprintln!("[!] You may need to install it manually"); + } } eprintln!(); - // 8. Print links + // 7. Print links print_links(&opts.username, &secret, opts.port, &opts.domain); Ok(()) @@ -581,35 +641,6 @@ weight = 10 ) } -fn generate_systemd_unit(exe_path: &Path, config_path: &Path) -> String { - format!( - r#"[Unit] -Description=Telemt MTProxy -Documentation=https://github.com/telemt/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) => { diff --git a/src/main.rs b/src/main.rs index 6ff3491..0e872c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod crypto; mod daemon; mod error; mod ip_tracker; +mod service; #[cfg(test)] #[path = "tests/ip_tracker_hotpath_adversarial_tests.rs"] mod ip_tracker_hotpath_adversarial_tests; diff --git a/src/service/mod.rs b/src/service/mod.rs new file mode 100644 index 0000000..c0a6f83 --- /dev/null +++ b/src/service/mod.rs @@ -0,0 +1,374 @@ +//! Service manager integration for telemt. +//! +//! Supports generating service files for: +//! - systemd (Linux) +//! - OpenRC (Alpine, Gentoo) +//! - rc.d (FreeBSD) + +use std::path::Path; + +/// Detected init/service system. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InitSystem { + /// systemd (most modern Linux distributions) + Systemd, + /// OpenRC (Alpine, Gentoo, some BSDs) + OpenRC, + /// FreeBSD rc.d + FreeBSDRc, + /// No known init system detected + Unknown, +} + +impl std::fmt::Display for InitSystem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InitSystem::Systemd => write!(f, "systemd"), + InitSystem::OpenRC => write!(f, "OpenRC"), + InitSystem::FreeBSDRc => write!(f, "FreeBSD rc.d"), + InitSystem::Unknown => write!(f, "unknown"), + } + } +} + +/// Detects the init system in use on the current host. +pub fn detect_init_system() -> InitSystem { + // Check for systemd first (most common on Linux) + if Path::new("/run/systemd/system").exists() { + return InitSystem::Systemd; + } + + // Check for OpenRC + if Path::new("/sbin/openrc-run").exists() || Path::new("/sbin/openrc").exists() { + return InitSystem::OpenRC; + } + + // Check for FreeBSD rc.d + if Path::new("/etc/rc.subr").exists() && Path::new("/etc/rc.d").exists() { + return InitSystem::FreeBSDRc; + } + + // Fallback: check if systemctl exists even without /run/systemd + if Path::new("/usr/bin/systemctl").exists() || Path::new("/bin/systemctl").exists() { + return InitSystem::Systemd; + } + + InitSystem::Unknown +} + +/// Returns the default service file path for the given init system. +pub fn service_file_path(init_system: InitSystem) -> &'static str { + match init_system { + InitSystem::Systemd => "/etc/systemd/system/telemt.service", + InitSystem::OpenRC => "/etc/init.d/telemt", + InitSystem::FreeBSDRc => "/usr/local/etc/rc.d/telemt", + InitSystem::Unknown => "/etc/init.d/telemt", + } +} + +/// Options for generating service files. +pub struct ServiceOptions<'a> { + /// Path to the telemt executable + pub exe_path: &'a Path, + /// Path to the configuration file + pub config_path: &'a Path, + /// User to run as (optional) + pub user: Option<&'a str>, + /// Group to run as (optional) + pub group: Option<&'a str>, + /// PID file path + pub pid_file: &'a str, + /// Working directory + pub working_dir: Option<&'a str>, + /// Description + pub description: &'a str, +} + +impl<'a> Default for ServiceOptions<'a> { + fn default() -> Self { + Self { + exe_path: Path::new("/usr/local/bin/telemt"), + config_path: Path::new("/etc/telemt/config.toml"), + user: Some("telemt"), + group: Some("telemt"), + pid_file: "/var/run/telemt.pid", + working_dir: Some("/var/lib/telemt"), + description: "Telemt MTProxy - Telegram MTProto Proxy", + } + } +} + +/// Generates a service file for the given init system. +pub fn generate_service_file(init_system: InitSystem, opts: &ServiceOptions) -> String { + match init_system { + InitSystem::Systemd => generate_systemd_unit(opts), + InitSystem::OpenRC => generate_openrc_script(opts), + InitSystem::FreeBSDRc => generate_freebsd_rc_script(opts), + InitSystem::Unknown => generate_systemd_unit(opts), // Default to systemd format + } +} + +/// Generates an enhanced systemd unit file. +fn generate_systemd_unit(opts: &ServiceOptions) -> String { + let user_line = opts.user.map(|u| format!("User={}", u)).unwrap_or_default(); + let group_line = opts.group.map(|g| format!("Group={}", g)).unwrap_or_default(); + let working_dir = opts.working_dir.map(|d| format!("WorkingDirectory={}", d)).unwrap_or_default(); + + format!( +r#"[Unit] +Description={description} +Documentation=https://github.com/telemt/telemt +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart={exe} --foreground --pid-file {pid_file} {config} +ExecReload=/bin/kill -HUP $MAINPID +PIDFile={pid_file} +Restart=always +RestartSec=5 +{user} +{group} +{working_dir} + +# Resource limits +LimitNOFILE=65535 +LimitNPROC=4096 + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +PrivateDevices=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=true +RestrictRealtime=true +RestrictSUIDSGID=true +MemoryDenyWriteExecute=true +LockPersonality=true + +# Allow binding to privileged ports and writing to specific paths +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +ReadWritePaths=/etc/telemt /var/run /var/lib/telemt + +[Install] +WantedBy=multi-user.target +"#, + description = opts.description, + exe = opts.exe_path.display(), + config = opts.config_path.display(), + pid_file = opts.pid_file, + user = user_line, + group = group_line, + working_dir = working_dir, + ) +} + +/// Generates an OpenRC init script. +fn generate_openrc_script(opts: &ServiceOptions) -> String { + let user = opts.user.unwrap_or("root"); + let group = opts.group.unwrap_or("root"); + + format!( +r#"#!/sbin/openrc-run +# OpenRC init script for telemt + +description="{description}" +command="{exe}" +command_args="--daemon --pid-file {pid_file} {config}" +command_user="{user}:{group}" +pidfile="{pid_file}" + +depend() {{ + need net + after firewall +}} + +start_pre() {{ + checkpath --directory --owner {user}:{group} --mode 0755 /var/run + checkpath --directory --owner {user}:{group} --mode 0755 /var/lib/telemt +}} + +reload() {{ + ebegin "Reloading ${{RC_SVCNAME}}" + start-stop-daemon --signal HUP --pidfile "${{pidfile}}" + eend $? +}} +"#, + description = opts.description, + exe = opts.exe_path.display(), + config = opts.config_path.display(), + pid_file = opts.pid_file, + user = user, + group = group, + ) +} + +/// Generates a FreeBSD rc.d script. +fn generate_freebsd_rc_script(opts: &ServiceOptions) -> String { + let user = opts.user.unwrap_or("root"); + let group = opts.group.unwrap_or("wheel"); + + format!( +r#"#!/bin/sh +# +# PROVIDE: telemt +# REQUIRE: LOGIN NETWORKING +# KEYWORD: shutdown +# +# Add the following lines to /etc/rc.conf to enable telemt: +# +# telemt_enable="YES" +# telemt_config="/etc/telemt/config.toml" # optional +# telemt_user="telemt" # optional +# telemt_group="telemt" # optional +# + +. /etc/rc.subr + +name="telemt" +rcvar="telemt_enable" +desc="{description}" + +load_rc_config $name + +: ${{telemt_enable:="NO"}} +: ${{telemt_config:="{config}"}} +: ${{telemt_user:="{user}"}} +: ${{telemt_group:="{group}"}} +: ${{telemt_pidfile:="{pid_file}"}} + +pidfile="${{telemt_pidfile}}" +command="{exe}" +command_args="--daemon --pid-file ${{telemt_pidfile}} ${{telemt_config}}" + +start_precmd="telemt_prestart" +reload_cmd="telemt_reload" +extra_commands="reload" + +telemt_prestart() {{ + install -d -o ${{telemt_user}} -g ${{telemt_group}} -m 755 /var/run + install -d -o ${{telemt_user}} -g ${{telemt_group}} -m 755 /var/lib/telemt +}} + +telemt_reload() {{ + if [ -f "${{pidfile}}" ]; then + echo "Reloading ${{name}} configuration." + kill -HUP $(cat ${{pidfile}}) + else + echo "${{name}} is not running." + return 1 + fi +}} + +run_rc_command "$1" +"#, + description = opts.description, + exe = opts.exe_path.display(), + config = opts.config_path.display(), + pid_file = opts.pid_file, + user = user, + group = group, + ) +} + +/// Installation instructions for each init system. +pub fn installation_instructions(init_system: InitSystem) -> &'static str { + match init_system { + InitSystem::Systemd => { +r#"To install and enable the service: + sudo systemctl daemon-reload + sudo systemctl enable telemt + sudo systemctl start telemt + +To check status: + sudo systemctl status telemt + +To view logs: + journalctl -u telemt -f + +To reload configuration: + sudo systemctl reload telemt +"# + } + InitSystem::OpenRC => { +r#"To install and enable the service: + sudo chmod +x /etc/init.d/telemt + sudo rc-update add telemt default + sudo rc-service telemt start + +To check status: + sudo rc-service telemt status + +To reload configuration: + sudo rc-service telemt reload +"# + } + InitSystem::FreeBSDRc => { +r#"To install and enable the service: + sudo chmod +x /usr/local/etc/rc.d/telemt + sudo sysrc telemt_enable="YES" + sudo service telemt start + +To check status: + sudo service telemt status + +To reload configuration: + sudo service telemt reload +"# + } + InitSystem::Unknown => { +r#"No supported init system detected. +You may need to create a service file manually or run telemt directly: + telemt start /etc/telemt/config.toml +"# + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_systemd_unit_generation() { + let opts = ServiceOptions::default(); + let unit = generate_systemd_unit(&opts); + assert!(unit.contains("[Unit]")); + assert!(unit.contains("[Service]")); + assert!(unit.contains("[Install]")); + assert!(unit.contains("ExecReload=")); + assert!(unit.contains("PIDFile=")); + } + + #[test] + fn test_openrc_script_generation() { + let opts = ServiceOptions::default(); + let script = generate_openrc_script(&opts); + assert!(script.contains("#!/sbin/openrc-run")); + assert!(script.contains("depend()")); + assert!(script.contains("reload()")); + } + + #[test] + fn test_freebsd_rc_script_generation() { + let opts = ServiceOptions::default(); + let script = generate_freebsd_rc_script(&opts); + assert!(script.contains("#!/bin/sh")); + assert!(script.contains("PROVIDE: telemt")); + assert!(script.contains("run_rc_command")); + } + + #[test] + fn test_service_file_paths() { + assert_eq!(service_file_path(InitSystem::Systemd), "/etc/systemd/system/telemt.service"); + assert_eq!(service_file_path(InitSystem::OpenRC), "/etc/init.d/telemt"); + assert_eq!(service_file_path(InitSystem::FreeBSDRc), "/usr/local/etc/rc.d/telemt"); + } +}