1#![cfg(target_os = "macos")]
2
3use std::collections::VecDeque;
16use std::io::{self, Read, Write};
17use std::net::{Ipv4Addr, SocketAddrV4, TcpListener, TcpStream, UdpSocket};
18use std::os::fd::{FromRawFd, IntoRawFd, RawFd};
19use std::os::unix::net::UnixDatagram;
20use std::path::{Path, PathBuf};
21use std::sync::{
22 atomic::{AtomicBool, AtomicU64, Ordering},
23 Arc,
24};
25use std::time::Duration;
26
27use a3s_box_core::error::{BoxError, Result};
28use smoltcp::iface::{Config, Interface, SocketSet};
29use smoltcp::socket::{tcp, udp};
30use smoltcp::time::Instant;
31use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, IpEndpoint, Ipv4Address};
32
33fn smoltcp_now() -> Instant {
37 use std::time::{SystemTime, UNIX_EPOCH};
38 let us = SystemTime::now()
39 .duration_since(UNIX_EPOCH)
40 .unwrap_or_default()
41 .as_micros() as i64;
42 Instant::from_micros(us)
43}
44
45fn to_smoltcp_ipv4(ip: Ipv4Addr) -> Ipv4Address {
47 Ipv4Address::from(ip)
48}
49
50const GATEWAY_MAC: EthernetAddress = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
54const MAX_FRAME: usize = 1514;
56const EPHEMERAL_BASE: u16 = 49152;
58const STATS_WRITE_INTERVAL: Duration = Duration::from_secs(1);
60
61#[derive(Default)]
62struct NetStats {
63 rx_bytes: AtomicU64,
64 tx_bytes: AtomicU64,
65 rx_packets: AtomicU64,
66 tx_packets: AtomicU64,
67}
68
69struct NetStatsSnapshot {
70 rx_bytes: u64,
71 tx_bytes: u64,
72 rx_packets: u64,
73 tx_packets: u64,
74}
75
76impl NetStats {
77 fn record_rx(&self, bytes: usize) {
78 self.rx_bytes.fetch_add(bytes as u64, Ordering::Relaxed);
79 self.rx_packets.fetch_add(1, Ordering::Relaxed);
80 }
81
82 fn record_tx(&self, bytes: usize) {
83 self.tx_bytes.fetch_add(bytes as u64, Ordering::Relaxed);
84 self.tx_packets.fetch_add(1, Ordering::Relaxed);
85 }
86
87 fn snapshot(&self) -> NetStatsSnapshot {
88 NetStatsSnapshot {
89 rx_bytes: self.rx_bytes.load(Ordering::Relaxed),
90 tx_bytes: self.tx_bytes.load(Ordering::Relaxed),
91 rx_packets: self.rx_packets.load(Ordering::Relaxed),
92 tx_packets: self.tx_packets.load(Ordering::Relaxed),
93 }
94 }
95}
96
97struct UnixgramDevice {
110 socket: UnixDatagram,
111 rx_queue: VecDeque<Vec<u8>>,
112 stats: Arc<NetStats>,
113}
114
115impl UnixgramDevice {
116 fn drain(&mut self) {
118 let mut buf = vec![0u8; MAX_FRAME];
119 for _ in 0..64 {
120 match self.socket.recv(&mut buf) {
121 Ok(n) => {
122 tracing::trace!(
123 bytes = n,
124 "NetProxy received ethernet frame from guest/libkrun"
125 );
126 self.stats.record_tx(n);
127 self.rx_queue.push_back(buf[..n].to_vec())
128 }
129 Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
130 Err(e) => {
131 tracing::warn!(error = %e, "NetProxy: recv from libkrun failed");
132 break;
133 }
134 }
135 }
136 }
137}
138
139struct OwnedRxToken(Vec<u8>);
141
142impl smoltcp::phy::RxToken for OwnedRxToken {
143 fn consume<R, F>(mut self, f: F) -> R
144 where
145 F: FnOnce(&mut [u8]) -> R,
146 {
147 f(&mut self.0)
148 }
149}
150
151struct TxToken {
156 socket: UnixDatagram,
157 stats: Arc<NetStats>,
158}
159
160impl smoltcp::phy::TxToken for TxToken {
161 fn consume<R, F>(self, len: usize, f: F) -> R
162 where
163 F: FnOnce(&mut [u8]) -> R,
164 {
165 let mut buf = vec![0u8; len];
166 let result = f(&mut buf);
167 tracing::trace!(
168 bytes = len,
169 "NetProxy sending ethernet frame to guest/libkrun"
170 );
171 if let Err(e) = self.socket.send(&buf) {
172 tracing::warn!(error = %e, len, "NetProxy: send to libkrun failed");
173 } else {
174 self.stats.record_rx(len);
175 }
176 result
177 }
178}
179
180impl smoltcp::phy::Device for UnixgramDevice {
181 type RxToken<'a>
182 = OwnedRxToken
183 where
184 Self: 'a;
185 type TxToken<'a>
186 = TxToken
187 where
188 Self: 'a;
189
190 fn receive(&mut self, _ts: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
191 let frame = self.rx_queue.pop_front()?;
192 let tx = TxToken {
193 socket: self.socket.try_clone().ok()?,
194 stats: Arc::clone(&self.stats),
195 };
196 Some((OwnedRxToken(frame), tx))
197 }
198
199 fn transmit(&mut self, _ts: Instant) -> Option<Self::TxToken<'_>> {
200 Some(TxToken {
201 socket: self.socket.try_clone().ok()?,
202 stats: Arc::clone(&self.stats),
203 })
204 }
205
206 fn capabilities(&self) -> smoltcp::phy::DeviceCapabilities {
207 let mut caps = smoltcp::phy::DeviceCapabilities::default();
208 caps.medium = smoltcp::phy::Medium::Ethernet;
209 caps.max_transmission_unit = MAX_FRAME;
210 caps
211 }
212}
213
214struct PortForward {
218 listener: TcpListener,
219 guest_ip: Ipv4Addr,
220 guest_port: u16,
221 pending: Vec<(smoltcp::iface::SocketHandle, TcpStream)>,
223 active: Vec<(smoltcp::iface::SocketHandle, TcpStream)>,
225}
226
227struct ProxyEngineConfig {
230 socket: UnixDatagram,
231 gateway_ip: Ipv4Addr,
232 prefix_len: u8,
233 dns_servers: Vec<Ipv4Addr>,
234 port_forwards: Vec<PortForward>,
235 shutdown: Arc<AtomicBool>,
236 stats: Arc<NetStats>,
237 stats_path: Option<PathBuf>,
238}
239
240struct ProxyEngine {
241 device: UnixgramDevice,
242 iface: Interface,
243 sockets: SocketSet<'static>,
244 dns_handle: smoltcp::iface::SocketHandle,
245 dns_servers: Vec<Ipv4Addr>,
246 port_forwards: Vec<PortForward>,
247 next_ephemeral: u16,
248 shutdown: Arc<AtomicBool>,
249 stats: Arc<NetStats>,
250 stats_path: Option<PathBuf>,
251 last_stats_write: std::time::Instant,
252}
253
254impl ProxyEngine {
255 fn new(config: ProxyEngineConfig) -> Self {
256 let ProxyEngineConfig {
257 socket,
258 gateway_ip,
259 prefix_len,
260 dns_servers,
261 port_forwards,
262 shutdown,
263 stats,
264 stats_path,
265 } = config;
266
267 let mut device = UnixgramDevice {
268 socket,
269 rx_queue: VecDeque::new(),
270 stats: Arc::clone(&stats),
271 };
272
273 let config = Config::new(GATEWAY_MAC.into());
275 let mut iface = Interface::new(config, &mut device, smoltcp_now());
276 iface.update_ip_addrs(|addrs| {
277 let cidr = IpCidr::new(IpAddress::Ipv4(to_smoltcp_ipv4(gateway_ip)), prefix_len);
278 addrs.push(cidr).ok();
279 });
280
281 let mut sockets = SocketSet::new(vec![]);
282
283 let dns_rx = udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY; 16], vec![0u8; 8192]);
285 let dns_tx = udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY; 16], vec![0u8; 8192]);
286 let mut dns_socket = udp::Socket::new(dns_rx, dns_tx);
287 dns_socket.bind(53).ok();
288 let dns_handle = sockets.add(dns_socket);
289
290 Self {
291 device,
292 iface,
293 sockets,
294 dns_handle,
295 dns_servers,
296 port_forwards,
297 next_ephemeral: EPHEMERAL_BASE,
298 shutdown,
299 stats,
300 stats_path,
301 last_stats_write: std::time::Instant::now(),
302 }
303 }
304
305 fn run(&mut self) {
306 self.write_stats_snapshot();
307 loop {
308 if self.shutdown.load(Ordering::Relaxed) {
309 break;
310 }
311
312 let now = smoltcp_now();
313
314 self.device.drain();
316
317 self.accept_connections(now);
319
320 self.iface.poll(now, &mut self.device, &mut self.sockets);
322
323 self.promote_established();
325
326 self.proxy_data();
328
329 self.forward_dns();
331
332 self.cleanup();
334
335 self.maybe_write_stats_snapshot();
337
338 let delay = self
340 .iface
341 .poll_delay(now, &self.sockets)
342 .unwrap_or(smoltcp::time::Duration::from_millis(1));
343 std::thread::sleep(Duration::from_micros(delay.micros().min(5_000)));
344 }
345 self.write_stats_snapshot();
346 }
347
348 fn maybe_write_stats_snapshot(&mut self) {
349 if self.last_stats_write.elapsed() < STATS_WRITE_INTERVAL {
350 return;
351 }
352 self.last_stats_write = std::time::Instant::now();
353 self.write_stats_snapshot();
354 }
355
356 fn write_stats_snapshot(&self) {
357 let Some(path) = self.stats_path.as_deref() else {
358 return;
359 };
360 if let Err(e) = write_stats_file(path, self.stats.snapshot()) {
361 tracing::debug!(error = %e, path = %path.display(), "NetProxy: failed to write stats file");
362 }
363 }
364
365 fn accept_connections(&mut self, now: Instant) {
368 let mut new_conns: Vec<(usize, TcpStream, Ipv4Addr, u16)> = Vec::new();
371 for (i, pf) in self.port_forwards.iter_mut().enumerate() {
372 loop {
373 match pf.listener.accept() {
374 Ok((stream, _)) => {
375 stream.set_nonblocking(true).ok();
376 stream.set_nodelay(true).ok();
377 new_conns.push((i, stream, pf.guest_ip, pf.guest_port));
378 }
379 Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
380 Err(e) => {
381 tracing::warn!(error = %e, "Port-forward listener error");
382 break;
383 }
384 }
385 }
386 }
387
388 for (i, stream, guest_ip, guest_port) in new_conns {
390 let handle = self.open_guest_tcp(guest_ip, guest_port, now);
391 self.port_forwards[i].pending.push((handle, stream));
392 tracing::debug!(
393 guest = %guest_ip,
394 port = guest_port,
395 handle = ?handle,
396 "NetProxy accepted host connection and initiated guest TCP connect"
397 );
398 }
399 }
400
401 fn open_guest_tcp(
403 &mut self,
404 guest_ip: Ipv4Addr,
405 guest_port: u16,
406 _now: Instant,
407 ) -> smoltcp::iface::SocketHandle {
408 let rx = tcp::SocketBuffer::new(vec![0u8; 65536]);
409 let tx = tcp::SocketBuffer::new(vec![0u8; 65536]);
410 let mut socket = tcp::Socket::new(rx, tx);
411
412 let local_port = self.next_ephemeral;
413 self.next_ephemeral = self.next_ephemeral.wrapping_add(1);
414 if self.next_ephemeral < EPHEMERAL_BASE {
415 self.next_ephemeral = EPHEMERAL_BASE;
416 }
417
418 let remote = IpEndpoint::new(IpAddress::Ipv4(to_smoltcp_ipv4(guest_ip)), guest_port);
419 socket
420 .connect(self.iface.context(), remote, local_port)
421 .ok();
422 socket.set_keep_alive(Some(smoltcp::time::Duration::from_secs(30)));
423
424 self.sockets.add(socket)
425 }
426
427 fn promote_established(&mut self) {
430 for pf in &mut self.port_forwards {
431 let mut still_pending = Vec::new();
432 for (handle, stream) in pf.pending.drain(..) {
433 let socket = self.sockets.get::<tcp::Socket>(handle);
434 use smoltcp::socket::tcp::State;
435 match socket.state() {
436 State::Established => {
437 tracing::debug!(handle = ?handle, "NetProxy guest TCP connection established");
438 pf.active.push((handle, stream));
439 }
440 State::Closed | State::TimeWait | State::CloseWait => {
441 tracing::debug!(handle = ?handle, state = ?socket.state(), "NetProxy guest TCP connection closed before establishment");
442 drop(stream);
444 self.sockets.remove(handle);
445 }
446 _ => {
447 still_pending.push((handle, stream));
448 }
449 }
450 }
451 pf.pending = still_pending;
452 }
453 }
454
455 fn proxy_data(&mut self) {
458 for pf in &mut self.port_forwards {
459 for (handle, host_stream) in &mut pf.active {
460 let socket = self.sockets.get_mut::<tcp::Socket>(*handle);
461
462 if socket.can_recv() {
464 socket
465 .recv(|data| {
466 tracing::trace!(handle = ?*handle, bytes = data.len(), "NetProxy forwarding guest -> host bytes");
467 let _ = host_stream.write_all(data);
468 (data.len(), ())
469 })
470 .ok();
471 }
472
473 if socket.can_send() {
475 let mut buf = [0u8; 8192];
476 match host_stream.read(&mut buf) {
477 Ok(0) => {
478 tracing::debug!(handle = ?*handle, "NetProxy host side closed connection");
479 socket.close();
480 }
481 Ok(n) => {
482 tracing::trace!(handle = ?*handle, bytes = n, "NetProxy forwarding host -> guest bytes");
483 socket.send_slice(&buf[..n]).ok();
484 }
485 Err(e) if e.kind() == io::ErrorKind::WouldBlock => {}
486 Err(_) => {
487 socket.close();
488 }
489 }
490 }
491 }
492 }
493 }
494
495 fn forward_dns(&mut self) {
498 let dns_server = match self.dns_servers.first() {
499 Some(s) => *s,
500 None => return,
501 };
502
503 let socket = self.sockets.get_mut::<udp::Socket>(self.dns_handle);
504 if !socket.can_recv() {
505 return;
506 }
507 let (query, src_endpoint) = match socket.recv() {
508 Ok(r) => r,
509 Err(_) => return,
510 };
511 let query = query.to_vec();
512 let src = src_endpoint;
513
514 match UdpSocket::bind("0.0.0.0:0") {
516 Ok(udp) => {
517 udp.set_read_timeout(Some(Duration::from_secs(2))).ok();
518 let dest = SocketAddrV4::new(dns_server, 53);
519 if udp.send_to(&query, dest).is_ok() {
520 let mut resp = vec![0u8; 4096];
521 if let Ok((n, _)) = udp.recv_from(&mut resp) {
522 let socket = self.sockets.get_mut::<udp::Socket>(self.dns_handle);
523 socket.send_slice(&resp[..n], src).ok();
524 }
525 }
526 }
527 Err(e) => {
528 tracing::warn!(error = %e, "DNS forward: failed to bind host UDP socket");
529 }
530 }
531 }
532
533 fn cleanup(&mut self) {
536 use smoltcp::socket::tcp::State;
537 for pf in &mut self.port_forwards {
538 let mut to_remove = Vec::new();
540 pf.active.retain(|(handle, _stream)| {
541 let state = self.sockets.get::<tcp::Socket>(*handle).state();
542 match state {
543 State::Closed | State::TimeWait | State::CloseWait => {
544 to_remove.push(*handle);
545 false
546 }
547 _ => true,
548 }
549 });
550 for h in to_remove {
551 self.sockets.remove(h);
552 }
553 }
554 }
555}
556
557pub struct NetProxyManager {
563 socket_path: PathBuf,
564 stats_path: PathBuf,
565 net_socket_fd: Option<RawFd>,
566 net_proxy_fd: Option<RawFd>,
567}
568
569impl NetProxyManager {
570 pub fn new(box_dir: &Path) -> Self {
573 let socket_dir = box_dir.join("sockets");
574 Self {
575 socket_path: socket_dir.join("net.sock"),
576 stats_path: socket_dir.join("net.stats.json"),
577 net_socket_fd: None,
578 net_proxy_fd: None,
579 }
580 }
581
582 pub fn socket_path(&self) -> &Path {
583 &self.socket_path
584 }
585
586 pub fn stats_path(&self) -> &Path {
587 &self.stats_path
588 }
589
590 pub fn net_socket_fd(&self) -> Option<RawFd> {
591 self.net_socket_fd
592 }
593
594 pub fn net_proxy_fd(&self) -> Option<RawFd> {
595 self.net_proxy_fd
596 }
597
598 pub fn spawn(
603 &mut self,
604 _ip: Ipv4Addr,
605 _gateway: Ipv4Addr,
606 _prefix_len: u8,
607 _dns_servers: &[Ipv4Addr],
608 _port_map: &[String],
609 ) -> Result<()> {
610 let (proxy_socket, krun_fd) = socketpair_unixgram()?;
611 self.net_socket_fd = Some(krun_fd);
612 self.net_proxy_fd = Some(proxy_socket.into_raw_fd());
613 Ok(())
614 }
615
616 pub fn stop(&mut self) {
617 if let Some(fd) = self.net_socket_fd.take() {
618 unsafe {
619 libc::close(fd);
620 }
621 }
622 if let Some(fd) = self.net_proxy_fd.take() {
623 unsafe {
624 libc::close(fd);
625 }
626 }
627 std::fs::remove_file(&self.socket_path).ok();
628 std::fs::remove_file(&self.stats_path).ok();
629 }
630
631 pub fn is_running(&mut self) -> bool {
632 self.net_socket_fd.is_some() || self.net_proxy_fd.is_some()
633 }
634}
635
636impl Drop for NetProxyManager {
637 fn drop(&mut self) {
638 self.stop();
639 }
640}
641
642pub fn spawn_inherited_netproxy(
645 fd: RawFd,
646 guest_ip: Ipv4Addr,
647 gateway: Ipv4Addr,
648 prefix_len: u8,
649 dns_servers: &[Ipv4Addr],
650 port_map: &[String],
651 stats_path: Option<PathBuf>,
652) -> Result<()> {
653 let socket = unsafe { UnixDatagram::from_raw_fd(fd) };
654 let port_forwards = parse_port_forwards(port_map, guest_ip)
655 .map_err(|e| BoxError::NetworkError(format!("invalid port_map: {}", e)))?;
656 let dns_servers = dns_servers.to_vec();
657 let shutdown = Arc::new(AtomicBool::new(false));
658 let stats = Arc::new(NetStats::default());
659
660 std::thread::Builder::new()
661 .name("a3s-netproxy".to_string())
662 .spawn(move || {
663 tracing::info!(fd, gateway = %gateway, guest_ip = %guest_ip, stats = ?stats_path, "NetProxy thread started");
664 if let Err(e) = socket.set_nonblocking(true) {
665 tracing::error!(error = %e, "NetProxy: set_nonblocking failed");
666 return;
667 }
668
669 let mut engine = ProxyEngine::new(ProxyEngineConfig {
670 socket,
671 gateway_ip: gateway,
672 prefix_len,
673 dns_servers,
674 port_forwards,
675 shutdown,
676 stats,
677 stats_path,
678 });
679 engine.run();
680 tracing::info!("NetProxy thread exiting");
681 })
682 .map_err(|e| BoxError::NetworkError(format!("failed to spawn netproxy thread: {}", e)))?;
683
684 Ok(())
685}
686
687fn socketpair_unixgram() -> Result<(UnixDatagram, RawFd)> {
688 let mut fds = [-1; 2];
689 let ret = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_DGRAM, 0, fds.as_mut_ptr()) };
690 if ret != 0 {
691 return Err(BoxError::NetworkError(format!(
692 "failed to create unix datagram socketpair: {}",
693 io::Error::last_os_error()
694 )));
695 }
696
697 let proxy_socket = unsafe { UnixDatagram::from_raw_fd(fds[0]) };
698 Ok((proxy_socket, fds[1]))
699}
700
701fn write_stats_file(path: &Path, stats: NetStatsSnapshot) -> io::Result<()> {
702 if let Some(parent) = path.parent() {
703 std::fs::create_dir_all(parent)?;
704 }
705 let updated_at_ms = std::time::SystemTime::now()
706 .duration_since(std::time::UNIX_EPOCH)
707 .unwrap_or_default()
708 .as_millis();
709 let body = format!(
710 "{{\"schema\":\"a3s-box.netproxy.stats.v1\",\"rx_bytes\":{},\"tx_bytes\":{},\"rx_packets\":{},\"tx_packets\":{},\"updated_at_ms\":{}}}\n",
711 stats.rx_bytes, stats.tx_bytes, stats.rx_packets, stats.tx_packets, updated_at_ms
712 );
713 let tmp = path.with_extension("json.tmp");
714 std::fs::write(&tmp, body)?;
715 std::fs::rename(tmp, path)
716}
717
718fn parse_port_forwards(
723 port_map: &[String],
724 guest_ip: Ipv4Addr,
725) -> std::result::Result<Vec<PortForward>, String> {
726 let mut forwards = Vec::new();
727 for entry in port_map {
728 let mapping = a3s_box_core::parse_port_mapping(entry)?;
729 let host_port = mapping.host_port;
730 let guest_port = mapping.guest_port;
731
732 let listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, host_port))
733 .map_err(|e| format!("cannot bind 0.0.0.0:{}: {}", host_port, e))?;
734 listener
735 .set_nonblocking(true)
736 .map_err(|e| format!("set_nonblocking on listener: {}", e))?;
737
738 tracing::info!(
739 host_port,
740 guest_port,
741 guest_ip = %guest_ip,
742 "Port-forward listener ready"
743 );
744 forwards.push(PortForward {
745 listener,
746 guest_ip,
747 guest_port,
748 pending: Vec::new(),
749 active: Vec::new(),
750 });
751 }
752 Ok(forwards)
753}
754
755#[cfg(test)]
758mod tests {
759 use super::*;
760
761 fn port_is_bindable(port: u16) -> bool {
762 TcpListener::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)).is_ok()
763 }
764
765 fn ports_are_bindable(ports: &[u16]) -> bool {
766 ports.iter().copied().all(port_is_bindable)
767 }
768
769 #[test]
770 fn test_smoltcp_now_returns_reasonable_value() {
771 let now = smoltcp_now();
772 assert!(now.micros() > 0);
774 }
775
776 #[test]
777 fn test_to_smoltcp_ipv4_conversion() {
778 let ip = Ipv4Addr::new(10, 88, 0, 1);
779 let smol_ip = to_smoltcp_ipv4(ip);
780 assert_eq!(smol_ip.as_bytes(), &[10, 88, 0, 1]);
781 }
782
783 #[test]
784 fn test_to_smoltcp_ipv4_loopback() {
785 let ip = Ipv4Addr::new(127, 0, 0, 1);
786 let smol_ip = to_smoltcp_ipv4(ip);
787 assert_eq!(smol_ip.as_bytes(), &[127, 0, 0, 1]);
788 }
789
790 #[test]
791 fn test_net_stats_records_bytes_and_packets() {
792 let stats = NetStats::default();
793
794 stats.record_rx(64);
795 stats.record_rx(128);
796 stats.record_tx(512);
797
798 let snapshot = stats.snapshot();
799 assert_eq!(snapshot.rx_bytes, 192);
800 assert_eq!(snapshot.rx_packets, 2);
801 assert_eq!(snapshot.tx_bytes, 512);
802 assert_eq!(snapshot.tx_packets, 1);
803 }
804
805 #[test]
806 fn test_parse_port_forwards_empty_rules() {
807 let guest = Ipv4Addr::new(10, 89, 0, 2);
808 let fwds = parse_port_forwards(&[], guest).unwrap();
809 assert!(fwds.is_empty());
810 }
811
812 #[test]
813 fn test_parse_port_forwards_rejects_udp_suffix() {
814 let guest = Ipv4Addr::new(10, 89, 0, 2);
815 let rules = vec!["19990:80/udp".to_string()];
816 let error = match parse_port_forwards(&rules, guest) {
817 Ok(_) => panic!("UDP port mapping unexpectedly succeeded"),
818 Err(error) => error,
819 };
820
821 assert!(error.contains("only TCP is supported"));
822 }
823
824 #[test]
825 fn test_parse_port_forwards_multiple_rules() {
826 let guest = Ipv4Addr::new(10, 89, 0, 2);
827 if !ports_are_bindable(&[19991, 19992, 19993]) {
828 eprintln!("skipping test: one or more host ports are not bindable");
829 return;
830 }
831 let rules = vec![
832 "19991:80".to_string(),
833 "19992:443".to_string(),
834 "19993:8080".to_string(),
835 ];
836 let fwds = parse_port_forwards(&rules, guest).unwrap();
837 assert_eq!(fwds.len(), 3);
838 assert_eq!(fwds[0].guest_port, 80);
839 assert_eq!(fwds[1].guest_port, 443);
840 assert_eq!(fwds[2].guest_port, 8080);
841 }
842
843 #[test]
844 fn test_parse_port_forwards_empty_string() {
845 let guest = Ipv4Addr::new(10, 89, 0, 2);
846 let rules = vec!["".to_string()];
848 let result = parse_port_forwards(&rules, guest);
849 assert!(result.is_err());
850 }
851
852 #[test]
853 fn test_netproxy_manager_new() {
854 let dir = tempfile::tempdir().unwrap();
855 let mgr = NetProxyManager::new(dir.path());
856 assert_eq!(
857 mgr.socket_path(),
858 dir.path().join("sockets").join("net.sock")
859 );
860 assert_eq!(
861 mgr.stats_path(),
862 dir.path().join("sockets").join("net.stats.json")
863 );
864 assert_eq!(mgr.net_socket_fd(), None);
865 }
866
867 #[test]
868 fn test_netproxy_manager_not_running_initially() {
869 let dir = tempfile::tempdir().unwrap();
870 let mut mgr = NetProxyManager::new(dir.path());
871 assert!(!mgr.is_running());
872 }
873
874 #[test]
875 fn test_netproxy_manager_stop_when_not_started() {
876 let dir = tempfile::tempdir().unwrap();
877 let mut mgr = NetProxyManager::new(dir.path());
878 mgr.stop(); assert!(!mgr.is_running());
880 }
881
882 #[test]
883 fn test_netproxy_manager_spawn_creates_socketpair_fds_and_stop_closes_them() {
884 let dir = tempfile::tempdir().unwrap();
885 let mut mgr = NetProxyManager::new(dir.path());
886
887 mgr.spawn(
888 Ipv4Addr::new(10, 89, 0, 2),
889 Ipv4Addr::new(10, 89, 0, 1),
890 24,
891 &[Ipv4Addr::new(8, 8, 8, 8)],
892 &[],
893 )
894 .unwrap();
895
896 assert!(mgr.is_running());
897 assert!(mgr.net_socket_fd().is_some());
898 assert!(mgr.net_proxy_fd().is_some());
899
900 mgr.stop();
901 assert!(!mgr.is_running());
902 assert!(mgr.net_socket_fd().is_none());
903 assert!(mgr.net_proxy_fd().is_none());
904 }
905
906 #[test]
907 fn test_netproxy_manager_drop_cleans_up() {
908 let dir = tempfile::tempdir().unwrap();
909 let socket_path = dir.path().join("sockets").join("net.sock");
910 std::fs::create_dir_all(dir.path().join("sockets")).unwrap();
911 std::fs::write(&socket_path, "fake").unwrap();
912 {
913 let _mgr = NetProxyManager::new(dir.path());
914 }
916 assert!(!socket_path.exists());
917 }
918
919 #[test]
920 fn test_write_stats_file_writes_json_snapshot() {
921 let dir = tempfile::tempdir().unwrap();
922 let path = dir.path().join("sockets").join("net.stats.json");
923
924 write_stats_file(
925 &path,
926 NetStatsSnapshot {
927 rx_bytes: 1024,
928 tx_bytes: 2048,
929 rx_packets: 3,
930 tx_packets: 4,
931 },
932 )
933 .unwrap();
934
935 let json: serde_json::Value =
936 serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
937 assert_eq!(json["schema"], "a3s-box.netproxy.stats.v1");
938 assert_eq!(json["rx_bytes"], 1024);
939 assert_eq!(json["tx_bytes"], 2048);
940 assert_eq!(json["rx_packets"], 3);
941 assert_eq!(json["tx_packets"], 4);
942 }
943
944 #[test]
945 fn test_write_stats_file_overwrites_existing_file_and_removes_temp() {
946 let dir = tempfile::tempdir().unwrap();
947 let path = dir.path().join("net.stats.json");
948 let tmp = path.with_extension("json.tmp");
949 std::fs::write(&path, "old").unwrap();
950 std::fs::write(&tmp, "stale temp").unwrap();
951
952 write_stats_file(
953 &path,
954 NetStatsSnapshot {
955 rx_bytes: 1,
956 tx_bytes: 2,
957 rx_packets: 3,
958 tx_packets: 4,
959 },
960 )
961 .unwrap();
962
963 let json: serde_json::Value =
964 serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
965 assert_eq!(json["rx_bytes"], 1);
966 assert_eq!(json["tx_bytes"], 2);
967 assert!(!tmp.exists());
968 }
969
970 #[test]
971 fn test_parse_port_forwards_valid() {
972 let guest = Ipv4Addr::new(10, 89, 0, 2);
973 if !ports_are_bindable(&[19988, 19443]) {
974 eprintln!("skipping test: one or more host ports are not bindable");
975 return;
976 }
977 let rules = vec!["19988:80".to_string(), "19443:443".to_string()];
979 let fwds = parse_port_forwards(&rules, guest).unwrap();
980 assert_eq!(fwds.len(), 2);
981 assert_eq!(fwds[0].guest_port, 80);
982 assert_eq!(fwds[1].guest_port, 443);
983 }
984
985 #[test]
986 fn test_parse_port_forwards_with_protocol_suffix() {
987 let guest = Ipv4Addr::new(10, 89, 0, 2);
988 if !port_is_bindable(19989) {
989 eprintln!("skipping test: host port 19989 is not bindable");
990 return;
991 }
992 let rules = vec!["19989:80/tcp".to_string()];
993 let fwds = parse_port_forwards(&rules, guest).unwrap();
994 assert_eq!(fwds[0].guest_port, 80);
995 }
996
997 #[test]
998 fn test_parse_port_forwards_invalid_format() {
999 let guest = Ipv4Addr::new(10, 89, 0, 2);
1000 assert!(parse_port_forwards(&["notaport".to_string()], guest).is_err());
1001 assert!(parse_port_forwards(&["abc:80".to_string()], guest).is_err());
1002 assert!(parse_port_forwards(&["80:xyz".to_string()], guest).is_err());
1003 }
1004
1005 #[test]
1006 fn test_parse_port_forwards_reports_bind_conflict() {
1007 let guest = Ipv4Addr::new(10, 89, 0, 2);
1008 let held = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)).unwrap();
1009 let port = held.local_addr().unwrap().port();
1010
1011 let error = match parse_port_forwards(&[format!("{port}:80")], guest) {
1012 Ok(_) => panic!("port-forward bind conflict should return an error"),
1013 Err(error) => error,
1014 };
1015
1016 assert!(error.contains(&format!("cannot bind 0.0.0.0:{port}")));
1017 }
1018
1019 }