Skip to main content

tailscale/
lib.rs

1//! A work-in-progress [Tailscale](https://tailscale.com/blog/how-tailscale-works) library.
2//!
3//! `tailscale` allows Rust programs to connect to a tailnet and exchange traffic with peers over
4//! TCP and UDP. It can communicate with other `tailscale`-based peers, `tailscaled` (the Tailscale
5//! Go client), `tsnet`, and `libtailscale` via public DERP servers.
6//!
7//! <div class="warning">
8//! `tailscale` is unstable and insecure.
9//!
10//! We welcome enthusiasm and interest, but please **do not** build production software using these
11//! libraries or rely on it for data privacy until we have a chance to batten down some hatches and
12//! complete a third-party audit.
13//!
14//! See the [Caveats section](#caveats) for more details.
15//! </div>
16//!
17//! For language bindings, see the following crates:
18//!
19//! - C: [ts_ffi](https://docs.rs/ts_ffi)
20//! - Python: [ts_python](https://docs.rs/ts_python)
21//! - Elixir: [ts_elixir](https://docs.rs/ts_elixir)
22//!
23//! For instructions on how to run tests, lints, etc., see [CONTRIBUTING.md]. For the high-level
24//! architecture and repository layout, see [ARCHITECTURE.md].
25//!
26//! ## Code Sample
27//!
28//! A simple UDP client that periodically sends messages to a tailnet peer at `100.64.0.1:5678`:
29//!
30//! ```no_run
31//! # use std::{
32//! #     time::Duration,
33//! #     net::Ipv4Addr,
34//! #     error::Error,
35//! # };
36//! #
37//! # #[tokio::main]
38//! # async fn main() -> Result<(), Box<dyn Error>> {
39//! // Open a new connection to the tailnet
40//! let dev = tailscale::Device::new(
41//!     &tailscale::Config::default_with_key_file("tsrs_keys.json").await?,
42//!     Some("YOUR_AUTH_KEY_HERE".to_owned()),
43//! ).await?;
44//!
45//! // Bind a UDP socket on our tailnet IP, port 1234
46//! let sock = dev.udp_bind((dev.ipv4_addr().await?, 1234).into()).await?;
47//!
48//! // Send a packet containing "hello, world!" to 100.64.0.1:5678 once per second
49//! loop {
50//!     sock.send_to((Ipv4Addr::new(100, 64, 0, 1), 5678).into(), b"hello, world!").await?;
51//!     tokio::time::sleep(Duration::from_secs(1)).await;
52//! }
53//! # }
54//! ```
55//!
56//! Additional examples of using the `tailscale` crate can be found in the [`examples/`] directory.
57//!
58//! ## Using `tailscale`
59//!
60//! To use this crate or the language bindings, you will need to set the `TS_RS_EXPERIMENT` env var
61//! to `this_is_unstable_software`. We'll remove this requirement after a third-party code/cryptography
62//! audit and any necessary fixes.
63//!
64//! Under the hood, we use Tokio for our async runtime. You must also use Tokio, any kind and most
65//! configurations of Tokio runtimes should work, but there must be one available when you call any
66//! async API functions. The easiest way to do this is to use `#[tokio::main]`, see the
67//! [Tokio docs](https://docs.rs/tokio) for more information. In the future, we would like to limit
68//! our reliance on Tokio so that there are alternatives for users of other async runtimes.
69//!
70//! ## Caveats
71//!
72//! This software is still a work-in-progress! We are providing it in the open at this stage out of
73//! a belief in open-source and to see where the community runs with it, but please be aware of a
74//! few important considerations:
75//!
76//! - This implementation contains unaudited cryptography and hasn't undergone a comprehensive
77//!   security analysis. Conservatively, assume there could be a critical security hole meaning
78//!   anything you send or receive could be in the clear on the public Internet.
79//! - There are no compatibility guarantees at the moment. This is early-days software - we may
80//!   break dependent code in order to get things right.
81//! - Direct peer-to-peer connections via NAT traversal are implemented (STUN-discovered endpoints
82//!   and Disco, with `CallMeMaybe` hole-punching over DERP), with DERP relays as the fallback when
83//!   no direct path is available. Hard/symmetric NATs get the same single fixed-local-port candidate
84//!   (`EndpointSTUN4LocalPort`) Go Tailscale uses; behind a NAT with no static port mapping a flow
85//!   may still stay relayed through DERP, which caps its throughput. (Upstream Go does **not** do a
86//!   "256-port birthday-paradox spray" — that is a common misconception; the single-candidate guess
87//!   is the actual behavior, and this fork matches it.)
88//!
89//! ## Feature Flags
90//!
91//! - `axum`: enables the `axum` module, which enables you to run an `axum` HTTP server on top
92//!   of a [`netstack::TcpListener`].
93//!
94//! ## Platform Support
95//!
96//! `tailscale` currently supports the following platforms:
97//!
98//! - Linux (x86_64 and ARM64)
99//! - macOS (ARM64)
100//!
101//! ## Component crates
102//!
103//! The following crates are part of the tailscale-rs project and are dependencies of this one. For
104//! many tasks, just this crate should be sufficient and these other crates are an implementation detail.
105//! There are other crates too, see [ARCHITECTURE.md]
106//! or the [GitHub repo](https://github.com/tailscale/tailscale-rs).
107//!
108//! - [ts_runtime](https://docs.rs/ts_runtime): for each API-level `Device`, the runtime uses an actor
109//!   architecture to manage the lifecycle of the control client, data plane components, netstack, etc.
110//!   A message bus passes updates and communications between these top-level actors.
111//! - [ts_netcheck](https://docs.rs/ts_netcheck): checks network availability and reports latency to
112//!   DERP servers in different regions.
113//! - [ts_netstack_smoltcp](https://docs.rs/ts_netstack_smoltcp): a [smoltcp](https://docs.rs/smoltcp)-based
114//!   network stack that processes Layer 3+ packets to/from the overlay network.
115//! - [ts_control](https://docs.rs/ts_control): control plane client that handles registration,
116//!   authorization/authentication, configuration, and streaming updates.
117//! - [ts_dataplane](https://docs.rs/ts_dataplane): wires all the individual data plane functions together,
118//!   flowing inbound and outbound packets through the components in the correct order.
119//! - [ts_tunnel](https://docs.rs/ts_tunnel): a partial implementation of the WireGuard specification
120//!   that protects all data plane traffic, and is interoperable with other WireGuard clients, including Tailscale clients.
121//! - [ts_cli_util](https://docs.rs/ts_cli_util): helpers for writing command line tools and initializing
122//!   logging, used in examples.
123//! - [ts_disco_protocol](https://docs.rs/ts_disco_protocol): incomplete implementation of Tailscale's
124//!   discovery protocol (disco).
125//!
126//! [ARCHITECTURE.md]: https://github.com/tailscale/tailscale-rs/blob/main/ARCHITECTURE.md
127//! [CONTRIBUTING.md]: https://github.com/tailscale/tailscale-rs/blob/main/CONTRIBUTING.md
128//! [`examples/`]: https://github.com/tailscale/tailscale-rs/blob/main/examples/README.md
129//! [open an issue]: https://github.com/tailscale/tailscale-rs/issues
130//! [`axum` HTTP server]: https://docs.rs/axum/latest/axum/
131
132use std::{
133    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
134    time::Duration,
135};
136
137#[doc(inline)]
138pub use config::Config;
139#[doc(inline)]
140pub use error::{Error, InternalErrorKind};
141// Re-exported so a downstream crate depending only on `tailscale` can name the auth-key secret type
142// for [`Device::new_with_secret`] without taking a separate, version-pinned dependency on `secrecy`
143// (which would risk a `SecretString`-type mismatch if the two `secrecy` majors diverged). Callers
144// pass `tailscale::SecretString`; `secrecy` is a pure-Rust wrapper (no aws-lc/openssl/ring).
145pub use secrecy::SecretString;
146#[doc(inline)]
147pub use ts_control::ExitNodeSelector;
148#[doc(inline)]
149pub use ts_control::Node as NodeInfo;
150#[doc(inline)]
151pub use ts_control::tls::{CertifiedKey, TlsAcceptor, TlsStream};
152#[doc(inline)]
153pub use ts_control::{CertError, MISSING_CERT_RPC, ServeConfig, ServeState, ServeTarget};
154/// The netmap DNS configuration returned by [`Device::dns_config`] (Go `netmap.NetworkMap.DNS`).
155#[doc(inline)]
156pub use ts_control::{DnsConfig, DnsResolver, ExtraRecord};
157#[doc(inline)]
158pub use ts_control::{ExitProxyConfig, ExitProxyScheme};
159pub use ts_control::{
160    IdTokenError, LogoutError, ServiceError, ServiceMode, SetDnsError, SetDnsInternalErrorKind,
161    SshAccept, SshAction, SshConnIdentity, SshDecision, SshDenyReason, SshPolicy, SshPrincipal,
162    SshRule, StableNodeId,
163};
164// Re-exported so the application data-path transport can be selected through the `tailscale`
165// facade alone: `Config::transport_mode` is `TransportMode` (default `Netstack`; `Tun(TunConfig {
166// name, mtu })` for a real kernel TUN interface). Both are `pub` in `ts_control` but were not
167// reachable through this facade, forcing downstream crates to depend on `ts_control` directly just
168// to name them.
169pub use ts_control::{TransportMode, TunConfig};
170#[doc(inline)]
171pub use ts_netstack_smoltcp::PingError;
172use ts_netstack_smoltcp::{CreateSocket, netcore::Channel};
173#[doc(inline)]
174pub use ts_runtime::fallback_tcp::{
175    FallbackConnFuture, FallbackConnHandler, FallbackDecision, FallbackTcpHandle,
176};
177#[doc(inline)]
178pub use ts_runtime::taildrop::WaitingFile;
179#[doc(inline)]
180pub use ts_runtime::{
181    DeviceState, FileTarget, NetcheckReport, RegionLatency, RegistrationError, Status, StatusNode,
182    WhoIs,
183};
184/// The interactive-login URL type returned by [`Device::pop_browser_url`].
185#[doc(inline)]
186pub use url::Url;
187
188#[cfg(feature = "axum")]
189pub mod axum;
190pub mod config;
191mod dial;
192mod error;
193mod loopback;
194#[cfg(feature = "ssh")]
195pub mod ssh;
196
197#[doc(inline)]
198pub use dial::{ConnectedUdpSocket, DialConn};
199#[doc(inline)]
200pub use loopback::LoopbackHandle;
201
202/// How a program connects to a tailnet and communicates with peers.
203///
204/// The `Device` connects to the control plane, registers itself with the tailnet, and communicates
205/// with tailnet peers. Its tailnet identity is determined by the key state provided at
206/// construction-time.
207pub struct Device {
208    runtime: ts_runtime::Runtime,
209    /// Command channel to the application netstack. `None` in TUN transport mode, where there is
210    /// no userspace application netstack; the channel-driven socket APIs ([`Device::udp_bind`],
211    /// [`Device::tcp_listen`], [`Device::tcp_connect`], [`Device::ping`]) are unsupported there.
212    channel: Option<Channel>,
213    /// Whether IPv6 is enabled on the tailnet overlay (the `Config::enable_ipv6` gate, default
214    /// `false`). Captured at construction; used by [`Device::listen_service`] to decide whether an
215    /// IPv6 VIP-service address is bindable (the netstack only accepts IPv6 overlay addresses when
216    /// this is set).
217    enable_ipv6: bool,
218    /// The stored Serve config + its live per-port accept loops (`tsnet`'s `Get/SetServeConfig` +
219    /// serving runtime). Built lazily on the first [`Device::set_serve_config`] (it needs this
220    /// node's overlay IPv4, only known after registration). Held here so its accept loops abort when
221    /// the `Device` drops; `None` (empty config) until the first `set`.
222    serve: std::sync::Mutex<Option<ts_runtime::serve::ServeManager>>,
223    /// The live Funnel ingress manager (`tsnet`'s `ListenFunnel` data path), built on
224    /// [`Device::listen_funnel`](crate::Device::listen_funnel). Held here so its TLS-termination pump and the installed peerAPI
225    /// ingress sink stay alive for the device's life (and tear down when a new `listen_funnel`
226    /// replaces it, or the `Device` drops). `None` until the first `listen_funnel`.
227    funnel: std::sync::Mutex<Option<ts_runtime::funnel::FunnelManager>>,
228}
229
230/// Map a [`ts_runtime::taildrop::TaildropError`] to the device-facing [`Error`]. `Error` is a
231/// `Copy` enum with no payload, so the I/O detail string is dropped, but the *kind* is preserved so
232/// a caller can still distinguish the actionable cases: an invalid name →
233/// [`InternalErrorKind::BadRequest`], an in-progress conflict → [`InternalErrorKind::AlreadyExists`],
234/// a missing file → [`InternalErrorKind::NotFound`], and any other filesystem failure →
235/// [`InternalErrorKind::Io`].
236fn taildrop_err(e: ts_runtime::taildrop::TaildropError) -> Error {
237    use ts_runtime::taildrop::TaildropError;
238    match e {
239        TaildropError::InvalidFileName => Error::Internal(InternalErrorKind::BadRequest),
240        TaildropError::FileExists => Error::Internal(InternalErrorKind::AlreadyExists),
241        TaildropError::Io(io) if io.kind() == std::io::ErrorKind::NotFound => {
242            Error::Internal(InternalErrorKind::NotFound)
243        }
244        TaildropError::Io(_) => Error::Internal(InternalErrorKind::Io),
245    }
246}
247
248/// Map a [`ts_runtime::taildrop_send::TaildropSendError`] (the Taildrop *sender*) to the
249/// device-facing [`Error`]. The send-side conflict/forbidden/unexpected-status cases all reduce to
250/// `BadRequest` (the peer refused the transfer for a request-level reason), a dial failure or
251/// timeout to `Timeout`, an invalid name to `BadRequest`, and any stream I/O failure to `Io`.
252fn taildrop_send_err(e: ts_runtime::taildrop_send::TaildropSendError) -> Error {
253    use ts_runtime::taildrop_send::TaildropSendError;
254    match e {
255        TaildropSendError::Connect | TaildropSendError::Timeout => Error::Timeout,
256        TaildropSendError::InvalidName
257        | TaildropSendError::Forbidden
258        | TaildropSendError::Conflict
259        | TaildropSendError::UnexpectedStatus(_) => Error::Internal(InternalErrorKind::BadRequest),
260        TaildropSendError::Io => Error::Internal(InternalErrorKind::Io),
261    }
262}
263
264/// Resolve the effective registration auth key from `auth_key` plus the config's
265/// workload-identity-federation (WIF) / OAuth-client fields.
266///
267/// With the `identity-federation` feature enabled, an OAuth client secret (`tskey-client-…`) or a
268/// `client_id` + (`id_token` | `audience`) is exchanged for a Tailscale auth key against the SaaS
269/// admin API before registration (Go `tsnet.Server`'s `resolveAuthKey`). Without the feature this is
270/// a pure pass-through: `auth_key` is returned unchanged and the WIF config fields are ignored, so
271/// the default build is byte-identical to before.
272#[cfg(feature = "identity-federation")]
273async fn resolve_auth_key(
274    config: &Config,
275    auth_key: Option<String>,
276) -> Result<Option<String>, Error> {
277    let wif = ts_control::WifConfig {
278        auth_key,
279        client_id: config.client_id.clone(),
280        client_secret: config.client_secret.clone(),
281        id_token: config.id_token.clone(),
282        audience: config.audience.clone(),
283        tags: config.requested_tags.clone(),
284    };
285    ts_control::resolve_auth_key(&wif, &config.control_server_url)
286        .await
287        .map_err(|e| {
288            tracing::error!(error = %e, "resolving auth key via workload-identity federation");
289            Error::Internal(InternalErrorKind::BadRequest)
290        })
291}
292
293/// Pass-through when the `identity-federation` feature is disabled: the auth key is used as-is and
294/// the WIF config fields have no effect (matching Go, where the federation path is compiled out
295/// unless its optional feature is linked).
296#[cfg(not(feature = "identity-federation"))]
297async fn resolve_auth_key(
298    _config: &Config,
299    auth_key: Option<String>,
300) -> Result<Option<String>, Error> {
301    Ok(auth_key)
302}
303
304impl Device {
305    /// Create a device from the given [`Config`] and auth key.
306    ///
307    /// Internally, this will spawn multiple asynchronous actors onto a Tokio runtime.
308    ///
309    /// # Example
310    ///
311    /// ```rust,no_run
312    /// # #[tokio::main]
313    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
314    /// # use tailscale::*;
315    /// let dev = Device::new(
316    ///     &Config::default_with_key_file("tsrs_keys.json").await?,
317    ///     Some("MY_AUTH_KEY".to_string()),
318    /// ).await?;
319    /// # Ok(()) }
320    /// ```
321    pub async fn new(config: &Config, auth_key: Option<String>) -> Result<Self, Error> {
322        check_magic_env()?;
323
324        // Resolve the effective registration auth key. The explicit `auth_key` argument wins; if it
325        // is `None`, fall back to `config.auth_key` (Go `tsnet.Server.AuthKey`). When the
326        // `identity-federation` feature is enabled, the resolved key is further passed through the
327        // WIF / OAuth-client bootstrap, which exchanges an OAuth client secret (`tskey-client-…`) or
328        // an IdP-issued OIDC token for a Tailscale auth key before registration (SaaS-only).
329        let auth_key = auth_key.or_else(|| config.auth_key.clone());
330        let auth_key = resolve_auth_key(config, auth_key).await?;
331
332        let rt =
333            ts_runtime::Runtime::spawn(config.into(), auth_key, (&config.key_state).into()).await?;
334        // In TUN transport mode there is no application netstack, so the runtime has no command
335        // channel: that surfaces as `UnsupportedInTunMode`, which we map to a `None` channel rather
336        // than an error (the device is still usable for control-plane and peer-lookup APIs).
337        let channel = match rt.channel().await {
338            Ok(c) => Some(c),
339            Err(e) if e.kind == ts_runtime::ErrorKind::UnsupportedInTunMode => None,
340            Err(e) => return Err(e.into()),
341        };
342
343        Ok(Self {
344            runtime: rt,
345            channel,
346            enable_ipv6: config.enable_ipv6,
347            serve: std::sync::Mutex::new(None),
348            funnel: std::sync::Mutex::new(None),
349        })
350    }
351
352    /// Create a device from the given [`Config`] and a [`SecretString`] auth key.
353    ///
354    /// This is a back-compat-preserving convenience over [`new`](Self::new) for callers that already
355    /// hold the registration auth key as a [`secrecy::SecretString`] (e.g. a daemon that keeps the
356    /// pre-auth key wrapped end-to-end). It lets the caller avoid materializing a plain `String` at
357    /// the engine boundary: the secret is exposed only on the last inch, immediately before being
358    /// handed to [`new`](Self::new).
359    ///
360    /// # Honesty about the plaintext window
361    ///
362    /// This closes the *caller's* boundary, **not** the engine's internal handling. The engine still
363    /// resolves the auth key to a plain `String` internally for registration (the plaintext `String`
364    /// window inside the engine is identical to calling [`new`](Self::new) directly) — this method
365    /// does not make the engine itself secret-clean. If you call [`new`](Self::new) you create that
366    /// `String` yourself; if you call this you do not, but the engine creates one either way.
367    ///
368    /// Passing `None` is equivalent to `new(config, None)` (falls back to `config.auth_key`).
369    ///
370    /// # Example
371    ///
372    /// ```rust,no_run
373    /// # #[tokio::main]
374    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
375    /// # use tailscale::*;
376    /// let dev = Device::new_with_secret(
377    ///     &Config::default_with_key_file("tsrs_keys.json").await?,
378    ///     Some(SecretString::from("MY_AUTH_KEY")),
379    /// ).await?;
380    /// # Ok(()) }
381    /// ```
382    pub async fn new_with_secret(
383        config: &Config,
384        auth_key: Option<SecretString>,
385    ) -> Result<Self, Error> {
386        use secrecy::ExposeSecret as _;
387
388        // Expose the secret on the last inch and delegate to `new`, so the spawn/registration path
389        // is shared verbatim (no duplicated runtime-spawn logic) and the engine-internal plaintext
390        // window is byte-for-byte identical to a direct `new` call.
391        let plain = auth_key.map(|s| s.expose_secret().to_string());
392        Self::new(config, plain).await
393    }
394
395    /// The application netstack command channel, or an error in TUN transport mode (no application
396    /// netstack exists).
397    fn channel(&self) -> Result<&Channel, Error> {
398        self.channel
399            .as_ref()
400            .ok_or(Error::Internal(InternalErrorKind::UnsupportedInTunMode))
401    }
402
403    /// Get this [`Device`]'s IPv4 tailnet address.
404    pub async fn ipv4_addr(&self) -> Result<Ipv4Addr, Error> {
405        self.runtime
406            .control
407            .ask(ts_runtime::control_runner::Ipv4)
408            .await
409            .map_err(ts_runtime::Error::from)?
410            .ok_or(Error::Internal(InternalErrorKind::Actor))
411    }
412
413    /// Get this [`Device`]'s IPv6 tailnet address.
414    pub async fn ipv6_addr(&self) -> Result<Ipv6Addr, Error> {
415        self.runtime
416            .control
417            .ask(ts_runtime::control_runner::Ipv6)
418            .await
419            .map_err(ts_runtime::Error::from)?
420            .ok_or(Error::Internal(InternalErrorKind::Actor))
421    }
422
423    /// This node's tailnet IPv4 and (when provisioned) IPv6 addresses as a pair — the Rust analog of
424    /// Go `tsnet.Server.TailscaleIPs() (ip4, ip6 netip.Addr)`.
425    ///
426    /// Reads the self node's assigned addresses (the same source Go splits by family). The tailnet
427    /// is IPv4-only unless [`Config::enable_ipv6`](crate::config::Config) is set, so the IPv6 half is
428    /// `None` when no v6 address is assigned — the Rust shape for Go returning the zero `netip.Addr`
429    /// in that case (Go's IPv6-absent sentinel). Errors until the first netmap is received (no self
430    /// node yet), matching Go returning invalid addresses before the node has joined.
431    pub async fn tailscale_ips(&self) -> Result<(Ipv4Addr, Option<Ipv6Addr>), Error> {
432        let me = self.self_node().await?;
433        let v4 = me.tailnet_address.ipv4.addr();
434        let v6 = me.tailnet_address.ipv6.addr();
435        // The decoder synthesizes the unspecified `::` placeholder on an IPv4-only tailnet; surface
436        // a real v6 only when IPv6 is enabled AND a non-placeholder address was assigned.
437        let v6 = (self.enable_ipv6 && !v6.is_unspecified()).then_some(v6);
438        Ok((v4, v6))
439    }
440
441    /// Bind a UDP socket to the specified [`SocketAddr`].
442    ///
443    /// Returns an error in TUN transport mode (there is no application netstack to bind on).
444    pub async fn udp_bind(&self, socket_addr: SocketAddr) -> Result<netstack::UdpSocket, Error> {
445        self.channel()?
446            .udp_bind(socket_addr)
447            .await
448            .map_err(Into::into)
449    }
450
451    /// Bind a TCP listener to the specified [`SocketAddr`].
452    ///
453    /// Returns an error in TUN transport mode (there is no application netstack to listen on).
454    pub async fn tcp_listen(
455        &self,
456        socket_addr: SocketAddr,
457    ) -> Result<netstack::TcpListener, Error> {
458        self.channel()?
459            .tcp_listen(socket_addr)
460            .await
461            .map_err(Into::into)
462    }
463
464    /// Register a fallback TCP handler (like `tsnet`'s `RegisterFallbackTCPHandler`).
465    ///
466    /// The callback is consulted for every inbound TCP flow that matches **no** explicit
467    /// [`Device::tcp_listen`] listener, with the flow's `(src, dst)` addresses. It returns
468    /// `(handler, intercept)`:
469    /// - `(_, false)` — decline; the next registered callback is tried.
470    /// - `(Some(h), true)` — claim the flow; `h` is handed the accepted [`netstack::TcpStream`].
471    /// - `(None, true)` — claim and reject the flow (the connection is closed).
472    ///
473    /// Multiple handlers may be registered; they are consulted in registration order and the first
474    /// to intercept wins. The returned [`FallbackTcpHandle`] deregisters the handler when dropped.
475    ///
476    /// Handlers serve flows over the overlay netstack only — never a host socket — and a flow no
477    /// handler claims is closed (fail-closed), never direct-dialed.
478    ///
479    /// Returns an error in TUN transport mode (there is no application netstack to attach to).
480    pub fn register_fallback_tcp_handler<F>(&self, cb: F) -> Result<FallbackTcpHandle, Error>
481    where
482        F: Fn(SocketAddr, SocketAddr) -> FallbackDecision + Send + Sync + 'static,
483    {
484        self.runtime
485            .register_fallback_tcp_handler(std::sync::Arc::new(cb))
486            .map_err(Into::into)
487    }
488
489    /// Resolve a tailnet peer (or this node) by MagicDNS name to its tailnet IPv4 address.
490    ///
491    /// This is an in-process lookup against the netmap we already hold — like `tsnet`'s in-memory
492    /// `dnsMap`, it does not query any DNS server (there is no `100.100.100.100` resolver). The
493    /// `name` may be a bare hostname or a fully-qualified MagicDNS name, with or without a trailing
494    /// dot, in any case (matching is case-insensitive). Returns `Ok(None)` if no tailnet node has
495    /// that name.
496    ///
497    /// Only MagicDNS names are resolved; names outside the tailnet are not looked up here, so the
498    /// caller's system resolver remains responsible for them. IPv6 is intentionally not resolved —
499    /// this fork operates IPv4-only on the tailnet.
500    pub async fn resolve(&self, name: &str) -> Result<Option<Ipv4Addr>, Error> {
501        if let Some(peer) = self.peer_by_name(name).await? {
502            return Ok(Some(peer.tailnet_address.ipv4.addr()));
503        }
504
505        // tsnet's dnsMap also resolves our own name; fall back to self when no peer matches.
506        let me = self.self_node().await?;
507        if me.matches_name(name) {
508            return Ok(Some(me.tailnet_address.ipv4.addr()));
509        }
510
511        Ok(None)
512    }
513
514    /// Connect to a tailnet peer by MagicDNS name and port over TCP.
515    ///
516    /// Resolves `name` via [`Device::resolve`] (an in-process netmap lookup, no DNS server), then
517    /// dials the resulting tailnet IPv4 address. Returns [`InternalErrorKind::BadRequest`] if the
518    /// name does not resolve to a tailnet node.
519    pub async fn connect_by_name(
520        &self,
521        name: &str,
522        port: u16,
523    ) -> Result<netstack::TcpStream, Error> {
524        let addr = self
525            .resolve(name)
526            .await?
527            .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
528
529        self.tcp_connect((addr, port).into()).await
530    }
531
532    /// Resolve a `host:port` string to a tailnet [`SocketAddr`], honoring the family forced by a
533    /// `network` suffix. The host may be an IP literal (parsed directly) or a MagicDNS name
534    /// (resolved via [`Device::resolve`], which yields a tailnet IPv4). Shared by [`Device::dial`]
535    /// and [`Device::dial_tcp`]. The IPv4-only invariant is enforced here: a `…6` network, or any v6
536    /// destination, requires `Config::enable_ipv6` and otherwise returns
537    /// [`InternalErrorKind::BadRequest`] (a clean typed error rather than a downstream actor error).
538    async fn resolve_dial_addr(
539        &self,
540        network: dial::Network,
541        addr: &str,
542    ) -> Result<SocketAddr, Error> {
543        let (host, port) = dial::split_host_port(addr)?;
544
545        // An IP literal is used directly; otherwise resolve the MagicDNS name (IPv4 only).
546        let ip: IpAddr = if let Ok(ip) = host.parse::<IpAddr>() {
547            ip
548        } else {
549            self.resolve(host)
550                .await?
551                .ok_or(Error::Internal(InternalErrorKind::BadRequest))?
552                .into()
553        };
554
555        dial::check_family(network.family, ip)?;
556
557        // IPv4-only invariant: a v6 destination is only reachable when IPv6 is provisioned.
558        if ip.is_ipv6() && !self.enable_ipv6 {
559            return Err(Error::Internal(InternalErrorKind::BadRequest));
560        }
561
562        Ok((ip, port).into())
563    }
564
565    /// Connect to a tailnet address over TCP or UDP, the Rust analog of Go
566    /// `tsnet.Server.Dial(ctx, network, address)`.
567    ///
568    /// `network` is one of `"tcp"`, `"tcp4"`, `"tcp6"`, `"udp"`, `"udp4"`, `"udp6"`; `addr` is a
569    /// `host:port` string where `host` is a MagicDNS name, an IPv4 literal, or a bracketed IPv6
570    /// literal (`[2001:db8::1]:443`). The host is resolved in-process via [`Device::resolve`] (no DNS
571    /// server). Returns a [`DialConn`] whose arm matches the transport — use [`Device::dial_tcp`]
572    /// when you want the TCP stream directly.
573    ///
574    /// Differences from Go (documented for parity): ports must be **numeric** (Go's `LookupPort`
575    /// also resolves named ports like `"http"`; this fork avoids a services-file dependency), and
576    /// `…6`/v6 destinations require `Config::enable_ipv6` (the tailnet is IPv4-only by default).
577    ///
578    /// # Errors
579    /// [`InternalErrorKind::BadRequest`] for an unsupported `network`, a malformed/portless `addr`,
580    /// an unresolvable name, or a v6 destination while IPv6 is disabled; otherwise the transport's
581    /// own connect error.
582    pub async fn dial(&self, network: &str, addr: &str) -> Result<DialConn, Error> {
583        let net = dial::parse_network(network)?;
584        let remote = self.resolve_dial_addr(net, addr).await?;
585
586        match net.transport {
587            dial::Transport::Tcp => Ok(DialConn::Tcp(self.tcp_connect(remote).await?)),
588            dial::Transport::Udp => {
589                // Bind an ephemeral local UDP socket on this node's tailnet address of the SAME
590                // family as the remote, then connect it (Go's `Dial("udp", …)` returns a connected
591                // UDP `net.Conn`, with the local source picked by `IfElse(dst.Is6(), v6, v4)`). A v4
592                // local socket cannot send to a v6 peer, so the family must match `remote`. (TCP gets
593                // this for free: `tcp_connect` already picks the source family from `remote`.)
594                let local_ip: IpAddr = if remote.is_ipv6() {
595                    self.ipv6_addr().await?.into()
596                } else {
597                    self.ipv4_addr().await?.into()
598                };
599                let sock = self.udp_bind((local_ip, 0).into()).await?;
600                Ok(DialConn::Udp(ConnectedUdpSocket::new(sock, remote)))
601            }
602        }
603    }
604
605    /// Connect to a tailnet address over TCP, returning the stream directly — the common case of
606    /// [`Device::dial`] for `"tcp"`. `addr` is a `host:port` string (MagicDNS name or IP literal).
607    /// This is the building block for HTTP-over-tailnet: an embedder's `hyper`/`reqwest` client can
608    /// route requests by calling `dial_tcp(&format!("{host}:{port}"))` from its connector, mirroring
609    /// how Go `tsnet.Server.HTTPClient` sets `http.Transport.DialContext = Server.Dial`.
610    ///
611    /// # Errors
612    /// As [`Device::dial`] for the `"tcp"` network.
613    pub async fn dial_tcp(&self, addr: &str) -> Result<netstack::TcpStream, Error> {
614        let remote = self
615            .resolve_dial_addr(
616                dial::Network {
617                    transport: dial::Transport::Tcp,
618                    family: dial::Family::Any,
619                },
620                addr,
621            )
622            .await?;
623        self.tcp_connect(remote).await
624    }
625
626    /// Connect to a tailnet address over UDP, returning a connected socket directly — the `"udp"`
627    /// sibling of [`dial_tcp`](Device::dial_tcp) and the common case of [`Device::dial`] for
628    /// `"udp"`. `addr` is a `host:port` string (MagicDNS name or IP literal).
629    ///
630    /// Returns a [`ConnectedUdpSocket`] (`send`/`recv` against a fixed peer), the connected
631    /// UDP-`net.Conn` shape Go's `tsnet.Server.Dial("udp", …)` returns — as opposed to
632    /// [`listen_packet`](Device::listen_packet), which yields an unconnected `net.PacketConn`. An
633    /// ephemeral local UDP socket is bound on this node's tailnet address of the same family as the
634    /// resolved remote (a v4 local socket cannot send to a v6 peer).
635    ///
636    /// # Errors
637    /// As [`Device::dial`] for the `"udp"` network (name resolution, the IPv4-only / `enable_ipv6`
638    /// family invariant, or TUN transport mode having no application netstack to bind on).
639    pub async fn dial_udp(&self, addr: &str) -> Result<ConnectedUdpSocket, Error> {
640        let remote = self
641            .resolve_dial_addr(
642                dial::Network {
643                    transport: dial::Transport::Udp,
644                    family: dial::Family::Any,
645                },
646                addr,
647            )
648            .await?;
649        let local_ip: IpAddr = if remote.is_ipv6() {
650            self.ipv6_addr().await?.into()
651        } else {
652            self.ipv4_addr().await?.into()
653        };
654        let sock = self.udp_bind((local_ip, 0).into()).await?;
655        Ok(ConnectedUdpSocket::new(sock, remote))
656    }
657
658    /// Bind a UDP socket from a `host:port` string, the Rust analog of Go
659    /// `tsnet.Server.ListenPacket(network, addr)`.
660    ///
661    /// `network` is one of `"udp"`, `"udp4"`, `"udp6"`; `addr` must be a **valid IP literal**
662    /// `host:port` (Go's `ListenPacket` rejects a name or empty host — unlike `Listen`). An
663    /// unspecified host (`0.0.0.0`/`[::]`) binds on this node's tailnet address. Returns the
664    /// unconnected [`netstack::UdpSocket`] (a `net.PacketConn`).
665    ///
666    /// # Errors
667    /// [`InternalErrorKind::BadRequest`] for a non-UDP/unsupported `network`, a malformed addr, a
668    /// non-IP host, a family mismatch, or a v6 bind while IPv6 is disabled.
669    pub async fn listen_packet(
670        &self,
671        network: &str,
672        addr: &str,
673    ) -> Result<netstack::UdpSocket, Error> {
674        let net = dial::parse_network(network)?;
675        if net.transport != dial::Transport::Udp {
676            return Err(Error::Internal(InternalErrorKind::BadRequest));
677        }
678        let (host, port) = dial::split_host_port(addr)?;
679
680        // ListenPacket requires a valid IP host (Go rejects a name here).
681        let ip: IpAddr = host
682            .parse()
683            .map_err(|_| Error::Internal(InternalErrorKind::BadRequest))?;
684        dial::check_family(net.family, ip)?;
685
686        // A v6 bind (whether an explicit literal or an unspecified `[::]`) requires IPv6 to be
687        // provisioned — enforce the gate for BOTH cases (the unspecified `[::]` path used to skip it).
688        if ip.is_ipv6() && !self.enable_ipv6 {
689            return Err(Error::Internal(InternalErrorKind::BadRequest));
690        }
691
692        // An unspecified bind host (`0.0.0.0` / `[::]`) means "this node's tailnet address" — of the
693        // SAME family as the requested address, so a `udp6` `[::]:0` binds a v6 socket (it used to
694        // fall through to the v4 address regardless, silently yielding an IPv4 socket for a v6 listen).
695        let bind_ip: IpAddr = if ip.is_unspecified() {
696            if ip.is_ipv6() {
697                self.ipv6_addr().await?.into()
698            } else {
699                self.ipv4_addr().await?.into()
700            }
701        } else {
702            ip
703        };
704
705        self.udp_bind((bind_ip, port).into()).await
706    }
707
708    /// Connect to a TCP socket at the remote address.
709    ///
710    /// Returns an error in TUN transport mode (there is no application netstack to dial from).
711    pub async fn tcp_connect(&self, remote: SocketAddr) -> Result<netstack::TcpStream, Error> {
712        let channel = self.channel()?;
713
714        let ip: IpAddr = match remote.is_ipv4() {
715            true => self.ipv4_addr().await?.into(),
716            false => self.ipv6_addr().await?.into(),
717        };
718
719        // TODO(npry): collision checking
720        let ephemeral_port = rand::random_range(49152..=u16::MAX);
721
722        channel
723            .tcp_connect((ip, ephemeral_port).into(), remote)
724            .await
725            .map_err(Into::into)
726    }
727
728    /// Start a SOCKS5 proxy on a host loopback address that dials into the tailnet (Go
729    /// `tsnet.Server.Loopback`, SOCKS5 half).
730    ///
731    /// Binds a TCP listener on `127.0.0.1:0` (host loopback only — never an external interface) and
732    /// serves SOCKS5 (RFC 1928) with required username/password auth (RFC 1929): username `tsnet`,
733    /// password = the returned `proxy_cred`. Each `CONNECT` is dialed INTO the overlay via
734    /// [`Device::connect_by_name`] / [`Device::tcp_connect`] and spliced to the accepted host socket, so
735    /// a non-Rust host process can reach tailnet peers through the proxy. Returns the bound address, the
736    /// proxy credential, and a [`LoopbackHandle`] whose drop stops the listener.
737    ///
738    /// Anti-leak: the listener is loopback-only and every connection egresses over the overlay, never a
739    /// host socket — the host's real origin IP is never used to reach the destination. Unlike Go, the
740    /// LocalAPI HTTP surface is not served (this fork exposes status/whois/id-token natively on
741    /// `Device`); only the SOCKS5 proxy is provided.
742    ///
743    /// Returns an error in TUN transport mode (no application netstack to dial from).
744    pub async fn loopback(&self) -> Result<(std::net::SocketAddr, String, LoopbackHandle), Error> {
745        // Capture only cloneable pieces — never `&self` — for the spawned accept loop: a clone of the
746        // netstack command channel, this device's own overlay IPv4 (fetched once), and a boxed
747        // resolver closure over clones of the control + peer-tracker actor refs. The resolver
748        // replicates `Device::resolve` (peer-by-name, falling back to this node's own name).
749        let channel = self.channel()?.clone();
750        let self_ipv4 = self.ipv4_addr().await?;
751
752        let control = self.runtime.control.clone();
753        let peer_tracker = self.runtime.peer_tracker.clone();
754        let resolve: loopback::Resolver = std::sync::Arc::new(move |name: String| {
755            let control = control.clone();
756            let peer_tracker = peer_tracker.clone();
757            Box::pin(async move {
758                let pt = peer_tracker
759                    .upgrade()
760                    .ok_or(Error::Internal(InternalErrorKind::Actor))?;
761                let peer = pt
762                    .ask(ts_runtime::peer_tracker::PeerByName { name: name.clone() })
763                    .await
764                    .map_err(ts_runtime::Error::from)?;
765                if let Some(peer) = peer {
766                    return Ok(Some(peer.tailnet_address.ipv4.addr()));
767                }
768                // tsnet's dnsMap also resolves our own name; fall back to self.
769                let me = control
770                    .ask(ts_runtime::control_runner::SelfNode)
771                    .await
772                    .map_err(ts_runtime::Error::from)?
773                    .ok_or(Error::Internal(InternalErrorKind::Actor))?;
774                if me.matches_name(&name) {
775                    Ok(Some(me.tailnet_address.ipv4.addr()))
776                } else {
777                    Ok(None)
778                }
779            }) as std::pin::Pin<Box<dyn std::future::Future<Output = _> + Send>>
780        });
781
782        let dialer = loopback::OverlayDialer::new(channel, self_ipv4, resolve);
783        loopback::start(dialer).await
784    }
785
786    /// Get our node info.
787    pub async fn self_node(&self) -> Result<NodeInfo, Error> {
788        self.runtime
789            .control
790            .ask(ts_runtime::control_runner::SelfNode)
791            .await
792            .map_err(ts_runtime::Error::from)?
793            .ok_or(Error::Internal(InternalErrorKind::Actor))
794    }
795
796    /// The DNS names this node can obtain TLS certificates for — Go `tsnet.Server.CertDomains()`.
797    ///
798    /// These are the `CertDomains` control pushed in the netmap DNS config: the names a TLS-serving
799    /// consumer (e.g. a `ListenTLS`/`GetCertificate`-style caller) should request a cert for. Returns
800    /// an empty `Vec` before the first netmap, or when control granted none — mirroring Go returning a
801    /// clone of `nm.DNS.CertDomains` (empty/`nil` when absent).
802    pub async fn cert_domains(&self) -> Result<Vec<String>, Error> {
803        self.runtime
804            .control
805            .ask(ts_runtime::control_runner::CertDomains)
806            .await
807            .map_err(ts_runtime::Error::from)
808            .map_err(Into::into)
809    }
810
811    /// The DNS configuration control pushed in the latest netmap — Go `tsnet`'s view of
812    /// `netmap.NetworkMap.DNS` (what `tailscale dns status` reports).
813    ///
814    /// Returns the full [`DnsConfig`] — MagicDNS on/off, search domains, global + fallback resolvers,
815    /// split-DNS routes, extra records, cert domains — or `None` before the first netmap / when
816    /// control has sent no DNS config. A superset of [`cert_domains`](Device::cert_domains), which
817    /// remains a separate narrower accessor for the TLS-cert use. Mirrors Go reading a clone of
818    /// `nm.DNS` (absent ⇒ `None`).
819    pub async fn dns_config(&self) -> Result<Option<DnsConfig>, Error> {
820        self.runtime
821            .control
822            .ask(ts_runtime::control_runner::DnsConfig)
823            .await
824            .map_err(ts_runtime::Error::from)
825            .map_err(Into::into)
826    }
827
828    /// The URL control last asked this node to open in a browser (`MapResponse.PopBrowserURL`), or
829    /// `None` if control has sent none.
830    ///
831    /// This is the interactive-login / consent URL an embedder driving a non-authkey (interactive)
832    /// login must surface to the user — the Rust analog of Go `ipn` delivering `BrowseToURL` through
833    /// the notification bus. A daemon polls this after starting an interactive login to obtain the
834    /// auth URL to present. `None` until control sends one; the value is replaced (not accumulated)
835    /// each time control pushes a new one.
836    pub async fn pop_browser_url(&self) -> Result<Option<Url>, Error> {
837        self.runtime
838            .control
839            .ask(ts_runtime::control_runner::PopBrowserUrl)
840            .await
841            .map_err(ts_runtime::Error::from)
842            .map_err(Into::into)
843    }
844
845    /// This node's latest network-conditions report — the Rust analog of Go's `netcheck.Report` as
846    /// `tailscale netcheck` surfaces it.
847    ///
848    /// Returns the [`NetcheckReport`]: the preferred (lowest-latency) DERP region and the per-region
849    /// latency map this node last measured. Empty (default) before the first measurement. This fork's
850    /// net-report path measures only DERP-region latency, so the report carries that subset rather
851    /// than fabricating the UDP/port-mapping fields Go also reports (see [`NetcheckReport`]).
852    pub async fn netcheck(&self) -> Result<NetcheckReport, Error> {
853        self.runtime
854            .control
855            .ask(ts_runtime::control_runner::Netcheck)
856            .await
857            .map_err(ts_runtime::Error::from)
858            .map_err(Into::into)
859    }
860
861    /// This node's key-expiry instant as Unix seconds (`Node.KeyExpiry` in Go), or `Ok(None)` if
862    /// the key never expires.
863    ///
864    /// Like Go, this fork is **reactive** about key expiry — it reports it rather than rotating the
865    /// node key in the background. A caller can schedule re-authentication around this time; on
866    /// expiry, re-create the [`Device`] (which re-registers), supplying a fresh node key + the prior
867    /// `old_node_key` to rotate, or the same key to refresh.
868    pub async fn self_key_expiry_unix(&self) -> Result<Option<i64>, Error> {
869        Ok(self.self_node().await?.key_expiry_unix())
870    }
871
872    /// Whether this node's key has expired as of now (`!KeyExpiry.IsZero() && KeyExpiry.Before(now)`
873    /// in Go). A key with no expiry is never expired. See [`Device::self_key_expiry_unix`] for the
874    /// reactive-rotation note.
875    pub async fn self_key_expired(&self) -> Result<bool, Error> {
876        let now = std::time::SystemTime::now()
877            .duration_since(std::time::UNIX_EPOCH)
878            .map(|d| d.as_secs() as i64)
879            // An unreadable clock (pre-epoch) is treated as the far future so a time-limited key
880            // looks expired — fail-safe toward prompting re-auth rather than trusting a stale key.
881            .unwrap_or(i64::MAX);
882        Ok(self.self_node().await?.key_expired_at_unix(now))
883    }
884
885    /// Fetch the current Tailscale SSH policy pushed by control, if any.
886    ///
887    /// Returns `Ok(None)` when control has not sent an SSH policy. The SSH server treats an absent
888    /// or empty policy as **deny-all** (fail-closed). Used by the SSH auth path
889    /// ([`SshPolicy::evaluate`][ts_control::SshPolicy::evaluate]) to authorize incoming
890    /// connections.
891    pub async fn ssh_policy(&self) -> Result<Option<ts_control::SshPolicy>, Error> {
892        self.runtime
893            .control
894            .ask(ts_runtime::control_runner::CurrentSshPolicy)
895            .await
896            .map_err(ts_runtime::Error::from)
897            .map_err(Into::into)
898    }
899
900    /// Look up a peer by name.
901    pub async fn peer_by_name(&self, name: &str) -> Result<Option<NodeInfo>, Error> {
902        let pt = self
903            .runtime
904            .peer_tracker
905            .upgrade()
906            .ok_or(Error::Internal(InternalErrorKind::Actor))?;
907
908        pt.ask(ts_runtime::peer_tracker::PeerByName {
909            name: name.to_string(),
910        })
911        .await
912        .map_err(ts_runtime::Error::from)
913        .map_err(Into::into)
914    }
915
916    /// Look up a peer by ip.
917    pub async fn peer_by_tailnet_ip(&self, ip: IpAddr) -> Result<Option<NodeInfo>, Error> {
918        let pt = self
919            .runtime
920            .peer_tracker
921            .upgrade()
922            .ok_or(Error::Internal(InternalErrorKind::Actor))?;
923
924        pt.ask(ts_runtime::peer_tracker::PeerByTailnetIp { ip })
925            .await
926            .map_err(ts_runtime::Error::from)
927            .map_err(Into::into)
928    }
929
930    /// Look up the peer(s) with the most-specific route matches for `ip`.
931    ///
932    /// This reports which peers *advertise* a route covering `ip`, independent of this device's
933    /// `accept_routes` setting — analogous to the Go client's informational `PrimaryRoutes`. It is
934    /// not a reachability oracle: with `accept_routes` off, the dataplane will not actually route
935    /// to (or accept return traffic from) advertised subnet routes even if this returns a peer.
936    pub async fn peers_with_route(&self, ip: IpAddr) -> Result<Vec<NodeInfo>, Error> {
937        let pt = self
938            .runtime
939            .peer_tracker
940            .upgrade()
941            .ok_or(Error::Internal(InternalErrorKind::Actor))?;
942
943        pt.ask(ts_runtime::peer_tracker::PeerByAcceptedRoute { ip })
944            .await
945            .map_err(ts_runtime::Error::from)
946            .map_err(Into::into)
947    }
948
949    /// List the Taildrop files this device has fully received and not yet consumed (Go LocalAPI
950    /// `WaitingFiles`).
951    ///
952    /// Returns the files waiting under the configured `taildrop_dir`, sorted by name. Returns an
953    /// empty list when Taildrop is disabled (`Config::taildrop_dir` unset) — fail-closed, never an
954    /// error for the disabled case. A filesystem error while listing surfaces as
955    /// [`InternalErrorKind::Actor`].
956    pub fn taildrop_waiting_files(&self) -> Result<Vec<WaitingFile>, Error> {
957        let Some(store) = self.runtime.taildrop_store() else {
958            return Ok(Vec::new());
959        };
960        store
961            .waiting_files()
962            .map_err(|_| Error::Internal(InternalErrorKind::Actor))
963    }
964
965    /// Open a received Taildrop file by name for reading, returning the handle and its size (Go
966    /// LocalAPI `OpenFile`).
967    ///
968    /// The `name` is validated (path-traversal-safe) inside the store before any path is built.
969    /// Returns [`InternalErrorKind::BadRequest`] when Taildrop is disabled or the name is invalid,
970    /// and [`InternalErrorKind::Actor`] for a filesystem error (e.g. the file does not exist).
971    pub fn taildrop_open_file(&self, name: &str) -> Result<(std::fs::File, u64), Error> {
972        let store = self
973            .runtime
974            .taildrop_store()
975            .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
976        store.open_file(name).map_err(taildrop_err)
977    }
978
979    /// Delete a received Taildrop file by name (Go LocalAPI `DeleteFile`).
980    ///
981    /// The `name` is validated (path-traversal-safe) inside the store before any path is built.
982    /// Returns [`InternalErrorKind::BadRequest`] when Taildrop is disabled or the name is invalid,
983    /// and [`InternalErrorKind::Actor`] for a filesystem error (e.g. the file does not exist).
984    pub fn taildrop_delete_file(&self, name: &str) -> Result<(), Error> {
985        let store = self
986            .runtime
987            .taildrop_store()
988            .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
989        store.delete_file(name).map_err(taildrop_err)
990    }
991
992    /// Send a local file to a tailnet `peer` via Taildrop (Go `PushFile` / `tailscale file cp`).
993    ///
994    /// Pushes `content_length` bytes from `reader` to the peer's peerAPI as
995    /// `PUT /v0/put/<name>` over the overlay netstack — the sending counterpart to the receive store
996    /// surfaced by [`Device::taildrop_waiting_files`]. The transfer rides the encrypted WireGuard
997    /// overlay, never a host socket. The body is streamed from offset 0 (no resume).
998    ///
999    /// The destination is derived **solely from `peer`'s own node record**
1000    /// ([`NodeInfo::peerapi_addr`][ts_control::Node::peerapi_addr]): its advertised tailnet IPv4 and
1001    /// `peerapi4` port. The caller obtains `peer` from [`Device::peer_by_name`] /
1002    /// [`Device::peer_by_tailnet_ip`], so it is always a current netmap peer — a raw control-supplied
1003    /// or attacker-chosen address can never be targeted. As defense in depth, the resolved address is
1004    /// additionally asserted to be a Tailscale CGNAT IP before dialing.
1005    ///
1006    /// Returns [`InternalErrorKind::BadRequest`] when the peer advertises no IPv4 peerAPI (so it
1007    /// cannot receive files), when the name is invalid, or when the peer refuses the transfer
1008    /// (`403`/`409`/unexpected status); [`Error::Timeout`] on a dial failure or timeout; and
1009    /// [`InternalErrorKind::Io`] on a mid-transfer stream error.
1010    pub async fn send_file<R>(
1011        &self,
1012        peer: &NodeInfo,
1013        name: &str,
1014        content_length: u64,
1015        reader: R,
1016    ) -> Result<(), Error>
1017    where
1018        R: tokio::io::AsyncRead + Unpin,
1019    {
1020        let channel = self.channel()?;
1021
1022        // Destination comes only from the peer's own node record — never an arbitrary address.
1023        let dst = peer
1024            .peerapi_addr()
1025            .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
1026        // Defense in depth: refuse to dial anything outside the Tailscale CGNAT range, so a
1027        // malformed node record can't steer the PUT at a non-tailnet host.
1028        if !ts_control::is_tailscale_ip(dst.ip()) {
1029            return Err(Error::Internal(InternalErrorKind::BadRequest));
1030        }
1031
1032        let self_ipv4 = self.ipv4_addr().await?;
1033
1034        ts_runtime::taildrop_send::send_file(channel, self_ipv4, dst, name, content_length, reader)
1035            .await
1036            .map_err(taildrop_send_err)
1037    }
1038
1039    /// List the tailnet peers this node can Taildrop a file *to* — the Rust analog of Go's LocalAPI
1040    /// `FileTargets`.
1041    ///
1042    /// Each [`FileTarget`] pairs a peer's node record with the `http://ip:port` base of its peerAPI;
1043    /// pass `target.node` straight to [`Device::send_file`]. A peer qualifies when it advertises a
1044    /// reachable IPv4 peerAPI **and** is either owned by the same user as this node **or** explicitly
1045    /// granted the file-sharing-target capability — mirroring upstream's send-path filter. The list is
1046    /// gated on this node holding the file-sharing capability (control grants it when the admin
1047    /// enables Taildrop); absent that, the result is empty (fail-closed, not an error). Sorted by the
1048    /// peer's MagicDNS name. Targets are listed regardless of online state (matching upstream — an
1049    /// offline target's [`send_file`](Device::send_file) simply times out). Empty before the first
1050    /// netmap.
1051    pub async fn file_targets(&self) -> Result<Vec<FileTarget>, Error> {
1052        self.runtime.file_targets().await.map_err(Into::into)
1053    }
1054
1055    /// Begin a debug packet capture, streaming a pcap of every packet crossing the dataplane to
1056    /// `writer` (Go `tsnet.Server.CapturePcap`).
1057    ///
1058    /// Installs a capture hook on the running dataplane: from now until [`Device::stop_capture`] is
1059    /// called (or another capture replaces this one), a copy of every plaintext IP packet on the
1060    /// datapath — outbound (pre-encrypt) and inbound (post-decrypt) — is framed and written to
1061    /// `writer`. The 24-byte pcap global header is written immediately on success.
1062    ///
1063    /// The format is byte-faithful classic pcap with Tailscale's `LINKTYPE_USER0` + 4-byte path
1064    /// preamble per record (see [`ts_runtime::capture`]); a resulting file opens in Wireshark, and
1065    /// with Tailscale's `ts-dissector.lua` the direction/path of each packet decodes.
1066    ///
1067    /// The hook runs **inline on the single-threaded dataplane step**, so `writer` must not block for
1068    /// long — a slow writer back-pressures the datapath. Records are **not** flushed per packet (that
1069    /// would be a syscall on every packet on the dataplane thread); buffered bytes are flushed when
1070    /// the writer is dropped on [`Device::stop_capture`]. Wrap `writer` in a [`std::io::BufWriter`] if
1071    /// you want buffering. A write error is swallowed per-packet (the capture silently drops that
1072    /// record) rather than tearing down the datapath; call [`Device::stop_capture`] to end it. Returns
1073    /// an error only if the dataplane actor is unreachable or the initial global-header write fails.
1074    pub async fn capture_pcap<W>(&self, writer: W) -> Result<(), Error>
1075    where
1076        W: std::io::Write + Send + 'static,
1077    {
1078        let sink = std::sync::Arc::new(std::sync::Mutex::new(
1079            ts_runtime::capture::PcapSink::new(writer)
1080                .map_err(|_| Error::Internal(InternalErrorKind::Io))?,
1081        ));
1082        let hook: ts_runtime::CaptureHook = std::sync::Arc::new(move |path, pkt: &[u8]| {
1083            if let Ok(mut sink) = sink.lock() {
1084                // A per-packet write failure (e.g. a closed pipe) silently drops that record rather
1085                // than tearing down the datapath; the caller ends capture via `stop_capture`.
1086                drop(sink.log_packet(path.code(), pkt));
1087            }
1088        });
1089        self.runtime.install_capture(Some(hook)).await?;
1090        Ok(())
1091    }
1092
1093    /// Stop a debug packet capture started by [`Device::capture_pcap`] (Go `ClearCaptureSink`).
1094    ///
1095    /// Clears the dataplane capture hook; the writer is dropped (its remaining buffered bytes are
1096    /// flushed by its own `Drop`). Idempotent — clearing when no capture is installed is a no-op.
1097    /// Returns an error only if the dataplane actor is unreachable.
1098    pub async fn stop_capture(&self) -> Result<(), Error> {
1099        self.runtime.install_capture(None).await?;
1100        Ok(())
1101    }
1102
1103    /// Snapshot of this device and its tailnet peers (like `tailscale status`).
1104    ///
1105    /// Combines this node's self info with the current peer set: each [`StatusNode`] reports the
1106    /// stable id, display name, tailnet IPs, advertised routes, and exit-node flag. (Per-peer
1107    /// `online`/user/capabilities are honestly `None`/empty in this fork — the domain node model
1108    /// does not yet carry the wire-level liveness/login fields; see `ts_runtime::status` docs.)
1109    pub async fn status(&self) -> Result<Status, Error> {
1110        self.runtime.status().await.map_err(Into::into)
1111    }
1112
1113    /// Fetch the current Tailnet Lock (TKA) status pushed by control, if any.
1114    ///
1115    /// Returns `Ok(None)` when control has sent no `TKAInfo` (tailnet lock not in use, or no change
1116    /// observed yet). The returned [`TkaStatus`][ts_control::TkaStatus] carries the authority head
1117    /// (a base32 `AUMHash`, decode with [`tka::AumHash::from_base32`][ts_tka::AumHash::from_base32])
1118    /// and the disablement signal. Signature verification of a peer's node-key signature against the
1119    /// authority is performed with the [`tka`] module's [`tka::Authority`][ts_tka::Authority].
1120    pub async fn tka_status(&self) -> Result<Option<ts_control::TkaStatus>, Error> {
1121        self.runtime
1122            .control
1123            .ask(ts_runtime::control_runner::CurrentTkaStatus)
1124            .await
1125            .map_err(ts_runtime::Error::from)
1126            .map_err(Into::into)
1127    }
1128
1129    /// Request an OIDC **ID token** from control for this node, scoped to `audience` (workload-
1130    /// identity federation, like `tailscale`'s `id-token` LocalAPI).
1131    ///
1132    /// Returns a signed JWT whose `sub` claim is this node's MagicDNS name and whose `aud` claim is
1133    /// `audience`, suitable for presenting to a third-party relying party (e.g. AWS/GCP
1134    /// workload-identity federation). The node is the token *subject*, not the authenticator — this
1135    /// is token issuance over the Noise transport (`POST /machine/id-token`), not a login path.
1136    /// Requires the control plane to support capability version ≥ 30.
1137    pub async fn fetch_id_token(&self, audience: &str) -> Result<String, ts_control::IdTokenError> {
1138        self.runtime.fetch_id_token(audience.to_string()).await
1139    }
1140
1141    /// Publish a `TXT` DNS record for this node into the tailnet's `ts.net` zone via control's
1142    /// `/machine/set-dns` RPC — the Rust analog of Go `tailscale.com/client/tailscale`'s
1143    /// `LocalClient.SetDNS(ctx, name, value)`.
1144    ///
1145    /// `name` is the full record name (e.g. `_acme-challenge.host.tailnet.ts.net`) and `value` is
1146    /// the record value (e.g. the base64url DNS-01 digest). Like Go's `SetDNS`, this publishes a
1147    /// `TXT` record specifically — its canonical use is satisfying an ACME DNS-01 challenge so a CA
1148    /// can verify control of a `*.ts.net` name. Issuance over the Noise transport (`POST
1149    /// /machine/set-dns`), not a login path.
1150    pub async fn set_dns(&self, name: &str, value: &str) -> Result<(), ts_control::SetDnsError> {
1151        self.runtime
1152            .set_dns(name.to_string(), value.to_string())
1153            .await
1154    }
1155
1156    /// Log this node out of the tailnet — deregister it from the control plane (the equivalent of
1157    /// Go `tsnet`'s `LocalClient.Logout`).
1158    ///
1159    /// Re-`POST`s `/machine/register` with this node's current node key and a past expiry, which the
1160    /// control plane honors by **expiring the node now**: it drops out of every peer's netmap and
1161    /// must re-register (re-authenticate) to rejoin.
1162    ///
1163    /// This is primarily for **non-ephemeral** nodes. An ephemeral node is garbage-collected by
1164    /// control shortly after it disconnects, but a persistent node lingers in the tailnet
1165    /// (visible to peers, counting against the machine limit) for up to ~24h after the process exits
1166    /// unless explicitly logged out. Call this before [`shutdown`](Self::shutdown) to deregister
1167    /// immediately. Calling it on an ephemeral node simply brings the GC forward; it is idempotent,
1168    /// so logging out an already-gone node is not an error.
1169    ///
1170    /// This is a **control-plane state change only**: it does not tear down the local datapath (do
1171    /// that via [`shutdown`](Self::shutdown)), and it does not delete or rotate the on-disk node key
1172    /// — re-registering with the same key (a fresh [`Device::new`]) is the re-login path.
1173    pub async fn logout(&self) -> Result<(), ts_control::LogoutError> {
1174        self.runtime.logout().await
1175    }
1176
1177    /// Snapshot this node's client metrics in Prometheus text exposition format.
1178    ///
1179    /// Mirrors Go Tailscale's `clientmetric` registry: process-global counters/gauges incremented
1180    /// on the datapath hot loops (e.g. `magicsock_send_udp`, `magicsock_recv_data_bytes_udp`),
1181    /// rendered as `# TYPE <name> <kind>\n<name> <value>\n` per metric, sorted by name. (Go `tsnet`
1182    /// exposes no metrics method of its own, so this is the fork's clean public surface.) The
1183    /// registry is process-global, so the output covers every `Device` in the process.
1184    pub fn metrics(&self) -> String {
1185        ts_metrics::write_prometheus()
1186    }
1187
1188    /// Map a tailnet source `addr` to the node that owns its IP (like `tsnet`'s `WhoIs`).
1189    ///
1190    /// Only the IP of `addr` is used; the port is ignored. Returns `Ok(None)` if no tailnet node
1191    /// owns that address.
1192    pub async fn whois(&self, addr: SocketAddr) -> Result<Option<WhoIs>, Error> {
1193        self.runtime.whois(addr).await.map_err(Into::into)
1194    }
1195
1196    /// Change the selected exit node at runtime, without recreating the [`Device`] — the equivalent
1197    /// of Go `tsnet`'s `LocalClient.EditPrefs(ExitNodeID/ExitNodeIP)`.
1198    ///
1199    /// The peer may be named by stable node ID, tailnet IP, or MagicDNS name via
1200    /// [`ExitNodeSelector`] (a bare IP or name parses with `selector.parse()`); this is the same
1201    /// selector type as [`Config::exit_node`](crate::Config::exit_node), so the construction-time
1202    /// and runtime paths are identical. Passing `None` clears the exit node — internet-bound traffic
1203    /// is then dropped (fail-closed) unless this node egresses directly.
1204    ///
1205    /// The change is applied immediately: the new selector is re-resolved against the live peer set
1206    /// and the outbound route + inbound source filter are recomputed at once. A selector for a peer
1207    /// not yet in the netmap simply takes effect once that peer appears.
1208    ///
1209    /// Only NEW flows use the changed exit; in-flight connections are not torn down and continue
1210    /// egressing via the previously-selected exit until they close.
1211    pub async fn set_exit_node(&self, exit_node: Option<ExitNodeSelector>) -> Result<(), Error> {
1212        self.runtime
1213            .set_exit_node(exit_node)
1214            .await
1215            .map_err(Into::into)
1216    }
1217
1218    /// The currently-selected exit node, or `None` if none is selected.
1219    pub fn exit_node(&self) -> Option<ExitNodeSelector> {
1220        self.runtime.exit_node()
1221    }
1222
1223    /// Change the subnet routes this node advertises at runtime — Go `tailscale set
1224    /// --advertise-routes`. This is the runtime equivalent of
1225    /// [`Config::advertise_routes`](crate::Config::advertise_routes): the node re-advertises the
1226    /// prefixes to control (so it is granted the subnet-router role for them) AND starts forwarding
1227    /// them on the data path, applied together so the two never disagree.
1228    ///
1229    /// `routes` is filtered to the IPv4-only, deduplicated set this fork honors (IPv6 prefixes are
1230    /// dropped under the IPv6-off posture). This sets the explicit subnet prefixes only; it does not
1231    /// affect the exit-node `0.0.0.0/0` advertisement. Only NEW forwarded flows use the changed set;
1232    /// in-flight flows keep their existing routing until they close.
1233    pub async fn set_advertise_routes(&self, routes: Vec<ipnet::IpNet>) -> Result<(), Error> {
1234        self.runtime
1235            .set_advertise_routes(routes)
1236            .await
1237            .map_err(Into::into)
1238    }
1239
1240    /// Re-bind the underlay UDP socket after a **network/link change** — Wi-Fi switch, sleep/wake,
1241    /// or any event that invalidates the device's local address/NAT mapping. This is the Rust
1242    /// analog of Go magicsock's `Conn.Rebind()`.
1243    ///
1244    /// The embedder owns deciding *when* to call this (it watches the OS for link changes — there is
1245    /// no built-in network monitor); `rebind` is the engine half that does the socket work:
1246    /// - Re-binds the underlay UDP socket, preferring the same local port (so the advertised
1247    ///   endpoint stays stable) and falling back to an ephemeral port. The IPv4-only-by-default
1248    ///   invariant is preserved.
1249    /// - Invalidates the now-stale local mapping: learned reflexive (STUN) addresses and every
1250    ///   peer's *confirmed* direct path are cleared, while candidate endpoints are kept — so peers
1251    ///   are re-probed over the new socket and **relay over DERP (never a direct host dial) until a
1252    ///   path re-confirms**. Endpoint discovery re-runs on its normal cadence.
1253    /// - Leaves peers, control, the netmap, disco keys, and DERP connections untouched; existing
1254    ///   WireGuard sessions survive (they ride whatever underlay carries them).
1255    ///
1256    /// A no-op if the underlay socket failed to bind at startup (the device is DERP-only). Existing
1257    /// connectivity is preserved on a re-bind error (the old socket is kept; the error is returned).
1258    pub async fn rebind(&self) -> Result<(), Error> {
1259        self.runtime.rebind().await.map_err(Into::into)
1260    }
1261
1262    /// The stable id of the exit node traffic is **currently** egressing through, or `None` if none
1263    /// is engaged (the equivalent of Go `tsnet`'s `Status.ExitNodeStatus.ID`).
1264    ///
1265    /// This differs from [`exit_node`](Self::exit_node), which returns the *configured* selector:
1266    /// the active exit node is the route updater's resolved, fail-closed answer. It is `None` when
1267    /// no exit node is configured, the configured selector matches no current peer, or the matched
1268    /// peer no longer advertises a default route (egress is then dropped, fail-closed). Match the id
1269    /// against [`Status::peers`](crate::Status::peers) (via [`status`](Self::status)) for details.
1270    pub fn active_exit_node(&self) -> Option<ts_control::StableNodeId> {
1271        self.runtime.active_exit_node()
1272    }
1273
1274    /// Watch for netmap changes: the returned receiver's value is the current set of peer
1275    /// [`StatusNode`]s and updates on every netmap change (like subscribing to `ipn` notifications).
1276    pub async fn watch_netmap(
1277        &self,
1278    ) -> Result<tokio::sync::watch::Receiver<Vec<StatusNode>>, Error> {
1279        self.runtime.watch_netmap().await.map_err(Into::into)
1280    }
1281
1282    /// The current device connection-[`DeviceState`] (`Connecting` / `Running` / `NeedsLogin` /
1283    /// `Expired` / `Failed`).
1284    pub fn device_state(&self) -> DeviceState {
1285        self.runtime.device_state()
1286    }
1287
1288    /// Watch the device connection-[`DeviceState`], reacting push-style to control connection
1289    /// transitions instead of polling [`status`](Self::status).
1290    ///
1291    /// Returns a [`tokio::sync::watch::Receiver`]; await its
1292    /// [`changed`](tokio::sync::watch::Receiver::changed) to be woken on each transition. The
1293    /// initial value is the current state.
1294    pub fn watch_state(&self) -> tokio::sync::watch::Receiver<DeviceState> {
1295        self.runtime.watch_state()
1296    }
1297
1298    /// Wait until the device finishes registering, returning a typed outcome — the clean
1299    /// replacement for polling [`ipv4_addr`](Self::ipv4_addr) in a loop.
1300    ///
1301    /// Resolves `Ok(())` once the device is [`DeviceState::Running`]. On a non-running outcome it
1302    /// returns a typed [`RegistrationError`]:
1303    /// - [`AuthRejected`](RegistrationError::AuthRejected) — bad/expired/unknown auth key;
1304    ///   **permanent** (re-pair).
1305    /// - [`NeedsLogin`](RegistrationError::NeedsLogin) — interactive authorization required;
1306    ///   **not permanent** (the runtime keeps retrying and reaches `Running` once the user
1307    ///   authorizes). Auth-key callers treat this as failure; interactive callers should ignore it
1308    ///   and drive the flow via [`watch_state`](Self::watch_state).
1309    /// - [`NetworkUnreachable`](RegistrationError::NetworkUnreachable) — **transient** (retry).
1310    /// - [`Timeout`](RegistrationError::Timeout) — no settled state within `timeout` (`None` waits
1311    ///   indefinitely).
1312    ///
1313    /// [`KeyExpired`](RegistrationError::KeyExpired) is not produced here (a key expires only after
1314    /// the node is up); observe it via [`watch_state`](Self::watch_state). Use
1315    /// [`RegistrationError::is_permanent`] to branch "re-pair" vs. "retry / drive login".
1316    pub async fn wait_until_running(
1317        &self,
1318        timeout: Option<Duration>,
1319    ) -> Result<(), RegistrationError> {
1320        self.runtime.wait_until_running(timeout).await
1321    }
1322
1323    /// Ping a tailnet peer over the overlay with an ICMPv4 echo, returning the round-trip time
1324    /// (like `tailscale ping`).
1325    ///
1326    /// The echo is sent from this device's own tailnet IPv4 over the overlay netstack — never a
1327    /// host socket. IPv6 destinations return [`PingError::Ipv6Unsupported`] (this fork is
1328    /// IPv4-only on the tailnet). A peer answers from its own OS stack; this netstack does not
1329    /// auto-reply to echo requests.
1330    ///
1331    /// In TUN transport mode there is no application netstack to ping from; this surfaces as
1332    /// [`PingError::Timeout`] (the same error this method already uses for an unavailable source
1333    /// address — `PingError` carries no dedicated "unsupported" variant).
1334    pub async fn ping(&self, dst: IpAddr, timeout: Duration) -> Result<Duration, PingError> {
1335        let channel = self.channel().map_err(|_| PingError::Timeout)?;
1336        let src = self.ipv4_addr().await.map_err(|_| PingError::Timeout)?;
1337        ts_netstack_smoltcp::ping(channel, src, dst, timeout).await
1338    }
1339
1340    /// Obtain a TLS certificate for a node's MagicDNS `name` (like `tsnet`'s `GetCertificate`).
1341    ///
1342    /// **Fail-closed without the `acme` feature.** By default this fork has no client-side ACME
1343    /// engine wired in, so this returns [`ts_control::CertError::Unimplemented`] (after a
1344    /// tailnet-name check) — it NEVER self-signs and NEVER returns a placeholder certificate
1345    /// ([`ts_control::MISSING_CERT_RPC`] names what is missing).
1346    ///
1347    /// **With the `acme` feature** this instead drives the client-side ACME DNS-01 engine to issue a
1348    /// real Let's Encrypt certificate for `name`, publishing the challenge TXT via the node's
1349    /// `POST /machine/set-dns` RPC (routed through the control runner). SaaS-only: a self-hosted
1350    /// control plane may 501 on set-dns, surfaced as [`ts_control::CertError::Acme`].
1351    #[cfg(not(feature = "acme"))]
1352    pub async fn get_certificate(&self, name: &str) -> Result<CertifiedKey, ts_control::CertError> {
1353        ts_control::get_certificate(name).await
1354    }
1355
1356    /// See the no-`acme` variant for the contract; with `acme` this issues a real cert via the
1357    /// runtime's ACME engine (`Device → Runtime → ControlRunner → issue_certificate_via_setdns`).
1358    #[cfg(feature = "acme")]
1359    pub async fn get_certificate(&self, name: &str) -> Result<CertifiedKey, ts_control::CertError> {
1360        self.runtime.get_certificate(name.to_string()).await
1361    }
1362
1363    /// Build a [`TlsAcceptor`] terminating TLS for `cfg.name` on the overlay (like `tsnet`'s
1364    /// `ListenTLS`).
1365    ///
1366    /// Obtains the certificate via [`Device::get_certificate`] — so with the `acme` feature this
1367    /// issues a real Let's Encrypt cert (when the control plane answers `set-dns`), and without it
1368    /// (or when issuance is unavailable) it surfaces the same fail-closed
1369    /// [`ts_control::CertError`] rather than ever serving a self-signed cert or downgrading to
1370    /// plaintext. Terminate accepted overlay streams with [`ts_control::accept_tls`].
1371    pub async fn listen_tls(
1372        &self,
1373        cfg: &ts_control::ServeConfig,
1374    ) -> Result<TlsAcceptor, ts_control::CertError> {
1375        // Route through Device::get_certificate (the acme-aware issuance path) rather than
1376        // ts_control::listen_tls, which only knows the non-acme stub. Validate the serve config
1377        // first (same fail-closed checks ts_control::listen_tls applies), then assemble the acceptor.
1378        cfg.validate()?;
1379        let cert = self.get_certificate(&cfg.name).await?;
1380        ts_control::tls_acceptor(cert)
1381    }
1382
1383    /// The currently-stored Serve config (like `tsnet`'s `GetServeConfig`).
1384    ///
1385    /// Returns the config last passed to [`Device::set_serve_config`], or an empty
1386    /// [`ts_control::ServeState`] (no ports) if none was ever set. Pure read — does not touch the
1387    /// network.
1388    pub fn get_serve_config(&self) -> ts_control::ServeState {
1389        match &*self.serve.lock().unwrap_or_else(|e| e.into_inner()) {
1390            Some(mgr) => mgr.get(),
1391            None => ts_control::ServeState::default(),
1392        }
1393    }
1394
1395    /// Replace this node's Serve config and (re)bind its tailnet ports (like `tsnet`'s
1396    /// `SetServeConfig`, REPLACE semantics).
1397    ///
1398    /// `state` becomes the **whole** config (full-replace reconcile: every previously-bound serve
1399    /// port's accept loop is torn down and the new config's ports are bound from scratch). For each
1400    /// configured port the manager binds an overlay listener on this node's tailnet IPv4 and
1401    /// dispatches per [`ts_control::ServeTarget`]:
1402    /// - [`Accept`](ts_control::ServeTarget::Accept) — the TLS-terminated stream is handed back over
1403    ///   the returned [`ServeAcceptedReceiver`](ts_runtime::serve::ServeAcceptedReceiver) (the
1404    ///   in-process stand-in for `ListenTLS`'s `net.Listener`).
1405    /// - [`Proxy`](ts_control::ServeTarget::Proxy) — reverse-proxy the decrypted stream to a local
1406    ///   host backend.
1407    /// - [`Text`](ts_control::ServeTarget::Text) — write a fixed body and close.
1408    /// - [`TcpForward`](ts_control::ServeTarget::TcpForward) — forward the **raw** (non-TLS) stream
1409    ///   to a local host backend.
1410    ///
1411    /// **Fail-closed.** `state.validate()` runs first. Every TLS-terminating port's acceptor is
1412    /// obtained up-front via [`Device::listen_tls`] (the ACME-aware cert path); if any cert cannot be
1413    /// issued the whole call fails with that [`ts_control::CertError`] and **nothing is bound** — a
1414    /// TLS port never downgrades to plaintext.
1415    ///
1416    /// **Anti-leak.** Listeners bind the overlay netstack only (never a host socket). The
1417    /// `Proxy`/`TcpForward` backend dial is a local host socket to the embedder's own backend (like
1418    /// Go's reverse-proxy to `127.0.0.1`), intentionally NOT routed through the exit-egress
1419    /// forwarder. A backend dial failure drops that connection; it never falls back.
1420    ///
1421    /// Returns an error in TUN transport mode (there is no application netstack to bind on). The
1422    /// previous config's accept loops (and any earlier `ServeAcceptedReceiver`) stop when this
1423    /// returns; the new receiver delivers every `Accept`-port connection.
1424    pub async fn set_serve_config(
1425        &self,
1426        state: ts_control::ServeState,
1427    ) -> Result<ts_runtime::serve::ServeAcceptedReceiver, Error> {
1428        state
1429            .validate()
1430            .map_err(|_| Error::Internal(InternalErrorKind::BadRequest))?;
1431
1432        // Fail-closed: build every TLS-terminating port's acceptor up-front via the ACME-aware cert
1433        // path. If any cert can't be issued, return before binding anything (no plaintext downgrade).
1434        let mut resolved = std::collections::BTreeMap::new();
1435        for (port, target) in &state.ports {
1436            let acceptor = if target.terminates_tls() {
1437                let cfg = ts_control::ServeConfig {
1438                    name: state.name.clone(),
1439                    port: *port,
1440                    target: target.clone(),
1441                };
1442                Some(self.listen_tls(&cfg).await.map_err(|_| {
1443                    // Cert issuance is fail-closed in this fork; surface as a request error rather
1444                    // than ever binding a plaintext TLS port.
1445                    Error::Internal(InternalErrorKind::BadRequest)
1446                })?)
1447            } else {
1448                None
1449            };
1450            resolved.insert(
1451                *port,
1452                ts_runtime::serve::ResolvedPort {
1453                    target: target.clone(),
1454                    acceptor,
1455                },
1456            );
1457        }
1458
1459        // The manager binds the OVERLAY netstack on this node's own tailnet IPv4.
1460        let self_ipv4 = self.ipv4_addr().await?;
1461        let channel = self.channel()?.clone();
1462
1463        let mut slot = self.serve.lock().unwrap_or_else(|e| e.into_inner());
1464        let mgr =
1465            slot.get_or_insert_with(|| ts_runtime::serve::ServeManager::new(channel, self_ipv4));
1466        Ok(mgr.set(state, resolved))
1467    }
1468
1469    /// Expose a tailnet TLS service to the public internet via Tailscale Funnel (like `tsnet`'s
1470    /// `ListenFunnel`), returning a [`FunnelAcceptedReceiver`](ts_runtime::funnel::FunnelAcceptedReceiver)
1471    /// that delivers each TLS-terminated public connection.
1472    ///
1473    /// **Two fail-closed gates, then the live ingress listener.** First the node-attribute gate is
1474    /// fully enforced from this node's own capability map (mirroring Go `ipn.NodeCanFunnel` +
1475    /// `ipn.CheckFunnelPort`): the tailnet admin must have enabled HTTPS and granted the `funnel`
1476    /// node attribute, and `cfg.port` must be in the set the `funnel-ports` capability allows —
1477    /// otherwise this returns [`ts_control::FunnelError::NotAllowed`] /
1478    /// [`ts_control::FunnelError::PortNotAllowed`] before touching any cert or network. Then the
1479    /// node's `*.ts.net` certificate is obtained via the ACME-aware [`Device::get_certificate`] (the
1480    /// Funnel hostname *is* the node's MagicDNS name, so its DNS-01 cert matches); fail-closed on
1481    /// [`ts_control::FunnelError::Cert`] — no self-signed or plaintext fallback.
1482    ///
1483    /// On success a [`FunnelManager`](ts_runtime::funnel::FunnelManager) is registered: its ingress
1484    /// sink is installed into the runtime's peerAPI `/v0/ingress` slot (making that route live without
1485    /// restarting the peerAPI server), and the `HostInfo.IngressEnabled` map-request signal is set so
1486    /// control routes Funnel traffic to this node. Public Funnel bytes arrive as a relay POST to
1487    /// `/v0/ingress`, are membership-gated + `101`-hijacked into a raw stream, TLS-terminated by the
1488    /// manager, and delivered over the returned receiver.
1489    ///
1490    /// **Where the relay comes from.** The public ingress **relay + DNS mapping** that feed
1491    /// `/v0/ingress` are Tailscale infrastructure ([`ts_control::MISSING_FUNNEL_RELAY`]), provisioned
1492    /// automatically against real Tailscale SaaS with a Funnel-enabled ACL; against a self-hosted
1493    /// control plane no relay exists, so the listener is correct but never fed.
1494    ///
1495    /// Anti-leak: Funnel TLS terminates only on the overlay netstack (the hijacked ingress stream
1496    /// arrives on the overlay peerAPI listener), never a host socket; there is no self-signed or
1497    /// plaintext fallback. A new `listen_funnel` replaces the previous manager (its pump + sink tear
1498    /// down); dropping the `Device` tears it down too.
1499    pub async fn listen_funnel(
1500        &self,
1501        cfg: &ts_control::ServeConfig,
1502        opts: ts_control::FunnelOptions,
1503    ) -> Result<ts_runtime::funnel::FunnelAcceptedReceiver, ts_control::FunnelError> {
1504        // Gate 1 (fail-closed, no network): node-attribute + funnel-port access from our cap map.
1505        let me = self
1506            .self_node()
1507            .await
1508            .map_err(|_| ts_control::FunnelError::NotAllowed)?;
1509        cfg.validate()?;
1510        ts_control::funnel_access(&me, cfg.port)?;
1511
1512        // Gate 2 (fail-closed): obtain the node's `*.ts.net` cert via the ACME-aware path and build
1513        // the TLS acceptor. A cert failure surfaces as FunnelError::Cert — never a plaintext listener.
1514        let cert = self
1515            .get_certificate(&cfg.name)
1516            .await
1517            .map_err(ts_control::FunnelError::Cert)?;
1518        let acceptor = ts_control::tls_acceptor(cert).map_err(ts_control::FunnelError::Cert)?;
1519
1520        // `opts.funnel_only` (reject tailnet-internal connections) is accepted for surface stability;
1521        // the ingress data path only ever carries relay-delivered public traffic, so there is no
1522        // tailnet-internal leg on this listener to reject. Documented as a no-op here for now.
1523        let _ = opts;
1524
1525        // Build the funnel manager + its ingress sink + the hand-back receiver, install the sink into
1526        // the runtime's shared peerAPI `/v0/ingress` slot (making the route live), and flip the
1527        // IngressEnabled map signal. Hold the manager on the device so its pump/sink live as long as
1528        // the listener; replacing a prior manager tears the old one down on drop at end of scope.
1529        let (manager, sink, receiver) = ts_runtime::funnel::FunnelManager::new(acceptor);
1530        {
1531            let slot = self.runtime.funnel_ingress_slot();
1532            *slot.lock().unwrap_or_else(|e| e.into_inner()) = Some(sink);
1533        }
1534        self.runtime
1535            .ingress_active_flag()
1536            .store(true, std::sync::atomic::Ordering::Relaxed);
1537
1538        let old = {
1539            let mut held = self.funnel.lock().unwrap_or_else(|e| e.into_inner());
1540            held.replace(manager)
1541        };
1542        drop(old);
1543
1544        Ok(receiver)
1545    }
1546
1547    /// Host a Tailscale **VIP service** (`svc:<label>`) by binding an overlay listener on the
1548    /// service's control-assigned virtual IP (like `tsnet`'s `ListenService`).
1549    ///
1550    /// **Fail-closed.** Mirrors Go `tsnet.Server.ListenService`'s preconditions, enforced from this
1551    /// node's own netmap state ([`ts_control::resolve_service_listen`]): the `name` must be a valid
1552    /// `svc:<dns-label>`, this node must be **tagged** (Go `ErrUntaggedServiceHost`), and control
1553    /// must have assigned the service a VIP address on this node (delivered via the `service-host`
1554    /// node-capability — see [`ts_control::Node::service_addresses`]). Any unmet precondition
1555    /// returns a typed [`ts_control::ServiceError`] before binding anything.
1556    ///
1557    /// When all hold, this binds a [`tcp_listen`][Device::tcp_listen] on the service VIP and the
1558    /// configured `mode` port over the **overlay netstack** (never a host socket) and returns the
1559    /// listener. The netstack already accepts packets for control-assigned VIPs (they are injected
1560    /// alongside the node's own tailnet address), so the listener is reachable by tailnet peers.
1561    ///
1562    /// The `Tun`/L3 service mode is unsupported (a TODO in upstream tsnet); only TCP/HTTP modes
1563    /// (which bind the same VIP:port at the listen layer) are offered. Returns an error in TUN
1564    /// transport mode (there is no application netstack to bind on).
1565    pub async fn listen_service(
1566        &self,
1567        name: &str,
1568        mode: ts_control::ServiceMode,
1569    ) -> Result<netstack::TcpListener, ts_control::ServiceError> {
1570        let me = self
1571            .self_node()
1572            .await
1573            .map_err(|e| ts_control::ServiceError::Listen(e.to_string()))?;
1574        let listen_addr = ts_control::resolve_service_listen(&me, name, mode, self.enable_ipv6)?;
1575        self.tcp_listen(listen_addr)
1576            .await
1577            .map_err(|e| ts_control::ServiceError::Listen(e.to_string()))
1578    }
1579
1580    /// Attempt to gracefully shut down this device's runtime.
1581    ///
1582    /// Reports whether the device was fully shut down before the timeout. It is still shut
1583    /// down if it timed out, just more violently and with potential resource leaks.
1584    ///
1585    /// If `timeout` is `None`, then shutdown will never time-out.
1586    pub async fn shutdown(self, timeout: Option<Duration>) -> bool {
1587        self.runtime.graceful_shutdown(timeout).await
1588    }
1589}
1590
1591/// Command-channel-driven userspace network stack.
1592///
1593/// This is an opinionated wrapper around [smoltcp](https://docs.rs/smoltcp) that provides an
1594/// easier-to-integrate, more-portable API.
1595pub mod netstack {
1596    #[doc(inline)]
1597    pub use ts_netstack_smoltcp::netcore::Error;
1598    #[doc(inline)]
1599    pub use ts_netstack_smoltcp::netcore::InternalErrorKind;
1600    #[doc(inline)]
1601    pub use ts_netstack_smoltcp::netsock::{TcpListener, TcpStream, UdpSocket};
1602}
1603
1604/// Geneve (RFC 8926) framing for Tailscale **peer-relay** traffic. A peer that advertises
1605/// [`NodeInfo::is_peer_relay`] runs a UDP relay server; relayed disco + WireGuard frames are
1606/// Geneve-encapsulated with a VNI. This module exposes the header codec so the framing is
1607/// recognizable. NOTE: the active relay *data path* (the relay-allocation handshake +
1608/// magicsock integration) is **not yet implemented** in this fork — this is the wire-aware slice.
1609pub mod geneve {
1610    #[doc(inline)]
1611    pub use ts_packet::geneve::{
1612        GENEVE_FIXED_HEADER_LEN, GENEVE_PROTOCOL_DISCO, GENEVE_PROTOCOL_WIREGUARD, GeneveError,
1613        GeneveHeader,
1614    };
1615}
1616
1617/// Tailnet Lock (TKA) verification: the [`tka::Authority`] checks a peer's node-key signature
1618/// against the trusted-key state, mirroring Go's `tka` package. Pair with [`Device::tka_status`]
1619/// (the control-pushed head/disablement signal).
1620pub mod tka {
1621    #[doc(inline)]
1622    pub use ts_tka::{
1623        AumHash, AumKind, Authority, Key, KeyKind, NodeKeySignature, SigKind, State, TkaError,
1624        aum_hash,
1625    };
1626}
1627
1628/// Tailscale cryptographic key types.
1629pub mod keys {
1630    #[doc(inline)]
1631    pub use ts_keys::{
1632        DiscoKeyPair, DiscoPrivateKey, DiscoPublicKey, MachineKeyPair, MachinePrivateKey,
1633        MachinePublicKey, NetworkLockKeyPair, NetworkLockPrivateKey, NetworkLockPublicKey,
1634        NodeKeyPair, NodePrivateKey, NodePublicKey, NodeState, PersistState,
1635    };
1636}
1637
1638const ENV_MAGIC_VAR: &str = "TS_RS_EXPERIMENT";
1639const ENV_MAGIC_VALUE: &str = "this_is_unstable_software";
1640
1641fn check_magic_env() -> Result<(), Error> {
1642    if std::env::var(ENV_MAGIC_VAR).as_deref() != Ok(ENV_MAGIC_VALUE) {
1643        let warning = format!(
1644            "
1645check failed: set {ENV_MAGIC_VAR}={ENV_MAGIC_VALUE} to acknowledge that tailscale-rs is early-days
1646experimental software containing bugs, unvalidated cryptography, and no stability or compatibility
1647guarantees.
1648            "
1649        );
1650
1651        eprintln!("{}", warning.trim());
1652
1653        return Err(Error::UnstableEnvVar);
1654    };
1655
1656    Ok(())
1657}
1658
1659#[cfg(test)]
1660mod tests {
1661    use secrecy::ExposeSecret as _;
1662
1663    use super::*;
1664
1665    // `Device::new`/`new_with_secret` cannot be unit-tested end-to-end without a live control
1666    // server (registration). The only behavioral difference `new_with_secret` introduces over `new`
1667    // is exposing the `SecretString` to a plain `String` on the last inch; everything after is the
1668    // shared `new` path. So we assert that equivalence at the auth-key-resolution level: the secret
1669    // path must resolve to the exact same key the plain path feeds into `resolve_auth_key`.
1670    const SAMPLE_KEY: &str = "tskey-auth-koCgSLP5R811CNTRL-EXAMPLEEXAMPLEEXAMPLEEXAMPLE";
1671
1672    // The mapping `new_with_secret` applies (`Option<SecretString>` -> `Option<String>`) must be a
1673    // byte-for-byte round-trip, so the spawn arg is identical to a direct `new(config, Some(..))`.
1674    #[test]
1675    fn secret_exposes_to_identical_string() {
1676        let plain = Some(SAMPLE_KEY.to_string());
1677        let from_secret =
1678            Some(SecretString::from(SAMPLE_KEY)).map(|s| s.expose_secret().to_string());
1679        assert_eq!(from_secret, plain);
1680
1681        // `None` must pass through unchanged (so it falls back to `config.auth_key` exactly as `new`).
1682        let none_secret: Option<SecretString> = None;
1683        assert_eq!(
1684            none_secret.map(|s| s.expose_secret().to_string()),
1685            None::<String>
1686        );
1687    }
1688
1689    // End-to-end equivalence at the resolve layer: feeding the exposed secret through
1690    // `resolve_auth_key` yields the same `Option<String>` as feeding the plain string — i.e. both
1691    // constructors reach the same spawn argument, without registering against a control server.
1692    #[tokio::test]
1693    async fn new_with_secret_resolves_same_as_new() {
1694        let config = Config::default();
1695
1696        let via_plain = resolve_auth_key(&config, Some(SAMPLE_KEY.to_string()))
1697            .await
1698            .expect("plain auth key resolves");
1699
1700        let exposed = Some(SecretString::from(SAMPLE_KEY)).map(|s| s.expose_secret().to_string());
1701        let via_secret = resolve_auth_key(&config, exposed)
1702            .await
1703            .expect("secret-derived auth key resolves");
1704
1705        assert_eq!(via_plain, via_secret);
1706        // Without the `identity-federation` feature `resolve_auth_key` is a pass-through, so the
1707        // resolved key is the input verbatim; assert that too to pin the default-build behavior.
1708        #[cfg(not(feature = "identity-federation"))]
1709        assert_eq!(via_secret, Some(SAMPLE_KEY.to_string()));
1710    }
1711}