Skip to main content

microsandbox_network/
device.rs

1//! Slot-based [`smoltcp::phy::Device`] implementation.
2//!
3//! [`SmoltcpDevice`] bridges [`SharedState`]'s lock-free queues to smoltcp's
4//! token-based `Device` API. It uses a **single-frame slot** design: the poll
5//! loop pops a frame from `tx_ring` via [`stage_next_frame()`], inspects it
6//! (creating TCP sockets before smoltcp sees a SYN), then smoltcp consumes
7//! the staged frame via [`receive()`](smoltcp::phy::Device::receive).
8//!
9//! [`stage_next_frame()`]: SmoltcpDevice::stage_next_frame
10
11use std::sync::Arc;
12use std::sync::atomic::{AtomicBool, Ordering};
13
14use smoltcp::phy::{self, DeviceCapabilities, Medium};
15use smoltcp::time::Instant;
16
17use crate::shared::SharedState;
18
19//--------------------------------------------------------------------------------------------------
20// Types
21//--------------------------------------------------------------------------------------------------
22
23/// smoltcp device backed by [`SharedState`]'s lock-free queues.
24///
25/// # Slot-based design
26///
27/// The poll loop controls when frames are popped from `tx_ring`:
28///
29/// 1. Call [`stage_next_frame()`](Self::stage_next_frame) to pop a frame and
30///    inspect it.
31/// 2. Optionally call [`drop_staged_frame()`](Self::drop_staged_frame) to
32///    discard the frame (e.g. non-DNS UDP handled outside smoltcp).
33/// 3. When smoltcp's `iface.poll()` calls `receive()`, the staged frame is
34///    consumed.
35pub struct SmoltcpDevice {
36    shared: Arc<SharedState>,
37    mtu: usize,
38    /// Single-frame slot. Set by the poll loop via `stage_next_frame()`,
39    /// consumed by smoltcp's `poll()` via `receive()`.
40    pending_rx: Option<Vec<u8>>,
41    /// Set by `TxToken::consume` when a frame is pushed to `rx_ring`.
42    /// The poll loop checks this flag after the egress loop and calls
43    /// `rx_wake.wake()` once instead of per-frame (coalesced wakes).
44    pub(crate) frames_emitted: AtomicBool,
45}
46
47/// Token returned by the `Device::receive()` implementation — delivers one
48/// frame from the guest to smoltcp.
49pub struct SmoltcpRxToken {
50    frame: Vec<u8>,
51}
52
53/// Token returned by the `Device::receive()` and `Device::transmit()`
54/// implementations — sends one frame from smoltcp to the guest.
55pub struct SmoltcpTxToken<'a> {
56    device: &'a mut SmoltcpDevice,
57}
58
59//--------------------------------------------------------------------------------------------------
60// Methods
61//--------------------------------------------------------------------------------------------------
62
63impl SmoltcpDevice {
64    /// Create a new device connected to the given shared state.
65    ///
66    /// `mtu` is the IP-level MTU (e.g. 1500). The Ethernet frame MTU reported
67    /// to smoltcp is `mtu + 14` (Ethernet header).
68    pub fn new(shared: Arc<SharedState>, mtu: usize) -> Self {
69        Self {
70            shared,
71            mtu,
72            pending_rx: None,
73            frames_emitted: AtomicBool::new(false),
74        }
75    }
76
77    /// Pop the next frame from `tx_ring` into the slot for inspection.
78    ///
79    /// Called by the poll loop **before** `iface.poll()`. Returns a reference
80    /// to the staged frame, or `None` if the queue is empty. Repeated calls
81    /// return the same frame until it is consumed or dropped.
82    pub fn stage_next_frame(&mut self) -> Option<&[u8]> {
83        if self.pending_rx.is_none() {
84            self.pending_rx = self.shared.tx_ring.pop();
85        }
86        self.pending_rx.as_deref()
87    }
88
89    /// Discard the staged frame without letting smoltcp process it.
90    ///
91    /// Used for frames handled outside smoltcp (e.g. non-DNS UDP relay).
92    pub fn drop_staged_frame(&mut self) {
93        self.pending_rx = None;
94    }
95}
96
97//--------------------------------------------------------------------------------------------------
98// Trait Implementations
99//--------------------------------------------------------------------------------------------------
100
101impl phy::Device for SmoltcpDevice {
102    type RxToken<'a> = SmoltcpRxToken;
103    type TxToken<'a> = SmoltcpTxToken<'a>;
104
105    fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
106        let frame = self.pending_rx.take()?;
107        Some((SmoltcpRxToken { frame }, SmoltcpTxToken { device: self }))
108    }
109
110    fn transmit(&mut self, _timestamp: Instant) -> Option<Self::TxToken<'_>> {
111        // Backpressure: if rx_ring is full the guest hasn't consumed frames
112        // yet. Return None so smoltcp retains data in socket buffers and
113        // retries later.
114        if self.shared.rx_ring.len() < self.shared.rx_ring.capacity() {
115            Some(SmoltcpTxToken { device: self })
116        } else {
117            None
118        }
119    }
120
121    fn capabilities(&self) -> DeviceCapabilities {
122        let mut caps = DeviceCapabilities::default();
123        caps.medium = Medium::Ethernet;
124        // smoltcp's max_transmission_unit for Ethernet is the full frame size
125        // including the 14-byte Ethernet header.
126        caps.max_transmission_unit = self.mtu + 14;
127        caps
128    }
129}
130
131impl phy::RxToken for SmoltcpRxToken {
132    fn consume<R, F>(self, f: F) -> R
133    where
134        F: FnOnce(&[u8]) -> R,
135    {
136        f(&self.frame)
137    }
138}
139
140impl<'a> phy::TxToken for SmoltcpTxToken<'a> {
141    fn consume<R, F>(self, len: usize, f: F) -> R
142    where
143        F: FnOnce(&mut [u8]) -> R,
144    {
145        let mut buf = vec![0u8; len];
146        let result = f(&mut buf);
147        // Push the frame to rx_ring for the guest. Don't wake yet —
148        // the poll loop will coalesce wakes after the egress loop.
149        self.device.shared.add_rx_bytes(buf.len());
150        let _ = self.device.shared.rx_ring.push(buf);
151        self.device.frames_emitted.store(true, Ordering::Relaxed);
152        result
153    }
154}