Skip to main content

truffle_core/transport/
mod.rs

1//! Layer 4: Transport — Protocol-specific connection management.
2//!
3//! This module defines three transport trait families:
4//!
5//! - [`StreamTransport`] + [`FramedStream`]: Persistent, framed, bidirectional
6//!   connections used for messaging, pub/sub, and signaling. Implemented by
7//!   [`WebSocketTransport`](websocket::WebSocketTransport) and [`QuicTransport`](quic::QuicTransport).
8//!
9//! - [`RawTransport`] + [`RawListener`]: Raw byte streams used for file transfer,
10//!   TCP proxy, and HTTP. Implemented by [`TcpTransport`](tcp::TcpTransport).
11//!
12//! - [`DatagramTransport`] + [`DatagramSocket`]: Unreliable datagrams for
13//!   real-time video/audio and game state. Implemented by [`UdpTransport`](udp::UdpTransport).
14//!
15//! # Layer rules
16//!
17//! - Layer 4 does NOT know about peers, sessions, or envelopes
18//! - Layer 4 works with [`PeerAddr`] (IP + hostname),
19//!   not peer IDs
20//! - All transports delegate raw networking to Layer 3's [`NetworkProvider`](crate::network::NetworkProvider)
21//! - The [`FramedStream`] trait is what Layer 5 (Session) will consume
22
23pub mod quic;
24pub mod quic_socket;
25pub mod tcp;
26pub mod udp;
27pub mod websocket;
28
29#[cfg(test)]
30mod tests;
31
32use std::time::Duration;
33
34use tokio::net::TcpStream;
35
36use crate::network::PeerAddr;
37
38// ---------------------------------------------------------------------------
39// Transport traits
40// ---------------------------------------------------------------------------
41
42/// A persistent, framed, bidirectional connection.
43///
44/// Used for: messaging, pub/sub, signaling.
45/// Implementations: WebSocket, QUIC streams (future).
46///
47/// The transport takes an `Arc<dyn NetworkProvider>` at construction time and
48/// uses Layer 3 to establish raw TCP connections before performing protocol
49/// upgrades (e.g., WebSocket handshake).
50#[allow(async_fn_in_trait)]
51pub trait StreamTransport: Send + Sync {
52    /// The framed stream type produced by this transport.
53    type Stream: FramedStream;
54
55    /// Connect to a peer address and return a framed bidirectional stream.
56    ///
57    /// The implementation should:
58    /// 1. Dial the peer via Layer 3 (`NetworkProvider::dial_tcp`)
59    /// 2. Perform any protocol upgrade (e.g., WebSocket handshake)
60    /// 3. Exchange a transport-level handshake message
61    /// 4. Return the resulting framed stream
62    async fn connect(&self, addr: &PeerAddr) -> Result<Self::Stream, TransportError>;
63
64    /// Start listening for incoming framed connections.
65    ///
66    /// Returns a [`StreamListener`] that yields framed streams as peers connect.
67    async fn listen(&self) -> Result<StreamListener<Self::Stream>, TransportError>;
68}
69
70/// A framed bidirectional stream (message-oriented, not byte-oriented).
71///
72/// Each `send()` call transmits one discrete message; each `recv()` call
73/// returns one discrete message. The transport handles framing internally
74/// (e.g., WebSocket frames, length-prefixed frames).
75///
76/// This is the primary interface that Layer 5 (Session) uses to exchange
77/// data with peers.
78#[allow(async_fn_in_trait)]
79pub trait FramedStream: Send + Sync + 'static {
80    /// Send a binary message to the peer.
81    async fn send(&mut self, data: &[u8]) -> Result<(), TransportError>;
82
83    /// Receive the next binary message from the peer.
84    ///
85    /// Returns `Ok(None)` when the stream is cleanly closed by the remote side.
86    async fn recv(&mut self) -> Result<Option<Vec<u8>>, TransportError>;
87
88    /// Gracefully close the stream.
89    async fn close(&mut self) -> Result<(), TransportError>;
90
91    /// The remote peer's address as a human-readable string.
92    fn peer_addr(&self) -> String;
93}
94
95/// A raw byte stream transport (not framed).
96///
97/// Used for: file transfer, TCP proxy, HTTP forwarding.
98/// Implementations: TCP, QUIC unidirectional streams (future).
99///
100/// Returns plain `TcpStream`s that Layer 7 applications use directly
101/// for byte-oriented I/O.
102#[allow(async_fn_in_trait)]
103pub trait RawTransport: Send + Sync {
104    /// Open a raw byte stream to a peer on the given port.
105    ///
106    /// Delegates to `NetworkProvider::dial_tcp` and returns the resulting
107    /// `TcpStream` without any protocol upgrade.
108    async fn open(&self, addr: &PeerAddr, port: u16) -> Result<TcpStream, TransportError>;
109
110    /// Listen for incoming raw byte streams on a port.
111    ///
112    /// Returns a [`RawListener`] that yields `TcpStream`s as peers connect.
113    async fn listen(&self, port: u16) -> Result<RawListener, TransportError>;
114}
115
116/// Unreliable datagram transport.
117///
118/// Used for: real-time video/audio, game state synchronization.
119/// Implementations: UDP, QUIC datagrams (future).
120///
121/// When backed by a [`NetworkProvider`](crate::network::NetworkProvider) that
122/// supports UDP (e.g., `TailscaleProvider`), the transport delegates to
123/// `NetworkProvider::bind_udp` and wraps the returned [`NetworkUdpSocket`](crate::network::NetworkUdpSocket)
124/// in a [`DatagramSocket`].
125#[allow(async_fn_in_trait)]
126pub trait DatagramTransport: Send + Sync {
127    /// Bind a datagram socket to a peer address.
128    ///
129    /// Returns a [`DatagramSocket`] that can send and receive datagrams.
130    async fn bind(&self, port: u16) -> Result<DatagramSocket, TransportError>;
131}
132
133// ---------------------------------------------------------------------------
134// Supporting types
135// ---------------------------------------------------------------------------
136
137/// A listener that yields framed streams as peers connect.
138///
139/// Wraps a `tokio::sync::mpsc::Receiver` internally. Call [`accept()`](Self::accept)
140/// in a loop to handle incoming connections.
141pub struct StreamListener<S: FramedStream> {
142    /// Receiver for incoming framed streams.
143    rx: tokio::sync::mpsc::Receiver<S>,
144    /// Port this listener is bound to.
145    pub port: u16,
146}
147
148impl<S: FramedStream> StreamListener<S> {
149    /// Create a new stream listener from a receiver and port.
150    pub fn new(rx: tokio::sync::mpsc::Receiver<S>, port: u16) -> Self {
151        Self { rx, port }
152    }
153
154    /// Accept the next incoming framed stream.
155    ///
156    /// Returns `None` when the listener has been shut down.
157    pub async fn accept(&mut self) -> Option<S> {
158        self.rx.recv().await
159    }
160}
161
162/// A listener that yields raw TCP streams as peers connect.
163///
164/// Wraps a `tokio::sync::mpsc::Receiver` internally. Call [`accept()`](Self::accept)
165/// in a loop to handle incoming connections.
166#[derive(Debug)]
167pub struct RawListener {
168    /// Receiver for incoming connections.
169    rx: tokio::sync::mpsc::Receiver<RawIncoming>,
170    /// Port this listener is bound to.
171    pub port: u16,
172}
173
174impl RawListener {
175    /// Create a new raw listener from a receiver and port.
176    pub fn new(rx: tokio::sync::mpsc::Receiver<RawIncoming>, port: u16) -> Self {
177        Self { rx, port }
178    }
179
180    /// Accept the next incoming raw connection.
181    ///
182    /// Returns `None` when the listener has been shut down.
183    pub async fn accept(&mut self) -> Option<RawIncoming> {
184        self.rx.recv().await
185    }
186}
187
188/// An incoming raw TCP connection with metadata.
189pub struct RawIncoming {
190    /// The raw TCP stream.
191    pub stream: TcpStream,
192    /// Remote address of the connecting peer.
193    pub remote_addr: String,
194    /// The connecting peer's Tailscale-authenticated identity (WhoIs), parsed
195    /// from the bridge header. `None` when the network provider supplies no
196    /// identity (e.g., the mock provider) or the header value did not parse as
197    /// identity JSON (a legacy bare DNS name).
198    pub remote_identity: Option<crate::network::TailscalePeerIdentity>,
199}
200
201/// A datagram socket for sending and receiving unreliable packets.
202///
203/// Supports two modes:
204/// - **Direct**: Wraps a `tokio::net::UdpSocket` for loopback/LAN use.
205/// - **Network**: Wraps a [`NetworkUdpSocket`](crate::network::NetworkUdpSocket)
206///   for Tailscale relay-based UDP.
207pub enum DatagramSocket {
208    /// A direct UDP socket (loopback / LAN).
209    Direct {
210        /// The underlying UDP socket.
211        socket: tokio::net::UdpSocket,
212    },
213    /// A network-relayed UDP socket (Tailscale tsnet).
214    Network {
215        /// The network UDP socket with address-framed relay.
216        socket: crate::network::NetworkUdpSocket,
217    },
218}
219
220impl DatagramSocket {
221    /// Create a direct datagram socket from a tokio UdpSocket.
222    pub fn direct(socket: tokio::net::UdpSocket) -> Self {
223        Self::Direct { socket }
224    }
225
226    /// Create a network-relayed datagram socket from a NetworkUdpSocket.
227    pub fn network(socket: crate::network::NetworkUdpSocket) -> Self {
228        Self::Network { socket }
229    }
230
231    /// Send a datagram to the specified address.
232    pub async fn send_to(&self, data: &[u8], addr: &str) -> Result<usize, TransportError> {
233        match self {
234            Self::Direct { socket } => socket.send_to(data, addr).await.map_err(TransportError::Io),
235            Self::Network { socket } => {
236                let sock_addr: std::net::SocketAddr = addr.parse().map_err(|e| {
237                    TransportError::ConnectFailed(format!("invalid address '{addr}': {e}"))
238                })?;
239                socket
240                    .send_to(data, sock_addr)
241                    .await
242                    .map_err(|e| TransportError::Io(std::io::Error::other(e.to_string())))
243            }
244        }
245    }
246
247    /// Receive a datagram, returning the data and sender address.
248    pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, String), TransportError> {
249        match self {
250            Self::Direct { socket } => {
251                let (n, addr) = socket.recv_from(buf).await.map_err(TransportError::Io)?;
252                Ok((n, addr.to_string()))
253            }
254            Self::Network { socket } => {
255                let (n, addr) = socket
256                    .recv_from(buf)
257                    .await
258                    .map_err(|e| TransportError::Io(std::io::Error::other(e.to_string())))?;
259                Ok((n, addr.to_string()))
260            }
261        }
262    }
263
264    /// Return the local address this socket is bound to.
265    pub fn local_addr(&self) -> Result<std::net::SocketAddr, TransportError> {
266        match self {
267            Self::Direct { socket } => socket.local_addr().map_err(TransportError::Io),
268            Self::Network { socket } => socket
269                .local_addr()
270                .map_err(|e| TransportError::Io(std::io::Error::other(e.to_string()))),
271        }
272    }
273}
274
275// ---------------------------------------------------------------------------
276// Handshake types (used by WebSocketTransport)
277// ---------------------------------------------------------------------------
278
279/// Transport-level handshake message exchanged during connection setup.
280///
281/// This is a Layer 4 concern — it identifies the transport endpoint,
282/// not the application. Layer 5 (Session) will use this to associate
283/// the connection with a known peer.
284#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
285pub struct Handshake {
286    /// Identifier of the connecting peer (e.g., Tailscale stable node ID).
287    pub peer_id: String,
288    /// Capabilities supported by this peer (e.g., ["ws", "binary", "compress"]).
289    pub capabilities: Vec<String>,
290    /// Protocol version for compatibility negotiation.
291    pub protocol_version: u32,
292}
293
294/// Current transport protocol version.
295pub const PROTOCOL_VERSION: u32 = 1;
296
297// ---------------------------------------------------------------------------
298// Configuration
299// ---------------------------------------------------------------------------
300
301// Re-export transport configurations from submodules.
302pub use quic::QuicConfig;
303pub use udp::UdpConfig;
304
305/// Configuration for WebSocket transport.
306#[derive(Debug, Clone)]
307pub struct WsConfig {
308    /// Port to listen on for incoming WebSocket connections.
309    pub port: u16,
310    /// Interval between ping frames sent to the peer.
311    pub ping_interval: Duration,
312    /// Maximum time to wait for a pong response before considering
313    /// the connection dead.
314    pub pong_timeout: Duration,
315    /// Maximum WebSocket message size in bytes.
316    pub max_message_size: usize,
317    /// Maximum number of concurrent in-flight incoming handshakes (WS
318    /// upgrade + hello exchange). Connections beyond the cap are dropped
319    /// before the upgrade; established connections are not counted.
320    pub max_pending_handshakes: usize,
321    /// Maximum time an incoming connection may take to complete the WS
322    /// upgrade + hello exchange before it is dropped. Must exceed
323    /// HELLO_TIMEOUT (5s) so hello timeouts keep their specific
324    /// classification.
325    pub handshake_timeout: Duration,
326}
327
328impl Default for WsConfig {
329    fn default() -> Self {
330        Self {
331            port: 9417,
332            ping_interval: Duration::from_secs(10),
333            pong_timeout: Duration::from_secs(30),
334            max_message_size: 16 * 1024 * 1024, // 16 MiB
335            max_pending_handshakes: 256,
336            handshake_timeout: Duration::from_secs(10),
337        }
338    }
339}
340
341// ---------------------------------------------------------------------------
342// Shared helpers
343// ---------------------------------------------------------------------------
344
345/// Resolve the best dial address from a [`PeerAddr`].
346///
347/// Prefers IP address (most reliable for Tailscale), falls back to DNS name,
348/// then hostname.
349pub(crate) fn resolve_dial_addr(addr: &PeerAddr) -> String {
350    if let Some(ip) = &addr.ip {
351        ip.to_string()
352    } else if let Some(dns) = &addr.dns_name {
353        dns.clone()
354    } else {
355        addr.hostname.clone()
356    }
357}
358
359// ---------------------------------------------------------------------------
360// Errors
361// ---------------------------------------------------------------------------
362
363/// Errors from Layer 4 transport operations.
364#[derive(Debug, thiserror::Error)]
365pub enum TransportError {
366    /// The requested transport or feature is not implemented.
367    #[error("not implemented: {0}")]
368    NotImplemented(String),
369
370    /// Failed to connect to the peer.
371    #[error("connect failed: {0}")]
372    ConnectFailed(String),
373
374    /// Failed to start listening.
375    #[error("listen failed: {0}")]
376    ListenFailed(String),
377
378    /// The transport-level handshake failed.
379    #[error("handshake failed: {0}")]
380    HandshakeFailed(String),
381
382    /// Protocol version mismatch between peers.
383    #[error("protocol version mismatch: local={local}, remote={remote}")]
384    VersionMismatch {
385        /// Local protocol version.
386        local: u32,
387        /// Remote protocol version.
388        remote: u32,
389    },
390
391    /// The remote peer did not produce a hello envelope within the timeout
392    /// (RFC 017 §8).
393    #[error("hello timeout: no envelope received within 5s")]
394    HelloTimeout,
395
396    /// The remote peer sent a hello envelope that did not parse as a valid
397    /// [`HelloEnvelope`](crate::session::HelloEnvelope), or used a version
398    /// we do not support (RFC 017 §8, close code 4002).
399    #[error("hello protocol error: {0}")]
400    HelloMalformed(String),
401
402    /// The remote peer advertised a different `app_id` in its hello envelope
403    /// than we did (RFC 017 §8, close code 4001).
404    #[error("app mismatch: local={local}, remote={remote}")]
405    AppMismatch {
406        /// Our local `app_id`.
407        local: String,
408        /// The `app_id` advertised by the remote peer.
409        remote: String,
410    },
411
412    /// The hello envelope's claimed `tailscale_id` did not match the
413    /// Tailscale-authenticated identity of the connection (RFC 017 §8,
414    /// close code 4003).
415    #[error("identity mismatch: hello claimed tailscale_id={claimed}, authenticated nodeId={authenticated}")]
416    IdentityMismatch {
417        /// The `tailscale_id` the hello envelope claimed.
418        claimed: String,
419        /// The Tailscale-authenticated node ID (WhoIs) of the connection.
420        authenticated: String,
421    },
422
423    /// The connection was closed unexpectedly.
424    #[error("connection closed: {0}")]
425    ConnectionClosed(String),
426
427    /// A send or receive operation timed out.
428    #[error("timeout: {0}")]
429    Timeout(String),
430
431    /// Heartbeat (ping/pong) failure — the peer stopped responding.
432    #[error("heartbeat timeout after {0:?}")]
433    HeartbeatTimeout(Duration),
434
435    /// WebSocket protocol error.
436    #[error("websocket error: {0}")]
437    WebSocket(String),
438
439    /// I/O error from the underlying stream.
440    #[error("io error: {0}")]
441    Io(#[from] std::io::Error),
442
443    /// Serialization/deserialization error (e.g., handshake JSON).
444    #[error("serialization error: {0}")]
445    Serialize(String),
446
447    /// Error from Layer 3 (network).
448    #[error("network error: {0}")]
449    Network(#[from] crate::network::NetworkError),
450}