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`](tailscale::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    // ── Reverse proxy (optional, requires sidecar) ──
130
131    /// Start a reverse proxy. Only supported by providers with sidecar integration.
132    fn proxy_add(
133        &self,
134        _config: ProxyAddParams,
135    ) -> impl std::future::Future<Output = Result<ProxyAddResult, NetworkError>> + Send {
136        std::future::ready(Err(NetworkError::Unsupported(
137            "proxy_add not supported by this provider".into(),
138        )))
139    }
140
141    /// Stop a reverse proxy.
142    fn proxy_remove(
143        &self,
144        _id: &str,
145    ) -> impl std::future::Future<Output = Result<(), NetworkError>> + Send {
146        std::future::ready(Err(NetworkError::Unsupported(
147            "proxy_remove not supported by this provider".into(),
148        )))
149    }
150
151    /// List active reverse proxies.
152    fn proxy_list(
153        &self,
154    ) -> impl std::future::Future<Output = Result<Vec<ProxyListEntry>, NetworkError>> + Send {
155        std::future::ready(Err(NetworkError::Unsupported(
156            "proxy_list not supported by this provider".into(),
157        )))
158    }
159}
160
161// ---------------------------------------------------------------------------
162// Public types
163// ---------------------------------------------------------------------------
164
165/// A peer as seen by the network layer (Layer 3).
166///
167/// Contains only information available from the network provider itself
168/// (e.g., Tailscale status). No transport or session state.
169#[derive(Debug, Clone)]
170pub struct NetworkPeer {
171    /// Stable node ID from the network provider.
172    pub id: String,
173    /// Hostname on the network (e.g., "truffle-cli-abc123").
174    pub hostname: String,
175    /// Network IP address (e.g., 100.x.x.x for Tailscale).
176    pub ip: IpAddr,
177    /// Whether the peer is currently online.
178    pub online: bool,
179    /// Direct endpoint address, if connected directly.
180    pub cur_addr: Option<String>,
181    /// DERP relay name if connection is relayed.
182    pub relay: Option<String>,
183    /// Operating system of the peer.
184    pub os: Option<String>,
185    /// Last time the peer was seen online (RFC 3339 string).
186    pub last_seen: Option<String>,
187    /// Key expiry timestamp (RFC 3339 string).
188    pub key_expiry: Option<String>,
189    /// DNS name on the tailnet (e.g., "truffle-cli-abc123.tailnet.ts.net").
190    pub dns_name: Option<String>,
191}
192
193/// Events emitted when network peers change state.
194#[derive(Debug, Clone)]
195pub enum NetworkPeerEvent {
196    /// A new peer appeared on the network.
197    Joined(NetworkPeer),
198    /// A peer left the network (by stable node ID).
199    Left(String),
200    /// A peer's metadata changed (IP, relay, online status, etc.).
201    Updated(NetworkPeer),
202    /// Authentication is required. The URL should be shown to the user.
203    /// Emitted during `start()` — the provider keeps waiting for auth to complete.
204    /// May be emitted multiple times if the URL expires and is refreshed.
205    AuthRequired {
206        /// URL the user should open in a browser.
207        url: String,
208    },
209}
210
211/// Network address of a peer.
212#[derive(Debug, Clone, Default)]
213pub struct PeerAddr {
214    /// IP address (100.x.x.x for Tailscale).
215    pub ip: Option<IpAddr>,
216    /// Hostname on the network.
217    pub hostname: String,
218    /// DNS name on the tailnet.
219    pub dns_name: Option<String>,
220}
221
222/// Identity of the local node on the network.
223///
224/// Carries the RFC 017 identity triple: `app_id` (namespace), `device_id`
225/// (stable per-device ULID), and `device_name` (human-readable). The
226/// Tailscale hostname and stable ID are kept alongside as escape hatches
227/// and for internal filtering.
228#[derive(Debug, Clone, Default)]
229pub struct NodeIdentity {
230    /// Application namespace identifier (RFC 017 §5.1).
231    pub app_id: String,
232    /// Stable per-device ULID (RFC 017 §5.4).
233    pub device_id: String,
234    /// Human-readable device name, original (unsanitised) string.
235    pub device_name: String,
236    /// Tailscale hostname — `truffle-{app_id}-{slug(device_name)}`.
237    pub tailscale_hostname: String,
238    /// Tailscale stable node ID (populated after the sidecar reaches Running).
239    pub tailscale_id: String,
240    /// DNS name on the tailnet.
241    pub dns_name: Option<String>,
242    /// Tailscale IP address.
243    pub ip: Option<IpAddr>,
244}
245
246/// Result of a network-level ping.
247#[derive(Debug, Clone)]
248pub struct PingResult {
249    /// Round-trip latency.
250    pub latency: Duration,
251    /// Connection type description (e.g., "direct" or "relay:sfo").
252    pub connection: String,
253    /// Direct peer endpoint address, if available.
254    pub peer_addr: Option<String>,
255}
256
257/// Health information from the network provider.
258#[derive(Debug, Clone, Default)]
259pub struct HealthInfo {
260    /// Current backend state (e.g., "Running", "NeedsLogin").
261    pub state: String,
262    /// Key expiry timestamp (RFC 3339), if applicable.
263    pub key_expiry: Option<String>,
264    /// Active health warnings.
265    pub warnings: Vec<String>,
266    /// Whether the network is fully operational.
267    pub healthy: bool,
268}
269
270/// A listener for incoming TCP connections via the network provider.
271///
272/// Wraps a channel that receives `TcpStream`s from the bridge. The bridge
273/// internals (binary headers, session tokens) are completely hidden.
274pub struct NetworkTcpListener {
275    /// Port this listener is bound to.
276    pub port: u16,
277    /// Receiver for incoming connections.
278    pub incoming: tokio::sync::mpsc::Receiver<IncomingConnection>,
279}
280
281/// An incoming TCP connection with metadata.
282#[derive(Debug)]
283pub struct IncomingConnection {
284    /// The raw TCP stream (bridge headers already consumed).
285    pub stream: TcpStream,
286    /// Remote address of the connecting peer.
287    pub remote_addr: String,
288    /// Remote DNS name or peer identity JSON.
289    pub remote_identity: String,
290    /// Port the connection arrived on.
291    pub port: u16,
292}
293
294// ---------------------------------------------------------------------------
295// NetworkUdpSocket — address-framed UDP relay wrapper
296// ---------------------------------------------------------------------------
297
298/// A UDP socket that relays datagrams through the network provider.
299///
300/// Under the hood, the Rust side talks to a local relay socket. Each outbound
301/// datagram is prefixed with a 6-byte address header (`[4-byte IPv4][2-byte port BE]`)
302/// so the relay (Go sidecar) knows where to forward the packet on the tsnet
303/// network. Inbound datagrams arrive with the same header prepended by the relay.
304///
305/// This struct hides the framing — callers use `send_to` / `recv_from` with
306/// normal `SocketAddr` values.
307pub struct NetworkUdpSocket {
308    /// The underlying tokio UDP socket connected to the local relay.
309    inner: tokio::net::UdpSocket,
310    /// The tsnet-bound port (the logical port on the Tailscale network).
311    tsnet_port: u16,
312}
313
314/// Address header size: 4 bytes IPv4 + 2 bytes port (big-endian).
315const UDP_ADDR_HEADER_SIZE: usize = 6;
316
317impl NetworkUdpSocket {
318    /// Create a new `NetworkUdpSocket` from a tokio UdpSocket and the tsnet port.
319    pub(crate) fn new(inner: tokio::net::UdpSocket, tsnet_port: u16) -> Self {
320        Self { inner, tsnet_port }
321    }
322
323    /// Send a datagram to the specified address via the relay.
324    ///
325    /// The relay will forward the datagram to the target on the tsnet network.
326    pub async fn send_to(&self, data: &[u8], addr: SocketAddr) -> Result<usize, NetworkError> {
327        let ip = match addr.ip() {
328            IpAddr::V4(v4) => v4,
329            IpAddr::V6(_) => {
330                return Err(NetworkError::Internal(
331                    "NetworkUdpSocket: IPv6 not supported in relay framing".into(),
332                ));
333            }
334        };
335        let port = addr.port();
336
337        // Build framed packet: [4-byte IPv4][2-byte port BE][payload]
338        let mut framed = Vec::with_capacity(UDP_ADDR_HEADER_SIZE + data.len());
339        framed.extend_from_slice(&ip.octets());
340        framed.extend_from_slice(&port.to_be_bytes());
341        framed.extend_from_slice(data);
342
343        tracing::debug!(
344            target_addr = %addr,
345            payload_len = data.len(),
346            framed_len = framed.len(),
347            relay_addr = ?self.inner.peer_addr().ok(),
348            "NetworkUdpSocket: sending framed datagram to relay"
349        );
350
351        let n = self.inner.send(&framed).await.map_err(NetworkError::Io)?;
352
353        tracing::debug!(
354            bytes_sent = n,
355            "NetworkUdpSocket: framed datagram sent to relay"
356        );
357
358        // Return the number of payload bytes sent (subtract header)
359        Ok(n.saturating_sub(UDP_ADDR_HEADER_SIZE))
360    }
361
362    /// Receive a datagram from the relay, returning the payload and sender address.
363    ///
364    /// The relay prepends a 6-byte address header to each inbound datagram.
365    pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr), NetworkError> {
366        tracing::debug!("NetworkUdpSocket: waiting for inbound datagram from relay...");
367
368        // Read into a temporary buffer that includes space for the header
369        let mut tmp = vec![0u8; UDP_ADDR_HEADER_SIZE + buf.len()];
370        let n = self.inner.recv(&mut tmp).await.map_err(NetworkError::Io)?;
371
372        if n < UDP_ADDR_HEADER_SIZE {
373            return Err(NetworkError::Internal(
374                "NetworkUdpSocket: received packet too short for address header".into(),
375            ));
376        }
377
378        // Parse address header
379        let ip = Ipv4Addr::new(tmp[0], tmp[1], tmp[2], tmp[3]);
380        let port = u16::from_be_bytes([tmp[4], tmp[5]]);
381        let addr = SocketAddr::new(IpAddr::V4(ip), port);
382
383        // Copy payload to caller's buffer
384        let payload_len = n - UDP_ADDR_HEADER_SIZE;
385        buf[..payload_len].copy_from_slice(&tmp[UDP_ADDR_HEADER_SIZE..n]);
386
387        tracing::debug!(
388            raw_bytes = n,
389            payload_len = payload_len,
390            sender_addr = %addr,
391            "NetworkUdpSocket: received inbound datagram from relay"
392        );
393
394        Ok((payload_len, addr))
395    }
396
397    /// Return the local address of the underlying relay socket.
398    pub fn local_addr(&self) -> Result<SocketAddr, NetworkError> {
399        self.inner.local_addr().map_err(NetworkError::Io)
400    }
401
402    /// Return the tsnet-bound port (the logical port on the Tailscale network).
403    pub fn tsnet_port(&self) -> u16 {
404        self.tsnet_port
405    }
406
407    /// Get a reference to the inner tokio UdpSocket.
408    ///
409    /// This is the socket connected to the local relay. Direct reads/writes
410    /// bypass the address framing — prefer `send_to` / `recv_from` instead.
411    pub fn inner(&self) -> &tokio::net::UdpSocket {
412        &self.inner
413    }
414}
415
416impl std::fmt::Debug for NetworkUdpSocket {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        f.debug_struct("NetworkUdpSocket")
419            .field("tsnet_port", &self.tsnet_port)
420            .field("local_addr", &self.inner.local_addr().ok())
421            .finish()
422    }
423}
424
425// ---------------------------------------------------------------------------
426// Reverse proxy types (used by NetworkProvider trait methods)
427// ---------------------------------------------------------------------------
428
429/// Parameters for starting a reverse proxy via the network provider.
430#[derive(Debug, Clone)]
431pub struct ProxyAddParams {
432    /// Unique identifier for this proxy.
433    pub id: String,
434    /// Human-readable name.
435    pub name: String,
436    /// Port on which the proxy listens on the tailnet (TLS).
437    pub listen_port: u16,
438    /// Target host to forward to (e.g., "localhost").
439    pub target_host: String,
440    /// Target port to forward to.
441    pub target_port: u16,
442    /// Target scheme ("http" or "https").
443    pub target_scheme: String,
444}
445
446/// Result of successfully starting a reverse proxy.
447#[derive(Debug, Clone)]
448pub struct ProxyAddResult {
449    /// Proxy ID (echoed back from sidecar).
450    pub id: String,
451    /// Actual listen port (may differ from requested if 0 was passed).
452    pub listen_port: u16,
453    /// Fully qualified URL (e.g., "<https://hostname.ts.net:3001>").
454    pub url: String,
455}
456
457/// Entry in the proxy list from the network provider.
458#[derive(Debug, Clone)]
459pub struct ProxyListEntry {
460    pub id: String,
461    pub name: String,
462    pub listen_port: u16,
463    pub target_host: String,
464    pub target_port: u16,
465    pub target_scheme: String,
466    pub url: String,
467}
468
469// ---------------------------------------------------------------------------
470// Errors
471// ---------------------------------------------------------------------------
472
473/// Errors from Layer 3 network operations.
474#[derive(Debug, thiserror::Error)]
475pub enum NetworkError {
476    /// The network provider is not running.
477    #[error("network provider not running")]
478    NotRunning,
479
480    /// The network provider is already running.
481    #[error("network provider already running")]
482    AlreadyRunning,
483
484    /// Failed to start the network provider.
485    #[error("start failed: {0}")]
486    StartFailed(String),
487
488    /// Failed to stop the network provider.
489    #[error("stop failed: {0}")]
490    StopFailed(String),
491
492    /// Authentication is required.
493    #[error("authentication required: {url}")]
494    AuthRequired { url: String },
495
496    /// A dial operation failed.
497    #[error("dial failed: {0}")]
498    DialFailed(String),
499
500    /// A dial operation timed out.
501    #[error("dial timed out after {0:?}")]
502    DialTimeout(Duration),
503
504    /// A listen operation failed.
505    #[error("listen failed: {0}")]
506    ListenFailed(String),
507
508    /// A ping operation failed.
509    #[error("ping failed: {0}")]
510    PingFailed(String),
511
512    /// The sidecar process crashed or is unavailable.
513    #[error("sidecar error: {0}")]
514    SidecarError(String),
515
516    /// Bridge communication error.
517    #[error("bridge error: {0}")]
518    BridgeError(String),
519
520    /// I/O error.
521    #[error("io error: {0}")]
522    Io(#[from] std::io::Error),
523
524    /// Serialization error.
525    #[error("serialization error: {0}")]
526    Serialize(#[from] serde_json::Error),
527
528    /// Generic internal error.
529    #[error("internal error: {0}")]
530    Internal(String),
531
532    /// The operation is not supported by this provider.
533    #[error("unsupported: {0}")]
534    Unsupported(String),
535
536    /// A reverse-proxy operation failed.
537    #[error("proxy error: {0}")]
538    ProxyError(String),
539}