Skip to main content

smolvm_network/
lib.rs

1//! Host-side virtio-net runtime.
2//!
3//! Context
4//! =======
5//!
6//! This module is the host-side half of the new networking path:
7//!
8//! ```text
9//! guest app
10//!   -> guest kernel TCP/IP stack
11//!   -> virtio-net device
12//!   -> libkrun unix-stream bridge
13//!   -> smolvm FrameStreamBridge
14//!   -> shared frame queues
15//!   -> smoltcp gateway/runtime
16//!   -> host sockets / DNS forwarding / TCP relay
17//!   -> external network
18//! ```
19//!
20//! Main runtime components:
21//!
22//! ```text
23//! VirtioNetworkRuntime
24//! ├─ FrameStreamBridge
25//! │  ├─ reader thread
26//! │  └─ writer thread
27//! ├─ TcpPortListeners
28//! │  └─ one non-blocking accept loop per `-p HOST:GUEST`
29//! ├─ Arc<NetworkFrameQueues>
30//! │  ├─ guest_to_host
31//! │  ├─ host_to_guest
32//! │  ├─ guest_wake
33//! │  ├─ host_wake
34//! │  └─ relay_wake
35//! └─ smolvm-net-poll thread
36//!    ├─ VirtioNetworkDevice
37//!    ├─ smoltcp Interface
38//!    ├─ SocketSet
39//!    └─ TcpRelayTable
40//! ```
41//!
42//! Component roles:
43//! - `FrameStreamBridge`: translates libkrun's Unix-stream frame protocol into
44//!   queue operations
45//! - `TcpPortListeners`: accepts host TCP connections for published ports
46//!   and hands them to the poll loop
47//! - `NetworkFrameQueues`: handoff boundary between threads
48//! - `VirtioNetworkDevice`: adapts those queues to smoltcp's `phy::Device`
49//! - poll thread: acts as the guest-visible gateway and protocol dispatcher
50//! - `TcpRelayTable`: maps guest TCP flows onto host-side relay threads
51//!
52//! This runtime is responsible for:
53//! - exchanging raw Ethernet frames with libkrun
54//! - presenting a gateway endpoint to the guest
55//! - handling DNS through a gateway UDP socket and host UDP forwarding
56//! - relaying guest TCP connections to host `TcpStream`s
57//! - accepting published host TCP ports and forwarding them into guest TCP
58//!   connections
59
60pub mod device;
61pub mod dns;
62pub mod dns_relay;
63pub mod egress;
64// The libkrun frame bridge speaks over an AF_UNIX stream socket, available on
65// both Unix and Windows (10 1809+), so the whole stack is cross-platform.
66pub mod frame_stream;
67pub mod icmp_relay;
68pub mod queues;
69pub mod stack;
70pub mod tcp_listeners;
71pub mod tcp_relay;
72pub mod udp_relay;
73
74pub use egress::EgressPolicy;
75
76use socket2::Socket;
77use std::fmt;
78use std::io;
79use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
80use std::thread::JoinHandle;
81use std::time::SystemTime;
82
83use frame_stream::{start_frame_stream_bridge, FrameStreamBridge};
84use queues::NetworkFrameQueues;
85use queues::DEFAULT_FRAME_QUEUE_CAPACITY;
86use stack::{start_network_stack, VirtioPollConfig};
87use tcp_listeners::create_tcp_channel;
88use tcp_listeners::TcpPortListeners;
89
90/// Default upstream DNS resolver used by the gateway runtime.
91pub const DEFAULT_DNS_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
92
93/// Host->guest published TCP port mapping serviced by the virtio gateway.
94///
95/// This stays crate-local so the launchers can translate CLI/data-layer port
96/// mappings into the gateway runtime without pulling the gateway logic back
97/// into the main `smolvm` crate.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct PortMapping {
100    /// Port bound on the host loopback interface.
101    pub host: u16,
102    /// Port exposed inside the guest.
103    pub guest: u16,
104}
105
106impl PortMapping {
107    /// Create a new published port mapping.
108    pub const fn new(host: u16, guest: u16) -> Self {
109        Self { host, guest }
110    }
111}
112
113/// Static guest network configuration for the virtio-net MVP.
114///
115/// This struct describes the two endpoints of the single virtual Ethernet link:
116/// - the guest NIC (`guest_*`)
117/// - the host-side gateway implemented by smolvm (`gateway_*`)
118///
119/// The link is dual-stack: a /30 IPv4 point-to-point pair and a /64 ULA IPv6
120/// pair (`fd53:4d00::/64` — `53:4d` = "SM", matching the MAC OUI scheme).
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct GuestNetworkConfig {
123    /// Guest IPv4 address.
124    pub guest_ip: Ipv4Addr,
125    /// Gateway IPv4 address.
126    pub gateway_ip: Ipv4Addr,
127    /// Prefix length.
128    pub prefix_len: u8,
129    /// Guest IPv6 (ULA) address.
130    pub guest_ip6: Ipv6Addr,
131    /// Gateway IPv6 (ULA) address.
132    pub gateway_ip6: Ipv6Addr,
133    /// IPv6 prefix length.
134    pub prefix_len6: u8,
135    /// Guest MAC address.
136    pub guest_mac: [u8; 6],
137    /// Gateway MAC address.
138    pub gateway_mac: [u8; 6],
139    /// DNS server address presented to the guest.
140    ///
141    /// For virtio-net this is the gateway's own address (`gateway_ip`): the
142    /// guest sends DNS to the gateway, which forwards upstream to
143    /// [`Self::upstream_dns`].
144    pub dns_server: Ipv4Addr,
145    /// Upstream resolver the gateway forwards guest DNS queries to.
146    ///
147    /// Defaults to [`DEFAULT_DNS_ADDR`]; the launcher overrides it when the
148    /// caller passes `--dns <ip>` so a VM on a network that blocks the default
149    /// resolver can still resolve names.
150    pub upstream_dns: Ipv4Addr,
151}
152
153impl GuestNetworkConfig {
154    /// Default Phase 1 guest network configuration.
155    pub const fn default() -> Self {
156        Self {
157            guest_ip: Ipv4Addr::new(100, 96, 0, 2),
158            gateway_ip: Ipv4Addr::new(100, 96, 0, 1),
159            prefix_len: 30,
160            guest_ip6: Ipv6Addr::new(0xfd53, 0x4d00, 0, 0, 0, 0, 0, 2),
161            gateway_ip6: Ipv6Addr::new(0xfd53, 0x4d00, 0, 0, 0, 0, 0, 1),
162            prefix_len6: 64,
163            guest_mac: [0x02, 0x53, 0x4d, 0x00, 0x00, 0x02],
164            gateway_mac: [0x02, 0x53, 0x4d, 0x00, 0x00, 0x01],
165            dns_server: Ipv4Addr::new(100, 96, 0, 1),
166            upstream_dns: match DEFAULT_DNS_ADDR {
167                IpAddr::V4(ip) => ip,
168                IpAddr::V6(_) => Ipv4Addr::new(1, 1, 1, 1),
169            },
170        }
171    }
172}
173
174fn format_network_log_line(timestamp: SystemTime, message: &str) -> String {
175    format!(
176        "[{}]: {}",
177        humantime::format_rfc3339_seconds(timestamp),
178        message
179    )
180}
181
182pub(crate) fn emit_network_log_line(message: fmt::Arguments<'_>) {
183    eprintln!(
184        "{}",
185        format_network_log_line(SystemTime::now(), &message.to_string())
186    );
187}
188
189macro_rules! virtio_net_log {
190    ($($arg:tt)*) => {
191        $crate::emit_network_log_line(format_args!($($arg)*))
192    };
193}
194
195pub(crate) use virtio_net_log;
196
197/// Running host-side virtio-net runtime for one guest NIC.
198///
199/// Ownership model:
200/// - one runtime instance corresponds to one guest virtio NIC
201/// - it owns the queue set shared by the worker threads
202/// - it owns the libkrun Unix-stream bridge threads
203/// - it owns the published-port listener threads
204/// - it owns the smoltcp poll thread
205///
206/// Dropping the runtime is the shutdown signal. `Drop` marks the shared queues
207/// as shutting down, wakes blocked workers, and joins the poll thread.
208pub struct VirtioNetworkRuntime {
209    queues: std::sync::Arc<NetworkFrameQueues>,
210    _frame_bridge: FrameStreamBridge,
211    published_ports: Option<TcpPortListeners>,
212    poll_handle: Option<JoinHandle<()>>,
213}
214
215/// Start the host-side virtio-net runtime for one guest NIC.
216///
217/// Inputs:
218/// - `host_fd`: the host-side Unix stream fd that libkrun will use for this
219///   guest NIC. The launcher eventually gets this from the libkrun
220///   `krun_add_net_unixstream()` setup path.
221/// - `guest_network`: the static guest/gateway addressing and MAC plan for this
222///   NIC.
223/// - `published_ports`: host->guest TCP port mappings that should be serviced
224///   directly by the virtio runtime instead of TSI.
225///
226/// High-level flow:
227///
228/// ```text
229/// start_virtio_network()
230///   -> create shared frame queues + wake pipes
231///   -> start frame reader/writer threads on the Unix stream
232///   -> start host TcpListeners for published ports
233///   -> start the smoltcp poll thread
234///   -> return a handle that owns the whole runtime
235/// ```
236///
237/// Expanded startup picture:
238///
239/// ```text
240/// host_fd from libkrun
241///   -> FrameStreamBridge(host_fd)
242///      -> reader thread
243///      -> writer thread
244///   -> TcpPortListeners
245///      -> accept host TcpStreams
246///      -> send them to the poll loop over a bounded channel
247///   -> NetworkFrameQueues
248///   -> start_network_stack(...)
249///      -> poll thread owns smoltcp Interface + sockets
250///   -> VirtioNetworkRuntime returned to launcher
251/// ```
252///
253/// Outcome:
254/// - guest->host Ethernet frames start flowing into the queues
255/// - host->guest Ethernet frames emitted by smoltcp are written back to libkrun
256/// - published host TCP connections can be forwarded toward guest listeners
257/// - the poll loop starts acting as the guest-visible gateway
258///
259/// Cross-platform: the libkrun frame bridge speaks over an AF_UNIX stream
260/// socket, available on Unix and Windows (10 1809+). The caller supplies the
261/// already-connected host-side socket (Unix: one end of a `socketpair`;
262/// Windows: the socket `accept`ed from libkrun on the per-VM path).
263pub fn start_virtio_network(
264    host_stream: Socket,
265    guest_network: GuestNetworkConfig,
266    published_ports: &[PortMapping],
267    egress: EgressPolicy,
268) -> io::Result<VirtioNetworkRuntime> {
269    virtio_net_log!(
270        "virtio-net: starting runtime guest_ip={} gateway_ip={} dns_server={}",
271        guest_network.guest_ip,
272        guest_network.gateway_ip,
273        guest_network.dns_server
274    );
275    let queues = NetworkFrameQueues::shared(DEFAULT_FRAME_QUEUE_CAPACITY);
276    let frame_bridge = start_frame_stream_bridge(host_stream, queues.clone())?;
277    // tcp_sender sends the accepted TCP connections to the channel
278    // tcp_receiver receives the accepted TCP connections via the channel, and let it be consumed in poll thread.
279    let (tcp_sender, tcp_receiver) = create_tcp_channel();
280    let tcp_listeners = if published_ports.is_empty() {
281        None
282    } else {
283        Some(TcpPortListeners::start(
284            published_ports,
285            tcp_sender,
286            queues.relay_wake.clone(),
287        )?)
288    };
289    let poll_handle = start_network_stack(
290        queues.clone(),
291        VirtioPollConfig {
292            gateway_mac: guest_network.gateway_mac,
293            guest_mac: guest_network.guest_mac,
294            gateway_ipv4: guest_network.gateway_ip,
295            guest_ipv4: guest_network.guest_ip,
296            gateway_ipv6: guest_network.gateway_ip6,
297            guest_ipv6: guest_network.guest_ip6,
298            prefix_len6: guest_network.prefix_len6,
299            upstream_dns: guest_network.upstream_dns,
300            mtu: 1500,
301        },
302        tcp_listeners.as_ref().map(|_| tcp_receiver),
303        egress,
304    )?;
305
306    Ok(VirtioNetworkRuntime {
307        queues,
308        _frame_bridge: frame_bridge,
309        published_ports: tcp_listeners,
310        poll_handle: Some(poll_handle),
311    })
312}
313
314impl VirtioNetworkRuntime {
315    /// Cumulative guest-outbound (egress) bytes on this NIC since boot, at the
316    /// ethernet-frame level. The launcher polls this to surface per-machine
317    /// egress to the node API for billing. TSI VMs have no virtio runtime, so
318    /// egress is unavailable for them (a documented metering gap).
319    pub fn egress_bytes(&self) -> u64 {
320        self.queues.egress_bytes()
321    }
322
323    /// A cheap, cloneable read handle to this NIC's egress counter, so the
324    /// launcher can spawn a thread that periodically flushes the value to the
325    /// VM's runtime dir for the node API to read (the runtime is not `Clone`).
326    pub fn egress_counter(&self) -> std::sync::Arc<std::sync::atomic::AtomicU64> {
327        self.queues.egress_counter()
328    }
329
330    /// Take ownership of the runtime and block until the libkrun stream closes
331    /// (the frame reader marks the queues shutting down on EOF), then drop it for
332    /// a graceful shutdown. Used on Windows, where the runtime is built on the
333    /// `accept` thread: that thread parks here so the runtime — and its worker
334    /// threads — live for the whole VM lifetime instead of being dropped at the
335    /// end of the `accept` callback.
336    pub fn block_until_shutdown(self) {
337        while !self.queues.is_shutting_down() {
338            std::thread::sleep(std::time::Duration::from_millis(200));
339        }
340        // `self` drops here, joining the worker threads.
341    }
342}
343
344impl Drop for VirtioNetworkRuntime {
345    /// Shut down the worker threads in a bounded, cooperative way.
346    ///
347    /// The queue shutdown flag wakes the frame bridge and smoltcp poll loop so
348    /// they can exit on their own. We only explicitly join the poll thread
349    /// here because the frame bridge joins its own threads in its own `Drop`.
350    fn drop(&mut self) {
351        self.queues.begin_shutdown();
352        self.published_ports = None;
353        if let Some(handle) = self.poll_handle.take() {
354            let _ = handle.join();
355        }
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::format_network_log_line;
362    use std::time::UNIX_EPOCH;
363
364    #[test]
365    fn formats_timestamped_network_log_prefix() {
366        let line = format_network_log_line(UNIX_EPOCH, "virtio-net: smoke test");
367        assert_eq!(line, "[1970-01-01T00:00:00Z]: virtio-net: smoke test");
368    }
369}