Skip to main content

smolvm_network/
stack.rs

1//! Host-side smoltcp runtime for the virtio-net backend.
2//!
3//! Context
4//! =======
5//!
6//! This file is the in-process "gateway" that sits behind the guest's virtio
7//! NIC. It does not configure the guest interface; that already happened inside
8//! `smolvm-agent`. Instead, this module:
9//! - receives raw Ethernet frames coming out of libkrun
10//! - feeds them into smoltcp as if smolvm were the guest's next-hop gateway
11//! - forwards guest DNS queries to a host UDP socket
12//! - relays guest TCP streams to host `TcpStream`s
13//!
14//! Conceptually, it plays the role of a tiny virtual router/NAT-side gateway:
15//!
16//! ```text
17//! guest eth0
18//!   -> Ethernet frame
19//!   -> Frame queues
20//!   -> smoltcp Interface (gateway MAC/IP)
21//!   -> protocol-specific handling:
22//!        - TCP  -> host relay threads
23//!        - DNS  -> host UDP socket
24//!        - UDP  -> per-flow host socket relay (udp_relay)
25//!   -> outbound network
26//! ```
27//!
28//! Poll-loop-centric view:
29//!
30//! ```text
31//! guest_to_host queue
32//!   -> VirtioNetworkDevice::stage_next_frame()
33//!   -> classify_guest_frame()
34//!   -> smoltcp ingress
35//!   -> protocol-specific side effects
36//!        - TCP SYN  -> create relay/socket state
37//!        - DNS UDP  -> gateway UDP socket
38//!        - other UDP-> destination-keyed relay socket
39//!   -> smoltcp egress
40//!   -> host_to_guest queue
41//!   -> FrameStream writer
42//! ```
43//!
44//! Runtime control flow:
45//!
46//! ```text
47//! new guest frame         -> guest_wake  -> poll loop
48//! host relay has data     -> relay_wake  -> poll loop
49//! published host connect  -> relay_wake  -> poll loop
50//! smoltcp emitted frames  -> host_wake   -> frame writer
51//! ```
52
53use crate::device::VirtioNetworkDevice;
54use crate::dns;
55use crate::egress::EgressPolicy;
56use crate::icmp_relay;
57use crate::queues::NetworkFrameQueues;
58use crate::tcp_listeners::AcceptedTcpConnection;
59use crate::tcp_relay::{spawn_tcp_relay, TcpRelayTable};
60use crate::udp_relay;
61use crate::virtio_net_log;
62use smoltcp::iface::{
63    Config, Interface, PollIngressSingleResult, PollResult, SocketHandle, SocketSet,
64};
65use smoltcp::socket::raw::{
66    PacketBuffer as RawPacketBuffer, PacketMetadata as RawPacketMetadata, Socket as RawSocket,
67};
68use smoltcp::socket::tcp;
69use smoltcp::socket::udp::{PacketBuffer, PacketMetadata, Socket as UdpSocket, UdpMetadata};
70use smoltcp::time::Instant;
71use smoltcp::wire::{
72    EthernetAddress, EthernetFrame, EthernetProtocol, HardwareAddress, IpAddress, IpCidr,
73    IpListenEndpoint, IpProtocol, IpVersion, Ipv4Packet, Ipv6Packet, TcpPacket, UdpPacket,
74};
75use std::io::{Read, Write};
76use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket as HostUdpSocket};
77use std::sync::atomic::Ordering;
78use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, TrySendError};
79use std::sync::Arc;
80use std::thread::{self, JoinHandle};
81use std::time::{Duration, Instant as StdInstant};
82
83const DNS_SOCKET_PORT: u16 = 53;
84const DNS_PACKET_SLOTS: usize = 8;
85const DNS_BUFFER_BYTES: usize = 2048;
86// DNS-over-TCP to the gateway. Real resolvers (and resolv.conf clients) fall
87// back to TCP for truncated answers and EDNS, so the gateway filter must serve
88// TCP/53 in addition to UDP/53. A small pool of listening sockets handles
89// concurrent queries; DNS/TCP is rare and short-lived (one query per
90// connection), so a few suffice.
91const DNS_TCP_LISTENERS: usize = 4;
92const DNS_TCP_RX_BYTES: usize = 4096;
93const DNS_TCP_TX_BYTES: usize = 8192;
94// A length-prefixed DNS message is bounded by a 16-bit length, but the gateway
95// only needs to handle ordinary queries/responses; cap to keep buffers small
96// and reject a guest that sends a bogus oversized prefix.
97const DNS_TCP_MAX_MSG: usize = 4096;
98const DEFAULT_IDLE_TIMEOUT_MS: i32 = 100;
99/// Packet slots per ICMP raw socket buffer (per direction).
100const ICMP_PACKET_SLOTS: usize = 16;
101/// Payload bytes per ICMP raw socket buffer (per direction).
102const ICMP_BUFFER_BYTES: usize = 32 * 1024;
103
104/// Resolved network parameters for one guest NIC.
105///
106/// These are the host-side parameters for the virtual link. Note that the
107/// smoltcp interface is configured with the *gateway* MAC/IP, because the host
108/// runtime is acting as the guest-visible gateway endpoint.
109#[derive(Debug, Clone, Copy)]
110pub struct VirtioPollConfig {
111    /// Host-side gateway MAC visible to the guest.
112    pub gateway_mac: [u8; 6],
113    /// Guest MAC address.
114    pub guest_mac: [u8; 6],
115    /// Gateway IPv4 address.
116    pub gateway_ipv4: Ipv4Addr,
117    /// Guest IPv4 address.
118    pub guest_ipv4: Ipv4Addr,
119    /// Gateway IPv6 (ULA) address.
120    pub gateway_ipv6: Ipv6Addr,
121    /// Guest IPv6 (ULA) address.
122    pub guest_ipv6: Ipv6Addr,
123    /// IPv6 prefix length for the virtual link.
124    pub prefix_len6: u8,
125    /// Upstream resolver the gateway forwards guest DNS queries to.
126    pub upstream_dns: Ipv4Addr,
127    /// IP-level MTU.
128    pub mtu: usize,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132enum FrameAction {
133    TcpSyn {
134        source: SocketAddr,
135        destination: SocketAddr,
136    },
137    DnsQuery,
138    /// Non-DNS guest UDP, relayed to a host socket (see `udp_relay`).
139    UdpFlow {
140        destination: SocketAddr,
141    },
142    Passthrough,
143}
144
145/// Start the dedicated smoltcp poll thread for the virtio-net backend.
146///
147/// This creates one long-lived poll loop thread per guest NIC. That thread owns
148/// the smoltcp `Interface`, its socket set, and the TCP relay table.
149///
150/// Ownership boundary:
151/// - this thread owns all smoltcp state
152/// - relay threads never touch smoltcp sockets directly
153/// - frame bridge threads never parse protocols beyond raw Ethernet framing
154pub fn start_network_stack(
155    queues: Arc<NetworkFrameQueues>,
156    config: VirtioPollConfig,
157    tcp_receiver: Option<Receiver<AcceptedTcpConnection>>,
158    egress: EgressPolicy,
159) -> std::io::Result<JoinHandle<()>> {
160    virtio_net_log!(
161        "virtio-net: spawning poll thread guest_ip={} gateway_ip={} mtu={}",
162        config.guest_ipv4,
163        config.gateway_ipv4,
164        config.mtu
165    );
166    thread::Builder::new()
167        .name("smolvm-net-poll".into())
168        .spawn(move || run_network_stack(queues, config, tcp_receiver, egress))
169}
170
171fn run_network_stack(
172    queues: Arc<NetworkFrameQueues>,
173    config: VirtioPollConfig,
174    mut tcp_receiver: Option<Receiver<AcceptedTcpConnection>>,
175    egress: EgressPolicy,
176) {
177    // Poll loop overview:
178    //
179    // 1. Drain staged guest Ethernet frames from the guest_to_host queue.
180    // 2. Pre-classify them so we can create relay/socket state before smoltcp
181    //    consumes the frame.
182    // 3. Poll smoltcp ingress/egress.
183    // 4. Forward DNS and relay TCP payloads.
184    // 5. Sleep in poll(2) on wake pipes until guest frames, relay activity, or
185    //    timers require more work.
186    //
187    // A useful mental model is:
188    //
189    //   queue input -> classify -> smoltcp -> protocol handling -> queue output
190    virtio_net_log!(
191        "virtio-net: poll loop started guest_ip={} gateway_ip={}",
192        config.guest_ipv4,
193        config.gateway_ipv4
194    );
195    let clock = StdInstant::now();
196    let mut device = VirtioNetworkDevice::new(queues.clone(), config.mtu);
197    let mut interface = create_interface(&mut device, &config);
198    let mut sockets = SocketSet::new(vec![]);
199    let dns_socket_handle = add_dns_socket(&mut sockets);
200    let dns_tcp_handles = add_dns_tcp_sockets(&mut sockets);
201    let mut dns_tcp_conns: Vec<DnsTcpConn> = (0..dns_tcp_handles.len())
202        .map(|_| DnsTcpConn::default())
203        .collect();
204    let (icmp4_handle, icmp6_handle) = add_icmp_raw_sockets(&mut sockets);
205    // Gateway addresses answer their own pings locally; everything else is
206    // relayed out to real host ICMP sockets.
207    let gateway_addrs = [
208        IpAddr::V4(config.gateway_ipv4),
209        IpAddr::V6(config.gateway_ipv6),
210        IpAddr::V6(link_local_from_mac(config.gateway_mac)),
211    ];
212    let relay_wake = Arc::new(queues.relay_wake.clone());
213    let mut relays = TcpRelayTable::new(None, egress.clone());
214    let mut udp_sockets = udp_relay::UdpSocketTable::new();
215    let udp_channels = {
216        let shutdown_queues = queues.clone();
217        udp_relay::start_udp_relay(
218            relay_wake.clone(),
219            Arc::new(move || shutdown_queues.is_shutting_down()),
220        )
221    };
222    let icmp_channels = {
223        let shutdown_queues = queues.clone();
224        icmp_relay::start_icmp_relay(
225            relay_wake.clone(),
226            Arc::new(move || shutdown_queues.is_shutting_down()),
227        )
228    };
229
230    // The smoltcp loop is driven by poller wakeups rather than busy spinning.
231    // guest_wake  -> new guest frame or shutdown
232    // relay_wake  -> host TCP relay thread produced data or shutdown
233    //
234    // Both wakes share a single underlying poller (see `NetworkFrameQueues`), so
235    // the loop blocks on that one poller and either side unblocks it. The loop
236    // re-runs its whole pipeline on every wakeup, so it does not need to know
237    // which wake fired.
238    let poller = queues.guest_wake.poller().clone();
239    let mut events = polling::Events::new();
240
241    loop {
242        if queues.is_shutting_down() {
243            return;
244        }
245        let now = smoltcp_now(clock);
246
247        while let Some(frame) = device.stage_next_frame() {
248            // We inspect the frame before giving it to smoltcp because certain
249            // flows need side effects first:
250            // - TCP SYN: pre-create a matching smoltcp socket + relay entry
251            // - DNS UDP: allow through for gateway-side forwarding
252            // - other UDP: pre-create the destination-keyed relay socket
253            match classify_guest_frame(frame, &gateway_addrs) {
254                FrameAction::TcpSyn {
255                    source,
256                    destination,
257                } => {
258                    virtio_net_log!(
259                        "virtio-net: guest TCP SYN source={} destination={}",
260                        source,
261                        destination
262                    );
263                    if !relays.has_socket_for(&source, &destination) {
264                        relays.create_tcp_socket(source, destination, &mut sockets);
265                    }
266                    if matches!(
267                        interface.poll_ingress_single(now, &mut device, &mut sockets),
268                        PollIngressSingleResult::None
269                    ) {
270                        device.drop_staged_frame();
271                    }
272                }
273                FrameAction::DnsQuery | FrameAction::Passthrough => {
274                    if matches!(
275                        interface.poll_ingress_single(now, &mut device, &mut sockets),
276                        PollIngressSingleResult::None
277                    ) {
278                        device.drop_staged_frame();
279                    }
280                }
281                FrameAction::UdpFlow { destination } => {
282                    // Same egress policy as TCP; a denied destination's datagram
283                    // is silently dropped (a guest sees a normal UDP black hole).
284                    if udp_relay::should_relay_udp(destination, &egress)
285                        && udp_sockets.ensure_socket(destination, &mut sockets)
286                    {
287                        if matches!(
288                            interface.poll_ingress_single(now, &mut device, &mut sockets),
289                            PollIngressSingleResult::None
290                        ) {
291                            device.drop_staged_frame();
292                        }
293                    } else {
294                        device.drop_staged_frame();
295                    }
296                }
297            }
298        }
299
300        relay_accepted_tcp_connection(
301            &mut tcp_receiver,
302            &mut relays,
303            &mut interface,
304            &mut sockets,
305            config.gateway_ipv4,
306            config.guest_ipv4,
307        );
308
309        // First egress pass: let smoltcp emit any packets caused by the most
310        // recent ingress work before we service higher-level relays.
311        flush_interface_egress(&mut interface, &mut device, &mut sockets, now);
312        interface.poll_maintenance(now);
313        wake_guest_if_needed(&queues, &device);
314
315        // Move payloads between established smoltcp TCP sockets and host relay
316        // threads, and service the DNS gateway socket.
317        relays.relay_data(&mut sockets);
318        process_dns_queries(
319            dns_socket_handle,
320            &mut sockets,
321            &egress,
322            config.upstream_dns,
323        );
324        process_dns_tcp(
325            &dns_tcp_handles,
326            &mut dns_tcp_conns,
327            &mut sockets,
328            &egress,
329            config.upstream_dns,
330        );
331
332        // General UDP: forward staged guest datagrams to the relay thread,
333        // deliver any replies it produced, and expire idle destination sockets.
334        if udp_sockets.drain_to_relay(&mut sockets, &udp_channels.to_relay) {
335            udp_channels.relay_thread_wake.wake();
336        }
337        udp_sockets.deliver_replies(&mut sockets, &udp_channels.from_relay);
338        udp_sockets.expire_idle(&mut sockets);
339
340        // ICMP echo: the raw sockets captured any guest echo requests during
341        // ingress above. Forward external pings to the relay (answering gateway
342        // pings locally), then send back any replies it produced.
343        let mut woke_icmp = false;
344        woke_icmp |= drain_icmp_echo(
345            &mut sockets,
346            icmp4_handle,
347            false,
348            &egress,
349            &gateway_addrs,
350            &icmp_channels.to_relay,
351        );
352        woke_icmp |= drain_icmp_echo(
353            &mut sockets,
354            icmp6_handle,
355            true,
356            &egress,
357            &gateway_addrs,
358            &icmp_channels.to_relay,
359        );
360        if woke_icmp {
361            icmp_channels.relay_thread_wake.wake();
362        }
363        deliver_icmp_replies(
364            &mut sockets,
365            icmp4_handle,
366            icmp6_handle,
367            &icmp_channels.from_relay,
368        );
369
370        // Once the guest-side TCP handshake is established inside smoltcp, we
371        // can spawn the corresponding host relay thread.
372        for connection in relays.take_new_connections(&mut sockets) {
373            spawn_tcp_relay(
374                connection.destination,
375                connection.relay_target,
376                connection.from_smoltcp,
377                connection.to_smoltcp,
378                relay_wake.clone(),
379                connection.exit_state,
380            );
381        }
382
383        relays.cleanup_closed(&mut sockets);
384
385        // Second egress pass: DNS responses or relay data may have queued more
386        // packets for the guest.
387        flush_interface_egress(&mut interface, &mut device, &mut sockets, now);
388        wake_guest_if_needed(&queues, &device);
389
390        let timeout = interface
391            .poll_delay(now, &sockets)
392            .map(|duration| Duration::from_millis(duration.total_millis().min(u32::MAX as u64)));
393        let timeout = match timeout {
394            Some(timeout) => Some(timeout),
395            None => Some(Duration::from_millis(DEFAULT_IDLE_TIMEOUT_MS as u64)),
396        };
397
398        // Block until either wake notifies the shared poller or the timeout
399        // elapses. The wakes are notify-only, so no events are reported; the
400        // loop re-runs unconditionally on the next iteration.
401        events.clear();
402        let _ = poller.wait(&mut events, timeout);
403    }
404}
405
406fn create_interface(device: &mut VirtioNetworkDevice, config: &VirtioPollConfig) -> Interface {
407    // This interface models the host-side gateway endpoint, not the guest NIC.
408    //
409    // Equivalent conceptual state:
410    //   MAC: config.gateway_mac
411    //   IP : config.gateway_ipv4/30
412    //        config.gateway_ipv6/64 (ULA) + fe80 link-local
413    //
414    // The guest IP exists as a peer on the same virtual link; it is not an
415    // address owned by this interface.
416    let mut interface = Interface::new(
417        Config::new(HardwareAddress::Ethernet(EthernetAddress(
418            config.gateway_mac,
419        ))),
420        device,
421        Instant::ZERO,
422    );
423    interface.update_ip_addrs(|addresses| {
424        addresses
425            .push(IpCidr::new(IpAddress::Ipv4(config.gateway_ipv4), 30))
426            .expect("failed to add gateway IPv4 address");
427        addresses
428            .push(IpCidr::new(
429                IpAddress::Ipv6(config.gateway_ipv6),
430                config.prefix_len6,
431            ))
432            .expect("failed to add gateway IPv6 address");
433        // RFC-clean NDP wants a link-local peer on the segment; derive the
434        // standard EUI-64 link-local from the gateway MAC so the guest kernel
435        // can talk NDP to fe80::… as well as to the ULA.
436        addresses
437            .push(IpCidr::new(
438                IpAddress::Ipv6(link_local_from_mac(config.gateway_mac)),
439                64,
440            ))
441            .expect("failed to add gateway IPv6 link-local address");
442    });
443    // The interface acts as the gateway and may need to answer packets for
444    // destinations other than its directly assigned IP, so the route table and
445    // "any IP" mode are opened up accordingly.
446    interface
447        .routes_mut()
448        .add_default_ipv4_route(config.gateway_ipv4)
449        .expect("failed to add default IPv4 route");
450    interface
451        .routes_mut()
452        .add_default_ipv6_route(config.gateway_ipv6)
453        .expect("failed to add default IPv6 route");
454    interface.set_any_ip(true);
455    interface
456}
457
458/// Derive the EUI-64 IPv6 link-local address for a MAC (RFC 4291 appendix A):
459/// flip the universal/local bit, insert `ff:fe` in the middle.
460fn link_local_from_mac(mac: [u8; 6]) -> Ipv6Addr {
461    Ipv6Addr::new(
462        0xfe80,
463        0,
464        0,
465        0,
466        u16::from_be_bytes([mac[0] ^ 0x02, mac[1]]),
467        u16::from_be_bytes([mac[2], 0xff]),
468        u16::from_be_bytes([0xfe, mac[3]]),
469        u16::from_be_bytes([mac[4], mac[5]]),
470    )
471}
472
473/// add_dns_socket is adding an UDP socket inside smoltcp, so that the guest DNS packet will
474/// hit this socket first. It is then proxied to the resolver. Note that this will not cause
475/// a host side :53 collesion, because the smoltcp Interface, SocketSet is per VM, and the
476/// gateway:53 is for that set of Interface and SocketSet, it is not bind to a host-kernel UDP socket.
477///
478/// The bind is wildcard (port-only) on purpose: combined with `set_any_ip`, every
479/// guest UDP datagram to port 53 — whatever its destination address or family
480/// (the v4 gateway, the v6 gateway, or an external resolver IP) — lands on this
481/// socket and is answered from that same destination address. That transparently
482/// intercepts hardcoded external resolvers too, matching TSI's DNS handling.
483fn add_dns_socket(sockets: &mut SocketSet<'_>) -> SocketHandle {
484    let rx_meta = vec![PacketMetadata::EMPTY; DNS_PACKET_SLOTS];
485    let tx_meta = vec![PacketMetadata::EMPTY; DNS_PACKET_SLOTS];
486    let rx_buffer = PacketBuffer::new(rx_meta, vec![0u8; DNS_BUFFER_BYTES]);
487    let tx_buffer = PacketBuffer::new(tx_meta, vec![0u8; DNS_BUFFER_BYTES]);
488    let mut socket = UdpSocket::new(rx_buffer, tx_buffer);
489    socket
490        .bind(smoltcp::wire::IpListenEndpoint {
491            addr: None,
492            port: DNS_SOCKET_PORT,
493        })
494        .expect("failed to bind gateway DNS socket");
495    sockets.add(socket)
496}
497
498/// Add the two raw IP sockets that capture guest ICMP echo traffic.
499///
500/// A `raw::Socket` receives a copy of every matching IP packet *before* the
501/// interface's "is this addressed to me?" check, so these capture the guest's
502/// echo requests even though their destination is some external host. The same
503/// sockets carry the relayed echo *replies* back out, fully addressed (source =
504/// the pinged host), letting smoltcp own the Ethernet framing and ARP/NDP.
505fn add_icmp_raw_sockets(sockets: &mut SocketSet<'_>) -> (SocketHandle, SocketHandle) {
506    fn raw_socket(version: IpVersion, protocol: IpProtocol) -> RawSocket<'static> {
507        let rx = RawPacketBuffer::new(
508            vec![RawPacketMetadata::EMPTY; ICMP_PACKET_SLOTS],
509            vec![0u8; ICMP_BUFFER_BYTES],
510        );
511        let tx = RawPacketBuffer::new(
512            vec![RawPacketMetadata::EMPTY; ICMP_PACKET_SLOTS],
513            vec![0u8; ICMP_BUFFER_BYTES],
514        );
515        RawSocket::new(Some(version), Some(protocol), rx, tx)
516    }
517
518    let v4 = sockets.add(raw_socket(IpVersion::Ipv4, IpProtocol::Icmp));
519    let v6 = sockets.add(raw_socket(IpVersion::Ipv6, IpProtocol::Icmpv6));
520    (v4, v6)
521}
522
523/// Drain guest echo requests captured on one ICMP raw socket. Gateway-destined
524/// pings are answered locally (the gateway *is* the source), external ones are
525/// forwarded to the relay thread subject to egress policy, and denied
526/// destinations are dropped. Returns true if anything was sent to the relay.
527fn drain_icmp_echo(
528    sockets: &mut SocketSet<'_>,
529    handle: SocketHandle,
530    is_ipv6: bool,
531    egress: &EgressPolicy,
532    gateway_addrs: &[IpAddr],
533    to_relay: &SyncSender<icmp_relay::IcmpEcho>,
534) -> bool {
535    // Phase 1: drain received requests into owned values so the socket can be
536    // re-borrowed below to emit local gateway replies.
537    let mut echoes = Vec::new();
538    {
539        let socket = sockets.get_mut::<RawSocket>(handle);
540        while socket.can_recv() {
541            let Ok(packet) = socket.recv() else {
542                break;
543            };
544            let parsed = if is_ipv6 {
545                icmp_relay::parse_guest_echo_v6(packet)
546            } else {
547                icmp_relay::parse_guest_echo_v4(packet)
548            };
549            if let Some(echo) = parsed {
550                echoes.push(echo);
551            }
552        }
553    }
554
555    // Phase 2: route each echo.
556    let mut woke = false;
557    let mut local_replies = Vec::new();
558    for echo in echoes {
559        if gateway_addrs.contains(&echo.destination) {
560            local_replies.push(echo);
561        } else if icmp_relay::should_relay_icmp(echo.destination, egress) {
562            match to_relay.try_send(echo) {
563                Ok(()) => woke = true,
564                Err(TrySendError::Full(_)) => {
565                    virtio_net_log!("virtio-net: dropping guest ICMP echo (relay queue full)");
566                }
567                Err(TrySendError::Disconnected(_)) => return woke,
568            }
569        }
570        // else: egress policy denies the destination — silent black hole.
571    }
572
573    // Phase 3: answer gateway pings straight back out the raw socket.
574    if !local_replies.is_empty() {
575        let socket = sockets.get_mut::<RawSocket>(handle);
576        for reply in local_replies {
577            let frame = if is_ipv6 {
578                icmp_relay::build_echo_reply_v6(&reply)
579            } else {
580                icmp_relay::build_echo_reply_v4(&reply)
581            };
582            if let Some(frame) = frame {
583                let _ = socket.send_slice(&frame);
584            }
585        }
586    }
587    woke
588}
589
590/// Deliver echo replies produced by the relay thread, sending each as a
591/// fully-addressed IP packet (source = the pinged host) out the matching raw
592/// socket so smoltcp frames it back to the guest.
593fn deliver_icmp_replies(
594    sockets: &mut SocketSet<'_>,
595    icmp4_handle: SocketHandle,
596    icmp6_handle: SocketHandle,
597    from_relay: &Receiver<icmp_relay::IcmpEcho>,
598) {
599    while let Ok(reply) = from_relay.try_recv() {
600        let (handle, frame) = match reply.guest {
601            IpAddr::V4(_) => (icmp4_handle, icmp_relay::build_echo_reply_v4(&reply)),
602            IpAddr::V6(_) => (icmp6_handle, icmp_relay::build_echo_reply_v6(&reply)),
603        };
604        let Some(frame) = frame else {
605            continue;
606        };
607        let socket = sockets.get_mut::<RawSocket>(handle);
608        if socket.send_slice(&frame).is_err() {
609            virtio_net_log!(
610                "virtio-net: dropping ICMP reply to {} (raw socket buffer full)",
611                reply.guest
612            );
613        }
614    }
615}
616
617/// Receive the accepted TCP connection from the tcp_channel, and then relay it to
618/// the TcpRelayTable where the TCP network packets will be relayed to the guest.
619fn relay_accepted_tcp_connection(
620    tcp_receiver: &mut Option<Receiver<AcceptedTcpConnection>>,
621    relays: &mut TcpRelayTable,
622    interface: &mut Interface,
623    sockets: &mut SocketSet<'_>,
624    gateway_ipv4: Ipv4Addr,
625    guest_ipv4: Ipv4Addr,
626) {
627    // Published-port model:
628    //
629    // host client -> accepted host TcpStream
630    //             -> create guest-facing smoltcp socket from gateway_ip:ephemeral
631    //             -> guest sees a normal inbound TCP connection to guest_port
632    //             -> once Established, the relay thread bridges payloads
633    //
634    // The guest does not see the original host peer address here. This path is
635    // effectively a small userspace TCP proxy/NAT at the gateway boundary.
636    let mut disconnected = false;
637
638    if let Some(receiver) = tcp_receiver.as_mut() {
639        loop {
640            match receiver.try_recv() {
641                Ok(connection) => {
642                    let guest_destination =
643                        SocketAddr::new(std::net::IpAddr::V4(guest_ipv4), connection.guest_port);
644                    virtio_net_log!(
645                        "virtio-net: accepted published TCP connection peer={} host_port={} guest_destination={}",
646                        connection.peer_addr,
647                        connection.host_port,
648                        guest_destination
649                    );
650                    if !relays.create_published_socket(
651                        interface,
652                        gateway_ipv4,
653                        guest_destination,
654                        connection.stream,
655                        sockets,
656                    ) {
657                        tracing::warn!(
658                            host_port = connection.host_port,
659                            guest_port = connection.guest_port,
660                            peer_addr = %connection.peer_addr,
661                            "dropping published TCP connection because the guest relay path could not be created"
662                        );
663                    }
664                }
665                Err(TryRecvError::Empty) => break,
666                Err(TryRecvError::Disconnected) => {
667                    disconnected = true;
668                    break;
669                }
670            }
671        }
672    }
673
674    if disconnected {
675        *tcp_receiver = None;
676    }
677}
678
679fn process_dns_queries(
680    dns_socket_handle: SocketHandle,
681    sockets: &mut SocketSet<'_>,
682    egress: &EgressPolicy,
683    upstream_dns: Ipv4Addr,
684) {
685    // Phase 1 DNS model:
686    // guest UDP/53 -> smoltcp gateway socket -> host UDP socket -> upstream DNS
687    //               <-               response bytes               <-
688    let socket = sockets.get_mut::<UdpSocket>(dns_socket_handle);
689    while socket.can_recv() {
690        let (query, metadata) = match socket.recv() {
691            Ok((q, m)) => (q.to_vec(), m),
692            Err(_) => break,
693        };
694        virtio_net_log!(
695            "virtio-net: forwarding guest DNS query guest={} local_address={:?} query_len={} upstream_dns={}",
696            metadata.endpoint,
697            metadata.local_address,
698            query.len(),
699            upstream_dns
700        );
701        // Apply the same allow-host filter as TCP (see `filtered_dns_response`),
702        // forwarding allowed queries over the host's UDP stack.
703        let response =
704            match filtered_dns_response(&query, egress, |q| forward_dns_query(upstream_dns, q)) {
705                Some(response) => response,
706                None => continue,
707            };
708        virtio_net_log!(
709            "virtio-net: forwarded DNS response back to guest guest={} response_len={}",
710            metadata.endpoint,
711            response.len()
712        );
713
714        let response_meta = UdpMetadata {
715            endpoint: metadata.endpoint,
716            local_address: metadata.local_address,
717            meta: Default::default(),
718        };
719        let _ = socket.send_slice(&response, response_meta);
720    }
721}
722
723fn forward_dns_query(upstream_dns: Ipv4Addr, query: &[u8]) -> std::io::Result<Vec<u8>> {
724    // This is intentionally a plain host UDP exchange rather than a smoltcp
725    // socket-to-socket relay. Once the guest packet reaches the gateway, the
726    // simplest MVP path is to proxy it with the host kernel's UDP stack.
727    //
728    // Rough shell equivalent:
729    //   send raw DNS message to `<upstream_dns>:53`
730    //   wait up to 2 seconds for one reply
731    let socket = HostUdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?;
732    socket.set_read_timeout(Some(Duration::from_secs(2)))?;
733    let local_addr = socket.local_addr()?;
734    virtio_net_log!(
735        "virtio-net: sending DNS query to upstream resolver local_addr={} upstream_dns={} query_len={}",
736        local_addr,
737        upstream_dns,
738        query.len()
739    );
740    socket.send_to(query, (upstream_dns, DNS_SOCKET_PORT))?;
741
742    let mut buffer = vec![0u8; DNS_BUFFER_BYTES];
743    let (bytes_read, _) = socket.recv_from(&mut buffer)?;
744    buffer.truncate(bytes_read);
745    virtio_net_log!(
746        "virtio-net: received DNS response from upstream resolver upstream_dns={} response_len={}",
747        upstream_dns,
748        buffer.len()
749    );
750    Ok(buffer)
751}
752
753/// Apply the allow-host DNS policy to a single query and produce the response
754/// bytes to return to the guest, or `None` to drop (a forwarding error on an
755/// allowed query — the guest sees a normal DNS timeout).
756///
757/// Shared by the UDP and TCP gateway paths so both enforce identical policy:
758/// only allow-listed names are forwarded (`forward`); others get NXDOMAIN; an
759/// unparseable query gets SERVFAIL. Answer A/AAAA records of allowed queries are
760/// learned as temporary egress IPs so the follow-up connection passes the
761/// filter. Mirrors libkrun's TSI DNS filter.
762fn filtered_dns_response(
763    query: &[u8],
764    egress: &EgressPolicy,
765    forward: impl FnOnce(&[u8]) -> std::io::Result<Vec<u8>>,
766) -> Option<Vec<u8>> {
767    if !egress.dns_filter_active() {
768        return match forward(query) {
769            Ok(response) => Some(response),
770            Err(err) => {
771                virtio_net_log!("virtio-net: host DNS forwarding failed error={}", err);
772                None
773            }
774        };
775    }
776    match dns::question_name(query) {
777        Some(name) if egress.hostname_allowed(&name) => match forward(query) {
778            Ok(response) => {
779                egress.learn_ip_records(&dns::answer_ip_records(&response));
780                Some(response)
781            }
782            Err(err) => {
783                virtio_net_log!("virtio-net: host DNS forwarding failed error={}", err);
784                None
785            }
786        },
787        Some(name) => {
788            virtio_net_log!(
789                "virtio-net: blocking DNS query by allow-host policy name={}",
790                name
791            );
792            Some(dns::error_response(query, dns::DNS_RCODE_NXDOMAIN))
793        }
794        None => Some(dns::error_response(query, dns::DNS_RCODE_SERVFAIL)),
795    }
796}
797
798/// Forward one DNS query to the upstream resolver over TCP (length-prefixed, per
799/// RFC 1035 §4.2.2) and return the raw response message. Synchronous host TCP
800/// exchange with a short timeout, matching the UDP path's MVP shape.
801fn forward_dns_query_tcp(upstream_dns: Ipv4Addr, query: &[u8]) -> std::io::Result<Vec<u8>> {
802    use std::io::{Error, ErrorKind};
803    let len = u16::try_from(query.len())
804        .map_err(|_| Error::new(ErrorKind::InvalidInput, "DNS query too large for TCP"))?;
805    let mut stream = std::net::TcpStream::connect_timeout(
806        &SocketAddr::new(IpAddr::V4(upstream_dns), DNS_SOCKET_PORT),
807        Duration::from_secs(2),
808    )?;
809    stream.set_read_timeout(Some(Duration::from_secs(2)))?;
810    stream.set_write_timeout(Some(Duration::from_secs(2)))?;
811    stream.write_all(&len.to_be_bytes())?;
812    stream.write_all(query)?;
813    stream.flush()?;
814
815    let mut len_buf = [0u8; 2];
816    stream.read_exact(&mut len_buf)?;
817    let resp_len = u16::from_be_bytes(len_buf) as usize;
818    if resp_len == 0 || resp_len > DNS_TCP_MAX_MSG {
819        return Err(Error::new(
820            ErrorKind::InvalidData,
821            "upstream DNS/TCP response length out of range",
822        ));
823    }
824    let mut response = vec![0u8; resp_len];
825    stream.read_exact(&mut response)?;
826    Ok(response)
827}
828
829/// Create the pool of smoltcp TCP listening sockets bound to the gateway's
830/// port 53. Each accepts one DNS-over-TCP connection at a time and is re-armed
831/// by [`process_dns_tcp`] after the connection closes.
832fn add_dns_tcp_sockets(sockets: &mut SocketSet<'_>) -> Vec<SocketHandle> {
833    (0..DNS_TCP_LISTENERS)
834        .map(|_| {
835            let rx_buffer = tcp::SocketBuffer::new(vec![0u8; DNS_TCP_RX_BYTES]);
836            let tx_buffer = tcp::SocketBuffer::new(vec![0u8; DNS_TCP_TX_BYTES]);
837            let mut socket = tcp::Socket::new(rx_buffer, tx_buffer);
838            socket
839                .listen(IpListenEndpoint {
840                    addr: None,
841                    port: DNS_SOCKET_PORT,
842                })
843                .expect("failed to listen on gateway DNS TCP socket");
844            sockets.add(socket)
845        })
846        .collect()
847}
848
849/// Per-listener state for an in-flight DNS-over-TCP connection: the
850/// accumulating length-prefixed query, and the framed response we still owe the
851/// guest.
852#[derive(Default)]
853struct DnsTcpConn {
854    /// Guest -> gateway bytes received so far (2-byte length prefix + query).
855    rx: Vec<u8>,
856    /// Framed response (2-byte length prefix + message) to send to the guest.
857    tx: Vec<u8>,
858    /// Bytes of `tx` already written to the socket.
859    tx_sent: usize,
860    /// The query has been answered (or rejected); drain `tx` then close.
861    done: bool,
862}
863
864/// Service the DNS-over-TCP listening sockets: accept a connection, read the
865/// length-prefixed query, apply the allow-host filter, forward allowed queries
866/// upstream over TCP, write the length-prefixed response, and close. Closed
867/// sockets are re-armed to listen again.
868fn process_dns_tcp(
869    handles: &[SocketHandle],
870    conns: &mut [DnsTcpConn],
871    sockets: &mut SocketSet<'_>,
872    egress: &EgressPolicy,
873    upstream_dns: Ipv4Addr,
874) {
875    for (handle, conn) in handles.iter().zip(conns.iter_mut()) {
876        let socket = sockets.get_mut::<tcp::Socket>(*handle);
877
878        // A closed/finished socket: reset state and re-arm to accept the next
879        // connection. `listen` only succeeds from the CLOSED state; if the
880        // socket is still draining (e.g. TIME-WAIT) the error is ignored and the
881        // next poll retries.
882        if !socket.is_open() {
883            if !conn.rx.is_empty() || !conn.tx.is_empty() || conn.done {
884                *conn = DnsTcpConn::default();
885            }
886            let _ = socket.listen(IpListenEndpoint {
887                addr: None,
888                port: DNS_SOCKET_PORT,
889            });
890            continue;
891        }
892
893        // Already answered: flush whatever response remains, then close.
894        if conn.done {
895            drain_dns_tcp_tx(socket, conn);
896            continue;
897        }
898
899        // Accumulate the length-prefixed query.
900        while socket.can_recv() {
901            let appended = socket.recv(|data| (data.len(), data.to_vec()));
902            match appended {
903                Ok(bytes) if !bytes.is_empty() => conn.rx.extend_from_slice(&bytes),
904                _ => break,
905            }
906        }
907
908        // Reject a guest that floods without ever completing a message.
909        if conn.rx.len() > DNS_TCP_MAX_MSG + 2 {
910            conn.done = true;
911            socket.close();
912            continue;
913        }
914
915        if conn.rx.len() >= 2 {
916            let msg_len = u16::from_be_bytes([conn.rx[0], conn.rx[1]]) as usize;
917            if msg_len == 0 || msg_len > DNS_TCP_MAX_MSG {
918                conn.done = true;
919                socket.close();
920                continue;
921            }
922            if conn.rx.len() >= 2 + msg_len {
923                let query = conn.rx[2..2 + msg_len].to_vec();
924                virtio_net_log!(
925                    "virtio-net: DNS/TCP query query_len={} upstream_dns={}",
926                    query.len(),
927                    upstream_dns
928                );
929                if let Some(response) = filtered_dns_response(&query, egress, |q| {
930                    forward_dns_query_tcp(upstream_dns, q)
931                }) {
932                    if let Ok(resp_len) = u16::try_from(response.len()) {
933                        conn.tx.extend_from_slice(&resp_len.to_be_bytes());
934                        conn.tx.extend_from_slice(&response);
935                    }
936                }
937                conn.done = true;
938                drain_dns_tcp_tx(socket, conn);
939            }
940        }
941    }
942}
943
944/// Write as much of the pending framed response as the socket will accept; once
945/// fully sent, close the connection (the guest reads the answer then sees EOF).
946fn drain_dns_tcp_tx(socket: &mut tcp::Socket<'_>, conn: &mut DnsTcpConn) {
947    while conn.tx_sent < conn.tx.len() && socket.can_send() {
948        match socket.send_slice(&conn.tx[conn.tx_sent..]) {
949            Ok(n) if n > 0 => conn.tx_sent += n,
950            _ => break,
951        }
952    }
953    if conn.tx_sent >= conn.tx.len() {
954        socket.close();
955    }
956}
957
958fn flush_interface_egress(
959    interface: &mut Interface,
960    device: &mut VirtioNetworkDevice,
961    sockets: &mut SocketSet<'_>,
962    now: Instant,
963) {
964    // smoltcp may have multiple pending egress packets after a single ingress
965    // event or timeout. Keep polling until the interface reports there is no
966    // more immediate work.
967    loop {
968        let result = interface.poll_egress(now, device, sockets);
969        if matches!(result, PollResult::None) {
970            break;
971        }
972    }
973}
974
975fn wake_guest_if_needed(queues: &NetworkFrameQueues, device: &VirtioNetworkDevice) {
976    // The device records only that "some frame was emitted". We convert that
977    // sticky bit into one wake for the writer thread and let the writer drain
978    // the entire host_to_guest queue.
979    if device.frames_emitted.swap(false, Ordering::Relaxed) {
980        queues.host_wake.wake();
981    }
982}
983
984fn smoltcp_now(clock: StdInstant) -> Instant {
985    let elapsed = clock.elapsed();
986    Instant::from_millis(elapsed.as_millis() as i64)
987}
988
989fn classify_guest_frame(frame: &[u8], gateway_addrs: &[IpAddr]) -> FrameAction {
990    let ethernet = match EthernetFrame::new_checked(frame) {
991        Ok(frame) => frame,
992        Err(_) => return FrameAction::Passthrough,
993    };
994
995    // Extract (src, dst, transport protocol, transport payload) from either IP
996    // family. Anything that isn't plain IPv4/IPv6 — ARP, and IPv6 packets with
997    // extension headers (which guest TCP/UDP traffic doesn't use) — passes
998    // through to smoltcp untouched; that also covers ICMPv6/NDP.
999    let (src_ip, dst_ip, protocol, transport): (IpAddr, IpAddr, _, _) = match ethernet.ethertype() {
1000        EthernetProtocol::Ipv4 => {
1001            let ipv4 = match Ipv4Packet::new_checked(ethernet.payload()) {
1002                Ok(packet) => packet,
1003                Err(_) => return FrameAction::Passthrough,
1004            };
1005            (
1006                IpAddr::V4(ipv4.src_addr()),
1007                IpAddr::V4(ipv4.dst_addr()),
1008                ipv4.next_header(),
1009                ipv4.payload(),
1010            )
1011        }
1012        EthernetProtocol::Ipv6 => {
1013            let ipv6 = match Ipv6Packet::new_checked(ethernet.payload()) {
1014                Ok(packet) => packet,
1015                Err(_) => return FrameAction::Passthrough,
1016            };
1017            (
1018                IpAddr::V6(ipv6.src_addr()),
1019                IpAddr::V6(ipv6.dst_addr()),
1020                ipv6.next_header(),
1021                ipv6.payload(),
1022            )
1023        }
1024        _ => return FrameAction::Passthrough,
1025    };
1026
1027    match protocol {
1028        smoltcp::wire::IpProtocol::Tcp => {
1029            let tcp = match TcpPacket::new_checked(transport) {
1030                Ok(packet) => packet,
1031                Err(_) => return FrameAction::Passthrough,
1032            };
1033
1034            if tcp.syn() && !tcp.ack() {
1035                // DNS-over-TCP to the gateway itself is intercepted by the local
1036                // listening sockets (process_dns_tcp), not relayed. TCP/53 to an
1037                // external resolver (an allow-listed IP) is left to the egress
1038                // relay so the policy still applies.
1039                if tcp.dst_port() == DNS_SOCKET_PORT && gateway_addrs.contains(&dst_ip) {
1040                    FrameAction::Passthrough
1041                } else {
1042                    FrameAction::TcpSyn {
1043                        source: SocketAddr::new(src_ip, tcp.src_port()),
1044                        destination: SocketAddr::new(dst_ip, tcp.dst_port()),
1045                    }
1046                }
1047            } else {
1048                FrameAction::Passthrough
1049            }
1050        }
1051        smoltcp::wire::IpProtocol::Udp => {
1052            let udp = match UdpPacket::new_checked(transport) {
1053                Ok(packet) => packet,
1054                Err(_) => return FrameAction::Passthrough,
1055            };
1056
1057            if udp.dst_port() == DNS_SOCKET_PORT {
1058                FrameAction::DnsQuery
1059            } else {
1060                FrameAction::UdpFlow {
1061                    destination: SocketAddr::new(dst_ip, udp.dst_port()),
1062                }
1063            }
1064        }
1065        _ => FrameAction::Passthrough,
1066    }
1067}
1068
1069/// Fuzz-only entrypoint for `classify_guest_frame`.
1070///
1071/// A malicious guest sends arbitrary ethernet frames over virtio-net, and the
1072/// host parses every one here — so this MUST NOT panic on any input. Gated
1073/// behind the `fuzzing` feature so it never ships in a normal build.
1074#[cfg(feature = "fuzzing")]
1075pub fn fuzz_classify_guest_frame(frame: &[u8]) {
1076    let _ = classify_guest_frame(frame, &[]);
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081    use super::*;
1082
1083    /// Minimal Ethernet(IPv4(TCP SYN)) frame with no payload. `new_checked`
1084    /// validates lengths/header fields (not checksums), so dummy checksums are
1085    /// fine for exercising `classify_guest_frame`.
1086    fn tcp_syn_frame(dst_ip: [u8; 4], dst_port: u16) -> Vec<u8> {
1087        let mut f = Vec::new();
1088        // Ethernet: dst MAC, src MAC, ethertype IPv4.
1089        f.extend_from_slice(&[0xff; 6]);
1090        f.extend_from_slice(&[0x02, 0, 0, 0, 0, 1]);
1091        f.extend_from_slice(&[0x08, 0x00]);
1092        // IPv4: v4/IHL5, DSCP, total_len=40, id, flags/frag, ttl, proto=TCP, csum, src, dst.
1093        f.extend_from_slice(&[0x45, 0x00, 0x00, 0x28, 0, 0, 0, 0, 0x40, 0x06, 0, 0]);
1094        f.extend_from_slice(&[10, 0, 0, 2]); // src ip
1095        f.extend_from_slice(&dst_ip);
1096        // TCP: src/dst port, seq, ack, data-offset(5)/flags(SYN), window, csum, urg.
1097        f.extend_from_slice(&54321u16.to_be_bytes());
1098        f.extend_from_slice(&dst_port.to_be_bytes());
1099        f.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0]); // seq + ack
1100        f.extend_from_slice(&[0x50, 0x02, 0xff, 0xff, 0, 0, 0, 0]); // offset/SYN/window/csum/urg
1101        f
1102    }
1103
1104    #[test]
1105    fn dns_tcp_to_gateway_is_intercepted_not_relayed() {
1106        let gw = IpAddr::V4(Ipv4Addr::new(100, 96, 0, 1));
1107        // TCP/53 to the gateway -> handled by the local DNS listeners (Passthrough).
1108        assert_eq!(
1109            classify_guest_frame(&tcp_syn_frame([100, 96, 0, 1], 53), &[gw]),
1110            FrameAction::Passthrough
1111        );
1112    }
1113
1114    #[test]
1115    fn dns_tcp_to_external_resolver_still_relayed() {
1116        let gw = IpAddr::V4(Ipv4Addr::new(100, 96, 0, 1));
1117        // TCP/53 to an external (allow-listed) resolver must go through the egress
1118        // relay, NOT be swallowed by the gateway DNS listeners.
1119        assert!(matches!(
1120            classify_guest_frame(&tcp_syn_frame([1, 1, 1, 1], 53), &[gw]),
1121            FrameAction::TcpSyn { .. }
1122        ));
1123    }
1124
1125    #[test]
1126    fn non_dns_tcp_to_gateway_still_relayed() {
1127        let gw = IpAddr::V4(Ipv4Addr::new(100, 96, 0, 1));
1128        // Only port 53 is intercepted; other gateway ports relay as usual.
1129        assert!(matches!(
1130            classify_guest_frame(&tcp_syn_frame([100, 96, 0, 1], 443), &[gw]),
1131            FrameAction::TcpSyn { .. }
1132        ));
1133    }
1134}