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#[doc(inline)]
142pub use ts_control::ExitNodeSelector;
143#[doc(inline)]
144pub use ts_control::Node as NodeInfo;
145#[doc(inline)]
146pub use ts_control::tls::{CertifiedKey, TlsAcceptor, TlsStream};
147#[doc(inline)]
148pub use ts_control::{CertError, MISSING_CERT_RPC, ServeConfig, ServeState, ServeTarget};
149#[doc(inline)]
150pub use ts_control::{ExitProxyConfig, ExitProxyScheme};
151pub use ts_control::{
152    IdTokenError, LogoutError, ServiceError, ServiceMode, SshAccept, SshAction, SshConnIdentity,
153    SshDecision, SshDenyReason, SshPolicy, SshPrincipal, SshRule, StableNodeId,
154};
155// Re-exported so the application data-path transport can be selected through the `tailscale`
156// facade alone: `Config::transport_mode` is `TransportMode` (default `Netstack`; `Tun(TunConfig {
157// name, mtu })` for a real kernel TUN interface). Both are `pub` in `ts_control` but were not
158// reachable through this facade, forcing downstream crates to depend on `ts_control` directly just
159// to name them.
160pub use ts_control::{TransportMode, TunConfig};
161#[doc(inline)]
162pub use ts_netstack_smoltcp::PingError;
163use ts_netstack_smoltcp::{CreateSocket, netcore::Channel};
164#[doc(inline)]
165pub use ts_runtime::fallback_tcp::{
166    FallbackConnFuture, FallbackConnHandler, FallbackDecision, FallbackTcpHandle,
167};
168#[doc(inline)]
169pub use ts_runtime::taildrop::WaitingFile;
170#[doc(inline)]
171pub use ts_runtime::{DeviceState, RegistrationError, Status, StatusNode, WhoIs};
172
173#[cfg(feature = "axum")]
174pub mod axum;
175pub mod config;
176mod error;
177mod loopback;
178#[cfg(feature = "ssh")]
179pub mod ssh;
180
181#[doc(inline)]
182pub use loopback::LoopbackHandle;
183
184/// How a program connects to a tailnet and communicates with peers.
185///
186/// The `Device` connects to the control plane, registers itself with the tailnet, and communicates
187/// with tailnet peers. Its tailnet identity is determined by the key state provided at
188/// construction-time.
189pub struct Device {
190    runtime: ts_runtime::Runtime,
191    /// Command channel to the application netstack. `None` in TUN transport mode, where there is
192    /// no userspace application netstack; the channel-driven socket APIs ([`Device::udp_bind`],
193    /// [`Device::tcp_listen`], [`Device::tcp_connect`], [`Device::ping`]) are unsupported there.
194    channel: Option<Channel>,
195    /// Whether IPv6 is enabled on the tailnet overlay (the `Config::enable_ipv6` gate, default
196    /// `false`). Captured at construction; used by [`Device::listen_service`] to decide whether an
197    /// IPv6 VIP-service address is bindable (the netstack only accepts IPv6 overlay addresses when
198    /// this is set).
199    enable_ipv6: bool,
200    /// The stored Serve config + its live per-port accept loops (`tsnet`'s `Get/SetServeConfig` +
201    /// serving runtime). Built lazily on the first [`Device::set_serve_config`] (it needs this
202    /// node's overlay IPv4, only known after registration). Held here so its accept loops abort when
203    /// the `Device` drops; `None` (empty config) until the first `set`.
204    serve: std::sync::Mutex<Option<ts_runtime::serve::ServeManager>>,
205    /// The live Funnel ingress manager (`tsnet`'s `ListenFunnel` data path), built on
206    /// [`Device::listen_funnel`](crate::Device::listen_funnel). Held here so its TLS-termination pump and the installed peerAPI
207    /// ingress sink stay alive for the device's life (and tear down when a new `listen_funnel`
208    /// replaces it, or the `Device` drops). `None` until the first `listen_funnel`.
209    funnel: std::sync::Mutex<Option<ts_runtime::funnel::FunnelManager>>,
210}
211
212/// Map a [`ts_runtime::taildrop::TaildropError`] to the device-facing [`Error`]. `Error` is a
213/// `Copy` enum with no payload, so the I/O detail string is dropped, but the *kind* is preserved so
214/// a caller can still distinguish the actionable cases: an invalid name →
215/// [`InternalErrorKind::BadRequest`], an in-progress conflict → [`InternalErrorKind::AlreadyExists`],
216/// a missing file → [`InternalErrorKind::NotFound`], and any other filesystem failure →
217/// [`InternalErrorKind::Io`].
218fn taildrop_err(e: ts_runtime::taildrop::TaildropError) -> Error {
219    use ts_runtime::taildrop::TaildropError;
220    match e {
221        TaildropError::InvalidFileName => Error::Internal(InternalErrorKind::BadRequest),
222        TaildropError::FileExists => Error::Internal(InternalErrorKind::AlreadyExists),
223        TaildropError::Io(io) if io.kind() == std::io::ErrorKind::NotFound => {
224            Error::Internal(InternalErrorKind::NotFound)
225        }
226        TaildropError::Io(_) => Error::Internal(InternalErrorKind::Io),
227    }
228}
229
230/// Map a [`ts_runtime::taildrop_send::TaildropSendError`] (the Taildrop *sender*) to the
231/// device-facing [`Error`]. The send-side conflict/forbidden/unexpected-status cases all reduce to
232/// `BadRequest` (the peer refused the transfer for a request-level reason), a dial failure or
233/// timeout to `Timeout`, an invalid name to `BadRequest`, and any stream I/O failure to `Io`.
234fn taildrop_send_err(e: ts_runtime::taildrop_send::TaildropSendError) -> Error {
235    use ts_runtime::taildrop_send::TaildropSendError;
236    match e {
237        TaildropSendError::Connect | TaildropSendError::Timeout => Error::Timeout,
238        TaildropSendError::InvalidName
239        | TaildropSendError::Forbidden
240        | TaildropSendError::Conflict
241        | TaildropSendError::UnexpectedStatus(_) => Error::Internal(InternalErrorKind::BadRequest),
242        TaildropSendError::Io => Error::Internal(InternalErrorKind::Io),
243    }
244}
245
246/// Resolve the effective registration auth key from `auth_key` plus the config's
247/// workload-identity-federation (WIF) / OAuth-client fields.
248///
249/// With the `identity-federation` feature enabled, an OAuth client secret (`tskey-client-…`) or a
250/// `client_id` + (`id_token` | `audience`) is exchanged for a Tailscale auth key against the SaaS
251/// admin API before registration (Go `tsnet.Server`'s `resolveAuthKey`). Without the feature this is
252/// a pure pass-through: `auth_key` is returned unchanged and the WIF config fields are ignored, so
253/// the default build is byte-identical to before.
254#[cfg(feature = "identity-federation")]
255async fn resolve_auth_key(
256    config: &Config,
257    auth_key: Option<String>,
258) -> Result<Option<String>, Error> {
259    let wif = ts_control::WifConfig {
260        auth_key,
261        client_id: config.client_id.clone(),
262        client_secret: config.client_secret.clone(),
263        id_token: config.id_token.clone(),
264        audience: config.audience.clone(),
265        tags: config.requested_tags.clone(),
266    };
267    ts_control::resolve_auth_key(&wif, &config.control_server_url)
268        .await
269        .map_err(|e| {
270            tracing::error!(error = %e, "resolving auth key via workload-identity federation");
271            Error::Internal(InternalErrorKind::BadRequest)
272        })
273}
274
275/// Pass-through when the `identity-federation` feature is disabled: the auth key is used as-is and
276/// the WIF config fields have no effect (matching Go, where the federation path is compiled out
277/// unless its optional feature is linked).
278#[cfg(not(feature = "identity-federation"))]
279async fn resolve_auth_key(
280    _config: &Config,
281    auth_key: Option<String>,
282) -> Result<Option<String>, Error> {
283    Ok(auth_key)
284}
285
286impl Device {
287    /// Create a device from the given [`Config`] and auth key.
288    ///
289    /// Internally, this will spawn multiple asynchronous actors onto a Tokio runtime.
290    ///
291    /// # Example
292    ///
293    /// ```rust,no_run
294    /// # #[tokio::main]
295    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
296    /// # use tailscale::*;
297    /// let dev = Device::new(
298    ///     &Config::default_with_key_file("tsrs_keys.json").await?,
299    ///     Some("MY_AUTH_KEY".to_string()),
300    /// ).await?;
301    /// # Ok(()) }
302    /// ```
303    pub async fn new(config: &Config, auth_key: Option<String>) -> Result<Self, Error> {
304        check_magic_env()?;
305
306        // Resolve the effective registration auth key. The explicit `auth_key` argument wins; if it
307        // is `None`, fall back to `config.auth_key` (Go `tsnet.Server.AuthKey`). When the
308        // `identity-federation` feature is enabled, the resolved key is further passed through the
309        // WIF / OAuth-client bootstrap, which exchanges an OAuth client secret (`tskey-client-…`) or
310        // an IdP-issued OIDC token for a Tailscale auth key before registration (SaaS-only).
311        let auth_key = auth_key.or_else(|| config.auth_key.clone());
312        let auth_key = resolve_auth_key(config, auth_key).await?;
313
314        let rt =
315            ts_runtime::Runtime::spawn(config.into(), auth_key, (&config.key_state).into()).await?;
316        // In TUN transport mode there is no application netstack, so the runtime has no command
317        // channel: that surfaces as `UnsupportedInTunMode`, which we map to a `None` channel rather
318        // than an error (the device is still usable for control-plane and peer-lookup APIs).
319        let channel = match rt.channel().await {
320            Ok(c) => Some(c),
321            Err(e) if e.kind == ts_runtime::ErrorKind::UnsupportedInTunMode => None,
322            Err(e) => return Err(e.into()),
323        };
324
325        Ok(Self {
326            runtime: rt,
327            channel,
328            enable_ipv6: config.enable_ipv6,
329            serve: std::sync::Mutex::new(None),
330            funnel: std::sync::Mutex::new(None),
331        })
332    }
333
334    /// The application netstack command channel, or an error in TUN transport mode (no application
335    /// netstack exists).
336    fn channel(&self) -> Result<&Channel, Error> {
337        self.channel
338            .as_ref()
339            .ok_or(Error::Internal(InternalErrorKind::UnsupportedInTunMode))
340    }
341
342    /// Get this [`Device`]'s IPv4 tailnet address.
343    pub async fn ipv4_addr(&self) -> Result<Ipv4Addr, Error> {
344        self.runtime
345            .control
346            .ask(ts_runtime::control_runner::Ipv4)
347            .await
348            .map_err(ts_runtime::Error::from)?
349            .ok_or(Error::Internal(InternalErrorKind::Actor))
350    }
351
352    /// Get this [`Device`]'s IPv6 tailnet address.
353    pub async fn ipv6_addr(&self) -> Result<Ipv6Addr, Error> {
354        self.runtime
355            .control
356            .ask(ts_runtime::control_runner::Ipv6)
357            .await
358            .map_err(ts_runtime::Error::from)?
359            .ok_or(Error::Internal(InternalErrorKind::Actor))
360    }
361
362    /// Bind a UDP socket to the specified [`SocketAddr`].
363    ///
364    /// Returns an error in TUN transport mode (there is no application netstack to bind on).
365    pub async fn udp_bind(&self, socket_addr: SocketAddr) -> Result<netstack::UdpSocket, Error> {
366        self.channel()?
367            .udp_bind(socket_addr)
368            .await
369            .map_err(Into::into)
370    }
371
372    /// Bind a TCP listener to the specified [`SocketAddr`].
373    ///
374    /// Returns an error in TUN transport mode (there is no application netstack to listen on).
375    pub async fn tcp_listen(
376        &self,
377        socket_addr: SocketAddr,
378    ) -> Result<netstack::TcpListener, Error> {
379        self.channel()?
380            .tcp_listen(socket_addr)
381            .await
382            .map_err(Into::into)
383    }
384
385    /// Register a fallback TCP handler (like `tsnet`'s `RegisterFallbackTCPHandler`).
386    ///
387    /// The callback is consulted for every inbound TCP flow that matches **no** explicit
388    /// [`Device::tcp_listen`] listener, with the flow's `(src, dst)` addresses. It returns
389    /// `(handler, intercept)`:
390    /// - `(_, false)` — decline; the next registered callback is tried.
391    /// - `(Some(h), true)` — claim the flow; `h` is handed the accepted [`netstack::TcpStream`].
392    /// - `(None, true)` — claim and reject the flow (the connection is closed).
393    ///
394    /// Multiple handlers may be registered; they are consulted in registration order and the first
395    /// to intercept wins. The returned [`FallbackTcpHandle`] deregisters the handler when dropped.
396    ///
397    /// Handlers serve flows over the overlay netstack only — never a host socket — and a flow no
398    /// handler claims is closed (fail-closed), never direct-dialed.
399    ///
400    /// Returns an error in TUN transport mode (there is no application netstack to attach to).
401    pub fn register_fallback_tcp_handler<F>(&self, cb: F) -> Result<FallbackTcpHandle, Error>
402    where
403        F: Fn(SocketAddr, SocketAddr) -> FallbackDecision + Send + Sync + 'static,
404    {
405        self.runtime
406            .register_fallback_tcp_handler(std::sync::Arc::new(cb))
407            .map_err(Into::into)
408    }
409
410    /// Resolve a tailnet peer (or this node) by MagicDNS name to its tailnet IPv4 address.
411    ///
412    /// This is an in-process lookup against the netmap we already hold — like `tsnet`'s in-memory
413    /// `dnsMap`, it does not query any DNS server (there is no `100.100.100.100` resolver). The
414    /// `name` may be a bare hostname or a fully-qualified MagicDNS name, with or without a trailing
415    /// dot, in any case (matching is case-insensitive). Returns `Ok(None)` if no tailnet node has
416    /// that name.
417    ///
418    /// Only MagicDNS names are resolved; names outside the tailnet are not looked up here, so the
419    /// caller's system resolver remains responsible for them. IPv6 is intentionally not resolved —
420    /// this fork operates IPv4-only on the tailnet.
421    pub async fn resolve(&self, name: &str) -> Result<Option<Ipv4Addr>, Error> {
422        if let Some(peer) = self.peer_by_name(name).await? {
423            return Ok(Some(peer.tailnet_address.ipv4.addr()));
424        }
425
426        // tsnet's dnsMap also resolves our own name; fall back to self when no peer matches.
427        let me = self.self_node().await?;
428        if me.matches_name(name) {
429            return Ok(Some(me.tailnet_address.ipv4.addr()));
430        }
431
432        Ok(None)
433    }
434
435    /// Connect to a tailnet peer by MagicDNS name and port over TCP.
436    ///
437    /// Resolves `name` via [`Device::resolve`] (an in-process netmap lookup, no DNS server), then
438    /// dials the resulting tailnet IPv4 address. Returns [`InternalErrorKind::BadRequest`] if the
439    /// name does not resolve to a tailnet node.
440    pub async fn connect_by_name(
441        &self,
442        name: &str,
443        port: u16,
444    ) -> Result<netstack::TcpStream, Error> {
445        let addr = self
446            .resolve(name)
447            .await?
448            .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
449
450        self.tcp_connect((addr, port).into()).await
451    }
452
453    /// Connect to a TCP socket at the remote address.
454    ///
455    /// Returns an error in TUN transport mode (there is no application netstack to dial from).
456    pub async fn tcp_connect(&self, remote: SocketAddr) -> Result<netstack::TcpStream, Error> {
457        let channel = self.channel()?;
458
459        let ip: IpAddr = match remote.is_ipv4() {
460            true => self.ipv4_addr().await?.into(),
461            false => self.ipv6_addr().await?.into(),
462        };
463
464        // TODO(npry): collision checking
465        let ephemeral_port = rand::random_range(49152..=u16::MAX);
466
467        channel
468            .tcp_connect((ip, ephemeral_port).into(), remote)
469            .await
470            .map_err(Into::into)
471    }
472
473    /// Start a SOCKS5 proxy on a host loopback address that dials into the tailnet (Go
474    /// `tsnet.Server.Loopback`, SOCKS5 half).
475    ///
476    /// Binds a TCP listener on `127.0.0.1:0` (host loopback only — never an external interface) and
477    /// serves SOCKS5 (RFC 1928) with required username/password auth (RFC 1929): username `tsnet`,
478    /// password = the returned `proxy_cred`. Each `CONNECT` is dialed INTO the overlay via
479    /// [`Device::connect_by_name`] / [`Device::tcp_connect`] and spliced to the accepted host socket, so
480    /// a non-Rust host process can reach tailnet peers through the proxy. Returns the bound address, the
481    /// proxy credential, and a [`LoopbackHandle`] whose drop stops the listener.
482    ///
483    /// Anti-leak: the listener is loopback-only and every connection egresses over the overlay, never a
484    /// host socket — the host's real origin IP is never used to reach the destination. Unlike Go, the
485    /// LocalAPI HTTP surface is not served (this fork exposes status/whois/id-token natively on
486    /// `Device`); only the SOCKS5 proxy is provided.
487    ///
488    /// Returns an error in TUN transport mode (no application netstack to dial from).
489    pub async fn loopback(&self) -> Result<(std::net::SocketAddr, String, LoopbackHandle), Error> {
490        // Capture only cloneable pieces — never `&self` — for the spawned accept loop: a clone of the
491        // netstack command channel, this device's own overlay IPv4 (fetched once), and a boxed
492        // resolver closure over clones of the control + peer-tracker actor refs. The resolver
493        // replicates `Device::resolve` (peer-by-name, falling back to this node's own name).
494        let channel = self.channel()?.clone();
495        let self_ipv4 = self.ipv4_addr().await?;
496
497        let control = self.runtime.control.clone();
498        let peer_tracker = self.runtime.peer_tracker.clone();
499        let resolve: loopback::Resolver = std::sync::Arc::new(move |name: String| {
500            let control = control.clone();
501            let peer_tracker = peer_tracker.clone();
502            Box::pin(async move {
503                let pt = peer_tracker
504                    .upgrade()
505                    .ok_or(Error::Internal(InternalErrorKind::Actor))?;
506                let peer = pt
507                    .ask(ts_runtime::peer_tracker::PeerByName { name: name.clone() })
508                    .await
509                    .map_err(ts_runtime::Error::from)?;
510                if let Some(peer) = peer {
511                    return Ok(Some(peer.tailnet_address.ipv4.addr()));
512                }
513                // tsnet's dnsMap also resolves our own name; fall back to self.
514                let me = control
515                    .ask(ts_runtime::control_runner::SelfNode)
516                    .await
517                    .map_err(ts_runtime::Error::from)?
518                    .ok_or(Error::Internal(InternalErrorKind::Actor))?;
519                if me.matches_name(&name) {
520                    Ok(Some(me.tailnet_address.ipv4.addr()))
521                } else {
522                    Ok(None)
523                }
524            }) as std::pin::Pin<Box<dyn std::future::Future<Output = _> + Send>>
525        });
526
527        let dialer = loopback::OverlayDialer::new(channel, self_ipv4, resolve);
528        loopback::start(dialer).await
529    }
530
531    /// Get our node info.
532    pub async fn self_node(&self) -> Result<NodeInfo, Error> {
533        self.runtime
534            .control
535            .ask(ts_runtime::control_runner::SelfNode)
536            .await
537            .map_err(ts_runtime::Error::from)?
538            .ok_or(Error::Internal(InternalErrorKind::Actor))
539    }
540
541    /// This node's key-expiry instant as Unix seconds (`Node.KeyExpiry` in Go), or `Ok(None)` if
542    /// the key never expires.
543    ///
544    /// Like Go, this fork is **reactive** about key expiry — it reports it rather than rotating the
545    /// node key in the background. A caller can schedule re-authentication around this time; on
546    /// expiry, re-create the [`Device`] (which re-registers), supplying a fresh node key + the prior
547    /// `old_node_key` to rotate, or the same key to refresh.
548    pub async fn self_key_expiry_unix(&self) -> Result<Option<i64>, Error> {
549        Ok(self.self_node().await?.key_expiry_unix())
550    }
551
552    /// Whether this node's key has expired as of now (`!KeyExpiry.IsZero() && KeyExpiry.Before(now)`
553    /// in Go). A key with no expiry is never expired. See [`Device::self_key_expiry_unix`] for the
554    /// reactive-rotation note.
555    pub async fn self_key_expired(&self) -> Result<bool, Error> {
556        let now = std::time::SystemTime::now()
557            .duration_since(std::time::UNIX_EPOCH)
558            .map(|d| d.as_secs() as i64)
559            // An unreadable clock (pre-epoch) is treated as the far future so a time-limited key
560            // looks expired — fail-safe toward prompting re-auth rather than trusting a stale key.
561            .unwrap_or(i64::MAX);
562        Ok(self.self_node().await?.key_expired_at_unix(now))
563    }
564
565    /// Fetch the current Tailscale SSH policy pushed by control, if any.
566    ///
567    /// Returns `Ok(None)` when control has not sent an SSH policy. The SSH server treats an absent
568    /// or empty policy as **deny-all** (fail-closed). Used by the SSH auth path
569    /// ([`SshPolicy::evaluate`][ts_control::SshPolicy::evaluate]) to authorize incoming
570    /// connections.
571    pub async fn ssh_policy(&self) -> Result<Option<ts_control::SshPolicy>, Error> {
572        self.runtime
573            .control
574            .ask(ts_runtime::control_runner::CurrentSshPolicy)
575            .await
576            .map_err(ts_runtime::Error::from)
577            .map_err(Into::into)
578    }
579
580    /// Look up a peer by name.
581    pub async fn peer_by_name(&self, name: &str) -> Result<Option<NodeInfo>, Error> {
582        let pt = self
583            .runtime
584            .peer_tracker
585            .upgrade()
586            .ok_or(Error::Internal(InternalErrorKind::Actor))?;
587
588        pt.ask(ts_runtime::peer_tracker::PeerByName {
589            name: name.to_string(),
590        })
591        .await
592        .map_err(ts_runtime::Error::from)
593        .map_err(Into::into)
594    }
595
596    /// Look up a peer by ip.
597    pub async fn peer_by_tailnet_ip(&self, ip: IpAddr) -> Result<Option<NodeInfo>, Error> {
598        let pt = self
599            .runtime
600            .peer_tracker
601            .upgrade()
602            .ok_or(Error::Internal(InternalErrorKind::Actor))?;
603
604        pt.ask(ts_runtime::peer_tracker::PeerByTailnetIp { ip })
605            .await
606            .map_err(ts_runtime::Error::from)
607            .map_err(Into::into)
608    }
609
610    /// Look up the peer(s) with the most-specific route matches for `ip`.
611    ///
612    /// This reports which peers *advertise* a route covering `ip`, independent of this device's
613    /// `accept_routes` setting — analogous to the Go client's informational `PrimaryRoutes`. It is
614    /// not a reachability oracle: with `accept_routes` off, the dataplane will not actually route
615    /// to (or accept return traffic from) advertised subnet routes even if this returns a peer.
616    pub async fn peers_with_route(&self, ip: IpAddr) -> Result<Vec<NodeInfo>, Error> {
617        let pt = self
618            .runtime
619            .peer_tracker
620            .upgrade()
621            .ok_or(Error::Internal(InternalErrorKind::Actor))?;
622
623        pt.ask(ts_runtime::peer_tracker::PeerByAcceptedRoute { ip })
624            .await
625            .map_err(ts_runtime::Error::from)
626            .map_err(Into::into)
627    }
628
629    /// List the Taildrop files this device has fully received and not yet consumed (Go LocalAPI
630    /// `WaitingFiles`).
631    ///
632    /// Returns the files waiting under the configured `taildrop_dir`, sorted by name. Returns an
633    /// empty list when Taildrop is disabled (`Config::taildrop_dir` unset) — fail-closed, never an
634    /// error for the disabled case. A filesystem error while listing surfaces as
635    /// [`InternalErrorKind::Actor`].
636    pub fn taildrop_waiting_files(&self) -> Result<Vec<WaitingFile>, Error> {
637        let Some(store) = self.runtime.taildrop_store() else {
638            return Ok(Vec::new());
639        };
640        store
641            .waiting_files()
642            .map_err(|_| Error::Internal(InternalErrorKind::Actor))
643    }
644
645    /// Open a received Taildrop file by name for reading, returning the handle and its size (Go
646    /// LocalAPI `OpenFile`).
647    ///
648    /// The `name` is validated (path-traversal-safe) inside the store before any path is built.
649    /// Returns [`InternalErrorKind::BadRequest`] when Taildrop is disabled or the name is invalid,
650    /// and [`InternalErrorKind::Actor`] for a filesystem error (e.g. the file does not exist).
651    pub fn taildrop_open_file(&self, name: &str) -> Result<(std::fs::File, u64), Error> {
652        let store = self
653            .runtime
654            .taildrop_store()
655            .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
656        store.open_file(name).map_err(taildrop_err)
657    }
658
659    /// Delete a received Taildrop file by name (Go LocalAPI `DeleteFile`).
660    ///
661    /// The `name` is validated (path-traversal-safe) inside the store before any path is built.
662    /// Returns [`InternalErrorKind::BadRequest`] when Taildrop is disabled or the name is invalid,
663    /// and [`InternalErrorKind::Actor`] for a filesystem error (e.g. the file does not exist).
664    pub fn taildrop_delete_file(&self, name: &str) -> Result<(), Error> {
665        let store = self
666            .runtime
667            .taildrop_store()
668            .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
669        store.delete_file(name).map_err(taildrop_err)
670    }
671
672    /// Send a local file to a tailnet `peer` via Taildrop (Go `PushFile` / `tailscale file cp`).
673    ///
674    /// Pushes `content_length` bytes from `reader` to the peer's peerAPI as
675    /// `PUT /v0/put/<name>` over the overlay netstack — the sending counterpart to the receive store
676    /// surfaced by [`Device::taildrop_waiting_files`]. The transfer rides the encrypted WireGuard
677    /// overlay, never a host socket. The body is streamed from offset 0 (no resume).
678    ///
679    /// The destination is derived **solely from `peer`'s own node record**
680    /// ([`NodeInfo::peerapi_addr`][ts_control::Node::peerapi_addr]): its advertised tailnet IPv4 and
681    /// `peerapi4` port. The caller obtains `peer` from [`Device::peer_by_name`] /
682    /// [`Device::peer_by_tailnet_ip`], so it is always a current netmap peer — a raw control-supplied
683    /// or attacker-chosen address can never be targeted. As defense in depth, the resolved address is
684    /// additionally asserted to be a Tailscale CGNAT IP before dialing.
685    ///
686    /// Returns [`InternalErrorKind::BadRequest`] when the peer advertises no IPv4 peerAPI (so it
687    /// cannot receive files), when the name is invalid, or when the peer refuses the transfer
688    /// (`403`/`409`/unexpected status); [`Error::Timeout`] on a dial failure or timeout; and
689    /// [`InternalErrorKind::Io`] on a mid-transfer stream error.
690    pub async fn send_file<R>(
691        &self,
692        peer: &NodeInfo,
693        name: &str,
694        content_length: u64,
695        reader: R,
696    ) -> Result<(), Error>
697    where
698        R: tokio::io::AsyncRead + Unpin,
699    {
700        let channel = self.channel()?;
701
702        // Destination comes only from the peer's own node record — never an arbitrary address.
703        let dst = peer
704            .peerapi_addr()
705            .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
706        // Defense in depth: refuse to dial anything outside the Tailscale CGNAT range, so a
707        // malformed node record can't steer the PUT at a non-tailnet host.
708        if !ts_control::is_tailscale_ip(dst.ip()) {
709            return Err(Error::Internal(InternalErrorKind::BadRequest));
710        }
711
712        let self_ipv4 = self.ipv4_addr().await?;
713
714        ts_runtime::taildrop_send::send_file(channel, self_ipv4, dst, name, content_length, reader)
715            .await
716            .map_err(taildrop_send_err)
717    }
718
719    /// Begin a debug packet capture, streaming a pcap of every packet crossing the dataplane to
720    /// `writer` (Go `tsnet.Server.CapturePcap`).
721    ///
722    /// Installs a capture hook on the running dataplane: from now until [`Device::stop_capture`] is
723    /// called (or another capture replaces this one), a copy of every plaintext IP packet on the
724    /// datapath — outbound (pre-encrypt) and inbound (post-decrypt) — is framed and written to
725    /// `writer`. The 24-byte pcap global header is written immediately on success.
726    ///
727    /// The format is byte-faithful classic pcap with Tailscale's `LINKTYPE_USER0` + 4-byte path
728    /// preamble per record (see [`ts_runtime::capture`]); a resulting file opens in Wireshark, and
729    /// with Tailscale's `ts-dissector.lua` the direction/path of each packet decodes.
730    ///
731    /// The hook runs **inline on the single-threaded dataplane step**, so `writer` must not block for
732    /// long — a slow writer back-pressures the datapath. Records are **not** flushed per packet (that
733    /// would be a syscall on every packet on the dataplane thread); buffered bytes are flushed when
734    /// the writer is dropped on [`Device::stop_capture`]. Wrap `writer` in a [`std::io::BufWriter`] if
735    /// you want buffering. A write error is swallowed per-packet (the capture silently drops that
736    /// record) rather than tearing down the datapath; call [`Device::stop_capture`] to end it. Returns
737    /// an error only if the dataplane actor is unreachable or the initial global-header write fails.
738    pub async fn capture_pcap<W>(&self, writer: W) -> Result<(), Error>
739    where
740        W: std::io::Write + Send + 'static,
741    {
742        let sink = std::sync::Arc::new(std::sync::Mutex::new(
743            ts_runtime::capture::PcapSink::new(writer)
744                .map_err(|_| Error::Internal(InternalErrorKind::Io))?,
745        ));
746        let hook: ts_runtime::CaptureHook = std::sync::Arc::new(move |path, pkt: &[u8]| {
747            if let Ok(mut sink) = sink.lock() {
748                // A per-packet write failure (e.g. a closed pipe) silently drops that record rather
749                // than tearing down the datapath; the caller ends capture via `stop_capture`.
750                drop(sink.log_packet(path.code(), pkt));
751            }
752        });
753        self.runtime.install_capture(Some(hook)).await?;
754        Ok(())
755    }
756
757    /// Stop a debug packet capture started by [`Device::capture_pcap`] (Go `ClearCaptureSink`).
758    ///
759    /// Clears the dataplane capture hook; the writer is dropped (its remaining buffered bytes are
760    /// flushed by its own `Drop`). Idempotent — clearing when no capture is installed is a no-op.
761    /// Returns an error only if the dataplane actor is unreachable.
762    pub async fn stop_capture(&self) -> Result<(), Error> {
763        self.runtime.install_capture(None).await?;
764        Ok(())
765    }
766
767    /// Snapshot of this device and its tailnet peers (like `tailscale status`).
768    ///
769    /// Combines this node's self info with the current peer set: each [`StatusNode`] reports the
770    /// stable id, display name, tailnet IPs, advertised routes, and exit-node flag. (Per-peer
771    /// `online`/user/capabilities are honestly `None`/empty in this fork — the domain node model
772    /// does not yet carry the wire-level liveness/login fields; see `ts_runtime::status` docs.)
773    pub async fn status(&self) -> Result<Status, Error> {
774        self.runtime.status().await.map_err(Into::into)
775    }
776
777    /// Fetch the current Tailnet Lock (TKA) status pushed by control, if any.
778    ///
779    /// Returns `Ok(None)` when control has sent no `TKAInfo` (tailnet lock not in use, or no change
780    /// observed yet). The returned [`TkaStatus`][ts_control::TkaStatus] carries the authority head
781    /// (a base32 `AUMHash`, decode with [`tka::AumHash::from_base32`][ts_tka::AumHash::from_base32])
782    /// and the disablement signal. Signature verification of a peer's node-key signature against the
783    /// authority is performed with the [`tka`] module's [`tka::Authority`][ts_tka::Authority].
784    pub async fn tka_status(&self) -> Result<Option<ts_control::TkaStatus>, Error> {
785        self.runtime
786            .control
787            .ask(ts_runtime::control_runner::CurrentTkaStatus)
788            .await
789            .map_err(ts_runtime::Error::from)
790            .map_err(Into::into)
791    }
792
793    /// Request an OIDC **ID token** from control for this node, scoped to `audience` (workload-
794    /// identity federation, like `tailscale`'s `id-token` LocalAPI).
795    ///
796    /// Returns a signed JWT whose `sub` claim is this node's MagicDNS name and whose `aud` claim is
797    /// `audience`, suitable for presenting to a third-party relying party (e.g. AWS/GCP
798    /// workload-identity federation). The node is the token *subject*, not the authenticator — this
799    /// is token issuance over the Noise transport (`POST /machine/id-token`), not a login path.
800    /// Requires the control plane to support capability version ≥ 30.
801    pub async fn fetch_id_token(&self, audience: &str) -> Result<String, ts_control::IdTokenError> {
802        self.runtime.fetch_id_token(audience.to_string()).await
803    }
804
805    /// Log this node out of the tailnet — deregister it from the control plane (the equivalent of
806    /// Go `tsnet`'s `LocalClient.Logout`).
807    ///
808    /// Re-`POST`s `/machine/register` with this node's current node key and a past expiry, which the
809    /// control plane honors by **expiring the node now**: it drops out of every peer's netmap and
810    /// must re-register (re-authenticate) to rejoin.
811    ///
812    /// This is primarily for **non-ephemeral** nodes. An ephemeral node is garbage-collected by
813    /// control shortly after it disconnects, but a persistent node lingers in the tailnet
814    /// (visible to peers, counting against the machine limit) for up to ~24h after the process exits
815    /// unless explicitly logged out. Call this before [`shutdown`](Self::shutdown) to deregister
816    /// immediately. Calling it on an ephemeral node simply brings the GC forward; it is idempotent,
817    /// so logging out an already-gone node is not an error.
818    ///
819    /// This is a **control-plane state change only**: it does not tear down the local datapath (do
820    /// that via [`shutdown`](Self::shutdown)), and it does not delete or rotate the on-disk node key
821    /// — re-registering with the same key (a fresh [`Device::new`]) is the re-login path.
822    pub async fn logout(&self) -> Result<(), ts_control::LogoutError> {
823        self.runtime.logout().await
824    }
825
826    /// Snapshot this node's client metrics in Prometheus text exposition format.
827    ///
828    /// Mirrors Go Tailscale's `clientmetric` registry: process-global counters/gauges incremented
829    /// on the datapath hot loops (e.g. `magicsock_send_udp`, `magicsock_recv_data_bytes_udp`),
830    /// rendered as `# TYPE <name> <kind>\n<name> <value>\n` per metric, sorted by name. (Go `tsnet`
831    /// exposes no metrics method of its own, so this is the fork's clean public surface.) The
832    /// registry is process-global, so the output covers every `Device` in the process.
833    pub fn metrics(&self) -> String {
834        ts_metrics::write_prometheus()
835    }
836
837    /// Map a tailnet source `addr` to the node that owns its IP (like `tsnet`'s `WhoIs`).
838    ///
839    /// Only the IP of `addr` is used; the port is ignored. Returns `Ok(None)` if no tailnet node
840    /// owns that address.
841    pub async fn whois(&self, addr: SocketAddr) -> Result<Option<WhoIs>, Error> {
842        self.runtime.whois(addr).await.map_err(Into::into)
843    }
844
845    /// Change the selected exit node at runtime, without recreating the [`Device`] — the equivalent
846    /// of Go `tsnet`'s `LocalClient.EditPrefs(ExitNodeID/ExitNodeIP)`.
847    ///
848    /// The peer may be named by stable node ID, tailnet IP, or MagicDNS name via
849    /// [`ExitNodeSelector`] (a bare IP or name parses with `selector.parse()`); this is the same
850    /// selector type as [`Config::exit_node`](crate::Config::exit_node), so the construction-time
851    /// and runtime paths are identical. Passing `None` clears the exit node — internet-bound traffic
852    /// is then dropped (fail-closed) unless this node egresses directly.
853    ///
854    /// The change is applied immediately: the new selector is re-resolved against the live peer set
855    /// and the outbound route + inbound source filter are recomputed at once. A selector for a peer
856    /// not yet in the netmap simply takes effect once that peer appears.
857    ///
858    /// Only NEW flows use the changed exit; in-flight connections are not torn down and continue
859    /// egressing via the previously-selected exit until they close.
860    pub async fn set_exit_node(&self, exit_node: Option<ExitNodeSelector>) -> Result<(), Error> {
861        self.runtime
862            .set_exit_node(exit_node)
863            .await
864            .map_err(Into::into)
865    }
866
867    /// The currently-selected exit node, or `None` if none is selected.
868    pub fn exit_node(&self) -> Option<ExitNodeSelector> {
869        self.runtime.exit_node()
870    }
871
872    /// The stable id of the exit node traffic is **currently** egressing through, or `None` if none
873    /// is engaged (the equivalent of Go `tsnet`'s `Status.ExitNodeStatus.ID`).
874    ///
875    /// This differs from [`exit_node`](Self::exit_node), which returns the *configured* selector:
876    /// the active exit node is the route updater's resolved, fail-closed answer. It is `None` when
877    /// no exit node is configured, the configured selector matches no current peer, or the matched
878    /// peer no longer advertises a default route (egress is then dropped, fail-closed). Match the id
879    /// against [`Status::peers`](crate::Status::peers) (via [`status`](Self::status)) for details.
880    pub fn active_exit_node(&self) -> Option<ts_control::StableNodeId> {
881        self.runtime.active_exit_node()
882    }
883
884    /// Watch for netmap changes: the returned receiver's value is the current set of peer
885    /// [`StatusNode`]s and updates on every netmap change (like subscribing to `ipn` notifications).
886    pub async fn watch_netmap(
887        &self,
888    ) -> Result<tokio::sync::watch::Receiver<Vec<StatusNode>>, Error> {
889        self.runtime.watch_netmap().await.map_err(Into::into)
890    }
891
892    /// The current device connection-[`DeviceState`] (`Connecting` / `Running` / `NeedsLogin` /
893    /// `Expired` / `Failed`).
894    pub fn device_state(&self) -> DeviceState {
895        self.runtime.device_state()
896    }
897
898    /// Watch the device connection-[`DeviceState`], reacting push-style to control connection
899    /// transitions instead of polling [`status`](Self::status).
900    ///
901    /// Returns a [`tokio::sync::watch::Receiver`]; await its
902    /// [`changed`](tokio::sync::watch::Receiver::changed) to be woken on each transition. The
903    /// initial value is the current state.
904    pub fn watch_state(&self) -> tokio::sync::watch::Receiver<DeviceState> {
905        self.runtime.watch_state()
906    }
907
908    /// Wait until the device finishes registering, returning a typed outcome — the clean
909    /// replacement for polling [`ipv4_addr`](Self::ipv4_addr) in a loop.
910    ///
911    /// Resolves `Ok(())` once the device is [`DeviceState::Running`]. On a non-running outcome it
912    /// returns a typed [`RegistrationError`]:
913    /// - [`AuthRejected`](RegistrationError::AuthRejected) — bad/expired/unknown auth key;
914    ///   **permanent** (re-pair).
915    /// - [`NeedsLogin`](RegistrationError::NeedsLogin) — interactive authorization required;
916    ///   **not permanent** (the runtime keeps retrying and reaches `Running` once the user
917    ///   authorizes). Auth-key callers treat this as failure; interactive callers should ignore it
918    ///   and drive the flow via [`watch_state`](Self::watch_state).
919    /// - [`NetworkUnreachable`](RegistrationError::NetworkUnreachable) — **transient** (retry).
920    /// - [`Timeout`](RegistrationError::Timeout) — no settled state within `timeout` (`None` waits
921    ///   indefinitely).
922    ///
923    /// [`KeyExpired`](RegistrationError::KeyExpired) is not produced here (a key expires only after
924    /// the node is up); observe it via [`watch_state`](Self::watch_state). Use
925    /// [`RegistrationError::is_permanent`] to branch "re-pair" vs. "retry / drive login".
926    pub async fn wait_until_running(
927        &self,
928        timeout: Option<Duration>,
929    ) -> Result<(), RegistrationError> {
930        self.runtime.wait_until_running(timeout).await
931    }
932
933    /// Ping a tailnet peer over the overlay with an ICMPv4 echo, returning the round-trip time
934    /// (like `tailscale ping`).
935    ///
936    /// The echo is sent from this device's own tailnet IPv4 over the overlay netstack — never a
937    /// host socket. IPv6 destinations return [`PingError::Ipv6Unsupported`] (this fork is
938    /// IPv4-only on the tailnet). A peer answers from its own OS stack; this netstack does not
939    /// auto-reply to echo requests.
940    ///
941    /// In TUN transport mode there is no application netstack to ping from; this surfaces as
942    /// [`PingError::Timeout`] (the same error this method already uses for an unavailable source
943    /// address — `PingError` carries no dedicated "unsupported" variant).
944    pub async fn ping(&self, dst: IpAddr, timeout: Duration) -> Result<Duration, PingError> {
945        let channel = self.channel().map_err(|_| PingError::Timeout)?;
946        let src = self.ipv4_addr().await.map_err(|_| PingError::Timeout)?;
947        ts_netstack_smoltcp::ping(channel, src, dst, timeout).await
948    }
949
950    /// Obtain a TLS certificate for a node's MagicDNS `name` (like `tsnet`'s `GetCertificate`).
951    ///
952    /// **Fail-closed without the `acme` feature.** By default this fork has no client-side ACME
953    /// engine wired in, so this returns [`ts_control::CertError::Unimplemented`] (after a
954    /// tailnet-name check) — it NEVER self-signs and NEVER returns a placeholder certificate
955    /// ([`ts_control::MISSING_CERT_RPC`] names what is missing).
956    ///
957    /// **With the `acme` feature** this instead drives the client-side ACME DNS-01 engine to issue a
958    /// real Let's Encrypt certificate for `name`, publishing the challenge TXT via the node's
959    /// `POST /machine/set-dns` RPC (routed through the control runner). SaaS-only: a self-hosted
960    /// control plane may 501 on set-dns, surfaced as [`ts_control::CertError::Acme`].
961    #[cfg(not(feature = "acme"))]
962    pub async fn get_certificate(&self, name: &str) -> Result<CertifiedKey, ts_control::CertError> {
963        ts_control::get_certificate(name).await
964    }
965
966    /// See the no-`acme` variant for the contract; with `acme` this issues a real cert via the
967    /// runtime's ACME engine (`Device → Runtime → ControlRunner → issue_certificate_via_setdns`).
968    #[cfg(feature = "acme")]
969    pub async fn get_certificate(&self, name: &str) -> Result<CertifiedKey, ts_control::CertError> {
970        self.runtime.get_certificate(name.to_string()).await
971    }
972
973    /// Build a [`TlsAcceptor`] terminating TLS for `cfg.name` on the overlay (like `tsnet`'s
974    /// `ListenTLS`).
975    ///
976    /// Obtains the certificate via [`Device::get_certificate`] — so with the `acme` feature this
977    /// issues a real Let's Encrypt cert (when the control plane answers `set-dns`), and without it
978    /// (or when issuance is unavailable) it surfaces the same fail-closed
979    /// [`ts_control::CertError`] rather than ever serving a self-signed cert or downgrading to
980    /// plaintext. Terminate accepted overlay streams with [`ts_control::accept_tls`].
981    pub async fn listen_tls(
982        &self,
983        cfg: &ts_control::ServeConfig,
984    ) -> Result<TlsAcceptor, ts_control::CertError> {
985        // Route through Device::get_certificate (the acme-aware issuance path) rather than
986        // ts_control::listen_tls, which only knows the non-acme stub. Validate the serve config
987        // first (same fail-closed checks ts_control::listen_tls applies), then assemble the acceptor.
988        cfg.validate()?;
989        let cert = self.get_certificate(&cfg.name).await?;
990        ts_control::tls_acceptor(cert)
991    }
992
993    /// The currently-stored Serve config (like `tsnet`'s `GetServeConfig`).
994    ///
995    /// Returns the config last passed to [`Device::set_serve_config`], or an empty
996    /// [`ts_control::ServeState`] (no ports) if none was ever set. Pure read — does not touch the
997    /// network.
998    pub fn get_serve_config(&self) -> ts_control::ServeState {
999        match &*self.serve.lock().unwrap_or_else(|e| e.into_inner()) {
1000            Some(mgr) => mgr.get(),
1001            None => ts_control::ServeState::default(),
1002        }
1003    }
1004
1005    /// Replace this node's Serve config and (re)bind its tailnet ports (like `tsnet`'s
1006    /// `SetServeConfig`, REPLACE semantics).
1007    ///
1008    /// `state` becomes the **whole** config (full-replace reconcile: every previously-bound serve
1009    /// port's accept loop is torn down and the new config's ports are bound from scratch). For each
1010    /// configured port the manager binds an overlay listener on this node's tailnet IPv4 and
1011    /// dispatches per [`ts_control::ServeTarget`]:
1012    /// - [`Accept`](ts_control::ServeTarget::Accept) — the TLS-terminated stream is handed back over
1013    ///   the returned [`ServeAcceptedReceiver`](ts_runtime::serve::ServeAcceptedReceiver) (the
1014    ///   in-process stand-in for `ListenTLS`'s `net.Listener`).
1015    /// - [`Proxy`](ts_control::ServeTarget::Proxy) — reverse-proxy the decrypted stream to a local
1016    ///   host backend.
1017    /// - [`Text`](ts_control::ServeTarget::Text) — write a fixed body and close.
1018    /// - [`TcpForward`](ts_control::ServeTarget::TcpForward) — forward the **raw** (non-TLS) stream
1019    ///   to a local host backend.
1020    ///
1021    /// **Fail-closed.** `state.validate()` runs first. Every TLS-terminating port's acceptor is
1022    /// obtained up-front via [`Device::listen_tls`] (the ACME-aware cert path); if any cert cannot be
1023    /// issued the whole call fails with that [`ts_control::CertError`] and **nothing is bound** — a
1024    /// TLS port never downgrades to plaintext.
1025    ///
1026    /// **Anti-leak.** Listeners bind the overlay netstack only (never a host socket). The
1027    /// `Proxy`/`TcpForward` backend dial is a local host socket to the embedder's own backend (like
1028    /// Go's reverse-proxy to `127.0.0.1`), intentionally NOT routed through the exit-egress
1029    /// forwarder. A backend dial failure drops that connection; it never falls back.
1030    ///
1031    /// Returns an error in TUN transport mode (there is no application netstack to bind on). The
1032    /// previous config's accept loops (and any earlier `ServeAcceptedReceiver`) stop when this
1033    /// returns; the new receiver delivers every `Accept`-port connection.
1034    pub async fn set_serve_config(
1035        &self,
1036        state: ts_control::ServeState,
1037    ) -> Result<ts_runtime::serve::ServeAcceptedReceiver, Error> {
1038        state
1039            .validate()
1040            .map_err(|_| Error::Internal(InternalErrorKind::BadRequest))?;
1041
1042        // Fail-closed: build every TLS-terminating port's acceptor up-front via the ACME-aware cert
1043        // path. If any cert can't be issued, return before binding anything (no plaintext downgrade).
1044        let mut resolved = std::collections::BTreeMap::new();
1045        for (port, target) in &state.ports {
1046            let acceptor = if target.terminates_tls() {
1047                let cfg = ts_control::ServeConfig {
1048                    name: state.name.clone(),
1049                    port: *port,
1050                    target: target.clone(),
1051                };
1052                Some(self.listen_tls(&cfg).await.map_err(|_| {
1053                    // Cert issuance is fail-closed in this fork; surface as a request error rather
1054                    // than ever binding a plaintext TLS port.
1055                    Error::Internal(InternalErrorKind::BadRequest)
1056                })?)
1057            } else {
1058                None
1059            };
1060            resolved.insert(
1061                *port,
1062                ts_runtime::serve::ResolvedPort {
1063                    target: target.clone(),
1064                    acceptor,
1065                },
1066            );
1067        }
1068
1069        // The manager binds the OVERLAY netstack on this node's own tailnet IPv4.
1070        let self_ipv4 = self.ipv4_addr().await?;
1071        let channel = self.channel()?.clone();
1072
1073        let mut slot = self.serve.lock().unwrap_or_else(|e| e.into_inner());
1074        let mgr =
1075            slot.get_or_insert_with(|| ts_runtime::serve::ServeManager::new(channel, self_ipv4));
1076        Ok(mgr.set(state, resolved))
1077    }
1078
1079    /// Expose a tailnet TLS service to the public internet via Tailscale Funnel (like `tsnet`'s
1080    /// `ListenFunnel`), returning a [`FunnelAcceptedReceiver`](ts_runtime::funnel::FunnelAcceptedReceiver)
1081    /// that delivers each TLS-terminated public connection.
1082    ///
1083    /// **Two fail-closed gates, then the live ingress listener.** First the node-attribute gate is
1084    /// fully enforced from this node's own capability map (mirroring Go `ipn.NodeCanFunnel` +
1085    /// `ipn.CheckFunnelPort`): the tailnet admin must have enabled HTTPS and granted the `funnel`
1086    /// node attribute, and `cfg.port` must be in the set the `funnel-ports` capability allows —
1087    /// otherwise this returns [`ts_control::FunnelError::NotAllowed`] /
1088    /// [`ts_control::FunnelError::PortNotAllowed`] before touching any cert or network. Then the
1089    /// node's `*.ts.net` certificate is obtained via the ACME-aware [`Device::get_certificate`] (the
1090    /// Funnel hostname *is* the node's MagicDNS name, so its DNS-01 cert matches); fail-closed on
1091    /// [`ts_control::FunnelError::Cert`] — no self-signed or plaintext fallback.
1092    ///
1093    /// On success a [`FunnelManager`](ts_runtime::funnel::FunnelManager) is registered: its ingress
1094    /// sink is installed into the runtime's peerAPI `/v0/ingress` slot (making that route live without
1095    /// restarting the peerAPI server), and the `HostInfo.IngressEnabled` map-request signal is set so
1096    /// control routes Funnel traffic to this node. Public Funnel bytes arrive as a relay POST to
1097    /// `/v0/ingress`, are membership-gated + `101`-hijacked into a raw stream, TLS-terminated by the
1098    /// manager, and delivered over the returned receiver.
1099    ///
1100    /// **Where the relay comes from.** The public ingress **relay + DNS mapping** that feed
1101    /// `/v0/ingress` are Tailscale infrastructure ([`ts_control::MISSING_FUNNEL_RELAY`]), provisioned
1102    /// automatically against real Tailscale SaaS with a Funnel-enabled ACL; against a self-hosted
1103    /// control plane no relay exists, so the listener is correct but never fed.
1104    ///
1105    /// Anti-leak: Funnel TLS terminates only on the overlay netstack (the hijacked ingress stream
1106    /// arrives on the overlay peerAPI listener), never a host socket; there is no self-signed or
1107    /// plaintext fallback. A new `listen_funnel` replaces the previous manager (its pump + sink tear
1108    /// down); dropping the `Device` tears it down too.
1109    pub async fn listen_funnel(
1110        &self,
1111        cfg: &ts_control::ServeConfig,
1112        opts: ts_control::FunnelOptions,
1113    ) -> Result<ts_runtime::funnel::FunnelAcceptedReceiver, ts_control::FunnelError> {
1114        // Gate 1 (fail-closed, no network): node-attribute + funnel-port access from our cap map.
1115        let me = self
1116            .self_node()
1117            .await
1118            .map_err(|_| ts_control::FunnelError::NotAllowed)?;
1119        cfg.validate()?;
1120        ts_control::funnel_access(&me, cfg.port)?;
1121
1122        // Gate 2 (fail-closed): obtain the node's `*.ts.net` cert via the ACME-aware path and build
1123        // the TLS acceptor. A cert failure surfaces as FunnelError::Cert — never a plaintext listener.
1124        let cert = self
1125            .get_certificate(&cfg.name)
1126            .await
1127            .map_err(ts_control::FunnelError::Cert)?;
1128        let acceptor = ts_control::tls_acceptor(cert).map_err(ts_control::FunnelError::Cert)?;
1129
1130        // `opts.funnel_only` (reject tailnet-internal connections) is accepted for surface stability;
1131        // the ingress data path only ever carries relay-delivered public traffic, so there is no
1132        // tailnet-internal leg on this listener to reject. Documented as a no-op here for now.
1133        let _ = opts;
1134
1135        // Build the funnel manager + its ingress sink + the hand-back receiver, install the sink into
1136        // the runtime's shared peerAPI `/v0/ingress` slot (making the route live), and flip the
1137        // IngressEnabled map signal. Hold the manager on the device so its pump/sink live as long as
1138        // the listener; replacing a prior manager tears the old one down on drop at end of scope.
1139        let (manager, sink, receiver) = ts_runtime::funnel::FunnelManager::new(acceptor);
1140        {
1141            let slot = self.runtime.funnel_ingress_slot();
1142            *slot.lock().unwrap_or_else(|e| e.into_inner()) = Some(sink);
1143        }
1144        self.runtime
1145            .ingress_active_flag()
1146            .store(true, std::sync::atomic::Ordering::Relaxed);
1147
1148        let old = {
1149            let mut held = self.funnel.lock().unwrap_or_else(|e| e.into_inner());
1150            held.replace(manager)
1151        };
1152        drop(old);
1153
1154        Ok(receiver)
1155    }
1156
1157    /// Host a Tailscale **VIP service** (`svc:<label>`) by binding an overlay listener on the
1158    /// service's control-assigned virtual IP (like `tsnet`'s `ListenService`).
1159    ///
1160    /// **Fail-closed.** Mirrors Go `tsnet.Server.ListenService`'s preconditions, enforced from this
1161    /// node's own netmap state ([`ts_control::resolve_service_listen`]): the `name` must be a valid
1162    /// `svc:<dns-label>`, this node must be **tagged** (Go `ErrUntaggedServiceHost`), and control
1163    /// must have assigned the service a VIP address on this node (delivered via the `service-host`
1164    /// node-capability — see [`ts_control::Node::service_addresses`]). Any unmet precondition
1165    /// returns a typed [`ts_control::ServiceError`] before binding anything.
1166    ///
1167    /// When all hold, this binds a [`tcp_listen`][Device::tcp_listen] on the service VIP and the
1168    /// configured `mode` port over the **overlay netstack** (never a host socket) and returns the
1169    /// listener. The netstack already accepts packets for control-assigned VIPs (they are injected
1170    /// alongside the node's own tailnet address), so the listener is reachable by tailnet peers.
1171    ///
1172    /// The `Tun`/L3 service mode is unsupported (a TODO in upstream tsnet); only TCP/HTTP modes
1173    /// (which bind the same VIP:port at the listen layer) are offered. Returns an error in TUN
1174    /// transport mode (there is no application netstack to bind on).
1175    pub async fn listen_service(
1176        &self,
1177        name: &str,
1178        mode: ts_control::ServiceMode,
1179    ) -> Result<netstack::TcpListener, ts_control::ServiceError> {
1180        let me = self
1181            .self_node()
1182            .await
1183            .map_err(|e| ts_control::ServiceError::Listen(e.to_string()))?;
1184        let listen_addr = ts_control::resolve_service_listen(&me, name, mode, self.enable_ipv6)?;
1185        self.tcp_listen(listen_addr)
1186            .await
1187            .map_err(|e| ts_control::ServiceError::Listen(e.to_string()))
1188    }
1189
1190    /// Attempt to gracefully shut down this device's runtime.
1191    ///
1192    /// Reports whether the device was fully shut down before the timeout. It is still shut
1193    /// down if it timed out, just more violently and with potential resource leaks.
1194    ///
1195    /// If `timeout` is `None`, then shutdown will never time-out.
1196    pub async fn shutdown(self, timeout: Option<Duration>) -> bool {
1197        self.runtime.graceful_shutdown(timeout).await
1198    }
1199}
1200
1201/// Command-channel-driven userspace network stack.
1202///
1203/// This is an opinionated wrapper around [smoltcp](https://docs.rs/smoltcp) that provides an
1204/// easier-to-integrate, more-portable API.
1205pub mod netstack {
1206    #[doc(inline)]
1207    pub use ts_netstack_smoltcp::netcore::Error;
1208    #[doc(inline)]
1209    pub use ts_netstack_smoltcp::netcore::InternalErrorKind;
1210    #[doc(inline)]
1211    pub use ts_netstack_smoltcp::netsock::{TcpListener, TcpStream, UdpSocket};
1212}
1213
1214/// Geneve (RFC 8926) framing for Tailscale **peer-relay** traffic. A peer that advertises
1215/// [`NodeInfo::is_peer_relay`] runs a UDP relay server; relayed disco + WireGuard frames are
1216/// Geneve-encapsulated with a VNI. This module exposes the header codec so the framing is
1217/// recognizable. NOTE: the active relay *data path* (the relay-allocation handshake +
1218/// magicsock integration) is **not yet implemented** in this fork — this is the wire-aware slice.
1219pub mod geneve {
1220    #[doc(inline)]
1221    pub use ts_packet::geneve::{
1222        GENEVE_FIXED_HEADER_LEN, GENEVE_PROTOCOL_DISCO, GENEVE_PROTOCOL_WIREGUARD, GeneveError,
1223        GeneveHeader,
1224    };
1225}
1226
1227/// Tailnet Lock (TKA) verification: the [`tka::Authority`] checks a peer's node-key signature
1228/// against the trusted-key state, mirroring Go's `tka` package. Pair with [`Device::tka_status`]
1229/// (the control-pushed head/disablement signal).
1230pub mod tka {
1231    #[doc(inline)]
1232    pub use ts_tka::{
1233        AumHash, AumKind, Authority, Key, KeyKind, NodeKeySignature, SigKind, State, TkaError,
1234        aum_hash,
1235    };
1236}
1237
1238/// Tailscale cryptographic key types.
1239pub mod keys {
1240    #[doc(inline)]
1241    pub use ts_keys::{
1242        DiscoKeyPair, DiscoPrivateKey, DiscoPublicKey, MachineKeyPair, MachinePrivateKey,
1243        MachinePublicKey, NetworkLockKeyPair, NetworkLockPrivateKey, NetworkLockPublicKey,
1244        NodeKeyPair, NodePrivateKey, NodePublicKey, NodeState, PersistState,
1245    };
1246}
1247
1248const ENV_MAGIC_VAR: &str = "TS_RS_EXPERIMENT";
1249const ENV_MAGIC_VALUE: &str = "this_is_unstable_software";
1250
1251fn check_magic_env() -> Result<(), Error> {
1252    if std::env::var(ENV_MAGIC_VAR).as_deref() != Ok(ENV_MAGIC_VALUE) {
1253        let warning = format!(
1254            "
1255check failed: set {ENV_MAGIC_VAR}={ENV_MAGIC_VALUE} to acknowledge that tailscale-rs is early-days
1256experimental software containing bugs, unvalidated cryptography, and no stability or compatibility
1257guarantees.
1258            "
1259        );
1260
1261        eprintln!("{}", warning.trim());
1262
1263        return Err(Error::UnstableEnvVar);
1264    };
1265
1266    Ok(())
1267}