Skip to main content

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}