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, DnsQueryResult, FileTarget, IpnBusWatcher, NetcheckReport, Notify, NotifyWatchOpt,
182 RegionLatency, RegistrationError, Status, StatusNode, 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;
193#[cfg(feature = "hyper")]
194pub mod http;
195mod loopback;
196#[cfg(feature = "ssh")]
197pub mod ssh;
198
199#[doc(inline)]
200pub use dial::{ConnectedUdpSocket, DialConn};
201#[doc(inline)]
202pub use loopback::LoopbackHandle;
203
204/// How a program connects to a tailnet and communicates with peers.
205///
206/// The `Device` connects to the control plane, registers itself with the tailnet, and communicates
207/// with tailnet peers. Its tailnet identity is determined by the key state provided at
208/// construction-time.
209pub struct Device {
210 runtime: ts_runtime::Runtime,
211 /// Command channel to the application netstack. `None` in TUN transport mode, where there is
212 /// no userspace application netstack; the channel-driven socket APIs ([`Device::udp_bind`],
213 /// [`Device::tcp_listen`], [`Device::tcp_connect`], [`Device::ping`]) are unsupported there.
214 channel: Option<Channel>,
215 /// Whether IPv6 is enabled on the tailnet overlay (the `Config::enable_ipv6` gate, default
216 /// `false`). Captured at construction; used by [`Device::listen_service`] to decide whether an
217 /// IPv6 VIP-service address is bindable (the netstack only accepts IPv6 overlay addresses when
218 /// this is set).
219 enable_ipv6: bool,
220 /// The stored Serve config + its live per-port accept loops (`tsnet`'s `Get/SetServeConfig` +
221 /// serving runtime). Built lazily on the first [`Device::set_serve_config`] (it needs this
222 /// node's overlay IPv4, only known after registration). Held here so its accept loops abort when
223 /// the `Device` drops; `None` (empty config) until the first `set`.
224 serve: std::sync::Mutex<Option<ts_runtime::serve::ServeManager>>,
225 /// The live Funnel ingress manager (`tsnet`'s `ListenFunnel` data path), built on
226 /// [`Device::listen_funnel`](crate::Device::listen_funnel). Held here so its TLS-termination pump and the installed peerAPI
227 /// ingress sink stay alive for the device's life (and tear down when a new `listen_funnel`
228 /// replaces it, or the `Device` drops). `None` until the first `listen_funnel`.
229 funnel: std::sync::Mutex<Option<ts_runtime::funnel::FunnelManager>>,
230}
231
232/// Map a [`ts_runtime::taildrop::TaildropError`] to the device-facing [`Error`]. `Error` is a
233/// `Copy` enum with no payload, so the I/O detail string is dropped, but the *kind* is preserved so
234/// a caller can still distinguish the actionable cases: an invalid name →
235/// [`InternalErrorKind::BadRequest`], an in-progress conflict → [`InternalErrorKind::AlreadyExists`],
236/// a missing file → [`InternalErrorKind::NotFound`], and any other filesystem failure →
237/// [`InternalErrorKind::Io`].
238fn taildrop_err(e: ts_runtime::taildrop::TaildropError) -> Error {
239 use ts_runtime::taildrop::TaildropError;
240 match e {
241 TaildropError::InvalidFileName => Error::Internal(InternalErrorKind::BadRequest),
242 TaildropError::FileExists => Error::Internal(InternalErrorKind::AlreadyExists),
243 TaildropError::Io(io) if io.kind() == std::io::ErrorKind::NotFound => {
244 Error::Internal(InternalErrorKind::NotFound)
245 }
246 TaildropError::Io(_) => Error::Internal(InternalErrorKind::Io),
247 }
248}
249
250/// Map a [`ts_runtime::taildrop_send::TaildropSendError`] (the Taildrop *sender*) to the
251/// device-facing [`Error`]. The send-side conflict/forbidden/unexpected-status cases all reduce to
252/// `BadRequest` (the peer refused the transfer for a request-level reason), a dial failure or
253/// timeout to `Timeout`, an invalid name to `BadRequest`, and any stream I/O failure to `Io`.
254fn taildrop_send_err(e: ts_runtime::taildrop_send::TaildropSendError) -> Error {
255 use ts_runtime::taildrop_send::TaildropSendError;
256 match e {
257 TaildropSendError::Connect | TaildropSendError::Timeout => Error::Timeout,
258 TaildropSendError::InvalidName
259 | TaildropSendError::Forbidden
260 | TaildropSendError::Conflict
261 | TaildropSendError::UnexpectedStatus(_) => Error::Internal(InternalErrorKind::BadRequest),
262 TaildropSendError::Io => Error::Internal(InternalErrorKind::Io),
263 }
264}
265
266/// Resolve the effective registration auth key from `auth_key` plus the config's
267/// workload-identity-federation (WIF) / OAuth-client fields.
268///
269/// With the `identity-federation` feature enabled, an OAuth client secret (`tskey-client-…`) or a
270/// `client_id` + (`id_token` | `audience`) is exchanged for a Tailscale auth key against the SaaS
271/// admin API before registration (Go `tsnet.Server`'s `resolveAuthKey`). Without the feature this is
272/// a pure pass-through: `auth_key` is returned unchanged and the WIF config fields are ignored, so
273/// the default build is byte-identical to before.
274#[cfg(feature = "identity-federation")]
275async fn resolve_auth_key(
276 config: &Config,
277 auth_key: Option<String>,
278) -> Result<Option<String>, Error> {
279 let wif = ts_control::WifConfig {
280 auth_key,
281 client_id: config.client_id.clone(),
282 client_secret: config.client_secret.clone(),
283 id_token: config.id_token.clone(),
284 audience: config.audience.clone(),
285 tags: config.requested_tags.clone(),
286 };
287 ts_control::resolve_auth_key(&wif, &config.control_server_url)
288 .await
289 .map_err(|e| {
290 tracing::error!(error = %e, "resolving auth key via workload-identity federation");
291 Error::Internal(InternalErrorKind::BadRequest)
292 })
293}
294
295/// Pass-through when the `identity-federation` feature is disabled: the auth key is used as-is and
296/// the WIF config fields have no effect (matching Go, where the federation path is compiled out
297/// unless its optional feature is linked).
298#[cfg(not(feature = "identity-federation"))]
299async fn resolve_auth_key(
300 _config: &Config,
301 auth_key: Option<String>,
302) -> Result<Option<String>, Error> {
303 Ok(auth_key)
304}
305
306impl Device {
307 /// Create a device from the given [`Config`] and auth key.
308 ///
309 /// Internally, this will spawn multiple asynchronous actors onto a Tokio runtime.
310 ///
311 /// # Example
312 ///
313 /// ```rust,no_run
314 /// # #[tokio::main]
315 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
316 /// # use tailscale::*;
317 /// let dev = Device::new(
318 /// &Config::default_with_key_file("tsrs_keys.json").await?,
319 /// Some("MY_AUTH_KEY".to_string()),
320 /// ).await?;
321 /// # Ok(()) }
322 /// ```
323 pub async fn new(config: &Config, auth_key: Option<String>) -> Result<Self, Error> {
324 check_magic_env()?;
325
326 // Resolve the effective registration auth key. The explicit `auth_key` argument wins; if it
327 // is `None`, fall back to `config.auth_key` (Go `tsnet.Server.AuthKey`). When the
328 // `identity-federation` feature is enabled, the resolved key is further passed through the
329 // WIF / OAuth-client bootstrap, which exchanges an OAuth client secret (`tskey-client-…`) or
330 // an IdP-issued OIDC token for a Tailscale auth key before registration (SaaS-only).
331 let auth_key = auth_key.or_else(|| config.auth_key.clone());
332 let auth_key = resolve_auth_key(config, auth_key).await?;
333
334 let rt =
335 ts_runtime::Runtime::spawn(config.into(), auth_key, (&config.key_state).into()).await?;
336 // In TUN transport mode there is no application netstack, so the runtime has no command
337 // channel: that surfaces as `UnsupportedInTunMode`, which we map to a `None` channel rather
338 // than an error (the device is still usable for control-plane and peer-lookup APIs).
339 let channel = match rt.channel().await {
340 Ok(c) => Some(c),
341 Err(e) if e.kind == ts_runtime::ErrorKind::UnsupportedInTunMode => None,
342 Err(e) => return Err(e.into()),
343 };
344
345 Ok(Self {
346 runtime: rt,
347 channel,
348 enable_ipv6: config.enable_ipv6,
349 serve: std::sync::Mutex::new(None),
350 funnel: std::sync::Mutex::new(None),
351 })
352 }
353
354 /// Create a device from the given [`Config`] and a [`SecretString`] auth key.
355 ///
356 /// This is a back-compat-preserving convenience over [`new`](Self::new) for callers that already
357 /// hold the registration auth key as a [`secrecy::SecretString`] (e.g. a daemon that keeps the
358 /// pre-auth key wrapped end-to-end). It lets the caller avoid materializing a plain `String` at
359 /// the engine boundary: the secret is exposed only on the last inch, immediately before being
360 /// handed to [`new`](Self::new).
361 ///
362 /// # Honesty about the plaintext window
363 ///
364 /// This closes the *caller's* boundary, **not** the engine's internal handling. The engine still
365 /// resolves the auth key to a plain `String` internally for registration (the plaintext `String`
366 /// window inside the engine is identical to calling [`new`](Self::new) directly) — this method
367 /// does not make the engine itself secret-clean. If you call [`new`](Self::new) you create that
368 /// `String` yourself; if you call this you do not, but the engine creates one either way.
369 ///
370 /// Passing `None` is equivalent to `new(config, None)` (falls back to `config.auth_key`).
371 ///
372 /// # Example
373 ///
374 /// ```rust,no_run
375 /// # #[tokio::main]
376 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
377 /// # use tailscale::*;
378 /// let dev = Device::new_with_secret(
379 /// &Config::default_with_key_file("tsrs_keys.json").await?,
380 /// Some(SecretString::from("MY_AUTH_KEY")),
381 /// ).await?;
382 /// # Ok(()) }
383 /// ```
384 pub async fn new_with_secret(
385 config: &Config,
386 auth_key: Option<SecretString>,
387 ) -> Result<Self, Error> {
388 use secrecy::ExposeSecret as _;
389
390 // Expose the secret on the last inch and delegate to `new`, so the spawn/registration path
391 // is shared verbatim (no duplicated runtime-spawn logic) and the engine-internal plaintext
392 // window is byte-for-byte identical to a direct `new` call.
393 let plain = auth_key.map(|s| s.expose_secret().to_string());
394 Self::new(config, plain).await
395 }
396
397 /// The application netstack command channel, or an error in TUN transport mode (no application
398 /// netstack exists).
399 fn channel(&self) -> Result<&Channel, Error> {
400 self.channel
401 .as_ref()
402 .ok_or(Error::Internal(InternalErrorKind::UnsupportedInTunMode))
403 }
404
405 /// Get this [`Device`]'s IPv4 tailnet address.
406 pub async fn ipv4_addr(&self) -> Result<Ipv4Addr, Error> {
407 self.runtime
408 .control
409 .ask(ts_runtime::control_runner::Ipv4)
410 .await
411 .map_err(ts_runtime::Error::from)?
412 .ok_or(Error::Internal(InternalErrorKind::Actor))
413 }
414
415 /// Get this [`Device`]'s IPv6 tailnet address.
416 pub async fn ipv6_addr(&self) -> Result<Ipv6Addr, Error> {
417 self.runtime
418 .control
419 .ask(ts_runtime::control_runner::Ipv6)
420 .await
421 .map_err(ts_runtime::Error::from)?
422 .ok_or(Error::Internal(InternalErrorKind::Actor))
423 }
424
425 /// This node's tailnet IPv4 and (when provisioned) IPv6 addresses as a pair — the Rust analog of
426 /// Go `tsnet.Server.TailscaleIPs() (ip4, ip6 netip.Addr)`.
427 ///
428 /// Reads the self node's assigned addresses (the same source Go splits by family). The tailnet
429 /// is IPv4-only unless [`Config::enable_ipv6`](crate::config::Config) is set, so the IPv6 half is
430 /// `None` when no v6 address is assigned — the Rust shape for Go returning the zero `netip.Addr`
431 /// in that case (Go's IPv6-absent sentinel). Errors until the first netmap is received (no self
432 /// node yet), matching Go returning invalid addresses before the node has joined.
433 pub async fn tailscale_ips(&self) -> Result<(Ipv4Addr, Option<Ipv6Addr>), Error> {
434 let me = self.self_node().await?;
435 let v4 = me.tailnet_address.ipv4.addr();
436 let v6 = me.tailnet_address.ipv6.addr();
437 // The decoder synthesizes the unspecified `::` placeholder on an IPv4-only tailnet; surface
438 // a real v6 only when IPv6 is enabled AND a non-placeholder address was assigned.
439 let v6 = (self.enable_ipv6 && !v6.is_unspecified()).then_some(v6);
440 Ok((v4, v6))
441 }
442
443 /// Bind a UDP socket to the specified [`SocketAddr`].
444 ///
445 /// Returns an error in TUN transport mode (there is no application netstack to bind on).
446 pub async fn udp_bind(&self, socket_addr: SocketAddr) -> Result<netstack::UdpSocket, Error> {
447 self.channel()?
448 .udp_bind(socket_addr)
449 .await
450 .map_err(Into::into)
451 }
452
453 /// Bind a TCP listener to the specified [`SocketAddr`].
454 ///
455 /// Returns an error in TUN transport mode (there is no application netstack to listen on).
456 pub async fn tcp_listen(
457 &self,
458 socket_addr: SocketAddr,
459 ) -> Result<netstack::TcpListener, Error> {
460 self.channel()?
461 .tcp_listen(socket_addr)
462 .await
463 .map_err(Into::into)
464 }
465
466 /// Register a fallback TCP handler (like `tsnet`'s `RegisterFallbackTCPHandler`).
467 ///
468 /// The callback is consulted for every inbound TCP flow that matches **no** explicit
469 /// [`Device::tcp_listen`] listener, with the flow's `(src, dst)` addresses. It returns
470 /// `(handler, intercept)`:
471 /// - `(_, false)` — decline; the next registered callback is tried.
472 /// - `(Some(h), true)` — claim the flow; `h` is handed the accepted [`netstack::TcpStream`].
473 /// - `(None, true)` — claim and reject the flow (the connection is closed).
474 ///
475 /// Multiple handlers may be registered; they are consulted in registration order and the first
476 /// to intercept wins. The returned [`FallbackTcpHandle`] deregisters the handler when dropped.
477 ///
478 /// Handlers serve flows over the overlay netstack only — never a host socket — and a flow no
479 /// handler claims is closed (fail-closed), never direct-dialed.
480 ///
481 /// Returns an error in TUN transport mode (there is no application netstack to attach to).
482 pub fn register_fallback_tcp_handler<F>(&self, cb: F) -> Result<FallbackTcpHandle, Error>
483 where
484 F: Fn(SocketAddr, SocketAddr) -> FallbackDecision + Send + Sync + 'static,
485 {
486 self.runtime
487 .register_fallback_tcp_handler(std::sync::Arc::new(cb))
488 .map_err(Into::into)
489 }
490
491 /// Resolve a tailnet peer (or this node) by MagicDNS name to its tailnet IPv4 address.
492 ///
493 /// This is an in-process lookup against the netmap we already hold — like `tsnet`'s in-memory
494 /// `dnsMap`, it does not query any DNS server (there is no `100.100.100.100` resolver). The
495 /// `name` may be a bare hostname or a fully-qualified MagicDNS name, with or without a trailing
496 /// dot, in any case (matching is case-insensitive). Returns `Ok(None)` if no tailnet node has
497 /// that name.
498 ///
499 /// Only MagicDNS names are resolved; names outside the tailnet are not looked up here, so the
500 /// caller's system resolver remains responsible for them. IPv6 is intentionally not resolved —
501 /// this fork operates IPv4-only on the tailnet.
502 pub async fn resolve(&self, name: &str) -> Result<Option<Ipv4Addr>, Error> {
503 if let Some(peer) = self.peer_by_name(name).await? {
504 return Ok(Some(peer.tailnet_address.ipv4.addr()));
505 }
506
507 // tsnet's dnsMap also resolves our own name; fall back to self when no peer matches.
508 let me = self.self_node().await?;
509 if me.matches_name(name) {
510 return Ok(Some(me.tailnet_address.ipv4.addr()));
511 }
512
513 Ok(None)
514 }
515
516 /// Run a real DNS query through the tailnet's MagicDNS responder (the `100.100.100.100`
517 /// forward path), returning the raw response, RCODE, and resolver(s) consulted — the analogue of
518 /// Go `LocalClient.QueryDNS`.
519 ///
520 /// Unlike [`resolve`](Self::resolve) (an in-memory netmap lookup that answers only MagicDNS
521 /// A-records), this issues an actual query of any `qtype` and runs it through the live
522 /// responder: an authoritative tailnet name is answered locally, anything else is forwarded to
523 /// the configured split-DNS / recursive upstreams (or delegated to the active exit node's DoH).
524 /// The response is returned as raw bytes (matching Go's `QueryDNS`), since this fork's DNS wire
525 /// codec has no answer-record decoder; the caller parses records itself if needed.
526 ///
527 /// `qtype` is the raw RFC 1035 TYPE value (`1`=A, `28`=AAAA, `12`=PTR, `16`=TXT, `33`=SRV,
528 /// `65`=HTTPS/SVCB, …). Anti-leak is inherited from the responder: a tailnet-suffix name never
529 /// egresses, recursive forwards delegate to the exit node when one is active, and only IPv4
530 /// upstreams are dialed.
531 ///
532 /// Returns an [`Error::Internal`] with `InternalErrorKind::UnsupportedInTunMode` in TUN
533 /// transport mode (MagicDNS there is an in-packet intercept, not a queryable responder).
534 pub async fn query_dns(&self, name: &str, qtype: u16) -> Result<DnsQueryResult, Error> {
535 self.runtime
536 .query_dns(name, qtype)
537 .await
538 .map_err(Into::into)
539 }
540
541 /// Connect to a tailnet peer by MagicDNS name and port over TCP.
542 ///
543 /// Resolves `name` via [`Device::resolve`] (an in-process netmap lookup, no DNS server), then
544 /// dials the resulting tailnet IPv4 address. Returns [`InternalErrorKind::BadRequest`] if the
545 /// name does not resolve to a tailnet node.
546 pub async fn connect_by_name(
547 &self,
548 name: &str,
549 port: u16,
550 ) -> Result<netstack::TcpStream, Error> {
551 let addr = self
552 .resolve(name)
553 .await?
554 .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
555
556 self.tcp_connect((addr, port).into()).await
557 }
558
559 /// Resolve a `host:port` string to a tailnet [`SocketAddr`], honoring the family forced by a
560 /// `network` suffix. The host may be an IP literal (parsed directly) or a MagicDNS name
561 /// (resolved via [`Device::resolve`], which yields a tailnet IPv4). Shared by [`Device::dial`]
562 /// and [`Device::dial_tcp`]. The IPv4-only invariant is enforced here: a `…6` network, or any v6
563 /// destination, requires `Config::enable_ipv6` and otherwise returns
564 /// [`InternalErrorKind::BadRequest`] (a clean typed error rather than a downstream actor error).
565 async fn resolve_dial_addr(
566 &self,
567 network: dial::Network,
568 addr: &str,
569 ) -> Result<SocketAddr, Error> {
570 let (host, port) = dial::split_host_port(addr)?;
571
572 // An IP literal is used directly; otherwise resolve the MagicDNS name (IPv4 only).
573 let ip: IpAddr = if let Ok(ip) = host.parse::<IpAddr>() {
574 ip
575 } else {
576 self.resolve(host)
577 .await?
578 .ok_or(Error::Internal(InternalErrorKind::BadRequest))?
579 .into()
580 };
581
582 dial::check_family(network.family, ip)?;
583
584 // IPv4-only invariant: a v6 destination is only reachable when IPv6 is provisioned.
585 if ip.is_ipv6() && !self.enable_ipv6 {
586 return Err(Error::Internal(InternalErrorKind::BadRequest));
587 }
588
589 Ok((ip, port).into())
590 }
591
592 /// Connect to a tailnet address over TCP or UDP, the Rust analog of Go
593 /// `tsnet.Server.Dial(ctx, network, address)`.
594 ///
595 /// `network` is one of `"tcp"`, `"tcp4"`, `"tcp6"`, `"udp"`, `"udp4"`, `"udp6"`; `addr` is a
596 /// `host:port` string where `host` is a MagicDNS name, an IPv4 literal, or a bracketed IPv6
597 /// literal (`[2001:db8::1]:443`). The host is resolved in-process via [`Device::resolve`] (no DNS
598 /// server). Returns a [`DialConn`] whose arm matches the transport — use [`Device::dial_tcp`]
599 /// when you want the TCP stream directly.
600 ///
601 /// Differences from Go (documented for parity): ports must be **numeric** (Go's `LookupPort`
602 /// also resolves named ports like `"http"`; this fork avoids a services-file dependency), and
603 /// `…6`/v6 destinations require `Config::enable_ipv6` (the tailnet is IPv4-only by default).
604 ///
605 /// # Errors
606 /// [`InternalErrorKind::BadRequest`] for an unsupported `network`, a malformed/portless `addr`,
607 /// an unresolvable name, or a v6 destination while IPv6 is disabled; otherwise the transport's
608 /// own connect error.
609 pub async fn dial(&self, network: &str, addr: &str) -> Result<DialConn, Error> {
610 let net = dial::parse_network(network)?;
611 let remote = self.resolve_dial_addr(net, addr).await?;
612
613 match net.transport {
614 dial::Transport::Tcp => Ok(DialConn::Tcp(self.tcp_connect(remote).await?)),
615 dial::Transport::Udp => {
616 // Bind an ephemeral local UDP socket on this node's tailnet address of the SAME
617 // family as the remote, then connect it (Go's `Dial("udp", …)` returns a connected
618 // UDP `net.Conn`, with the local source picked by `IfElse(dst.Is6(), v6, v4)`). A v4
619 // local socket cannot send to a v6 peer, so the family must match `remote`. (TCP gets
620 // this for free: `tcp_connect` already picks the source family from `remote`.)
621 let local_ip: IpAddr = if remote.is_ipv6() {
622 self.ipv6_addr().await?.into()
623 } else {
624 self.ipv4_addr().await?.into()
625 };
626 let sock = self.udp_bind((local_ip, 0).into()).await?;
627 Ok(DialConn::Udp(ConnectedUdpSocket::new(sock, remote)))
628 }
629 }
630 }
631
632 /// Connect to a tailnet address over TCP, returning the stream directly — the common case of
633 /// [`Device::dial`] for `"tcp"`. `addr` is a `host:port` string (MagicDNS name or IP literal).
634 /// This is the building block for HTTP-over-tailnet: an embedder's `hyper`/`reqwest` client can
635 /// route requests by calling `dial_tcp(&format!("{host}:{port}"))` from its connector, mirroring
636 /// how Go `tsnet.Server.HTTPClient` sets `http.Transport.DialContext = Server.Dial`.
637 ///
638 /// # Errors
639 /// As [`Device::dial`] for the `"tcp"` network.
640 pub async fn dial_tcp(&self, addr: &str) -> Result<netstack::TcpStream, Error> {
641 let remote = self
642 .resolve_dial_addr(
643 dial::Network {
644 transport: dial::Transport::Tcp,
645 family: dial::Family::Any,
646 },
647 addr,
648 )
649 .await?;
650 self.tcp_connect(remote).await
651 }
652
653 /// Connect to a tailnet address over UDP, returning a connected socket directly — the `"udp"`
654 /// sibling of [`dial_tcp`](Device::dial_tcp) and the common case of [`Device::dial`] for
655 /// `"udp"`. `addr` is a `host:port` string (MagicDNS name or IP literal).
656 ///
657 /// Returns a [`ConnectedUdpSocket`] (`send`/`recv` against a fixed peer), the connected
658 /// UDP-`net.Conn` shape Go's `tsnet.Server.Dial("udp", …)` returns — as opposed to
659 /// [`listen_packet`](Device::listen_packet), which yields an unconnected `net.PacketConn`. An
660 /// ephemeral local UDP socket is bound on this node's tailnet address of the same family as the
661 /// resolved remote (a v4 local socket cannot send to a v6 peer).
662 ///
663 /// # Errors
664 /// As [`Device::dial`] for the `"udp"` network (name resolution, the IPv4-only / `enable_ipv6`
665 /// family invariant, or TUN transport mode having no application netstack to bind on).
666 pub async fn dial_udp(&self, addr: &str) -> Result<ConnectedUdpSocket, Error> {
667 let remote = self
668 .resolve_dial_addr(
669 dial::Network {
670 transport: dial::Transport::Udp,
671 family: dial::Family::Any,
672 },
673 addr,
674 )
675 .await?;
676 let local_ip: IpAddr = if remote.is_ipv6() {
677 self.ipv6_addr().await?.into()
678 } else {
679 self.ipv4_addr().await?.into()
680 };
681 let sock = self.udp_bind((local_ip, 0).into()).await?;
682 Ok(ConnectedUdpSocket::new(sock, remote))
683 }
684
685 /// Bind a UDP socket from a `host:port` string, the Rust analog of Go
686 /// `tsnet.Server.ListenPacket(network, addr)`.
687 ///
688 /// `network` is one of `"udp"`, `"udp4"`, `"udp6"`; `addr` must be a **valid IP literal**
689 /// `host:port` (Go's `ListenPacket` rejects a name or empty host — unlike `Listen`). An
690 /// unspecified host (`0.0.0.0`/`[::]`) binds on this node's tailnet address. Returns the
691 /// unconnected [`netstack::UdpSocket`] (a `net.PacketConn`).
692 ///
693 /// # Errors
694 /// [`InternalErrorKind::BadRequest`] for a non-UDP/unsupported `network`, a malformed addr, a
695 /// non-IP host, a family mismatch, or a v6 bind while IPv6 is disabled.
696 pub async fn listen_packet(
697 &self,
698 network: &str,
699 addr: &str,
700 ) -> Result<netstack::UdpSocket, Error> {
701 let net = dial::parse_network(network)?;
702 if net.transport != dial::Transport::Udp {
703 return Err(Error::Internal(InternalErrorKind::BadRequest));
704 }
705 let (host, port) = dial::split_host_port(addr)?;
706
707 // ListenPacket requires a valid IP host (Go rejects a name here).
708 let ip: IpAddr = host
709 .parse()
710 .map_err(|_| Error::Internal(InternalErrorKind::BadRequest))?;
711 dial::check_family(net.family, ip)?;
712
713 // A v6 bind (whether an explicit literal or an unspecified `[::]`) requires IPv6 to be
714 // provisioned — enforce the gate for BOTH cases (the unspecified `[::]` path used to skip it).
715 if ip.is_ipv6() && !self.enable_ipv6 {
716 return Err(Error::Internal(InternalErrorKind::BadRequest));
717 }
718
719 // An unspecified bind host (`0.0.0.0` / `[::]`) means "this node's tailnet address" — of the
720 // SAME family as the requested address, so a `udp6` `[::]:0` binds a v6 socket (it used to
721 // fall through to the v4 address regardless, silently yielding an IPv4 socket for a v6 listen).
722 let bind_ip: IpAddr = if ip.is_unspecified() {
723 if ip.is_ipv6() {
724 self.ipv6_addr().await?.into()
725 } else {
726 self.ipv4_addr().await?.into()
727 }
728 } else {
729 ip
730 };
731
732 self.udp_bind((bind_ip, port).into()).await
733 }
734
735 /// Connect to a TCP socket at the remote address.
736 ///
737 /// Returns an error in TUN transport mode (there is no application netstack to dial from).
738 pub async fn tcp_connect(&self, remote: SocketAddr) -> Result<netstack::TcpStream, Error> {
739 let channel = self.channel()?;
740
741 let ip: IpAddr = match remote.is_ipv4() {
742 true => self.ipv4_addr().await?.into(),
743 false => self.ipv6_addr().await?.into(),
744 };
745
746 // TODO(npry): collision checking
747 let ephemeral_port = rand::random_range(49152..=u16::MAX);
748
749 channel
750 .tcp_connect((ip, ephemeral_port).into(), remote)
751 .await
752 .map_err(Into::into)
753 }
754
755 /// Start a SOCKS5 proxy on a host loopback address that dials into the tailnet (Go
756 /// `tsnet.Server.Loopback`, SOCKS5 half).
757 ///
758 /// Binds a TCP listener on `127.0.0.1:0` (host loopback only — never an external interface) and
759 /// serves SOCKS5 (RFC 1928) with required username/password auth (RFC 1929): username `tsnet`,
760 /// password = the returned `proxy_cred`. Each `CONNECT` is dialed INTO the overlay via
761 /// [`Device::connect_by_name`] / [`Device::tcp_connect`] and spliced to the accepted host socket, so
762 /// a non-Rust host process can reach tailnet peers through the proxy. Returns the bound address, the
763 /// proxy credential, and a [`LoopbackHandle`] whose drop stops the listener.
764 ///
765 /// Anti-leak: the listener is loopback-only and every connection egresses over the overlay, never a
766 /// host socket — the host's real origin IP is never used to reach the destination. Unlike Go, the
767 /// LocalAPI HTTP surface is not served (this fork exposes status/whois/id-token natively on
768 /// `Device`); only the SOCKS5 proxy is provided.
769 ///
770 /// Returns an error in TUN transport mode (no application netstack to dial from).
771 pub async fn loopback(&self) -> Result<(std::net::SocketAddr, String, LoopbackHandle), Error> {
772 loopback::start(self.overlay_dialer().await?).await
773 }
774
775 /// Build an [`OverlayDialer`](loopback::OverlayDialer): the cloneable, `&Device`-free dialer that
776 /// resolves a MagicDNS name (or takes an IPv4 literal) and `tcp_connect`s it into the overlay,
777 /// reused by [`Device::loopback`] (SOCKS5) and the `hyper` [`http_connector`](Device::http_connector).
778 ///
779 /// Captures only cloneable pieces — never `&self` — so the dialer (and anything built on it, like a
780 /// spawned accept loop or an HTTP connector) carries no borrow of the `Device`: a clone of the
781 /// netstack command channel, this device's own overlay IPv4 (fetched once), and a boxed resolver
782 /// closure over clones of the control + peer-tracker actor refs. The resolver replicates
783 /// [`Device::resolve`] (peer-by-name, falling back to this node's own name).
784 async fn overlay_dialer(&self) -> Result<loopback::OverlayDialer, Error> {
785 let channel = self.channel()?.clone();
786 let self_ipv4 = self.ipv4_addr().await?;
787
788 let control = self.runtime.control.clone();
789 let peer_tracker = self.runtime.peer_tracker.clone();
790 let resolve: loopback::Resolver = std::sync::Arc::new(move |name: String| {
791 let control = control.clone();
792 let peer_tracker = peer_tracker.clone();
793 Box::pin(async move {
794 let pt = peer_tracker
795 .upgrade()
796 .ok_or(Error::Internal(InternalErrorKind::Actor))?;
797 let peer = pt
798 .ask(ts_runtime::peer_tracker::PeerByName { name: name.clone() })
799 .await
800 .map_err(ts_runtime::Error::from)?;
801 if let Some(peer) = peer {
802 return Ok(Some(peer.tailnet_address.ipv4.addr()));
803 }
804 // tsnet's dnsMap also resolves our own name; fall back to self.
805 let me = control
806 .ask(ts_runtime::control_runner::SelfNode)
807 .await
808 .map_err(ts_runtime::Error::from)?
809 .ok_or(Error::Internal(InternalErrorKind::Actor))?;
810 if me.matches_name(&name) {
811 Ok(Some(me.tailnet_address.ipv4.addr()))
812 } else {
813 Ok(None)
814 }
815 }) as std::pin::Pin<Box<dyn std::future::Future<Output = _> + Send>>
816 });
817
818 Ok(loopback::OverlayDialer::new(channel, self_ipv4, resolve))
819 }
820
821 /// Build a [`hyper`-compatible connector](crate::http::TailnetConnector) that routes outbound HTTP
822 /// requests over the tailnet — the analog of Go `tsnet.Server.HTTPClient`, whose mechanism is
823 /// simply `http.Transport{DialContext: s.Dial}`.
824 ///
825 /// Hand the returned connector to `hyper_util::client::legacy::Client::builder(...).build(conn)`;
826 /// each request's `Uri` host is resolved as a MagicDNS name (or IPv4 literal) and dialed into the
827 /// overlay (default port 80 for `http`, 443 for `https`), so the request egresses over the tailnet
828 /// rather than the host's network. TLS, redirects, and pooling are the hyper client's concern — the
829 /// connector only supplies the transport, exactly like Go's bare `DialContext` injection.
830 ///
831 /// Available only with the **`hyper`** crate feature.
832 ///
833 /// # Errors
834 /// Fails for the same reasons as [`Device::loopback`]'s setup: TUN transport mode (no application
835 /// netstack) or the node not yet having an overlay IPv4.
836 #[cfg(feature = "hyper")]
837 pub async fn http_connector(&self) -> Result<crate::http::TailnetConnector, Error> {
838 Ok(crate::http::TailnetConnector::new(
839 self.overlay_dialer().await?,
840 ))
841 }
842
843 /// Get our node info.
844 pub async fn self_node(&self) -> Result<NodeInfo, Error> {
845 self.runtime
846 .control
847 .ask(ts_runtime::control_runner::SelfNode)
848 .await
849 .map_err(ts_runtime::Error::from)?
850 .ok_or(Error::Internal(InternalErrorKind::Actor))
851 }
852
853 /// The DNS names this node can obtain TLS certificates for — Go `tsnet.Server.CertDomains()`.
854 ///
855 /// These are the `CertDomains` control pushed in the netmap DNS config: the names a TLS-serving
856 /// consumer (e.g. a `ListenTLS`/`GetCertificate`-style caller) should request a cert for. Returns
857 /// an empty `Vec` before the first netmap, or when control granted none — mirroring Go returning a
858 /// clone of `nm.DNS.CertDomains` (empty/`nil` when absent).
859 pub async fn cert_domains(&self) -> Result<Vec<String>, Error> {
860 self.runtime
861 .control
862 .ask(ts_runtime::control_runner::CertDomains)
863 .await
864 .map_err(ts_runtime::Error::from)
865 .map_err(Into::into)
866 }
867
868 /// The DNS configuration control pushed in the latest netmap — Go `tsnet`'s view of
869 /// `netmap.NetworkMap.DNS` (what `tailscale dns status` reports).
870 ///
871 /// Returns the full [`DnsConfig`] — MagicDNS on/off, search domains, global + fallback resolvers,
872 /// split-DNS routes, extra records, cert domains — or `None` before the first netmap / when
873 /// control has sent no DNS config. A superset of [`cert_domains`](Device::cert_domains), which
874 /// remains a separate narrower accessor for the TLS-cert use. Mirrors Go reading a clone of
875 /// `nm.DNS` (absent ⇒ `None`).
876 pub async fn dns_config(&self) -> Result<Option<DnsConfig>, Error> {
877 self.runtime
878 .control
879 .ask(ts_runtime::control_runner::DnsConfig)
880 .await
881 .map_err(ts_runtime::Error::from)
882 .map_err(Into::into)
883 }
884
885 /// The URL control last asked this node to open in a browser (`MapResponse.PopBrowserURL`), or
886 /// `None` if control has never sent one.
887 ///
888 /// This is the interactive-login / consent URL an embedder driving a non-authkey (interactive)
889 /// login must surface to the user — the Rust analog of Go `ipn` delivering `BrowseToURL` through
890 /// the notification bus. A daemon polls this after starting an interactive login to obtain the
891 /// auth URL to present.
892 ///
893 /// **Sticky semantics** (Go `controlclient`'s `sess.lastPopBrowserURL`): once control sends a
894 /// URL it remains the returned value until control sends a *different* non-empty one — it is
895 /// **never cleared back to `None`** (control sends `PopBrowserURL` empty on nearly every netmap
896 /// tick; those empty updates are ignored, not treated as "clear"). So a non-`None` result does
897 /// **not** signal "control is asking *right now*" vs. "already handled" — it is the last URL
898 /// seen this session. A consumer that acts on it should de-duplicate on the URL value rather than
899 /// re-acting on every poll. For a push stream of *new* consent URLs (rather than polling this
900 /// sticky value), subscribe to [`watch_ipn_bus`](Self::watch_ipn_bus) and react to
901 /// [`Notify::browse_to_url`](crate::Notify::browse_to_url).
902 pub async fn pop_browser_url(&self) -> Result<Option<Url>, Error> {
903 self.runtime
904 .control
905 .ask(ts_runtime::control_runner::PopBrowserUrl)
906 .await
907 .map_err(ts_runtime::Error::from)
908 .map_err(Into::into)
909 }
910
911 /// This node's latest network-conditions report — the Rust analog of Go's `netcheck.Report` as
912 /// `tailscale netcheck` surfaces it.
913 ///
914 /// Returns the [`NetcheckReport`]: the preferred (lowest-latency) DERP region and the per-region
915 /// latency map this node last measured. Empty (default) before the first measurement. This fork's
916 /// net-report path measures only DERP-region latency, so the report carries that subset rather
917 /// than fabricating the UDP/port-mapping fields Go also reports (see [`NetcheckReport`]).
918 pub async fn netcheck(&self) -> Result<NetcheckReport, Error> {
919 self.runtime
920 .control
921 .ask(ts_runtime::control_runner::Netcheck)
922 .await
923 .map_err(ts_runtime::Error::from)
924 .map_err(Into::into)
925 }
926
927 /// This node's key-expiry instant as Unix seconds (`Node.KeyExpiry` in Go), or `Ok(None)` if
928 /// the key never expires.
929 ///
930 /// Like Go, this fork is **reactive** about key expiry — it reports it rather than rotating the
931 /// node key in the background. A caller can schedule re-authentication around this time; on
932 /// expiry, re-create the [`Device`] (which re-registers), supplying a fresh node key + the prior
933 /// `old_node_key` to rotate, or the same key to refresh.
934 pub async fn self_key_expiry_unix(&self) -> Result<Option<i64>, Error> {
935 Ok(self.self_node().await?.key_expiry_unix())
936 }
937
938 /// Whether this node's key has expired as of now (`!KeyExpiry.IsZero() && KeyExpiry.Before(now)`
939 /// in Go). A key with no expiry is never expired. See [`Device::self_key_expiry_unix`] for the
940 /// reactive-rotation note.
941 pub async fn self_key_expired(&self) -> Result<bool, Error> {
942 let now = std::time::SystemTime::now()
943 .duration_since(std::time::UNIX_EPOCH)
944 .map(|d| d.as_secs() as i64)
945 // An unreadable clock (pre-epoch) is treated as the far future so a time-limited key
946 // looks expired — fail-safe toward prompting re-auth rather than trusting a stale key.
947 .unwrap_or(i64::MAX);
948 Ok(self.self_node().await?.key_expired_at_unix(now))
949 }
950
951 /// Fetch the current Tailscale SSH policy pushed by control, if any.
952 ///
953 /// Returns `Ok(None)` when control has not sent an SSH policy. The SSH server treats an absent
954 /// or empty policy as **deny-all** (fail-closed). Used by the SSH auth path
955 /// ([`SshPolicy::evaluate`][ts_control::SshPolicy::evaluate]) to authorize incoming
956 /// connections.
957 pub async fn ssh_policy(&self) -> Result<Option<ts_control::SshPolicy>, Error> {
958 self.runtime
959 .control
960 .ask(ts_runtime::control_runner::CurrentSshPolicy)
961 .await
962 .map_err(ts_runtime::Error::from)
963 .map_err(Into::into)
964 }
965
966 /// Look up a peer by name.
967 pub async fn peer_by_name(&self, name: &str) -> Result<Option<NodeInfo>, Error> {
968 let pt = self
969 .runtime
970 .peer_tracker
971 .upgrade()
972 .ok_or(Error::Internal(InternalErrorKind::Actor))?;
973
974 pt.ask(ts_runtime::peer_tracker::PeerByName {
975 name: name.to_string(),
976 })
977 .await
978 .map_err(ts_runtime::Error::from)
979 .map_err(Into::into)
980 }
981
982 /// Look up a peer by ip.
983 pub async fn peer_by_tailnet_ip(&self, ip: IpAddr) -> Result<Option<NodeInfo>, Error> {
984 let pt = self
985 .runtime
986 .peer_tracker
987 .upgrade()
988 .ok_or(Error::Internal(InternalErrorKind::Actor))?;
989
990 pt.ask(ts_runtime::peer_tracker::PeerByTailnetIp { ip })
991 .await
992 .map_err(ts_runtime::Error::from)
993 .map_err(Into::into)
994 }
995
996 /// Look up the peer(s) with the most-specific route matches for `ip`.
997 ///
998 /// This reports which peers *advertise* a route covering `ip`, independent of this device's
999 /// `accept_routes` setting — analogous to the Go client's informational `PrimaryRoutes`. It is
1000 /// not a reachability oracle: with `accept_routes` off, the dataplane will not actually route
1001 /// to (or accept return traffic from) advertised subnet routes even if this returns a peer.
1002 pub async fn peers_with_route(&self, ip: IpAddr) -> Result<Vec<NodeInfo>, Error> {
1003 let pt = self
1004 .runtime
1005 .peer_tracker
1006 .upgrade()
1007 .ok_or(Error::Internal(InternalErrorKind::Actor))?;
1008
1009 pt.ask(ts_runtime::peer_tracker::PeerByAcceptedRoute { ip })
1010 .await
1011 .map_err(ts_runtime::Error::from)
1012 .map_err(Into::into)
1013 }
1014
1015 /// List the Taildrop files this device has fully received and not yet consumed (Go LocalAPI
1016 /// `WaitingFiles`).
1017 ///
1018 /// Returns the files waiting under the configured `taildrop_dir`, sorted by name. Returns an
1019 /// empty list when Taildrop is disabled (`Config::taildrop_dir` unset) — fail-closed, never an
1020 /// error for the disabled case. A filesystem error while listing surfaces as
1021 /// [`InternalErrorKind::Actor`].
1022 pub fn taildrop_waiting_files(&self) -> Result<Vec<WaitingFile>, Error> {
1023 let Some(store) = self.runtime.taildrop_store() else {
1024 return Ok(Vec::new());
1025 };
1026 store
1027 .waiting_files()
1028 .map_err(|_| Error::Internal(InternalErrorKind::Actor))
1029 }
1030
1031 /// Open a received Taildrop file by name for reading, returning the handle and its size (Go
1032 /// LocalAPI `OpenFile`).
1033 ///
1034 /// The `name` is validated (path-traversal-safe) inside the store before any path is built.
1035 /// Returns [`InternalErrorKind::BadRequest`] when Taildrop is disabled or the name is invalid,
1036 /// and [`InternalErrorKind::Actor`] for a filesystem error (e.g. the file does not exist).
1037 pub fn taildrop_open_file(&self, name: &str) -> Result<(std::fs::File, u64), Error> {
1038 let store = self
1039 .runtime
1040 .taildrop_store()
1041 .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
1042 store.open_file(name).map_err(taildrop_err)
1043 }
1044
1045 /// Delete a received Taildrop file by name (Go LocalAPI `DeleteFile`).
1046 ///
1047 /// The `name` is validated (path-traversal-safe) inside the store before any path is built.
1048 /// Returns [`InternalErrorKind::BadRequest`] when Taildrop is disabled or the name is invalid,
1049 /// and [`InternalErrorKind::Actor`] for a filesystem error (e.g. the file does not exist).
1050 pub fn taildrop_delete_file(&self, name: &str) -> Result<(), Error> {
1051 let store = self
1052 .runtime
1053 .taildrop_store()
1054 .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
1055 store.delete_file(name).map_err(taildrop_err)
1056 }
1057
1058 /// Send a local file to a tailnet `peer` via Taildrop (Go `PushFile` / `tailscale file cp`).
1059 ///
1060 /// Pushes `content_length` bytes from `reader` to the peer's peerAPI as
1061 /// `PUT /v0/put/<name>` over the overlay netstack — the sending counterpart to the receive store
1062 /// surfaced by [`Device::taildrop_waiting_files`]. The transfer rides the encrypted WireGuard
1063 /// overlay, never a host socket. The body is streamed from offset 0 (no resume).
1064 ///
1065 /// The destination is derived **solely from `peer`'s own node record**
1066 /// ([`NodeInfo::peerapi_addr`][ts_control::Node::peerapi_addr]): its advertised tailnet IPv4 and
1067 /// `peerapi4` port. The caller obtains `peer` from [`Device::peer_by_name`] /
1068 /// [`Device::peer_by_tailnet_ip`], so it is always a current netmap peer — a raw control-supplied
1069 /// or attacker-chosen address can never be targeted. As defense in depth, the resolved address is
1070 /// additionally asserted to be a Tailscale CGNAT IP before dialing.
1071 ///
1072 /// Returns [`InternalErrorKind::BadRequest`] when the peer advertises no IPv4 peerAPI (so it
1073 /// cannot receive files), when the name is invalid, or when the peer refuses the transfer
1074 /// (`403`/`409`/unexpected status); [`Error::Timeout`] on a dial failure or timeout; and
1075 /// [`InternalErrorKind::Io`] on a mid-transfer stream error.
1076 pub async fn send_file<R>(
1077 &self,
1078 peer: &NodeInfo,
1079 name: &str,
1080 content_length: u64,
1081 reader: R,
1082 ) -> Result<(), Error>
1083 where
1084 R: tokio::io::AsyncRead + Unpin,
1085 {
1086 let channel = self.channel()?;
1087
1088 // Destination comes only from the peer's own node record — never an arbitrary address.
1089 let dst = peer
1090 .peerapi_addr()
1091 .ok_or(Error::Internal(InternalErrorKind::BadRequest))?;
1092 // Defense in depth: refuse to dial anything outside the Tailscale CGNAT range, so a
1093 // malformed node record can't steer the PUT at a non-tailnet host.
1094 if !ts_control::is_tailscale_ip(dst.ip()) {
1095 return Err(Error::Internal(InternalErrorKind::BadRequest));
1096 }
1097
1098 let self_ipv4 = self.ipv4_addr().await?;
1099
1100 ts_runtime::taildrop_send::send_file(channel, self_ipv4, dst, name, content_length, reader)
1101 .await
1102 .map_err(taildrop_send_err)
1103 }
1104
1105 /// List the tailnet peers this node can Taildrop a file *to* — the Rust analog of Go's LocalAPI
1106 /// `FileTargets`.
1107 ///
1108 /// Each [`FileTarget`] pairs a peer's node record with the `http://ip:port` base of its peerAPI;
1109 /// pass `target.node` straight to [`Device::send_file`]. A peer qualifies when it advertises a
1110 /// reachable IPv4 peerAPI **and** is either owned by the same user as this node **or** explicitly
1111 /// granted the file-sharing-target capability — mirroring upstream's send-path filter. The list is
1112 /// gated on this node holding the file-sharing capability (control grants it when the admin
1113 /// enables Taildrop); absent that, the result is empty (fail-closed, not an error). Sorted by the
1114 /// peer's MagicDNS name. Targets are listed regardless of online state (matching upstream — an
1115 /// offline target's [`send_file`](Device::send_file) simply times out). Empty before the first
1116 /// netmap.
1117 pub async fn file_targets(&self) -> Result<Vec<FileTarget>, Error> {
1118 self.runtime.file_targets().await.map_err(Into::into)
1119 }
1120
1121 /// Begin a debug packet capture, streaming a pcap of every packet crossing the dataplane to
1122 /// `writer` (Go `tsnet.Server.CapturePcap`).
1123 ///
1124 /// Installs a capture hook on the running dataplane: from now until [`Device::stop_capture`] is
1125 /// called (or another capture replaces this one), a copy of every plaintext IP packet on the
1126 /// datapath — outbound (pre-encrypt) and inbound (post-decrypt) — is framed and written to
1127 /// `writer`. The 24-byte pcap global header is written immediately on success.
1128 ///
1129 /// The format is byte-faithful classic pcap with Tailscale's `LINKTYPE_USER0` + 4-byte path
1130 /// preamble per record (see [`ts_runtime::capture`]); a resulting file opens in Wireshark, and
1131 /// with Tailscale's `ts-dissector.lua` the direction/path of each packet decodes.
1132 ///
1133 /// The hook runs **inline on the single-threaded dataplane step**, so `writer` must not block for
1134 /// long — a slow writer back-pressures the datapath. Records are **not** flushed per packet (that
1135 /// would be a syscall on every packet on the dataplane thread); buffered bytes are flushed when
1136 /// the writer is dropped on [`Device::stop_capture`]. Wrap `writer` in a [`std::io::BufWriter`] if
1137 /// you want buffering. A write error is swallowed per-packet (the capture silently drops that
1138 /// record) rather than tearing down the datapath; call [`Device::stop_capture`] to end it. Returns
1139 /// an error only if the dataplane actor is unreachable or the initial global-header write fails.
1140 pub async fn capture_pcap<W>(&self, writer: W) -> Result<(), Error>
1141 where
1142 W: std::io::Write + Send + 'static,
1143 {
1144 let sink = std::sync::Arc::new(std::sync::Mutex::new(
1145 ts_runtime::capture::PcapSink::new(writer)
1146 .map_err(|_| Error::Internal(InternalErrorKind::Io))?,
1147 ));
1148 let hook: ts_runtime::CaptureHook = std::sync::Arc::new(move |path, pkt: &[u8]| {
1149 if let Ok(mut sink) = sink.lock() {
1150 // A per-packet write failure (e.g. a closed pipe) silently drops that record rather
1151 // than tearing down the datapath; the caller ends capture via `stop_capture`.
1152 drop(sink.log_packet(path.code(), pkt));
1153 }
1154 });
1155 self.runtime.install_capture(Some(hook)).await?;
1156 Ok(())
1157 }
1158
1159 /// Stop a debug packet capture started by [`Device::capture_pcap`] (Go `ClearCaptureSink`).
1160 ///
1161 /// Clears the dataplane capture hook; the writer is dropped (its remaining buffered bytes are
1162 /// flushed by its own `Drop`). Idempotent — clearing when no capture is installed is a no-op.
1163 /// Returns an error only if the dataplane actor is unreachable.
1164 pub async fn stop_capture(&self) -> Result<(), Error> {
1165 self.runtime.install_capture(None).await?;
1166 Ok(())
1167 }
1168
1169 /// Snapshot of this device and its tailnet peers (like `tailscale status`).
1170 ///
1171 /// Combines this node's self info with the current peer set: each [`StatusNode`] reports the
1172 /// stable id, display name, tailnet IPs, advertised routes, and exit-node flag. (Per-peer
1173 /// `online`/user/capabilities are honestly `None`/empty in this fork — the domain node model
1174 /// does not yet carry the wire-level liveness/login fields; see `ts_runtime::status` docs.)
1175 pub async fn status(&self) -> Result<Status, Error> {
1176 self.runtime.status().await.map_err(Into::into)
1177 }
1178
1179 /// Fetch the current Tailnet Lock (TKA) status pushed by control, if any.
1180 ///
1181 /// Returns `Ok(None)` when control has sent no `TKAInfo` (tailnet lock not in use, or no change
1182 /// observed yet). The returned [`TkaStatus`][ts_control::TkaStatus] carries the authority head
1183 /// (a base32 `AUMHash`, decode with [`tka::AumHash::from_base32`][ts_tka::AumHash::from_base32])
1184 /// and the disablement signal. Signature verification of a peer's node-key signature against the
1185 /// authority is performed with the [`tka`] module's [`tka::Authority`][ts_tka::Authority].
1186 pub async fn tka_status(&self) -> Result<Option<ts_control::TkaStatus>, Error> {
1187 self.runtime
1188 .control
1189 .ask(ts_runtime::control_runner::CurrentTkaStatus)
1190 .await
1191 .map_err(ts_runtime::Error::from)
1192 .map_err(Into::into)
1193 }
1194
1195 /// Sign a peer's `node_key` with this node's network-lock key and submit the signature to
1196 /// control — the Rust analog of Go `LocalClient.NetworkLockSign` for the Direct case.
1197 ///
1198 /// Builds a `Direct` [`NodeKeySignature`][ts_tka::NodeKeySignature] authorizing `node_key`, signed
1199 /// by this node's network-lock private key, and POSTs it to `/machine/tka/sign`. The signing node
1200 /// must itself be trusted under the current authority for control to accept the signature.
1201 ///
1202 /// **This only *submits* the signature; it does not mutate this node's local
1203 /// [`Authority`][ts_tka::Authority].** The local trusted-key state advances solely through the
1204 /// verified netmap-driven sync path (every applied AUM passes
1205 /// [`VerifiedAumChain::verify`][ts_tka::VerifiedAumChain::verify]), so a successful `tka_sign` is
1206 /// reflected locally on the next sync — the verify-and-log posture is unchanged.
1207 ///
1208 /// # Errors
1209 /// [`ts_control::TkaSyncError::Unsupported`] if control has no TKA endpoint (no lock / control too
1210 /// old), [`ts_control::TkaSyncError::NetworkError`] on a transient failure, or a coarse
1211 /// `Internal` for other RPC failures.
1212 pub async fn tka_sign(
1213 &self,
1214 node_key: &ts_keys::NodePublicKey,
1215 ) -> Result<(), ts_control::TkaSyncError> {
1216 self.runtime.tka_sign(node_key.to_bytes()).await
1217 }
1218
1219 /// Disable Tailnet Lock by presenting the `disablement_secret` to control — the Rust analog of
1220 /// Go `LocalClient.NetworkLockDisable`.
1221 ///
1222 /// Targets this node's current authority head (from the cached [`tka_status`](Device::tka_status));
1223 /// the `disablement_secret` is the operator-held capability (one of the lock's
1224 /// `DisablementValues`) that authorizes turning the lock off. Control verifies the secret against
1225 /// the authority's disablement set and, if valid, disables the lock for the tailnet.
1226 ///
1227 /// **Submit-only:** this POSTs the disablement; it does not mutate this node's local
1228 /// [`Authority`][ts_tka::Authority]. The disablement is reflected locally through the existing
1229 /// verified netmap-driven sync. Verify-and-log posture is unchanged.
1230 ///
1231 /// # Errors
1232 /// [`ts_control::TkaSyncError::Unsupported`] when there is no known TKA head to disable (lock not
1233 /// in use / control hasn't pushed a status) or control has no TKA endpoint;
1234 /// [`ts_control::TkaSyncError::NetworkError`] on a transient failure; a coarse `Internal` for
1235 /// other RPC failures (incl. control rejecting an invalid secret).
1236 pub async fn tka_disable(
1237 &self,
1238 disablement_secret: Vec<u8>,
1239 ) -> Result<(), ts_control::TkaSyncError> {
1240 self.runtime.tka_disable(disablement_secret).await
1241 }
1242
1243 /// Request an OIDC **ID token** from control for this node, scoped to `audience` (workload-
1244 /// identity federation, like `tailscale`'s `id-token` LocalAPI).
1245 ///
1246 /// Returns a signed JWT whose `sub` claim is this node's MagicDNS name and whose `aud` claim is
1247 /// `audience`, suitable for presenting to a third-party relying party (e.g. AWS/GCP
1248 /// workload-identity federation). The node is the token *subject*, not the authenticator — this
1249 /// is token issuance over the Noise transport (`POST /machine/id-token`), not a login path.
1250 /// Requires the control plane to support capability version ≥ 30.
1251 pub async fn fetch_id_token(&self, audience: &str) -> Result<String, ts_control::IdTokenError> {
1252 self.runtime.fetch_id_token(audience.to_string()).await
1253 }
1254
1255 /// Publish a `TXT` DNS record for this node into the tailnet's `ts.net` zone via control's
1256 /// `/machine/set-dns` RPC — the Rust analog of Go `tailscale.com/client/tailscale`'s
1257 /// `LocalClient.SetDNS(ctx, name, value)`.
1258 ///
1259 /// `name` is the full record name (e.g. `_acme-challenge.host.tailnet.ts.net`) and `value` is
1260 /// the record value (e.g. the base64url DNS-01 digest). Like Go's `SetDNS`, this publishes a
1261 /// `TXT` record specifically — its canonical use is satisfying an ACME DNS-01 challenge so a CA
1262 /// can verify control of a `*.ts.net` name. Issuance over the Noise transport (`POST
1263 /// /machine/set-dns`), not a login path.
1264 pub async fn set_dns(&self, name: &str, value: &str) -> Result<(), ts_control::SetDnsError> {
1265 self.runtime
1266 .set_dns(name.to_string(), value.to_string())
1267 .await
1268 }
1269
1270 /// Log this node out of the tailnet — deregister it from the control plane (the equivalent of
1271 /// Go `tsnet`'s `LocalClient.Logout`).
1272 ///
1273 /// Re-`POST`s `/machine/register` with this node's current node key and a past expiry, which the
1274 /// control plane honors by **expiring the node now**: it drops out of every peer's netmap and
1275 /// must re-register (re-authenticate) to rejoin.
1276 ///
1277 /// This is primarily for **non-ephemeral** nodes. An ephemeral node is garbage-collected by
1278 /// control shortly after it disconnects, but a persistent node lingers in the tailnet
1279 /// (visible to peers, counting against the machine limit) for up to ~24h after the process exits
1280 /// unless explicitly logged out. Call this before [`shutdown`](Self::shutdown) to deregister
1281 /// immediately. Calling it on an ephemeral node simply brings the GC forward; it is idempotent,
1282 /// so logging out an already-gone node is not an error.
1283 ///
1284 /// This is a **control-plane state change only**: it does not tear down the local datapath (do
1285 /// that via [`shutdown`](Self::shutdown)), and it does not delete or rotate the on-disk node key
1286 /// — re-registering with the same key (a fresh [`Device::new`]) is the re-login path.
1287 pub async fn logout(&self) -> Result<(), ts_control::LogoutError> {
1288 self.runtime.logout().await
1289 }
1290
1291 /// Snapshot this node's client metrics in Prometheus text exposition format.
1292 ///
1293 /// Mirrors Go Tailscale's `clientmetric` registry: process-global counters/gauges incremented
1294 /// on the datapath hot loops (e.g. `magicsock_send_udp`, `magicsock_recv_data_bytes_udp`),
1295 /// rendered as `# TYPE <name> <kind>\n<name> <value>\n` per metric, sorted by name. (Go `tsnet`
1296 /// exposes no metrics method of its own, so this is the fork's clean public surface.) The
1297 /// registry is process-global, so the output covers every `Device` in the process.
1298 pub fn metrics(&self) -> String {
1299 ts_metrics::write_prometheus()
1300 }
1301
1302 /// Map a tailnet source `addr` to the node that owns its IP (like `tsnet`'s `WhoIs`).
1303 ///
1304 /// Only the IP of `addr` is used; the port is ignored. Returns `Ok(None)` if no tailnet node
1305 /// owns that address.
1306 pub async fn whois(&self, addr: SocketAddr) -> Result<Option<WhoIs>, Error> {
1307 self.runtime.whois(addr).await.map_err(Into::into)
1308 }
1309
1310 /// Change the selected exit node at runtime, without recreating the [`Device`] — the equivalent
1311 /// of Go `tsnet`'s `LocalClient.EditPrefs(ExitNodeID/ExitNodeIP)`.
1312 ///
1313 /// The peer may be named by stable node ID, tailnet IP, or MagicDNS name via
1314 /// [`ExitNodeSelector`] (a bare IP or name parses with `selector.parse()`); this is the same
1315 /// selector type as [`Config::exit_node`](crate::Config::exit_node), so the construction-time
1316 /// and runtime paths are identical. Passing `None` clears the exit node — internet-bound traffic
1317 /// is then dropped (fail-closed) unless this node egresses directly.
1318 ///
1319 /// The change is applied immediately: the new selector is re-resolved against the live peer set
1320 /// and the outbound route + inbound source filter are recomputed at once. A selector for a peer
1321 /// not yet in the netmap simply takes effect once that peer appears.
1322 ///
1323 /// Only NEW flows use the changed exit; in-flight connections are not torn down and continue
1324 /// egressing via the previously-selected exit until they close.
1325 pub async fn set_exit_node(&self, exit_node: Option<ExitNodeSelector>) -> Result<(), Error> {
1326 self.runtime
1327 .set_exit_node(exit_node)
1328 .await
1329 .map_err(Into::into)
1330 }
1331
1332 /// The currently-selected exit node, or `None` if none is selected.
1333 pub fn exit_node(&self) -> Option<ExitNodeSelector> {
1334 self.runtime.exit_node()
1335 }
1336
1337 /// Toggle whether this node accepts peer-advertised subnet routes at runtime, without recreating
1338 /// the [`Device`] — the equivalent of Go `tsnet`'s `LocalClient.EditPrefs(RouteAll)` /
1339 /// `tailscale set --accept-routes`.
1340 ///
1341 /// This is a purely **local** preference: unlike [`set_advertise_routes`](Self::set_advertise_routes)
1342 /// it is never reported to control, so it only changes which peer-advertised subnet routes *this*
1343 /// node installs. The change is applied immediately — the outbound route table and the inbound
1344 /// source filter are recomputed together against the live peer set, so turning it on installs (and
1345 /// accepts traffic from) newly-accepted subnets and turning it off removes them from both in
1346 /// lock-step. A peer's own tailnet address is always reachable regardless; the exit-node default
1347 /// route is governed by [`set_exit_node`](Self::set_exit_node), not this flag.
1348 ///
1349 /// Only NEW flows are affected; in-flight connections are not torn down. In TUN transport mode the
1350 /// netstack data path honors the toggle immediately, but the host routing table is not re-steered
1351 /// until the device is rebuilt.
1352 pub async fn set_accept_routes(&self, accept: bool) -> Result<(), Error> {
1353 self.runtime
1354 .set_accept_routes(accept)
1355 .await
1356 .map_err(Into::into)
1357 }
1358
1359 /// Whether this node currently accepts peer-advertised subnet routes (`--accept-routes`).
1360 pub fn accept_routes(&self) -> bool {
1361 self.runtime.accept_routes()
1362 }
1363
1364 /// Toggle whether this node accepts the tailnet's DNS configuration at runtime, without
1365 /// recreating the [`Device`] — the equivalent of Go `tsnet`'s `LocalClient.EditPrefs(CorpDNS)` /
1366 /// `tailscale set --accept-dns`.
1367 ///
1368 /// Like [`set_accept_routes`](Self::set_accept_routes) this is a purely **local** preference,
1369 /// never reported to control. When `false`, the MagicDNS responder ignores the control-pushed DNS
1370 /// configuration and answers every query `REFUSED` (mirroring Go applying an empty `dns.Config`
1371 /// when `CorpDNS` is off), so the node can join the tailnet for connectivity without taking over
1372 /// its DNS. The change is applied immediately to the netstack responder and the peerAPI DoH server
1373 /// that shares its view; flipping it back to `true` restores serving from the still-current config
1374 /// (the config is only gated at the read site, never destroyed), so the OFF→ON restore is
1375 /// automatic.
1376 ///
1377 /// In TUN transport mode the in-datapath responder honors the toggle immediately, but the host
1378 /// resolver/route programming (which points the host at `100.100.100.100`) is applied once at
1379 /// device build and is not re-steered until the device is rebuilt.
1380 pub async fn set_accept_dns(&self, accept: bool) -> Result<(), Error> {
1381 self.runtime
1382 .set_accept_dns(accept)
1383 .await
1384 .map_err(Into::into)
1385 }
1386
1387 /// Whether this node currently accepts the tailnet's DNS configuration (`--accept-dns` / `CorpDNS`).
1388 pub fn accept_dns(&self) -> bool {
1389 self.runtime.accept_dns()
1390 }
1391
1392 /// Change the subnet routes this node advertises at runtime — Go `tailscale set
1393 /// --advertise-routes`. This is the runtime equivalent of
1394 /// [`Config::advertise_routes`](crate::Config::advertise_routes): the node re-advertises the
1395 /// prefixes to control (so it is granted the subnet-router role for them) AND starts forwarding
1396 /// them on the data path, applied together so the two never disagree.
1397 ///
1398 /// `routes` is filtered to the IPv4-only, deduplicated set this fork honors (IPv6 prefixes are
1399 /// dropped under the IPv6-off posture). This sets the explicit subnet prefixes only; it does not
1400 /// affect the exit-node `0.0.0.0/0` advertisement. Only NEW forwarded flows use the changed set;
1401 /// in-flight flows keep their existing routing until they close.
1402 pub async fn set_advertise_routes(&self, routes: Vec<ipnet::IpNet>) -> Result<(), Error> {
1403 self.runtime
1404 .set_advertise_routes(routes)
1405 .await
1406 .map_err(Into::into)
1407 }
1408
1409 /// Advertise (or stop advertising) this node as an **exit node** at runtime — Go `tailscale set
1410 /// --advertise-exit-node`. The runtime equivalent of
1411 /// [`Config::advertise_exit_node`](crate::Config::advertise_exit_node): when `enable` it adds the
1412 /// `0.0.0.0/0` default route to what this node advertises (and forwards), when `false` it removes
1413 /// it.
1414 ///
1415 /// Composes with [`set_advertise_routes`](Device::set_advertise_routes): the explicit subnet
1416 /// routes and the exit-node advertisement are independent — toggling one preserves the other.
1417 /// Advertising an exit node only makes this node *eligible*; control + the peer still decide
1418 /// whether to route through it. Only NEW forwarded flows see the change; in-flight flows keep
1419 /// their routing.
1420 pub async fn set_advertise_exit_node(&self, enable: bool) -> Result<(), Error> {
1421 self.runtime
1422 .set_advertise_exit_node(enable)
1423 .await
1424 .map_err(Into::into)
1425 }
1426
1427 /// Change this node's hostname at runtime — Go `tailscale set --hostname`. Re-reports
1428 /// `Hostinfo.Hostname` to control on the live connection (no rebuild, no reconnect); control
1429 /// reflects the new name in the netmap (it drives the node's MagicDNS name / `tailscale status`
1430 /// display). Hostname is display metadata, so there is no data-path effect. The new value also
1431 /// persists across a later re-registration.
1432 pub async fn set_hostname(&self, hostname: String) -> Result<(), Error> {
1433 self.runtime
1434 .set_hostname(hostname)
1435 .await
1436 .map_err(Into::into)
1437 }
1438
1439 /// Re-bind the underlay UDP socket after a **network/link change** — Wi-Fi switch, sleep/wake,
1440 /// or any event that invalidates the device's local address/NAT mapping. This is the Rust
1441 /// analog of Go magicsock's `Conn.Rebind()`.
1442 ///
1443 /// The embedder owns deciding *when* to call this (it watches the OS for link changes — there is
1444 /// no built-in network monitor); `rebind` is the engine half that does the socket work:
1445 /// - Re-binds the underlay UDP socket, preferring the same local port (so the advertised
1446 /// endpoint stays stable) and falling back to an ephemeral port. The IPv4-only-by-default
1447 /// invariant is preserved.
1448 /// - Invalidates the now-stale local mapping: learned reflexive (STUN) addresses and every
1449 /// peer's *confirmed* direct path are cleared, while candidate endpoints are kept — so peers
1450 /// are re-probed over the new socket and **relay over DERP (never a direct host dial) until a
1451 /// path re-confirms**. Endpoint discovery re-runs on its normal cadence.
1452 /// - Leaves peers, control, the netmap, disco keys, and DERP connections untouched; existing
1453 /// WireGuard sessions survive (they ride whatever underlay carries them).
1454 ///
1455 /// A no-op if the underlay socket failed to bind at startup (the device is DERP-only). Existing
1456 /// connectivity is preserved on a re-bind error (the old socket is kept; the error is returned).
1457 pub async fn rebind(&self) -> Result<(), Error> {
1458 self.runtime.rebind().await.map_err(Into::into)
1459 }
1460
1461 /// The stable id of the exit node traffic is **currently** egressing through, or `None` if none
1462 /// is engaged (the equivalent of Go `tsnet`'s `Status.ExitNodeStatus.ID`).
1463 ///
1464 /// This differs from [`exit_node`](Self::exit_node), which returns the *configured* selector:
1465 /// the active exit node is the route updater's resolved, fail-closed answer. It is `None` when
1466 /// no exit node is configured, the configured selector matches no current peer, or the matched
1467 /// peer no longer advertises a default route (egress is then dropped, fail-closed). Match the id
1468 /// against [`Status::peers`](crate::Status::peers) (via [`status`](Self::status)) for details.
1469 pub fn active_exit_node(&self) -> Option<ts_control::StableNodeId> {
1470 self.runtime.active_exit_node()
1471 }
1472
1473 /// Watch for netmap changes: the returned receiver's value is the current set of peer
1474 /// [`StatusNode`]s and updates on every netmap change. This is the narrow peer-only view; for
1475 /// the unified Go-`WatchIPNBus` feed (peers + device-state + login URL in one stream) use
1476 /// [`watch_ipn_bus`](Self::watch_ipn_bus).
1477 pub async fn watch_netmap(
1478 &self,
1479 ) -> Result<tokio::sync::watch::Receiver<Vec<StatusNode>>, Error> {
1480 self.runtime.watch_netmap().await.map_err(Into::into)
1481 }
1482
1483 /// The current device connection-[`DeviceState`] (`Connecting` / `Running` / `NeedsLogin` /
1484 /// `Expired` / `Failed`).
1485 pub fn device_state(&self) -> DeviceState {
1486 self.runtime.device_state()
1487 }
1488
1489 /// Watch the device connection-[`DeviceState`], reacting push-style to control connection
1490 /// transitions instead of polling [`status`](Self::status).
1491 ///
1492 /// Returns a [`tokio::sync::watch::Receiver`]; await its
1493 /// [`changed`](tokio::sync::watch::Receiver::changed) to be woken on each transition. The
1494 /// initial value is the current state.
1495 pub fn watch_state(&self) -> tokio::sync::watch::Receiver<DeviceState> {
1496 self.runtime.watch_state()
1497 }
1498
1499 /// Subscribe to the unified IPN notification bus (Go `ipn`'s `WatchIPNBus`).
1500 ///
1501 /// Returns an [`IpnBusWatcher`]; await [`next`](IpnBusWatcher::next) to receive [`Notify`]
1502 /// events that merge device-[`DeviceState`] transitions (with the interactive-login URL surfaced
1503 /// as [`Notify::browse_to_url`]) and netmap peer-set changes into one feed — the single stream a
1504 /// consumer porting from Go's `WatchNotifications` expects, instead of composing
1505 /// [`watch_state`](Self::watch_state) and [`watch_netmap`](Self::watch_netmap) by hand. `mask`
1506 /// ([`NotifyWatchOpt`]) front-loads the current state as an initial snapshot on subscribe
1507 /// (`INITIAL_STATE` / `INITIAL_NETMAP`), mirroring Go's `NotifyInitialState` /
1508 /// `NotifyInitialNetMap`. Delivery is best-effort (a slow consumer drops notifications rather
1509 /// than stalling the runtime); the stream ends when the device shuts down.
1510 pub async fn watch_ipn_bus(&self, mask: NotifyWatchOpt) -> Result<IpnBusWatcher, Error> {
1511 self.runtime.watch_ipn_bus(mask).await.map_err(Into::into)
1512 }
1513
1514 /// Wait until the device finishes registering, returning a typed outcome — the clean
1515 /// replacement for polling [`ipv4_addr`](Self::ipv4_addr) in a loop.
1516 ///
1517 /// Resolves `Ok(())` once the device is [`DeviceState::Running`]. On a non-running outcome it
1518 /// returns a typed [`RegistrationError`]:
1519 /// - [`AuthRejected`](RegistrationError::AuthRejected) — bad/expired/unknown auth key;
1520 /// **permanent** (re-pair).
1521 /// - [`NeedsLogin`](RegistrationError::NeedsLogin) — interactive authorization required;
1522 /// **not permanent** (the runtime keeps retrying and reaches `Running` once the user
1523 /// authorizes). Auth-key callers treat this as failure; interactive callers should ignore it
1524 /// and drive the flow via [`watch_state`](Self::watch_state).
1525 /// - [`NetworkUnreachable`](RegistrationError::NetworkUnreachable) — **transient** (retry).
1526 /// - [`Timeout`](RegistrationError::Timeout) — no settled state within `timeout` (`None` waits
1527 /// indefinitely).
1528 ///
1529 /// [`KeyExpired`](RegistrationError::KeyExpired) is not produced here (a key expires only after
1530 /// the node is up); observe it via [`watch_state`](Self::watch_state). Use
1531 /// [`RegistrationError::is_permanent`] to branch "re-pair" vs. "retry / drive login".
1532 pub async fn wait_until_running(
1533 &self,
1534 timeout: Option<Duration>,
1535 ) -> Result<(), RegistrationError> {
1536 self.runtime.wait_until_running(timeout).await
1537 }
1538
1539 /// Ping a tailnet peer over the overlay with an ICMPv4 echo, returning the round-trip time
1540 /// (like `tailscale ping`).
1541 ///
1542 /// The echo is sent from this device's own tailnet IPv4 over the overlay netstack — never a
1543 /// host socket. IPv6 destinations return [`PingError::Ipv6Unsupported`] (this fork is
1544 /// IPv4-only on the tailnet). A peer answers from its own OS stack; this netstack does not
1545 /// auto-reply to echo requests.
1546 ///
1547 /// In TUN transport mode there is no application netstack to ping from; this surfaces as
1548 /// [`PingError::Timeout`] (the same error this method already uses for an unavailable source
1549 /// address — `PingError` carries no dedicated "unsupported" variant).
1550 pub async fn ping(&self, dst: IpAddr, timeout: Duration) -> Result<Duration, PingError> {
1551 let channel = self.channel().map_err(|_| PingError::Timeout)?;
1552 let src = self.ipv4_addr().await.map_err(|_| PingError::Timeout)?;
1553 ts_netstack_smoltcp::ping(channel, src, dst, timeout).await
1554 }
1555
1556 /// The current **direct path** to the peer at tailnet IP `dst`: its confirmed direct UDP
1557 /// endpoint and that path's last-measured round-trip latency, or `None` when traffic to the peer
1558 /// is **relayed via DERP** (no trusted direct path right now), the peer is unknown, or it has no
1559 /// disco key.
1560 ///
1561 /// This is the direct-path analog of Go's `tailscale ping`/`PeerStatus` connectivity: a present
1562 /// result means packets reach the peer directly at the returned address, with roughly the
1563 /// returned RTT. The latency is a live snapshot taken from the most recent disco ping/pong that
1564 /// confirmed the path (up to one probe interval stale) — not a fresh on-demand round-trip. Unlike
1565 /// [`ping`](Device::ping) (an ICMP echo over the netstack), this reports the *underlay* path the
1566 /// data plane actually uses, distinguishing a direct connection from a DERP-relayed one.
1567 pub async fn direct_path(&self, dst: IpAddr) -> Result<Option<(SocketAddr, Duration)>, Error> {
1568 self.runtime.direct_path(dst).await.map_err(Into::into)
1569 }
1570
1571 /// Send a disco ping to the peer at tailnet IP `dst` **now** and await the pong — a fresh,
1572 /// on-demand round-trip measurement (Go's `tailscale ping`, `PingType::Disco`). Returns the
1573 /// endpoint that answered and the measured RTT, or `None` if no pong arrives within `timeout`
1574 /// (or the peer is unknown / has no candidate direct path).
1575 ///
1576 /// Unlike [`direct_path`](Device::direct_path) — which reports the *last periodic probe's* RTT
1577 /// from cache — this actively sends a ping and waits for the reply, so the latency is current. A
1578 /// `None` here means "no direct path confirmed within the timeout" (the peer may still be
1579 /// reachable via DERP). Unlike [`ping`](Device::ping) (an ICMP echo over the netstack), this
1580 /// measures the disco/underlay path the data plane uses for direct connections.
1581 pub async fn ping_disco(
1582 &self,
1583 dst: IpAddr,
1584 timeout: Duration,
1585 ) -> Result<Option<(SocketAddr, Duration)>, Error> {
1586 self.runtime
1587 .ping_disco(dst, timeout)
1588 .await
1589 .map_err(Into::into)
1590 }
1591
1592 /// Obtain a TLS certificate for a node's MagicDNS `name` (like `tsnet`'s `GetCertificate`).
1593 ///
1594 /// **Fail-closed without the `acme` feature.** By default this fork has no client-side ACME
1595 /// engine wired in, so this returns [`ts_control::CertError::Unimplemented`] (after a
1596 /// tailnet-name check) — it NEVER self-signs and NEVER returns a placeholder certificate
1597 /// ([`ts_control::MISSING_CERT_RPC`] names what is missing).
1598 ///
1599 /// **With the `acme` feature** this instead drives the client-side ACME DNS-01 engine to issue a
1600 /// real Let's Encrypt certificate for `name`, publishing the challenge TXT via the node's
1601 /// `POST /machine/set-dns` RPC (routed through the control runner). SaaS-only: a self-hosted
1602 /// control plane may 501 on set-dns, surfaced as [`ts_control::CertError::Acme`].
1603 #[cfg(not(feature = "acme"))]
1604 pub async fn get_certificate(&self, name: &str) -> Result<CertifiedKey, ts_control::CertError> {
1605 ts_control::get_certificate(name).await
1606 }
1607
1608 /// See the no-`acme` variant for the contract; with `acme` this issues a real cert via the
1609 /// runtime's ACME engine (`Device → Runtime → ControlRunner → issue_certificate_via_setdns`).
1610 #[cfg(feature = "acme")]
1611 pub async fn get_certificate(&self, name: &str) -> Result<CertifiedKey, ts_control::CertError> {
1612 self.runtime.get_certificate(name.to_string()).await
1613 }
1614
1615 /// Issue a real Let's Encrypt certificate for a node's MagicDNS `name` and return the **PEM
1616 /// pair** `(cert_chain_pem, key_pem)` — the analog of Go's `LocalClient.CertPairWithValidity`,
1617 /// for writing the daemon's on-disk `.crt` + `.key` (`tnet cert`). **`acme` feature only.**
1618 ///
1619 /// This drives the same client-side ACME DNS-01 issuance as [`Device::get_certificate`] (one
1620 /// order, the challenge TXT published via the node's `POST /machine/set-dns` RPC, routed through
1621 /// the runtime → control runner); it differs only in returning the raw leaf+chain PEM and the
1622 /// leaf private-key PEM instead of the opaque [`CertifiedKey`]. The second tuple element is
1623 /// **secret key material**: it is never logged anywhere on this path — persist it to a `0600`
1624 /// file and never trace it.
1625 ///
1626 /// **`min_validity` (honest "always fresh").** Go's `CertPairWithValidity` reuses a cached cert
1627 /// when it has at least `min_validity` of its lifetime remaining, re-issuing otherwise. This
1628 /// fork keeps **no cert cache** — every call issues fresh — so `min_validity` is accepted for
1629 /// signature compatibility but does not alter behavior: a freshly issued (full-lifetime) cert
1630 /// satisfies any `min_validity`. A reuse cache is separate future work; this does NOT fake one.
1631 ///
1632 /// Fail-closed: returns a [`ts_control::CertError`] (never a self-signed or partial pair) on any
1633 /// ACME/HTTP failure. SaaS-only: a self-hosted control plane may 501 on set-dns, surfaced as
1634 /// [`ts_control::CertError::Acme`].
1635 #[cfg(feature = "acme")]
1636 pub async fn cert_pair(
1637 &self,
1638 name: &str,
1639 min_validity: Option<Duration>,
1640 ) -> Result<(String, String), ts_control::CertError> {
1641 self.runtime.cert_pair(name.to_string(), min_validity).await
1642 }
1643
1644 /// Build a [`TlsAcceptor`] terminating TLS for `cfg.name` on the overlay (like `tsnet`'s
1645 /// `ListenTLS`).
1646 ///
1647 /// Obtains the certificate via [`Device::get_certificate`] — so with the `acme` feature this
1648 /// issues a real Let's Encrypt cert (when the control plane answers `set-dns`), and without it
1649 /// (or when issuance is unavailable) it surfaces the same fail-closed
1650 /// [`ts_control::CertError`] rather than ever serving a self-signed cert or downgrading to
1651 /// plaintext. Terminate accepted overlay streams with [`ts_control::accept_tls`].
1652 pub async fn listen_tls(
1653 &self,
1654 cfg: &ts_control::ServeConfig,
1655 ) -> Result<TlsAcceptor, ts_control::CertError> {
1656 // Route through Device::get_certificate (the acme-aware issuance path) rather than
1657 // ts_control::listen_tls, which only knows the non-acme stub. Validate the serve config
1658 // first (same fail-closed checks ts_control::listen_tls applies), then assemble the acceptor.
1659 cfg.validate()?;
1660 let cert = self.get_certificate(&cfg.name).await?;
1661 ts_control::tls_acceptor(cert)
1662 }
1663
1664 /// The currently-stored Serve config (like `tsnet`'s `GetServeConfig`).
1665 ///
1666 /// Returns the config last passed to [`Device::set_serve_config`], or an empty
1667 /// [`ts_control::ServeState`] (no ports) if none was ever set. Pure read — does not touch the
1668 /// network.
1669 pub fn get_serve_config(&self) -> ts_control::ServeState {
1670 match &*self.serve.lock().unwrap_or_else(|e| e.into_inner()) {
1671 Some(mgr) => mgr.get(),
1672 None => ts_control::ServeState::default(),
1673 }
1674 }
1675
1676 /// Replace this node's Serve config and (re)bind its tailnet ports (like `tsnet`'s
1677 /// `SetServeConfig`, REPLACE semantics).
1678 ///
1679 /// `state` becomes the **whole** config (full-replace reconcile: every previously-bound serve
1680 /// port's accept loop is torn down and the new config's ports are bound from scratch). For each
1681 /// configured port the manager binds an overlay listener on this node's tailnet IPv4 and
1682 /// dispatches per [`ts_control::ServeTarget`]:
1683 /// - [`Accept`](ts_control::ServeTarget::Accept) — the TLS-terminated stream is handed back over
1684 /// the returned [`ServeAcceptedReceiver`](ts_runtime::serve::ServeAcceptedReceiver) (the
1685 /// in-process stand-in for `ListenTLS`'s `net.Listener`).
1686 /// - [`Proxy`](ts_control::ServeTarget::Proxy) — reverse-proxy the decrypted stream to a local
1687 /// host backend.
1688 /// - [`Text`](ts_control::ServeTarget::Text) — write a fixed body and close.
1689 /// - [`TcpForward`](ts_control::ServeTarget::TcpForward) — forward the **raw** (non-TLS) stream
1690 /// to a local host backend.
1691 ///
1692 /// **Fail-closed.** `state.validate()` runs first. Every TLS-terminating port's acceptor is
1693 /// obtained up-front via [`Device::listen_tls`] (the ACME-aware cert path); if any cert cannot be
1694 /// issued the whole call fails with that [`ts_control::CertError`] and **nothing is bound** — a
1695 /// TLS port never downgrades to plaintext.
1696 ///
1697 /// **Anti-leak.** Listeners bind the overlay netstack only (never a host socket). The
1698 /// `Proxy`/`TcpForward` backend dial is a local host socket to the embedder's own backend (like
1699 /// Go's reverse-proxy to `127.0.0.1`), intentionally NOT routed through the exit-egress
1700 /// forwarder. A backend dial failure drops that connection; it never falls back.
1701 ///
1702 /// Returns an error in TUN transport mode (there is no application netstack to bind on). The
1703 /// previous config's accept loops (and any earlier `ServeAcceptedReceiver`) stop when this
1704 /// returns; the new receiver delivers every `Accept`-port connection.
1705 pub async fn set_serve_config(
1706 &self,
1707 state: ts_control::ServeState,
1708 ) -> Result<ts_runtime::serve::ServeAcceptedReceiver, Error> {
1709 state
1710 .validate()
1711 .map_err(|_| Error::Internal(InternalErrorKind::BadRequest))?;
1712
1713 // Fail-closed: build every TLS-terminating port's acceptor up-front via the ACME-aware cert
1714 // path. If any cert can't be issued, return before binding anything (no plaintext downgrade).
1715 let mut resolved = std::collections::BTreeMap::new();
1716 for (port, target) in &state.ports {
1717 let acceptor = if target.terminates_tls() {
1718 let cfg = ts_control::ServeConfig {
1719 name: state.name.clone(),
1720 port: *port,
1721 target: target.clone(),
1722 };
1723 Some(self.listen_tls(&cfg).await.map_err(|_| {
1724 // Cert issuance is fail-closed in this fork; surface as a request error rather
1725 // than ever binding a plaintext TLS port.
1726 Error::Internal(InternalErrorKind::BadRequest)
1727 })?)
1728 } else {
1729 None
1730 };
1731 resolved.insert(
1732 *port,
1733 ts_runtime::serve::ResolvedPort {
1734 target: target.clone(),
1735 acceptor,
1736 },
1737 );
1738 }
1739
1740 // The manager binds the OVERLAY netstack on this node's own tailnet IPv4.
1741 let self_ipv4 = self.ipv4_addr().await?;
1742 let channel = self.channel()?.clone();
1743
1744 let mut slot = self.serve.lock().unwrap_or_else(|e| e.into_inner());
1745 let mgr =
1746 slot.get_or_insert_with(|| ts_runtime::serve::ServeManager::new(channel, self_ipv4));
1747 Ok(mgr.set(state, resolved))
1748 }
1749
1750 /// Expose a tailnet TLS service to the public internet via Tailscale Funnel (like `tsnet`'s
1751 /// `ListenFunnel`), returning a [`FunnelAcceptedReceiver`](ts_runtime::funnel::FunnelAcceptedReceiver)
1752 /// that delivers each TLS-terminated public connection.
1753 ///
1754 /// **Two fail-closed gates, then the live ingress listener.** First the node-attribute gate is
1755 /// fully enforced from this node's own capability map (mirroring Go `ipn.NodeCanFunnel` +
1756 /// `ipn.CheckFunnelPort`): the tailnet admin must have enabled HTTPS and granted the `funnel`
1757 /// node attribute, and `cfg.port` must be in the set the `funnel-ports` capability allows —
1758 /// otherwise this returns [`ts_control::FunnelError::NotAllowed`] /
1759 /// [`ts_control::FunnelError::PortNotAllowed`] before touching any cert or network. Then the
1760 /// node's `*.ts.net` certificate is obtained via the ACME-aware [`Device::get_certificate`] (the
1761 /// Funnel hostname *is* the node's MagicDNS name, so its DNS-01 cert matches); fail-closed on
1762 /// [`ts_control::FunnelError::Cert`] — no self-signed or plaintext fallback.
1763 ///
1764 /// On success a [`FunnelManager`](ts_runtime::funnel::FunnelManager) is registered: its ingress
1765 /// sink is installed into the runtime's peerAPI `/v0/ingress` slot (making that route live without
1766 /// restarting the peerAPI server), and the `HostInfo.IngressEnabled` map-request signal is set so
1767 /// control routes Funnel traffic to this node. Public Funnel bytes arrive as a relay POST to
1768 /// `/v0/ingress`, are membership-gated + `101`-hijacked into a raw stream, TLS-terminated by the
1769 /// manager, and delivered over the returned receiver.
1770 ///
1771 /// **Where the relay comes from.** The public ingress **relay + DNS mapping** that feed
1772 /// `/v0/ingress` are Tailscale infrastructure ([`ts_control::MISSING_FUNNEL_RELAY`]), provisioned
1773 /// automatically against real Tailscale SaaS with a Funnel-enabled ACL; against a self-hosted
1774 /// control plane no relay exists, so the listener is correct but never fed.
1775 ///
1776 /// Anti-leak: Funnel TLS terminates only on the overlay netstack (the hijacked ingress stream
1777 /// arrives on the overlay peerAPI listener), never a host socket; there is no self-signed or
1778 /// plaintext fallback. A new `listen_funnel` replaces the previous manager (its pump + sink tear
1779 /// down); dropping the `Device` tears it down too.
1780 pub async fn listen_funnel(
1781 &self,
1782 cfg: &ts_control::ServeConfig,
1783 opts: ts_control::FunnelOptions,
1784 ) -> Result<ts_runtime::funnel::FunnelAcceptedReceiver, ts_control::FunnelError> {
1785 // Gate 1 (fail-closed, no network): node-attribute + funnel-port access from our cap map.
1786 let me = self
1787 .self_node()
1788 .await
1789 .map_err(|_| ts_control::FunnelError::NotAllowed)?;
1790 cfg.validate()?;
1791 ts_control::funnel_access(&me, cfg.port)?;
1792
1793 // Gate 2 (fail-closed): obtain the node's `*.ts.net` cert via the ACME-aware path and build
1794 // the TLS acceptor. A cert failure surfaces as FunnelError::Cert — never a plaintext listener.
1795 let cert = self
1796 .get_certificate(&cfg.name)
1797 .await
1798 .map_err(ts_control::FunnelError::Cert)?;
1799 let acceptor = ts_control::tls_acceptor(cert).map_err(ts_control::FunnelError::Cert)?;
1800
1801 // `opts.funnel_only` (reject tailnet-internal connections) is accepted for surface stability;
1802 // the ingress data path only ever carries relay-delivered public traffic, so there is no
1803 // tailnet-internal leg on this listener to reject. Documented as a no-op here for now.
1804 let _ = opts;
1805
1806 // Build the funnel manager + its ingress sink + the hand-back receiver, install the sink into
1807 // the runtime's shared peerAPI `/v0/ingress` slot (making the route live), and flip the
1808 // IngressEnabled map signal. Hold the manager on the device so its pump/sink live as long as
1809 // the listener; replacing a prior manager tears the old one down on drop at end of scope.
1810 let (manager, sink, receiver) = ts_runtime::funnel::FunnelManager::new(acceptor);
1811 {
1812 let slot = self.runtime.funnel_ingress_slot();
1813 *slot.lock().unwrap_or_else(|e| e.into_inner()) = Some(sink);
1814 }
1815 self.runtime
1816 .ingress_active_flag()
1817 .store(true, std::sync::atomic::Ordering::Relaxed);
1818
1819 let old = {
1820 let mut held = self.funnel.lock().unwrap_or_else(|e| e.into_inner());
1821 held.replace(manager)
1822 };
1823 drop(old);
1824
1825 Ok(receiver)
1826 }
1827
1828 /// Host a Tailscale **VIP service** (`svc:<label>`) by binding an overlay listener on the
1829 /// service's control-assigned virtual IP (like `tsnet`'s `ListenService`).
1830 ///
1831 /// **Fail-closed.** Mirrors Go `tsnet.Server.ListenService`'s preconditions, enforced from this
1832 /// node's own netmap state ([`ts_control::resolve_service_listen`]): the `name` must be a valid
1833 /// `svc:<dns-label>`, this node must be **tagged** (Go `ErrUntaggedServiceHost`), and control
1834 /// must have assigned the service a VIP address on this node (delivered via the `service-host`
1835 /// node-capability — see [`ts_control::Node::service_addresses`]). Any unmet precondition
1836 /// returns a typed [`ts_control::ServiceError`] before binding anything.
1837 ///
1838 /// When all hold, this binds a [`tcp_listen`][Device::tcp_listen] on the service VIP and the
1839 /// configured `mode` port over the **overlay netstack** (never a host socket) and returns the
1840 /// listener. The netstack already accepts packets for control-assigned VIPs (they are injected
1841 /// alongside the node's own tailnet address), so the listener is reachable by tailnet peers.
1842 ///
1843 /// The `Tun`/L3 service mode is unsupported (a TODO in upstream tsnet); only TCP/HTTP modes
1844 /// (which bind the same VIP:port at the listen layer) are offered. Returns an error in TUN
1845 /// transport mode (there is no application netstack to bind on).
1846 pub async fn listen_service(
1847 &self,
1848 name: &str,
1849 mode: ts_control::ServiceMode,
1850 ) -> Result<netstack::TcpListener, ts_control::ServiceError> {
1851 let me = self
1852 .self_node()
1853 .await
1854 .map_err(|e| ts_control::ServiceError::Listen(e.to_string()))?;
1855 let listen_addr = ts_control::resolve_service_listen(&me, name, mode, self.enable_ipv6)?;
1856 self.tcp_listen(listen_addr)
1857 .await
1858 .map_err(|e| ts_control::ServiceError::Listen(e.to_string()))
1859 }
1860
1861 /// Attempt to gracefully shut down this device's runtime.
1862 ///
1863 /// Reports whether the device was fully shut down before the timeout. It is still shut
1864 /// down if it timed out, just more violently and with potential resource leaks.
1865 ///
1866 /// If `timeout` is `None`, then shutdown will never time-out.
1867 pub async fn shutdown(self, timeout: Option<Duration>) -> bool {
1868 self.runtime.graceful_shutdown(timeout).await
1869 }
1870}
1871
1872/// Command-channel-driven userspace network stack.
1873///
1874/// This is an opinionated wrapper around [smoltcp](https://docs.rs/smoltcp) that provides an
1875/// easier-to-integrate, more-portable API.
1876pub mod netstack {
1877 #[doc(inline)]
1878 pub use ts_netstack_smoltcp::netcore::Error;
1879 #[doc(inline)]
1880 pub use ts_netstack_smoltcp::netcore::InternalErrorKind;
1881 #[doc(inline)]
1882 pub use ts_netstack_smoltcp::netsock::{TcpListener, TcpStream, UdpSocket};
1883}
1884
1885/// Geneve (RFC 8926) framing for Tailscale **peer-relay** traffic. A peer that advertises
1886/// [`NodeInfo::is_peer_relay`] runs a UDP relay server; relayed disco + WireGuard frames are
1887/// Geneve-encapsulated with a VNI. This module exposes the header codec so the framing is
1888/// recognizable. NOTE: the active relay *data path* (the relay-allocation handshake +
1889/// magicsock integration) is **not yet implemented** in this fork — this is the wire-aware slice.
1890pub mod geneve {
1891 #[doc(inline)]
1892 pub use ts_packet::geneve::{
1893 GENEVE_FIXED_HEADER_LEN, GENEVE_PROTOCOL_DISCO, GENEVE_PROTOCOL_WIREGUARD, GeneveError,
1894 GeneveHeader,
1895 };
1896}
1897
1898/// Tailnet Lock (TKA) verification: the [`tka::Authority`] checks a peer's node-key signature
1899/// against the trusted-key state, mirroring Go's `tka` package. Pair with [`Device::tka_status`]
1900/// (the control-pushed head/disablement signal).
1901pub mod tka {
1902 #[doc(inline)]
1903 pub use ts_tka::{
1904 AumHash, AumKind, Authority, Key, KeyKind, NodeKeySignature, SigKind, State, TkaError,
1905 aum_hash,
1906 };
1907}
1908
1909/// Tailscale cryptographic key types.
1910pub mod keys {
1911 #[doc(inline)]
1912 pub use ts_keys::{
1913 DiscoKeyPair, DiscoPrivateKey, DiscoPublicKey, MachineKeyPair, MachinePrivateKey,
1914 MachinePublicKey, NetworkLockKeyPair, NetworkLockPrivateKey, NetworkLockPublicKey,
1915 NodeKeyPair, NodePrivateKey, NodePublicKey, NodeState, PersistState,
1916 };
1917}
1918
1919const ENV_MAGIC_VAR: &str = "TS_RS_EXPERIMENT";
1920const ENV_MAGIC_VALUE: &str = "this_is_unstable_software";
1921
1922fn check_magic_env() -> Result<(), Error> {
1923 if std::env::var(ENV_MAGIC_VAR).as_deref() != Ok(ENV_MAGIC_VALUE) {
1924 let warning = format!(
1925 "
1926check failed: set {ENV_MAGIC_VAR}={ENV_MAGIC_VALUE} to acknowledge that tailscale-rs is early-days
1927experimental software containing bugs, unvalidated cryptography, and no stability or compatibility
1928guarantees.
1929 "
1930 );
1931
1932 eprintln!("{}", warning.trim());
1933
1934 return Err(Error::UnstableEnvVar);
1935 };
1936
1937 Ok(())
1938}
1939
1940#[cfg(test)]
1941mod tests {
1942 use secrecy::ExposeSecret as _;
1943
1944 use super::*;
1945
1946 // `Device::new`/`new_with_secret` cannot be unit-tested end-to-end without a live control
1947 // server (registration). The only behavioral difference `new_with_secret` introduces over `new`
1948 // is exposing the `SecretString` to a plain `String` on the last inch; everything after is the
1949 // shared `new` path. So we assert that equivalence at the auth-key-resolution level: the secret
1950 // path must resolve to the exact same key the plain path feeds into `resolve_auth_key`.
1951 const SAMPLE_KEY: &str = "tskey-auth-koCgSLP5R811CNTRL-EXAMPLEEXAMPLEEXAMPLEEXAMPLE";
1952
1953 // The mapping `new_with_secret` applies (`Option<SecretString>` -> `Option<String>`) must be a
1954 // byte-for-byte round-trip, so the spawn arg is identical to a direct `new(config, Some(..))`.
1955 #[test]
1956 fn secret_exposes_to_identical_string() {
1957 let plain = Some(SAMPLE_KEY.to_string());
1958 let from_secret =
1959 Some(SecretString::from(SAMPLE_KEY)).map(|s| s.expose_secret().to_string());
1960 assert_eq!(from_secret, plain);
1961
1962 // `None` must pass through unchanged (so it falls back to `config.auth_key` exactly as `new`).
1963 let none_secret: Option<SecretString> = None;
1964 assert_eq!(
1965 none_secret.map(|s| s.expose_secret().to_string()),
1966 None::<String>
1967 );
1968 }
1969
1970 // End-to-end equivalence at the resolve layer: feeding the exposed secret through
1971 // `resolve_auth_key` yields the same `Option<String>` as feeding the plain string — i.e. both
1972 // constructors reach the same spawn argument, without registering against a control server.
1973 #[tokio::test]
1974 async fn new_with_secret_resolves_same_as_new() {
1975 let config = Config::default();
1976
1977 let via_plain = resolve_auth_key(&config, Some(SAMPLE_KEY.to_string()))
1978 .await
1979 .expect("plain auth key resolves");
1980
1981 let exposed = Some(SecretString::from(SAMPLE_KEY)).map(|s| s.expose_secret().to_string());
1982 let via_secret = resolve_auth_key(&config, exposed)
1983 .await
1984 .expect("secret-derived auth key resolves");
1985
1986 assert_eq!(via_plain, via_secret);
1987 // Without the `identity-federation` feature `resolve_auth_key` is a pass-through, so the
1988 // resolved key is the input verbatim; assert that too to pin the default-build behavior.
1989 #[cfg(not(feature = "identity-federation"))]
1990 assert_eq!(via_secret, Some(SAMPLE_KEY.to_string()));
1991 }
1992}