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