Files
nym/wasm/smolmix/src/device.rs
T
mfahampshire 43a1bd38e8 Max/smolmix wasm (#6784)
* Mod gitignore + license trimming + comment trimming

* Big rewrite

* SURB inputs + DNS button in internal-dev

* Make ipr addr optional

* Accidentatly omitted files from rewrite commit

* Makefile + readme

* Comment rewrite

* Optimisation comment

* Replace manual waker map with
      smoltcp built-ins + adaptive poll

* Comments

* Extract socket creation helpers into stream.rs

* Cleanup comments

* Comment

* Comment notes and restrict ciphersuites wrt rustls-rustcrypto

* Dep. hack fix for demo + add clearnet fetch() for contrast

* Stripped down devtester

* Fix Clippy arg (fatfingered deletion)

* CodeRabbit catches

* Cargofmt

* Review nits: bridge logs, fetch early-return, static port counter, copyright years, README + Cargo + headless.js tidying

* PHONY + taskset override, switch internal-dev/tests to pnpm, fix wasm-pack out-dir

* Gate codec tests behind the codec feature for no-default-features builds

* IPv6 addr/route on smoltcp iface + configurable DNS resolvers via TunnelOpts

* DNS GUI inputs, close stale WS on reconnect, worker init guards + ws-send warning, Playwright listener cleanup, pnpm-lock in internal-dev

* Fix lp -> lp-data after rebase

* Revert nym-lp/nym-lp-data feature-gating left over from rebase

* Lift getrandom wasm_js cfg to workspace .cargo/config.toml so cargo check -p smolmix-wasm works from any CWD

* temp will amend git message

* Auto-discover IPR when none specified + 'Use random IPR' checkbox in internal-dev

* smolmix_tracker + State machine + ready_tunnel gate + getTunnelState JS surface

* Mirror red display() entries to console.error

* Add left out package-lock

* Reactor clock + yield_now + atomic seq + gateway-storage errors

* setupMixTunnel gate + MTU 1980 + http::Uri cleanup

* Review pass + fix test + clippy

* restore axum 0.8 bump from borked earlier merge

* Feature gating (dns/fetch/socket) + TunnelOptsBuilder + pnpm bypass

* Cont. with review comments

* tokio Nofity reactor wakes + cancellation + setup polishing

* Notify wakes + inner pattern + close_notify + util

* Tunable tunnelopts

* Fix tired commit

* CI prep

* Lint + Clippy

* coderabbit u32 fix

* nits + runtime debugging + expose in internal-dev

* remove redudant default-features

* Remove more redundant default-features
2026-05-28 15:57:10 +00:00

163 lines
5.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! smoltcp `Device` implementation backed by in-memory VecDeque buffers.
//!
//! The native smolmix uses `NymAsyncDevice` (Stream/Sink over mpsc channels) fed
//! into `tokio-smoltcp`. On wasm32 we drive smoltcp directly, so we need a sync
//! `Device` impl instead. The bridge pushes incoming IP packets into `rx_queue`
//! and pops outgoing packets from `tx_queue`.
use std::collections::VecDeque;
use smoltcp::phy::{Device, DeviceCapabilities, Medium, RxToken, TxToken};
use smoltcp::time::Instant;
/// smoltcp device backed by in-memory packet queues.
///
/// The bridge task feeds incoming IP packets via [`push_rx`](Self::push_rx) and
/// drains outgoing packets via [`drain_tx`](Self::drain_tx). The reactor calls
/// smoltcp's `Interface::poll()` which invokes the `Device` trait methods.
pub struct WasmDevice {
rx_queue: VecDeque<Vec<u8>>,
tx_queue: VecDeque<Vec<u8>>,
capabilities: DeviceCapabilities,
}
impl WasmDevice {
pub fn new() -> Self {
let mut capabilities = DeviceCapabilities::default();
capabilities.medium = Medium::Ip;
// Sized so one IP packet fits in one sphinx packet payload (no
// chunking-layer fragmentation). Budget in bytes from the 2048 B
// sphinx plaintext: 344 (SURB-ack) 32 (x25519 ephemeral key,
// Repliable msgs) 7 (frag header) 1 (padding) 53 (LP+IPR
// framing + AEAD) ≈ 1611. 1600 leaves ~11 B headroom for IPR
// overhead variability.
capabilities.max_transmission_unit = 1600;
// Native smolmix also uses Some(1) in the device, but tokio-smoltcp
// compensates with a burst loop that calls Interface::poll() up to 100
// times per reactor iteration (each processing 1 packet). Our WASM
// reactor only calls poll() once per tick, so we set a higher burst
// size to let smoltcp drain all pending packets in a single poll().
capabilities.max_burst_size = Some(100);
Self {
rx_queue: VecDeque::new(),
tx_queue: VecDeque::new(),
capabilities,
}
}
/// Push an incoming IP packet (from the mixnet) into the receive queue.
pub fn push_rx(&mut self, packet: Vec<u8>) {
self.rx_queue.push_back(packet);
}
/// Drain all outgoing IP packets (generated by smoltcp) from the transmit queue.
pub fn drain_tx(&mut self) -> impl Iterator<Item = Vec<u8>> + '_ {
self.tx_queue.drain(..)
}
}
impl Device for WasmDevice {
type RxToken<'a> = WasmRxToken;
type TxToken<'a> = WasmTxToken<'a>;
fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
let packet = self.rx_queue.pop_front()?;
Some((
WasmRxToken { buffer: packet },
WasmTxToken {
queue: &mut self.tx_queue,
},
))
}
fn transmit(&mut self, _timestamp: Instant) -> Option<Self::TxToken<'_>> {
Some(WasmTxToken {
queue: &mut self.tx_queue,
})
}
fn capabilities(&self) -> DeviceCapabilities {
self.capabilities.clone()
}
}
/// Receive token: delivers one packet from the rx queue to smoltcp.
pub struct WasmRxToken {
buffer: Vec<u8>,
}
impl RxToken for WasmRxToken {
fn consume<R, F>(self, f: F) -> R
where
F: FnOnce(&[u8]) -> R,
{
f(&self.buffer)
}
}
/// Transmit token: captures one packet from smoltcp into the tx queue.
pub struct WasmTxToken<'a> {
queue: &'a mut VecDeque<Vec<u8>>,
}
impl<'a> TxToken for WasmTxToken<'a> {
fn consume<R, F>(self, len: usize, f: F) -> R
where
F: FnOnce(&mut [u8]) -> R,
{
let mut buffer = vec![0u8; len];
let result = f(&mut buffer);
self.queue.push_back(buffer);
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_rx_and_receive() {
let mut dev = WasmDevice::new();
dev.push_rx(vec![1, 2, 3]);
let now = Instant::from_millis(0);
let (rx, _tx) = dev.receive(now).expect("should have a packet");
let data = rx.consume(|buf| buf.to_vec());
assert_eq!(data, vec![1, 2, 3]);
}
#[test]
fn transmit_and_drain() {
let mut dev = WasmDevice::new();
let now = Instant::from_millis(0);
let tx = dev.transmit(now).expect("should get tx token");
tx.consume(4, |buf| {
buf.copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
});
let packets: Vec<_> = dev.drain_tx().collect();
assert_eq!(packets.len(), 1);
assert_eq!(packets[0], vec![0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn empty_receive_returns_none() {
let mut dev = WasmDevice::new();
assert!(dev.receive(Instant::from_millis(0)).is_none());
}
#[test]
fn capabilities_are_ip_mode() {
let dev = WasmDevice::new();
let caps = dev.capabilities();
assert_eq!(caps.medium, Medium::Ip);
assert_eq!(caps.max_transmission_unit, 1980);
}
}