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