Skip to main content

microsandbox_network/engine/ports/
publisher.rs

1//! Published port handling: host-side listeners that forward connections
2//! into the guest VM via smoltcp.
3//!
4//! For each configured [`PublishedPort`], a tokio TCP listener or UDP socket
5//! binds on the host. TCP connections are queued for the poll loop to create
6//! smoltcp sockets into the guest. UDP datagrams are injected as guest-visible
7//! packets, and guest replies to active peers are sent back through the same
8//! host socket.
9
10use 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
30//--------------------------------------------------------------------------------------------------
31// Constants
32//--------------------------------------------------------------------------------------------------
33
34/// TCP socket buffer sizes for inbound connections.
35const TCP_RX_BUF_SIZE: usize = 65536;
36const TCP_TX_BUF_SIZE: usize = 65536;
37
38/// Channel capacity for relay tasks.
39const CHANNEL_CAPACITY: usize = 32;
40
41/// Buffer size for reading from host sockets.
42const RELAY_BUF_SIZE: usize = 16384;
43
44/// Buffer size for host-side UDP published-port sockets.
45const UDP_RELAY_BUF_SIZE: usize = 65535;
46
47/// Idle timeout for UDP peers that have contacted a published port.
48const UDP_PEER_TIMEOUT: Duration = Duration::from_secs(60);
49
50/// First ephemeral source port used to represent host UDP peers inside the guest.
51const UDP_EPHEMERAL_PORT_START: u16 = 49152;
52
53/// Number of usable ephemeral ports from [`UDP_EPHEMERAL_PORT_START`] through `u16::MAX`.
54const UDP_EPHEMERAL_PORT_COUNT: usize =
55    (u16::MAX as usize) - (UDP_EPHEMERAL_PORT_START as usize) + 1;
56
57//--------------------------------------------------------------------------------------------------
58// Types
59//--------------------------------------------------------------------------------------------------
60
61/// Manages published port listeners and inbound connections.
62///
63/// Spawns tokio listeners for each published port. When connections arrive,
64/// they are queued for the poll loop to create smoltcp sockets and initiate
65/// connections to the guest.
66pub struct PortPublisher {
67    /// Receives accepted connections from listener tasks.
68    inbound_rx: mpsc::Receiver<InboundConnection>,
69    /// Held to keep the channel open (listener tasks hold clones).
70    _inbound_tx: mpsc::Sender<InboundConnection>,
71    /// Tracked inbound connections (smoltcp socket → relay state).
72    connections: Vec<InboundRelay>,
73    /// Guest IP that inbound connections are dialed to. Prefers IPv4 (the
74    /// common case — most services bind `0.0.0.0` or dual-stack `::`, both
75    /// of which accept v4) and falls back to IPv6 for v6-only sandboxes.
76    /// `None` when neither family is active; listeners are not spawned.
77    guest_ip: Option<IpAddr>,
78    /// Guest IPv4, when active.
79    guest_ipv4: Option<Ipv4Addr>,
80    /// Guest IPv6, when active.
81    guest_ipv6: Option<Ipv6Addr>,
82    /// Ephemeral port counter.
83    ephemeral_port: Arc<AtomicU16>,
84    /// Maximum inbound connections (prevents resource exhaustion from host-side floods).
85    max_inbound: usize,
86    /// UDP published-port routes, keyed by guest-side port.
87    udp_routes: PublishedUdpRoutes,
88}
89
90/// An accepted host-side connection waiting to be wired to the guest.
91struct InboundConnection {
92    /// The accepted host-side TCP stream.
93    stream: TcpStream,
94    /// Guest port to connect to.
95    guest_port: u16,
96}
97
98/// Shared UDP published-port route table.
99type PublishedUdpRoutes = Arc<Mutex<HashMap<u16, Vec<PublishedUdpRoute>>>>;
100
101/// A host UDP socket that can send replies for active peers.
102struct PublishedUdpRoute {
103    /// Host bind address for diagnostics.
104    bind_addr: SocketAddr,
105    /// Send guest reply payloads to the UDP listener task.
106    outbound_tx: mpsc::Sender<PublishedUdpOutbound>,
107    /// NAT mappings for peers that recently sent datagrams to this published port.
108    peers: Arc<Mutex<PublishedUdpPeers>>,
109}
110
111/// Guest response payload for a host peer.
112struct PublishedUdpOutbound {
113    peer: SocketAddr,
114    payload: Bytes,
115}
116
117/// Active UDP peer NAT mappings for one published route.
118#[derive(Default)]
119struct PublishedUdpPeers {
120    host_to_guest: HashMap<SocketAddr, PublishedUdpPeer>,
121    guest_to_host: HashMap<SocketAddr, SocketAddr>,
122}
123
124/// One host peer as represented on the guest-side virtual network.
125struct PublishedUdpPeer {
126    guest_addr: SocketAddr,
127    last_seen: Instant,
128}
129
130/// Maximum number of poll iterations to attempt flushing remaining data
131/// after the relay task has exited before force-aborting the socket.
132const DEFERRED_CLOSE_LIMIT: u16 = 64;
133
134/// A single inbound connection relay (host socket ↔ smoltcp socket).
135struct InboundRelay {
136    handle: SocketHandle,
137    /// Send data from smoltcp socket to host relay task.
138    to_host: mpsc::Sender<Bytes>,
139    /// Data removed from smoltcp while the host relay channel was full.
140    read_buf: Option<Bytes>,
141    /// Receive data from host relay task to write to smoltcp socket.
142    from_host: mpsc::Receiver<Bytes>,
143    /// Partial data that couldn't be fully written to smoltcp socket.
144    write_buf: Option<(Bytes, usize)>,
145    /// Counter for deferred close attempts (prevents stalling forever).
146    close_attempts: u16,
147}
148
149#[derive(Debug, Clone, Copy, Eq, PartialEq)]
150enum BindExposure {
151    /// Listener is reachable only through host loopback.
152    Loopback,
153    /// Listener is reachable through every host interface in that address family.
154    Wildcard,
155    /// Listener is reachable through one non-loopback host interface address.
156    Interface,
157}
158
159//--------------------------------------------------------------------------------------------------
160// Methods
161//--------------------------------------------------------------------------------------------------
162
163impl PortPublisher {
164    /// Create a new publisher and spawn listeners for all published ports.
165    ///
166    /// Listeners are only spawned when at least one of `guest_ipv4` /
167    /// `guest_ipv6` is `Some`; published ports need a smoltcp dial target.
168    /// Each TCP listener task gates accepted connections through the
169    /// supplied [`NetworkPolicy`]'s `evaluate_ingress` before queuing
170    /// them; rejected connections drop with TCP RST (zero-linger) so
171    /// the peer observes `ECONNRESET`.
172    #[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    /// Accept queued inbound connections: create smoltcp sockets and
230    /// initiate connections to the guest.
231    ///
232    /// Must be called each poll iteration.
233    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        // No guest IP means listeners weren't spawned; the channel is empty
241        // and there's nothing to do.
242        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            // Create smoltcp TCP socket.
253            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            // Connect to the guest.
258            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            // Create channel pair for relay.
273            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            // Spawn relay task: host TcpStream ↔ channels.
277            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    /// Relay data between smoltcp sockets and host relay tasks.
295    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            // Detect relay task exit — close the smoltcp socket.
302            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                    // Abort if we've been trying to flush for too long
308                    // (guest stopped reading, socket send buffer full).
309                    relay.close_attempts += 1;
310                    if relay.close_attempts >= DEFERRED_CLOSE_LIMIT {
311                        socket.abort();
312                    }
313                }
314                continue;
315            }
316
317            // smoltcp → host: flush read_buf first, then read from socket.
318            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            // host → smoltcp: write pending data, then drain channel.
340            write_host_data(socket, relay);
341        }
342    }
343
344    /// Relay a guest UDP datagram to a host peer that recently sent traffic
345    /// to a UDP published port.
346    ///
347    /// Returns `true` when the frame belongs to a published-port flow and
348    /// should be consumed by the caller.
349    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    /// Remove closed inbound connections.
390    ///
391    /// Only removes sockets in `Closed` state. Sockets in `TimeWait` are
392    /// left for smoltcp's 2*MSL timer to handle naturally.
393    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    /// Spawn one tokio listener task per TCP published port.
406    #[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            // Wrap around in the ephemeral range.
506            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
532//--------------------------------------------------------------------------------------------------
533// Functions
534//--------------------------------------------------------------------------------------------------
535
536/// Set zero-linger on a stream so the kernel sends a TCP RST instead of
537/// the default FIN close when the stream drops. Used for deliberate
538/// rejection paths (policy deny, max-inbound exhaustion,
539/// smoltcp-connect failure) so the peer sees `ECONNRESET` rather than
540/// a graceful close that looks like the server simply went away.
541///
542/// Goes through `socket2` rather than tokio's deprecated
543/// `TcpStream::set_linger` so the call site doesn't trip
544/// `#[deny(deprecated)]` in clippy. The cast to `SockRef` is
545/// zero-cost — it borrows the underlying fd.
546fn reject_with_rst(stream: &TcpStream) {
547    let _ = socket2::SockRef::from(stream).set_linger(Some(Duration::ZERO));
548}
549
550/// Listener task: accepts TCP connections on the host, runs each
551/// through the network policy's ingress evaluator, and queues
552/// allowed connections for the publisher's accept loop. Denied
553/// connections are dropped with TCP RST (zero-linger) so the peer
554/// sees `ECONNRESET` rather than a graceful close.
555async 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        // Policy gate: peer source IP and the guest's listening port.
569        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; // Publisher dropped.
584        }
585    }
586
587    Ok(())
588}
589
590/// UDP listener task: receives host datagrams, injects them into the guest,
591/// and sends guest replies back to active peers through the same socket.
592#[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
807/// Try to queue guest data for the host relay, returning it when backpressured.
808fn 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
812/// Relay task: bridges a host TcpStream to channels connected to smoltcp.
813async 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            // smoltcp → host: data from guest arrives via channel.
825            data = to_host_rx.recv() => {
826                match data {
827                    Some(bytes) => {
828                        // Wake as soon as recv frees channel capacity. Waiting
829                        // for write_all can stall the poll loop behind a slow
830                        // host client.
831                        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            // host → smoltcp: data from host client to write to guest.
842            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
864/// Write data from the host relay channel to the smoltcp socket.
865fn write_host_data(socket: &mut tcp::Socket<'_>, relay: &mut InboundRelay) {
866    // First, try to finish writing any pending partial data.
867    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    // Then drain the channel.
884    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//--------------------------------------------------------------------------------------------------
907// Tests
908//--------------------------------------------------------------------------------------------------
909
910#[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}