Skip to main content

arcbox_proxy/egress/
mod.rs

1//! Host-socket egress for guest network traffic.
2//!
3//! Instead of forwarding raw IP packets through a utun and relying on kernel
4//! routing/NAT, each outbound packet is parsed and driven onto host OS sockets
5//! directly. This bypasses kernel routing, VPN interference, and pf issues.
6//!
7//! # Architecture
8//!
9//! ```text
10//! Guest frame → HostEgress::handle_outbound()
11//!   ├─ ICMP → icmp::IcmpProxy (ICMP datagram socket sendto/recv)
12//!   └─ UDP  → udp::UdpProxy   (per-flow host UdpSocket, optionally via SOCKS5)
13//!         ↓
14//!   reply_tx → mpsc → datapath select! → guest FD
15//! ```
16
17mod icmp;
18mod udp;
19
20use std::net::Ipv4Addr;
21
22use tokio::sync::mpsc;
23use tokio_util::sync::CancellationToken;
24
25use arcbox_fakeip::dns_log::DnsResolutionLog;
26use arcbox_fakeip::proxy_detect::ProxyEnvironment;
27use arcbox_packet::ethernet::ETH_HEADER_LEN;
28
29use icmp::IcmpProxy;
30use udp::UdpProxy;
31
32/// Top-level host-socket egress that dispatches guest traffic to protocol-specific
33/// handlers and manages inbound port forwarding.
34pub struct HostEgress {
35    icmp: IcmpProxy,
36    udp: UdpProxy,
37    /// Inbound relay for host → guest port forwarding.
38    inbound: super::inbound_relay::InboundRelay,
39    /// Shared reply sender for injecting L2 frames towards the guest.
40    reply_tx: mpsc::Sender<Vec<u8>>,
41    /// Cancellation token for graceful shutdown of spawned tasks.
42    cancel: CancellationToken,
43}
44
45impl HostEgress {
46    /// Creates a new host-socket egress.
47    ///
48    /// `reply_tx` is used by all sub-proxies to send L2 frames back to the
49    /// datapath for writing to the guest FD. `mtu` is the guest link MTU;
50    /// guest-bound UDP datagrams above it are IPv4-fragmented.
51    #[must_use]
52    pub fn new(
53        gateway_ip: Ipv4Addr,
54        gateway_mac: [u8; 6],
55        guest_ip: Ipv4Addr,
56        reply_tx: mpsc::Sender<Vec<u8>>,
57        cancel: CancellationToken,
58        mtu: usize,
59    ) -> Self {
60        let inbound = super::inbound_relay::InboundRelay::new(
61            reply_tx.clone(),
62            gateway_mac,
63            gateway_ip,
64            guest_ip,
65            mtu,
66        );
67        Self {
68            icmp: IcmpProxy::new(reply_tx.clone(), gateway_mac),
69            udp: UdpProxy::new(reply_tx.clone(), gateway_mac, gateway_ip, mtu),
70            inbound,
71            reply_tx,
72            cancel,
73        }
74    }
75
76    /// Returns a clone of the reply sender for external use (e.g. async DNS
77    /// forwarding).
78    #[must_use]
79    pub fn reply_sender(&self) -> mpsc::Sender<Vec<u8>> {
80        self.reply_tx.clone()
81    }
82
83    /// Makes the UDP path proxy-aware, mirroring `TcpBridge::set_proxy_awareness`:
84    /// shares the fake-IP `dns_log` (so a fake-IP destination is reversed to its
85    /// domain) and the detected `proxy_env` (so a configured SOCKS proxy routes
86    /// non-gateway, non-bypassed UDP flows). Opt-in and off by default, so the VMM
87    /// and existing callers are unaffected. HTTP proxies can't carry UDP, so only
88    /// a SOCKS proxy engages this.
89    pub fn set_proxy_awareness(&mut self, dns_log: DnsResolutionLog, proxy_env: ProxyEnvironment) {
90        self.udp.dns_log = Some(dns_log);
91        self.udp.proxy_env = Some(proxy_env);
92    }
93
94    /// Dispatches an outbound IPv4 frame to the appropriate protocol proxy.
95    ///
96    /// Inbound reply frames (matching an active inbound connection) are
97    /// intercepted first; everything else is proxied through host sockets.
98    pub fn handle_outbound(&mut self, frame: &[u8], guest_mac: [u8; 6]) {
99        if frame.len() < ETH_HEADER_LEN + 20 {
100            return;
101        }
102
103        // Check inbound relay first — fast-path ephemeral port range check
104        // short-circuits for 99%+ of outbound frames.
105        if self.inbound.try_handle_reply(frame, guest_mac) {
106            return;
107        }
108
109        let protocol = frame[ETH_HEADER_LEN + 9];
110        let cancel = self.cancel.clone();
111        match protocol {
112            1 => self.icmp.proxy_icmp(frame, guest_mac, cancel),
113            17 => self.udp.proxy_udp(frame, guest_mac, cancel),
114            _ => {
115                tracing::trace!("Host egress: dropping protocol {}", protocol);
116            }
117        }
118    }
119
120    /// Handles an inbound UDP command from the listener manager.
121    pub fn handle_inbound_command(
122        &mut self,
123        cmd: super::inbound_relay::InboundCommand,
124        guest_mac: [u8; 6],
125    ) {
126        use super::inbound_relay::InboundCommand;
127        if let InboundCommand::UdpReceived {
128            host_port,
129            data,
130            reply_tx,
131            ..
132        } = cmd
133        {
134            // Use host_port (the Docker-exposed port on the guest), not
135            // container_port (the port inside the container).
136            self.inbound
137                .inject_udp(host_port, &data, reply_tx, guest_mac);
138        }
139    }
140
141    /// Runs periodic maintenance (flow cleanup).
142    pub fn maintenance(&mut self) {
143        self.udp.cleanup_stale_flows();
144        self.inbound.cleanup();
145    }
146
147    /// Expire a specific flow by address (called from timer wheel).
148    ///
149    /// This is the timer-wheel-driven counterpart to `maintenance()`.
150    /// Instead of scanning all flows, it targets a specific expired flow.
151    pub fn expire_flow(&mut self, _addr: std::net::SocketAddr) {
152        // TODO: Once UDP/ICMP flows are registered with the timer wheel,
153        // this will remove the specific flow by address. For now, the
154        // timer wheel is wired into the datapath loop but individual
155        // flow registration is not yet migrated from per-flow timeouts.
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_host_egress_creation() {
165        let (tx, _rx) = mpsc::channel(16);
166        let gw_ip = Ipv4Addr::new(192, 168, 64, 1);
167        let gw_mac = [0x02, 0xAB, 0xCD, 0x00, 0x00, 0x01];
168        let guest_ip = Ipv4Addr::new(192, 168, 64, 2);
169        let _egress = HostEgress::new(gw_ip, gw_mac, guest_ip, tx, CancellationToken::new(), 1500);
170    }
171
172    #[test]
173    fn test_host_egress_drops_unknown_protocol() {
174        let (tx, _rx) = mpsc::channel(16);
175        let gw_ip = Ipv4Addr::new(192, 168, 64, 1);
176        let gw_mac = [0x02, 0xAB, 0xCD, 0x00, 0x00, 0x01];
177        let guest_ip = Ipv4Addr::new(192, 168, 64, 2);
178        let mut egress =
179            HostEgress::new(gw_ip, gw_mac, guest_ip, tx, CancellationToken::new(), 1500);
180
181        // Build a minimal Ethernet + IPv4 frame with protocol=50 (ESP).
182        let mut frame = vec![0u8; ETH_HEADER_LEN + 20];
183        // Ethernet header
184        frame[12..14].copy_from_slice(&0x0800u16.to_be_bytes());
185        // IPv4 header
186        frame[ETH_HEADER_LEN] = 0x45; // Version 4, IHL 5
187        frame[ETH_HEADER_LEN + 9] = 50; // Protocol: ESP (unsupported)
188
189        let guest_mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x99];
190        // Should not panic.
191        egress.handle_outbound(&frame, guest_mac);
192    }
193
194    #[test]
195    fn test_host_egress_ignores_short_frames() {
196        let (tx, _rx) = mpsc::channel(16);
197        let gw_ip = Ipv4Addr::new(192, 168, 64, 1);
198        let gw_mac = [0x02, 0xAB, 0xCD, 0x00, 0x00, 0x01];
199        let guest_ip = Ipv4Addr::new(192, 168, 64, 2);
200        let mut egress =
201            HostEgress::new(gw_ip, gw_mac, guest_ip, tx, CancellationToken::new(), 1500);
202
203        let guest_mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x99];
204        // Frame shorter than Ethernet + IP minimum.
205        egress.handle_outbound(&[0u8; 10], guest_mac);
206    }
207
208    /// Verifies that reply frames from the egress flow through the mpsc channel.
209    #[tokio::test]
210    async fn test_reply_channel_flow() {
211        let (tx, mut rx) = mpsc::channel(16);
212        let gw_ip = Ipv4Addr::new(192, 168, 64, 1);
213        let gw_mac = [0x02, 0xAB, 0xCD, 0x00, 0x00, 0x01];
214
215        // Simulate what a proxy task does: send a frame through reply_tx.
216        let test_frame = vec![0xDE, 0xAD, 0xBE, 0xEF];
217        tx.send(test_frame.clone()).await.unwrap();
218
219        let received = rx.recv().await.unwrap();
220        assert_eq!(received, test_frame);
221
222        // Verify the egress creates correctly with the same tx.
223        let guest_ip = Ipv4Addr::new(192, 168, 64, 2);
224        let _egress = HostEgress::new(gw_ip, gw_mac, guest_ip, tx, CancellationToken::new(), 1500);
225    }
226}