1use std::collections::HashMap;
11use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
12use std::sync::Arc;
13use std::sync::atomic::{AtomicU16, Ordering};
14use std::time::{Duration, Instant};
15
16use bytes::Bytes;
17use parking_lot::Mutex;
18use smoltcp::iface::{Interface, SocketHandle, SocketSet};
19use smoltcp::socket::tcp;
20use smoltcp::wire::{EthernetAddress, IpEndpoint};
21use tokio::io::{AsyncReadExt, AsyncWriteExt};
22use tokio::net::{TcpListener, TcpStream, UdpSocket};
23use tokio::sync::mpsc;
24
25use crate::config::{PortProtocol, PublishedPort};
26use crate::netstack::shared::SharedState;
27use crate::policy::{NetworkPolicy, Protocol};
28use crate::udp::relay::{construct_udp_response, extract_udp_payload};
29
30const TCP_RX_BUF_SIZE: usize = 65536;
36const TCP_TX_BUF_SIZE: usize = 65536;
37
38const CHANNEL_CAPACITY: usize = 32;
40
41const RELAY_BUF_SIZE: usize = 16384;
43
44const UDP_RELAY_BUF_SIZE: usize = 65535;
46
47const UDP_PEER_TIMEOUT: Duration = Duration::from_secs(60);
49
50const UDP_EPHEMERAL_PORT_START: u16 = 49152;
52
53const UDP_EPHEMERAL_PORT_COUNT: usize =
55 (u16::MAX as usize) - (UDP_EPHEMERAL_PORT_START as usize) + 1;
56
57pub struct PortPublisher {
67 inbound_rx: mpsc::Receiver<InboundConnection>,
69 _inbound_tx: mpsc::Sender<InboundConnection>,
71 connections: Vec<InboundRelay>,
73 guest_ip: Option<IpAddr>,
78 guest_ipv4: Option<Ipv4Addr>,
80 guest_ipv6: Option<Ipv6Addr>,
82 ephemeral_port: Arc<AtomicU16>,
84 max_inbound: usize,
86 udp_routes: PublishedUdpRoutes,
88}
89
90struct InboundConnection {
92 stream: TcpStream,
94 guest_port: u16,
96}
97
98type PublishedUdpRoutes = Arc<Mutex<HashMap<u16, Vec<PublishedUdpRoute>>>>;
100
101struct PublishedUdpRoute {
103 bind_addr: SocketAddr,
105 outbound_tx: mpsc::Sender<PublishedUdpOutbound>,
107 peers: Arc<Mutex<PublishedUdpPeers>>,
109}
110
111struct PublishedUdpOutbound {
113 peer: SocketAddr,
114 payload: Bytes,
115}
116
117#[derive(Default)]
119struct PublishedUdpPeers {
120 host_to_guest: HashMap<SocketAddr, PublishedUdpPeer>,
121 guest_to_host: HashMap<SocketAddr, SocketAddr>,
122}
123
124struct PublishedUdpPeer {
126 guest_addr: SocketAddr,
127 last_seen: Instant,
128}
129
130const DEFERRED_CLOSE_LIMIT: u16 = 64;
133
134struct InboundRelay {
136 handle: SocketHandle,
137 to_host: mpsc::Sender<Bytes>,
139 read_buf: Option<Bytes>,
141 from_host: mpsc::Receiver<Bytes>,
143 write_buf: Option<(Bytes, usize)>,
145 close_attempts: u16,
147}
148
149#[derive(Debug, Clone, Copy, Eq, PartialEq)]
150enum BindExposure {
151 Loopback,
153 Wildcard,
155 Interface,
157}
158
159impl PortPublisher {
164 #[allow(clippy::too_many_arguments)]
173 pub fn new(
174 ports: &[PublishedPort],
175 guest_ipv4: Option<Ipv4Addr>,
176 guest_ipv6: Option<Ipv6Addr>,
177 gateway_ipv4: Option<Ipv4Addr>,
178 gateway_ipv6: Option<Ipv6Addr>,
179 gateway_mac: [u8; 6],
180 guest_mac: [u8; 6],
181 policy: Arc<NetworkPolicy>,
182 shared: Arc<SharedState>,
183 tokio_handle: &tokio::runtime::Handle,
184 ) -> Self {
185 let (inbound_tx, inbound_rx) = mpsc::channel(64);
186 let udp_routes = Arc::new(Mutex::new(HashMap::new()));
187 let ephemeral_port = Arc::new(AtomicU16::new(49152));
188
189 let guest_ip = guest_ipv4
190 .map(IpAddr::V4)
191 .or_else(|| guest_ipv6.map(IpAddr::V6));
192
193 if guest_ip.is_some() {
194 Self::spawn_listeners(
195 ports,
196 &inbound_tx,
197 udp_routes.clone(),
198 guest_ipv4,
199 guest_ipv6,
200 gateway_ipv4,
201 gateway_ipv6,
202 ephemeral_port.clone(),
203 gateway_mac,
204 guest_mac,
205 policy,
206 shared,
207 tokio_handle,
208 );
209 } else if !ports.is_empty() {
210 tracing::warn!(
211 count = ports.len(),
212 "skipping published port listeners: guest has no IPv4 or IPv6 address",
213 );
214 }
215
216 Self {
217 inbound_rx,
218 _inbound_tx: inbound_tx,
219 connections: Vec::new(),
220 guest_ip,
221 guest_ipv4,
222 guest_ipv6,
223 ephemeral_port,
224 max_inbound: 256,
225 udp_routes,
226 }
227 }
228
229 pub fn accept_inbound(
234 &mut self,
235 iface: &mut Interface,
236 sockets: &mut SocketSet<'_>,
237 shared: &Arc<SharedState>,
238 tokio_handle: &tokio::runtime::Handle,
239 ) {
240 let Some(guest_ip) = self.guest_ip else {
243 return;
244 };
245
246 while let Ok(conn) = self.inbound_rx.try_recv() {
247 if self.connections.len() >= self.max_inbound {
248 tracing::debug!("published port: max inbound connections reached, rejecting");
249 reject_with_rst(&conn.stream);
250 continue;
251 }
252 let rx_buf = tcp::SocketBuffer::new(vec![0u8; TCP_RX_BUF_SIZE]);
254 let tx_buf = tcp::SocketBuffer::new(vec![0u8; TCP_TX_BUF_SIZE]);
255 let mut socket = tcp::Socket::new(rx_buf, tx_buf);
256
257 let remote = IpEndpoint::new(guest_ip.into(), conn.guest_port);
259 let local_port = self.alloc_ephemeral_port();
260
261 if socket.connect(iface.context(), remote, local_port).is_err() {
262 tracing::debug!(
263 guest_port = conn.guest_port,
264 "failed to connect smoltcp socket to guest",
265 );
266 reject_with_rst(&conn.stream);
267 continue;
268 }
269
270 let handle = sockets.add(socket);
271
272 let (to_host_tx, to_host_rx) = mpsc::channel(CHANNEL_CAPACITY);
274 let (from_host_tx, from_host_rx) = mpsc::channel(CHANNEL_CAPACITY);
275
276 let shared_clone = shared.clone();
278 tokio_handle.spawn(async move {
279 let _ =
280 inbound_relay_task(conn.stream, to_host_rx, from_host_tx, shared_clone).await;
281 });
282
283 self.connections.push(InboundRelay {
284 handle,
285 to_host: to_host_tx,
286 read_buf: None,
287 from_host: from_host_rx,
288 write_buf: None,
289 close_attempts: 0,
290 });
291 }
292 }
293
294 pub fn relay_data(&mut self, sockets: &mut SocketSet<'_>) {
296 let mut relay_buf = [0u8; RELAY_BUF_SIZE];
297
298 for relay in &mut self.connections {
299 let socket = sockets.get_mut::<tcp::Socket>(relay.handle);
300
301 if relay.to_host.is_closed() {
303 write_host_data(socket, relay);
304 if relay.write_buf.is_none() {
305 socket.close();
306 } else {
307 relay.close_attempts += 1;
310 if relay.close_attempts >= DEFERRED_CLOSE_LIMIT {
311 socket.abort();
312 }
313 }
314 continue;
315 }
316
317 if let Some(pending) = relay.read_buf.take()
319 && let Err(unsent) = try_send_to_host_relay(&relay.to_host, pending)
320 {
321 relay.read_buf = Some(unsent);
322 }
323
324 if relay.read_buf.is_none() {
325 while socket.can_recv() {
326 match socket.recv_slice(&mut relay_buf) {
327 Ok(n) if n > 0 => {
328 let data = Bytes::copy_from_slice(&relay_buf[..n]);
329 if let Err(unsent) = try_send_to_host_relay(&relay.to_host, data) {
330 relay.read_buf = Some(unsent);
331 break;
332 }
333 }
334 _ => break,
335 }
336 }
337 }
338
339 write_host_data(socket, relay);
341 }
342 }
343
344 pub fn relay_udp_outbound(&self, frame: &[u8], src: SocketAddr, dst: SocketAddr) -> bool {
350 if !self.is_guest_ip(src.ip()) {
351 return false;
352 }
353
354 let Some(payload) = extract_udp_payload(frame) else {
355 return false;
356 };
357
358 let routes = self.udp_routes.lock();
359 let Some(routes) = routes.get(&src.port()) else {
360 return false;
361 };
362
363 let now = Instant::now();
364 for route in routes {
365 let mut peers = route.peers.lock();
366 cleanup_udp_peer_mappings(&mut peers, now);
367 let Some(peer) = peers.guest_to_host.get(&dst).copied() else {
368 continue;
369 };
370 drop(peers);
371
372 let outbound = PublishedUdpOutbound {
373 peer,
374 payload: Bytes::copy_from_slice(payload),
375 };
376 if route.outbound_tx.try_send(outbound).is_err() {
377 tracing::debug!(
378 bind = %route.bind_addr,
379 peer = %peer,
380 "published UDP reply dropped because outbound queue is unavailable",
381 );
382 }
383 return true;
384 }
385
386 false
387 }
388
389 pub fn cleanup_closed(&mut self, sockets: &mut SocketSet<'_>) {
394 self.connections.retain(|relay| {
395 let socket = sockets.get::<tcp::Socket>(relay.handle);
396 let closed = matches!(socket.state(), tcp::State::Closed);
397 if closed {
398 sockets.remove(relay.handle);
399 }
400 !closed
401 });
402 self.cleanup_udp_peers();
403 }
404
405 #[allow(clippy::too_many_arguments)]
407 fn spawn_listeners(
408 ports: &[PublishedPort],
409 inbound_tx: &mpsc::Sender<InboundConnection>,
410 udp_routes: PublishedUdpRoutes,
411 guest_ipv4: Option<Ipv4Addr>,
412 guest_ipv6: Option<Ipv6Addr>,
413 gateway_ipv4: Option<Ipv4Addr>,
414 gateway_ipv6: Option<Ipv6Addr>,
415 ephemeral_port: Arc<AtomicU16>,
416 gateway_mac: [u8; 6],
417 guest_mac: [u8; 6],
418 policy: Arc<NetworkPolicy>,
419 shared: Arc<SharedState>,
420 tokio_handle: &tokio::runtime::Handle,
421 ) {
422 for port in ports {
423 let bind_addr = SocketAddr::new(port.host_bind, port.host_port);
424 let guest_port = port.guest_port;
425
426 match port.protocol {
427 PortProtocol::Tcp => {
428 let tx = inbound_tx.clone();
429 let policy = policy.clone();
430 let shared = shared.clone();
431 tokio_handle.spawn(async move {
432 if let Err(e) =
433 tcp_listener_task(bind_addr, guest_port, tx, policy, shared).await
434 {
435 tracing::error!(
436 bind = %bind_addr,
437 error = %e,
438 "published TCP port listener failed",
439 );
440 }
441 });
442 }
443 PortProtocol::Udp => {
444 let Some((guest_ip, gateway_ip)) = udp_ips_for_bind(
445 port.host_bind,
446 guest_ipv4,
447 guest_ipv6,
448 gateway_ipv4,
449 gateway_ipv6,
450 ) else {
451 tracing::warn!(
452 bind = %bind_addr,
453 guest_port,
454 "skipping UDP published port: guest has no matching gateway/guest IP family",
455 );
456 continue;
457 };
458
459 let (outbound_tx, outbound_rx) = mpsc::channel(CHANNEL_CAPACITY);
460 let peers = Arc::new(Mutex::new(PublishedUdpPeers::default()));
461 udp_routes
462 .lock()
463 .entry(guest_port)
464 .or_default()
465 .push(PublishedUdpRoute {
466 bind_addr,
467 outbound_tx,
468 peers: peers.clone(),
469 });
470
471 let policy = policy.clone();
472 let shared = shared.clone();
473 let ephemeral_port = ephemeral_port.clone();
474 tokio_handle.spawn(async move {
475 if let Err(e) = udp_listener_task(
476 bind_addr,
477 guest_ip,
478 gateway_ip,
479 guest_port,
480 outbound_rx,
481 peers,
482 ephemeral_port.clone(),
483 policy,
484 shared,
485 EthernetAddress(gateway_mac),
486 EthernetAddress(guest_mac),
487 )
488 .await
489 {
490 tracing::error!(
491 bind = %bind_addr,
492 error = %e,
493 "published UDP port listener failed",
494 );
495 }
496 });
497 }
498 }
499 }
500 }
501
502 fn alloc_ephemeral_port(&self) -> u16 {
503 loop {
504 let port = self.ephemeral_port.fetch_add(1, Ordering::Relaxed);
505 if port == 0 || port < UDP_EPHEMERAL_PORT_START {
507 self.ephemeral_port
508 .store(UDP_EPHEMERAL_PORT_START, Ordering::Relaxed);
509 continue;
510 }
511 return port;
512 }
513 }
514
515 fn cleanup_udp_peers(&self) {
516 let now = Instant::now();
517 for routes in self.udp_routes.lock().values() {
518 for route in routes {
519 cleanup_udp_peer_mappings(&mut route.peers.lock(), now);
520 }
521 }
522 }
523
524 fn is_guest_ip(&self, ip: IpAddr) -> bool {
525 match ip {
526 IpAddr::V4(ip) => self.guest_ipv4 == Some(ip),
527 IpAddr::V6(ip) => self.guest_ipv6 == Some(ip),
528 }
529 }
530}
531
532fn reject_with_rst(stream: &TcpStream) {
547 let _ = socket2::SockRef::from(stream).set_linger(Some(Duration::ZERO));
548}
549
550async fn tcp_listener_task(
556 bind_addr: SocketAddr,
557 guest_port: u16,
558 inbound_tx: mpsc::Sender<InboundConnection>,
559 policy: Arc<NetworkPolicy>,
560 shared: Arc<SharedState>,
561) -> std::io::Result<()> {
562 let listener = TcpListener::bind(bind_addr).await?;
563 log_published_port_listener("TCP", bind_addr, guest_port);
564
565 loop {
566 let (stream, peer) = listener.accept().await?;
567
568 let action = policy.evaluate_ingress(peer, guest_port, Protocol::Tcp, &shared);
570 if action.is_deny() {
571 tracing::debug!(
572 peer = %peer,
573 guest_port,
574 "ingress denied by policy; sending RST",
575 );
576 reject_with_rst(&stream);
577 drop(stream);
578 continue;
579 }
580
581 let conn = InboundConnection { stream, guest_port };
582 if !queue_inbound_connection(&inbound_tx, conn, &shared).await {
583 break; }
585 }
586
587 Ok(())
588}
589
590#[allow(clippy::too_many_arguments)]
593async fn udp_listener_task(
594 bind_addr: SocketAddr,
595 guest_ip: IpAddr,
596 gateway_ip: IpAddr,
597 guest_port: u16,
598 mut outbound_rx: mpsc::Receiver<PublishedUdpOutbound>,
599 peers: Arc<Mutex<PublishedUdpPeers>>,
600 ephemeral_port: Arc<AtomicU16>,
601 policy: Arc<NetworkPolicy>,
602 shared: Arc<SharedState>,
603 gateway_mac: EthernetAddress,
604 guest_mac: EthernetAddress,
605) -> std::io::Result<()> {
606 let socket = UdpSocket::bind(bind_addr).await?;
607 log_published_port_listener("UDP", bind_addr, guest_port);
608
609 let mut buf = vec![0u8; UDP_RELAY_BUF_SIZE];
610 loop {
611 tokio::select! {
612 inbound = socket.recv_from(&mut buf) => {
613 let (n, peer) = inbound?;
614 let action = policy.evaluate_ingress(peer, guest_port, Protocol::Udp, &shared);
615 if action.is_deny() {
616 tracing::debug!(
617 peer = %peer,
618 guest_port,
619 "UDP ingress denied by policy",
620 );
621 continue;
622 }
623
624 let Some(guest_peer) =
625 resolve_udp_guest_peer(peer, gateway_ip, &peers, &ephemeral_port)
626 else {
627 tracing::debug!(
628 peer = %peer,
629 guest_port,
630 "UDP ingress dropped because published-port peer table is full",
631 );
632 continue;
633 };
634 inject_udp_datagram_to_guest(
635 guest_peer,
636 SocketAddr::new(guest_ip, guest_port),
637 &buf[..n],
638 &shared,
639 gateway_mac,
640 guest_mac,
641 );
642 }
643 outbound = outbound_rx.recv() => {
644 let Some(outbound) = outbound else {
645 break;
646 };
647 if let Err(e) = socket.send_to(&outbound.payload, outbound.peer).await {
648 tracing::debug!(
649 peer = %outbound.peer,
650 error = %e,
651 "published UDP send to host peer failed",
652 );
653 }
654 }
655 }
656 }
657
658 Ok(())
659}
660
661fn log_published_port_listener(protocol: &'static str, bind_addr: SocketAddr, guest_port: u16) {
662 match bind_exposure(bind_addr.ip()) {
663 BindExposure::Loopback => {
664 tracing::debug!(
665 protocol,
666 bind = %bind_addr,
667 guest_port,
668 "published port listener started on host loopback",
669 );
670 }
671 BindExposure::Wildcard => {
672 tracing::warn!(
673 protocol,
674 bind = %bind_addr,
675 guest_port,
676 windows_firewall_prompt = cfg!(windows),
677 "published port is listening on all host interfaces",
678 );
679 }
680 BindExposure::Interface => {
681 tracing::warn!(
682 protocol,
683 bind = %bind_addr,
684 guest_port,
685 windows_firewall_prompt = cfg!(windows),
686 "published port is listening on a non-loopback host interface",
687 );
688 }
689 }
690}
691
692fn bind_exposure(ip: IpAddr) -> BindExposure {
693 if ip.is_loopback() {
694 BindExposure::Loopback
695 } else if ip.is_unspecified() {
696 BindExposure::Wildcard
697 } else {
698 BindExposure::Interface
699 }
700}
701
702async fn queue_inbound_connection<T>(
703 inbound_tx: &mpsc::Sender<T>,
704 conn: T,
705 shared: &SharedState,
706) -> bool {
707 if inbound_tx.send(conn).await.is_err() {
708 return false;
709 }
710
711 shared.proxy_wake.wake();
712 true
713}
714
715fn udp_ips_for_bind(
716 host_bind: IpAddr,
717 guest_ipv4: Option<Ipv4Addr>,
718 guest_ipv6: Option<Ipv6Addr>,
719 gateway_ipv4: Option<Ipv4Addr>,
720 gateway_ipv6: Option<Ipv6Addr>,
721) -> Option<(IpAddr, IpAddr)> {
722 match host_bind {
723 IpAddr::V4(_) => Some((IpAddr::V4(guest_ipv4?), IpAddr::V4(gateway_ipv4?))),
724 IpAddr::V6(_) => Some((IpAddr::V6(guest_ipv6?), IpAddr::V6(gateway_ipv6?))),
725 }
726}
727
728fn resolve_udp_guest_peer(
729 host_peer: SocketAddr,
730 gateway_ip: IpAddr,
731 peers: &Arc<Mutex<PublishedUdpPeers>>,
732 ephemeral_port: &AtomicU16,
733) -> Option<SocketAddr> {
734 let now = Instant::now();
735 let mut peers = peers.lock();
736 cleanup_udp_peer_mappings(&mut peers, now);
737
738 if let Some(peer) = peers.host_to_guest.get_mut(&host_peer) {
739 peer.last_seen = now;
740 return Some(peer.guest_addr);
741 }
742
743 let guest_addr = (0..UDP_EPHEMERAL_PORT_COUNT).find_map(|_| {
744 let candidate = SocketAddr::new(gateway_ip, next_ephemeral_port(ephemeral_port));
745 if !peers.guest_to_host.contains_key(&candidate) {
746 Some(candidate)
747 } else {
748 None
749 }
750 })?;
751
752 peers.host_to_guest.insert(
753 host_peer,
754 PublishedUdpPeer {
755 guest_addr,
756 last_seen: now,
757 },
758 );
759 peers.guest_to_host.insert(guest_addr, host_peer);
760 Some(guest_addr)
761}
762
763fn cleanup_udp_peer_mappings(peers: &mut PublishedUdpPeers, now: Instant) {
764 peers
765 .host_to_guest
766 .retain(|_, peer| now.duration_since(peer.last_seen) <= UDP_PEER_TIMEOUT);
767 let host_to_guest = &peers.host_to_guest;
768 peers
769 .guest_to_host
770 .retain(|_, host_peer| host_to_guest.contains_key(host_peer));
771}
772
773fn next_ephemeral_port(ephemeral_port: &AtomicU16) -> u16 {
774 loop {
775 let port = ephemeral_port.fetch_add(1, Ordering::Relaxed);
776 if port == 0 || port < UDP_EPHEMERAL_PORT_START {
777 ephemeral_port.store(UDP_EPHEMERAL_PORT_START, Ordering::Relaxed);
778 continue;
779 }
780 return port;
781 }
782}
783
784fn inject_udp_datagram_to_guest(
785 peer: SocketAddr,
786 guest_dst: SocketAddr,
787 payload: &[u8],
788 shared: &SharedState,
789 gateway_mac: EthernetAddress,
790 guest_mac: EthernetAddress,
791) {
792 let Some(frame) = construct_udp_response(peer, guest_dst, payload, gateway_mac, guest_mac)
793 else {
794 tracing::debug!(
795 peer = %peer,
796 guest = %guest_dst,
797 "published UDP datagram dropped because address families differ",
798 );
799 return;
800 };
801
802 if !shared.push_rx_frame_and_wake(frame) {
803 tracing::debug!("published UDP datagram dropped because rx_ring is full");
804 }
805}
806
807fn try_send_to_host_relay(to_host: &mpsc::Sender<Bytes>, data: Bytes) -> Result<(), Bytes> {
809 to_host.try_send(data).map_err(|err| err.into_inner())
810}
811
812async fn inbound_relay_task(
814 stream: TcpStream,
815 mut to_host_rx: mpsc::Receiver<Bytes>,
816 from_host_tx: mpsc::Sender<Bytes>,
817 shared: Arc<SharedState>,
818) -> std::io::Result<()> {
819 let (mut rx, mut tx) = stream.into_split();
820 let mut buf = vec![0u8; RELAY_BUF_SIZE];
821
822 loop {
823 tokio::select! {
824 data = to_host_rx.recv() => {
826 match data {
827 Some(bytes) => {
828 shared.proxy_wake.wake();
832 if let Err(e) = tx.write_all(&bytes).await {
833 tracing::debug!(error = %e, "write to host client failed");
834 break;
835 }
836 }
837 None => break,
838 }
839 }
840
841 result = rx.read(&mut buf) => {
843 match result {
844 Ok(0) => break,
845 Ok(n) => {
846 let data = Bytes::copy_from_slice(&buf[..n]);
847 if from_host_tx.send(data).await.is_err() {
848 break;
849 }
850 shared.proxy_wake.wake();
851 }
852 Err(e) => {
853 tracing::debug!(error = %e, "read from host client failed");
854 break;
855 }
856 }
857 }
858 }
859 }
860
861 Ok(())
862}
863
864fn write_host_data(socket: &mut tcp::Socket<'_>, relay: &mut InboundRelay) {
866 if let Some((data, offset)) = &mut relay.write_buf {
868 if socket.can_send() {
869 match socket.send_slice(&data[*offset..]) {
870 Ok(written) => {
871 *offset += written;
872 if *offset >= data.len() {
873 relay.write_buf = None;
874 }
875 }
876 Err(_) => return,
877 }
878 } else {
879 return;
880 }
881 }
882
883 while relay.write_buf.is_none() {
885 match relay.from_host.try_recv() {
886 Ok(data) => {
887 if socket.can_send() {
888 match socket.send_slice(&data) {
889 Ok(written) if written < data.len() => {
890 relay.write_buf = Some((data, written));
891 }
892 Err(_) => {
893 relay.write_buf = Some((data, 0));
894 }
895 _ => {}
896 }
897 } else {
898 relay.write_buf = Some((data, 0));
899 }
900 }
901 Err(_) => break,
902 }
903 }
904}
905
906#[cfg(test)]
911mod tests {
912 use super::*;
913
914 #[tokio::test]
915 async fn queue_inbound_connection_wakes_poll_loop() {
916 let shared = SharedState::new(4);
917 shared.proxy_wake.drain();
918
919 let (tx, mut rx) = mpsc::channel(1);
920
921 assert!(queue_inbound_connection(&tx, (), &shared).await);
922 assert!(rx.try_recv().is_ok());
923 assert!(shared.proxy_wake.wait_timeout(Duration::ZERO));
924 }
925
926 #[tokio::test]
927 async fn inbound_relay_wakes_when_to_host_channel_slot_is_freed() {
928 let shared = Arc::new(SharedState::new(4));
929 shared.proxy_wake.drain();
930
931 let listener = TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
932 .await
933 .unwrap();
934 let addr = listener.local_addr().unwrap();
935 let client = tokio::spawn(TcpStream::connect(addr));
936 let (server_stream, _) = listener.accept().await.unwrap();
937 let client = client.await.unwrap().unwrap();
938
939 socket2::SockRef::from(&server_stream)
940 .set_send_buffer_size(4096)
941 .unwrap();
942
943 let (to_host_tx, to_host_rx) = mpsc::channel(1);
944 let (from_host_tx, _from_host_rx) = mpsc::channel(1);
945 let task = tokio::spawn(inbound_relay_task(
946 server_stream,
947 to_host_rx,
948 from_host_tx,
949 shared.clone(),
950 ));
951
952 to_host_tx
953 .send(Bytes::from(vec![b'a'; 64 * 1024 * 1024]))
954 .await
955 .unwrap();
956
957 tokio::time::timeout(
958 Duration::from_secs(1),
959 to_host_tx.send(Bytes::from_static(b"next")),
960 )
961 .await
962 .unwrap()
963 .unwrap();
964
965 assert!(shared.proxy_wake.wait_timeout(Duration::ZERO));
966
967 drop(client);
968 drop(to_host_tx);
969 task.abort();
970 let _ = task.await;
971 }
972
973 #[test]
974 fn full_host_channel_returns_guest_data_for_retry() {
975 let (to_host, mut to_host_rx) = mpsc::channel(1);
976
977 let occupied = Bytes::from_static(b"occupied");
978 to_host.try_send(occupied.clone()).unwrap();
979
980 let pending = Bytes::from_static(b"preserve me");
981 let unsent = try_send_to_host_relay(&to_host, pending.clone()).unwrap_err();
982 assert_eq!(unsent, pending);
983
984 assert_eq!(to_host_rx.try_recv().unwrap(), occupied);
985 try_send_to_host_relay(&to_host, unsent).unwrap();
986 assert_eq!(
987 to_host_rx.try_recv().unwrap(),
988 Bytes::from_static(b"preserve me")
989 );
990 }
991
992 #[test]
993 fn inject_udp_datagram_to_guest_counts_rx_bytes() {
994 let shared = SharedState::new(4);
995 let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1)), 50000);
996 let guest = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 2)), 5353);
997
998 inject_udp_datagram_to_guest(
999 peer,
1000 guest,
1001 b"hello",
1002 &shared,
1003 EthernetAddress([0x02, 0, 0, 0, 0, 1]),
1004 EthernetAddress([0x02, 0, 0, 0, 0, 2]),
1005 );
1006
1007 let frame = shared.rx_ring.pop().expect("published UDP frame");
1008 assert_eq!(shared.rx_bytes(), frame.len() as u64);
1009 }
1010
1011 #[test]
1012 fn relay_udp_outbound_queues_reply_for_active_peer() {
1013 let (inbound_tx, inbound_rx) = mpsc::channel(1);
1014 let (outbound_tx, mut outbound_rx) = mpsc::channel(1);
1015 let routes = Arc::new(Mutex::new(HashMap::new()));
1016 let peers = Arc::new(Mutex::new(PublishedUdpPeers::default()));
1017 let guest_ip = Ipv4Addr::new(172, 16, 0, 2);
1018 let host_peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 50000);
1019 let guest_peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1)), 49152);
1020
1021 {
1022 let mut peers = peers.lock();
1023 peers.host_to_guest.insert(
1024 host_peer,
1025 PublishedUdpPeer {
1026 guest_addr: guest_peer,
1027 last_seen: Instant::now(),
1028 },
1029 );
1030 peers.guest_to_host.insert(guest_peer, host_peer);
1031 }
1032 routes.lock().insert(
1033 5353,
1034 vec![PublishedUdpRoute {
1035 bind_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5353),
1036 outbound_tx,
1037 peers,
1038 }],
1039 );
1040
1041 let publisher = PortPublisher {
1042 inbound_rx,
1043 _inbound_tx: inbound_tx,
1044 connections: Vec::new(),
1045 guest_ip: Some(IpAddr::V4(guest_ip)),
1046 guest_ipv4: Some(guest_ip),
1047 guest_ipv6: None,
1048 ephemeral_port: Arc::new(AtomicU16::new(49152)),
1049 max_inbound: 256,
1050 udp_routes: routes,
1051 };
1052 let src = SocketAddr::new(IpAddr::V4(guest_ip), 5353);
1053 let frame = construct_udp_response(
1054 src,
1055 guest_peer,
1056 b"pong",
1057 EthernetAddress([0x02, 0, 0, 0, 0, 1]),
1058 EthernetAddress([0x02, 0, 0, 0, 0, 2]),
1059 )
1060 .unwrap();
1061
1062 assert!(publisher.relay_udp_outbound(&frame, src, guest_peer));
1063 let outbound = outbound_rx.try_recv().unwrap();
1064 assert_eq!(outbound.peer, host_peer);
1065 assert_eq!(outbound.payload.as_ref(), b"pong");
1066 }
1067
1068 #[test]
1069 fn relay_udp_outbound_ignores_inactive_peer() {
1070 let (inbound_tx, inbound_rx) = mpsc::channel(1);
1071 let (outbound_tx, _outbound_rx) = mpsc::channel(1);
1072 let routes = Arc::new(Mutex::new(HashMap::new()));
1073 let guest_ip = Ipv4Addr::new(172, 16, 0, 2);
1074 let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 50000);
1075
1076 routes.lock().insert(
1077 5353,
1078 vec![PublishedUdpRoute {
1079 bind_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5353),
1080 outbound_tx,
1081 peers: Arc::new(Mutex::new(PublishedUdpPeers::default())),
1082 }],
1083 );
1084
1085 let publisher = PortPublisher {
1086 inbound_rx,
1087 _inbound_tx: inbound_tx,
1088 connections: Vec::new(),
1089 guest_ip: Some(IpAddr::V4(guest_ip)),
1090 guest_ipv4: Some(guest_ip),
1091 guest_ipv6: None,
1092 ephemeral_port: Arc::new(AtomicU16::new(49152)),
1093 max_inbound: 256,
1094 udp_routes: routes,
1095 };
1096 let src = SocketAddr::new(IpAddr::V4(guest_ip), 5353);
1097 let frame = construct_udp_response(
1098 src,
1099 peer,
1100 b"pong",
1101 EthernetAddress([0x02, 0, 0, 0, 0, 1]),
1102 EthernetAddress([0x02, 0, 0, 0, 0, 2]),
1103 )
1104 .unwrap();
1105
1106 assert!(!publisher.relay_udp_outbound(&frame, src, peer));
1107 }
1108
1109 #[test]
1110 fn resolve_udp_guest_peer_returns_none_when_ephemeral_ports_exhausted() {
1111 let peers = Arc::new(Mutex::new(PublishedUdpPeers::default()));
1112 let gateway_ip = IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1));
1113 let now = Instant::now();
1114
1115 {
1116 let mut peers = peers.lock();
1117 for port in UDP_EPHEMERAL_PORT_START..=u16::MAX {
1118 let host_peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
1119 let guest_addr = SocketAddr::new(gateway_ip, port);
1120 peers.host_to_guest.insert(
1121 host_peer,
1122 PublishedUdpPeer {
1123 guest_addr,
1124 last_seen: now,
1125 },
1126 );
1127 peers.guest_to_host.insert(guest_addr, host_peer);
1128 }
1129 }
1130
1131 let next = resolve_udp_guest_peer(
1132 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 40000),
1133 gateway_ip,
1134 &peers,
1135 &AtomicU16::new(UDP_EPHEMERAL_PORT_START),
1136 );
1137
1138 assert!(next.is_none());
1139 }
1140
1141 #[test]
1142 fn bind_exposure_keeps_loopback_distinct_from_lan_binds() {
1143 assert_eq!(
1144 bind_exposure(IpAddr::V4(Ipv4Addr::LOCALHOST)),
1145 BindExposure::Loopback
1146 );
1147 assert_eq!(
1148 bind_exposure(IpAddr::V6(Ipv6Addr::LOCALHOST)),
1149 BindExposure::Loopback
1150 );
1151 assert_eq!(
1152 bind_exposure(IpAddr::V4(Ipv4Addr::UNSPECIFIED)),
1153 BindExposure::Wildcard
1154 );
1155 assert_eq!(
1156 bind_exposure(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
1157 BindExposure::Wildcard
1158 );
1159 assert_eq!(
1160 bind_exposure(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10))),
1161 BindExposure::Interface
1162 );
1163 }
1164}