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