Skip to main content

ts_control/
lib.rs

1#![doc = include_str!("../README.md")]
2
3extern crate alloc;
4
5/// Package version of `ts_control` as reported by cargo.
6// TODO(npry): this is used to populate Hostinfo.ipn_version, which requests "long format":
7//  attach build info and whatever else that entails
8const PKG_VERSION: &str = if let Some(version) = option_env!("CARGO_PKG_VERSION") {
9    version
10} else {
11    ""
12};
13
14/// Maximum size of a control-plane RPC response body read into memory.
15///
16/// The control server is the identity trust root, but a buggy/compromised/MITM'd-past-Noise server
17/// must not be able to OOM the client by answering a small request with an unbounded streamed body.
18/// Every control RPC response (register, key-fetch, id-token, set-dns, logout, WIF) carries a small
19/// JSON payload, so 1 MiB is comfortably generous; reads use
20/// [`ResponseExt::collect_bytes_limited`](ts_http_util::ResponseExt::collect_bytes_limited) with this
21/// cap so the allocation is bounded *during* the read. (TKA-sync keeps its own larger 10 MiB bound;
22/// ACME its own 256 KiB bound — those payloads are differently sized.)
23pub(crate) const MAX_CONTROL_RESPONSE: usize = 1024 * 1024;
24
25/// Client-side ACME (Let's Encrypt) DNS-01 cert issuance engine (`acme` feature, SaaS-only).
26#[cfg(feature = "acme")]
27pub mod acme;
28#[cfg(feature = "async_tokio")]
29mod cert;
30mod config;
31mod control_dialer;
32mod derp;
33mod dial_plan;
34mod dns;
35mod expiry;
36mod hostinfo;
37#[cfg_attr(not(feature = "async_tokio"), expect(dead_code))]
38mod map_request_builder;
39mod node;
40#[cfg(feature = "async_tokio")]
41mod serve;
42mod service;
43mod ssh_policy;
44mod tka;
45#[cfg(feature = "async_tokio")]
46mod tokio;
47#[cfg(feature = "identity-federation")]
48pub mod wif;
49
50use std::fmt;
51
52#[cfg(feature = "async_tokio")]
53pub use cert::{
54    CertError, MISSING_CERT_RPC, certified_key_from_pem, get_certificate, is_tailnet_name,
55};
56#[cfg(feature = "acme")]
57pub use cert::{
58    PublishTxt, SetDnsPublisher, issue_cert_pair_via_setdns, issue_certificate_via_setdns,
59};
60#[doc(inline)]
61pub use config::{
62    Config, DEFAULT_CONTROL_SERVER, DEFAULT_PERSISTENT_KEEPALIVE, ExitProxyConfig, ExitProxyScheme,
63    LocalApi, TransportMode, TunConfig, services_hash,
64};
65pub use control_dialer::{ControlDialer, TcpDialer, complete_connection};
66pub use derp::{Map as DerpMap, Region as DerpRegion, convert_derp_map};
67pub use dial_plan::{DialCandidate, DialMode, DialPlan};
68pub use dns::{DnsConfig, ExtraRecord, Resolver as DnsResolver, ResolverTransport};
69pub use expiry::{
70    CLOCK_SKEW_EXPIRY_FLOOR_SECS, EXPIRY_TIMER_SLACK_SECS, ExpiryManager,
71    FLAG_EXPIRED_PEERS_EPOCH_UNIX, FlaggedPeer, MIN_CLOCK_DELTA_SECS, PEER_KEY_EXPIRED,
72    flag_expired_peers_epoch,
73};
74pub use node::{
75    ExitNodeSelector, Id as NodeId, Node, NodeCapMap, PeerChange, StableId as StableNodeId,
76    TailnetAddress, UserProfile, is_tailscale_ip, validate_service_name,
77};
78#[cfg(feature = "async_tokio")]
79pub use serve::{
80    FunnelError, FunnelOptions, MISSING_FUNNEL_RELAY, ServeConfig, ServeState, ServeTarget,
81    accept_tls, funnel_access, listen_funnel, listen_tls, tls_acceptor,
82};
83pub use service::{ServiceError, ServiceMode, resolve_service_listen};
84pub use ssh_policy::{
85    SshAccept, SshAction, SshConnIdentity, SshDecision, SshDenyReason, SshPolicy, SshPrincipal,
86    SshRecorderFailureAction, SshRule,
87};
88pub use tka::TkaStatus;
89pub use ts_control_serde::{
90    Endpoint, EndpointType, NODE_ATTR_SUGGEST_EXIT_NODE, TkaBootstrapRequest, TkaBootstrapResponse,
91    TkaDisableRequest, TkaDisableResponse, TkaInitBeginRequest, TkaInitBeginResponse,
92    TkaInitFinishRequest, TkaInitFinishResponse, TkaSignInfo, TkaSubmitSignatureRequest,
93    TkaSubmitSignatureResponse, TkaSyncOfferRequest, TkaSyncOfferResponse, TkaSyncSendRequest,
94    TkaSyncSendResponse, UserId,
95};
96#[cfg(feature = "identity-federation")]
97pub use wif::{WifConfig, WifError, resolve_auth_key};
98
99/// Re-exported TLS types from the `tokio-rustls`/`ring` stack used by `cert`/`serve`, so
100/// embedders can name [`get_certificate`]/[`listen_tls`] return types without taking their own
101/// direct `tokio-rustls` dependency (and risking a second, mismatched crypto provider).
102#[cfg(feature = "async_tokio")]
103pub mod tls {
104    pub use tokio_rustls::{TlsAcceptor, rustls::sign::CertifiedKey, server::TlsStream};
105}
106
107#[cfg(feature = "async_tokio")]
108pub use crate::tokio::{
109    AsyncControlClient, FilterUpdate, IdTokenError, LogoutError, LogoutInternalErrorKind,
110    NETMAP_CACHE_FILE, NODE_ATTR_CACHE_NETWORK_MAPS, NODE_ATTR_DISABLE_CACHE_NETWORK_MAPS,
111    NetmapCache, PeerUpdate, SetDnsError, SetDnsInternalErrorKind, StateUpdate,
112    TKA_CHAIN_CACHE_FILE, TkaSyncError, TkaSyncInternalErrorKind, fetch_id_token, logout,
113    netmap_caching_enabled, set_dns, tka_bootstrap, tka_disable, tka_init_begin, tka_init_finish,
114    tka_submit_signature, tka_sync_offer, tka_sync_send,
115};
116
117/// An error which occurred while connecting to the control server or control plane.
118#[derive(Debug, thiserror::Error, Clone, Eq, PartialEq)]
119pub enum Error {
120    /// A machine was not authorized by control to join tailnet; authorize via the supplied URL.
121    #[error("machine was not authorized by control to join tailnet, authorize at {0}")]
122    MachineNotAuthorized(url::Url),
123
124    /// A machine is not yet authorized and control offered **no** interactive auth URL — it is
125    /// awaiting admin approval on an approval-gated tailnet. **Transient and recoverable**: the node
126    /// holds a valid key and must poll-and-retry registration until an admin approves, then it comes
127    /// up with no re-registration (Go's `ipn.State::NeedsMachineAuth` → `Starting` auto-transition).
128    /// Distinct from [`Internal`](Self::Internal)`(MachineAuthorization, _)` so the control runner can
129    /// tell "awaiting approval" (poll) apart from a hard internal failure (stop), and from
130    /// [`MachineNotAuthorized`](Self::MachineNotAuthorized) which carries a URL for interactive login.
131    #[error("machine awaiting admin approval to join tailnet (no interactive auth URL)")]
132    NeedsMachineAuth,
133
134    /// The user supplied an invalid URL.
135    #[error("invalid URL: {0}")]
136    InvalidUrl(url::Url),
137
138    /// Control rejected registration with a specific reason (e.g. a bad/expired/unknown auth key).
139    /// The string is control's verbatim `RegisterResponse.Error` message.
140    #[error("control rejected registration: {0}")]
141    Registration(String),
142
143    /// Control rate-limited us (HTTP 429). The [`Duration`](core::time::Duration) is the
144    /// server-requested cooldown (from `Retry-After`); the retry loop waits exactly this before the
145    /// next attempt rather than its own backoff, so we never re-hit control inside the cooldown.
146    #[error("control rate limited the request; retry after {0:?}")]
147    RateLimited(core::time::Duration),
148
149    /// Some kind of networking error.
150    ///
151    /// These might be addressed by retrying, or might be an unresolvable error.
152    ///
153    /// [`Operation`] is intended to be informational, rather then inspected during handling.
154    #[error("a networking error occurred in {0}")]
155    NetworkError(Operation),
156
157    /// An internal error that users of the library are not expected to handle.
158    ///
159    /// [`InternalErrorKind`] and [`Operation`] are intended to be informational, rather then
160    /// inspected during handling.
161    #[error("{0} error in {1}")]
162    Internal(InternalErrorKind, Operation),
163}
164
165impl Error {
166    fn io_error(err: std::io::Error, op: Operation) -> Self {
167        if crate::is_network_error(&err) {
168            Error::NetworkError(op)
169        } else {
170            Error::Internal(InternalErrorKind::Io, op)
171        }
172    }
173}
174
175/// What kind of internal error has occurred.
176///
177/// This is intended to be useful for reporting a crash to an end user, rather than being handled.
178#[non_exhaustive]
179#[derive(Debug, Clone, Copy, Eq, PartialEq)]
180pub enum InternalErrorKind {
181    /// An error in URL parsing.
182    Url,
183    /// An unsuccessful HTTP request or upgrade.
184    Http,
185    /// An error in serialization or deserialization.
186    SerDe,
187    /// An error in I/O.
188    Io,
189    /// An invalid message format.
190    MessageFormat,
191    /// An error parsing a string as UTF8.
192    Utf8,
193    /// Noise framework handshake.
194    NoiseHandshake,
195    /// Tailscale challenge packet.
196    Challenge,
197    /// The user's machine was not authorized to register with a Tailnet and there is no URL for
198    /// the user to authorize at.
199    MachineAuthorization,
200}
201
202impl fmt::Display for InternalErrorKind {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        match self {
205            InternalErrorKind::Url => write!(f, "URL parsing error"),
206            InternalErrorKind::Http => write!(f, "unsuccessful HTTP request or upgrade"),
207            InternalErrorKind::SerDe => write!(f, "serialization/deserialization error"),
208            InternalErrorKind::Io => write!(f, "I/O error"),
209            InternalErrorKind::MessageFormat => write!(f, "message format error"),
210            InternalErrorKind::Utf8 => write!(f, "invalid UTF8"),
211            InternalErrorKind::NoiseHandshake => write!(f, "error in Noise handshake"),
212            InternalErrorKind::Challenge => write!(f, "error with Tailscale challenge packet"),
213            InternalErrorKind::MachineAuthorization => {
214                write!(f, "machine not authorized to register with Tailnet")
215            }
216        }
217    }
218}
219
220/// The phase of connecting the control plane to a Tailnet in which an error occurs.
221#[derive(Debug, Clone, Copy, Eq, PartialEq)]
222pub enum Operation {
223    /// Requesting a net map.
224    MapRequest,
225    /// Connecting to a control server.
226    ConnectToControlServer,
227    /// Registering the user's device with a Tailnet.
228    Registration,
229}
230
231impl fmt::Display for Operation {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        match self {
234            Operation::MapRequest => write!(f, "net map request"),
235            Operation::ConnectToControlServer => write!(f, "connection to control server"),
236            Operation::Registration => write!(f, "registration"),
237        }
238    }
239}
240
241impl From<ts_http_util::Error> for Error {
242    fn from(error: ts_http_util::Error) -> Self {
243        tracing::error!(%error, "http error");
244
245        if http_error_is_recoverable(error) {
246            Error::NetworkError(Operation::ConnectToControlServer)
247        } else {
248            Error::Internal(InternalErrorKind::Http, Operation::ConnectToControlServer)
249        }
250    }
251}
252
253/// Returns true if the input io error should be classed as a network error.
254fn is_network_error(err: &std::io::Error) -> bool {
255    use std::io::ErrorKind::*;
256    matches!(
257        err.kind(),
258        ConnectionRefused
259            | ConnectionReset
260            | HostUnreachable
261            | NetworkUnreachable
262            | ConnectionAborted
263            | NotConnected
264            | TimedOut
265            | AddrNotAvailable
266            | Interrupted
267            | NetworkDown
268    )
269}
270
271/// Returns true if the error is likely to be a transient network error.
272fn http_error_is_recoverable(error: ts_http_util::Error) -> bool {
273    match error {
274        ts_http_util::Error::Io => true,
275        ts_http_util::Error::InvalidInput
276        // A TCP timeout (recoverable) should get classed as an IO error, so any other kind of
277        // timeout is probably not.
278        | ts_http_util::Error::Timeout
279        | ts_http_util::Error::InvalidResponse
280        // A peer that streamed an over-cap body is an attack/misconfig signal, not a transient
281        // blip — terminal, do not retry.
282        | ts_http_util::Error::BodyTooLarge => false,
283        // In the future, this might be recoverable with a reset.
284        ts_http_util::Error::ConnectionClosed => false,
285    }
286}