use crate::model::Trojan; use anyhow::{Context, Result, ensure}; use serde_json::{Value, json}; use std::{ fs, os::unix::fs::{MetadataExt, PermissionsExt}, path::Path, process::Stdio, time::Duration, }; use tokio::process::Command; pub const RUNTIME_DIR: &str = "/run/roentgen"; pub fn identity() -> Result<(u32, u32)> { let mut buffer = vec![0u8; 65536]; let mut record = std::mem::MaybeUninit::::uninit(); let mut result = std::ptr::null_mut(); // SAFETY: the output record and buffer are writable and remain alive while // the reentrant account lookup fills them. The returned IDs are copied. let error = unsafe { libc::getpwnam_r( c"roentgen-xray".as_ptr(), record.as_mut_ptr(), buffer.as_mut_ptr().cast(), buffer.len(), &mut result, ) }; ensure!( error == 0 && !result.is_null(), "Missing roentgen-xray service account; run roentgen install" ); let record = unsafe { record.assume_init() }; ensure!( record.pw_uid != 0 && record.pw_gid != 0, "Xray must use a dedicated non-root account and group" ); Ok((record.pw_uid, record.pw_gid)) } pub async fn preflight() -> Result<()> { let metadata = fs::metadata("/usr/bin/xray").context("Install ALT's current xray package first")?; ensure!( metadata.uid() == 0 && metadata.permissions().mode() & 0o022 == 0, "System Xray must be root-owned and not writable by other users" ); let output = tokio::time::timeout( Duration::from_secs(5), Command::new("/usr/bin/xray").arg("version").output(), ) .await??; ensure!( output.status.success(), "Could not read system Xray version" ); let version = String::from_utf8_lossy(&output.stdout); let version = version.split_whitespace().nth(1).unwrap_or(""); // Native TUN routing behavior is pinned to the researched release family. ensure!( matches!(version, "26.10.29"), "Unsupported Xray release; this build requires validated Xray 26.10.29 capabilities" ); ensure!( Path::new("/dev/net/tun").exists(), "Linux TUN is unavailable" ); Ok(()) } pub fn configuration(connection: &Trojan, tun: bool) -> Value { let mut inbounds = vec![]; if tun { inbounds.push(json!({"tag":"host-tun","protocol":"tun","settings":{"name":"roentgen0","MTU":1500,"gateway":["198.18.0.1/30","fdfe:726f:656e::1/126"],"autoSystemRoutingTable":[],"autoOutboundsInterface":"","autoSystemDnsToGateway":false}})); } let mut tls = json!({"serverName":connection.server_name,"allowInsecure":false}); if !connection.alpn.is_empty() { tls["alpn"] = json!(connection.alpn); } if let Some(fingerprint) = &connection.fingerprint { tls["fingerprint"] = json!(fingerprint); } json!({"log":{"loglevel":"none"},"inbounds":inbounds,"outbounds":[{"tag":"proxy","protocol":"trojan","settings":{"servers":[{"address":connection.host,"port":connection.port,"password":connection.password}]},"streamSettings":{"network":"tcp","security":"tls","tlsSettings":tls}}]}) } pub fn write_config(path: &Path, value: &Value) -> Result<()> { let (_, gid) = identity()?; fs::create_dir_all(RUNTIME_DIR)?; let directory = Path::new(RUNTIME_DIR); fs::set_permissions(directory, fs::Permissions::from_mode(0o750))?; std::os::unix::fs::chown(directory, Some(0), Some(gid))?; crate::storage::atomic_private_write(path, &serde_json::to_vec(value)?)?; std::os::unix::fs::chown(path, Some(0), Some(gid))?; fs::set_permissions(path, fs::Permissions::from_mode(0o640))?; Ok(()) } pub async fn systemctl(args: &[&str]) -> Result<()> { let status = tokio::time::timeout( Duration::from_secs(30), Command::new("/usr/bin/systemctl") .args(args) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .status(), ) .await??; ensure!( status.success(), "Service operation failed; inspect systemctl status for Roentgen" ); Ok(()) } pub struct Probe { unit: String, path: std::path::PathBuf, pub proxy: String, pub certificate: String, active: bool, } impl Probe { pub async fn start(connection: &Trojan, duration: u64) -> Result { preflight().await?; let listener = std::net::TcpListener::bind("127.0.0.1:0")?; let port = listener.local_addr()?.port(); let token = uuid::Uuid::new_v4().simple().to_string(); let unit = format!("roentgen-probe-{token}"); let path = Path::new(RUNTIME_DIR).join(format!("{unit}.json")); let certified = rcgen::generate_simple_self_signed(vec!["127.0.0.1".into()])?; let certificate = certified.cert.pem(); let key = certified.key_pair.serialize_pem(); let mut configuration = configuration(connection, false); configuration["inbounds"] = json!([{ "tag":"probe", "listen":"127.0.0.1", "port":port, "protocol":"http", "settings":{"accounts":[{"user":"roentgen","pass":token}]}, "streamSettings":{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{ "certificate":certificate.lines().collect::>(), "key":key.lines().collect::>() }]}} }]); write_config(&path, &configuration)?; drop(listener); let runtime_limit = format!("RuntimeMaxSec={}", duration.saturating_add(10)); let probe = Self { unit, path, proxy: format!("https://roentgen:{token}@127.0.0.1:{port}"), certificate, active: true, }; let result = Command::new("/usr/bin/systemd-run") .args([ "--quiet", "--collect", "--unit", &probe.unit, "--uid=roentgen-xray", "--property=NoNewPrivileges=yes", "--property=PrivateTmp=yes", "--property=ProtectSystem=strict", "--property=ProtectHome=yes", "--property", &runtime_limit, "/usr/bin/xray", "run", "-config", ]) .arg(&probe.path) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .await; if !result?.success() { probe.stop().await; anyhow::bail!("Could not launch protected Xray probe"); } for _ in 0..50 { if tokio::net::TcpStream::connect(("127.0.0.1", port)) .await .is_ok() { return Ok(probe); } tokio::time::sleep(Duration::from_millis(20)).await; } probe.stop().await; anyhow::bail!("Protected Xray probe did not start") } pub async fn stop(mut self) { self.active = false; let _ = systemctl(&["stop", &self.unit]).await; let _ = fs::remove_file(&self.path); } } impl Drop for Probe { fn drop(&mut self) { if !self.active { return; } let unit = self.unit.clone(); let path = self.path.clone(); if let Ok(runtime) = tokio::runtime::Handle::try_current() { runtime.spawn(async move { let _ = systemctl(&["stop", &unit]).await; let _ = fs::remove_file(path); }); } // systemd RuntimeMaxSec also bounds the process after controller death. } }