truffle-core 0.4.0

Truffle mesh networking core (clean architecture)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
//! Layer 3: Network — Peer discovery, addressing, encrypted tunnels.
//!
//! This module defines the [`NetworkProvider`] trait, the public API for Layer 3.
//! The trait is generic — no Tailscale-specific types leak through.
//!
//! The [`tailscale`] submodule contains the [`TailscaleProvider`] implementation
//! that wraps the Go sidecar (tsnet) and bridge.

pub mod tailscale;

use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;

use tokio::net::TcpStream;
use tokio::sync::broadcast;

// ---------------------------------------------------------------------------
// NetworkProvider trait — the public API of Layer 3
// ---------------------------------------------------------------------------

/// Provides network-level peer discovery and raw connectivity.
///
/// The primary implementation is [`TailscaleProvider`](tailscale::TailscaleProvider)
/// which uses tsnet via a Go sidecar. The trait is designed to be swappable —
/// future providers could use mDNS (LAN), STUN/TURN (internet), or Bluetooth.
///
/// # Layer rules
///
/// - Layer 3 does NOT know about WebSocket, QUIC, or any Layer 4 protocol
/// - Layer 3 does NOT know about envelopes, namespaces, or messages
/// - Layer 3 provides raw `TcpStream` — not framed connections
/// - `peer_events()` is the ONLY source of peer events — no polling, no announce
///
/// All async methods return `Send` futures so that `Node<N>` can be used
/// inside `tokio::spawn` tasks (required by the file transfer subsystem
/// and any other code that needs to spawn tasks with node access).
pub trait NetworkProvider: Send + Sync {
    /// Start the network provider.
    ///
    /// This spawns child processes, binds ports, and performs authentication.
    /// If authentication is required (e.g., Tailscale browser auth), the provider
    /// emits `NetworkPeerEvent::AuthRequired { url }` via `peer_events()` and
    /// **keeps waiting** until auth completes or the timeout is reached.
    ///
    /// Callers should subscribe to `peer_events()` BEFORE calling `start()` to
    /// receive auth URLs and display them to the user.
    ///
    /// Returns `Ok(())` when the provider is fully online, or `Err` if auth
    /// times out or a fatal error occurs.
    fn start(&mut self) -> impl std::future::Future<Output = Result<(), NetworkError>> + Send;

    /// Stop the network provider and clean up all resources.
    fn stop(&mut self) -> impl std::future::Future<Output = Result<(), NetworkError>> + Send;

    /// Local node's identity (stable ID, hostname, display name).
    ///
    /// Returns a clone of the current cached identity. The identity is
    /// populated after [`start()`](Self::start) completes and may be
    /// updated when the sidecar reports `tsnet:status`.
    fn local_identity(&self) -> NodeIdentity;

    /// Local node's network address.
    ///
    /// Returns a clone of the current cached address. The address is
    /// populated after [`start()`](Self::start) completes and may be
    /// updated when the sidecar reports `tsnet:status`.
    fn local_addr(&self) -> PeerAddr;

    // ── Discovery (event-driven, NOT polling) ──

    /// Subscribe to peer events. Fires immediately when peers join/leave/update.
    ///
    /// Uses `WatchIPNBus` for real-time notifications instead of polling.
    fn peer_events(&self) -> broadcast::Receiver<NetworkPeerEvent>;

    /// Snapshot of all currently known peers.
    fn peers(&self) -> impl std::future::Future<Output = Vec<NetworkPeer>> + Send;

    // ── Connectivity primitives for Layer 4 ──

    /// Dial a TCP connection to a peer via the encrypted Tailscale tunnel.
    ///
    /// Returns a plain `TcpStream` — all bridge internals (pending_dials,
    /// session token, binary headers) are hidden inside the provider.
    fn dial_tcp(
        &self,
        addr: &str,
        port: u16,
    ) -> impl std::future::Future<Output = Result<TcpStream, NetworkError>> + Send;

    /// Listen for incoming TCP connections on a port via the Tailscale tunnel.
    ///
    /// The returned receiver yields `TcpStream`s for each accepted connection.
    fn listen_tcp(
        &self,
        port: u16,
    ) -> impl std::future::Future<Output = Result<NetworkTcpListener, NetworkError>> + Send;

    /// Stop listening on a previously opened port.
    fn unlisten_tcp(
        &self,
        port: u16,
    ) -> impl std::future::Future<Output = Result<(), NetworkError>> + Send;

    /// Bind a UDP socket on a port via the network tunnel.
    ///
    /// Returns a [`NetworkUdpSocket`] that transparently relays datagrams
    /// through the network provider. The socket supports `send_to` / `recv_from`
    /// with full remote address information.
    ///
    /// Not all providers support UDP. Returns [`NetworkError::Internal`] if
    /// the provider has not implemented UDP transport yet.
    fn bind_udp(
        &self,
        port: u16,
    ) -> impl std::future::Future<Output = Result<NetworkUdpSocket, NetworkError>> + Send;

    // ── Diagnostics ──

    /// Ping a peer via the network layer (Tailscale TSMP).
    fn ping(
        &self,
        addr: &str,
    ) -> impl std::future::Future<Output = Result<PingResult, NetworkError>> + Send;

    /// Node health info (key expiry, connection quality, warnings).
    fn health(&self) -> impl std::future::Future<Output = HealthInfo> + Send;
}

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

/// A peer as seen by the network layer (Layer 3).
///
/// Contains only information available from the network provider itself
/// (e.g., Tailscale status). No transport or session state.
#[derive(Debug, Clone)]
pub struct NetworkPeer {
    /// Stable node ID from the network provider.
    pub id: String,
    /// Hostname on the network (e.g., "truffle-cli-abc123").
    pub hostname: String,
    /// Network IP address (e.g., 100.x.x.x for Tailscale).
    pub ip: IpAddr,
    /// Whether the peer is currently online.
    pub online: bool,
    /// Direct endpoint address, if connected directly.
    pub cur_addr: Option<String>,
    /// DERP relay name if connection is relayed.
    pub relay: Option<String>,
    /// Operating system of the peer.
    pub os: Option<String>,
    /// Last time the peer was seen online (RFC 3339 string).
    pub last_seen: Option<String>,
    /// Key expiry timestamp (RFC 3339 string).
    pub key_expiry: Option<String>,
    /// DNS name on the tailnet (e.g., "truffle-cli-abc123.tailnet.ts.net").
    pub dns_name: Option<String>,
}

/// Events emitted when network peers change state.
#[derive(Debug, Clone)]
pub enum NetworkPeerEvent {
    /// A new peer appeared on the network.
    Joined(NetworkPeer),
    /// A peer left the network (by stable node ID).
    Left(String),
    /// A peer's metadata changed (IP, relay, online status, etc.).
    Updated(NetworkPeer),
    /// Authentication is required. The URL should be shown to the user.
    /// Emitted during `start()` — the provider keeps waiting for auth to complete.
    /// May be emitted multiple times if the URL expires and is refreshed.
    AuthRequired {
        /// URL the user should open in a browser.
        url: String,
    },
}

/// Network address of a peer.
#[derive(Debug, Clone, Default)]
pub struct PeerAddr {
    /// IP address (100.x.x.x for Tailscale).
    pub ip: Option<IpAddr>,
    /// Hostname on the network.
    pub hostname: String,
    /// DNS name on the tailnet.
    pub dns_name: Option<String>,
}

/// Identity of the local node on the network.
///
/// Carries the RFC 017 identity triple: `app_id` (namespace), `device_id`
/// (stable per-device ULID), and `device_name` (human-readable). The
/// Tailscale hostname and stable ID are kept alongside as escape hatches
/// and for internal filtering.
#[derive(Debug, Clone, Default)]
pub struct NodeIdentity {
    /// Application namespace identifier (RFC 017 §5.1).
    pub app_id: String,
    /// Stable per-device ULID (RFC 017 §5.4).
    pub device_id: String,
    /// Human-readable device name, original (unsanitised) string.
    pub device_name: String,
    /// Tailscale hostname — `truffle-{app_id}-{slug(device_name)}`.
    pub tailscale_hostname: String,
    /// Tailscale stable node ID (populated after the sidecar reaches Running).
    pub tailscale_id: String,
    /// DNS name on the tailnet.
    pub dns_name: Option<String>,
    /// Tailscale IP address.
    pub ip: Option<IpAddr>,
}

/// Result of a network-level ping.
#[derive(Debug, Clone)]
pub struct PingResult {
    /// Round-trip latency.
    pub latency: Duration,
    /// Connection type description (e.g., "direct" or "relay:sfo").
    pub connection: String,
    /// Direct peer endpoint address, if available.
    pub peer_addr: Option<String>,
}

/// Health information from the network provider.
#[derive(Debug, Clone, Default)]
pub struct HealthInfo {
    /// Current backend state (e.g., "Running", "NeedsLogin").
    pub state: String,
    /// Key expiry timestamp (RFC 3339), if applicable.
    pub key_expiry: Option<String>,
    /// Active health warnings.
    pub warnings: Vec<String>,
    /// Whether the network is fully operational.
    pub healthy: bool,
}

/// A listener for incoming TCP connections via the network provider.
///
/// Wraps a channel that receives `TcpStream`s from the bridge. The bridge
/// internals (binary headers, session tokens) are completely hidden.
pub struct NetworkTcpListener {
    /// Port this listener is bound to.
    pub port: u16,
    /// Receiver for incoming connections.
    pub incoming: tokio::sync::mpsc::Receiver<IncomingConnection>,
}

/// An incoming TCP connection with metadata.
#[derive(Debug)]
pub struct IncomingConnection {
    /// The raw TCP stream (bridge headers already consumed).
    pub stream: TcpStream,
    /// Remote address of the connecting peer.
    pub remote_addr: String,
    /// Remote DNS name or peer identity JSON.
    pub remote_identity: String,
    /// Port the connection arrived on.
    pub port: u16,
}

// ---------------------------------------------------------------------------
// NetworkUdpSocket — address-framed UDP relay wrapper
// ---------------------------------------------------------------------------

/// A UDP socket that relays datagrams through the network provider.
///
/// Under the hood, the Rust side talks to a local relay socket. Each outbound
/// datagram is prefixed with a 6-byte address header (`[4-byte IPv4][2-byte port BE]`)
/// so the relay (Go sidecar) knows where to forward the packet on the tsnet
/// network. Inbound datagrams arrive with the same header prepended by the relay.
///
/// This struct hides the framing — callers use `send_to` / `recv_from` with
/// normal `SocketAddr` values.
pub struct NetworkUdpSocket {
    /// The underlying tokio UDP socket connected to the local relay.
    inner: tokio::net::UdpSocket,
    /// The tsnet-bound port (the logical port on the Tailscale network).
    tsnet_port: u16,
}

/// Address header size: 4 bytes IPv4 + 2 bytes port (big-endian).
const UDP_ADDR_HEADER_SIZE: usize = 6;

impl NetworkUdpSocket {
    /// Create a new `NetworkUdpSocket` from a tokio UdpSocket and the tsnet port.
    pub(crate) fn new(inner: tokio::net::UdpSocket, tsnet_port: u16) -> Self {
        Self { inner, tsnet_port }
    }

    /// Send a datagram to the specified address via the relay.
    ///
    /// The relay will forward the datagram to the target on the tsnet network.
    pub async fn send_to(&self, data: &[u8], addr: SocketAddr) -> Result<usize, NetworkError> {
        let ip = match addr.ip() {
            IpAddr::V4(v4) => v4,
            IpAddr::V6(_) => {
                return Err(NetworkError::Internal(
                    "NetworkUdpSocket: IPv6 not supported in relay framing".into(),
                ));
            }
        };
        let port = addr.port();

        // Build framed packet: [4-byte IPv4][2-byte port BE][payload]
        let mut framed = Vec::with_capacity(UDP_ADDR_HEADER_SIZE + data.len());
        framed.extend_from_slice(&ip.octets());
        framed.extend_from_slice(&port.to_be_bytes());
        framed.extend_from_slice(data);

        tracing::debug!(
            target_addr = %addr,
            payload_len = data.len(),
            framed_len = framed.len(),
            relay_addr = ?self.inner.peer_addr().ok(),
            "NetworkUdpSocket: sending framed datagram to relay"
        );

        let n = self.inner.send(&framed).await.map_err(NetworkError::Io)?;

        tracing::debug!(
            bytes_sent = n,
            "NetworkUdpSocket: framed datagram sent to relay"
        );

        // Return the number of payload bytes sent (subtract header)
        Ok(n.saturating_sub(UDP_ADDR_HEADER_SIZE))
    }

    /// Receive a datagram from the relay, returning the payload and sender address.
    ///
    /// The relay prepends a 6-byte address header to each inbound datagram.
    pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr), NetworkError> {
        tracing::debug!("NetworkUdpSocket: waiting for inbound datagram from relay...");

        // Read into a temporary buffer that includes space for the header
        let mut tmp = vec![0u8; UDP_ADDR_HEADER_SIZE + buf.len()];
        let n = self.inner.recv(&mut tmp).await.map_err(NetworkError::Io)?;

        if n < UDP_ADDR_HEADER_SIZE {
            return Err(NetworkError::Internal(
                "NetworkUdpSocket: received packet too short for address header".into(),
            ));
        }

        // Parse address header
        let ip = Ipv4Addr::new(tmp[0], tmp[1], tmp[2], tmp[3]);
        let port = u16::from_be_bytes([tmp[4], tmp[5]]);
        let addr = SocketAddr::new(IpAddr::V4(ip), port);

        // Copy payload to caller's buffer
        let payload_len = n - UDP_ADDR_HEADER_SIZE;
        buf[..payload_len].copy_from_slice(&tmp[UDP_ADDR_HEADER_SIZE..n]);

        tracing::debug!(
            raw_bytes = n,
            payload_len = payload_len,
            sender_addr = %addr,
            "NetworkUdpSocket: received inbound datagram from relay"
        );

        Ok((payload_len, addr))
    }

    /// Return the local address of the underlying relay socket.
    pub fn local_addr(&self) -> Result<SocketAddr, NetworkError> {
        self.inner.local_addr().map_err(NetworkError::Io)
    }

    /// Return the tsnet-bound port (the logical port on the Tailscale network).
    pub fn tsnet_port(&self) -> u16 {
        self.tsnet_port
    }

    /// Get a reference to the inner tokio UdpSocket.
    ///
    /// This is the socket connected to the local relay. Direct reads/writes
    /// bypass the address framing — prefer `send_to` / `recv_from` instead.
    pub fn inner(&self) -> &tokio::net::UdpSocket {
        &self.inner
    }
}

impl std::fmt::Debug for NetworkUdpSocket {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("NetworkUdpSocket")
            .field("tsnet_port", &self.tsnet_port)
            .field("local_addr", &self.inner.local_addr().ok())
            .finish()
    }
}

// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------

/// Errors from Layer 3 network operations.
#[derive(Debug, thiserror::Error)]
pub enum NetworkError {
    /// The network provider is not running.
    #[error("network provider not running")]
    NotRunning,

    /// The network provider is already running.
    #[error("network provider already running")]
    AlreadyRunning,

    /// Failed to start the network provider.
    #[error("start failed: {0}")]
    StartFailed(String),

    /// Failed to stop the network provider.
    #[error("stop failed: {0}")]
    StopFailed(String),

    /// Authentication is required.
    #[error("authentication required: {url}")]
    AuthRequired { url: String },

    /// A dial operation failed.
    #[error("dial failed: {0}")]
    DialFailed(String),

    /// A dial operation timed out.
    #[error("dial timed out after {0:?}")]
    DialTimeout(Duration),

    /// A listen operation failed.
    #[error("listen failed: {0}")]
    ListenFailed(String),

    /// A ping operation failed.
    #[error("ping failed: {0}")]
    PingFailed(String),

    /// The sidecar process crashed or is unavailable.
    #[error("sidecar error: {0}")]
    SidecarError(String),

    /// Bridge communication error.
    #[error("bridge error: {0}")]
    BridgeError(String),

    /// I/O error.
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),

    /// Serialization error.
    #[error("serialization error: {0}")]
    Serialize(#[from] serde_json::Error),

    /// Generic internal error.
    #[error("internal error: {0}")]
    Internal(String),
}