Skip to main content

truffle_core/network/
mod.rs

1//! Layer 3: Network — Peer discovery, addressing, encrypted tunnels.
2//!
3//! This module defines the [`NetworkProvider`] trait, the public API for Layer 3.
4//! The trait is generic — no Tailscale-specific types leak through.
5//!
6//! The [`tailscale`] submodule contains the [`TailscaleProvider`] implementation
7//! that wraps the Go sidecar (tsnet) and bridge.
8
9pub mod tailscale;
10
11use std::net::{IpAddr, Ipv4Addr, SocketAddr};
12use std::time::Duration;
13
14use tokio::net::TcpStream;
15use tokio::sync::broadcast;
16
17// ---------------------------------------------------------------------------
18// NetworkProvider trait — the public API of Layer 3
19// ---------------------------------------------------------------------------
20
21/// Provides network-level peer discovery and raw connectivity.
22///
23/// The primary implementation is [`TailscaleProvider`](tailscale::TailscaleProvider)
24/// which uses tsnet via a Go sidecar. The trait is designed to be swappable —
25/// future providers could use mDNS (LAN), STUN/TURN (internet), or Bluetooth.
26///
27/// # Layer rules
28///
29/// - Layer 3 does NOT know about WebSocket, QUIC, or any Layer 4 protocol
30/// - Layer 3 does NOT know about envelopes, namespaces, or messages
31/// - Layer 3 provides raw `TcpStream` — not framed connections
32/// - `peer_events()` is the ONLY source of peer events — no polling, no announce
33///
34/// All async methods return `Send` futures so that `Node<N>` can be used
35/// inside `tokio::spawn` tasks (required by the file transfer subsystem
36/// and any other code that needs to spawn tasks with node access).
37pub trait NetworkProvider: Send + Sync {
38    /// Start the network provider.
39    ///
40    /// This spawns child processes, binds ports, and performs authentication.
41    /// If authentication is required (e.g., Tailscale browser auth), the provider
42    /// emits `NetworkPeerEvent::AuthRequired { url }` via `peer_events()` and
43    /// **keeps waiting** until auth completes or the timeout is reached.
44    ///
45    /// Callers should subscribe to `peer_events()` BEFORE calling `start()` to
46    /// receive auth URLs and display them to the user.
47    ///
48    /// Returns `Ok(())` when the provider is fully online, or `Err` if auth
49    /// times out or a fatal error occurs.
50    fn start(&mut self) -> impl std::future::Future<Output = Result<(), NetworkError>> + Send;
51
52    /// Stop the network provider and clean up all resources.
53    fn stop(&mut self) -> impl std::future::Future<Output = Result<(), NetworkError>> + Send;
54
55    /// Local node's identity (stable ID, hostname, display name).
56    ///
57    /// Returns a clone of the current cached identity. The identity is
58    /// populated after [`start()`](Self::start) completes and may be
59    /// updated when the sidecar reports `tsnet:status`.
60    fn local_identity(&self) -> NodeIdentity;
61
62    /// Local node's network address.
63    ///
64    /// Returns a clone of the current cached address. The address is
65    /// populated after [`start()`](Self::start) completes and may be
66    /// updated when the sidecar reports `tsnet:status`.
67    fn local_addr(&self) -> PeerAddr;
68
69    // ── Discovery (event-driven, NOT polling) ──
70
71    /// Subscribe to peer events. Fires immediately when peers join/leave/update.
72    ///
73    /// Uses `WatchIPNBus` for real-time notifications instead of polling.
74    fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent>;
75
76    /// Snapshot of all currently known peers.
77    fn peers(&self) -> impl std::future::Future<Output = Vec<NetworkPeer>> + Send;
78
79    // ── Connectivity primitives for Layer 4 ──
80
81    /// Dial a TCP connection to a peer via the encrypted Tailscale tunnel.
82    ///
83    /// Returns a plain `TcpStream` — all bridge internals (pending_dials,
84    /// session token, binary headers) are hidden inside the provider.
85    fn dial_tcp(
86        &self,
87        addr: &str,
88        port: u16,
89    ) -> impl std::future::Future<Output = Result<TcpStream, NetworkError>> + Send;
90
91    /// Listen for incoming TCP connections on a port via the Tailscale tunnel.
92    ///
93    /// The returned receiver yields `TcpStream`s for each accepted connection.
94    fn listen_tcp(
95        &self,
96        port: u16,
97    ) -> impl std::future::Future<Output = Result<NetworkTcpListener, NetworkError>> + Send;
98
99    /// Stop listening on a previously opened port.
100    fn unlisten_tcp(
101        &self,
102        port: u16,
103    ) -> impl std::future::Future<Output = Result<(), NetworkError>> + Send;
104
105    /// Bind a UDP socket on a port via the network tunnel.
106    ///
107    /// Returns a [`NetworkUdpSocket`] that transparently relays datagrams
108    /// through the network provider. The socket supports `send_to` / `recv_from`
109    /// with full remote address information.
110    ///
111    /// Not all providers support UDP. Returns [`NetworkError::Internal`] if
112    /// the provider has not implemented UDP transport yet.
113    fn bind_udp(
114        &self,
115        port: u16,
116    ) -> impl std::future::Future<Output = Result<NetworkUdpSocket, NetworkError>> + Send;
117
118    // ── Diagnostics ──
119
120    /// Ping a peer via the network layer (Tailscale TSMP).
121    fn ping(
122        &self,
123        addr: &str,
124    ) -> impl std::future::Future<Output = Result<PingResult, NetworkError>> + Send;
125
126    /// Node health info (key expiry, connection quality, warnings).
127    fn health(&self) -> impl std::future::Future<Output = HealthInfo> + Send;
128}
129
130// ---------------------------------------------------------------------------
131// Public types
132// ---------------------------------------------------------------------------
133
134/// A peer as seen by the network layer (Layer 3).
135///
136/// Contains only information available from the network provider itself
137/// (e.g., Tailscale status). No transport or session state.
138#[derive(Debug, Clone)]
139pub struct NetworkPeer {
140    /// Stable node ID from the network provider.
141    pub id: String,
142    /// Hostname on the network (e.g., "truffle-cli-abc123").
143    pub hostname: String,
144    /// Network IP address (e.g., 100.x.x.x for Tailscale).
145    pub ip: IpAddr,
146    /// Whether the peer is currently online.
147    pub online: bool,
148    /// Direct endpoint address, if connected directly.
149    pub cur_addr: Option<String>,
150    /// DERP relay name if connection is relayed.
151    pub relay: Option<String>,
152    /// Operating system of the peer.
153    pub os: Option<String>,
154    /// Last time the peer was seen online (RFC 3339 string).
155    pub last_seen: Option<String>,
156    /// Key expiry timestamp (RFC 3339 string).
157    pub key_expiry: Option<String>,
158    /// DNS name on the tailnet (e.g., "truffle-cli-abc123.tailnet.ts.net").
159    pub dns_name: Option<String>,
160}
161
162/// Events emitted when network peers change state.
163#[derive(Debug, Clone)]
164pub enum NetworkPeerEvent {
165    /// A new peer appeared on the network.
166    Joined(NetworkPeer),
167    /// A peer left the network (by stable node ID).
168    Left(String),
169    /// A peer's metadata changed (IP, relay, online status, etc.).
170    Updated(NetworkPeer),
171    /// Authentication is required. The URL should be shown to the user.
172    /// Emitted during `start()` — the provider keeps waiting for auth to complete.
173    /// May be emitted multiple times if the URL expires and is refreshed.
174    AuthRequired {
175        /// URL the user should open in a browser.
176        url: String,
177    },
178}
179
180/// Network address of a peer.
181#[derive(Debug, Clone, Default)]
182pub struct PeerAddr {
183    /// IP address (100.x.x.x for Tailscale).
184    pub ip: Option<IpAddr>,
185    /// Hostname on the network.
186    pub hostname: String,
187    /// DNS name on the tailnet.
188    pub dns_name: Option<String>,
189}
190
191/// Identity of the local node on the network.
192///
193/// Carries the RFC 017 identity triple: `app_id` (namespace), `device_id`
194/// (stable per-device ULID), and `device_name` (human-readable). The
195/// Tailscale hostname and stable ID are kept alongside as escape hatches
196/// and for internal filtering.
197#[derive(Debug, Clone, Default)]
198pub struct NodeIdentity {
199    /// Application namespace identifier (RFC 017 §5.1).
200    pub app_id: String,
201    /// Stable per-device ULID (RFC 017 §5.4).
202    pub device_id: String,
203    /// Human-readable device name, original (unsanitised) string.
204    pub device_name: String,
205    /// Tailscale hostname — `truffle-{app_id}-{slug(device_name)}`.
206    pub tailscale_hostname: String,
207    /// Tailscale stable node ID (populated after the sidecar reaches Running).
208    pub tailscale_id: String,
209    /// DNS name on the tailnet.
210    pub dns_name: Option<String>,
211    /// Tailscale IP address.
212    pub ip: Option<IpAddr>,
213}
214
215/// Result of a network-level ping.
216#[derive(Debug, Clone)]
217pub struct PingResult {
218    /// Round-trip latency.
219    pub latency: Duration,
220    /// Connection type description (e.g., "direct" or "relay:sfo").
221    pub connection: String,
222    /// Direct peer endpoint address, if available.
223    pub peer_addr: Option<String>,
224}
225
226/// Health information from the network provider.
227#[derive(Debug, Clone, Default)]
228pub struct HealthInfo {
229    /// Current backend state (e.g., "Running", "NeedsLogin").
230    pub state: String,
231    /// Key expiry timestamp (RFC 3339), if applicable.
232    pub key_expiry: Option<String>,
233    /// Active health warnings.
234    pub warnings: Vec<String>,
235    /// Whether the network is fully operational.
236    pub healthy: bool,
237}
238
239/// A listener for incoming TCP connections via the network provider.
240///
241/// Wraps a channel that receives `TcpStream`s from the bridge. The bridge
242/// internals (binary headers, session tokens) are completely hidden.
243pub struct NetworkTcpListener {
244    /// Port this listener is bound to.
245    pub port: u16,
246    /// Receiver for incoming connections.
247    pub incoming: tokio::sync::mpsc::Receiver<IncomingConnection>,
248}
249
250/// An incoming TCP connection with metadata.
251#[derive(Debug)]
252pub struct IncomingConnection {
253    /// The raw TCP stream (bridge headers already consumed).
254    pub stream: TcpStream,
255    /// Remote address of the connecting peer.
256    pub remote_addr: String,
257    /// Remote DNS name or peer identity JSON.
258    pub remote_identity: String,
259    /// Port the connection arrived on.
260    pub port: u16,
261}
262
263// ---------------------------------------------------------------------------
264// NetworkUdpSocket — address-framed UDP relay wrapper
265// ---------------------------------------------------------------------------
266
267/// A UDP socket that relays datagrams through the network provider.
268///
269/// Under the hood, the Rust side talks to a local relay socket. Each outbound
270/// datagram is prefixed with a 6-byte address header (`[4-byte IPv4][2-byte port BE]`)
271/// so the relay (Go sidecar) knows where to forward the packet on the tsnet
272/// network. Inbound datagrams arrive with the same header prepended by the relay.
273///
274/// This struct hides the framing — callers use `send_to` / `recv_from` with
275/// normal `SocketAddr` values.
276pub struct NetworkUdpSocket {
277    /// The underlying tokio UDP socket connected to the local relay.
278    inner: tokio::net::UdpSocket,
279    /// The tsnet-bound port (the logical port on the Tailscale network).
280    tsnet_port: u16,
281}
282
283/// Address header size: 4 bytes IPv4 + 2 bytes port (big-endian).
284const UDP_ADDR_HEADER_SIZE: usize = 6;
285
286impl NetworkUdpSocket {
287    /// Create a new `NetworkUdpSocket` from a tokio UdpSocket and the tsnet port.
288    pub(crate) fn new(inner: tokio::net::UdpSocket, tsnet_port: u16) -> Self {
289        Self { inner, tsnet_port }
290    }
291
292    /// Send a datagram to the specified address via the relay.
293    ///
294    /// The relay will forward the datagram to the target on the tsnet network.
295    pub async fn send_to(&self, data: &[u8], addr: SocketAddr) -> Result<usize, NetworkError> {
296        let ip = match addr.ip() {
297            IpAddr::V4(v4) => v4,
298            IpAddr::V6(_) => {
299                return Err(NetworkError::Internal(
300                    "NetworkUdpSocket: IPv6 not supported in relay framing".into(),
301                ));
302            }
303        };
304        let port = addr.port();
305
306        // Build framed packet: [4-byte IPv4][2-byte port BE][payload]
307        let mut framed = Vec::with_capacity(UDP_ADDR_HEADER_SIZE + data.len());
308        framed.extend_from_slice(&ip.octets());
309        framed.extend_from_slice(&port.to_be_bytes());
310        framed.extend_from_slice(data);
311
312        tracing::debug!(
313            target_addr = %addr,
314            payload_len = data.len(),
315            framed_len = framed.len(),
316            relay_addr = ?self.inner.peer_addr().ok(),
317            "NetworkUdpSocket: sending framed datagram to relay"
318        );
319
320        let n = self.inner.send(&framed).await.map_err(NetworkError::Io)?;
321
322        tracing::debug!(
323            bytes_sent = n,
324            "NetworkUdpSocket: framed datagram sent to relay"
325        );
326
327        // Return the number of payload bytes sent (subtract header)
328        Ok(n.saturating_sub(UDP_ADDR_HEADER_SIZE))
329    }
330
331    /// Receive a datagram from the relay, returning the payload and sender address.
332    ///
333    /// The relay prepends a 6-byte address header to each inbound datagram.
334    pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr), NetworkError> {
335        tracing::debug!("NetworkUdpSocket: waiting for inbound datagram from relay...");
336
337        // Read into a temporary buffer that includes space for the header
338        let mut tmp = vec![0u8; UDP_ADDR_HEADER_SIZE + buf.len()];
339        let n = self.inner.recv(&mut tmp).await.map_err(NetworkError::Io)?;
340
341        if n < UDP_ADDR_HEADER_SIZE {
342            return Err(NetworkError::Internal(
343                "NetworkUdpSocket: received packet too short for address header".into(),
344            ));
345        }
346
347        // Parse address header
348        let ip = Ipv4Addr::new(tmp[0], tmp[1], tmp[2], tmp[3]);
349        let port = u16::from_be_bytes([tmp[4], tmp[5]]);
350        let addr = SocketAddr::new(IpAddr::V4(ip), port);
351
352        // Copy payload to caller's buffer
353        let payload_len = n - UDP_ADDR_HEADER_SIZE;
354        buf[..payload_len].copy_from_slice(&tmp[UDP_ADDR_HEADER_SIZE..n]);
355
356        tracing::debug!(
357            raw_bytes = n,
358            payload_len = payload_len,
359            sender_addr = %addr,
360            "NetworkUdpSocket: received inbound datagram from relay"
361        );
362
363        Ok((payload_len, addr))
364    }
365
366    /// Return the local address of the underlying relay socket.
367    pub fn local_addr(&self) -> Result<SocketAddr, NetworkError> {
368        self.inner.local_addr().map_err(NetworkError::Io)
369    }
370
371    /// Return the tsnet-bound port (the logical port on the Tailscale network).
372    pub fn tsnet_port(&self) -> u16 {
373        self.tsnet_port
374    }
375
376    /// Get a reference to the inner tokio UdpSocket.
377    ///
378    /// This is the socket connected to the local relay. Direct reads/writes
379    /// bypass the address framing — prefer `send_to` / `recv_from` instead.
380    pub fn inner(&self) -> &tokio::net::UdpSocket {
381        &self.inner
382    }
383}
384
385impl std::fmt::Debug for NetworkUdpSocket {
386    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387        f.debug_struct("NetworkUdpSocket")
388            .field("tsnet_port", &self.tsnet_port)
389            .field("local_addr", &self.inner.local_addr().ok())
390            .finish()
391    }
392}
393
394// ---------------------------------------------------------------------------
395// Errors
396// ---------------------------------------------------------------------------
397
398/// Errors from Layer 3 network operations.
399#[derive(Debug, thiserror::Error)]
400pub enum NetworkError {
401    /// The network provider is not running.
402    #[error("network provider not running")]
403    NotRunning,
404
405    /// The network provider is already running.
406    #[error("network provider already running")]
407    AlreadyRunning,
408
409    /// Failed to start the network provider.
410    #[error("start failed: {0}")]
411    StartFailed(String),
412
413    /// Failed to stop the network provider.
414    #[error("stop failed: {0}")]
415    StopFailed(String),
416
417    /// Authentication is required.
418    #[error("authentication required: {url}")]
419    AuthRequired { url: String },
420
421    /// A dial operation failed.
422    #[error("dial failed: {0}")]
423    DialFailed(String),
424
425    /// A dial operation timed out.
426    #[error("dial timed out after {0:?}")]
427    DialTimeout(Duration),
428
429    /// A listen operation failed.
430    #[error("listen failed: {0}")]
431    ListenFailed(String),
432
433    /// A ping operation failed.
434    #[error("ping failed: {0}")]
435    PingFailed(String),
436
437    /// The sidecar process crashed or is unavailable.
438    #[error("sidecar error: {0}")]
439    SidecarError(String),
440
441    /// Bridge communication error.
442    #[error("bridge error: {0}")]
443    BridgeError(String),
444
445    /// I/O error.
446    #[error("io error: {0}")]
447    Io(#[from] std::io::Error),
448
449    /// Serialization error.
450    #[error("serialization error: {0}")]
451    Serialize(#[from] serde_json::Error),
452
453    /// Generic internal error.
454    #[error("internal error: {0}")]
455    Internal(String),
456}