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