Skip to main content

a3s_box_netproxy/
lib.rs

1#![cfg(any(target_os = "macos", all(test, unix)))]
2
3//! Pure-Rust userspace network proxy for libkrun on macOS.
4//!
5//! Uses a Unix datagram `socketpair()` to connect libkrun's virtio-net backend
6//! to the userspace gateway and provides the gateway services needed by the guest:
7//!
8//! - **ARP**: handled automatically by smoltcp's interface layer.
9//! - **DNS**: UDP/53 queries forwarded to the host's configured DNS servers.
10//! - **Inbound TCP port-forwarding**: `host_port → guest_ip:guest_port` pairs
11//!   parsed from the box's `port_map` config (e.g. `"8088:80"`).
12//! - **Outbound TCP proxying**: guest connections addressed through the gateway
13//!   are terminated by smoltcp and connected through the host TCP stack.
14
15mod device;
16mod manager;
17#[cfg(test)]
18mod tests;
19
20use std::collections::HashSet;
21use std::io::{self, Read, Write};
22use std::net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpListener, TcpStream, UdpSocket};
23use std::os::unix::net::UnixDatagram;
24use std::path::PathBuf;
25use std::sync::{
26    atomic::{AtomicBool, AtomicUsize, Ordering},
27    mpsc::{self, Receiver, TryRecvError},
28    Arc,
29};
30use std::time::Duration;
31
32use smoltcp::iface::{Config, Interface, SocketSet};
33use smoltcp::socket::{tcp, udp};
34use smoltcp::time::Instant;
35use smoltcp::wire::{
36    EthernetFrame, EthernetProtocol, IpAddress, IpCidr, IpEndpoint, IpProtocol, Ipv4Address,
37    Ipv4Packet, TcpPacket,
38};
39
40use device::{BridgePort, NetStats, UnixgramDevice, GATEWAY_MAC};
41use manager::write_stats_file;
42
43pub use manager::{spawn_inherited_netproxy, InheritedNetProxyConfig, NetProxyManager};
44
45// ── Helpers ───────────────────────────────────────────────────────────────────
46
47/// Returns the current time as a smoltcp `Instant` (microseconds since Unix epoch).
48fn smoltcp_now() -> Instant {
49    use std::time::{SystemTime, UNIX_EPOCH};
50    let us = SystemTime::now()
51        .duration_since(UNIX_EPOCH)
52        .unwrap_or_default()
53        .as_micros() as i64;
54    Instant::from_micros(us)
55}
56
57/// Convert `std::net::Ipv4Addr` to `smoltcp::wire::Ipv4Address`.
58fn to_smoltcp_ipv4(ip: Ipv4Addr) -> Ipv4Address {
59    Ipv4Address::from(ip)
60}
61
62// ── Constants ─────────────────────────────────────────────────────────────────
63
64/// Ephemeral port range start for outbound TCP connections from the gateway.
65const EPHEMERAL_BASE: u16 = 49152;
66/// Bound per-box memory and host resources consumed by transparent TCP flows.
67const MAX_OUTBOUND_CONNECTIONS: usize = 256;
68/// Do not let a host-side connect stall the guest indefinitely.
69const OUTBOUND_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
70/// Idle TCP state is eventually reclaimed even if one endpoint disappears.
71const TCP_IDLE_TIMEOUT: smoltcp::time::Duration = smoltcp::time::Duration::from_secs(300);
72/// How often the proxy refreshes its stats file.
73const STATS_WRITE_INTERVAL: Duration = Duration::from_secs(1);
74
75// ── Port-forward state ────────────────────────────────────────────────────────
76
77/// Parsed port-forward rule: `host_port → guest_ip:guest_port`.
78struct PortForward {
79    listener: TcpListener,
80    guest_ip: Ipv4Addr,
81    guest_port: u16,
82    /// TCP handshake in progress from the gateway to the guest.
83    pending: Vec<PendingGuestConnection>,
84    /// Fully established connections ready for data proxying.
85    active: Vec<TcpProxyConnection>,
86}
87
88struct PendingGuestConnection {
89    handle: smoltcp::iface::SocketHandle,
90    host_stream: TcpStream,
91    started_at: std::time::Instant,
92}
93
94struct TcpProxyConnection {
95    handle: smoltcp::iface::SocketHandle,
96    host_stream: TcpStream,
97    host_read_closed: bool,
98    guest_read_closed: bool,
99    /// An abort raised after the most recent interface poll must survive until
100    /// the next poll so smoltcp can emit its reset packet.
101    abort_pending: bool,
102}
103
104impl TcpProxyConnection {
105    fn new(handle: smoltcp::iface::SocketHandle, host_stream: TcpStream) -> Self {
106        Self {
107            handle,
108            host_stream,
109            host_read_closed: false,
110            guest_read_closed: false,
111            abort_pending: false,
112        }
113    }
114}
115
116#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
117struct OutboundFlow {
118    guest_ip: Ipv4Addr,
119    guest_port: u16,
120    remote_ip: Ipv4Addr,
121    remote_port: u16,
122}
123
124struct PendingOutboundConnection {
125    flow: OutboundFlow,
126    handle: smoltcp::iface::SocketHandle,
127    connect_result: Receiver<io::Result<TcpStream>>,
128    host_stream: Option<TcpStream>,
129    started_at: std::time::Instant,
130    failed: bool,
131}
132
133struct ActiveOutboundConnection {
134    flow: OutboundFlow,
135    proxy: TcpProxyConnection,
136}
137
138// ── Proxy engine ──────────────────────────────────────────────────────────────
139
140struct ProxyEngineConfig {
141    socket: UnixDatagram,
142    guest_ip: Ipv4Addr,
143    gateway_ip: Ipv4Addr,
144    prefix_len: u8,
145    dns_servers: Vec<Ipv4Addr>,
146    port_forwards: Vec<PortForward>,
147    shutdown: Arc<AtomicBool>,
148    stats: Arc<NetStats>,
149    stats_path: Option<PathBuf>,
150    bridge: Option<BridgePort>,
151}
152
153struct ProxyEngine {
154    device: UnixgramDevice,
155    iface: Interface,
156    sockets: SocketSet<'static>,
157    dns_sockets: Vec<(smoltcp::iface::SocketHandle, Ipv4Addr)>,
158    guest_ip: Ipv4Addr,
159    gateway_ip: Ipv4Addr,
160    port_forwards: Vec<PortForward>,
161    pending_outbound: Vec<PendingOutboundConnection>,
162    active_outbound: Vec<ActiveOutboundConnection>,
163    outbound_connectors: Arc<AtomicUsize>,
164    next_ephemeral: u16,
165    shutdown: Arc<AtomicBool>,
166    stats: Arc<NetStats>,
167    stats_path: Option<PathBuf>,
168    last_stats_write: std::time::Instant,
169}
170
171impl ProxyEngine {
172    fn new(config: ProxyEngineConfig) -> Self {
173        let ProxyEngineConfig {
174            socket,
175            guest_ip,
176            gateway_ip,
177            prefix_len,
178            dns_servers,
179            port_forwards,
180            shutdown,
181            stats,
182            stats_path,
183            bridge,
184        } = config;
185
186        let mut device = UnixgramDevice::new(socket, bridge, Arc::clone(&stats));
187
188        // Configure smoltcp interface as the gateway.
189        let config = Config::new(GATEWAY_MAC.into());
190        let mut iface = Interface::new(config, &mut device, smoltcp_now());
191        iface.update_ip_addrs(|addrs| {
192            let cidr = IpCidr::new(IpAddress::Ipv4(to_smoltcp_ipv4(gateway_ip)), prefix_len);
193            addrs.push(cidr).ok();
194        });
195        // The guest keeps the real destination IP in its packets and uses the
196        // gateway only as the Ethernet next hop. AnyIP plus a default route via
197        // our own gateway address lets smoltcp terminate those transparent TCP
198        // connections while preserving their original destination endpoints.
199        iface.set_any_ip(true);
200        let _ = iface
201            .routes_mut()
202            .add_default_ipv4_route(to_smoltcp_ipv4(gateway_ip));
203
204        let mut sockets = SocketSet::new(vec![]);
205
206        // The guest's resolv.conf contains the configured upstream addresses
207        // (for example 8.8.8.8), not the gateway address. Bind one AnyIP UDP
208        // socket per upstream so replies preserve the queried source IP; a
209        // wildcard :53 socket would reply from gateway_ip and resolvers would
210        // reject the mismatched response.
211        let dns_sockets = dns_servers
212            .iter()
213            .copied()
214            .filter_map(|server| {
215                let dns_rx =
216                    udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY; 16], vec![0u8; 8192]);
217                let dns_tx =
218                    udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY; 16], vec![0u8; 8192]);
219                let mut dns_socket = udp::Socket::new(dns_rx, dns_tx);
220                let endpoint = IpEndpoint::new(IpAddress::Ipv4(to_smoltcp_ipv4(server)), 53);
221                dns_socket
222                    .bind(endpoint)
223                    .ok()
224                    .map(|_| (sockets.add(dns_socket), server))
225            })
226            .collect();
227
228        Self {
229            device,
230            iface,
231            sockets,
232            dns_sockets,
233            guest_ip,
234            gateway_ip,
235            port_forwards,
236            pending_outbound: Vec::new(),
237            active_outbound: Vec::new(),
238            outbound_connectors: Arc::new(AtomicUsize::new(0)),
239            next_ephemeral: EPHEMERAL_BASE,
240            shutdown,
241            stats,
242            stats_path,
243            last_stats_write: std::time::Instant::now(),
244        }
245    }
246
247    fn run(&mut self) {
248        self.write_stats_snapshot();
249        loop {
250            if self.shutdown.load(Ordering::Relaxed) {
251                break;
252            }
253
254            let now = smoltcp_now();
255
256            // 1. Drain UnixGram socket into rx_queue.
257            self.device.drain();
258
259            // 2. Accept published-port clients and discover new guest outbound
260            // TCP flows before smoltcp consumes their SYN packets.
261            self.accept_connections();
262            self.accept_outbound_flows();
263
264            // 3. Collect non-blocking host connect results. Failed established
265            // flows are aborted before the interface poll so the reset is sent.
266            self.poll_outbound_connectors();
267
268            // 4. Poll smoltcp (processes ARP, TCP, UDP frames).
269            self.iface.poll(now, &mut self.device, &mut self.sockets);
270
271            // Connections aborted after the previous poll have now had one
272            // dispatch opportunity and can release their socket handles.
273            self.finish_aborted_connections();
274
275            // 5. Promote pending TCP connections to active once established.
276            self.promote_established();
277            self.promote_outbound_established();
278
279            // 6. Proxy data for active TCP connections.
280            self.proxy_data();
281
282            // 7. Forward DNS queries to real DNS servers.
283            self.forward_dns();
284
285            // 8. Remove closed connections and release their smoltcp sockets.
286            self.cleanup();
287
288            // 9. Publish resource counters for `a3s-box stats`.
289            self.maybe_write_stats_snapshot();
290
291            // 10. Sleep until the next smoltcp event or at most 5 ms.
292            let delay = self
293                .iface
294                .poll_delay(now, &self.sockets)
295                .unwrap_or(smoltcp::time::Duration::from_millis(1));
296            std::thread::sleep(Duration::from_micros(delay.micros().min(5_000)));
297        }
298        self.write_stats_snapshot();
299    }
300
301    fn maybe_write_stats_snapshot(&mut self) {
302        if self.last_stats_write.elapsed() < STATS_WRITE_INTERVAL {
303            return;
304        }
305        self.last_stats_write = std::time::Instant::now();
306        self.write_stats_snapshot();
307    }
308
309    fn write_stats_snapshot(&self) {
310        let Some(path) = self.stats_path.as_deref() else {
311            return;
312        };
313        if let Err(e) = write_stats_file(path, self.stats.snapshot()) {
314            tracing::debug!(error = %e, path = %path.display(), "NetProxy: failed to write stats file");
315        }
316    }
317
318    // ── Accept new host connections ───────────────────────────────────────────
319
320    fn accept_connections(&mut self) {
321        // First pass: accept connections, collect (forward_index, stream, guest_ip, guest_port).
322        // We can't call open_guest_tcp while mutably borrowing port_forwards.
323        let mut new_conns: Vec<(usize, TcpStream, Ipv4Addr, u16)> = Vec::new();
324        for (i, pf) in self.port_forwards.iter_mut().enumerate() {
325            loop {
326                match pf.listener.accept() {
327                    Ok((stream, _)) => {
328                        stream.set_nonblocking(true).ok();
329                        stream.set_nodelay(true).ok();
330                        new_conns.push((i, stream, pf.guest_ip, pf.guest_port));
331                    }
332                    Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
333                    Err(e) => {
334                        tracing::warn!(error = %e, "Port-forward listener error");
335                        break;
336                    }
337                }
338            }
339        }
340
341        // Second pass: open smoltcp TCP sockets and push to pending.
342        for (i, stream, guest_ip, guest_port) in new_conns {
343            let handle = self.open_guest_tcp(guest_ip, guest_port);
344            self.port_forwards[i].pending.push(PendingGuestConnection {
345                handle,
346                host_stream: stream,
347                started_at: std::time::Instant::now(),
348            });
349            tracing::debug!(
350                guest = %guest_ip,
351                port = guest_port,
352                handle = ?handle,
353                "NetProxy accepted host connection and initiated guest TCP connect"
354            );
355        }
356    }
357
358    /// Create a smoltcp TCP socket and initiate a connection to the guest.
359    fn open_guest_tcp(
360        &mut self,
361        guest_ip: Ipv4Addr,
362        guest_port: u16,
363    ) -> smoltcp::iface::SocketHandle {
364        let rx = tcp::SocketBuffer::new(vec![0u8; 65536]);
365        let tx = tcp::SocketBuffer::new(vec![0u8; 65536]);
366        let mut socket = tcp::Socket::new(rx, tx);
367
368        let local_port = self.next_ephemeral;
369        self.next_ephemeral = self.next_ephemeral.wrapping_add(1);
370        if self.next_ephemeral < EPHEMERAL_BASE {
371            self.next_ephemeral = EPHEMERAL_BASE;
372        }
373
374        let remote = IpEndpoint::new(IpAddress::Ipv4(to_smoltcp_ipv4(guest_ip)), guest_port);
375        socket
376            .connect(self.iface.context(), remote, local_port)
377            .ok();
378        socket.set_keep_alive(Some(smoltcp::time::Duration::from_secs(30)));
379        socket.set_timeout(Some(TCP_IDLE_TIMEOUT));
380
381        self.sockets.add(socket)
382    }
383
384    // ── Discover guest outbound TCP connections ──────────────────────────────
385
386    fn accept_outbound_flows(&mut self) {
387        let queued_flows: HashSet<_> = self
388            .device
389            .rx_queue
390            .iter()
391            .filter_map(|frame| outbound_syn_flow(frame, self.guest_ip, self.gateway_ip))
392            .collect();
393
394        for flow in queued_flows {
395            if self.outbound_flow_exists(flow) {
396                continue;
397            }
398            if self.pending_outbound.len() + self.active_outbound.len() >= MAX_OUTBOUND_CONNECTIONS
399                || self.outbound_connectors.load(Ordering::Relaxed) >= MAX_OUTBOUND_CONNECTIONS
400            {
401                tracing::warn!(
402                    limit = MAX_OUTBOUND_CONNECTIONS,
403                    "NetProxy outbound connection limit reached"
404                );
405                continue;
406            }
407
408            let connect_result = match spawn_outbound_connect(
409                flow,
410                Arc::clone(&self.outbound_connectors),
411            ) {
412                Ok(receiver) => receiver,
413                Err(error) => {
414                    tracing::warn!(%error, ?flow, "NetProxy failed to spawn outbound connector");
415                    continue;
416                }
417            };
418
419            let rx = tcp::SocketBuffer::new(vec![0u8; 65536]);
420            let tx = tcp::SocketBuffer::new(vec![0u8; 65536]);
421            let mut socket = tcp::Socket::new(rx, tx);
422            let endpoint = IpEndpoint::new(
423                IpAddress::Ipv4(to_smoltcp_ipv4(flow.remote_ip)),
424                flow.remote_port,
425            );
426            if let Err(error) = socket.listen(endpoint) {
427                tracing::warn!(?error, ?flow, "NetProxy failed to listen for outbound flow");
428                continue;
429            }
430            socket.set_keep_alive(Some(smoltcp::time::Duration::from_secs(30)));
431            socket.set_timeout(Some(TCP_IDLE_TIMEOUT));
432            let handle = self.sockets.add(socket);
433
434            self.pending_outbound.push(PendingOutboundConnection {
435                flow,
436                handle,
437                connect_result,
438                host_stream: None,
439                started_at: std::time::Instant::now(),
440                failed: false,
441            });
442            tracing::debug!(
443                ?flow,
444                ?handle,
445                "NetProxy discovered guest outbound TCP flow"
446            );
447        }
448    }
449
450    fn outbound_flow_exists(&self, flow: OutboundFlow) -> bool {
451        self.pending_outbound
452            .iter()
453            .any(|pending| pending.flow == flow)
454            || self
455                .active_outbound
456                .iter()
457                .any(|active| active.flow == flow)
458    }
459
460    /// Collect host connect results without blocking the netproxy packet loop.
461    /// This runs before `iface.poll`, so an aborted smoltcp socket gets a chance
462    /// to emit its reset before cleanup removes it.
463    fn poll_outbound_connectors(&mut self) {
464        for pending in &mut self.pending_outbound {
465            if pending.failed || pending.host_stream.is_some() {
466                continue;
467            }
468
469            match pending.connect_result.try_recv() {
470                Ok(Ok(stream)) => {
471                    pending.host_stream = Some(stream);
472                    tracing::debug!(flow = ?pending.flow, "NetProxy host TCP connection established");
473                }
474                Ok(Err(error)) => {
475                    tracing::debug!(%error, flow = ?pending.flow, "NetProxy host TCP connection failed");
476                    self.sockets.get_mut::<tcp::Socket>(pending.handle).abort();
477                    pending.failed = true;
478                }
479                Err(TryRecvError::Empty) => {
480                    if pending.started_at.elapsed()
481                        > OUTBOUND_CONNECT_TIMEOUT + Duration::from_secs(1)
482                    {
483                        tracing::debug!(flow = ?pending.flow, "NetProxy host TCP connection timed out");
484                        self.sockets.get_mut::<tcp::Socket>(pending.handle).abort();
485                        pending.failed = true;
486                    }
487                }
488                Err(TryRecvError::Disconnected) => {
489                    self.sockets.get_mut::<tcp::Socket>(pending.handle).abort();
490                    pending.failed = true;
491                }
492            }
493        }
494    }
495
496    // ── Promote pending → active ──────────────────────────────────────────────
497
498    fn promote_established(&mut self) {
499        for pf in &mut self.port_forwards {
500            let mut still_pending = Vec::new();
501            let mut to_remove = Vec::new();
502            for pending in pf.pending.drain(..) {
503                let socket = self.sockets.get::<tcp::Socket>(pending.handle);
504                use smoltcp::socket::tcp::State;
505                match socket.state() {
506                    State::Established => {
507                        tracing::debug!(handle = ?pending.handle, "NetProxy guest TCP connection established");
508                        pf.active
509                            .push(TcpProxyConnection::new(pending.handle, pending.host_stream));
510                    }
511                    State::Closed | State::TimeWait => {
512                        tracing::debug!(handle = ?pending.handle, state = ?socket.state(), "NetProxy guest TCP connection closed before establishment");
513                        to_remove.push(pending.handle);
514                    }
515                    _ if pending.started_at.elapsed() > OUTBOUND_CONNECT_TIMEOUT => {
516                        tracing::debug!(handle = ?pending.handle, "NetProxy guest TCP connection timed out");
517                        to_remove.push(pending.handle);
518                    }
519                    _ => {
520                        still_pending.push(pending);
521                    }
522                }
523            }
524            pf.pending = still_pending;
525            for handle in to_remove {
526                self.sockets.remove(handle);
527            }
528        }
529    }
530
531    fn promote_outbound_established(&mut self) {
532        use smoltcp::socket::tcp::State;
533
534        let mut still_pending = Vec::new();
535        let mut to_remove = Vec::new();
536        for mut pending in self.pending_outbound.drain(..) {
537            let state = self.sockets.get::<tcp::Socket>(pending.handle).state();
538            if pending.failed || matches!(state, State::Closed | State::TimeWait) {
539                to_remove.push(pending.handle);
540                continue;
541            }
542
543            if matches!(state, State::Established | State::CloseWait) {
544                if let Some(stream) = pending.host_stream.take() {
545                    tracing::debug!(flow = ?pending.flow, handle = ?pending.handle, "NetProxy outbound TCP proxy active");
546                    self.active_outbound.push(ActiveOutboundConnection {
547                        flow: pending.flow,
548                        proxy: TcpProxyConnection::new(pending.handle, stream),
549                    });
550                    continue;
551                }
552            }
553
554            still_pending.push(pending);
555        }
556        self.pending_outbound = still_pending;
557        for handle in to_remove {
558            self.sockets.remove(handle);
559        }
560    }
561
562    fn finish_aborted_connections(&mut self) {
563        for pf in &mut self.port_forwards {
564            let mut to_remove = Vec::new();
565            pf.active.retain(|connection| {
566                if connection.abort_pending {
567                    to_remove.push(connection.handle);
568                    false
569                } else {
570                    true
571                }
572            });
573            for handle in to_remove {
574                self.sockets.remove(handle);
575            }
576        }
577
578        let mut to_remove = Vec::new();
579        self.active_outbound.retain(|connection| {
580            if connection.proxy.abort_pending {
581                to_remove.push(connection.proxy.handle);
582                false
583            } else {
584                true
585            }
586        });
587        for handle in to_remove {
588            self.sockets.remove(handle);
589        }
590    }
591
592    // ── Bidirectional data proxy ──────────────────────────────────────────────
593
594    fn proxy_data(&mut self) {
595        for pf in &mut self.port_forwards {
596            for connection in &mut pf.active {
597                proxy_tcp_connection(&mut self.sockets, connection);
598            }
599        }
600        for connection in &mut self.active_outbound {
601            proxy_tcp_connection(&mut self.sockets, &mut connection.proxy);
602        }
603    }
604
605    // ── DNS forwarding ────────────────────────────────────────────────────────
606
607    fn forward_dns(&mut self) {
608        let next_query = self.dns_sockets.iter().find_map(|(handle, server)| {
609            let socket = self.sockets.get_mut::<udp::Socket>(*handle);
610            if !socket.can_recv() {
611                return None;
612            }
613            let (query, source) = socket.recv().ok()?;
614            Some((*handle, *server, query.to_vec(), source))
615        });
616        let Some((handle, dns_server, query, source)) = next_query else {
617            return;
618        };
619
620        // Forward query to the real DNS server via a host UDP socket.
621        match UdpSocket::bind("0.0.0.0:0") {
622            Ok(udp) => {
623                udp.set_read_timeout(Some(Duration::from_secs(2))).ok();
624                let dest = SocketAddrV4::new(dns_server, 53);
625                if udp.send_to(&query, dest).is_ok() {
626                    let mut resp = vec![0u8; 4096];
627                    if let Ok((n, _)) = udp.recv_from(&mut resp) {
628                        let socket = self.sockets.get_mut::<udp::Socket>(handle);
629                        socket.send_slice(&resp[..n], source).ok();
630                    }
631                }
632            }
633            Err(e) => {
634                tracing::warn!(error = %e, "DNS forward: failed to bind host UDP socket");
635            }
636        }
637    }
638
639    // ── Cleanup ───────────────────────────────────────────────────────────────
640
641    fn cleanup(&mut self) {
642        use smoltcp::socket::tcp::State;
643        for pf in &mut self.port_forwards {
644            // Collect handles that need to be removed first, then remove outside retain.
645            let mut to_remove = Vec::new();
646            pf.active.retain(|connection| {
647                let state = self.sockets.get::<tcp::Socket>(connection.handle).state();
648                if matches!(state, State::Closed | State::TimeWait) && !connection.abort_pending {
649                    to_remove.push(connection.handle);
650                    false
651                } else {
652                    true
653                }
654            });
655            for handle in to_remove {
656                self.sockets.remove(handle);
657            }
658        }
659
660        let mut to_remove = Vec::new();
661        self.active_outbound.retain(|connection| {
662            let state = self
663                .sockets
664                .get::<tcp::Socket>(connection.proxy.handle)
665                .state();
666            if matches!(state, State::Closed | State::TimeWait) && !connection.proxy.abort_pending {
667                to_remove.push(connection.proxy.handle);
668                false
669            } else {
670                true
671            }
672        });
673        for handle in to_remove {
674            self.sockets.remove(handle);
675        }
676    }
677}
678
679/// Extract the original four-tuple from a guest TCP SYN routed through the
680/// gateway. Peer-to-peer bridge frames use the peer MAC and are deliberately
681/// excluded so the local Ethernet switch keeps owning those connections.
682fn outbound_syn_flow(
683    frame: &[u8],
684    expected_guest_ip: Ipv4Addr,
685    gateway_ip: Ipv4Addr,
686) -> Option<OutboundFlow> {
687    let ethernet = EthernetFrame::new_checked(frame).ok()?;
688    if ethernet.dst_addr() != GATEWAY_MAC || ethernet.ethertype() != EthernetProtocol::Ipv4 {
689        return None;
690    }
691
692    let ipv4 = Ipv4Packet::new_checked(ethernet.payload()).ok()?;
693    if ipv4.next_header() != IpProtocol::Tcp {
694        return None;
695    }
696    let guest_ip = Ipv4Addr::from(ipv4.src_addr().0);
697    let remote_ip = Ipv4Addr::from(ipv4.dst_addr().0);
698    if guest_ip != expected_guest_ip
699        || remote_ip == gateway_ip
700        || remote_ip.is_unspecified()
701        || remote_ip.is_multicast()
702        || remote_ip == Ipv4Addr::BROADCAST
703    {
704        return None;
705    }
706
707    let tcp = TcpPacket::new_checked(ipv4.payload()).ok()?;
708    if !tcp.syn() || tcp.ack() || tcp.src_port() == 0 || tcp.dst_port() == 0 {
709        return None;
710    }
711
712    Some(OutboundFlow {
713        guest_ip,
714        guest_port: tcp.src_port(),
715        remote_ip,
716        remote_port: tcp.dst_port(),
717    })
718}
719
720fn spawn_outbound_connect(
721    flow: OutboundFlow,
722    connector_count: Arc<AtomicUsize>,
723) -> io::Result<Receiver<io::Result<TcpStream>>> {
724    let (sender, receiver) = mpsc::sync_channel(1);
725    connector_count.fetch_add(1, Ordering::Relaxed);
726    let thread_count = Arc::clone(&connector_count);
727    let spawn = std::thread::Builder::new()
728        .name("a3s-netproxy-connect".to_string())
729        .spawn(move || {
730            let address = SocketAddr::V4(SocketAddrV4::new(flow.remote_ip, flow.remote_port));
731            let result =
732                TcpStream::connect_timeout(&address, OUTBOUND_CONNECT_TIMEOUT).and_then(|stream| {
733                    stream.set_nonblocking(true)?;
734                    let _ = stream.set_nodelay(true);
735                    Ok(stream)
736                });
737            let _ = sender.send(result);
738            thread_count.fetch_sub(1, Ordering::Relaxed);
739        });
740
741    match spawn {
742        Ok(_) => Ok(receiver),
743        Err(error) => {
744            connector_count.fetch_sub(1, Ordering::Relaxed);
745            Err(error)
746        }
747    }
748}
749
750/// Move as much data as each non-blocking endpoint can currently accept.
751/// Consuming only the byte count returned by `write` and reading directly into
752/// smoltcp's available transmit slice prevents partial writes from dropping
753/// bytes under backpressure.
754fn proxy_tcp_connection(sockets: &mut SocketSet<'static>, connection: &mut TcpProxyConnection) {
755    let handle = connection.handle;
756    let socket = sockets.get_mut::<tcp::Socket>(handle);
757
758    let mut guest_to_host_bytes = 0usize;
759    let mut host_write_error = None;
760    if socket.can_recv() {
761        let _ = socket.recv(|data| match connection.host_stream.write(data) {
762            Ok(0) if !data.is_empty() => {
763                host_write_error = Some(io::Error::from(io::ErrorKind::WriteZero));
764                (0, ())
765            }
766            Ok(written) => {
767                guest_to_host_bytes = written;
768                (written, ())
769            }
770            Err(error)
771                if matches!(
772                    error.kind(),
773                    io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
774                ) =>
775            {
776                (0, ())
777            }
778            Err(error) => {
779                host_write_error = Some(error);
780                (0, ())
781            }
782        });
783    }
784    if guest_to_host_bytes > 0 {
785        tracing::trace!(
786            ?handle,
787            bytes = guest_to_host_bytes,
788            "NetProxy forwarded guest -> host bytes"
789        );
790    }
791    if let Some(error) = host_write_error {
792        tracing::debug!(%error, ?handle, "NetProxy host write failed");
793        let _ = connection.host_stream.shutdown(Shutdown::Both);
794        socket.abort();
795        connection.abort_pending = true;
796        return;
797    }
798
799    if !connection.guest_read_closed && !socket.may_recv() {
800        let _ = connection.host_stream.shutdown(Shutdown::Write);
801        connection.guest_read_closed = true;
802    }
803
804    let mut host_to_guest_bytes = 0usize;
805    let mut host_eof = false;
806    let mut host_read_error = None;
807    if !connection.host_read_closed && socket.can_send() {
808        let _ = socket.send(|buffer| match connection.host_stream.read(buffer) {
809            Ok(0) => {
810                host_eof = true;
811                (0, ())
812            }
813            Ok(read) => {
814                host_to_guest_bytes = read;
815                (read, ())
816            }
817            Err(error)
818                if matches!(
819                    error.kind(),
820                    io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
821                ) =>
822            {
823                (0, ())
824            }
825            Err(error) => {
826                host_read_error = Some(error);
827                (0, ())
828            }
829        });
830    }
831    if host_to_guest_bytes > 0 {
832        tracing::trace!(
833            ?handle,
834            bytes = host_to_guest_bytes,
835            "NetProxy forwarded host -> guest bytes"
836        );
837    }
838    if let Some(error) = host_read_error {
839        tracing::debug!(%error, ?handle, "NetProxy host read failed");
840        let _ = connection.host_stream.shutdown(Shutdown::Both);
841        socket.abort();
842        connection.abort_pending = true;
843    } else if host_eof {
844        tracing::debug!(?handle, "NetProxy host side closed its write half");
845        connection.host_read_closed = true;
846        socket.close();
847    }
848}