Files
fips/src/transport/loopback.rs
T
Johnathan Corgan de327e4527 test: run node-level mesh tests over an in-process loopback transport
Add a Loopback variant to TransportHandle backed by an unbounded
in-process channel and a shared address-to-receiver registry, so
node-level multi-node tests deliver packets directly between nodes
instead of over real localhost UDP sockets. This removes the kernel
UDP receive-buffer overflow that dropped handshake packets when many
tests ran in parallel under CPU contention, and lets the large-network
convergence tests run reliably in the default suite again (their
parallel-load ignore markers are removed).

The new transport and its enum variant are cfg(test)-gated, so the
daemon build is unaffected.
2026-06-05 03:32:57 +00:00

175 lines
5.6 KiB
Rust

//! In-process loopback transport (test harness only).
//!
//! Delivers packets directly between nodes running in the same process via
//! an unbounded in-process channel and a shared address-to-receiver
//! registry, instead of going over real localhost UDP sockets. This is used
//! by node-level multi-node tests to avoid the kernel UDP receive-buffer
//! overflow that drops handshake packets when many tests run in parallel
//! under CPU contention.
//!
//! An UNBOUNDED channel is used deliberately: the test harness drains
//! packets sequentially (it fires the whole handshake burst before draining,
//! with no background reader), so a bounded awaiting send would deadlock and
//! a bounded try_send would drop. `UnboundedSender::send` is synchronous,
//! never blocks, and never drops — it only errors if the receiver is gone —
//! making delivery provably lossless and deadlock-free.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc::UnboundedSender;
use super::{
DiscoveredPeer, ReceivedPacket, Transport, TransportAddr, TransportError, TransportId,
TransportState, TransportType,
};
/// Shared registry mapping each loopback address to the receiver-side
/// channel sender for the node listening on that address.
///
/// One registry instance is shared by all loopback transports in a given
/// test run so they can locate each other by address.
pub type LoopbackRegistry = Arc<Mutex<HashMap<TransportAddr, UnboundedSender<ReceivedPacket>>>>;
/// Create a fresh, empty loopback registry.
pub fn new_registry() -> LoopbackRegistry {
Arc::new(Mutex::new(HashMap::new()))
}
/// Default loopback MTU, mirroring the UDP test path.
const DEFAULT_LOOPBACK_MTU: u16 = 1280;
/// In-process loopback transport.
pub struct LoopbackTransport {
transport_id: TransportId,
/// This transport's synthetic unique address (e.g. "loopback:7").
my_addr: TransportAddr,
/// Transport MTU. Enforced on send to mirror UDP's MtuExceeded behavior,
/// so PMTUD/heterogeneous-MTU tests still exercise the forward-path
/// bottleneck detection.
mtu: u16,
/// Shared address-to-receiver registry.
registry: LoopbackRegistry,
}
impl LoopbackTransport {
/// Create a new loopback transport bound to `my_addr` with the default
/// MTU, sharing `registry`.
pub fn new(
transport_id: TransportId,
my_addr: TransportAddr,
registry: LoopbackRegistry,
) -> Self {
Self::with_mtu(transport_id, my_addr, DEFAULT_LOOPBACK_MTU, registry)
}
/// Create a new loopback transport with an explicit MTU.
pub fn with_mtu(
transport_id: TransportId,
my_addr: TransportAddr,
mtu: u16,
registry: LoopbackRegistry,
) -> Self {
Self {
transport_id,
my_addr,
mtu,
registry,
}
}
/// This transport's synthetic loopback address.
pub fn my_addr(&self) -> &TransportAddr {
&self.my_addr
}
/// Send data to a destination loopback address.
///
/// Looks up `dest_addr` in the shared registry and, if found, delivers a
/// `ReceivedPacket` to its receiver. The packet's `remote_addr` is set to
/// the sender's own address (`my_addr`), mirroring how UDP sets
/// `remote_addr` from the datagram source, so the receiver's handlers
/// learn the peer source.
pub async fn send_async(
&self,
dest_addr: &TransportAddr,
data: &[u8],
) -> Result<usize, TransportError> {
if data.len() > self.mtu as usize {
return Err(TransportError::MtuExceeded {
packet_size: data.len(),
mtu: self.mtu,
});
}
let dest_tx = {
let registry = self.registry.lock().map_err(|e| {
TransportError::SendFailed(format!("registry lock poisoned: {}", e))
})?;
registry.get(dest_addr).cloned()
};
match dest_tx {
Some(tx) => {
let packet =
ReceivedPacket::new(self.transport_id, self.my_addr.clone(), data.to_vec());
tx.send(packet).map_err(|_| {
TransportError::SendFailed(format!("loopback receiver gone for {}", dest_addr))
})?;
Ok(data.len())
}
None => Err(TransportError::SendFailed(format!(
"no loopback route to {}",
dest_addr
))),
}
}
/// Asynchronous start (no-op; the transport is ready on construction).
pub async fn start_async(&mut self) -> Result<(), TransportError> {
Ok(())
}
/// Asynchronous stop (no-op).
pub async fn stop_async(&mut self) -> Result<(), TransportError> {
Ok(())
}
}
impl Transport for LoopbackTransport {
fn transport_id(&self) -> TransportId {
self.transport_id
}
fn transport_type(&self) -> &TransportType {
&TransportType::LOOPBACK
}
fn state(&self) -> TransportState {
TransportState::Up
}
fn mtu(&self) -> u16 {
self.mtu
}
fn start(&mut self) -> Result<(), TransportError> {
Ok(())
}
fn stop(&mut self) -> Result<(), TransportError> {
Ok(())
}
fn send(&self, _addr: &TransportAddr, _data: &[u8]) -> Result<(), TransportError> {
// Synchronous send not supported — use send_async().
Err(TransportError::NotSupported(
"use send_async() for loopback transport".into(),
))
}
fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
Ok(Vec::new())
}
}