Skip to main content

microsandbox_network/engine/netstack/
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`](super::shared::WakePipe)
8//! notifications. Unix libkrun registers [`raw_socket_fd`](NetBackend::raw_socket_fd)
9//! in edge-triggered mode, while Windows libkrun waits on an event source. Reads
10//! must drain the wake primitive before returning.
11
12#[cfg(unix)]
13use std::os::fd::RawFd;
14use std::sync::Arc;
15
16use msb_krun::backends::net::{NetBackend, ReadError, WriteError};
17#[cfg(windows)]
18use msb_krun_utils::event::{EventSource, EventToken};
19
20use super::shared::SharedState;
21
22//--------------------------------------------------------------------------------------------------
23// Constants
24//--------------------------------------------------------------------------------------------------
25
26/// Size of the virtio-net header (`virtio_net_hdr_v1`): 12 bytes.
27///
28/// libkrun's NetWorker prepends this header to every frame buffer. The
29/// backend must strip it on TX (guest → smoltcp) and prepend a zeroed
30/// header on RX (smoltcp → guest).
31const VIRTIO_NET_HDR_LEN: usize = 12;
32
33//--------------------------------------------------------------------------------------------------
34// Types
35//--------------------------------------------------------------------------------------------------
36
37/// Network backend that bridges libkrun's NetWorker to smoltcp via lock-free
38/// queues.
39///
40/// - **TX path** (`write_frame`): strips the virtio-net header, pushes the
41///   ethernet frame to `tx_ring`, wakes the smoltcp poll thread.
42/// - **RX path** (`read_frame`): pops a frame from `rx_ring`, prepends a
43///   zeroed virtio-net header for the guest.
44/// - **Wake source**: returns `rx_wake`'s pollable fd on Unix or waitable
45///   event handle on Windows so the NetWorker can detect new frames.
46pub struct SmoltcpBackend {
47    shared: Arc<SharedState>,
48}
49
50//--------------------------------------------------------------------------------------------------
51// Methods
52//--------------------------------------------------------------------------------------------------
53
54impl SmoltcpBackend {
55    /// Create a new backend connected to the given shared state.
56    pub fn new(shared: Arc<SharedState>) -> Self {
57        Self { shared }
58    }
59
60    fn read_frame_from_ring(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> {
61        self.shared.rx_wake.drain();
62
63        let frame = self.shared.rx_ring.pop().ok_or(ReadError::NothingRead)?;
64
65        let total_len = VIRTIO_NET_HDR_LEN + frame.len();
66        if total_len > buf.len() {
67            // Frame too large for the buffer — drop it to avoid panicking.
68            tracing::debug!(
69                frame_len = frame.len(),
70                buf_len = buf.len(),
71                "dropping oversized frame from rx_ring"
72            );
73            return Err(ReadError::NothingRead);
74        }
75
76        // Prepend zeroed virtio-net header.
77        buf[..VIRTIO_NET_HDR_LEN].fill(0);
78        buf[VIRTIO_NET_HDR_LEN..total_len].copy_from_slice(&frame);
79
80        Ok(total_len)
81    }
82}
83
84//--------------------------------------------------------------------------------------------------
85// Trait Implementations
86//--------------------------------------------------------------------------------------------------
87
88impl NetBackend for SmoltcpBackend {
89    /// Guest is sending a frame. Strip the virtio-net header and enqueue
90    /// the raw ethernet frame for smoltcp.
91    fn write_frame(&mut self, hdr_len: usize, buf: &mut [u8]) -> Result<(), WriteError> {
92        let ethernet_frame = buf[hdr_len..].to_vec();
93        let frame_len = ethernet_frame.len();
94
95        if self.shared.tx_ring.push(ethernet_frame).is_err() {
96            // This backend exposes a wake pipe to libkrun, not a real writable
97            // socket. Returning NothingWritten would make the virtio worker
98            // undo the TX pop and wait for write readiness that cannot signal
99            // tx_ring capacity. Treat overflow like a lossy NIC queue instead:
100            // drop the frame and let upper layers retransmit if needed.
101            tracing::debug!("dropping guest network frame because tx_ring is full");
102            return Ok(());
103        }
104
105        self.shared.add_tx_bytes(frame_len);
106        self.shared.tx_wake.wake();
107        Ok(())
108    }
109
110    /// Deliver a frame from smoltcp to the guest. Prepends a zeroed
111    /// virtio-net header.
112    fn read_frame(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> {
113        self.read_frame_from_ring(buf)
114    }
115
116    /// No partial writes — queue push is atomic.
117    fn has_unfinished_write(&self) -> bool {
118        false
119    }
120
121    /// No partial writes — nothing to finish.
122    fn try_finish_write(&mut self, _hdr_len: usize, _buf: &[u8]) -> Result<(), WriteError> {
123        Ok(())
124    }
125
126    /// The lock-free ring backend never blocks inside a frame operation and owns no helper thread
127    /// beyond the separately coordinated smoltcp participant.
128    fn supports_quiesce(&self) -> bool {
129        true
130    }
131
132    /// File descriptor for NetWorker's epoll. Becomes readable when
133    /// `rx_ring` has frames for the guest (i.e. when smoltcp's
134    /// `SmoltcpDevice::transmit()` pushes a frame and wakes `rx_wake`).
135    #[cfg(unix)]
136    fn raw_socket_fd(&self) -> RawFd {
137        self.shared.rx_wake.as_raw_fd()
138    }
139
140    /// Waitable event source for NetWorker on Windows.
141    #[cfg(windows)]
142    fn event_source(&self, token: EventToken) -> EventSource {
143        EventSource::waitable_handle(self.shared.rx_wake.as_raw_handle(), token)
144    }
145}
146
147//--------------------------------------------------------------------------------------------------
148// Tests
149//--------------------------------------------------------------------------------------------------
150
151#[cfg(all(test, unix))]
152mod tests {
153    use std::sync::Arc;
154
155    use super::*;
156
157    #[test]
158    fn read_frame_drains_rx_wake_pipe() {
159        let shared = Arc::new(SharedState::new(4));
160        let mut backend = SmoltcpBackend::new(shared.clone());
161        let mut buf = [0u8; 64];
162
163        assert!(shared.push_rx_frame_and_wake(vec![0xaa, 0xbb]));
164        assert!(fd_is_readable(backend.raw_socket_fd()));
165
166        let n = backend.read_frame(&mut buf).expect("frame should be read");
167        assert_eq!(n, VIRTIO_NET_HDR_LEN + 2);
168        assert_eq!(&buf[VIRTIO_NET_HDR_LEN..n], &[0xaa, 0xbb]);
169        assert!(!fd_is_readable(backend.raw_socket_fd()));
170
171        assert!(shared.push_rx_frame_and_wake(vec![0xcc]));
172        assert!(fd_is_readable(backend.raw_socket_fd()));
173    }
174
175    #[test]
176    fn write_frame_enqueues_guest_frame_and_wakes_poll_loop() {
177        let shared = Arc::new(SharedState::new(1));
178        let mut backend = SmoltcpBackend::new(shared.clone());
179        let mut buf = vec![0u8; VIRTIO_NET_HDR_LEN + 3];
180        buf[VIRTIO_NET_HDR_LEN..].copy_from_slice(&[0xaa, 0xbb, 0xcc]);
181
182        backend
183            .write_frame(VIRTIO_NET_HDR_LEN, &mut buf)
184            .expect("accepted frame should be queued");
185
186        assert_eq!(shared.tx_bytes(), 3);
187        assert!(fd_is_readable(shared.tx_wake.as_raw_fd()));
188        assert_eq!(shared.tx_ring.pop(), Some(vec![0xaa, 0xbb, 0xcc]));
189    }
190
191    #[test]
192    fn write_frame_drops_guest_frame_when_tx_ring_is_full() {
193        let shared = Arc::new(SharedState::new(1));
194        shared.tx_ring.push(vec![0x11]).unwrap();
195        let mut backend = SmoltcpBackend::new(shared.clone());
196        let mut buf = vec![0u8; VIRTIO_NET_HDR_LEN + 2];
197        buf[VIRTIO_NET_HDR_LEN..].copy_from_slice(&[0xaa, 0xbb]);
198
199        backend
200            .write_frame(VIRTIO_NET_HDR_LEN, &mut buf)
201            .expect("overflow should not stall the virtio TX queue");
202
203        assert_eq!(shared.tx_bytes(), 0);
204        assert_eq!(shared.tx_ring.pop(), Some(vec![0x11]));
205        assert_eq!(shared.tx_ring.pop(), None);
206    }
207
208    fn fd_is_readable(fd: RawFd) -> bool {
209        let mut pfd = libc::pollfd {
210            fd,
211            events: libc::POLLIN,
212            revents: 0,
213        };
214
215        // SAFETY: `pfd` points to a valid pollfd for a live file descriptor.
216        let ret = unsafe { libc::poll(&mut pfd, 1, 0) };
217        assert!(ret >= 0, "poll failed: {}", std::io::Error::last_os_error());
218
219        ret == 1 && pfd.revents & libc::POLLIN != 0
220    }
221}