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