//! A pipe-lifetime guardian is necessary: exec of a capability-bearing binary clears //! Linux PR_SET_PDEATHSIG. Closing the GUI's pipe (even after SIGKILL) stops the core. use std::{ io::Read, path::Path, process::{Child, Command, Stdio}, sync::{ atomic::{AtomicBool, Ordering}, mpsc, Arc, }, time::{Duration, Instant}, }; pub fn terminate(child: &mut Child) { if matches!(child.try_wait(), Ok(Some(_))) { return; } // Child remains unreaped until wait below, so its PID cannot be reused here. unsafe { libc::kill(child.id() as i32, libc::SIGTERM); } let deadline = Instant::now() + Duration::from_secs(3); while Instant::now() < deadline { if matches!(child.try_wait(), Ok(Some(_))) { return; } std::thread::sleep(Duration::from_millis(30)); } let _ = child.kill(); let _ = child.wait(); } pub fn run(binary: &Path, config: &Path) -> i32 { let stopping = Arc::new(AtomicBool::new(false)); for signal in [libc::SIGTERM, libc::SIGINT, libc::SIGHUP] { if signal_hook::flag::register(signal, stopping.clone()).is_err() { return 1; } } // The separate lock is also held by the guardian during GUI-crash cleanup. let lock_dir = config.parent().and_then(Path::parent); let _lock = match lock_dir.and_then(|dir| crate::tunnel::core_lock(dir).ok()) { Some(lock) => lock, None => { eprintln!("Another tunnel is still running or stopping"); return 1; } }; let (tx, rx) = mpsc::channel(); std::thread::spawn(move || { let mut byte = [0; 1]; let _ = std::io::stdin().read(&mut byte); let _ = tx.send(()); }); let mut child = match Command::new(binary) .args(["run", "--disable-color", "-c"]) // Pinned sing-tun uses PATH to find resolvectl. DNS interception is native // nftables; disable external helpers so it never changes resolved settings // or asks Polkit for additional privileges. All core I/O is in-process. .env("PATH", "") .arg(config) .stdin(Stdio::null()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .spawn() { Ok(child) => child, Err(_) => { eprintln!("Cannot start sing-box"); cleanup(config); return 1; } }; let code = loop { if stopping.load(Ordering::Relaxed) || rx.try_recv().is_ok() { terminate(&mut child); break 0; } match child.try_wait() { Ok(Some(s)) => break s.code().unwrap_or(1), Err(_) => { terminate(&mut child); break 1; } _ => {} } std::thread::sleep(Duration::from_millis(30)); }; cleanup(config); code } pub fn cleanup(config: &Path) { // Only files created by our runtime writer are eligible for deletion. if config.file_name().is_some_and(|s| s == "config.json") { if let Some(dir) = config.parent().filter(|p| { p.file_name() .is_some_and(|n| n.to_string_lossy().starts_with("runtime-")) }) { let _ = std::fs::remove_file(config); let _ = std::fs::remove_dir(dir); } } }