microsandbox_network/backend.rs
1//! `SmoltcpBackend` — libkrun [`NetBackend`] implementation that bridges the
2//! NetWorker thread to the smoltcp poll thread via lock-free queues.
3//!
4//! The NetWorker calls [`write_frame()`](NetBackend::write_frame) when the
5//! guest sends a frame and [`read_frame()`](NetBackend::read_frame) to deliver
6//! frames back to the guest. Frames flow through [`SharedState`]'s
7//! `tx_ring`/`rx_ring` queues with [`WakePipe`](crate::shared::WakePipe)
8//! notifications. libkrun registers [`raw_socket_fd`](NetBackend::raw_socket_fd)
9//! in edge-triggered mode, so reads must drain the wake pipe before returning.
10
11use std::{os::fd::RawFd, sync::Arc};
12
13use msb_krun::backends::net::{NetBackend, ReadError, WriteError};
14
15use crate::shared::SharedState;
16
17//--------------------------------------------------------------------------------------------------
18// Constants
19//--------------------------------------------------------------------------------------------------
20
21/// Size of the virtio-net header (`virtio_net_hdr_v1`): 12 bytes.
22///
23/// libkrun's NetWorker prepends this header to every frame buffer. The
24/// backend must strip it on TX (guest → smoltcp) and prepend a zeroed
25/// header on RX (smoltcp → guest).
26const VIRTIO_NET_HDR_LEN: usize = 12;
27
28//--------------------------------------------------------------------------------------------------
29// Types
30//--------------------------------------------------------------------------------------------------
31
32/// Network backend that bridges libkrun's NetWorker to smoltcp via lock-free
33/// queues.
34///
35/// - **TX path** (`write_frame`): strips the virtio-net header, pushes the
36/// ethernet frame to `tx_ring`, wakes the smoltcp poll thread.
37/// - **RX path** (`read_frame`): pops a frame from `rx_ring`, prepends a
38/// zeroed virtio-net header for the guest.
39/// - **Wake fd** (`raw_socket_fd`): returns `rx_wake`'s read end so the
40/// NetWorker's epoll can detect new frames.
41pub struct SmoltcpBackend {
42 shared: Arc<SharedState>,
43}
44
45//--------------------------------------------------------------------------------------------------
46// Methods
47//--------------------------------------------------------------------------------------------------
48
49impl SmoltcpBackend {
50 /// Create a new backend connected to the given shared state.
51 pub fn new(shared: Arc<SharedState>) -> Self {
52 Self { shared }
53 }
54}
55
56//--------------------------------------------------------------------------------------------------
57// Trait Implementations
58//--------------------------------------------------------------------------------------------------
59
60impl NetBackend for SmoltcpBackend {
61 /// Guest is sending a frame. Strip the virtio-net header and enqueue
62 /// the raw ethernet frame for smoltcp.
63 fn write_frame(&mut self, hdr_len: usize, buf: &mut [u8]) -> Result<(), WriteError> {
64 let ethernet_frame = buf[hdr_len..].to_vec();
65 self.shared.add_tx_bytes(ethernet_frame.len());
66 self.shared
67 .tx_ring
68 .push(ethernet_frame)
69 .map_err(|_| WriteError::NothingWritten)?;
70 self.shared.tx_wake.wake();
71 Ok(())
72 }
73
74 /// Deliver a frame from smoltcp to the guest. Prepends a zeroed
75 /// virtio-net header.
76 fn read_frame(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> {
77 self.shared.rx_wake.drain();
78
79 let frame = self.shared.rx_ring.pop().ok_or(ReadError::NothingRead)?;
80
81 let total_len = VIRTIO_NET_HDR_LEN + frame.len();
82 if total_len > buf.len() {
83 // Frame too large for the buffer — drop it to avoid panicking.
84 tracing::debug!(
85 frame_len = frame.len(),
86 buf_len = buf.len(),
87 "dropping oversized frame from rx_ring"
88 );
89 return Err(ReadError::NothingRead);
90 }
91
92 // Prepend zeroed virtio-net header.
93 buf[..VIRTIO_NET_HDR_LEN].fill(0);
94 buf[VIRTIO_NET_HDR_LEN..total_len].copy_from_slice(&frame);
95
96 Ok(total_len)
97 }
98
99 /// No partial writes — queue push is atomic.
100 fn has_unfinished_write(&self) -> bool {
101 false
102 }
103
104 /// No partial writes — nothing to finish.
105 fn try_finish_write(&mut self, _hdr_len: usize, _buf: &[u8]) -> Result<(), WriteError> {
106 Ok(())
107 }
108
109 /// File descriptor for NetWorker's epoll. Becomes readable when
110 /// `rx_ring` has frames for the guest (i.e. when smoltcp's
111 /// `SmoltcpDevice::transmit()` pushes a frame and wakes `rx_wake`).
112 fn raw_socket_fd(&self) -> RawFd {
113 self.shared.rx_wake.as_raw_fd()
114 }
115}
116
117//--------------------------------------------------------------------------------------------------
118// Tests
119//--------------------------------------------------------------------------------------------------
120
121#[cfg(test)]
122mod tests {
123 use std::sync::Arc;
124
125 use super::*;
126
127 #[test]
128 fn read_frame_drains_rx_wake_pipe() {
129 let shared = Arc::new(SharedState::new(4));
130 let mut backend = SmoltcpBackend::new(shared.clone());
131 let mut buf = [0u8; 64];
132
133 assert!(shared.push_rx_frame_and_wake(vec![0xaa, 0xbb]));
134 assert!(fd_is_readable(backend.raw_socket_fd()));
135
136 let n = backend.read_frame(&mut buf).expect("frame should be read");
137 assert_eq!(n, VIRTIO_NET_HDR_LEN + 2);
138 assert_eq!(&buf[VIRTIO_NET_HDR_LEN..n], &[0xaa, 0xbb]);
139 assert!(!fd_is_readable(backend.raw_socket_fd()));
140
141 assert!(shared.push_rx_frame_and_wake(vec![0xcc]));
142 assert!(fd_is_readable(backend.raw_socket_fd()));
143 }
144
145 fn fd_is_readable(fd: RawFd) -> bool {
146 let mut pfd = libc::pollfd {
147 fd,
148 events: libc::POLLIN,
149 revents: 0,
150 };
151
152 // SAFETY: `pfd` points to a valid pollfd for a live file descriptor.
153 let ret = unsafe { libc::poll(&mut pfd, 1, 0) };
154 assert!(ret >= 0, "poll failed: {}", std::io::Error::last_os_error());
155
156 ret == 1 && pfd.revents & libc::POLLIN != 0
157 }
158}