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 pcap_path: PathBuf,
19 child: Option<Child>,
21 pid_file: PathBuf,
23}
24
25impl PasstManager {
26 pub fn new(socket_dir: &Path) -> Self {
38 Self {
39 socket_path: socket_dir.join("passt.sock"),
40 pcap_path: socket_dir.join("passt.pcap"),
41 pid_file: socket_dir.join("passt.pid"),
42 child: None,
43 }
44 }
45
46 pub fn socket_path(&self) -> &Path {
48 &self.socket_path
49 }
50
51 pub fn pcap_path(&self) -> &Path {
53 &self.pcap_path
54 }
55
56 pub fn spawn(
65 &mut self,
66 ip: Ipv4Addr,
67 gateway: Ipv4Addr,
68 prefix_len: u8,
69 dns_servers: &[Ipv4Addr],
70 port_map: &[String],
71 ) -> Result<()> {
72 if let Some(parent) = self.socket_path.parent() {
74 std::fs::create_dir_all(parent).map_err(|e| {
75 BoxError::NetworkError(format!(
76 "failed to create socket directory {}: {}",
77 parent.display(),
78 e
79 ))
80 })?;
81
82 #[cfg(unix)]
88 {
89 use std::os::unix::fs::PermissionsExt;
90 if let Err(e) =
91 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o777))
92 {
93 tracing::warn!(
94 dir = %parent.display(),
95 error = %e,
96 "Failed to widen passt socket directory permissions; \
97 passt may be unable to bind its socket after dropping privileges"
98 );
99 }
100 }
101 }
102
103 if self.socket_path.exists() {
105 std::fs::remove_file(&self.socket_path).ok();
106 }
107 if self.pcap_path.exists() {
108 std::fs::remove_file(&self.pcap_path).ok();
109 }
110
111 let mut cmd = Command::new("passt");
112 cmd.arg("--socket")
113 .arg(&self.socket_path)
114 .arg("--pid")
115 .arg(&self.pid_file)
116 .arg("--pcap")
117 .arg(&self.pcap_path)
118 .arg("--foreground")
120 .arg("--address")
122 .arg(ip.to_string())
123 .arg("--gateway")
124 .arg(gateway.to_string())
125 .arg("--netmask")
126 .arg(format!("{}", prefix_to_netmask(prefix_len)));
127
128 for dns in dns_servers {
130 cmd.arg("--dns").arg(dns.to_string());
131 }
132
133 let tcp_specs: Vec<String> = port_map
139 .iter()
140 .filter_map(|m| a3s_box_core::parse_port_mapping(m).ok())
141 .filter(|m| m.host_port != 0)
142 .map(|m| format!("{}:{}", m.host_port, m.guest_port))
143 .collect();
144 if !tcp_specs.is_empty() {
145 let spec = tcp_specs.join(",");
146 tracing::info!(tcp_ports = %spec, "Configuring passt inbound TCP port forwarding");
147 cmd.arg("--tcp-ports").arg(spec);
148 }
149
150 cmd.stdout(std::process::Stdio::null());
154 match self
155 .socket_path
156 .parent()
157 .map(|p| p.join("passt.stderr.log"))
158 .and_then(|p| std::fs::File::create(p).ok())
159 {
160 Some(file) => {
161 cmd.stderr(std::process::Stdio::from(file));
162 }
163 None => {
164 cmd.stderr(std::process::Stdio::null());
165 }
166 }
167
168 let child = cmd.spawn().map_err(|e| {
169 BoxError::NetworkError(format!(
170 "failed to spawn passt: {} (is passt installed?)",
171 e
172 ))
173 })?;
174
175 tracing::info!(
176 pid = child.id(),
177 socket = %self.socket_path.display(),
178 ip = %ip,
179 gateway = %gateway,
180 "Passt daemon started"
181 );
182
183 self.child = Some(child);
184
185 self.wait_for_socket()?;
187
188 Ok(())
189 }
190
191 fn wait_for_socket(&mut self) -> Result<()> {
197 let stderr_path = self
198 .socket_path
199 .parent()
200 .map(|p| p.join("passt.stderr.log"));
201 let read_stderr = |path: &Option<PathBuf>| -> String {
202 path.as_ref()
203 .and_then(|p| std::fs::read_to_string(p).ok())
204 .map(|s| {
205 let mut tail: Vec<&str> = s.lines().rev().take(4).collect();
206 tail.reverse();
207 tail.join("; ")
208 })
209 .filter(|s| !s.trim().is_empty())
210 .map(|s| format!(" (passt stderr: {s})"))
211 .unwrap_or_default()
212 };
213
214 let max_attempts = 50; for _ in 0..max_attempts {
216 if self.socket_path.exists() {
217 return Ok(());
218 }
219 if let Some(child) = self.child.as_mut() {
220 if let Ok(Some(status)) = child.try_wait() {
221 return Err(BoxError::NetworkError(format!(
223 "passt exited early with {status} before creating its socket{}",
224 read_stderr(&stderr_path)
225 )));
226 }
227 }
228 std::thread::sleep(std::time::Duration::from_millis(100));
229 }
230
231 if let Some(mut child) = self.child.take() {
237 let _ = child.kill();
238 let _ = child.wait();
239 }
240
241 Err(BoxError::NetworkError(format!(
242 "passt socket {} did not appear within 5 seconds{}",
243 self.socket_path.display(),
244 read_stderr(&stderr_path)
245 )))
246 }
247
248 pub fn stop(&mut self) {
250 if let Some(ref mut child) = self.child {
251 let pid = child.id();
252 if let Err(e) = child.kill() {
253 tracing::warn!(pid, error = %e, "Failed to kill passt process");
254 } else {
255 let _ = child.wait();
257 tracing::info!(pid, "Passt daemon stopped");
258 }
259 }
260 self.child = None;
261
262 std::fs::remove_file(&self.socket_path).ok();
264 std::fs::remove_file(&self.pcap_path).ok();
265 std::fs::remove_file(&self.pid_file).ok();
266 }
267
268 pub fn is_running(&mut self) -> bool {
270 match self.child {
271 Some(ref mut child) => child.try_wait().ok().flatten().is_none(),
272 None => false,
273 }
274 }
275}
276
277impl Drop for PasstManager {
278 fn drop(&mut self) {
279 }
286}
287
288pub fn terminate_passt(socket_dir: &Path) {
294 let pid_file = socket_dir.join("passt.pid");
295 if let Ok(contents) = std::fs::read_to_string(&pid_file) {
296 if let Ok(pid) = contents.trim().parse::<i32>() {
297 if pid > 1 && pid_is_passt(pid) {
301 #[cfg(unix)]
303 unsafe {
304 libc::kill(pid, libc::SIGTERM);
305 }
306 tracing::info!(pid, "Terminated passt daemon");
307 }
308 }
309 }
310 let _ = std::fs::remove_file(&pid_file);
311 let _ = std::fs::remove_file(socket_dir.join("passt.sock"));
312 let _ = std::fs::remove_file(socket_dir.join("passt.pcap"));
313}
314
315#[cfg(target_os = "linux")]
321fn pid_is_passt(pid: i32) -> bool {
322 std::fs::read_to_string(format!("/proc/{pid}/comm"))
323 .map(|comm| comm.trim() == "passt")
324 .unwrap_or(false)
325}
326
327#[cfg(not(target_os = "linux"))]
328fn pid_is_passt(_pid: i32) -> bool {
329 true
332}
333
334impl super::NetworkBackend for PasstManager {
335 fn socket_path(&self) -> &std::path::Path {
336 self.socket_path()
337 }
338
339 fn stop(&mut self) {
340 self.stop();
341 }
342}
343
344fn prefix_to_netmask(prefix: u8) -> Ipv4Addr {
346 if prefix == 0 {
347 return Ipv4Addr::new(0, 0, 0, 0);
348 }
349 let mask = !((1u32 << (32 - prefix)) - 1);
350 Ipv4Addr::from(mask)
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn test_prefix_to_netmask() {
359 assert_eq!(prefix_to_netmask(24), Ipv4Addr::new(255, 255, 255, 0));
360 assert_eq!(prefix_to_netmask(16), Ipv4Addr::new(255, 255, 0, 0));
361 assert_eq!(prefix_to_netmask(8), Ipv4Addr::new(255, 0, 0, 0));
362 assert_eq!(prefix_to_netmask(32), Ipv4Addr::new(255, 255, 255, 255));
363 assert_eq!(prefix_to_netmask(0), Ipv4Addr::new(0, 0, 0, 0));
364 assert_eq!(prefix_to_netmask(28), Ipv4Addr::new(255, 255, 255, 240));
365 }
366
367 #[test]
368 fn test_passt_manager_new() {
369 let dir = tempfile::tempdir().unwrap();
370 let mgr = PasstManager::new(dir.path());
371 assert_eq!(mgr.socket_path(), dir.path().join("passt.sock"));
372 assert_eq!(mgr.pcap_path(), dir.path().join("passt.pcap"));
373 }
374
375 #[test]
376 fn test_passt_manager_not_running_initially() {
377 let dir = tempfile::tempdir().unwrap();
378 let mut mgr = PasstManager::new(dir.path());
379 assert!(!mgr.is_running());
380 }
381
382 #[test]
383 fn test_passt_manager_stop_when_not_started() {
384 let dir = tempfile::tempdir().unwrap();
385 let mut mgr = PasstManager::new(dir.path());
386 mgr.stop();
388 assert!(!mgr.is_running());
389 }
390
391 #[test]
392 fn test_passt_manager_socket_path() {
393 let dir = tempfile::tempdir().unwrap();
394 let box_dir = dir.path().join("boxes").join("test-box-id");
395 let mgr = PasstManager::new(&box_dir);
396 assert_eq!(mgr.socket_path(), box_dir.join("passt.sock"));
397 assert_eq!(mgr.pcap_path(), box_dir.join("passt.pcap"));
398 }
399
400 #[test]
401 fn test_terminate_passt_removes_socket_and_pid_files() {
402 let dir = tempfile::tempdir().unwrap();
403 let socket_path = dir.path().join("passt.sock");
404 let pid_path = dir.path().join("passt.pid");
405 let pcap_path = dir.path().join("passt.pcap");
406
407 std::fs::write(&socket_path, "fake").unwrap();
409 std::fs::write(&pid_path, "2147483647").unwrap();
410 std::fs::write(&pcap_path, "fake pcap").unwrap();
411
412 terminate_passt(dir.path());
413
414 assert!(!socket_path.exists());
415 assert!(!pid_path.exists());
416 assert!(!pcap_path.exists());
417 }
418}