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}