Files
fips/src/transport/mod.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

1709 lines
53 KiB
Rust

//! Transport Layer Abstractions
//!
//! Traits and types for FIPS transport drivers. Transports provide the
//! underlying communication mechanisms (UDP, Ethernet, Tor, etc.) over
//! which FIPS links are established.
#[cfg(test)]
pub mod loopback;
pub mod tcp;
pub mod tor;
pub mod udp;
#[cfg(unix)]
pub mod ethernet;
#[cfg(target_os = "linux")]
pub mod ble;
#[cfg(target_os = "linux")]
use ble::DefaultBleTransport;
#[cfg(unix)]
use ethernet::EthernetTransport;
#[cfg(test)]
use loopback::LoopbackTransport;
use secp256k1::XOnlyPublicKey;
use std::fmt;
use std::net::SocketAddr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tcp::TcpTransport;
use thiserror::Error;
use tor::TorTransport;
use tor::control::TorMonitoringInfo;
use udp::UdpTransport;
// ============================================================================
// Packet Channel Types
// ============================================================================
/// A packet received from a transport.
#[derive(Clone, Debug)]
pub struct ReceivedPacket {
/// Which transport received this packet.
pub transport_id: TransportId,
/// Remote peer address.
pub remote_addr: TransportAddr,
/// Packet data.
pub data: Vec<u8>,
/// Receipt timestamp (Unix milliseconds).
pub timestamp_ms: u64,
}
impl ReceivedPacket {
/// Create a new received packet with current timestamp.
pub fn new(transport_id: TransportId, remote_addr: TransportAddr, data: Vec<u8>) -> Self {
let timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
Self {
transport_id,
remote_addr,
data,
timestamp_ms,
}
}
/// Create a received packet with explicit timestamp.
pub fn with_timestamp(
transport_id: TransportId,
remote_addr: TransportAddr,
data: Vec<u8>,
timestamp_ms: u64,
) -> Self {
Self {
transport_id,
remote_addr,
data,
timestamp_ms,
}
}
}
/// Channel sender for received packets.
pub type PacketTx = tokio::sync::mpsc::Sender<ReceivedPacket>;
/// Channel receiver for received packets.
pub type PacketRx = tokio::sync::mpsc::Receiver<ReceivedPacket>;
/// Create a packet channel with the given buffer size.
pub fn packet_channel(buffer: usize) -> (PacketTx, PacketRx) {
tokio::sync::mpsc::channel(buffer)
}
// ============================================================================
// Transport Identifiers
// ============================================================================
/// Unique identifier for a transport instance.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TransportId(u32);
impl TransportId {
/// Create a new transport ID.
pub fn new(id: u32) -> Self {
Self(id)
}
/// Get the raw ID value.
pub fn as_u32(&self) -> u32 {
self.0
}
}
impl fmt::Display for TransportId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "transport:{}", self.0)
}
}
/// Unique identifier for a link instance.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct LinkId(u64);
impl LinkId {
/// Create a new link ID.
pub fn new(id: u64) -> Self {
Self(id)
}
/// Get the raw ID value.
pub fn as_u64(&self) -> u64 {
self.0
}
}
impl fmt::Display for LinkId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "link:{}", self.0)
}
}
// ============================================================================
// Errors
// ============================================================================
/// Errors related to transport operations.
#[derive(Debug, Error)]
pub enum TransportError {
#[error("transport not started")]
NotStarted,
#[error("transport already started")]
AlreadyStarted,
#[error("transport failed to start: {0}")]
StartFailed(String),
#[error("transport shutdown failed: {0}")]
ShutdownFailed(String),
#[error("link failed: {0}")]
LinkFailed(String),
#[error("send failed: {0}")]
SendFailed(String),
#[error("receive failed: {0}")]
RecvFailed(String),
#[error("invalid transport address: {0}")]
InvalidAddress(String),
#[error("mtu exceeded: packet {packet_size} > mtu {mtu}")]
MtuExceeded { packet_size: usize, mtu: u16 },
#[error("transport timeout")]
Timeout,
#[error("connection refused")]
ConnectionRefused,
#[error("transport not supported: {0}")]
NotSupported(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
// ============================================================================
// Transport Type Metadata
// ============================================================================
/// Static metadata about a transport type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TransportType {
/// Human-readable name (e.g., "udp", "ethernet", "tor").
pub name: &'static str,
/// Whether this transport requires connection establishment.
pub connection_oriented: bool,
/// Whether the transport guarantees delivery.
pub reliable: bool,
}
impl TransportType {
/// UDP/IP transport.
pub const UDP: TransportType = TransportType {
name: "udp",
connection_oriented: false,
reliable: false,
};
/// TCP/IP transport.
pub const TCP: TransportType = TransportType {
name: "tcp",
connection_oriented: true,
reliable: true,
};
/// Raw Ethernet transport.
pub const ETHERNET: TransportType = TransportType {
name: "ethernet",
connection_oriented: false,
reliable: false,
};
/// WiFi (same characteristics as Ethernet).
pub const WIFI: TransportType = TransportType {
name: "wifi",
connection_oriented: false,
reliable: false,
};
/// Tor onion transport.
pub const TOR: TransportType = TransportType {
name: "tor",
connection_oriented: true,
reliable: true,
};
/// Serial/UART transport.
pub const SERIAL: TransportType = TransportType {
name: "serial",
connection_oriented: false,
reliable: true, // typically uses framing with checksums
};
/// BLE L2CAP CoC transport.
pub const BLE: TransportType = TransportType {
name: "ble",
connection_oriented: true,
reliable: true, // L2CAP SeqPacket guarantees delivery
};
/// In-process loopback transport (test harness only).
#[cfg(test)]
pub const LOOPBACK: TransportType = TransportType {
name: "loopback",
connection_oriented: false,
reliable: true, // in-process channel delivery is lossless
};
/// Check if the transport is connectionless.
pub fn is_connectionless(&self) -> bool {
!self.connection_oriented
}
}
impl fmt::Display for TransportType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
}
// ============================================================================
// Transport State
// ============================================================================
/// Transport lifecycle state.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TransportState {
/// Configured but not started.
Configured,
/// Initialization in progress.
Starting,
/// Ready for links.
Up,
/// Was up, now unavailable.
Down,
/// Failed to start.
Failed,
}
impl TransportState {
/// Check if the transport is operational.
pub fn is_operational(&self) -> bool {
matches!(self, TransportState::Up)
}
/// Check if the transport can be started.
pub fn can_start(&self) -> bool {
matches!(
self,
TransportState::Configured | TransportState::Down | TransportState::Failed
)
}
/// Check if the transport is in a terminal state.
pub fn is_terminal(&self) -> bool {
matches!(self, TransportState::Failed)
}
}
impl fmt::Display for TransportState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
TransportState::Configured => "configured",
TransportState::Starting => "starting",
TransportState::Up => "up",
TransportState::Down => "down",
TransportState::Failed => "failed",
};
write!(f, "{}", s)
}
}
// ============================================================================
// Link State
// ============================================================================
/// Link lifecycle state.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinkState {
/// Connection in progress (connection-oriented only).
Connecting,
/// Ready for traffic.
Connected,
/// Was connected, now gone.
Disconnected,
/// Connection attempt failed.
Failed,
}
impl LinkState {
/// Check if the link is operational.
pub fn is_operational(&self) -> bool {
matches!(self, LinkState::Connected)
}
/// Check if the link is in a terminal state.
pub fn is_terminal(&self) -> bool {
matches!(self, LinkState::Disconnected | LinkState::Failed)
}
}
impl fmt::Display for LinkState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
LinkState::Connecting => "connecting",
LinkState::Connected => "connected",
LinkState::Disconnected => "disconnected",
LinkState::Failed => "failed",
};
write!(f, "{}", s)
}
}
/// Direction of link establishment.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinkDirection {
/// We initiated the connection.
Outbound,
/// They initiated the connection.
Inbound,
}
impl fmt::Display for LinkDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
LinkDirection::Outbound => "outbound",
LinkDirection::Inbound => "inbound",
};
write!(f, "{}", s)
}
}
// ============================================================================
// Transport Address
// ============================================================================
/// Opaque transport-specific address.
///
/// Each transport type interprets this differently:
/// - UDP/TCP: "host:port" (IP address or DNS hostname)
/// - Ethernet: MAC address (6 bytes)
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct TransportAddr(Vec<u8>);
impl TransportAddr {
/// Create a transport address from raw bytes.
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
/// Create a transport address from a byte slice.
pub fn from_bytes(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
/// Create a transport address from a string.
pub fn from_string(s: &str) -> Self {
Self(s.as_bytes().to_vec())
}
/// Get the raw bytes.
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
/// Try to interpret as a UTF-8 string.
pub fn as_str(&self) -> Option<&str> {
std::str::from_utf8(&self.0).ok()
}
/// Get the length in bytes.
pub fn len(&self) -> usize {
self.0.len()
}
/// Check if empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Debug for TransportAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.as_str() {
Some(s) => write!(f, "TransportAddr(\"{}\")", s),
None => write!(f, "TransportAddr({:?})", self.0),
}
}
}
impl fmt::Display for TransportAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Best-effort display as string if valid UTF-8, else hex
match self.as_str() {
Some(s) => write!(f, "{}", s),
None => {
for byte in &self.0 {
write!(f, "{:02x}", byte)?;
}
Ok(())
}
}
}
}
impl From<&str> for TransportAddr {
fn from(s: &str) -> Self {
Self::from_string(s)
}
}
impl From<String> for TransportAddr {
fn from(s: String) -> Self {
Self(s.into_bytes())
}
}
// ============================================================================
// Link Statistics
// ============================================================================
/// Statistics for a link.
#[derive(Clone, Debug, Default)]
pub struct LinkStats {
/// Total packets sent.
pub packets_sent: u64,
/// Total packets received.
pub packets_recv: u64,
/// Total bytes sent.
pub bytes_sent: u64,
/// Total bytes received.
pub bytes_recv: u64,
/// Timestamp of last received packet (Unix milliseconds).
pub last_recv_ms: u64,
/// Estimated round-trip time.
rtt_estimate: Option<Duration>,
/// Observed packet loss rate (0.0-1.0).
pub loss_rate: f32,
/// Estimated throughput in bytes/second.
pub throughput_estimate: u64,
}
impl LinkStats {
/// Create new link statistics.
pub fn new() -> Self {
Self::default()
}
/// Record a sent packet.
pub fn record_sent(&mut self, bytes: usize) {
self.packets_sent += 1;
self.bytes_sent += bytes as u64;
}
/// Record a received packet.
pub fn record_recv(&mut self, bytes: usize, timestamp_ms: u64) {
self.packets_recv += 1;
self.bytes_recv += bytes as u64;
self.last_recv_ms = timestamp_ms;
}
/// Get the RTT estimate, if available.
pub fn rtt_estimate(&self) -> Option<Duration> {
self.rtt_estimate
}
/// Update RTT estimate from a probe response.
///
/// Uses exponential moving average with alpha=0.2.
pub fn update_rtt(&mut self, rtt: Duration) {
match self.rtt_estimate {
Some(old_rtt) => {
let alpha = 0.2;
let new_rtt_nanos = (alpha * rtt.as_nanos() as f64
+ (1.0 - alpha) * old_rtt.as_nanos() as f64)
as u64;
self.rtt_estimate = Some(Duration::from_nanos(new_rtt_nanos));
}
None => {
self.rtt_estimate = Some(rtt);
}
}
}
/// Time since last receive (for keepalive/timeout).
pub fn time_since_recv(&self, current_time_ms: u64) -> u64 {
if self.last_recv_ms == 0 {
return u64::MAX;
}
current_time_ms.saturating_sub(self.last_recv_ms)
}
/// Reset all statistics.
pub fn reset(&mut self) {
*self = Self::default();
}
}
// ============================================================================
// Link
// ============================================================================
/// A link to a remote endpoint over a transport.
#[derive(Clone, Debug)]
pub struct Link {
/// Unique link identifier.
link_id: LinkId,
/// Which transport this link uses.
transport_id: TransportId,
/// Transport-specific remote address.
remote_addr: TransportAddr,
/// Whether we initiated or they initiated.
direction: LinkDirection,
/// Current link state.
state: LinkState,
/// Base RTT hint from transport type.
base_rtt: Duration,
/// Measured statistics.
stats: LinkStats,
/// When this link was created (Unix milliseconds).
created_at: u64,
}
impl Link {
/// Create a new link in Connecting state.
pub fn new(
link_id: LinkId,
transport_id: TransportId,
remote_addr: TransportAddr,
direction: LinkDirection,
base_rtt: Duration,
) -> Self {
Self {
link_id,
transport_id,
remote_addr,
direction,
state: LinkState::Connecting,
base_rtt,
stats: LinkStats::new(),
created_at: 0,
}
}
/// Create a link with a creation timestamp.
pub fn new_with_timestamp(
link_id: LinkId,
transport_id: TransportId,
remote_addr: TransportAddr,
direction: LinkDirection,
base_rtt: Duration,
created_at: u64,
) -> Self {
let mut link = Self::new(link_id, transport_id, remote_addr, direction, base_rtt);
link.created_at = created_at;
link
}
/// Create a connectionless link (immediately connected).
///
/// For connectionless transports (UDP, Ethernet), links are immediately
/// in the Connected state.
pub fn connectionless(
link_id: LinkId,
transport_id: TransportId,
remote_addr: TransportAddr,
direction: LinkDirection,
base_rtt: Duration,
) -> Self {
let mut link = Self::new(link_id, transport_id, remote_addr, direction, base_rtt);
link.state = LinkState::Connected;
link
}
/// Get the link ID.
pub fn link_id(&self) -> LinkId {
self.link_id
}
/// Get the transport ID.
pub fn transport_id(&self) -> TransportId {
self.transport_id
}
/// Get the remote address.
pub fn remote_addr(&self) -> &TransportAddr {
&self.remote_addr
}
/// Get the link direction.
pub fn direction(&self) -> LinkDirection {
self.direction
}
/// Get the current state.
pub fn state(&self) -> LinkState {
self.state
}
/// Get the base RTT hint.
pub fn base_rtt(&self) -> Duration {
self.base_rtt
}
/// Get the link statistics.
pub fn stats(&self) -> &LinkStats {
&self.stats
}
/// Get mutable access to link statistics.
pub fn stats_mut(&mut self) -> &mut LinkStats {
&mut self.stats
}
/// Get the creation timestamp.
pub fn created_at(&self) -> u64 {
self.created_at
}
/// Set the creation timestamp.
pub fn set_created_at(&mut self, timestamp: u64) {
self.created_at = timestamp;
}
/// Mark the link as connected.
pub fn set_connected(&mut self) {
self.state = LinkState::Connected;
}
/// Mark the link as disconnected.
pub fn set_disconnected(&mut self) {
self.state = LinkState::Disconnected;
}
/// Mark the link as failed.
pub fn set_failed(&mut self) {
self.state = LinkState::Failed;
}
/// Check if this link is operational.
pub fn is_operational(&self) -> bool {
self.state.is_operational()
}
/// Check if this link is in a terminal state.
pub fn is_terminal(&self) -> bool {
self.state.is_terminal()
}
/// Get effective RTT (measured if available, else base hint).
pub fn effective_rtt(&self) -> Duration {
self.stats.rtt_estimate().unwrap_or(self.base_rtt)
}
/// Age of the link in milliseconds.
pub fn age(&self, current_time_ms: u64) -> u64 {
if self.created_at == 0 {
return 0;
}
current_time_ms.saturating_sub(self.created_at)
}
}
// ============================================================================
// Discovered Peer
// ============================================================================
/// A peer discovered via transport-layer discovery.
#[derive(Clone, Debug)]
pub struct DiscoveredPeer {
/// Transport that discovered this peer.
pub transport_id: TransportId,
/// Transport address where the peer was found.
pub addr: TransportAddr,
/// Optional hint about the peer's identity (if known from discovery).
pub pubkey_hint: Option<XOnlyPublicKey>,
}
impl DiscoveredPeer {
/// Create a discovered peer without identity hint.
pub fn new(transport_id: TransportId, addr: TransportAddr) -> Self {
Self {
transport_id,
addr,
pubkey_hint: None,
}
}
/// Create a discovered peer with identity hint.
pub fn with_hint(
transport_id: TransportId,
addr: TransportAddr,
pubkey: XOnlyPublicKey,
) -> Self {
Self {
transport_id,
addr,
pubkey_hint: Some(pubkey),
}
}
}
// ============================================================================
// Transport Trait
// ============================================================================
/// Transport trait defining the interface for transport drivers.
///
/// This is a simplified synchronous trait. Actual implementations would
/// be async and use channels for event delivery.
pub trait Transport {
/// Get the transport identifier.
fn transport_id(&self) -> TransportId;
/// Get the transport type metadata.
fn transport_type(&self) -> &TransportType;
/// Get the current state.
fn state(&self) -> TransportState;
/// Get the MTU for this transport.
fn mtu(&self) -> u16;
/// Get the MTU for a specific link.
///
/// Returns the MTU negotiated for the given transport address, or
/// falls back to the transport-wide default if the address is unknown
/// or the transport doesn't support per-link MTU negotiation.
fn link_mtu(&self, addr: &TransportAddr) -> u16 {
let _ = addr;
self.mtu()
}
/// Start the transport.
fn start(&mut self) -> Result<(), TransportError>;
/// Stop the transport.
fn stop(&mut self) -> Result<(), TransportError>;
/// Send data to a transport address.
fn send(&self, addr: &TransportAddr, data: &[u8]) -> Result<(), TransportError>;
/// Discover potential peers (if supported).
fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError>;
/// Whether to auto-connect to peers returned by discover().
/// Default: false. Concrete transports read from their own config.
fn auto_connect(&self) -> bool {
false
}
/// Whether to accept inbound handshake initiations on this transport.
/// Default: true (preserves UDP's current implicit behavior).
fn accept_connections(&self) -> bool {
true
}
/// Close a specific connection (connection-oriented transports only).
///
/// For connectionless transports (UDP, Ethernet), this is a no-op.
/// Connection-oriented transports (TCP, Tor) remove the connection
/// from their pool and drop the underlying stream.
fn close_connection(&self, _addr: &TransportAddr) {
// Default no-op for connectionless transports
}
}
// ============================================================================
// Connection State (for non-blocking connect)
// ============================================================================
/// State of a transport-level connection attempt.
///
/// Used by connection-oriented transports (TCP, Tor) to report the progress
/// of a background connection attempt initiated by `connect()`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ConnectionState {
/// No connection attempt in progress for this address.
None,
/// Connection attempt is in progress (background task running).
Connecting,
/// Connection is established and ready for send().
Connected,
/// Connection attempt failed with the given error message.
Failed(String),
}
// ============================================================================
// Transport Congestion
// ============================================================================
/// Transport-local congestion indicators.
///
/// All fields are optional — transports report what they can.
/// Consumers compute deltas from cumulative counters.
#[derive(Clone, Debug, Default)]
pub struct TransportCongestion {
/// Cumulative packets dropped by kernel/OS before reaching the application.
/// Monotonically increasing since transport start.
pub recv_drops: Option<u64>,
}
// ============================================================================
// Transport Handle
// ============================================================================
/// Wrapper enum for concrete transport implementations.
///
/// This enables polymorphic transport handling without trait objects,
/// supporting async methods that the sync Transport trait cannot express.
pub enum TransportHandle {
/// UDP/IP transport.
Udp(UdpTransport),
/// Raw Ethernet transport.
#[cfg(unix)]
Ethernet(EthernetTransport),
/// TCP/IP transport.
Tcp(TcpTransport),
/// Tor transport (via SOCKS5).
Tor(TorTransport),
/// BLE L2CAP transport.
#[cfg(target_os = "linux")]
Ble(DefaultBleTransport),
/// In-process loopback transport (test harness only).
#[cfg(test)]
Loopback(LoopbackTransport),
}
impl TransportHandle {
/// Start the transport asynchronously.
pub async fn start(&mut self) -> Result<(), TransportError> {
match self {
TransportHandle::Udp(t) => t.start_async().await,
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.start_async().await,
TransportHandle::Tcp(t) => t.start_async().await,
TransportHandle::Tor(t) => t.start_async().await,
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.start_async().await,
#[cfg(test)]
TransportHandle::Loopback(t) => t.start_async().await,
}
}
/// Stop the transport asynchronously.
pub async fn stop(&mut self) -> Result<(), TransportError> {
match self {
TransportHandle::Udp(t) => t.stop_async().await,
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.stop_async().await,
TransportHandle::Tcp(t) => t.stop_async().await,
TransportHandle::Tor(t) => t.stop_async().await,
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.stop_async().await,
#[cfg(test)]
TransportHandle::Loopback(t) => t.stop_async().await,
}
}
/// Send data to a remote address asynchronously.
pub async fn send(&self, addr: &TransportAddr, data: &[u8]) -> Result<usize, TransportError> {
match self {
TransportHandle::Udp(t) => t.send_async(addr, data).await,
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.send_async(addr, data).await,
TransportHandle::Tcp(t) => t.send_async(addr, data).await,
TransportHandle::Tor(t) => t.send_async(addr, data).await,
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.send_async(addr, data).await,
#[cfg(test)]
TransportHandle::Loopback(t) => t.send_async(addr, data).await,
}
}
/// Get the transport ID.
pub fn transport_id(&self) -> TransportId {
match self {
TransportHandle::Udp(t) => t.transport_id(),
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.transport_id(),
TransportHandle::Tcp(t) => t.transport_id(),
TransportHandle::Tor(t) => t.transport_id(),
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.transport_id(),
#[cfg(test)]
TransportHandle::Loopback(t) => t.transport_id(),
}
}
/// Get the instance name (if configured as a named instance).
pub fn name(&self) -> Option<&str> {
match self {
TransportHandle::Udp(t) => t.name(),
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.name(),
TransportHandle::Tcp(t) => t.name(),
TransportHandle::Tor(t) => t.name(),
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.name(),
#[cfg(test)]
TransportHandle::Loopback(_) => None,
}
}
/// Get the transport type metadata.
pub fn transport_type(&self) -> &TransportType {
match self {
TransportHandle::Udp(t) => t.transport_type(),
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.transport_type(),
TransportHandle::Tcp(t) => t.transport_type(),
TransportHandle::Tor(t) => t.transport_type(),
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.transport_type(),
#[cfg(test)]
TransportHandle::Loopback(t) => t.transport_type(),
}
}
/// Get current transport state.
pub fn state(&self) -> TransportState {
match self {
TransportHandle::Udp(t) => t.state(),
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.state(),
TransportHandle::Tcp(t) => t.state(),
TransportHandle::Tor(t) => t.state(),
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.state(),
#[cfg(test)]
TransportHandle::Loopback(t) => t.state(),
}
}
/// Get the transport MTU.
pub fn mtu(&self) -> u16 {
match self {
TransportHandle::Udp(t) => t.mtu(),
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.mtu(),
TransportHandle::Tcp(t) => t.mtu(),
TransportHandle::Tor(t) => t.mtu(),
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.mtu(),
#[cfg(test)]
TransportHandle::Loopback(t) => t.mtu(),
}
}
/// Get the MTU for a specific link address.
///
/// Falls back to transport-wide MTU if the transport doesn't
/// support per-link MTU or the address is unknown.
pub fn link_mtu(&self, addr: &TransportAddr) -> u16 {
match self {
TransportHandle::Udp(t) => t.link_mtu(addr),
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.link_mtu(addr),
TransportHandle::Tcp(t) => t.link_mtu(addr),
TransportHandle::Tor(t) => t.link_mtu(addr),
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.link_mtu(addr),
#[cfg(test)]
TransportHandle::Loopback(t) => t.link_mtu(addr),
}
}
/// Get the local bound address (UDP/TCP only, returns None for other transports).
pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
match self {
TransportHandle::Udp(t) => t.local_addr(),
#[cfg(unix)]
TransportHandle::Ethernet(_) => None,
TransportHandle::Tcp(t) => t.local_addr(),
TransportHandle::Tor(_) => None,
#[cfg(target_os = "linux")]
TransportHandle::Ble(_) => None,
#[cfg(test)]
TransportHandle::Loopback(_) => None,
}
}
/// Get the interface name (Ethernet only, returns None for other transports).
pub fn interface_name(&self) -> Option<&str> {
match self {
TransportHandle::Udp(_) => None,
#[cfg(unix)]
TransportHandle::Ethernet(t) => Some(t.interface_name()),
TransportHandle::Tcp(_) => None,
TransportHandle::Tor(_) => None,
#[cfg(target_os = "linux")]
TransportHandle::Ble(_) => None,
#[cfg(test)]
TransportHandle::Loopback(_) => None,
}
}
/// Get the onion service address (Tor only, returns None for other transports).
pub fn onion_address(&self) -> Option<&str> {
match self {
TransportHandle::Tor(t) => t.onion_address(),
_ => None,
}
}
/// Get cached Tor daemon monitoring info (Tor only).
pub fn tor_monitoring(&self) -> Option<TorMonitoringInfo> {
match self {
TransportHandle::Tor(t) => t.cached_monitoring(),
_ => None,
}
}
/// Get the Tor transport mode (Tor only).
pub fn tor_mode(&self) -> Option<&str> {
match self {
TransportHandle::Tor(t) => Some(t.mode()),
_ => None,
}
}
/// Drain discovered peers from this transport.
pub fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
match self {
TransportHandle::Udp(t) => t.discover(),
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.discover(),
TransportHandle::Tcp(t) => t.discover(),
TransportHandle::Tor(t) => t.discover(),
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.discover(),
#[cfg(test)]
TransportHandle::Loopback(t) => t.discover(),
}
}
/// Whether this transport auto-connects to discovered peers.
pub fn auto_connect(&self) -> bool {
match self {
TransportHandle::Udp(t) => t.auto_connect(),
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.auto_connect(),
TransportHandle::Tcp(t) => t.auto_connect(),
TransportHandle::Tor(t) => t.auto_connect(),
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.auto_connect(),
#[cfg(test)]
TransportHandle::Loopback(t) => t.auto_connect(),
}
}
/// Whether this transport accepts inbound connections.
pub fn accept_connections(&self) -> bool {
match self {
TransportHandle::Udp(t) => t.accept_connections(),
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.accept_connections(),
TransportHandle::Tcp(t) => t.accept_connections(),
TransportHandle::Tor(t) => t.accept_connections(),
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.accept_connections(),
#[cfg(test)]
TransportHandle::Loopback(t) => t.accept_connections(),
}
}
/// Initiate a non-blocking connection to a remote address.
///
/// For connection-oriented transports (TCP, Tor), spawns a background
/// task to establish the connection. For connectionless transports
/// (UDP, Ethernet), this is a no-op that returns Ok immediately.
///
/// Poll `connection_state()` to check when the connection is ready.
pub async fn connect(&self, addr: &TransportAddr) -> Result<(), TransportError> {
match self {
TransportHandle::Udp(_) => Ok(()), // connectionless
#[cfg(unix)]
TransportHandle::Ethernet(_) => Ok(()), // connectionless
TransportHandle::Tcp(t) => t.connect_async(addr).await,
TransportHandle::Tor(t) => t.connect_async(addr).await,
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.connect_async(addr).await,
#[cfg(test)]
TransportHandle::Loopback(_) => Ok(()), // connectionless
}
}
/// Query the state of a connection attempt to a remote address.
///
/// For connectionless transports, always returns `ConnectionState::Connected`
/// (they are always "connected"). For connection-oriented transports, returns
/// the current state of the background connection attempt.
pub fn connection_state(&self, addr: &TransportAddr) -> ConnectionState {
match self {
TransportHandle::Udp(_) => ConnectionState::Connected,
#[cfg(unix)]
TransportHandle::Ethernet(_) => ConnectionState::Connected,
TransportHandle::Tcp(t) => t.connection_state_sync(addr),
TransportHandle::Tor(t) => t.connection_state_sync(addr),
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.connection_state_sync(addr),
#[cfg(test)]
TransportHandle::Loopback(_) => ConnectionState::Connected,
}
}
/// Close a specific connection on this transport.
///
/// No-op for connectionless transports. For TCP/Tor, removes the
/// connection from the pool and drops the stream.
pub async fn close_connection(&self, addr: &TransportAddr) {
match self {
TransportHandle::Udp(t) => t.close_connection(addr),
#[cfg(unix)]
TransportHandle::Ethernet(t) => t.close_connection(addr),
TransportHandle::Tcp(t) => t.close_connection_async(addr).await,
TransportHandle::Tor(t) => t.close_connection_async(addr).await,
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => t.close_connection_async(addr).await,
#[cfg(test)]
TransportHandle::Loopback(_) => {} // connectionless no-op
}
}
/// Check if transport is operational.
pub fn is_operational(&self) -> bool {
self.state().is_operational()
}
/// Query transport-local congestion indicators.
///
/// Returns a snapshot of congestion signals that the transport can
/// observe locally (e.g., kernel receive buffer drops). Fields are
/// `None` when the transport doesn't support that signal.
pub fn congestion(&self) -> TransportCongestion {
match self {
TransportHandle::Udp(t) => t.congestion(),
#[cfg(unix)]
TransportHandle::Ethernet(_) => TransportCongestion::default(),
TransportHandle::Tcp(_) => TransportCongestion::default(),
TransportHandle::Tor(_) => TransportCongestion::default(),
#[cfg(target_os = "linux")]
TransportHandle::Ble(_) => TransportCongestion::default(),
#[cfg(test)]
TransportHandle::Loopback(_) => TransportCongestion::default(),
}
}
/// Get transport-specific stats as a JSON value.
///
/// Returns a snapshot of counters for the specific transport type.
pub fn transport_stats(&self) -> serde_json::Value {
match self {
TransportHandle::Udp(t) => {
serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
}
#[cfg(unix)]
TransportHandle::Ethernet(t) => {
let snap = t.stats().snapshot();
serde_json::json!({
"frames_sent": snap.frames_sent,
"frames_recv": snap.frames_recv,
"bytes_sent": snap.bytes_sent,
"bytes_recv": snap.bytes_recv,
"send_errors": snap.send_errors,
"recv_errors": snap.recv_errors,
"beacons_sent": snap.beacons_sent,
"beacons_recv": snap.beacons_recv,
"frames_too_short": snap.frames_too_short,
"frames_too_long": snap.frames_too_long,
})
}
TransportHandle::Tcp(t) => {
serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
}
TransportHandle::Tor(t) => {
serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
}
#[cfg(target_os = "linux")]
TransportHandle::Ble(t) => {
serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
}
#[cfg(test)]
TransportHandle::Loopback(_) => serde_json::json!({}),
}
}
}
// ============================================================================
// DNS Resolution
// ============================================================================
/// Resolve a TransportAddr to a SocketAddr.
///
/// Fast path: if the address parses as a numeric IP:port, returns
/// immediately with no DNS lookup. Otherwise, treats the address as
/// `hostname:port` and performs async DNS resolution via the system
/// resolver.
pub(crate) async fn resolve_socket_addr(
addr: &TransportAddr,
) -> Result<SocketAddr, TransportError> {
let s = addr
.as_str()
.ok_or_else(|| TransportError::InvalidAddress("not valid UTF-8".into()))?;
// Fast path: numeric IP address — no DNS lookup
if let Ok(sock_addr) = s.parse::<SocketAddr>() {
return Ok(sock_addr);
}
// Slow path: DNS resolution
tokio::net::lookup_host(s)
.await
.map_err(|e| {
TransportError::InvalidAddress(format!("DNS resolution failed for {}: {}", s, e))
})?
.next()
.ok_or_else(|| {
TransportError::InvalidAddress(format!(
"DNS resolution returned no addresses for {}",
s
))
})
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transport_id() {
let id = TransportId::new(42);
assert_eq!(id.as_u32(), 42);
assert_eq!(format!("{}", id), "transport:42");
}
#[test]
fn test_link_id() {
let id = LinkId::new(12345);
assert_eq!(id.as_u64(), 12345);
assert_eq!(format!("{}", id), "link:12345");
}
#[test]
fn test_transport_state_transitions() {
assert!(TransportState::Configured.can_start());
assert!(TransportState::Down.can_start());
assert!(TransportState::Failed.can_start());
assert!(!TransportState::Starting.can_start());
assert!(!TransportState::Up.can_start());
assert!(TransportState::Up.is_operational());
assert!(!TransportState::Starting.is_operational());
assert!(!TransportState::Failed.is_operational());
}
#[test]
fn test_link_state() {
assert!(LinkState::Connected.is_operational());
assert!(!LinkState::Connecting.is_operational());
assert!(!LinkState::Disconnected.is_operational());
assert!(!LinkState::Failed.is_operational());
assert!(LinkState::Disconnected.is_terminal());
assert!(LinkState::Failed.is_terminal());
assert!(!LinkState::Connected.is_terminal());
}
#[test]
#[allow(clippy::assertions_on_constants)]
fn test_transport_type_constants() {
// These assertions verify the constant definitions are correct
assert!(!TransportType::UDP.connection_oriented);
assert!(!TransportType::UDP.reliable);
assert!(TransportType::UDP.is_connectionless());
assert!(TransportType::TOR.connection_oriented);
assert!(TransportType::TOR.reliable);
assert!(!TransportType::TOR.is_connectionless());
assert_eq!(TransportType::UDP.name, "udp");
assert_eq!(TransportType::ETHERNET.name, "ethernet");
}
#[test]
fn test_transport_addr_string() {
let addr = TransportAddr::from_string("192.168.1.1:2121");
assert_eq!(format!("{}", addr), "192.168.1.1:2121");
assert_eq!(addr.as_str(), Some("192.168.1.1:2121"));
}
#[test]
fn test_transport_addr_binary() {
// Binary address with invalid UTF-8 bytes (0xff, 0x80 are invalid UTF-8)
let binary = TransportAddr::new(vec![0xff, 0x80, 0x2b, 0x3c, 0x4d, 0x5e]);
assert_eq!(format!("{}", binary), "ff802b3c4d5e");
assert!(binary.as_str().is_none());
assert_eq!(binary.len(), 6);
}
#[test]
fn test_transport_addr_from_string() {
let addr: TransportAddr = "test:1234".into();
assert_eq!(addr.as_str(), Some("test:1234"));
let addr2: TransportAddr = String::from("hello").into();
assert_eq!(addr2.as_str(), Some("hello"));
}
#[test]
fn test_link_stats_basic() {
let mut stats = LinkStats::new();
stats.record_sent(100);
stats.record_recv(200, 1000);
assert_eq!(stats.packets_sent, 1);
assert_eq!(stats.bytes_sent, 100);
assert_eq!(stats.packets_recv, 1);
assert_eq!(stats.bytes_recv, 200);
assert_eq!(stats.last_recv_ms, 1000);
}
#[test]
fn test_link_stats_rtt() {
let mut stats = LinkStats::new();
assert!(stats.rtt_estimate().is_none());
stats.update_rtt(Duration::from_millis(100));
assert_eq!(stats.rtt_estimate(), Some(Duration::from_millis(100)));
// Second update uses EMA
stats.update_rtt(Duration::from_millis(200));
// EMA: 0.2 * 200 + 0.8 * 100 = 120ms
let rtt = stats.rtt_estimate().unwrap();
assert!(rtt.as_millis() >= 110 && rtt.as_millis() <= 130);
}
#[test]
fn test_link_stats_time_since_recv() {
let mut stats = LinkStats::new();
// No receive yet
assert_eq!(stats.time_since_recv(1000), u64::MAX);
stats.record_recv(100, 500);
assert_eq!(stats.time_since_recv(1000), 500);
assert_eq!(stats.time_since_recv(500), 0);
}
#[test]
fn test_link_creation() {
let link = Link::new(
LinkId::new(1),
TransportId::new(1),
TransportAddr::from_string("test"),
LinkDirection::Outbound,
Duration::from_millis(50),
);
assert_eq!(link.state(), LinkState::Connecting);
assert!(!link.is_operational());
assert_eq!(link.direction(), LinkDirection::Outbound);
}
#[test]
fn test_link_connectionless() {
let link = Link::connectionless(
LinkId::new(1),
TransportId::new(1),
TransportAddr::from_string("test"),
LinkDirection::Inbound,
Duration::from_millis(5),
);
assert_eq!(link.state(), LinkState::Connected);
assert!(link.is_operational());
}
#[test]
fn test_link_state_changes() {
let mut link = Link::new(
LinkId::new(1),
TransportId::new(1),
TransportAddr::from_string("test"),
LinkDirection::Outbound,
Duration::from_millis(50),
);
assert!(!link.is_operational());
link.set_connected();
assert!(link.is_operational());
assert!(!link.is_terminal());
link.set_disconnected();
assert!(!link.is_operational());
assert!(link.is_terminal());
}
#[test]
fn test_link_effective_rtt() {
let mut link = Link::connectionless(
LinkId::new(1),
TransportId::new(1),
TransportAddr::from_string("test"),
LinkDirection::Inbound,
Duration::from_millis(50),
);
// Before measurement, uses base RTT
assert_eq!(link.effective_rtt(), Duration::from_millis(50));
// After measurement, uses measured RTT
link.stats_mut().update_rtt(Duration::from_millis(100));
assert_eq!(link.effective_rtt(), Duration::from_millis(100));
}
#[test]
fn test_link_age() {
let mut link = Link::new(
LinkId::new(1),
TransportId::new(1),
TransportAddr::from_string("test"),
LinkDirection::Outbound,
Duration::from_millis(50),
);
// No timestamp set
assert_eq!(link.age(1000), 0);
link.set_created_at(500);
assert_eq!(link.age(1000), 500);
assert_eq!(link.age(500), 0);
}
#[test]
fn test_discovered_peer() {
let peer = DiscoveredPeer::new(
TransportId::new(1),
TransportAddr::from_string("192.168.1.1:2121"),
);
assert_eq!(peer.transport_id, TransportId::new(1));
assert!(peer.pubkey_hint.is_none());
}
#[test]
fn test_link_direction_display() {
assert_eq!(format!("{}", LinkDirection::Outbound), "outbound");
assert_eq!(format!("{}", LinkDirection::Inbound), "inbound");
}
#[test]
fn test_transport_state_display() {
assert_eq!(format!("{}", TransportState::Up), "up");
assert_eq!(format!("{}", TransportState::Failed), "failed");
}
#[test]
fn test_received_packet() {
let packet = ReceivedPacket::new(
TransportId::new(1),
TransportAddr::from_string("192.168.1.1:2121"),
vec![1, 2, 3, 4],
);
assert_eq!(packet.transport_id, TransportId::new(1));
assert_eq!(packet.data, vec![1, 2, 3, 4]);
assert!(packet.timestamp_ms > 0);
}
#[test]
fn test_received_packet_with_timestamp() {
let packet = ReceivedPacket::with_timestamp(
TransportId::new(1),
TransportAddr::from_string("test"),
vec![5, 6],
12345,
);
assert_eq!(packet.timestamp_ms, 12345);
}
#[tokio::test]
async fn test_packet_channel() {
let (tx, mut rx) = packet_channel(10);
let packet = ReceivedPacket::new(
TransportId::new(1),
TransportAddr::from_string("test"),
vec![1, 2, 3],
);
tx.send(packet.clone()).await.unwrap();
let received = rx.recv().await.unwrap();
assert_eq!(received.data, vec![1, 2, 3]);
}
// ========================================================================
// link_mtu tests
// ========================================================================
/// Minimal mock transport for testing the default link_mtu() behavior.
struct MockTransport {
id: TransportId,
mtu_value: u16,
}
impl MockTransport {
fn new(mtu: u16) -> Self {
Self {
id: TransportId::new(99),
mtu_value: mtu,
}
}
}
impl Transport for MockTransport {
fn transport_id(&self) -> TransportId {
self.id
}
fn transport_type(&self) -> &TransportType {
&TransportType::UDP
}
fn state(&self) -> TransportState {
TransportState::Up
}
fn mtu(&self) -> u16 {
self.mtu_value
}
fn start(&mut self) -> Result<(), TransportError> {
Ok(())
}
fn stop(&mut self) -> Result<(), TransportError> {
Ok(())
}
fn send(&self, _addr: &TransportAddr, _data: &[u8]) -> Result<(), TransportError> {
Ok(())
}
fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
Ok(vec![])
}
}
/// Mock transport that overrides link_mtu() to return per-link values.
struct PerLinkMtuTransport {
id: TransportId,
default_mtu: u16,
/// Address-specific MTU overrides.
overrides: Vec<(TransportAddr, u16)>,
}
impl PerLinkMtuTransport {
fn new(default_mtu: u16, overrides: Vec<(TransportAddr, u16)>) -> Self {
Self {
id: TransportId::new(100),
default_mtu,
overrides,
}
}
}
impl Transport for PerLinkMtuTransport {
fn transport_id(&self) -> TransportId {
self.id
}
fn transport_type(&self) -> &TransportType {
&TransportType::UDP
}
fn state(&self) -> TransportState {
TransportState::Up
}
fn mtu(&self) -> u16 {
self.default_mtu
}
fn link_mtu(&self, addr: &TransportAddr) -> u16 {
for (a, mtu) in &self.overrides {
if a == addr {
return *mtu;
}
}
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> {
Ok(())
}
fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
Ok(vec![])
}
}
#[test]
fn test_link_mtu_default_falls_back_to_mtu() {
let transport = MockTransport::new(1280);
let addr = TransportAddr::from_string("192.168.1.1:2121");
// Default link_mtu() should return the transport-wide mtu()
assert_eq!(transport.link_mtu(&addr), 1280);
assert_eq!(transport.link_mtu(&addr), transport.mtu());
// Any address should return the same value
let other_addr = TransportAddr::from_string("10.0.0.1:5000");
assert_eq!(transport.link_mtu(&other_addr), 1280);
}
#[test]
fn test_link_mtu_per_link_override() {
let addr_a = TransportAddr::from_string("192.168.1.1:2121");
let addr_b = TransportAddr::from_string("10.0.0.1:5000");
let addr_unknown = TransportAddr::from_string("172.16.0.1:6000");
let transport =
PerLinkMtuTransport::new(1280, vec![(addr_a.clone(), 512), (addr_b.clone(), 247)]);
// Known addresses return their per-link MTU
assert_eq!(transport.link_mtu(&addr_a), 512);
assert_eq!(transport.link_mtu(&addr_b), 247);
// Unknown address falls back to transport-wide default
assert_eq!(transport.link_mtu(&addr_unknown), 1280);
assert_eq!(transport.mtu(), 1280);
}
#[test]
fn test_transport_handle_link_mtu_delegation() {
use crate::config::UdpConfig;
use crate::transport::udp::UdpTransport;
let config = UdpConfig::default();
let expected_mtu = config.mtu();
let (tx, _rx) = packet_channel(1);
let transport = UdpTransport::new(TransportId::new(1), None, config, tx);
let handle = TransportHandle::Udp(transport);
let addr = TransportAddr::from_string("192.168.1.1:2121");
// TransportHandle::link_mtu() should delegate and return the same
// as TransportHandle::mtu() for UDP (no per-link overrides)
assert_eq!(handle.link_mtu(&addr), expected_mtu);
assert_eq!(handle.link_mtu(&addr), handle.mtu());
}
}