smolvm_network/frame_stream.rs
1//! libkrun unix-stream framing for the virtio-net backend.
2//!
3//! Context
4//! =======
5//!
6//! libkrun's `krun_add_net_unixstream()` interface does not hand us raw virtio
7//! rings or a tap device. Instead, it exposes a Unix stream file descriptor
8//! carrying Ethernet frames in a tiny framing protocol:
9//!
10//! ```text
11//! [4-byte big-endian frame length][raw ethernet frame bytes]
12//! ```
13//!
14//! Important details:
15//! - the payload is a raw Ethernet frame
16//! - there is no virtio-net header on this stream
17//! - libkrun adds/removes its internal virtio-net header itself
18//! - partial reads and partial writes are normal stream-socket behavior and
19//! must be handled explicitly
20//!
21//! So this module is not the TCP/IP stack. It is just the bridge between:
22//! - libkrun's Unix stream transport
23//! - the in-process frame queues consumed by the host smoltcp runtime
24//!
25//! Data flow:
26//!
27//! ```text
28//! guest -> libkrun -> UnixStream -> run_reader() -> guest_to_host queue
29//! host <- libkrun <- UnixStream <- run_writer() <- host_to_guest queue
30//! ```
31//!
32//! In the broader runtime, this module sits here:
33//!
34//! ```text
35//! libkrun unixstream
36//! <-> FrameStreamBridge
37//! <-> NetworkFrameQueues
38//! <-> VirtioNetworkDevice / smoltcp poll loop
39//! ```
40
41use crate::queues::NetworkFrameQueues;
42use socket2::Socket;
43use std::io::{self, Read, Write};
44use std::net::Shutdown;
45use std::sync::Arc;
46use std::thread::{self, JoinHandle};
47
48const FRAME_HEADER_LEN: usize = 4;
49const SOCKET_SENDBUF_BYTES: usize = 16 * 1024 * 1024;
50const MAX_FRAME_LEN: usize = 64 * 1024;
51
52/// Running libkrun unix-stream bridge for one virtio NIC.
53///
54/// The bridge owns:
55/// - the shared AF_UNIX stream socket (used to trigger shutdown via `shutdown`)
56/// - a reader thread for guest->host frames
57/// - a writer thread for host->guest frames
58///
59/// The transport is an AF_UNIX stream socket, wrapped in [`socket2::Socket`] so
60/// the same code serves Unix hosts and Windows (10 1809+ has native AF_UNIX).
61/// On Unix the launcher hands us one end of a `socketpair`; on Windows it hands
62/// us the socket it `accept`ed from libkrun on a per-VM path.
63///
64/// The socket is shared via `Arc` and the reader/writer threads use it through
65/// `&Socket` (socket2 implements `Read`/`Write` for `&Socket`), rather than
66/// `try_clone`-ing it three ways: duplicating an AF_UNIX socket handle is
67/// unreliable on Windows (a clone may not observe the peer's writes), which
68/// silently stalled all guest traffic. Concurrent read + write on one socket is
69/// safe — the directions are independent.
70pub struct FrameStreamBridge {
71 socket: Arc<Socket>,
72 queues: Arc<NetworkFrameQueues>,
73 reader_handle: Option<JoinHandle<()>>,
74 writer_handle: Option<JoinHandle<()>>,
75}
76
77/// Start the libkrun stream reader and writer threads for one virtio NIC.
78///
79/// Takes ownership of an already-connected AF_UNIX stream socket to libkrun.
80pub fn start_frame_stream_bridge(
81 stream: Socket,
82 queues: Arc<NetworkFrameQueues>,
83) -> io::Result<FrameStreamBridge> {
84 set_socket_send_buffer(&stream);
85 let socket = Arc::new(stream);
86
87 let reader_socket = socket.clone();
88 let reader_handle = thread::Builder::new()
89 .name("smolvm-net-reader".into())
90 .spawn({
91 let queues = queues.clone();
92 move || run_reader(reader_socket, queues)
93 })?;
94
95 let writer_socket = socket.clone();
96 let writer_queues = queues.clone();
97 let writer_handle = thread::Builder::new()
98 .name("smolvm-net-writer".into())
99 .spawn(move || run_writer(writer_socket, writer_queues))?;
100
101 Ok(FrameStreamBridge {
102 socket,
103 queues,
104 reader_handle: Some(reader_handle),
105 writer_handle: Some(writer_handle),
106 })
107}
108
109impl Drop for FrameStreamBridge {
110 /// Request shutdown and join the reader/writer workers.
111 ///
112 /// `shutdown(Shutdown::Both)` is the important part here: it forces any
113 /// blocking read/write on the shared socket to wake up and fail, which lets
114 /// the threads notice shutdown and return.
115 fn drop(&mut self) {
116 self.queues.begin_shutdown();
117 let _ = self.socket.shutdown(Shutdown::Both);
118
119 if let Some(handle) = self.reader_handle.take() {
120 let _ = handle.join();
121 }
122 if let Some(handle) = self.writer_handle.take() {
123 let _ = handle.join();
124 }
125 }
126}
127
128fn run_reader(reader: Arc<Socket>, queues: Arc<NetworkFrameQueues>) {
129 // Reader thread:
130 // libkrun -> Unix stream -> guest_to_host queue -> smoltcp poll loop
131 let mut sock: &Socket = &reader;
132 loop {
133 match read_frame(&mut sock) {
134 Ok(frame) => {
135 if queues.guest_to_host.push(frame).is_ok() {
136 queues.guest_wake.wake();
137 } else {
138 tracing::warn!("dropping guest ethernet frame because the host queue is full");
139 }
140 }
141 Err(err) => {
142 queues.begin_shutdown();
143 tracing::debug!(error = %err, "virtio-net reader thread stopped");
144 return;
145 }
146 }
147 }
148}
149
150fn run_writer(writer: Arc<Socket>, queues: Arc<NetworkFrameQueues>) {
151 // Writer thread:
152 // smoltcp / host runtime -> host_to_guest queue -> Unix stream -> libkrun
153 let mut sock: &Socket = &writer;
154 loop {
155 if queues.is_shutting_down() && queues.host_to_guest.is_empty() {
156 return;
157 }
158 match queues.host_wake.wait(None) {
159 Ok(true) => queues.host_wake.drain(),
160 Ok(false) => continue,
161 Err(err) => {
162 queues.begin_shutdown();
163 tracing::debug!(error = %err, "virtio-net writer wake pipe failed");
164 return;
165 }
166 }
167
168 while let Some(frame) = queues.host_to_guest.pop() {
169 if let Err(err) = write_frame(&mut sock, &frame) {
170 queues.begin_shutdown();
171 tracing::debug!(error = %err, "virtio-net writer thread stopped");
172 return;
173 }
174 }
175 }
176}
177
178/// Read one raw Ethernet frame using libkrun's 4-byte big-endian length prefix.
179///
180/// Wire format:
181///
182/// ```text
183/// 0 3 4 ...
184/// +----------------+----------------------+
185/// | frame_len (BE) | ethernet frame bytes |
186/// +----------------+----------------------+
187/// ```
188///
189/// `read_exact` is intentional:
190/// - Unix stream sockets are byte streams, not message sockets
191/// - one `read` may return only part of the header or part of the frame
192/// - the bridge must keep reading until the whole logical frame arrives
193///
194/// Outcome:
195/// - returns the next complete raw Ethernet frame
196/// - rejects zero-length or implausibly large frames as protocol errors
197pub(crate) fn read_frame<R: Read>(reader: &mut R) -> io::Result<Vec<u8>> {
198 let mut header = [0u8; FRAME_HEADER_LEN];
199 reader.read_exact(&mut header)?;
200 let frame_len = u32::from_be_bytes(header) as usize;
201
202 if frame_len == 0 || frame_len > MAX_FRAME_LEN {
203 return Err(io::Error::new(
204 io::ErrorKind::InvalidData,
205 format!("invalid ethernet frame length: {frame_len}"),
206 ));
207 }
208
209 let mut frame = vec![0u8; frame_len];
210 reader.read_exact(&mut frame)?;
211 Ok(frame)
212}
213
214/// Write one raw Ethernet frame using libkrun's 4-byte big-endian length prefix.
215///
216/// This is the inverse of [`read_frame`]:
217///
218/// ```text
219/// write 4-byte BE length
220/// write raw frame bytes
221/// flush stream
222/// ```
223///
224/// `write_all` is used instead of a single `write` because stream sockets may
225/// accept only part of the buffer. The caller should not need to reason about
226/// partial-write state; this helper completes the logical frame write or fails.
227pub(crate) fn write_frame<W: Write>(writer: &mut W, frame: &[u8]) -> io::Result<()> {
228 if frame.is_empty() || frame.len() > MAX_FRAME_LEN {
229 return Err(io::Error::new(
230 io::ErrorKind::InvalidInput,
231 format!("invalid ethernet frame length: {}", frame.len()),
232 ));
233 }
234
235 let header = (frame.len() as u32).to_be_bytes();
236 write_all(writer, &header)?;
237 write_all(writer, frame)?;
238 writer.flush()
239}
240
241fn write_all<W: Write>(writer: &mut W, mut buf: &[u8]) -> io::Result<()> {
242 // This is the stream-socket equivalent of "keep sending until the whole
243 // logical message is written". `Write::write` may legally write fewer bytes
244 // than requested even on success.
245 while !buf.is_empty() {
246 let written = writer.write(buf)?;
247 if written == 0 {
248 return Err(io::Error::new(
249 io::ErrorKind::WriteZero,
250 "short write while sending ethernet frame",
251 ));
252 }
253 buf = &buf[written..];
254 }
255 Ok(())
256}
257
258/// Increase the socket's send-buffer size (SO_SNDBUF). This stream carries
259/// Ethernet frames toward libkrun; a large send buffer absorbs bursts so we can
260/// queue several frames before libkrun catches up. The OS may clamp the request,
261/// and the option is non-critical, so a failure is logged and ignored.
262/// `socket2` issues the platform `setsockopt` so this works on Unix and Windows.
263fn set_socket_send_buffer(stream: &Socket) {
264 if let Err(err) = stream.set_send_buffer_size(SOCKET_SENDBUF_BYTES) {
265 tracing::warn!(
266 error = %err,
267 "failed to increase SO_SNDBUF for virtio-net unixstream"
268 );
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 struct PartialWriter {
277 written: Vec<u8>,
278 chunk_size: usize,
279 }
280
281 impl Write for PartialWriter {
282 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
283 let take = buf.len().min(self.chunk_size);
284 self.written.extend_from_slice(&buf[..take]);
285 Ok(take)
286 }
287
288 fn flush(&mut self) -> io::Result<()> {
289 Ok(())
290 }
291 }
292
293 #[test]
294 fn write_frame_handles_partial_writes() {
295 let mut writer = PartialWriter {
296 written: Vec::new(),
297 chunk_size: 3,
298 };
299 write_frame(&mut writer, &[1, 2, 3, 4, 5, 6]).unwrap();
300 assert_eq!(writer.written[..4], [0, 0, 0, 6]);
301 assert_eq!(writer.written[4..], [1, 2, 3, 4, 5, 6]);
302 }
303
304 #[test]
305 fn read_frame_decodes_length_prefix() {
306 let mut input = std::io::Cursor::new(vec![0, 0, 0, 3, 7, 8, 9]);
307 assert_eq!(read_frame(&mut input).unwrap(), vec![7, 8, 9]);
308 }
309
310 #[cfg(unix)]
311 #[test]
312 fn unix_stream_round_trip_multiple_frames() {
313 use std::os::unix::net::UnixStream;
314 let (mut left, mut right) = UnixStream::pair().unwrap();
315 write_frame(&mut left, &[1, 2, 3]).unwrap();
316 write_frame(&mut left, &[4, 5]).unwrap();
317
318 assert_eq!(read_frame(&mut right).unwrap(), vec![1, 2, 3]);
319 assert_eq!(read_frame(&mut right).unwrap(), vec![4, 5]);
320 }
321}