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