a3s_box_runtime/network/
passt.rs1use a3s_box_core::error::{BoxError, Result};
8use std::net::Ipv4Addr;
9use std::path::{Path, PathBuf};
10use std::process::{Child, Command};
11
12#[derive(Debug)]
14pub struct PasstManager {
15 socket_path: PathBuf,
17 child: Option<Child>,
19 pid_file: PathBuf,
21}
22
23impl PasstManager {
24 pub fn new(socket_dir: &Path) -> Self {
36 Self {
37 socket_path: socket_dir.join("passt.sock"),
38 pid_file: socket_dir.join("passt.pid"),
39 child: None,
40 }
41 }
42
43 pub fn socket_path(&self) -> &Path {
45 &self.socket_path
46 }
47
48 pub fn spawn(
57 &mut self,
58 ip: Ipv4Addr,
59 gateway: Ipv4Addr,
60 prefix_len: u8,
61 dns_servers: &[Ipv4Addr],
62 port_map: &[String],
63 ) -> Result<()> {
64 if let Some(parent) = self.socket_path.parent() {
66 std::fs::create_dir_all(parent).map_err(|e| {
67 BoxError::NetworkError(format!(
68 "failed to create socket directory {}: {}",
69 parent.display(),
70 e
71 ))
72 })?;
73
74 #[cfg(unix)]
80 {
81 use std::os::unix::fs::PermissionsExt;
82 if let Err(e) =
83 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o777))
84 {
85 tracing::warn!(
86 dir = %parent.display(),
87 error = %e,
88 "Failed to widen passt socket directory permissions; \
89 passt may be unable to bind its socket after dropping privileges"
90 );
91 }
92 }
93 }
94
95 if self.socket_path.exists() {
97 std::fs::remove_file(&self.socket_path).ok();
98 }
99
100 let mut cmd = Command::new("passt");
101 cmd.arg("--socket")
102 .arg(&self.socket_path)
103 .arg("--pid")
104 .arg(&self.pid_file)
105 .arg("--foreground")
107 .arg("--address")
109 .arg(ip.to_string())
110 .arg("--gateway")
111 .arg(gateway.to_string())
112 .arg("--netmask")
113 .arg(format!("{}", prefix_to_netmask(prefix_len)));
114
115 for dns in dns_servers {
117 cmd.arg("--dns").arg(dns.to_string());
118 }
119
120 let tcp_specs: Vec<String> = port_map
126 .iter()
127 .filter_map(|m| a3s_box_core::parse_port_mapping(m).ok())
128 .filter(|m| m.host_port != 0)
129 .map(|m| format!("{}:{}", m.host_port, m.guest_port))
130 .collect();
131 if !tcp_specs.is_empty() {
132 let spec = tcp_specs.join(",");
133 tracing::info!(tcp_ports = %spec, "Configuring passt inbound TCP port forwarding");
134 cmd.arg("--tcp-ports").arg(spec);
135 }
136
137 cmd.stdout(std::process::Stdio::null());
141 match self
142 .socket_path
143 .parent()
144 .map(|p| p.join("passt.stderr.log"))
145 .and_then(|p| std::fs::File::create(p).ok())
146 {
147 Some(file) => {
148 cmd.stderr(std::process::Stdio::from(file));
149 }
150 None => {
151 cmd.stderr(std::process::Stdio::null());
152 }
153 }
154
155 let child = cmd.spawn().map_err(|e| {
156 BoxError::NetworkError(format!(
157 "failed to spawn passt: {} (is passt installed?)",
158 e
159 ))
160 })?;
161
162 tracing::info!(
163 pid = child.id(),
164 socket = %self.socket_path.display(),
165 ip = %ip,
166 gateway = %gateway,
167 "Passt daemon started"
168 );
169
170 self.child = Some(child);
171
172 self.wait_for_socket()?;
174
175 Ok(())
176 }
177
178 fn wait_for_socket(&mut self) -> Result<()> {
184 let stderr_path = self
185 .socket_path
186 .parent()
187 .map(|p| p.join("passt.stderr.log"));
188 let read_stderr = |path: &Option<PathBuf>| -> String {
189 path.as_ref()
190 .and_then(|p| std::fs::read_to_string(p).ok())
191 .map(|s| {
192 let mut tail: Vec<&str> = s.lines().rev().take(4).collect();
193 tail.reverse();
194 tail.join("; ")
195 })
196 .filter(|s| !s.trim().is_empty())
197 .map(|s| format!(" (passt stderr: {s})"))
198 .unwrap_or_default()
199 };
200
201 let max_attempts = 50; for _ in 0..max_attempts {
203 if self.socket_path.exists() {
204 return Ok(());
205 }
206 if let Some(child) = self.child.as_mut() {
207 if let Ok(Some(status)) = child.try_wait() {
208 return Err(BoxError::NetworkError(format!(
210 "passt exited early with {status} before creating its socket{}",
211 read_stderr(&stderr_path)
212 )));
213 }
214 }
215 std::thread::sleep(std::time::Duration::from_millis(100));
216 }
217
218 if let Some(mut child) = self.child.take() {
224 let _ = child.kill();
225 let _ = child.wait();
226 }
227
228 Err(BoxError::NetworkError(format!(
229 "passt socket {} did not appear within 5 seconds{}",
230 self.socket_path.display(),
231 read_stderr(&stderr_path)
232 )))
233 }
234
235 pub fn stop(&mut self) {
237 if let Some(ref mut child) = self.child {
238 let pid = child.id();
239 if let Err(e) = child.kill() {
240 tracing::warn!(pid, error = %e, "Failed to kill passt process");
241 } else {
242 let _ = child.wait();
244 tracing::info!(pid, "Passt daemon stopped");
245 }
246 }
247 self.child = None;
248
249 std::fs::remove_file(&self.socket_path).ok();
251 std::fs::remove_file(&self.pid_file).ok();
252 }
253
254 pub fn is_running(&mut self) -> bool {
256 match self.child {
257 Some(ref mut child) => child.try_wait().ok().flatten().is_none(),
258 None => false,
259 }
260 }
261}
262
263impl Drop for PasstManager {
264 fn drop(&mut self) {
265 }
272}
273
274pub fn terminate_passt(socket_dir: &Path) {
280 let pid_file = socket_dir.join("passt.pid");
281 if let Ok(contents) = std::fs::read_to_string(&pid_file) {
282 if let Ok(pid) = contents.trim().parse::<i32>() {
283 if pid > 1 && pid_is_passt(pid) {
287 #[cfg(unix)]
289 unsafe {
290 libc::kill(pid, libc::SIGTERM);
291 }
292 tracing::info!(pid, "Terminated passt daemon");
293 }
294 }
295 }
296 let _ = std::fs::remove_file(&pid_file);
297 let _ = std::fs::remove_file(socket_dir.join("passt.sock"));
298}
299
300#[cfg(target_os = "linux")]
306fn pid_is_passt(pid: i32) -> bool {
307 std::fs::read_to_string(format!("/proc/{pid}/comm"))
308 .map(|comm| comm.trim() == "passt")
309 .unwrap_or(false)
310}
311
312#[cfg(not(target_os = "linux"))]
313fn pid_is_passt(_pid: i32) -> bool {
314 true
317}
318
319impl super::NetworkBackend for PasstManager {
320 fn socket_path(&self) -> &std::path::Path {
321 self.socket_path()
322 }
323
324 fn stop(&mut self) {
325 self.stop();
326 }
327}
328
329fn prefix_to_netmask(prefix: u8) -> Ipv4Addr {
331 if prefix == 0 {
332 return Ipv4Addr::new(0, 0, 0, 0);
333 }
334 let mask = !((1u32 << (32 - prefix)) - 1);
335 Ipv4Addr::from(mask)
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_prefix_to_netmask() {
344 assert_eq!(prefix_to_netmask(24), Ipv4Addr::new(255, 255, 255, 0));
345 assert_eq!(prefix_to_netmask(16), Ipv4Addr::new(255, 255, 0, 0));
346 assert_eq!(prefix_to_netmask(8), Ipv4Addr::new(255, 0, 0, 0));
347 assert_eq!(prefix_to_netmask(32), Ipv4Addr::new(255, 255, 255, 255));
348 assert_eq!(prefix_to_netmask(0), Ipv4Addr::new(0, 0, 0, 0));
349 assert_eq!(prefix_to_netmask(28), Ipv4Addr::new(255, 255, 255, 240));
350 }
351
352 #[test]
353 fn test_passt_manager_new() {
354 let dir = tempfile::tempdir().unwrap();
355 let mgr = PasstManager::new(dir.path());
356 assert_eq!(mgr.socket_path(), dir.path().join("passt.sock"));
357 }
358
359 #[test]
360 fn test_passt_manager_not_running_initially() {
361 let dir = tempfile::tempdir().unwrap();
362 let mut mgr = PasstManager::new(dir.path());
363 assert!(!mgr.is_running());
364 }
365
366 #[test]
367 fn test_passt_manager_stop_when_not_started() {
368 let dir = tempfile::tempdir().unwrap();
369 let mut mgr = PasstManager::new(dir.path());
370 mgr.stop();
372 assert!(!mgr.is_running());
373 }
374
375 #[test]
376 fn test_passt_manager_socket_path() {
377 let dir = tempfile::tempdir().unwrap();
378 let box_dir = dir.path().join("boxes").join("test-box-id");
379 let mgr = PasstManager::new(&box_dir);
380 assert_eq!(mgr.socket_path(), box_dir.join("passt.sock"));
381 }
382
383 #[test]
384 fn test_terminate_passt_removes_socket_and_pid_files() {
385 let dir = tempfile::tempdir().unwrap();
386 let socket_path = dir.path().join("passt.sock");
387 let pid_path = dir.path().join("passt.pid");
388
389 std::fs::write(&socket_path, "fake").unwrap();
391 std::fs::write(&pid_path, "2147483647").unwrap();
392
393 terminate_passt(dir.path());
394
395 assert!(!socket_path.exists());
396 assert!(!pid_path.exists());
397 }
398}