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