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