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}