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