Skip to main content

fips_core/transport/
mod.rs

1//! Transport Layer Abstractions
2//!
3//! Traits and types for FIPS transport drivers. Transports provide the
4//! underlying communication mechanisms (UDP, Ethernet, Tor, etc.) over
5//! which FIPS links are established.
6
7pub mod tcp;
8pub mod tor;
9pub mod udp;
10#[cfg(feature = "webrtc-transport")]
11pub mod webrtc;
12
13#[cfg(feature = "sim-transport")]
14pub mod sim;
15
16#[cfg(any(target_os = "linux", target_os = "macos"))]
17pub mod ethernet;
18
19#[cfg(target_os = "linux")]
20pub mod ble;
21
22mod handle;
23mod packet_channel;
24
25#[cfg(test)]
26mod tests;
27
28pub use handle::TransportHandle;
29#[cfg(any(target_os = "linux", target_os = "macos"))]
30pub(crate) use packet_channel::received_timestamp_ms;
31pub use packet_channel::{PacketBuffer, PacketRx, PacketTx, ReceivedPacket, packet_channel};
32
33use secp256k1::XOnlyPublicKey;
34use std::fmt;
35use std::net::SocketAddr;
36use std::sync::Arc;
37use std::time::Duration;
38use thiserror::Error;
39
40// ============================================================================
41// Transport Identifiers
42// ============================================================================
43
44/// Unique identifier for a transport instance.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub struct TransportId(u32);
47
48impl TransportId {
49    /// Create a new transport ID.
50    pub fn new(id: u32) -> Self {
51        Self(id)
52    }
53
54    /// Get the raw ID value.
55    pub fn as_u32(&self) -> u32 {
56        self.0
57    }
58}
59
60impl fmt::Display for TransportId {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "transport:{}", self.0)
63    }
64}
65
66/// Unique identifier for a link instance.
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
68pub struct LinkId(u64);
69
70impl LinkId {
71    /// Create a new link ID.
72    pub fn new(id: u64) -> Self {
73        Self(id)
74    }
75
76    /// Get the raw ID value.
77    pub fn as_u64(&self) -> u64 {
78        self.0
79    }
80}
81
82impl fmt::Display for LinkId {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "link:{}", self.0)
85    }
86}
87
88// ============================================================================
89// Errors
90// ============================================================================
91
92/// Errors related to transport operations.
93#[derive(Debug, Error)]
94pub enum TransportError {
95    #[error("transport not started")]
96    NotStarted,
97
98    #[error("transport already started")]
99    AlreadyStarted,
100
101    #[error("transport failed to start: {0}")]
102    StartFailed(String),
103
104    #[error("transport shutdown failed: {0}")]
105    ShutdownFailed(String),
106
107    #[error("link failed: {0}")]
108    LinkFailed(String),
109
110    #[error("send failed: {0}")]
111    SendFailed(String),
112
113    #[error("receive failed: {0}")]
114    RecvFailed(String),
115
116    #[error("invalid transport address: {0}")]
117    InvalidAddress(String),
118
119    #[error("mtu exceeded: packet {packet_size} > mtu {mtu}")]
120    MtuExceeded { packet_size: usize, mtu: u16 },
121
122    #[error("transport timeout")]
123    Timeout,
124
125    #[error("connection refused")]
126    ConnectionRefused,
127
128    #[error("transport not supported: {0}")]
129    NotSupported(String),
130
131    #[error("io error: {0}")]
132    Io(#[from] std::io::Error),
133}
134
135impl TransportError {
136    /// True when the local OS says the outbound underlay path is temporarily
137    /// unsendable, rather than the peer or protocol being bad.
138    pub fn is_local_route_unavailable(&self) -> bool {
139        match self {
140            TransportError::Io(error) => is_local_route_error_kind(error.kind()),
141            TransportError::SendFailed(message) => is_local_route_error_text(message),
142            _ => false,
143        }
144    }
145}
146
147fn is_local_route_error_kind(kind: std::io::ErrorKind) -> bool {
148    matches!(
149        kind,
150        std::io::ErrorKind::NetworkUnreachable
151            | std::io::ErrorKind::HostUnreachable
152            | std::io::ErrorKind::AddrNotAvailable
153            | std::io::ErrorKind::PermissionDenied
154    )
155}
156
157fn is_local_route_error_text(message: &str) -> bool {
158    let lower = message.to_ascii_lowercase();
159    lower.contains("network is unreachable")
160        || lower.contains("no route to host")
161        || lower.contains("host is unreachable")
162        || lower.contains("can't assign requested address")
163        || lower.contains("cannot assign requested address")
164        || lower.contains("operation not permitted")
165        || lower.contains("permission denied")
166        || lower.contains("os error 51")
167        || lower.contains("os error 65")
168        || lower.contains("os error 49")
169        || lower.contains("os error 1")
170}
171
172// ============================================================================
173// Transport Type Metadata
174// ============================================================================
175
176/// Static metadata about a transport type.
177#[derive(Clone, Debug, PartialEq, Eq)]
178pub struct TransportType {
179    /// Human-readable name (e.g., "udp", "ethernet", "tor").
180    pub name: &'static str,
181    /// Whether this transport requires connection establishment.
182    pub connection_oriented: bool,
183    /// Whether the transport guarantees delivery.
184    pub reliable: bool,
185}
186
187impl TransportType {
188    /// UDP/IP transport.
189    pub const UDP: TransportType = TransportType {
190        name: "udp",
191        connection_oriented: false,
192        reliable: false,
193    };
194
195    /// TCP/IP transport.
196    pub const TCP: TransportType = TransportType {
197        name: "tcp",
198        connection_oriented: true,
199        reliable: true,
200    };
201
202    /// Raw Ethernet transport.
203    pub const ETHERNET: TransportType = TransportType {
204        name: "ethernet",
205        connection_oriented: false,
206        reliable: false,
207    };
208
209    /// WiFi (same characteristics as Ethernet).
210    pub const WIFI: TransportType = TransportType {
211        name: "wifi",
212        connection_oriented: false,
213        reliable: false,
214    };
215
216    /// Tor onion transport.
217    pub const TOR: TransportType = TransportType {
218        name: "tor",
219        connection_oriented: true,
220        reliable: true,
221    };
222
223    /// Serial/UART transport.
224    pub const SERIAL: TransportType = TransportType {
225        name: "serial",
226        connection_oriented: false,
227        reliable: true, // typically uses framing with checksums
228    };
229
230    /// BLE L2CAP CoC transport.
231    pub const BLE: TransportType = TransportType {
232        name: "ble",
233        connection_oriented: true,
234        reliable: true, // L2CAP SeqPacket guarantees delivery
235    };
236
237    /// WebRTC DataChannel transport.
238    pub const WEBRTC: TransportType = TransportType {
239        name: "webrtc",
240        connection_oriented: true,
241        reliable: false,
242    };
243
244    /// In-memory simulated packet transport.
245    #[cfg(feature = "sim-transport")]
246    pub const SIM: TransportType = TransportType {
247        name: "sim",
248        connection_oriented: false,
249        reliable: false,
250    };
251
252    /// Check if the transport is connectionless.
253    pub fn is_connectionless(&self) -> bool {
254        !self.connection_oriented
255    }
256}
257
258impl fmt::Display for TransportType {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        write!(f, "{}", self.name)
261    }
262}
263
264// ============================================================================
265// Transport State
266// ============================================================================
267
268/// Transport lifecycle state.
269#[derive(Clone, Copy, Debug, PartialEq, Eq)]
270pub enum TransportState {
271    /// Configured but not started.
272    Configured,
273    /// Initialization in progress.
274    Starting,
275    /// Ready for links.
276    Up,
277    /// Was up, now unavailable.
278    Down,
279    /// Failed to start.
280    Failed,
281}
282
283impl TransportState {
284    /// Check if the transport is operational.
285    pub fn is_operational(&self) -> bool {
286        matches!(self, TransportState::Up)
287    }
288
289    /// Check if the transport can be started.
290    pub fn can_start(&self) -> bool {
291        matches!(
292            self,
293            TransportState::Configured | TransportState::Down | TransportState::Failed
294        )
295    }
296
297    /// Check if the transport is in a terminal state.
298    pub fn is_terminal(&self) -> bool {
299        matches!(self, TransportState::Failed)
300    }
301}
302
303impl fmt::Display for TransportState {
304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        let s = match self {
306            TransportState::Configured => "configured",
307            TransportState::Starting => "starting",
308            TransportState::Up => "up",
309            TransportState::Down => "down",
310            TransportState::Failed => "failed",
311        };
312        write!(f, "{}", s)
313    }
314}
315
316// ============================================================================
317// Link State
318// ============================================================================
319
320/// Link lifecycle state.
321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub enum LinkState {
323    /// Connection in progress (connection-oriented only).
324    Connecting,
325    /// Ready for traffic.
326    Connected,
327    /// Was connected, now gone.
328    Disconnected,
329    /// Connection attempt failed.
330    Failed,
331}
332
333impl LinkState {
334    /// Check if the link is operational.
335    pub fn is_operational(&self) -> bool {
336        matches!(self, LinkState::Connected)
337    }
338
339    /// Check if the link is in a terminal state.
340    pub fn is_terminal(&self) -> bool {
341        matches!(self, LinkState::Disconnected | LinkState::Failed)
342    }
343}
344
345impl fmt::Display for LinkState {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        let s = match self {
348            LinkState::Connecting => "connecting",
349            LinkState::Connected => "connected",
350            LinkState::Disconnected => "disconnected",
351            LinkState::Failed => "failed",
352        };
353        write!(f, "{}", s)
354    }
355}
356
357/// Direction of link establishment.
358#[derive(Clone, Copy, Debug, PartialEq, Eq)]
359pub enum LinkDirection {
360    /// We initiated the connection.
361    Outbound,
362    /// They initiated the connection.
363    Inbound,
364}
365
366impl fmt::Display for LinkDirection {
367    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368        let s = match self {
369            LinkDirection::Outbound => "outbound",
370            LinkDirection::Inbound => "inbound",
371        };
372        write!(f, "{}", s)
373    }
374}
375
376// ============================================================================
377// Transport Address
378// ============================================================================
379
380/// Opaque transport-specific address.
381///
382/// Each transport type interprets this differently:
383/// - UDP/TCP: "host:port" (IP address or DNS hostname)
384/// - Ethernet: MAC address (6 bytes)
385///
386/// The bytes are immutable and shared so hot-path clones can carry path
387/// evidence through receive/session bookkeeping without copying address bytes.
388#[derive(Clone, PartialEq, Eq, Hash)]
389pub struct TransportAddr(Arc<[u8]>);
390
391impl TransportAddr {
392    /// Create a transport address from raw bytes.
393    pub fn new(bytes: Vec<u8>) -> Self {
394        Self(bytes.into())
395    }
396
397    /// Create a transport address from a byte slice.
398    pub fn from_bytes(bytes: &[u8]) -> Self {
399        Self(Arc::from(bytes))
400    }
401
402    /// Create a transport address from a string.
403    pub fn from_string(s: &str) -> Self {
404        Self(Arc::from(s.as_bytes()))
405    }
406
407    /// Create a transport address from a `SocketAddr` without going
408    /// through `to_string()`.
409    ///
410    /// The standard path is `from_string(&addr.to_string())`, which
411    /// allocates a `String` for the formatted address and then copies
412    /// its bytes into a fresh `Vec<u8>` — two heap allocations per
413    /// inbound packet on the UDP receive hot path. At line rate that's
414    /// a few percent of one core in malloc/free. This variant writes
415    /// the `SocketAddr::Display` representation directly into a
416    /// `Vec<u8>` via `std::io::Write`, halving the alloc count and
417    /// skipping the intermediate `String` materialisation entirely.
418    pub fn from_socket_addr(addr: std::net::SocketAddr) -> Self {
419        use std::io::Write;
420        // Pre-size to fit `[ipv6_lit]:65535` (47 + brackets + colon +
421        // port digits ≈ 56 bytes worst case) so we don't re-grow the
422        // buffer mid-format on common addresses.
423        let mut buf = Vec::with_capacity(56);
424        // The `write!` macro on `&mut Vec<u8>` cannot fail (Vec's
425        // `Write` impl is infallible for in-memory buffers), so the
426        // expect is for shape only.
427        write!(&mut buf, "{addr}").expect("Vec<u8>::write_fmt is infallible");
428        Self(buf.into())
429    }
430
431    /// Get the raw bytes.
432    pub fn as_bytes(&self) -> &[u8] {
433        &self.0
434    }
435
436    /// Try to interpret as a UTF-8 string.
437    pub fn as_str(&self) -> Option<&str> {
438        std::str::from_utf8(&self.0).ok()
439    }
440
441    /// Get the length in bytes.
442    pub fn len(&self) -> usize {
443        self.0.len()
444    }
445
446    /// Check if empty.
447    pub fn is_empty(&self) -> bool {
448        self.0.is_empty()
449    }
450}
451
452impl fmt::Debug for TransportAddr {
453    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454        match self.as_str() {
455            Some(s) => write!(f, "TransportAddr(\"{}\")", s),
456            None => write!(f, "TransportAddr({:?})", self.0),
457        }
458    }
459}
460
461impl fmt::Display for TransportAddr {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        // Best-effort display as string if valid UTF-8, else hex
464        match self.as_str() {
465            Some(s) => write!(f, "{}", s),
466            None => {
467                for byte in self.0.iter() {
468                    write!(f, "{:02x}", byte)?;
469                }
470                Ok(())
471            }
472        }
473    }
474}
475
476impl From<&str> for TransportAddr {
477    fn from(s: &str) -> Self {
478        Self::from_string(s)
479    }
480}
481
482impl From<String> for TransportAddr {
483    fn from(s: String) -> Self {
484        Self(s.into_bytes().into())
485    }
486}
487
488// ============================================================================
489// Link Statistics
490// ============================================================================
491
492/// Statistics for a link.
493#[derive(Clone, Debug, Default)]
494pub struct LinkStats {
495    /// Total packets sent.
496    pub packets_sent: u64,
497    /// Total packets received.
498    pub packets_recv: u64,
499    /// Total bytes sent.
500    pub bytes_sent: u64,
501    /// Total bytes received.
502    pub bytes_recv: u64,
503    /// Timestamp of last received packet (Unix milliseconds).
504    pub last_recv_ms: u64,
505    /// Estimated round-trip time.
506    rtt_estimate: Option<Duration>,
507    /// Observed packet loss rate (0.0-1.0).
508    pub loss_rate: f32,
509    /// Estimated throughput in bytes/second.
510    pub throughput_estimate: u64,
511}
512
513impl LinkStats {
514    /// Create new link statistics.
515    pub fn new() -> Self {
516        Self::default()
517    }
518
519    /// Record a sent packet.
520    pub fn record_sent(&mut self, bytes: usize) {
521        self.packets_sent += 1;
522        self.bytes_sent += bytes as u64;
523    }
524
525    /// Record multiple sent packets.
526    pub fn record_sent_batch(&mut self, packets: usize, bytes: usize) {
527        self.packets_sent += packets as u64;
528        self.bytes_sent += bytes as u64;
529    }
530
531    /// Record a received packet.
532    pub fn record_recv(&mut self, bytes: usize, timestamp_ms: u64) {
533        self.packets_recv += 1;
534        self.bytes_recv += bytes as u64;
535        self.last_recv_ms = timestamp_ms;
536    }
537
538    /// Get the RTT estimate, if available.
539    pub fn rtt_estimate(&self) -> Option<Duration> {
540        self.rtt_estimate
541    }
542
543    /// Update RTT estimate from a probe response.
544    ///
545    /// Uses exponential moving average with alpha=0.2.
546    pub fn update_rtt(&mut self, rtt: Duration) {
547        match self.rtt_estimate {
548            Some(old_rtt) => {
549                let alpha = 0.2;
550                let new_rtt_nanos = (alpha * rtt.as_nanos() as f64
551                    + (1.0 - alpha) * old_rtt.as_nanos() as f64)
552                    as u64;
553                self.rtt_estimate = Some(Duration::from_nanos(new_rtt_nanos));
554            }
555            None => {
556                self.rtt_estimate = Some(rtt);
557            }
558        }
559    }
560
561    /// Time since last receive (for keepalive/timeout).
562    pub fn time_since_recv(&self, current_time_ms: u64) -> u64 {
563        if self.last_recv_ms == 0 {
564            return u64::MAX;
565        }
566        current_time_ms.saturating_sub(self.last_recv_ms)
567    }
568
569    /// Reset all statistics.
570    pub fn reset(&mut self) {
571        *self = Self::default();
572    }
573}
574
575// ============================================================================
576// Link
577// ============================================================================
578
579/// A link to a remote endpoint over a transport.
580#[derive(Clone, Debug)]
581pub struct Link {
582    /// Unique link identifier.
583    link_id: LinkId,
584    /// Which transport this link uses.
585    transport_id: TransportId,
586    /// Transport-specific remote address.
587    remote_addr: TransportAddr,
588    /// Whether we initiated or they initiated.
589    direction: LinkDirection,
590    /// Current link state.
591    state: LinkState,
592    /// Base RTT hint from transport type.
593    base_rtt: Duration,
594    /// Measured statistics.
595    stats: LinkStats,
596    /// When this link was created (Unix milliseconds).
597    created_at: u64,
598}
599
600impl Link {
601    /// Create a new link in Connecting state.
602    pub fn new(
603        link_id: LinkId,
604        transport_id: TransportId,
605        remote_addr: TransportAddr,
606        direction: LinkDirection,
607        base_rtt: Duration,
608    ) -> Self {
609        Self {
610            link_id,
611            transport_id,
612            remote_addr,
613            direction,
614            state: LinkState::Connecting,
615            base_rtt,
616            stats: LinkStats::new(),
617            created_at: 0,
618        }
619    }
620
621    /// Create a link with a creation timestamp.
622    pub fn new_with_timestamp(
623        link_id: LinkId,
624        transport_id: TransportId,
625        remote_addr: TransportAddr,
626        direction: LinkDirection,
627        base_rtt: Duration,
628        created_at: u64,
629    ) -> Self {
630        let mut link = Self::new(link_id, transport_id, remote_addr, direction, base_rtt);
631        link.created_at = created_at;
632        link
633    }
634
635    /// Create a connectionless link (immediately connected).
636    ///
637    /// For connectionless transports (UDP, Ethernet), links are immediately
638    /// in the Connected state.
639    pub fn connectionless(
640        link_id: LinkId,
641        transport_id: TransportId,
642        remote_addr: TransportAddr,
643        direction: LinkDirection,
644        base_rtt: Duration,
645    ) -> Self {
646        let mut link = Self::new(link_id, transport_id, remote_addr, direction, base_rtt);
647        link.state = LinkState::Connected;
648        link
649    }
650
651    /// Get the link ID.
652    pub fn link_id(&self) -> LinkId {
653        self.link_id
654    }
655
656    /// Get the transport ID.
657    pub fn transport_id(&self) -> TransportId {
658        self.transport_id
659    }
660
661    /// Get the remote address.
662    pub fn remote_addr(&self) -> &TransportAddr {
663        &self.remote_addr
664    }
665
666    /// Get the link direction.
667    pub fn direction(&self) -> LinkDirection {
668        self.direction
669    }
670
671    /// Get the current state.
672    pub fn state(&self) -> LinkState {
673        self.state
674    }
675
676    /// Get the base RTT hint.
677    pub fn base_rtt(&self) -> Duration {
678        self.base_rtt
679    }
680
681    /// Get the link statistics.
682    pub fn stats(&self) -> &LinkStats {
683        &self.stats
684    }
685
686    /// Get mutable access to link statistics.
687    pub fn stats_mut(&mut self) -> &mut LinkStats {
688        &mut self.stats
689    }
690
691    /// Get the creation timestamp.
692    pub fn created_at(&self) -> u64 {
693        self.created_at
694    }
695
696    /// Set the creation timestamp.
697    pub fn set_created_at(&mut self, timestamp: u64) {
698        self.created_at = timestamp;
699    }
700
701    /// Mark the link as connected.
702    pub fn set_connected(&mut self) {
703        self.state = LinkState::Connected;
704    }
705
706    /// Mark the link as disconnected.
707    pub fn set_disconnected(&mut self) {
708        self.state = LinkState::Disconnected;
709    }
710
711    /// Mark the link as failed.
712    pub fn set_failed(&mut self) {
713        self.state = LinkState::Failed;
714    }
715
716    /// Check if this link is operational.
717    pub fn is_operational(&self) -> bool {
718        self.state.is_operational()
719    }
720
721    /// Check if this link is in a terminal state.
722    pub fn is_terminal(&self) -> bool {
723        self.state.is_terminal()
724    }
725
726    /// Get effective RTT (measured if available, else base hint).
727    pub fn effective_rtt(&self) -> Duration {
728        self.stats.rtt_estimate().unwrap_or(self.base_rtt)
729    }
730
731    /// Age of the link in milliseconds.
732    pub fn age(&self, current_time_ms: u64) -> u64 {
733        if self.created_at == 0 {
734            return 0;
735        }
736        current_time_ms.saturating_sub(self.created_at)
737    }
738}
739
740// ============================================================================
741// Discovered Peer
742// ============================================================================
743
744/// A peer discovered via transport-layer discovery.
745#[derive(Clone, Debug)]
746pub struct DiscoveredPeer {
747    /// Transport that discovered this peer.
748    pub transport_id: TransportId,
749    /// Transport address where the peer was found.
750    pub addr: TransportAddr,
751    /// Optional hint about the peer's identity (if known from discovery).
752    pub pubkey_hint: Option<XOnlyPublicKey>,
753}
754
755impl DiscoveredPeer {
756    /// Create a discovered peer without identity hint.
757    pub fn new(transport_id: TransportId, addr: TransportAddr) -> Self {
758        Self {
759            transport_id,
760            addr,
761            pubkey_hint: None,
762        }
763    }
764
765    /// Create a discovered peer with identity hint.
766    pub fn with_hint(
767        transport_id: TransportId,
768        addr: TransportAddr,
769        pubkey: XOnlyPublicKey,
770    ) -> Self {
771        Self {
772            transport_id,
773            addr,
774            pubkey_hint: Some(pubkey),
775        }
776    }
777}
778
779// ============================================================================
780// Transport Trait
781// ============================================================================
782
783/// Transport trait defining the interface for transport drivers.
784///
785/// This is a simplified synchronous trait. Actual implementations would
786/// be async and use channels for event delivery.
787pub trait Transport {
788    /// Get the transport identifier.
789    fn transport_id(&self) -> TransportId;
790
791    /// Get the transport type metadata.
792    fn transport_type(&self) -> &TransportType;
793
794    /// Get the current state.
795    fn state(&self) -> TransportState;
796
797    /// Get the MTU for this transport.
798    fn mtu(&self) -> u16;
799
800    /// Get the MTU for a specific link.
801    ///
802    /// Returns the MTU negotiated for the given transport address, or
803    /// falls back to the transport-wide default if the address is unknown
804    /// or the transport doesn't support per-link MTU negotiation.
805    fn link_mtu(&self, addr: &TransportAddr) -> u16 {
806        let _ = addr;
807        self.mtu()
808    }
809
810    /// Start the transport.
811    fn start(&mut self) -> Result<(), TransportError>;
812
813    /// Stop the transport.
814    fn stop(&mut self) -> Result<(), TransportError>;
815
816    /// Send data to a transport address.
817    fn send(&self, addr: &TransportAddr, data: &[u8]) -> Result<(), TransportError>;
818
819    /// Discover potential peers (if supported).
820    fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError>;
821
822    /// Whether to auto-connect to peers returned by discover().
823    /// Default: false. Concrete transports read from their own config.
824    fn auto_connect(&self) -> bool {
825        false
826    }
827
828    /// Whether to accept inbound handshake initiations on this transport.
829    /// Default: true (preserves UDP's current implicit behavior).
830    fn accept_connections(&self) -> bool {
831        true
832    }
833
834    /// Close a specific connection (connection-oriented transports only).
835    ///
836    /// For connectionless transports (UDP, Ethernet), this is a no-op.
837    /// Connection-oriented transports (TCP, Tor) remove the connection
838    /// from their pool and drop the underlying stream.
839    fn close_connection(&self, _addr: &TransportAddr) {
840        // Default no-op for connectionless transports
841    }
842}
843
844// ============================================================================
845// Connection State (for non-blocking connect)
846// ============================================================================
847
848/// State of a transport-level connection attempt.
849///
850/// Used by connection-oriented transports (TCP, Tor) to report the progress
851/// of a background connection attempt initiated by `connect()`.
852#[derive(Clone, Debug, PartialEq, Eq)]
853pub enum ConnectionState {
854    /// No connection attempt in progress for this address.
855    None,
856    /// Connection attempt is in progress (background task running).
857    Connecting,
858    /// Connection is established and ready for send().
859    Connected,
860    /// Connection attempt failed with the given error message.
861    Failed(String),
862}
863
864// ============================================================================
865// Transport Congestion
866// ============================================================================
867
868/// Transport-local congestion indicators.
869///
870/// All fields are optional — transports report what they can.
871/// Consumers compute deltas from cumulative counters.
872#[derive(Clone, Debug, Default)]
873pub struct TransportCongestion {
874    /// Cumulative packets dropped by kernel/OS before reaching the application.
875    /// Monotonically increasing since transport start.
876    pub recv_drops: Option<u64>,
877    /// Cumulative packets dropped by this transport socket before userspace
878    /// receive, when the platform exposes a socket-local counter.
879    pub socket_recv_drops: Option<u64>,
880    /// Cumulative Linux namespace UDP receive-buffer errors since transport
881    /// start. This is broader than one socket, so callers should report it
882    /// separately from socket-local drops.
883    pub namespace_recv_drops: Option<u64>,
884}
885
886// ============================================================================
887// DNS Resolution
888// ============================================================================
889
890/// Resolve a TransportAddr to a SocketAddr.
891///
892/// Fast path: if the address parses as a numeric IP:port, returns
893/// immediately with no DNS lookup. Otherwise, treats the address as
894/// `hostname:port` and performs async DNS resolution via the system
895/// resolver.
896pub(crate) async fn resolve_socket_addr(
897    addr: &TransportAddr,
898) -> Result<SocketAddr, TransportError> {
899    resolve_socket_addrs(addr)
900        .await?
901        .into_iter()
902        .next()
903        .ok_or_else(|| {
904            TransportError::InvalidAddress(format!(
905                "DNS resolution returned no addresses for {}",
906                addr.as_str().unwrap_or("<non-utf8>")
907            ))
908        })
909}
910
911/// Resolve a TransportAddr to every SocketAddr returned by the resolver.
912///
913/// Numeric IP addresses still bypass DNS. Hostnames keep the resolver's
914/// address order, and callers that establish connections should try more than
915/// one address so dual-stack hosts still work when one address family is
916/// temporarily broken.
917pub(crate) async fn resolve_socket_addrs(
918    addr: &TransportAddr,
919) -> Result<Vec<SocketAddr>, TransportError> {
920    let s = addr
921        .as_str()
922        .ok_or_else(|| TransportError::InvalidAddress("not valid UTF-8".into()))?;
923
924    // Fast path: numeric IP address — no DNS lookup
925    if let Ok(sock_addr) = s.parse::<SocketAddr>() {
926        return Ok(vec![sock_addr]);
927    }
928
929    // Slow path: DNS resolution
930    let addrs = tokio::net::lookup_host(s)
931        .await
932        .map_err(|e| {
933            TransportError::InvalidAddress(format!("DNS resolution failed for {}: {}", s, e))
934        })?
935        .collect::<Vec<_>>();
936    if addrs.is_empty() {
937        return Err(TransportError::InvalidAddress(format!(
938            "DNS resolution returned no addresses for {}",
939            s
940        )));
941    }
942    Ok(addrs)
943}