Skip to main content

microsandbox_network/
stack.rs

1//! smoltcp interface setup, frame classification, and poll loop.
2//!
3//! This module contains the core networking event loop that runs on a
4//! dedicated OS thread. It bridges guest ethernet frames (via
5//! [`SmoltcpDevice`]) to smoltcp's TCP/IP stack and services connections
6//! through tokio proxy tasks.
7
8use std::collections::HashSet;
9use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
10use std::sync::Arc;
11use std::sync::atomic::Ordering;
12
13#[cfg(windows)]
14use msb_krun_utils::event::{EventSet, EventSource, WaitContext, WaitEvent};
15use smoltcp::iface::{Config, Interface, SocketSet};
16use smoltcp::time::Instant;
17
18use smoltcp::wire::{
19    EthernetAddress, EthernetFrame, EthernetProtocol, HardwareAddress, Icmpv4Packet, Icmpv4Repr,
20    Icmpv6Packet, Icmpv6Repr, IpAddress, IpCidr, IpProtocol, Ipv4Packet, Ipv4Repr, Ipv6Packet,
21    Ipv6Repr, TcpPacket, UdpPacket,
22};
23
24use crate::config::{DnsConfig, PublishedPort};
25use crate::conn::ConnectionTracker;
26use crate::device::SmoltcpDevice;
27use crate::dns::common::ports::DnsPortType;
28use crate::dns::{
29    interceptor::DnsInterceptor,
30    proxies::{dot::DotProxy, tcp::DnsTcpProxy},
31};
32use crate::icmp_relay::IcmpRelay;
33use crate::policy::{EgressEvaluation, HostnameSource, NetworkPolicy, Protocol};
34use crate::proxy;
35use crate::publisher::PortPublisher;
36use crate::secrets::handle::SecretsHandle;
37use crate::shared::SharedState;
38use crate::tls::{proxy as tls_proxy, state::TlsState};
39use crate::udp_fragments::{
40    Ipv4UdpFragmentReassembler, Ipv6UdpFragmentReassembler, ReassembledUdpDatagram,
41    is_ipv4_udp_fragment, is_ipv6_fragment, is_ipv6_udp_fragment,
42};
43use crate::udp_relay::UdpRelay;
44
45//--------------------------------------------------------------------------------------------------
46// Constants
47//--------------------------------------------------------------------------------------------------
48
49#[cfg(windows)]
50const TX_WAKE_TOKEN: u64 = 1;
51
52#[cfg(windows)]
53const PROXY_WAKE_TOKEN: u64 = 2;
54
55//--------------------------------------------------------------------------------------------------
56// Types
57//--------------------------------------------------------------------------------------------------
58
59/// Result of classifying a guest ethernet frame before smoltcp processes it.
60///
61/// Pre-inspection allows the poll loop to:
62/// - Create TCP sockets before smoltcp sees a SYN (preventing auto-RST).
63/// - Handle non-DNS UDP outside smoltcp (smoltcp lacks wildcard port binding).
64/// - Route DNS queries to the interception handler.
65pub enum FrameAction {
66    /// TCP SYN to a new destination — create a smoltcp socket before
67    /// letting smoltcp process the frame.
68    TcpSyn { src: SocketAddr, dst: SocketAddr },
69
70    /// Non-DNS UDP datagram — handle entirely outside smoltcp via the UDP
71    /// relay.
72    UdpRelay { src: SocketAddr, dst: SocketAddr },
73
74    /// DNS query (UDP to port 53) — let smoltcp's bound UDP socket handle it.
75    Dns,
76
77    /// IPv4 UDP fragment — reassemble before the UDP relay sees it.
78    Ipv4UdpFragment,
79
80    /// IPv6 UDP fragment — reassemble before the UDP relay sees it.
81    Ipv6UdpFragment,
82
83    /// IPv6 fragment for a protocol this relay cannot safely classify.
84    Ipv6UnsupportedFragment,
85
86    /// Everything else (ARP, NDP, ICMP, TCP data/ACK/FIN, etc.) — let
87    /// smoltcp process normally.
88    Passthrough,
89}
90
91/// Resolved network parameters for the poll loop. Created by
92/// `SmoltcpNetwork::new()` from `NetworkConfig` + sandbox slot.
93pub struct PollLoopConfig {
94    /// Gateway MAC address (smoltcp's identity on the virtual LAN).
95    pub gateway_mac: [u8; 6],
96    /// Guest MAC address.
97    pub guest_mac: [u8; 6],
98    /// Gateway addresses owned by the smoltcp virtual stack. Each family
99    /// is `Some` when that family is active for this sandbox (host has a
100    /// route, or the user supplied an explicit address).
101    pub gateway: GatewayIps,
102    /// Guest IPv4 address. `None` when IPv4 is inactive for this sandbox.
103    pub guest_ipv4: Option<Ipv4Addr>,
104    /// Guest IPv6 address. `None` when IPv6 is inactive for this sandbox.
105    pub guest_ipv6: Option<Ipv6Addr>,
106    /// IP-level MTU (e.g. 1500).
107    pub mtu: usize,
108}
109
110/// Per-sandbox gateway addresses owned by the smoltcp virtual stack.
111///
112/// Each family is `Some` when active for this sandbox and `None` otherwise.
113/// `resolve_host_dst` rewrites gateway-bound connections to loopback at dial time.
114#[derive(Debug, Clone, Copy)]
115pub struct GatewayIps {
116    /// Gateway IPv4.
117    pub ipv4: Option<Ipv4Addr>,
118    /// Gateway IPv6.
119    pub ipv6: Option<Ipv6Addr>,
120}
121
122//--------------------------------------------------------------------------------------------------
123// Functions
124//--------------------------------------------------------------------------------------------------
125
126/// Classify a raw ethernet frame for pre-inspection.
127///
128/// Uses smoltcp's wire module for zero-copy parsing. Returns
129/// [`FrameAction::Passthrough`] for any frame that cannot be parsed or
130/// doesn't match a special case.
131pub fn classify_frame(frame: &[u8]) -> FrameAction {
132    let Ok(eth) = EthernetFrame::new_checked(frame) else {
133        return FrameAction::Passthrough;
134    };
135
136    match eth.ethertype() {
137        EthernetProtocol::Ipv4 => classify_ipv4(eth.payload()),
138        EthernetProtocol::Ipv6 => classify_ipv6(eth.payload()),
139        _ => FrameAction::Passthrough, // ARP, etc.
140    }
141}
142
143/// Create and configure the smoltcp [`Interface`].
144///
145/// The interface is configured as the **gateway**: it owns the gateway IP
146/// addresses and responds to ARP/NDP for them. `any_ip` mode is enabled so
147/// smoltcp accepts traffic destined for arbitrary remote IPs (not just the
148/// gateway), combined with default routes.
149pub fn create_interface(device: &mut SmoltcpDevice, config: &PollLoopConfig) -> Interface {
150    let hw_addr = HardwareAddress::Ethernet(EthernetAddress(config.gateway_mac));
151    let iface_config = Config::new(hw_addr);
152    let mut iface = Interface::new(iface_config, device, smoltcp_now());
153
154    // Configure gateway IP addresses for the active families.
155    iface.update_ip_addrs(|addrs| {
156        if let Some(ipv4) = config.gateway.ipv4 {
157            addrs
158                .push(IpCidr::new(IpAddress::Ipv4(ipv4), 30)) // 30 subnet: gateway + guest.
159                .expect("failed to add gateway IPv4 address");
160        }
161        if let Some(ipv6) = config.gateway.ipv6 {
162            addrs
163                .push(IpCidr::new(IpAddress::Ipv6(ipv6), 64))
164                .expect("failed to add gateway IPv6 address");
165        }
166    });
167
168    // Default routes so smoltcp accepts traffic for all destinations.
169    if let Some(ipv4) = config.gateway.ipv4 {
170        iface
171            .routes_mut()
172            .add_default_ipv4_route(ipv4)
173            .expect("failed to add default IPv4 route");
174    }
175    if let Some(ipv6) = config.gateway.ipv6 {
176        iface
177            .routes_mut()
178            .add_default_ipv6_route(ipv6)
179            .expect("failed to add default IPv6 route");
180    }
181
182    // Accept traffic destined for any IP, not just gateway addresses.
183    iface.set_any_ip(true);
184
185    iface
186}
187
188/// Main smoltcp poll loop. Runs on a dedicated OS thread.
189///
190/// Processes guest frames with pre-inspection, drives smoltcp's TCP/IP stack,
191/// and sleeps via `poll(2)` between events.
192///
193/// # Phases per iteration
194///
195/// 1. **Drain guest frames** — pop from `tx_ring`, classify, pre-inspect.
196/// 2. **smoltcp egress + maintenance** — transmit queued packets, run timers.
197/// 3. **Service connections** — relay data between smoltcp sockets and proxy
198///    tasks (added by later tasks).
199/// 4. **Sleep** — wait on `tx_wake` + `proxy_wake` with smoltcp's requested
200///    timeout.
201///
202/// # Arguments
203///
204/// * `shared` - Stack-wide shared state: `tx_ring` / `rx_ring` for the virtio-net boundary
205///   and the wake eventfds.
206/// * `config` - Resolved per-sandbox parameters (gateway / guest MAC + IPv4 + IPv6, MTU).
207/// * `network_policy` - User-provided egress policy. Evaluated against the sandbox's
208///   gateway IPs (stored on [`SharedState`]) so `DestinationGroup::Host` rules match.
209/// * `dns_config` - DNS interception settings (block lists, upstreams, timeout).
210/// * `tls_state` - Optional TLS MITM state; drives interception of intercepted ports and DoT
211///   when present.
212/// * `published_ports` - Host → guest port publishes; the publisher accepts inbound
213///   connections on the host-bind address and forwards into the guest.
214/// * `max_connections` - Optional cap on concurrent guest connections tracked by
215///   [`ConnectionTracker`]; `None` uses the default.
216/// * `tokio_handle` - Runtime handle used for proxy tasks, DNS forwarding, port publishing,
217///   and ICMP relays.
218#[allow(clippy::too_many_arguments)]
219pub fn smoltcp_poll_loop(
220    shared: Arc<SharedState>,
221    config: PollLoopConfig,
222    network_policy: NetworkPolicy,
223    dns_config: DnsConfig,
224    tls_state: Option<Arc<TlsState>>,
225    published_ports: Vec<PublishedPort>,
226    max_connections: Option<usize>,
227    tokio_handle: tokio::runtime::Handle,
228    secrets: SecretsHandle,
229) {
230    let mut device = SmoltcpDevice::new(shared.clone(), config.mtu);
231    let mut iface = create_interface(&mut device, &config);
232    let mut sockets = SocketSet::new(vec![]);
233    let mut conn_tracker = ConnectionTracker::new(max_connections);
234
235    // The DNS forwarder needs to know which IPs count as "the gateway"
236    // (so it routes guest queries to those addresses through the
237    // configured upstream) and a policy evaluator (so guest-chosen
238    // `@target` resolvers are gated by egress rules just like any
239    // other outbound).
240    let gateway_ips: Arc<HashSet<IpAddr>> = Arc::new(
241        config
242            .gateway
243            .ipv4
244            .map(IpAddr::V4)
245            .into_iter()
246            .chain(config.gateway.ipv6.map(IpAddr::V6))
247            .collect(),
248    );
249    // Gateway IPs must be on SharedState before any egress evaluation runs,
250    // so `DestinationGroup::Host` rules can resolve to the right address.
251    shared.set_gateway_ips(config.gateway.ipv4, config.gateway.ipv6);
252    let network_policy = Arc::new(network_policy);
253
254    let (mut dns_interceptor, dns_forwarder_handle) = DnsInterceptor::new(
255        &mut sockets,
256        dns_config,
257        shared.clone(),
258        &tokio_handle,
259        gateway_ips,
260        network_policy.clone(),
261        config.gateway,
262        config.gateway_mac,
263        config.guest_mac,
264    );
265    let mut port_publisher = PortPublisher::new(
266        &published_ports,
267        config.guest_ipv4,
268        config.guest_ipv6,
269        config.gateway.ipv4,
270        config.gateway.ipv6,
271        config.gateway_mac,
272        config.guest_mac,
273        network_policy.clone(),
274        shared.clone(),
275        &tokio_handle,
276    );
277    let mut udp_relay = UdpRelay::new(
278        shared.clone(),
279        config.gateway_mac,
280        config.guest_mac,
281        config.mtu,
282        tokio_handle.clone(),
283    );
284    let mut udp_fragments = Ipv4UdpFragmentReassembler::new();
285    let mut ipv6_udp_fragments = Ipv6UdpFragmentReassembler::new();
286    let icmp_relay = IcmpRelay::new(
287        shared.clone(),
288        config.gateway_mac,
289        config.guest_mac,
290        tokio_handle.clone(),
291    );
292
293    // Rate-limit cleanup operations: run at most once per second.
294    let mut last_cleanup = std::time::Instant::now();
295
296    // Wake sources for sleeping.
297    #[cfg(unix)]
298    let mut poll_fds = [
299        libc::pollfd {
300            fd: shared.tx_wake.as_raw_fd(),
301            events: libc::POLLIN,
302            revents: 0,
303        },
304        libc::pollfd {
305            fd: shared.proxy_wake.as_raw_fd(),
306            events: libc::POLLIN,
307            revents: 0,
308        },
309    ];
310    #[cfg(windows)]
311    let wait_context = match windows_stack_wait_context(&shared) {
312        Ok(context) => context,
313        Err(err) => {
314            tracing::error!(error = %err, "network poll loop: failed to create wait context");
315            return;
316        }
317    };
318
319    loop {
320        let now = smoltcp_now();
321
322        // ── Phase 1: Drain all guest frames with pre-inspection ──────────
323        while let Some(frame) = device.stage_next_frame() {
324            if handle_gateway_icmp_echo(frame, &config, &shared) {
325                device.drop_staged_frame();
326                continue;
327            }
328
329            if icmp_relay.relay_outbound_if_echo(frame, &config, &network_policy) {
330                device.drop_staged_frame();
331                continue;
332            }
333
334            match classify_frame(frame) {
335                FrameAction::TcpSyn { src, dst } => {
336                    let allow = match DnsPortType::from_tcp(dst.port()) {
337                        // Plain DNS: the interceptor enforces policy at
338                        // the application layer (block list + rebind
339                        // protection); bypass the network egress check.
340                        DnsPortType::Dns => true,
341                        // DoT: intercept only when TLS MITM is
342                        // configured. Without it, the block list can't
343                        // apply (traffic is encrypted end-to-end), so
344                        // we refuse to force a fall-back to plain
345                        // TCP/53. When TLS MITM is configured, bypass
346                        // egress policy the same way plain DNS does —
347                        // policy for the upstream resolver is applied
348                        // per query by the forwarder.
349                        DnsPortType::EncryptedDns => {
350                            if tls_state.is_some() {
351                                true
352                            } else {
353                                tracing::debug!(%dst, "DoT port refused (TLS interception not configured); stub should fall back to TCP/53");
354                                false
355                            }
356                        }
357                        // Alternative DNS protocol we can't proxy:
358                        // refuse outright — no socket means smoltcp
359                        // emits RST, which the guest's stub treats as
360                        // "upstream unavailable" and falls back to
361                        // plain TCP/53.
362                        DnsPortType::AlternativeDns => {
363                            tracing::debug!(%dst, "alternative-DNS TCP port refused; stub should fall back to TCP/53");
364                            false
365                        }
366                        // Other: regular outbound — defer Domain rules to first-flight;
367                        // accept unless an IP-layer rule denies.
368                        DnsPortType::Other => match network_policy.evaluate_egress_with_source(
369                            dst,
370                            Protocol::Tcp,
371                            &shared,
372                            HostnameSource::Deferred,
373                        ) {
374                            EgressEvaluation::Allow | EgressEvaluation::DeferUntilHostname => true,
375                            EgressEvaluation::Deny => false,
376                        },
377                    };
378                    if allow && !conn_tracker.has_socket_for(&src, &dst) {
379                        conn_tracker.create_tcp_socket(src, dst, &mut sockets);
380                    }
381                    // Let smoltcp process — matching socket completes
382                    // handshake, no socket means auto-RST.
383                    iface.poll_ingress_single(now, &mut device, &mut sockets);
384                }
385
386                FrameAction::UdpRelay { src, dst } => {
387                    relay_udp_frame(
388                        frame,
389                        src,
390                        dst,
391                        &config,
392                        &network_policy,
393                        &shared,
394                        &mut port_publisher,
395                        tls_state.as_deref(),
396                        &mut udp_relay,
397                    );
398                    device.drop_staged_frame();
399                }
400
401                FrameAction::Ipv4UdpFragment => {
402                    if let Some(datagram) = udp_fragments.push(frame) {
403                        handle_reassembled_udp_datagram(
404                            datagram,
405                            &mut device,
406                            &mut iface,
407                            now,
408                            &mut sockets,
409                            &config,
410                            &network_policy,
411                            &shared,
412                            &mut port_publisher,
413                            tls_state.as_deref(),
414                            &mut udp_relay,
415                        );
416                    } else {
417                        device.drop_staged_frame();
418                    }
419                }
420
421                FrameAction::Ipv6UdpFragment => {
422                    if let Some(datagram) = ipv6_udp_fragments.push(frame) {
423                        handle_reassembled_udp_datagram(
424                            datagram,
425                            &mut device,
426                            &mut iface,
427                            now,
428                            &mut sockets,
429                            &config,
430                            &network_policy,
431                            &shared,
432                            &mut port_publisher,
433                            tls_state.as_deref(),
434                            &mut udp_relay,
435                        );
436                    } else {
437                        device.drop_staged_frame();
438                    }
439                }
440
441                FrameAction::Ipv6UnsupportedFragment => {
442                    // Fragmented UDP is only forwarded after reassembly and policy evaluation.
443                    // Other fragmented IPv6 traffic is dropped rather than passed through with
444                    // an unknown transport tuple.
445                    device.drop_staged_frame();
446                }
447
448                FrameAction::Dns | FrameAction::Passthrough => {
449                    // ARP, ICMP, DNS (port 53), TCP data — smoltcp handles.
450                    iface.poll_ingress_single(now, &mut device, &mut sockets);
451                }
452            }
453        }
454
455        // ── Phase 2: Ingress egress + maintenance ─────────────────────────
456        // Flush frames generated by Phase 1 ingress (ACKs, SYN-ACKs, etc.)
457        // before relaying data so smoltcp has up-to-date state.
458        loop {
459            let result = iface.poll_egress(now, &mut device, &mut sockets);
460            if matches!(result, smoltcp::iface::PollResult::None) {
461                break;
462            }
463        }
464        iface.poll_maintenance(now);
465
466        // Coalesced wake: if Phase 1/2 emitted any frames, wake the
467        // NetWorker once instead of per-frame.
468        if device.frames_emitted.swap(false, Ordering::Relaxed) {
469            shared.rx_wake.wake();
470        }
471
472        // ── Phase 3: Service connections + relay data ────────────────────
473        // Relay proxy data INTO smoltcp sockets first, then a single egress
474        // pass flushes everything. This eliminates the former "Phase 2b"
475        // double-egress pattern.
476        conn_tracker.relay_data(&mut sockets);
477        dns_interceptor.process(&mut sockets);
478
479        // Accept queued inbound connections from published port listeners.
480        port_publisher.accept_inbound(&mut iface, &mut sockets, &shared, &tokio_handle);
481        port_publisher.relay_data(&mut sockets);
482
483        // Detect newly-established connections and spawn proxy tasks.
484        let new_conns = conn_tracker.take_new_connections(&mut sockets);
485        for conn in new_conns {
486            if let Some(ref tls_state) = tls_state
487                && tls_state
488                    .config
489                    .intercepted_ports
490                    .contains(&conn.dst.port())
491            {
492                // TLS-intercepted port — spawn TLS MITM proxy.
493                let connect_dst = resolve_host_dst(conn.dst, config.gateway);
494                tls_proxy::spawn_tls_proxy(
495                    &tokio_handle,
496                    conn.dst,
497                    connect_dst,
498                    conn.from_smoltcp,
499                    conn.to_smoltcp,
500                    shared.clone(),
501                    tls_state.clone(),
502                    network_policy.clone(),
503                    conn.proxy_connect,
504                );
505                continue;
506            }
507            if conn.dst.port() == 53 {
508                // DNS proxies have no guest-visible
509                // "upstream-unreachable" failure mode — even an
510                // upstream DNS failure yields SERVFAIL responses
511                // rather than a silently-closed connection. Mark the
512                // connection as connected so normal task exit
513                // produces FIN, not RST.
514                conn.proxy_connect.mark_connected();
515
516                // DNS over TCP: route through the same forwarder the UDP
517                // path uses. The forwarder applies the domain block list
518                // and rebind protection to every query and routes
519                // upstream based on `conn.dst.ip()` — the configured
520                // upstream for queries to the gateway, direct forward
521                // to the chosen `@target` (subject to egress policy)
522                // otherwise. No gateway→loopback rewrite here: the
523                // forwarder dials the configured upstream, not the
524                // gateway.
525                DnsTcpProxy::spawn(
526                    &tokio_handle,
527                    conn.dst,
528                    conn.from_smoltcp,
529                    conn.to_smoltcp,
530                    dns_forwarder_handle.clone(),
531                    shared.clone(),
532                );
533                continue;
534            }
535            if conn.dst.port() == 853
536                && let Some(ref tls_state) = tls_state
537            {
538                // Same "always upstream-connected" reasoning as plain DNS over TCP.
539                conn.proxy_connect.mark_connected();
540
541                // DNS over TLS: terminate TLS at the gateway with a
542                // per-domain cert, hand the inner DNS frames to the
543                // same forwarder plain DNS uses. Policy for the
544                // chosen `@target` resolver is applied per-query by
545                // the forwarder (block list + rebind + egress).
546                DotProxy::spawn(
547                    &tokio_handle,
548                    conn.dst,
549                    conn.from_smoltcp,
550                    conn.to_smoltcp,
551                    dns_forwarder_handle.clone(),
552                    tls_state.clone(),
553                    shared.clone(),
554                );
555                continue;
556            }
557            // Plain TCP proxy.
558            let connect_dst = resolve_host_dst(conn.dst, config.gateway);
559            proxy::spawn_tcp_proxy(
560                &tokio_handle,
561                conn.dst,
562                connect_dst,
563                conn.from_smoltcp,
564                conn.to_smoltcp,
565                shared.clone(),
566                network_policy.clone(),
567                // Load the current snapshot per connection so live secret
568                // updates apply to traffic the guest starts afterwards.
569                secrets.load(),
570                tls_state.clone(),
571                conn.proxy_connect,
572            );
573        }
574
575        // Rate-limited cleanup: TIME_WAIT is 60s, session timeout is 60s,
576        // so checking once per second is more than sufficient.
577        if last_cleanup.elapsed() >= std::time::Duration::from_secs(1) {
578            conn_tracker.cleanup_closed(&mut sockets);
579            port_publisher.cleanup_closed(&mut sockets);
580            udp_relay.cleanup_expired();
581            udp_fragments.cleanup_expired();
582            ipv6_udp_fragments.cleanup_expired();
583            shared.cleanup_resolved_hostnames();
584            last_cleanup = std::time::Instant::now();
585        }
586
587        // ── Phase 4: Flush relay data + sleep ────────────────────────────
588        // Single egress pass flushes all data written by Phase 3.
589        loop {
590            let result = iface.poll_egress(now, &mut device, &mut sockets);
591            if matches!(result, smoltcp::iface::PollResult::None) {
592                break;
593            }
594        }
595
596        // Coalesced wake: if Phase 3/4 emitted any frames, wake once.
597        if device.frames_emitted.swap(false, Ordering::Relaxed) {
598            shared.rx_wake.wake();
599        }
600
601        let timeout_ms = iface
602            .poll_delay(now, &sockets)
603            .map(|d| d.total_millis().min(i32::MAX as u64) as i32)
604            .unwrap_or(100); // 100ms fallback when no timers pending.
605
606        #[cfg(unix)]
607        sleep_until_stack_wake(&shared, timeout_ms, &mut poll_fds);
608        #[cfg(windows)]
609        sleep_until_stack_wake_windows(&shared, timeout_ms, &wait_context);
610    }
611}
612
613//--------------------------------------------------------------------------------------------------
614// Functions: Helpers
615//--------------------------------------------------------------------------------------------------
616
617#[cfg(unix)]
618fn sleep_until_stack_wake(shared: &SharedState, timeout_ms: i32, poll_fds: &mut [libc::pollfd; 2]) {
619    // SAFETY: poll_fds is a valid array of pollfd structs with valid fds.
620    unsafe {
621        libc::poll(
622            poll_fds.as_mut_ptr(),
623            poll_fds.len() as libc::nfds_t,
624            timeout_ms,
625        );
626    }
627
628    if poll_fds[0].revents & libc::POLLIN != 0 {
629        shared.tx_wake.drain();
630    }
631    if poll_fds[1].revents & libc::POLLIN != 0 {
632        shared.proxy_wake.drain();
633    }
634}
635
636#[cfg(windows)]
637fn windows_stack_wait_context(shared: &SharedState) -> std::io::Result<WaitContext> {
638    let mut context = WaitContext::new();
639    context.add(
640        EventSource::waitable_handle(shared.tx_wake.as_raw_handle(), TX_WAKE_TOKEN),
641        EventSet::IN,
642    )?;
643    context.add(
644        EventSource::waitable_handle(shared.proxy_wake.as_raw_handle(), PROXY_WAKE_TOKEN),
645        EventSet::IN,
646    )?;
647    Ok(context)
648}
649
650#[cfg(windows)]
651fn sleep_until_stack_wake_windows(
652    shared: &SharedState,
653    timeout_ms: i32,
654    wait_context: &WaitContext,
655) {
656    let mut events = [WaitEvent::default(); 2];
657    let count = match wait_context.wait(timeout_ms, &mut events) {
658        Ok(count) => count,
659        Err(err) => {
660            tracing::warn!(error = %err, "network poll loop: wait failed");
661            return;
662        }
663    };
664
665    for event in events.iter().take(count) {
666        match event.token() {
667            TX_WAKE_TOKEN => shared.tx_wake.drain(),
668            PROXY_WAKE_TOKEN => shared.proxy_wake.drain(),
669            token => tracing::warn!(token, "network poll loop: unknown wake token"),
670        }
671    }
672}
673
674/// Apply the common non-DNS UDP dispatch path to a complete guest datagram.
675#[allow(clippy::too_many_arguments)]
676fn relay_udp_frame(
677    frame: &[u8],
678    src: SocketAddr,
679    dst: SocketAddr,
680    config: &PollLoopConfig,
681    network_policy: &NetworkPolicy,
682    shared: &Arc<SharedState>,
683    port_publisher: &mut PortPublisher,
684    tls_state: Option<&TlsState>,
685    udp_relay: &mut UdpRelay,
686) {
687    if port_publisher.relay_udp_outbound(frame, src, dst) {
688        return;
689    }
690
691    // QUIC blocking: drop UDP to intercepted ports when TLS interception is active.
692    if let Some(tls) = tls_state
693        && tls.config.intercepted_ports.contains(&dst.port())
694        && tls.config.block_quic_on_intercept
695    {
696        return;
697    }
698
699    match DnsPortType::from_udp(dst.port()) {
700        // Dns: unreachable here — classify_transport routes UDP/53 to
701        // FrameAction::Dns, not UdpRelay. Defensive drop covers regressions.
702        DnsPortType::Dns | DnsPortType::EncryptedDns => return,
703        // Alternative DNS protocols on well-known UDP ports are dropped —
704        // forces fall-back to UDP/53.
705        DnsPortType::AlternativeDns => {
706            tracing::debug!(%dst, "alternative-DNS UDP port dropped; stub should fall back to UDP/53");
707            return;
708        }
709        DnsPortType::Other => {}
710    }
711
712    // Policy is applied after reassembly, when the UDP destination port is known.
713    if network_policy
714        .evaluate_egress(dst, Protocol::Udp, shared)
715        .is_deny()
716    {
717        return;
718    }
719
720    // Resolve the host-side destination for the dial. `dst` stays unchanged so
721    // reply frames are stamped with the IP the guest expects.
722    let host_dst = resolve_host_dst(dst, config.gateway);
723    udp_relay.relay_outbound(frame, src, dst, host_dst);
724}
725
726/// Dispatch a complete datagram produced by fragment reassembly.
727#[allow(clippy::too_many_arguments)]
728fn handle_reassembled_udp_datagram(
729    datagram: ReassembledUdpDatagram,
730    device: &mut SmoltcpDevice,
731    iface: &mut Interface,
732    now: Instant,
733    sockets: &mut SocketSet<'_>,
734    config: &PollLoopConfig,
735    network_policy: &NetworkPolicy,
736    shared: &Arc<SharedState>,
737    port_publisher: &mut PortPublisher,
738    tls_state: Option<&TlsState>,
739    udp_relay: &mut UdpRelay,
740) {
741    if DnsPortType::from_udp(datagram.dst.port()) == DnsPortType::Dns {
742        device.replace_staged_frame(datagram.frame);
743        iface.poll_ingress_single(now, device, sockets);
744        return;
745    }
746
747    relay_udp_frame(
748        &datagram.frame,
749        datagram.src,
750        datagram.dst,
751        config,
752        network_policy,
753        shared,
754        port_publisher,
755        tls_state,
756        udp_relay,
757    );
758    device.drop_staged_frame();
759}
760
761/// Map a guest-wire destination to its host-socket equivalent.
762///
763/// Gateway IPs rewrite to loopback (`127.0.0.1` / `::1`); everything else
764/// passes through. Shared by the TCP proxy dispatch and the UDP relay.
765///
766/// # Arguments
767///
768/// * `dst` - Destination from the guest's packet.
769/// * `gateway` - Per-sandbox gateway IPs that trigger the loopback rewrite.
770pub(crate) fn resolve_host_dst(dst: SocketAddr, gateway: GatewayIps) -> SocketAddr {
771    match dst.ip() {
772        IpAddr::V4(v4) if gateway.ipv4 == Some(v4) => {
773            SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), dst.port())
774        }
775        IpAddr::V6(v6) if gateway.ipv6 == Some(v6) => {
776            SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), dst.port())
777        }
778        _ => dst,
779    }
780}
781
782/// Get the current time as a smoltcp [`Instant`] using a monotonic clock.
783///
784/// Uses `std::time::Instant` (monotonic) instead of `SystemTime` (wall
785/// clock) to avoid issues with NTP clock step corrections that could
786/// cause smoltcp timers to misbehave.
787fn smoltcp_now() -> Instant {
788    static EPOCH: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
789    let epoch = EPOCH.get_or_init(std::time::Instant::now);
790    let elapsed = epoch.elapsed();
791    Instant::from_millis(elapsed.as_millis() as i64)
792}
793
794/// Reply locally to ICMP echo requests aimed at the sandbox gateway.
795///
796/// `any_ip` is required so smoltcp accepts guest traffic for arbitrary remote
797/// destinations, but that would make smoltcp's automatic ICMP echo replies
798/// spoof remote hosts. Handle only the real gateway IPs here and leave all
799/// other ICMP traffic untouched.
800fn handle_gateway_icmp_echo(frame: &[u8], config: &PollLoopConfig, shared: &SharedState) -> bool {
801    let Ok(eth) = EthernetFrame::new_checked(frame) else {
802        return false;
803    };
804
805    let reply = match eth.ethertype() {
806        EthernetProtocol::Ipv4 => gateway_icmpv4_echo_reply(&eth, config),
807        EthernetProtocol::Ipv6 => gateway_icmpv6_echo_reply(&eth, config),
808        _ => None,
809    };
810    let Some(reply) = reply else {
811        return false;
812    };
813
814    shared.push_rx_frame_and_wake(reply);
815
816    true
817}
818
819/// Build an IPv4 ICMP echo reply when the guest pings the gateway IPv4.
820fn gateway_icmpv4_echo_reply(
821    eth: &EthernetFrame<&[u8]>,
822    config: &PollLoopConfig,
823) -> Option<Vec<u8>> {
824    let gateway_ipv4 = config.gateway.ipv4?;
825    let ipv4 = Ipv4Packet::new_checked(eth.payload()).ok()?;
826    if ipv4.dst_addr() != gateway_ipv4 || ipv4.next_header() != IpProtocol::Icmp {
827        return None;
828    }
829
830    let icmp = Icmpv4Packet::new_checked(ipv4.payload()).ok()?;
831    let Icmpv4Repr::EchoRequest {
832        ident,
833        seq_no,
834        data,
835    } = Icmpv4Repr::parse(&icmp, &smoltcp::phy::ChecksumCapabilities::default()).ok()?
836    else {
837        return None;
838    };
839
840    let ipv4_repr = Ipv4Repr {
841        src_addr: gateway_ipv4,
842        dst_addr: ipv4.src_addr(),
843        next_header: IpProtocol::Icmp,
844        payload_len: 8 + data.len(),
845        hop_limit: 64,
846    };
847    let icmp_repr = Icmpv4Repr::EchoReply {
848        ident,
849        seq_no,
850        data,
851    };
852    let mut reply = vec![0u8; 14 + ipv4_repr.buffer_len() + icmp_repr.buffer_len()];
853
854    let mut reply_eth = EthernetFrame::new_unchecked(&mut reply);
855    reply_eth.set_src_addr(EthernetAddress(config.gateway_mac));
856    reply_eth.set_dst_addr(eth.src_addr());
857    reply_eth.set_ethertype(EthernetProtocol::Ipv4);
858
859    ipv4_repr.emit(
860        &mut Ipv4Packet::new_unchecked(&mut reply[14..34]),
861        &smoltcp::phy::ChecksumCapabilities::default(),
862    );
863    icmp_repr.emit(
864        &mut Icmpv4Packet::new_unchecked(&mut reply[34..]),
865        &smoltcp::phy::ChecksumCapabilities::default(),
866    );
867
868    Some(reply)
869}
870
871/// Build an IPv6 ICMP echo reply when the guest pings the gateway IPv6.
872fn gateway_icmpv6_echo_reply(
873    eth: &EthernetFrame<&[u8]>,
874    config: &PollLoopConfig,
875) -> Option<Vec<u8>> {
876    let gateway_ipv6 = config.gateway.ipv6?;
877    let ipv6 = Ipv6Packet::new_checked(eth.payload()).ok()?;
878    if ipv6.dst_addr() != gateway_ipv6 || ipv6.next_header() != IpProtocol::Icmpv6 {
879        return None;
880    }
881
882    let icmp = Icmpv6Packet::new_checked(ipv6.payload()).ok()?;
883    let Icmpv6Repr::EchoRequest {
884        ident,
885        seq_no,
886        data,
887    } = Icmpv6Repr::parse(
888        &ipv6.src_addr(),
889        &ipv6.dst_addr(),
890        &icmp,
891        &smoltcp::phy::ChecksumCapabilities::default(),
892    )
893    .ok()?
894    else {
895        return None;
896    };
897
898    let ipv6_repr = Ipv6Repr {
899        src_addr: gateway_ipv6,
900        dst_addr: ipv6.src_addr(),
901        next_header: IpProtocol::Icmpv6,
902        payload_len: icmp_repr_buffer_len_v6(data),
903        hop_limit: 64,
904    };
905    let icmp_repr = Icmpv6Repr::EchoReply {
906        ident,
907        seq_no,
908        data,
909    };
910    let ipv6_hdr_len = 40;
911    let mut reply = vec![0u8; 14 + ipv6_hdr_len + icmp_repr.buffer_len()];
912
913    let mut reply_eth = EthernetFrame::new_unchecked(&mut reply);
914    reply_eth.set_src_addr(EthernetAddress(config.gateway_mac));
915    reply_eth.set_dst_addr(eth.src_addr());
916    reply_eth.set_ethertype(EthernetProtocol::Ipv6);
917
918    ipv6_repr.emit(&mut Ipv6Packet::new_unchecked(&mut reply[14..54]));
919    icmp_repr.emit(
920        &gateway_ipv6,
921        &ipv6.src_addr(),
922        &mut Icmpv6Packet::new_unchecked(&mut reply[54..]),
923        &smoltcp::phy::ChecksumCapabilities::default(),
924    );
925
926    Some(reply)
927}
928
929fn icmp_repr_buffer_len_v6(data: &[u8]) -> usize {
930    Icmpv6Repr::EchoReply {
931        ident: 0,
932        seq_no: 0,
933        data,
934    }
935    .buffer_len()
936}
937
938/// Classify an IPv4 packet payload (after stripping the Ethernet header).
939fn classify_ipv4(payload: &[u8]) -> FrameAction {
940    let Ok(ipv4) = Ipv4Packet::new_checked(payload) else {
941        return FrameAction::Passthrough;
942    };
943    if is_ipv4_udp_fragment(&ipv4) {
944        return FrameAction::Ipv4UdpFragment;
945    }
946    classify_transport(
947        ipv4.next_header(),
948        ipv4.src_addr().into(),
949        ipv4.dst_addr().into(),
950        ipv4.payload(),
951    )
952}
953
954/// Classify an IPv6 packet payload (after stripping the Ethernet header).
955fn classify_ipv6(payload: &[u8]) -> FrameAction {
956    let Ok(ipv6) = Ipv6Packet::new_checked(payload) else {
957        return FrameAction::Passthrough;
958    };
959    if is_ipv6_udp_fragment(&ipv6) {
960        return FrameAction::Ipv6UdpFragment;
961    }
962    if is_ipv6_fragment(&ipv6) {
963        return FrameAction::Ipv6UnsupportedFragment;
964    }
965    classify_transport(
966        ipv6.next_header(),
967        ipv6.src_addr().into(),
968        ipv6.dst_addr().into(),
969        ipv6.payload(),
970    )
971}
972
973/// Classify the transport-layer protocol (shared by IPv4 and IPv6).
974fn classify_transport(
975    protocol: IpProtocol,
976    src_ip: std::net::IpAddr,
977    dst_ip: std::net::IpAddr,
978    transport_payload: &[u8],
979) -> FrameAction {
980    match protocol {
981        IpProtocol::Tcp => {
982            let Ok(tcp) = TcpPacket::new_checked(transport_payload) else {
983                return FrameAction::Passthrough;
984            };
985            if tcp.syn() && !tcp.ack() {
986                FrameAction::TcpSyn {
987                    src: SocketAddr::new(src_ip, tcp.src_port()),
988                    dst: SocketAddr::new(dst_ip, tcp.dst_port()),
989                }
990            } else {
991                FrameAction::Passthrough
992            }
993        }
994        IpProtocol::Udp => {
995            let Ok(udp) = UdpPacket::new_checked(transport_payload) else {
996                return FrameAction::Passthrough;
997            };
998            // The plain-DNS port (UDP/53) lives in dns::common::ports so
999            // the alternative-DNS refusal logic and this dispatcher
1000            // share one source of truth for "which UDP ports are DNS".
1001            if DnsPortType::from_udp(udp.dst_port()) == DnsPortType::Dns {
1002                FrameAction::Dns
1003            } else {
1004                FrameAction::UdpRelay {
1005                    src: SocketAddr::new(src_ip, udp.src_port()),
1006                    dst: SocketAddr::new(dst_ip, udp.dst_port()),
1007                }
1008            }
1009        }
1010        _ => FrameAction::Passthrough, // ICMP, etc.
1011    }
1012}
1013
1014//--------------------------------------------------------------------------------------------------
1015// Tests
1016//--------------------------------------------------------------------------------------------------
1017
1018#[cfg(test)]
1019mod tests {
1020    use super::*;
1021    use std::sync::Arc;
1022
1023    use smoltcp::phy::ChecksumCapabilities;
1024    use smoltcp::wire::{
1025        ArpOperation, ArpPacket, ArpRepr, EthernetRepr, Icmpv4Packet, Icmpv4Repr, Ipv4Repr,
1026    };
1027
1028    use crate::device::SmoltcpDevice;
1029    use crate::shared::SharedState;
1030
1031    /// Build a minimal Ethernet + IPv4 + TCP SYN frame.
1032    fn build_tcp_syn_frame(
1033        src_ip: [u8; 4],
1034        dst_ip: [u8; 4],
1035        src_port: u16,
1036        dst_port: u16,
1037    ) -> Vec<u8> {
1038        let mut frame = vec![0u8; 14 + 20 + 20]; // eth + ipv4 + tcp
1039
1040        // Ethernet header.
1041        frame[12] = 0x08; // EtherType: IPv4
1042        frame[13] = 0x00;
1043
1044        // IPv4 header.
1045        let ip = &mut frame[14..34];
1046        ip[0] = 0x45; // Version + IHL
1047        let total_len = 40u16; // 20 (IP) + 20 (TCP)
1048        ip[2..4].copy_from_slice(&total_len.to_be_bytes());
1049        ip[6] = 0x40; // Don't Fragment
1050        ip[8] = 64; // TTL
1051        ip[9] = 6; // Protocol: TCP
1052        ip[12..16].copy_from_slice(&src_ip);
1053        ip[16..20].copy_from_slice(&dst_ip);
1054
1055        // TCP header.
1056        let tcp = &mut frame[34..54];
1057        tcp[0..2].copy_from_slice(&src_port.to_be_bytes());
1058        tcp[2..4].copy_from_slice(&dst_port.to_be_bytes());
1059        tcp[12] = 0x50; // Data offset: 5 words
1060        tcp[13] = 0x02; // SYN flag
1061
1062        frame
1063    }
1064
1065    /// Build a minimal Ethernet + IPv4 + UDP frame.
1066    fn build_udp_frame(src_ip: [u8; 4], dst_ip: [u8; 4], src_port: u16, dst_port: u16) -> Vec<u8> {
1067        let mut frame = vec![0u8; 14 + 20 + 8]; // eth + ipv4 + udp
1068
1069        // Ethernet header.
1070        frame[12] = 0x08;
1071        frame[13] = 0x00;
1072
1073        // IPv4 header.
1074        let ip = &mut frame[14..34];
1075        ip[0] = 0x45;
1076        let total_len = 28u16; // 20 (IP) + 8 (UDP)
1077        ip[2..4].copy_from_slice(&total_len.to_be_bytes());
1078        ip[8] = 64;
1079        ip[9] = 17; // Protocol: UDP
1080        ip[12..16].copy_from_slice(&src_ip);
1081        ip[16..20].copy_from_slice(&dst_ip);
1082
1083        // UDP header.
1084        let udp = &mut frame[34..42];
1085        udp[0..2].copy_from_slice(&src_port.to_be_bytes());
1086        udp[2..4].copy_from_slice(&dst_port.to_be_bytes());
1087        let udp_len = 8u16;
1088        udp[4..6].copy_from_slice(&udp_len.to_be_bytes());
1089
1090        frame
1091    }
1092
1093    /// Build a minimal Ethernet + IPv4 + ICMP echo request frame.
1094    fn build_icmpv4_echo_frame(
1095        src_mac: [u8; 6],
1096        dst_mac: [u8; 6],
1097        src_ip: [u8; 4],
1098        dst_ip: [u8; 4],
1099        ident: u16,
1100        seq_no: u16,
1101        data: &[u8],
1102    ) -> Vec<u8> {
1103        let ipv4_repr = Ipv4Repr {
1104            src_addr: Ipv4Addr::from(src_ip),
1105            dst_addr: Ipv4Addr::from(dst_ip),
1106            next_header: IpProtocol::Icmp,
1107            payload_len: 8 + data.len(),
1108            hop_limit: 64,
1109        };
1110        let icmp_repr = Icmpv4Repr::EchoRequest {
1111            ident,
1112            seq_no,
1113            data,
1114        };
1115        let frame_len = 14 + ipv4_repr.buffer_len() + icmp_repr.buffer_len();
1116        let mut frame = vec![0u8; frame_len];
1117
1118        let mut eth_frame = EthernetFrame::new_unchecked(&mut frame);
1119        EthernetRepr {
1120            src_addr: EthernetAddress(src_mac),
1121            dst_addr: EthernetAddress(dst_mac),
1122            ethertype: EthernetProtocol::Ipv4,
1123        }
1124        .emit(&mut eth_frame);
1125
1126        ipv4_repr.emit(
1127            &mut Ipv4Packet::new_unchecked(&mut frame[14..34]),
1128            &ChecksumCapabilities::default(),
1129        );
1130        icmp_repr.emit(
1131            &mut Icmpv4Packet::new_unchecked(&mut frame[34..]),
1132            &ChecksumCapabilities::default(),
1133        );
1134
1135        frame
1136    }
1137
1138    /// Build a minimal Ethernet + ARP request frame.
1139    fn build_arp_request_frame(src_mac: [u8; 6], src_ip: [u8; 4], target_ip: [u8; 4]) -> Vec<u8> {
1140        let mut frame = vec![0u8; 14 + 28];
1141
1142        let mut eth_frame = EthernetFrame::new_unchecked(&mut frame);
1143        EthernetRepr {
1144            src_addr: EthernetAddress(src_mac),
1145            dst_addr: EthernetAddress([0xff; 6]),
1146            ethertype: EthernetProtocol::Arp,
1147        }
1148        .emit(&mut eth_frame);
1149
1150        ArpRepr::EthernetIpv4 {
1151            operation: ArpOperation::Request,
1152            source_hardware_addr: EthernetAddress(src_mac),
1153            source_protocol_addr: Ipv4Addr::from(src_ip),
1154            target_hardware_addr: EthernetAddress([0x00; 6]),
1155            target_protocol_addr: Ipv4Addr::from(target_ip),
1156        }
1157        .emit(&mut ArpPacket::new_unchecked(&mut frame[14..]));
1158
1159        frame
1160    }
1161
1162    #[test]
1163    fn classify_tcp_syn() {
1164        let frame = build_tcp_syn_frame([10, 0, 0, 2], [93, 184, 216, 34], 54321, 443);
1165        match classify_frame(&frame) {
1166            FrameAction::TcpSyn { src, dst } => {
1167                assert_eq!(
1168                    src,
1169                    SocketAddr::new(Ipv4Addr::new(10, 0, 0, 2).into(), 54321)
1170                );
1171                assert_eq!(
1172                    dst,
1173                    SocketAddr::new(Ipv4Addr::new(93, 184, 216, 34).into(), 443)
1174                );
1175            }
1176            _ => panic!("expected TcpSyn"),
1177        }
1178    }
1179
1180    #[test]
1181    fn classify_tcp_ack_is_passthrough() {
1182        let mut frame = build_tcp_syn_frame([10, 0, 0, 2], [93, 184, 216, 34], 54321, 443);
1183        // Change flags to ACK only (not SYN).
1184        frame[34 + 13] = 0x10; // ACK flag
1185        assert!(matches!(classify_frame(&frame), FrameAction::Passthrough));
1186    }
1187
1188    #[test]
1189    fn classify_udp_dns() {
1190        let frame = build_udp_frame([10, 0, 0, 2], [10, 0, 0, 1], 12345, 53);
1191        assert!(matches!(classify_frame(&frame), FrameAction::Dns));
1192    }
1193
1194    #[test]
1195    fn classify_udp_non_dns() {
1196        let frame = build_udp_frame([10, 0, 0, 2], [8, 8, 8, 8], 12345, 443);
1197        match classify_frame(&frame) {
1198            FrameAction::UdpRelay { src, dst } => {
1199                assert_eq!(src.port(), 12345);
1200                assert_eq!(dst.port(), 443);
1201            }
1202            _ => panic!("expected UdpRelay"),
1203        }
1204    }
1205
1206    #[test]
1207    fn classify_ipv4_udp_fragment() {
1208        let mut frame = build_udp_frame([10, 0, 0, 2], [8, 8, 8, 8], 12345, 443);
1209        frame[14 + 6] = 0x20; // More Fragments flag.
1210        assert!(matches!(
1211            classify_frame(&frame),
1212            FrameAction::Ipv4UdpFragment
1213        ));
1214    }
1215
1216    #[test]
1217    fn classify_ipv6_udp_fragment() {
1218        let mut frame = vec![0u8; 14 + 40 + 8];
1219
1220        frame[12] = 0x86;
1221        frame[13] = 0xdd;
1222
1223        let ip = &mut frame[14..54];
1224        ip[0] = 0x60;
1225        ip[4..6].copy_from_slice(&8u16.to_be_bytes());
1226        ip[6] = u8::from(IpProtocol::Ipv6Frag);
1227        ip[7] = 64;
1228        ip[8..24].copy_from_slice(&Ipv6Addr::LOCALHOST.octets());
1229        ip[24..40].copy_from_slice(&Ipv6Addr::LOCALHOST.octets());
1230
1231        let fragment = &mut frame[54..62];
1232        fragment[0] = u8::from(IpProtocol::Udp);
1233        fragment[3] = 1; // More Fragments flag.
1234
1235        assert!(matches!(
1236            classify_frame(&frame),
1237            FrameAction::Ipv6UdpFragment
1238        ));
1239    }
1240
1241    #[test]
1242    fn classify_ipv6_non_udp_fragment_is_unsupported() {
1243        let mut frame = vec![0u8; 14 + 40 + 8];
1244
1245        frame[12] = 0x86;
1246        frame[13] = 0xdd;
1247
1248        let ip = &mut frame[14..54];
1249        ip[0] = 0x60;
1250        ip[4..6].copy_from_slice(&8u16.to_be_bytes());
1251        ip[6] = u8::from(IpProtocol::Ipv6Frag);
1252        ip[7] = 64;
1253        ip[8..24].copy_from_slice(&Ipv6Addr::LOCALHOST.octets());
1254        ip[24..40].copy_from_slice(&Ipv6Addr::LOCALHOST.octets());
1255
1256        let fragment = &mut frame[54..62];
1257        fragment[0] = u8::from(IpProtocol::Tcp);
1258        fragment[3] = 1; // More Fragments flag.
1259
1260        assert!(matches!(
1261            classify_frame(&frame),
1262            FrameAction::Ipv6UnsupportedFragment
1263        ));
1264    }
1265
1266    #[test]
1267    fn classify_arp_is_passthrough() {
1268        let mut frame = vec![0u8; 42]; // ARP frame
1269        frame[12] = 0x08;
1270        frame[13] = 0x06; // EtherType: ARP
1271        assert!(matches!(classify_frame(&frame), FrameAction::Passthrough));
1272    }
1273
1274    #[test]
1275    fn classify_garbage_is_passthrough() {
1276        assert!(matches!(classify_frame(&[]), FrameAction::Passthrough));
1277        assert!(matches!(classify_frame(&[0; 5]), FrameAction::Passthrough));
1278    }
1279
1280    #[test]
1281    fn gateway_replies_to_icmp_echo_requests() {
1282        fn drive_one_frame(
1283            device: &mut SmoltcpDevice,
1284            iface: &mut Interface,
1285            sockets: &mut SocketSet<'_>,
1286            shared: &Arc<SharedState>,
1287            poll_config: &PollLoopConfig,
1288            now: Instant,
1289        ) {
1290            let frame = device.stage_next_frame().expect("expected staged frame");
1291            if handle_gateway_icmp_echo(frame, poll_config, shared) {
1292                device.drop_staged_frame();
1293                return;
1294            }
1295            let _ = iface.poll_ingress_single(now, device, sockets);
1296            let _ = iface.poll_egress(now, device, sockets);
1297        }
1298
1299        let shared = Arc::new(SharedState::new(4));
1300        let poll_config = PollLoopConfig {
1301            gateway_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
1302            guest_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
1303            gateway: GatewayIps {
1304                ipv4: Some(Ipv4Addr::new(100, 96, 0, 1)),
1305                ipv6: Some(Ipv6Addr::LOCALHOST),
1306            },
1307            guest_ipv4: Some(Ipv4Addr::new(100, 96, 0, 2)),
1308            guest_ipv6: None,
1309            mtu: 1500,
1310        };
1311        let guest_ipv4 = poll_config.guest_ipv4.unwrap();
1312        let gateway_ipv4 = poll_config.gateway.ipv4.unwrap();
1313        let mut device = SmoltcpDevice::new(shared.clone(), poll_config.mtu);
1314        let mut iface = create_interface(&mut device, &poll_config);
1315        let mut sockets = SocketSet::new(vec![]);
1316        let now = smoltcp_now();
1317
1318        // Mirror the real guest flow: resolve the gateway MAC before sending
1319        // the ICMP echo request.
1320        shared
1321            .tx_ring
1322            .push(build_arp_request_frame(
1323                poll_config.guest_mac,
1324                guest_ipv4.octets(),
1325                gateway_ipv4.octets(),
1326            ))
1327            .unwrap();
1328        shared
1329            .tx_ring
1330            .push(build_icmpv4_echo_frame(
1331                poll_config.guest_mac,
1332                poll_config.gateway_mac,
1333                guest_ipv4.octets(),
1334                gateway_ipv4.octets(),
1335                0x1234,
1336                0xABCD,
1337                b"ping",
1338            ))
1339            .unwrap();
1340
1341        drive_one_frame(
1342            &mut device,
1343            &mut iface,
1344            &mut sockets,
1345            &shared,
1346            &poll_config,
1347            now,
1348        );
1349        let _ = shared.rx_ring.pop().expect("expected ARP reply");
1350
1351        drive_one_frame(
1352            &mut device,
1353            &mut iface,
1354            &mut sockets,
1355            &shared,
1356            &poll_config,
1357            now,
1358        );
1359
1360        let reply = shared.rx_ring.pop().expect("expected ICMP echo reply");
1361        let eth = EthernetFrame::new_checked(&reply).expect("valid ethernet frame");
1362        assert_eq!(eth.src_addr(), EthernetAddress(poll_config.gateway_mac));
1363        assert_eq!(eth.dst_addr(), EthernetAddress(poll_config.guest_mac));
1364        assert_eq!(eth.ethertype(), EthernetProtocol::Ipv4);
1365
1366        let ipv4 = Ipv4Packet::new_checked(eth.payload()).expect("valid IPv4 packet");
1367        assert_eq!(ipv4.src_addr(), gateway_ipv4);
1368        assert_eq!(ipv4.dst_addr(), guest_ipv4);
1369        assert_eq!(ipv4.next_header(), IpProtocol::Icmp);
1370
1371        let icmp = Icmpv4Packet::new_checked(ipv4.payload()).expect("valid ICMP packet");
1372        let icmp_repr = Icmpv4Repr::parse(&icmp, &ChecksumCapabilities::default())
1373            .expect("valid ICMP echo reply");
1374        assert_eq!(
1375            icmp_repr,
1376            Icmpv4Repr::EchoReply {
1377                ident: 0x1234,
1378                seq_no: 0xABCD,
1379                data: b"ping",
1380            }
1381        );
1382    }
1383
1384    fn test_gateway() -> GatewayIps {
1385        GatewayIps {
1386            ipv4: Some(Ipv4Addr::new(100, 96, 0, 1)),
1387            ipv6: Some("fd42:6d73:62::1".parse().unwrap()),
1388        }
1389    }
1390
1391    #[test]
1392    fn resolve_host_dst_matches_ipv4() {
1393        let gw = test_gateway();
1394        let dst = SocketAddr::new(IpAddr::V4(gw.ipv4.unwrap()), 8080);
1395        assert_eq!(
1396            resolve_host_dst(dst, gw),
1397            SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080)
1398        );
1399    }
1400
1401    #[test]
1402    fn resolve_host_dst_matches_ipv6() {
1403        let gw = test_gateway();
1404        let dst = SocketAddr::new(IpAddr::V6(gw.ipv6.unwrap()), 8080);
1405        assert_eq!(
1406            resolve_host_dst(dst, gw),
1407            SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080)
1408        );
1409    }
1410
1411    #[test]
1412    fn resolve_host_dst_passes_through_when_family_absent() {
1413        let gw = GatewayIps {
1414            ipv4: None,
1415            ipv6: Some("fd42:6d73:62::1".parse().unwrap()),
1416        };
1417        // IPv4 dst with no IPv4 gateway must not be rewritten to loopback.
1418        let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(100, 96, 0, 1)), 8080);
1419        assert_eq!(resolve_host_dst(dst, gw), dst);
1420    }
1421
1422    #[test]
1423    fn resolve_host_dst_passes_through_non_gateway() {
1424        let gw = test_gateway();
1425        let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 443);
1426        assert_eq!(resolve_host_dst(dst, gw), dst);
1427    }
1428
1429    #[test]
1430    fn external_icmp_echo_requests_are_not_answered_locally() {
1431        fn drive_one_frame(
1432            device: &mut SmoltcpDevice,
1433            iface: &mut Interface,
1434            sockets: &mut SocketSet<'_>,
1435            shared: &Arc<SharedState>,
1436            poll_config: &PollLoopConfig,
1437            now: Instant,
1438        ) {
1439            let frame = device.stage_next_frame().expect("expected staged frame");
1440            if handle_gateway_icmp_echo(frame, poll_config, shared) {
1441                device.drop_staged_frame();
1442                return;
1443            }
1444            let _ = iface.poll_ingress_single(now, device, sockets);
1445            let _ = iface.poll_egress(now, device, sockets);
1446        }
1447
1448        let shared = Arc::new(SharedState::new(4));
1449        let poll_config = PollLoopConfig {
1450            gateway_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
1451            guest_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
1452            gateway: GatewayIps {
1453                ipv4: Some(Ipv4Addr::new(100, 96, 0, 1)),
1454                ipv6: Some(Ipv6Addr::LOCALHOST),
1455            },
1456            guest_ipv4: Some(Ipv4Addr::new(100, 96, 0, 2)),
1457            guest_ipv6: None,
1458            mtu: 1500,
1459        };
1460        let guest_ipv4 = poll_config.guest_ipv4.unwrap();
1461        let gateway_ipv4 = poll_config.gateway.ipv4.unwrap();
1462        let mut device = SmoltcpDevice::new(shared.clone(), poll_config.mtu);
1463        let mut iface = create_interface(&mut device, &poll_config);
1464        let mut sockets = SocketSet::new(vec![]);
1465        let now = smoltcp_now();
1466
1467        shared
1468            .tx_ring
1469            .push(build_arp_request_frame(
1470                poll_config.guest_mac,
1471                guest_ipv4.octets(),
1472                gateway_ipv4.octets(),
1473            ))
1474            .unwrap();
1475        shared
1476            .tx_ring
1477            .push(build_icmpv4_echo_frame(
1478                poll_config.guest_mac,
1479                poll_config.gateway_mac,
1480                guest_ipv4.octets(),
1481                [142, 251, 216, 46],
1482                0x1234,
1483                0xABCD,
1484                b"ping",
1485            ))
1486            .unwrap();
1487
1488        drive_one_frame(
1489            &mut device,
1490            &mut iface,
1491            &mut sockets,
1492            &shared,
1493            &poll_config,
1494            now,
1495        );
1496        let _ = shared.rx_ring.pop().expect("expected ARP reply");
1497
1498        drive_one_frame(
1499            &mut device,
1500            &mut iface,
1501            &mut sockets,
1502            &shared,
1503            &poll_config,
1504            now,
1505        );
1506        assert!(
1507            shared.rx_ring.pop().is_none(),
1508            "external ICMP should not be answered locally"
1509        );
1510    }
1511}