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.
9
10use std::{os::fd::RawFd, sync::Arc};
11
12use msb_krun::backends::net::{NetBackend, ReadError, WriteError};
13
14use crate::shared::SharedState;
15
16//--------------------------------------------------------------------------------------------------
17// Constants
18//--------------------------------------------------------------------------------------------------
19
20/// Size of the virtio-net header (`virtio_net_hdr_v1`): 12 bytes.
21///
22/// libkrun's NetWorker prepends this header to every frame buffer. The
23/// backend must strip it on TX (guest → smoltcp) and prepend a zeroed
24/// header on RX (smoltcp → guest).
25const VIRTIO_NET_HDR_LEN: usize = 12;
26
27//--------------------------------------------------------------------------------------------------
28// Types
29//--------------------------------------------------------------------------------------------------
30
31/// Network backend that bridges libkrun's NetWorker to smoltcp via lock-free
32/// queues.
33///
34/// - **TX path** (`write_frame`): strips the virtio-net header, pushes the
35/// ethernet frame to `tx_ring`, wakes the smoltcp poll thread.
36/// - **RX path** (`read_frame`): pops a frame from `rx_ring`, prepends a
37/// zeroed virtio-net header for the guest.
38/// - **Wake fd** (`raw_socket_fd`): returns `rx_wake`'s read end so the
39/// NetWorker's epoll can detect new frames.
40pub struct SmoltcpBackend {
41 shared: Arc<SharedState>,
42}
43
44//--------------------------------------------------------------------------------------------------
45// Methods
46//--------------------------------------------------------------------------------------------------
47
48impl SmoltcpBackend {
49 /// Create a new backend connected to the given shared state.
50 pub fn new(shared: Arc<SharedState>) -> Self {
51 Self { shared }
52 }
53}
54
55//--------------------------------------------------------------------------------------------------
56// Trait Implementations
57//--------------------------------------------------------------------------------------------------
58
59impl NetBackend for SmoltcpBackend {
60 /// Guest is sending a frame. Strip the virtio-net header and enqueue
61 /// the raw ethernet frame for smoltcp.
62 fn write_frame(&mut self, hdr_len: usize, buf: &mut [u8]) -> Result<(), WriteError> {
63 let ethernet_frame = buf[hdr_len..].to_vec();
64 self.shared.add_tx_bytes(ethernet_frame.len());
65 self.shared
66 .tx_ring
67 .push(ethernet_frame)
68 .map_err(|_| WriteError::NothingWritten)?;
69 self.shared.tx_wake.wake();
70 Ok(())
71 }
72
73 /// Deliver a frame from smoltcp to the guest. Prepends a zeroed
74 /// virtio-net header.
75 fn read_frame(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> {
76 let frame = self.shared.rx_ring.pop().ok_or(ReadError::NothingRead)?;
77
78 let total_len = VIRTIO_NET_HDR_LEN + frame.len();
79 if total_len > buf.len() {
80 // Frame too large for the buffer — drop it to avoid panicking.
81 tracing::debug!(
82 frame_len = frame.len(),
83 buf_len = buf.len(),
84 "dropping oversized frame from rx_ring"
85 );
86 return Err(ReadError::NothingRead);
87 }
88
89 // Prepend zeroed virtio-net header.
90 buf[..VIRTIO_NET_HDR_LEN].fill(0);
91 buf[VIRTIO_NET_HDR_LEN..total_len].copy_from_slice(&frame);
92
93 Ok(total_len)
94 }
95
96 /// No partial writes — queue push is atomic.
97 fn has_unfinished_write(&self) -> bool {
98 false
99 }
100
101 /// No partial writes — nothing to finish.
102 fn try_finish_write(&mut self, _hdr_len: usize, _buf: &[u8]) -> Result<(), WriteError> {
103 Ok(())
104 }
105
106 /// File descriptor for NetWorker's epoll. Becomes readable when
107 /// `rx_ring` has frames for the guest (i.e. when smoltcp's
108 /// `SmoltcpDevice::transmit()` pushes a frame and wakes `rx_wake`).
109 fn raw_socket_fd(&self) -> RawFd {
110 self.shared.rx_wake.as_raw_fd()
111 }
112}