1use std::collections::HashMap;
27use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
28use std::time::Instant;
29
30use std::sync::Arc;
31
32use socket2::SockRef;
33use tokio::net::{TcpListener, UdpSocket};
34use tokio::sync::mpsc;
35use tokio::task::JoinHandle;
36use tokio_util::sync::CancellationToken;
37
38use arcbox_packet::ethernet::{ETH_HEADER_LEN, build_udp_ip_ethernet};
39
40const INBOUND_TCP_BUF_SIZE: usize = 4 * 1024 * 1024;
50
51const EPHEMERAL_START: u16 = 61000;
57const EPHEMERAL_END: u16 = 65535;
59
60pub(crate) struct EphemeralPorts {
62 next: u16,
63}
64
65impl EphemeralPorts {
66 pub(crate) fn new() -> Self {
67 Self {
68 next: EPHEMERAL_START,
69 }
70 }
71
72 pub(crate) fn allocate(&mut self) -> u16 {
74 let port = self.next;
75 self.next = if self.next == EPHEMERAL_END {
76 EPHEMERAL_START
77 } else {
78 self.next + 1
79 };
80 port
81 }
82
83 #[inline]
85 pub(crate) fn in_range(port: u16) -> bool {
86 (EPHEMERAL_START..=EPHEMERAL_END).contains(&port)
87 }
88}
89
90pub enum InboundCommand {
96 TcpAccepted {
98 host_port: u16,
99 container_port: u16,
100 stream: tokio::net::TcpStream,
101 },
102 UdpReceived {
104 host_port: u16,
105 container_port: u16,
106 data: Vec<u8>,
107 reply_tx: mpsc::Sender<Vec<u8>>,
109 client_addr: SocketAddr,
110 },
111}
112
113struct InboundUdpFlow {
119 client_tx: mpsc::Sender<Vec<u8>>,
121 last_active: Instant,
123}
124
125pub(crate) struct InboundRelay {
132 udp_flows: HashMap<(Ipv4Addr, u16, Ipv4Addr, u16), InboundUdpFlow>,
134 reply_tx: mpsc::Sender<Vec<u8>>,
136 gateway_mac: [u8; 6],
137 gateway_ip: Ipv4Addr,
138 guest_ip: Ipv4Addr,
139 mtu: usize,
141 ephemeral_ports: EphemeralPorts,
142}
143
144impl InboundRelay {
145 pub(crate) fn new(
146 reply_tx: mpsc::Sender<Vec<u8>>,
147 gateway_mac: [u8; 6],
148 gateway_ip: Ipv4Addr,
149 guest_ip: Ipv4Addr,
150 mtu: usize,
151 ) -> Self {
152 Self {
153 udp_flows: HashMap::new(),
154 reply_tx,
155 gateway_mac,
156 gateway_ip,
157 guest_ip,
158 mtu,
159 ephemeral_ports: EphemeralPorts::new(),
160 }
161 }
162
163 pub(crate) fn try_handle_reply(&mut self, frame: &[u8], _guest_mac: [u8; 6]) -> bool {
173 if frame.len() < ETH_HEADER_LEN + 20 {
174 return false;
175 }
176
177 let ip_start = ETH_HEADER_LEN;
178 let protocol = frame[ip_start + 9];
179
180 let ihl = ((frame[ip_start] & 0x0F) as usize) * 4;
181 let l4_start = ip_start + ihl;
182
183 match protocol {
184 6 => false, 17 => self.try_handle_udp_reply(frame, ip_start, l4_start),
186 _ => false,
187 }
188 }
189
190 fn try_handle_udp_reply(&mut self, frame: &[u8], ip_start: usize, udp_start: usize) -> bool {
192 if frame.len() < udp_start + 8 {
193 return false;
194 }
195
196 let dst_port = u16::from_be_bytes([frame[udp_start + 2], frame[udp_start + 3]]);
197 if !EphemeralPorts::in_range(dst_port) {
198 return false;
199 }
200
201 let src_ip = Ipv4Addr::new(
202 frame[ip_start + 12],
203 frame[ip_start + 13],
204 frame[ip_start + 14],
205 frame[ip_start + 15],
206 );
207 let dst_ip = Ipv4Addr::new(
208 frame[ip_start + 16],
209 frame[ip_start + 17],
210 frame[ip_start + 18],
211 frame[ip_start + 19],
212 );
213 let src_port = u16::from_be_bytes([frame[udp_start], frame[udp_start + 1]]);
214
215 let key = (dst_ip, dst_port, src_ip, src_port);
216
217 if let Some(flow) = self.udp_flows.get_mut(&key) {
218 let udp_len = u16::from_be_bytes([frame[udp_start + 4], frame[udp_start + 5]]) as usize;
219 if udp_len >= 8 && udp_start + udp_len <= frame.len() {
220 let payload = frame[udp_start + 8..udp_start + udp_len].to_vec();
221 flow.last_active = Instant::now();
222 let _ = flow.client_tx.try_send(payload);
223 }
224 return true;
225 }
226
227 false
228 }
229
230 pub(crate) fn inject_udp(
236 &mut self,
237 container_port: u16,
238 data: &[u8],
239 client_tx: mpsc::Sender<Vec<u8>>,
240 guest_mac: [u8; 6],
241 ) {
242 let ephemeral_port = self.ephemeral_ports.allocate();
243 let key = (
244 self.gateway_ip,
245 ephemeral_port,
246 self.guest_ip,
247 container_port,
248 );
249
250 self.udp_flows.insert(
251 key,
252 InboundUdpFlow {
253 client_tx,
254 last_active: Instant::now(),
255 },
256 );
257
258 let frames = build_udp_ip_ethernet(
259 self.gateway_ip,
260 self.guest_ip,
261 ephemeral_port,
262 container_port,
263 data,
264 self.gateway_mac,
265 guest_mac,
266 self.mtu,
267 );
268
269 for frame in frames {
270 if self.reply_tx.try_send(frame).is_err() {
271 break;
273 }
274 }
275
276 tracing::debug!(
277 "Inbound UDP: injected {} bytes gw:{} → guest:{}",
278 data.len(),
279 ephemeral_port,
280 container_port,
281 );
282 }
283
284 pub(crate) fn cleanup(&mut self) {
290 let now = Instant::now();
291 self.udp_flows
292 .retain(|_, flow| now.duration_since(flow.last_active).as_secs() < 60);
293 }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
302pub enum InboundProtocol {
303 Tcp,
304 Udp,
305}
306
307pub struct InboundListenerManager {
310 cmd_tx: mpsc::Sender<InboundCommand>,
311 listeners: HashMap<(Ipv4Addr, u16, InboundProtocol), (JoinHandle<()>, CancellationToken)>,
312}
313
314impl InboundListenerManager {
315 #[must_use]
317 pub fn new(cmd_tx: mpsc::Sender<InboundCommand>) -> Self {
318 Self {
319 cmd_tx,
320 listeners: HashMap::new(),
321 }
322 }
323
324 pub async fn add_rule(
330 &mut self,
331 host_ip: Ipv4Addr,
332 host_port: u16,
333 container_port: u16,
334 protocol: InboundProtocol,
335 ) -> std::io::Result<()> {
336 let key = (host_ip, host_port, protocol);
337 if self.listeners.contains_key(&key) {
338 return Ok(()); }
340
341 let cancel = CancellationToken::new();
342 let cmd_tx = self.cmd_tx.clone();
343
344 let handle = match protocol {
345 InboundProtocol::Tcp => {
346 let listener =
347 TcpListener::bind(SocketAddr::V4(SocketAddrV4::new(host_ip, host_port)))
348 .await?;
349 tracing::info!(
350 "Inbound listener: TCP {}:{} → container :{}",
351 host_ip,
352 host_port,
353 container_port,
354 );
355 let cancel_clone = cancel.clone();
356 tokio::spawn(async move {
357 tcp_listener_task(listener, container_port, cmd_tx, cancel_clone).await;
358 })
359 }
360 InboundProtocol::Udp => {
361 let socket =
362 UdpSocket::bind(SocketAddr::V4(SocketAddrV4::new(host_ip, host_port))).await?;
363 tracing::info!(
364 "Inbound listener: UDP {}:{} → container :{}",
365 host_ip,
366 host_port,
367 container_port,
368 );
369 let cancel_clone = cancel.clone();
370 tokio::spawn(async move {
371 udp_listener_task(socket, container_port, cmd_tx, cancel_clone).await;
372 })
373 }
374 };
375
376 self.listeners.insert(key, (handle, cancel));
377 Ok(())
378 }
379
380 pub fn remove_rule(&mut self, host_ip: Ipv4Addr, host_port: u16, protocol: InboundProtocol) {
382 let key = (host_ip, host_port, protocol);
383 if let Some((handle, cancel)) = self.listeners.remove(&key) {
384 cancel.cancel();
385 handle.abort();
386 tracing::debug!(
387 "Inbound listener removed: {:?} {}:{}",
388 protocol,
389 host_ip,
390 host_port
391 );
392 }
393 }
394
395 pub fn stop_all(&mut self) {
397 for ((ip, port, proto), (handle, cancel)) in self.listeners.drain() {
398 cancel.cancel();
399 handle.abort();
400 tracing::debug!("Inbound listener stopped: {:?} {}:{}", proto, ip, port);
401 }
402 }
403}
404
405async fn tcp_listener_task(
411 listener: TcpListener,
412 container_port: u16,
413 cmd_tx: mpsc::Sender<InboundCommand>,
414 cancel: CancellationToken,
415) {
416 let host_port = listener.local_addr().map_or(0, |a| a.port());
417 loop {
418 tokio::select! {
419 biased;
420 () = cancel.cancelled() => break,
421 result = listener.accept() => {
422 match result {
423 Ok((stream, peer)) => {
424 tracing::debug!(
425 "Inbound TCP accept: {} → host:{} → container:{}",
426 peer, host_port, container_port,
427 );
428 let sock = SockRef::from(&stream);
432 if let Err(e) = sock.set_recv_buffer_size(INBOUND_TCP_BUF_SIZE) {
433 tracing::warn!("Failed to set SO_RCVBUF on inbound stream: {e}");
434 }
435 if let Err(e) = sock.set_send_buffer_size(INBOUND_TCP_BUF_SIZE) {
436 tracing::warn!("Failed to set SO_SNDBUF on inbound stream: {e}");
437 }
438 let cmd = InboundCommand::TcpAccepted {
439 host_port,
440 container_port,
441 stream,
442 };
443 if cmd_tx.send(cmd).await.is_err() {
444 break;
445 }
446 }
447 Err(e) => {
448 tracing::warn!("Inbound TCP accept error on :{}: {}", host_port, e);
449 }
450 }
451 }
452 }
453 }
454}
455
456async fn udp_listener_task(
458 socket: UdpSocket,
459 container_port: u16,
460 cmd_tx: mpsc::Sender<InboundCommand>,
461 cancel: CancellationToken,
462) {
463 let host_port = socket.local_addr().map_or(0, |a| a.port());
464 let socket = Arc::new(socket);
465 let mut reply_flows: HashMap<SocketAddr, mpsc::Sender<Vec<u8>>> = HashMap::new();
466 let mut buf = vec![0u8; 65535];
467
468 loop {
469 tokio::select! {
470 biased;
471 () = cancel.cancelled() => break,
472 result = socket.recv_from(&mut buf) => {
473 match result {
474 Ok((n, client_addr)) => {
475 let reply_tx = if let Some(tx) = reply_flows.get(&client_addr) {
476 if tx.is_closed() {
477 reply_flows.remove(&client_addr);
478 create_udp_reply_flow(client_addr, &socket, &cancel, &mut reply_flows)
479 } else {
480 tx.clone()
481 }
482 } else {
483 create_udp_reply_flow(client_addr, &socket, &cancel, &mut reply_flows)
484 };
485
486 let cmd = InboundCommand::UdpReceived {
487 host_port,
488 container_port,
489 data: buf[..n].to_vec(),
490 reply_tx,
491 client_addr,
492 };
493 if cmd_tx.send(cmd).await.is_err() {
494 break;
495 }
496 }
497 Err(e) => {
498 tracing::warn!("Inbound UDP recv error on :{}: {}", host_port, e);
499 }
500 }
501 }
502 }
503 }
504}
505
506fn create_udp_reply_flow(
507 client_addr: SocketAddr,
508 socket: &Arc<UdpSocket>,
509 cancel: &CancellationToken,
510 reply_flows: &mut HashMap<SocketAddr, mpsc::Sender<Vec<u8>>>,
511) -> mpsc::Sender<Vec<u8>> {
512 let (reply_tx, mut reply_rx) = mpsc::channel::<Vec<u8>>(16);
513 let reply_sock = Arc::clone(socket);
514 let flow_cancel = cancel.clone();
515 tokio::spawn(async move {
516 loop {
517 tokio::select! {
518 biased;
519 () = flow_cancel.cancelled() => break,
520 maybe_data = reply_rx.recv() => {
521 let Some(data) = maybe_data else {
522 break;
523 };
524 let _ = reply_sock.send_to(&data, client_addr).await;
525 }
526 }
527 }
528 });
529 reply_flows.insert(client_addr, reply_tx.clone());
530 reply_tx
531}
532
533#[cfg(test)]
538mod tests {
539 use super::*;
540
541 const GW_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 64, 1);
542 const GUEST_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 64, 2);
543 const GW_MAC: [u8; 6] = [0x02, 0xAB, 0xCD, 0x00, 0x00, 0x01];
544 const GUEST_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x99];
545
546 #[test]
547 fn ephemeral_ports_allocation() {
548 let mut ep = EphemeralPorts::new();
549 assert_eq!(ep.allocate(), 61000);
550 assert_eq!(ep.allocate(), 61001);
551 }
552
553 #[test]
554 fn ephemeral_ports_wrap_around() {
555 let mut ep = EphemeralPorts::new();
556 ep.next = EPHEMERAL_END;
557 assert_eq!(ep.allocate(), EPHEMERAL_END);
558 assert_eq!(ep.allocate(), EPHEMERAL_START);
559 }
560
561 #[test]
562 fn ephemeral_ports_in_range() {
563 assert!(EphemeralPorts::in_range(61000));
564 assert!(EphemeralPorts::in_range(65535));
565 assert!(EphemeralPorts::in_range(63000));
566 assert!(!EphemeralPorts::in_range(60999));
567 assert!(!EphemeralPorts::in_range(32768));
568 assert!(!EphemeralPorts::in_range(80));
569 }
570
571 #[test]
572 fn inbound_relay_rejects_non_ephemeral() {
573 let (tx, _rx) = mpsc::channel(16);
574 let mut relay = InboundRelay::new(tx, GW_MAC, GW_IP, GUEST_IP, 1500);
575
576 let mut frame = vec![0u8; ETH_HEADER_LEN + 40];
578 frame[12..14].copy_from_slice(&0x0800u16.to_be_bytes());
579 let ip = &mut frame[ETH_HEADER_LEN..];
580 ip[0] = 0x45;
581 ip[9] = 6; ip[12..16].copy_from_slice(&GUEST_IP.octets());
583 ip[16..20].copy_from_slice(&GW_IP.octets());
584 let tcp = &mut frame[ETH_HEADER_LEN + 20..];
586 tcp[0..2].copy_from_slice(&8080u16.to_be_bytes());
587 tcp[2..4].copy_from_slice(&80u16.to_be_bytes());
588 tcp[12] = 0x50; assert!(!relay.try_handle_reply(&frame, GUEST_MAC));
591 }
592
593 #[tokio::test]
594 async fn inject_udp_sends_frame_and_tracks_flow() {
595 let (tx, mut rx) = mpsc::channel(16);
596 let mut relay = InboundRelay::new(tx, GW_MAC, GW_IP, GUEST_IP, 1500);
597
598 let (client_tx, _client_rx) = mpsc::channel(16);
599 relay.inject_udp(53, b"dns query", client_tx, GUEST_MAC);
600
601 let key = (GW_IP, EPHEMERAL_START, GUEST_IP, 53);
603 assert!(relay.udp_flows.contains_key(&key));
604
605 let frame = rx.recv().await.expect("should receive UDP frame");
607 assert!(frame.len() >= ETH_HEADER_LEN + 28, "UDP frame too short");
608
609 assert_eq!(frame[ETH_HEADER_LEN + 9], 17);
611
612 let udp_start = ETH_HEADER_LEN + 20;
614 let src_port = u16::from_be_bytes([frame[udp_start], frame[udp_start + 1]]);
615 let dst_port = u16::from_be_bytes([frame[udp_start + 2], frame[udp_start + 3]]);
616 assert_eq!(src_port, EPHEMERAL_START);
617 assert_eq!(dst_port, 53);
618 }
619
620 #[test]
621 fn cleanup_removes_expired_udp_flows() {
622 let (tx, _rx) = mpsc::channel(16);
623 let mut relay = InboundRelay::new(tx, GW_MAC, GW_IP, GUEST_IP, 1500);
624
625 let (client_tx, _client_rx) = mpsc::channel(16);
626 let key = (GW_IP, 61000, GUEST_IP, 53);
627 relay.udp_flows.insert(
628 key,
629 InboundUdpFlow {
630 client_tx,
631 last_active: Instant::now()
632 .checked_sub(std::time::Duration::from_secs(120))
633 .unwrap(),
634 },
635 );
636 assert_eq!(relay.udp_flows.len(), 1);
637
638 relay.cleanup();
639 assert_eq!(
640 relay.udp_flows.len(),
641 0,
642 "expired UDP flow should be removed"
643 );
644 }
645
646 #[tokio::test]
647 async fn listener_manager_add_and_remove_rule() {
648 let (cmd_tx, mut cmd_rx) = mpsc::channel(16);
649 let mut manager = InboundListenerManager::new(cmd_tx);
650
651 manager
653 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
654 .await
655 .expect("should bind to port 0 (OS-assigned)");
656
657 manager.remove_rule(Ipv4Addr::LOCALHOST, 0, InboundProtocol::Tcp);
659
660 assert!(cmd_rx.try_recv().is_err(), "no commands expected yet");
662 }
663
664 #[tokio::test]
665 async fn listener_manager_stop_all() {
666 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
667 let mut manager = InboundListenerManager::new(cmd_tx);
668
669 manager
670 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
671 .await
672 .unwrap();
673 manager
674 .add_rule(Ipv4Addr::LOCALHOST, 0, 53, InboundProtocol::Udp)
675 .await
676 .unwrap();
677
678 manager.stop_all();
679 manager
683 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
684 .await
685 .unwrap();
686 }
687
688 #[tokio::test]
689 async fn same_port_different_ip_coexist() {
690 let (cmd_tx, _cmd_rx) = mpsc::channel(16);
691 let mut manager = InboundListenerManager::new(cmd_tx);
692
693 manager
695 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
696 .await
697 .unwrap();
698 manager
699 .add_rule(Ipv4Addr::UNSPECIFIED, 0, 80, InboundProtocol::Tcp)
700 .await
701 .unwrap();
702
703 manager.remove_rule(Ipv4Addr::LOCALHOST, 0, InboundProtocol::Tcp);
705 manager
706 .add_rule(Ipv4Addr::LOCALHOST, 0, 80, InboundProtocol::Tcp)
707 .await
708 .unwrap();
709 }
710
711 #[test]
712 fn invalid_host_ip_is_rejected() {
713 assert!(
716 "::1".parse::<Ipv4Addr>().is_err(),
717 "IPv6 should fail Ipv4Addr parse"
718 );
719 assert!("not-an-ip".parse::<Ipv4Addr>().is_err());
720 assert!("".parse::<Ipv4Addr>().is_err());
721 assert!("127.0.0.1".parse::<Ipv4Addr>().is_ok());
723 assert!("0.0.0.0".parse::<Ipv4Addr>().is_ok());
724 }
725}