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