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}