1use super::{
2 egress, BridgeConfig, BridgeNetwork, EgressPolicy, NatBackend, NetworkState, PortForward,
3};
4use crate::error::{NucleusError, Result, StateTransition};
5use nix::fcntl::{fcntl, FcntlArg, FdFlag};
6use serde_json::json;
7use std::io::{Read, Write};
8use std::os::fd::{AsRawFd, OwnedFd};
9use std::os::unix::net::UnixStream;
10use std::os::unix::process::ExitStatusExt;
11use std::path::{Path, PathBuf};
12use std::process::{Child, Command};
13use std::time::{Duration, Instant};
14use tracing::{debug, info, warn};
15
16const SLIRP_TAP_NAME: &str = "tap0";
17
18pub enum BridgeDriver {
20 Kernel(BridgeNetwork),
21 Userspace(UserspaceNetwork),
22}
23
24impl BridgeDriver {
25 pub fn setup_with_id(
26 pid: u32,
27 config: &BridgeConfig,
28 container_id: &str,
29 host_is_root: bool,
30 rootless: bool,
31 ) -> Result<Self> {
32 match config.selected_nat_backend(host_is_root, rootless) {
33 NatBackend::Kernel => Ok(Self::Kernel(BridgeNetwork::setup_with_id(
34 pid,
35 config,
36 container_id,
37 )?)),
38 NatBackend::Userspace => Ok(Self::Userspace(UserspaceNetwork::setup_with_id(
39 pid,
40 config,
41 container_id,
42 host_is_root,
43 rootless,
44 )?)),
45 NatBackend::Auto => Err(NucleusError::NetworkError(
46 "nat backend selection resolved to auto unexpectedly".to_string(),
47 )),
48 }
49 }
50
51 pub fn apply_egress_policy(
52 &self,
53 pid: u32,
54 policy: &EgressPolicy,
55 rootless: bool,
56 ) -> Result<()> {
57 match self {
58 Self::Kernel(net) => net.apply_egress_policy(pid, policy),
59 Self::Userspace(net) => net.apply_egress_policy(pid, policy, rootless),
60 }
61 }
62
63 pub fn cleanup(self) -> Result<()> {
64 match self {
65 Self::Kernel(net) => net.cleanup(),
66 Self::Userspace(net) => net.cleanup(),
67 }
68 }
69}
70
71pub struct UserspaceNetwork {
73 config: BridgeConfig,
74 guest_ip: String,
75 container_id: String,
76 api_socket_path: PathBuf,
77 runtime_dir: PathBuf,
78 exit_signal: Option<OwnedFd>,
79 child: Child,
80 state: NetworkState,
81}
82
83impl UserspaceNetwork {
84 pub(crate) fn default_dns_server(subnet: &str) -> Result<String> {
85 Self::dns_ip_from_subnet(subnet)
86 }
87
88 pub fn setup_with_id(
89 pid: u32,
90 config: &BridgeConfig,
91 container_id: &str,
92 host_is_root: bool,
93 rootless: bool,
94 ) -> Result<Self> {
95 config.validate()?;
96
97 let guest_ip = Self::guest_ip_from_subnet(&config.subnet)?;
98 Self::validate_userspace_config(config, &guest_ip)?;
99
100 let mut state = NetworkState::Unconfigured;
101 state = state.transition(NetworkState::Configuring)?;
102
103 let runtime_dir = Self::runtime_dir(container_id);
104 Self::ensure_runtime_dir(&runtime_dir)?;
105 let api_socket_path = runtime_dir.join("slirp4netns.sock");
106
107 let slirp = BridgeNetwork::resolve_bin("slirp4netns")?;
108 let needs_userns = Self::should_join_userns(host_is_root, rootless);
114
115 let slirp_path = Path::new(&slirp);
116 let (child, exit_write) = match Self::spawn_slirp(
117 slirp_path,
118 pid,
119 config,
120 needs_userns,
121 &api_socket_path,
122 true,
123 ) {
124 Ok(result) => result,
125 Err(e) => {
126 warn!(
127 "slirp4netns sandbox failed ({}), retrying without --enable-sandbox",
128 e
129 );
130 let _ = std::fs::remove_file(&api_socket_path);
136 Self::spawn_slirp(
137 slirp_path,
138 pid,
139 config,
140 needs_userns,
141 &api_socket_path,
142 false,
143 )
144 .inspect_err(|_| {
145 let _ = std::fs::remove_dir_all(&runtime_dir);
146 })?
147 }
148 };
149
150 let mut network = Self {
151 config: config.clone(),
152 guest_ip: guest_ip.to_string(),
153 container_id: container_id.to_string(),
154 api_socket_path,
155 runtime_dir,
156 exit_signal: Some(exit_write),
157 child,
158 state,
159 };
160
161 if let Err(e) = network.configure_port_forwards() {
162 network.cleanup_best_effort();
163 return Err(e);
164 }
165
166 network.state = network.state.transition(NetworkState::Active)?;
167
168 info!(
169 "Userspace NAT configured via slirp4netns for container {} (guest IP {})",
170 network.container_id, network.guest_ip
171 );
172
173 Ok(network)
174 }
175
176 pub fn apply_egress_policy(
177 &self,
178 pid: u32,
179 policy: &EgressPolicy,
180 rootless: bool,
181 ) -> Result<()> {
182 egress::apply_egress_policy(pid, &self.effective_dns_servers(), policy, rootless)
183 }
184
185 pub fn cleanup(mut self) -> Result<()> {
186 self.state = self.state.transition(NetworkState::Cleaned)?;
187 self.stop_child()?;
188 self.cleanup_runtime_dir();
189 Ok(())
190 }
191
192 fn effective_dns_servers(&self) -> Vec<String> {
193 if self.config.dns.is_empty() {
194 vec![Self::dns_ip_from_subnet(&self.config.subnet)
195 .unwrap_or_else(|_| "10.0.2.3".to_string())]
196 } else {
197 self.config.dns.clone()
198 }
199 }
200
201 fn configure_port_forwards(&mut self) -> Result<()> {
202 for pf in &self.config.port_forwards {
203 self.add_port_forward(pf)?;
204 }
205 Ok(())
206 }
207
208 fn add_port_forward(&self, pf: &PortForward) -> Result<()> {
209 let mut arguments = serde_json::Map::new();
210 arguments.insert("proto".to_string(), json!(pf.protocol.as_str()));
211 arguments.insert("host_port".to_string(), json!(pf.host_port));
212 arguments.insert("guest_port".to_string(), json!(pf.container_port));
213 if let Some(host_ip) = pf.host_ip {
214 arguments.insert("host_addr".to_string(), json!(host_ip.to_string()));
215 }
216
217 let response = Self::api_request(
218 &self.api_socket_path,
219 &json!({
220 "execute": "add_hostfwd",
221 "arguments": arguments,
222 }),
223 )?;
224
225 if let Some(error) = response.get("error") {
226 return Err(NucleusError::NetworkError(format!(
227 "slirp4netns add_hostfwd failed for {}:{}->{}/{}: {}",
228 pf.host_ip
229 .map(|ip| ip.to_string())
230 .unwrap_or_else(|| "0.0.0.0".to_string()),
231 pf.host_port,
232 pf.container_port,
233 pf.protocol,
234 error
235 )));
236 }
237
238 debug!(
239 "Configured slirp4netns port forward {}:{} -> {}:{}/{}",
240 pf.host_ip
241 .map(|ip| ip.to_string())
242 .unwrap_or_else(|| "0.0.0.0".to_string()),
243 pf.host_port,
244 self.guest_ip,
245 pf.container_port,
246 pf.protocol
247 );
248 Ok(())
249 }
250
251 fn api_request(socket_path: &Path, request: &serde_json::Value) -> Result<serde_json::Value> {
252 let mut stream = UnixStream::connect(socket_path).map_err(|e| {
253 NucleusError::NetworkError(format!(
254 "connect slirp4netns API socket {:?}: {}",
255 socket_path, e
256 ))
257 })?;
258 let payload = serde_json::to_vec(request).map_err(|e| {
259 NucleusError::NetworkError(format!("serialize slirp4netns API request: {}", e))
260 })?;
261 stream.write_all(&payload).map_err(|e| {
262 NucleusError::NetworkError(format!("write slirp4netns API request: {}", e))
263 })?;
264 stream
265 .shutdown(std::net::Shutdown::Write)
266 .map_err(|e| NucleusError::NetworkError(format!("shutdown slirp4netns API: {}", e)))?;
267
268 let mut buf = Vec::new();
269 stream.read_to_end(&mut buf).map_err(|e| {
270 NucleusError::NetworkError(format!("read slirp4netns API response: {}", e))
271 })?;
272
273 serde_json::from_slice(&buf).map_err(|e| {
274 NucleusError::NetworkError(format!(
275 "parse slirp4netns API response '{}': {}",
276 String::from_utf8_lossy(&buf),
277 e
278 ))
279 })
280 }
281
282 fn wait_until_ready(child: &mut Child, ready_read: OwnedFd) -> Result<()> {
283 let mut ready = std::fs::File::from(ready_read);
284 let mut buf = [0u8; 1];
285 match ready.read_exact(&mut buf) {
286 Ok(()) if buf == [b'1'] => Ok(()),
287 Ok(()) => Err(NucleusError::NetworkError(format!(
288 "slirp4netns ready-fd returned unexpected byte {:?}",
289 buf
290 ))),
291 Err(e) => {
292 if let Ok(Some(status)) = child.try_wait() {
293 let detail = status
294 .code()
295 .map(|code| format!("exit code {}", code))
296 .or_else(|| status.signal().map(|sig| format!("signal {}", sig)))
297 .unwrap_or_else(|| "unknown status".to_string());
298 Err(NucleusError::NetworkError(format!(
299 "slirp4netns exited before ready: {}",
300 detail
301 )))
302 } else {
303 Err(NucleusError::NetworkError(format!(
304 "failed waiting for slirp4netns readiness: {}",
305 e
306 )))
307 }
308 }
309 }
310 }
311
312 fn stop_child(&mut self) -> Result<()> {
313 self.exit_signal.take();
314
315 let deadline = Instant::now() + Duration::from_secs(2);
316 loop {
317 match self.child.try_wait() {
318 Ok(Some(_)) => break,
319 Ok(None) if Instant::now() < deadline => {
320 std::thread::sleep(Duration::from_millis(50))
321 }
322 Ok(None) => {
323 self.child.kill().map_err(|e| {
324 NucleusError::NetworkError(format!("kill slirp4netns: {}", e))
325 })?;
326 let _ = self.child.wait();
327 break;
328 }
329 Err(e) => {
330 return Err(NucleusError::NetworkError(format!(
331 "wait for slirp4netns shutdown: {}",
332 e
333 )))
334 }
335 }
336 }
337
338 info!(
339 "Userspace NAT cleaned up for container {}",
340 self.container_id
341 );
342 Ok(())
343 }
344
345 fn cleanup_best_effort(&mut self) {
346 if self.state == NetworkState::Cleaned {
347 return;
348 }
349
350 self.exit_signal.take();
351
352 if let Ok(None) = self.child.try_wait() {
353 let deadline = Instant::now() + Duration::from_secs(1);
354 while Instant::now() < deadline {
355 match self.child.try_wait() {
356 Ok(Some(_)) => break,
357 Ok(None) => std::thread::sleep(Duration::from_millis(25)),
358 Err(_) => break,
359 }
360 }
361
362 if let Ok(None) = self.child.try_wait() {
363 let _ = self.child.kill();
364 let _ = self.child.wait();
365 }
366 }
367
368 self.cleanup_runtime_dir();
369 self.state = NetworkState::Cleaned;
370 debug!(
371 "Userspace NAT cleaned up (best-effort via drop) for container {}",
372 self.container_id
373 );
374 }
375
376 fn cleanup_runtime_dir(&self) {
377 if let Err(e) = std::fs::remove_dir_all(&self.runtime_dir) {
378 if self.runtime_dir.exists() {
379 warn!(
380 "Failed to remove slirp4netns runtime dir {:?}: {}",
381 self.runtime_dir, e
382 );
383 }
384 }
385 }
386
387 fn validate_userspace_config(config: &BridgeConfig, guest_ip: &str) -> Result<()> {
388 let prefix = config
389 .subnet
390 .split_once('/')
391 .and_then(|(_, prefix)| prefix.parse::<u8>().ok())
392 .unwrap_or(24);
393 if prefix > 25 {
394 return Err(NucleusError::NetworkError(format!(
395 "Userspace NAT requires a subnet with at least 128 addresses; '{}' is too small",
396 config.subnet
397 )));
398 }
399
400 if let Some(requested_ip) = config.container_ip.as_deref() {
401 if requested_ip != guest_ip {
402 return Err(NucleusError::NetworkError(format!(
403 "Userspace NAT uses the slirp4netns guest address {}; requested container IP {} is unsupported",
404 guest_ip, requested_ip
405 )));
406 }
407 }
408
409 Ok(())
410 }
411
412 fn should_join_userns(_host_is_root: bool, rootless: bool) -> bool {
413 rootless
414 }
415
416 fn spawn_slirp(
417 slirp_bin: &Path,
418 pid: u32,
419 config: &BridgeConfig,
420 needs_userns: bool,
421 api_socket_path: &Path,
422 enable_sandbox: bool,
423 ) -> Result<(Child, OwnedFd)> {
424 let (ready_read, ready_write) = nix::unistd::pipe()
425 .map_err(|e| NucleusError::NetworkError(format!("ready pipe: {}", e)))?;
426 let (exit_read, exit_write) = nix::unistd::pipe()
427 .map_err(|e| NucleusError::NetworkError(format!("exit pipe: {}", e)))?;
428 Self::clear_cloexec(&ready_write)?;
429 Self::clear_cloexec(&exit_read)?;
430
431 let args = Self::command_args(
432 pid,
433 config,
434 needs_userns,
435 api_socket_path,
436 ready_write.as_raw_fd(),
437 exit_read.as_raw_fd(),
438 enable_sandbox,
439 );
440
441 let mut child = Command::new(slirp_bin)
442 .args(&args)
443 .spawn()
444 .map_err(|e| NucleusError::NetworkError(format!("spawn slirp4netns: {}", e)))?;
445
446 drop(ready_write);
447 drop(exit_read);
448
449 match Self::wait_until_ready(&mut child, ready_read) {
450 Ok(()) => Ok((child, exit_write)),
451 Err(e) => {
452 let _ = child.kill();
453 let _ = child.wait();
454 Err(e)
455 }
456 }
457 }
458
459 fn command_args(
460 pid: u32,
461 config: &BridgeConfig,
462 join_userns: bool,
463 api_socket_path: &Path,
464 ready_fd: i32,
465 exit_fd: i32,
466 enable_sandbox: bool,
467 ) -> Vec<String> {
468 let mut args = vec![
469 "--configure".to_string(),
470 "--ready-fd".to_string(),
471 ready_fd.to_string(),
472 "--exit-fd".to_string(),
473 exit_fd.to_string(),
474 "--api-socket".to_string(),
475 api_socket_path.display().to_string(),
476 "--cidr".to_string(),
477 config.subnet.clone(),
478 "--disable-host-loopback".to_string(),
479 ];
480
481 if enable_sandbox {
482 args.push("--enable-sandbox".to_string());
483 }
484
485 if !config.dns.is_empty() {
486 args.push("--disable-dns".to_string());
487 }
488
489 if join_userns {
490 args.push("--userns-path".to_string());
491 args.push(format!("/proc/{}/ns/user", pid));
492 }
493
494 args.push(pid.to_string());
495 args.push(SLIRP_TAP_NAME.to_string());
496 args
497 }
498
499 fn runtime_dir(container_id: &str) -> PathBuf {
500 let base = if nix::unistd::Uid::effective().is_root() {
501 PathBuf::from("/run/nucleus/userspace-net")
502 } else {
503 dirs::runtime_dir()
504 .map(|dir| dir.join("nucleus/userspace-net"))
505 .or_else(|| dirs::data_local_dir().map(|dir| dir.join("nucleus/userspace-net")))
506 .unwrap_or_else(|| std::env::temp_dir().join("nucleus-userspace-net"))
507 };
508 base.join(container_id)
509 }
510
511 fn ensure_runtime_dir(path: &Path) -> Result<()> {
512 if let Some(parent) = path.parent() {
513 std::fs::create_dir_all(parent).map_err(|e| {
514 NucleusError::NetworkError(format!(
515 "create userspace-net parent dir {:?}: {}",
516 parent, e
517 ))
518 })?;
519 }
520 std::fs::create_dir_all(path).map_err(|e| {
521 NucleusError::NetworkError(format!("create userspace-net dir {:?}: {}", path, e))
522 })?;
523 use std::os::unix::fs::PermissionsExt;
524 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).map_err(|e| {
525 NucleusError::NetworkError(format!(
526 "secure userspace-net dir permissions for {:?}: {}",
527 path, e
528 ))
529 })?;
530 Ok(())
531 }
532
533 fn clear_cloexec(fd: &OwnedFd) -> Result<()> {
534 let flags = fcntl(fd, FcntlArg::F_GETFD).map_err(|e| {
535 NucleusError::NetworkError(format!("fcntl(F_GETFD) on fd {}: {}", fd.as_raw_fd(), e))
536 })?;
537 let fd_flags = FdFlag::from_bits_truncate(flags);
538 let new_flags = fd_flags & !FdFlag::FD_CLOEXEC;
539 fcntl(fd, FcntlArg::F_SETFD(new_flags)).map_err(|e| {
540 NucleusError::NetworkError(format!("fcntl(F_SETFD) on fd {}: {}", fd.as_raw_fd(), e))
541 })?;
542 Ok(())
543 }
544
545 fn guest_ip_from_subnet(subnet: &str) -> Result<String> {
546 Self::offset_ip_from_subnet(subnet, 100).map(|ip| ip.to_string())
547 }
548
549 fn dns_ip_from_subnet(subnet: &str) -> Result<String> {
550 Self::offset_ip_from_subnet(subnet, 3).map(|ip| ip.to_string())
551 }
552
553 fn offset_ip_from_subnet(subnet: &str, offset: u32) -> Result<std::net::Ipv4Addr> {
554 let (base, prefix) = subnet.split_once('/').ok_or_else(|| {
555 NucleusError::NetworkError(format!("Invalid CIDR (missing /prefix): '{}'", subnet))
556 })?;
557 let prefix = prefix.parse::<u8>().map_err(|e| {
558 NucleusError::NetworkError(format!("Invalid CIDR prefix '{}': {}", subnet, e))
559 })?;
560 let base_ip = base.parse::<std::net::Ipv4Addr>().map_err(|e| {
561 NucleusError::NetworkError(format!("Invalid CIDR base '{}': {}", subnet, e))
562 })?;
563
564 let host_capacity = if prefix == 32 {
565 1u64
566 } else {
567 1u64 << (32 - prefix)
568 };
569 if offset as u64 >= host_capacity {
570 return Err(NucleusError::NetworkError(format!(
571 "CIDR '{}' does not have room for host offset {}",
572 subnet, offset
573 )));
574 }
575
576 let candidate = u32::from(base_ip)
577 .checked_add(offset)
578 .ok_or_else(|| NucleusError::NetworkError(format!("CIDR '{}' overflowed", subnet)))?;
579 Ok(std::net::Ipv4Addr::from(candidate))
580 }
581}
582
583impl Drop for UserspaceNetwork {
584 fn drop(&mut self) {
585 self.cleanup_best_effort();
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592 use crate::network::Protocol;
593
594 #[test]
595 fn test_auto_nat_backend_prefers_kernel_for_rootful_hosts() {
596 let cfg = BridgeConfig::default();
597 assert_eq!(cfg.selected_nat_backend(true, false), NatBackend::Kernel);
598 assert_eq!(cfg.selected_nat_backend(true, true), NatBackend::Userspace);
599 assert_eq!(cfg.selected_nat_backend(false, true), NatBackend::Userspace);
600 }
601
602 #[test]
603 fn test_userspace_backend_rejects_too_small_subnets() {
604 let cfg = BridgeConfig {
605 subnet: "10.0.42.0/26".to_string(),
606 ..BridgeConfig::default()
607 };
608
609 let guest_ip = UserspaceNetwork::guest_ip_from_subnet(&cfg.subnet).unwrap_err();
610 assert!(
611 guest_ip.to_string().contains("does not have room"),
612 "unexpected error: {guest_ip}"
613 );
614 }
615
616 #[test]
617 fn test_userspace_backend_rejects_custom_guest_ip() {
618 let cfg = BridgeConfig {
619 container_ip: Some("10.0.42.2".to_string()),
620 ..BridgeConfig::default()
621 };
622
623 let err = UserspaceNetwork::validate_userspace_config(&cfg, "10.0.42.100").unwrap_err();
624 assert!(err
625 .to_string()
626 .contains("requested container IP 10.0.42.2 is unsupported"));
627 }
628
629 #[test]
630 fn test_slirp_command_args_disable_builtin_dns_when_explicit_dns_is_set() {
631 let cfg = BridgeConfig::default().with_dns(vec!["1.1.1.1".to_string()]);
632 let args = UserspaceNetwork::command_args(
633 4242,
634 &cfg,
635 true,
636 Path::new("/tmp/slirp.sock"),
637 5,
638 6,
639 true,
640 );
641
642 assert!(args.iter().any(|arg| arg == "--disable-dns"));
643 assert!(args.iter().any(|arg| arg == "--userns-path"));
644 }
645
646 #[test]
647 fn test_slirp_userns_join_is_kept_for_root_remapped_hosts() {
648 assert!(UserspaceNetwork::should_join_userns(true, true));
649 assert!(UserspaceNetwork::should_join_userns(false, true));
650 assert!(!UserspaceNetwork::should_join_userns(true, false));
651 assert!(!UserspaceNetwork::should_join_userns(false, false));
652 }
653
654 #[test]
655 fn test_slirp_command_args_keep_userns_without_sandbox() {
656 let cfg = BridgeConfig::default();
657 let args = UserspaceNetwork::command_args(
658 4242,
659 &cfg,
660 true,
661 Path::new("/tmp/slirp.sock"),
662 5,
663 6,
664 false,
665 );
666
667 assert!(!args.iter().any(|arg| arg == "--enable-sandbox"));
668 assert!(args.iter().any(|arg| arg == "--userns-path"));
669 assert!(args.iter().any(|arg| arg == "/proc/4242/ns/user"));
670 }
671
672 #[test]
673 fn test_userspace_port_forward_request_uses_slirp_hostfwd_shape() {
674 let pf = PortForward {
675 host_ip: Some(std::net::Ipv4Addr::new(127, 0, 0, 1)),
676 host_port: 8080,
677 container_port: 80,
678 protocol: Protocol::Tcp,
679 };
680
681 let mut arguments = serde_json::Map::new();
682 arguments.insert("proto".to_string(), json!(pf.protocol.as_str()));
683 arguments.insert("host_port".to_string(), json!(pf.host_port));
684 arguments.insert("guest_port".to_string(), json!(pf.container_port));
685 if let Some(host_ip) = pf.host_ip {
686 arguments.insert("host_addr".to_string(), json!(host_ip.to_string()));
687 }
688 let request = json!({
689 "execute": "add_hostfwd",
690 "arguments": arguments,
691 });
692
693 assert_eq!(request["execute"], "add_hostfwd");
694 assert_eq!(request["arguments"]["proto"], "tcp");
695 assert_eq!(request["arguments"]["host_addr"], "127.0.0.1");
696 assert_eq!(request["arguments"]["host_port"], 8080);
697 assert_eq!(request["arguments"]["guest_port"], 80);
698 }
699}