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