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