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 egress;
63pub mod frame_stream;
64pub mod queues;
65pub mod stack;
66pub mod tcp_listeners;
67pub mod tcp_relay;
68
69pub use egress::EgressPolicy;
70
71use std::fmt;
72use std::io;
73use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
74use std::os::fd::RawFd;
75use std::thread::JoinHandle;
76use std::time::SystemTime;
77
78use frame_stream::{start_frame_stream_bridge, FrameStreamBridge};
79use queues::{NetworkFrameQueues, DEFAULT_FRAME_QUEUE_CAPACITY};
80use stack::{start_network_stack, VirtioPollConfig};
81use tcp_listeners::{create_tcp_channel, TcpPortListeners};
82
83/// Default upstream DNS resolver used by the gateway runtime.
84pub const DEFAULT_DNS_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
85
86/// Host->guest published TCP port mapping serviced by the virtio gateway.
87///
88/// This stays crate-local so the launchers can translate CLI/data-layer port
89/// mappings into the gateway runtime without pulling the gateway logic back
90/// into the main `smolvm` crate.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct PortMapping {
93 /// Port bound on the host loopback interface.
94 pub host: u16,
95 /// Port exposed inside the guest.
96 pub guest: u16,
97}
98
99impl PortMapping {
100 /// Create a new published port mapping.
101 pub const fn new(host: u16, guest: u16) -> Self {
102 Self { host, guest }
103 }
104}
105
106/// Static guest network configuration for the virtio-net MVP.
107///
108/// This struct describes the two endpoints of the single virtual Ethernet link:
109/// - the guest NIC (`guest_*`)
110/// - the host-side gateway implemented by smolvm (`gateway_*`)
111///
112/// The link is dual-stack: a /30 IPv4 point-to-point pair and a /64 ULA IPv6
113/// pair (`fd53:4d00::/64` — `53:4d` = "SM", matching the MAC OUI scheme).
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct GuestNetworkConfig {
116 /// Guest IPv4 address.
117 pub guest_ip: Ipv4Addr,
118 /// Gateway IPv4 address.
119 pub gateway_ip: Ipv4Addr,
120 /// Prefix length.
121 pub prefix_len: u8,
122 /// Guest IPv6 (ULA) address.
123 pub guest_ip6: Ipv6Addr,
124 /// Gateway IPv6 (ULA) address.
125 pub gateway_ip6: Ipv6Addr,
126 /// IPv6 prefix length.
127 pub prefix_len6: u8,
128 /// Guest MAC address.
129 pub guest_mac: [u8; 6],
130 /// Gateway MAC address.
131 pub gateway_mac: [u8; 6],
132 /// DNS server address presented to the guest.
133 pub dns_server: Ipv4Addr,
134}
135
136impl GuestNetworkConfig {
137 /// Default Phase 1 guest network configuration.
138 pub const fn default() -> Self {
139 Self {
140 guest_ip: Ipv4Addr::new(100, 96, 0, 2),
141 gateway_ip: Ipv4Addr::new(100, 96, 0, 1),
142 prefix_len: 30,
143 guest_ip6: Ipv6Addr::new(0xfd53, 0x4d00, 0, 0, 0, 0, 0, 2),
144 gateway_ip6: Ipv6Addr::new(0xfd53, 0x4d00, 0, 0, 0, 0, 0, 1),
145 prefix_len6: 64,
146 guest_mac: [0x02, 0x53, 0x4d, 0x00, 0x00, 0x02],
147 gateway_mac: [0x02, 0x53, 0x4d, 0x00, 0x00, 0x01],
148 dns_server: Ipv4Addr::new(100, 96, 0, 1),
149 }
150 }
151}
152
153fn format_network_log_line(timestamp: SystemTime, message: &str) -> String {
154 format!(
155 "[{}]: {}",
156 humantime::format_rfc3339_seconds(timestamp),
157 message
158 )
159}
160
161pub(crate) fn emit_network_log_line(message: fmt::Arguments<'_>) {
162 eprintln!(
163 "{}",
164 format_network_log_line(SystemTime::now(), &message.to_string())
165 );
166}
167
168macro_rules! virtio_net_log {
169 ($($arg:tt)*) => {
170 $crate::emit_network_log_line(format_args!($($arg)*))
171 };
172}
173
174pub(crate) use virtio_net_log;
175
176/// Running host-side virtio-net runtime for one guest NIC.
177///
178/// Ownership model:
179/// - one runtime instance corresponds to one guest virtio NIC
180/// - it owns the queue set shared by the worker threads
181/// - it owns the libkrun Unix-stream bridge threads
182/// - it owns the published-port listener threads
183/// - it owns the smoltcp poll thread
184///
185/// Dropping the runtime is the shutdown signal. `Drop` marks the shared queues
186/// as shutting down, wakes blocked workers, and joins the poll thread.
187pub struct VirtioNetworkRuntime {
188 queues: std::sync::Arc<NetworkFrameQueues>,
189 _frame_bridge: FrameStreamBridge,
190 published_ports: Option<TcpPortListeners>,
191 poll_handle: Option<JoinHandle<()>>,
192}
193
194/// Start the host-side virtio-net runtime for one guest NIC.
195///
196/// Inputs:
197/// - `host_fd`: the host-side Unix stream fd that libkrun will use for this
198/// guest NIC. The launcher eventually gets this from the libkrun
199/// `krun_add_net_unixstream()` setup path.
200/// - `guest_network`: the static guest/gateway addressing and MAC plan for this
201/// NIC.
202/// - `published_ports`: host->guest TCP port mappings that should be serviced
203/// directly by the virtio runtime instead of TSI.
204///
205/// High-level flow:
206///
207/// ```text
208/// start_virtio_network()
209/// -> create shared frame queues + wake pipes
210/// -> start frame reader/writer threads on the Unix stream
211/// -> start host TcpListeners for published ports
212/// -> start the smoltcp poll thread
213/// -> return a handle that owns the whole runtime
214/// ```
215///
216/// Expanded startup picture:
217///
218/// ```text
219/// host_fd from libkrun
220/// -> FrameStreamBridge(host_fd)
221/// -> reader thread
222/// -> writer thread
223/// -> TcpPortListeners
224/// -> accept host TcpStreams
225/// -> send them to the poll loop over a bounded channel
226/// -> NetworkFrameQueues
227/// -> start_network_stack(...)
228/// -> poll thread owns smoltcp Interface + sockets
229/// -> VirtioNetworkRuntime returned to launcher
230/// ```
231///
232/// Outcome:
233/// - guest->host Ethernet frames start flowing into the queues
234/// - host->guest Ethernet frames emitted by smoltcp are written back to libkrun
235/// - published host TCP connections can be forwarded toward guest listeners
236/// - the poll loop starts acting as the guest-visible gateway
237pub fn start_virtio_network(
238 host_fd: RawFd,
239 guest_network: GuestNetworkConfig,
240 published_ports: &[PortMapping],
241 egress: EgressPolicy,
242) -> io::Result<VirtioNetworkRuntime> {
243 virtio_net_log!(
244 "virtio-net: starting runtime host_fd={} guest_ip={} gateway_ip={} dns_server={}",
245 host_fd,
246 guest_network.guest_ip,
247 guest_network.gateway_ip,
248 guest_network.dns_server
249 );
250 let queues = NetworkFrameQueues::shared(DEFAULT_FRAME_QUEUE_CAPACITY);
251 let frame_bridge = start_frame_stream_bridge(host_fd, queues.clone())?;
252 // tcp_sender sends the accepted TCP connections to the channel
253 // tcp_receiver receives the accepted TCP connections via the channel, and let it be consumed in poll thread.
254 let (tcp_sender, tcp_receiver) = create_tcp_channel();
255 let tcp_listeners = if published_ports.is_empty() {
256 None
257 } else {
258 Some(TcpPortListeners::start(
259 published_ports,
260 tcp_sender,
261 queues.relay_wake.clone(),
262 )?)
263 };
264 let poll_handle = start_network_stack(
265 queues.clone(),
266 VirtioPollConfig {
267 gateway_mac: guest_network.gateway_mac,
268 guest_mac: guest_network.guest_mac,
269 gateway_ipv4: guest_network.gateway_ip,
270 guest_ipv4: guest_network.guest_ip,
271 gateway_ipv6: guest_network.gateway_ip6,
272 guest_ipv6: guest_network.guest_ip6,
273 prefix_len6: guest_network.prefix_len6,
274 mtu: 1500,
275 },
276 tcp_listeners.as_ref().map(|_| tcp_receiver),
277 egress,
278 )?;
279
280 Ok(VirtioNetworkRuntime {
281 queues,
282 _frame_bridge: frame_bridge,
283 published_ports: tcp_listeners,
284 poll_handle: Some(poll_handle),
285 })
286}
287
288impl Drop for VirtioNetworkRuntime {
289 /// Shut down the worker threads in a bounded, cooperative way.
290 ///
291 /// The queue shutdown flag wakes the frame bridge and smoltcp poll loop so
292 /// they can exit on their own. We only explicitly join the poll thread
293 /// here because the frame bridge joins its own threads in its own `Drop`.
294 fn drop(&mut self) {
295 self.queues.begin_shutdown();
296 self.published_ports = None;
297 if let Some(handle) = self.poll_handle.take() {
298 let _ = handle.join();
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::format_network_log_line;
306 use std::time::UNIX_EPOCH;
307
308 #[test]
309 fn formats_timestamped_network_log_prefix() {
310 let line = format_network_log_line(UNIX_EPOCH, "virtio-net: smoke test");
311 assert_eq!(line, "[1970-01-01T00:00:00Z]: virtio-net: smoke test");
312 }
313}