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).
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. Stubs only in this phase.
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.
166pub struct RawListener {
167 /// Receiver for incoming connections.
168 rx: tokio::sync::mpsc::Receiver<RawIncoming>,
169 /// Port this listener is bound to.
170 pub port: u16,
171}
172
173impl RawListener {
174 /// Create a new raw listener from a receiver and port.
175 pub fn new(rx: tokio::sync::mpsc::Receiver<RawIncoming>, port: u16) -> Self {
176 Self { rx, port }
177 }
178
179 /// Accept the next incoming raw connection.
180 ///
181 /// Returns `None` when the listener has been shut down.
182 pub async fn accept(&mut self) -> Option<RawIncoming> {
183 self.rx.recv().await
184 }
185}
186
187/// An incoming raw TCP connection with metadata.
188pub struct RawIncoming {
189 /// The raw TCP stream.
190 pub stream: TcpStream,
191 /// Remote address of the connecting peer.
192 pub remote_addr: String,
193}
194
195/// A datagram socket for sending and receiving unreliable packets.
196///
197/// Supports two modes:
198/// - **Direct**: Wraps a `tokio::net::UdpSocket` for loopback/LAN use.
199/// - **Network**: Wraps a [`NetworkUdpSocket`](crate::network::NetworkUdpSocket)
200/// for Tailscale relay-based UDP.
201pub enum DatagramSocket {
202 /// A direct UDP socket (loopback / LAN).
203 Direct {
204 /// The underlying UDP socket.
205 socket: tokio::net::UdpSocket,
206 },
207 /// A network-relayed UDP socket (Tailscale tsnet).
208 Network {
209 /// The network UDP socket with address-framed relay.
210 socket: crate::network::NetworkUdpSocket,
211 },
212}
213
214impl DatagramSocket {
215 /// Create a direct datagram socket from a tokio UdpSocket.
216 pub fn direct(socket: tokio::net::UdpSocket) -> Self {
217 Self::Direct { socket }
218 }
219
220 /// Create a network-relayed datagram socket from a NetworkUdpSocket.
221 pub fn network(socket: crate::network::NetworkUdpSocket) -> Self {
222 Self::Network { socket }
223 }
224
225 /// Send a datagram to the specified address.
226 pub async fn send_to(&self, data: &[u8], addr: &str) -> Result<usize, TransportError> {
227 match self {
228 Self::Direct { socket } => socket.send_to(data, addr).await.map_err(TransportError::Io),
229 Self::Network { socket } => {
230 let sock_addr: std::net::SocketAddr = addr.parse().map_err(|e| {
231 TransportError::ConnectFailed(format!("invalid address '{addr}': {e}"))
232 })?;
233 socket.send_to(data, sock_addr).await.map_err(|e| {
234 TransportError::Io(std::io::Error::new(
235 std::io::ErrorKind::Other,
236 e.to_string(),
237 ))
238 })
239 }
240 }
241 }
242
243 /// Receive a datagram, returning the data and sender address.
244 pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, String), TransportError> {
245 match self {
246 Self::Direct { socket } => {
247 let (n, addr) = socket.recv_from(buf).await.map_err(TransportError::Io)?;
248 Ok((n, addr.to_string()))
249 }
250 Self::Network { socket } => {
251 let (n, addr) = socket.recv_from(buf).await.map_err(|e| {
252 TransportError::Io(std::io::Error::new(
253 std::io::ErrorKind::Other,
254 e.to_string(),
255 ))
256 })?;
257 Ok((n, addr.to_string()))
258 }
259 }
260 }
261
262 /// Return the local address this socket is bound to.
263 pub fn local_addr(&self) -> Result<std::net::SocketAddr, TransportError> {
264 match self {
265 Self::Direct { socket } => socket.local_addr().map_err(TransportError::Io),
266 Self::Network { socket } => socket.local_addr().map_err(|e| {
267 TransportError::Io(std::io::Error::new(
268 std::io::ErrorKind::Other,
269 e.to_string(),
270 ))
271 }),
272 }
273 }
274}
275
276// ---------------------------------------------------------------------------
277// Handshake types (used by WebSocketTransport)
278// ---------------------------------------------------------------------------
279
280/// Transport-level handshake message exchanged during connection setup.
281///
282/// This is a Layer 4 concern — it identifies the transport endpoint,
283/// not the application. Layer 5 (Session) will use this to associate
284/// the connection with a known peer.
285#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
286pub struct Handshake {
287 /// Identifier of the connecting peer (e.g., Tailscale stable node ID).
288 pub peer_id: String,
289 /// Capabilities supported by this peer (e.g., ["ws", "binary", "compress"]).
290 pub capabilities: Vec<String>,
291 /// Protocol version for compatibility negotiation.
292 pub protocol_version: u32,
293}
294
295/// Current transport protocol version.
296pub const PROTOCOL_VERSION: u32 = 1;
297
298// ---------------------------------------------------------------------------
299// Configuration
300// ---------------------------------------------------------------------------
301
302// Re-export transport configurations from submodules.
303pub use quic::QuicConfig;
304pub use udp::UdpConfig;
305
306/// Configuration for WebSocket transport.
307#[derive(Debug, Clone)]
308pub struct WsConfig {
309 /// Port to listen on for incoming WebSocket connections.
310 pub port: u16,
311 /// Interval between ping frames sent to the peer.
312 pub ping_interval: Duration,
313 /// Maximum time to wait for a pong response before considering
314 /// the connection dead.
315 pub pong_timeout: Duration,
316 /// Maximum WebSocket message size in bytes.
317 pub max_message_size: usize,
318}
319
320impl Default for WsConfig {
321 fn default() -> Self {
322 Self {
323 port: 9417,
324 ping_interval: Duration::from_secs(10),
325 pong_timeout: Duration::from_secs(30),
326 max_message_size: 16 * 1024 * 1024, // 16 MiB
327 }
328 }
329}
330
331// ---------------------------------------------------------------------------
332// Shared helpers
333// ---------------------------------------------------------------------------
334
335/// Resolve the best dial address from a [`PeerAddr`].
336///
337/// Prefers IP address (most reliable for Tailscale), falls back to DNS name,
338/// then hostname.
339pub(crate) fn resolve_dial_addr(addr: &PeerAddr) -> String {
340 if let Some(ip) = &addr.ip {
341 ip.to_string()
342 } else if let Some(dns) = &addr.dns_name {
343 dns.clone()
344 } else {
345 addr.hostname.clone()
346 }
347}
348
349// ---------------------------------------------------------------------------
350// Errors
351// ---------------------------------------------------------------------------
352
353/// Errors from Layer 4 transport operations.
354#[derive(Debug, thiserror::Error)]
355pub enum TransportError {
356 /// The requested transport or feature is not implemented.
357 #[error("not implemented: {0}")]
358 NotImplemented(String),
359
360 /// Failed to connect to the peer.
361 #[error("connect failed: {0}")]
362 ConnectFailed(String),
363
364 /// Failed to start listening.
365 #[error("listen failed: {0}")]
366 ListenFailed(String),
367
368 /// The transport-level handshake failed.
369 #[error("handshake failed: {0}")]
370 HandshakeFailed(String),
371
372 /// Protocol version mismatch between peers.
373 #[error("protocol version mismatch: local={local}, remote={remote}")]
374 VersionMismatch {
375 /// Local protocol version.
376 local: u32,
377 /// Remote protocol version.
378 remote: u32,
379 },
380
381 /// The remote peer did not produce a hello envelope within the timeout
382 /// (RFC 017 §8).
383 #[error("hello timeout: no envelope received within 5s")]
384 HelloTimeout,
385
386 /// The remote peer sent a hello envelope that did not parse as a valid
387 /// [`HelloEnvelope`](crate::session::HelloEnvelope), or used a version
388 /// we do not support (RFC 017 §8, close code 4002).
389 #[error("hello protocol error: {0}")]
390 HelloMalformed(String),
391
392 /// The remote peer advertised a different `app_id` in its hello envelope
393 /// than we did (RFC 017 §8, close code 4001).
394 #[error("app mismatch: local={local}, remote={remote}")]
395 AppMismatch {
396 /// Our local `app_id`.
397 local: String,
398 /// The `app_id` advertised by the remote peer.
399 remote: String,
400 },
401
402 /// The connection was closed unexpectedly.
403 #[error("connection closed: {0}")]
404 ConnectionClosed(String),
405
406 /// A send or receive operation timed out.
407 #[error("timeout: {0}")]
408 Timeout(String),
409
410 /// Heartbeat (ping/pong) failure — the peer stopped responding.
411 #[error("heartbeat timeout after {0:?}")]
412 HeartbeatTimeout(Duration),
413
414 /// WebSocket protocol error.
415 #[error("websocket error: {0}")]
416 WebSocket(String),
417
418 /// I/O error from the underlying stream.
419 #[error("io error: {0}")]
420 Io(#[from] std::io::Error),
421
422 /// Serialization/deserialization error (e.g., handshake JSON).
423 #[error("serialization error: {0}")]
424 Serialize(String),
425
426 /// Error from Layer 3 (network).
427 #[error("network error: {0}")]
428 Network(#[from] crate::network::NetworkError),
429}