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;
35#[cfg_attr(not(feature = "async_tokio"), expect(dead_code))]
36mod map_request_builder;
37mod node;
38#[cfg(feature = "async_tokio")]
39mod serve;
40mod service;
41mod ssh_policy;
42mod tka;
43#[cfg(feature = "async_tokio")]
44mod tokio;
45#[cfg(feature = "identity-federation")]
46pub mod wif;
47
48use std::fmt;
49
50#[cfg(feature = "async_tokio")]
51pub use cert::{
52    CertError, MISSING_CERT_RPC, certified_key_from_pem, get_certificate, is_tailnet_name,
53};
54#[cfg(feature = "acme")]
55pub use cert::{
56    PublishTxt, SetDnsPublisher, issue_cert_pair_via_setdns, issue_certificate_via_setdns,
57};
58#[doc(inline)]
59pub use config::{
60    Config, DEFAULT_CONTROL_SERVER, DEFAULT_PERSISTENT_KEEPALIVE, ExitProxyConfig, ExitProxyScheme,
61    TransportMode, TunConfig, services_hash,
62};
63pub use control_dialer::{ControlDialer, TcpDialer, complete_connection};
64pub use derp::{Map as DerpMap, Region as DerpRegion, convert_derp_map};
65pub use dial_plan::{DialCandidate, DialMode, DialPlan};
66pub use dns::{DnsConfig, ExtraRecord, Resolver as DnsResolver, ResolverTransport};
67pub use node::{
68    ExitNodeSelector, Id as NodeId, Node, NodeCapMap, PeerChange, StableId as StableNodeId,
69    TailnetAddress, UserProfile, is_tailscale_ip, validate_service_name,
70};
71#[cfg(feature = "async_tokio")]
72pub use serve::{
73    FunnelError, FunnelOptions, MISSING_FUNNEL_RELAY, ServeConfig, ServeState, ServeTarget,
74    accept_tls, funnel_access, listen_funnel, listen_tls, tls_acceptor,
75};
76pub use service::{ServiceError, ServiceMode, resolve_service_listen};
77pub use ssh_policy::{
78    SshAccept, SshAction, SshConnIdentity, SshDecision, SshDenyReason, SshPolicy, SshPrincipal,
79    SshRule,
80};
81pub use tka::TkaStatus;
82pub use ts_control_serde::{
83    Endpoint, EndpointType, TkaBootstrapRequest, TkaBootstrapResponse, TkaSyncOfferRequest,
84    TkaSyncOfferResponse, TkaSyncSendRequest, TkaSyncSendResponse, UserId,
85};
86#[cfg(feature = "identity-federation")]
87pub use wif::{WifConfig, WifError, resolve_auth_key};
88
89/// Re-exported TLS types from the `tokio-rustls`/`ring` stack used by `cert`/`serve`, so
90/// embedders can name [`get_certificate`]/[`listen_tls`] return types without taking their own
91/// direct `tokio-rustls` dependency (and risking a second, mismatched crypto provider).
92#[cfg(feature = "async_tokio")]
93pub mod tls {
94    pub use tokio_rustls::{TlsAcceptor, rustls::sign::CertifiedKey, server::TlsStream};
95}
96
97#[cfg(feature = "async_tokio")]
98pub use crate::tokio::{
99    AsyncControlClient, FilterUpdate, IdTokenError, LogoutError, LogoutInternalErrorKind,
100    PeerUpdate, SetDnsError, SetDnsInternalErrorKind, StateUpdate, TkaSyncError,
101    TkaSyncInternalErrorKind, fetch_id_token, logout, set_dns, tka_bootstrap, tka_sync_offer,
102    tka_sync_send,
103};
104
105/// An error which occurred while connecting to the control server or control plane.
106#[derive(Debug, thiserror::Error, Clone, Eq, PartialEq)]
107pub enum Error {
108    /// A machine was not authorized by control to join tailnet; authorize via the supplied URL.
109    #[error("machine was not authorized by control to join tailnet, authorize at {0}")]
110    MachineNotAuthorized(url::Url),
111
112    /// The user supplied an invalid URL.
113    #[error("invalid URL: {0}")]
114    InvalidUrl(url::Url),
115
116    /// Control rejected registration with a specific reason (e.g. a bad/expired/unknown auth key).
117    /// The string is control's verbatim `RegisterResponse.Error` message.
118    #[error("control rejected registration: {0}")]
119    Registration(String),
120
121    /// Some kind of networking error.
122    ///
123    /// These might be addressed by retrying, or might be an unresolvable error.
124    ///
125    /// [`Operation`] is intended to be informational, rather then inspected during handling.
126    #[error("a networking error occurred in {0}")]
127    NetworkError(Operation),
128
129    /// An internal error that users of the library are not expected to handle.
130    ///
131    /// [`InternalErrorKind`] and [`Operation`] are intended to be informational, rather then
132    /// inspected during handling.
133    #[error("{0} error in {1}")]
134    Internal(InternalErrorKind, Operation),
135}
136
137impl Error {
138    fn io_error(err: std::io::Error, op: Operation) -> Self {
139        if crate::is_network_error(&err) {
140            Error::NetworkError(op)
141        } else {
142            Error::Internal(InternalErrorKind::Io, op)
143        }
144    }
145}
146
147/// What kind of internal error has occurred.
148///
149/// This is intended to be useful for reporting a crash to an end user, rather than being handled.
150#[non_exhaustive]
151#[derive(Debug, Clone, Copy, Eq, PartialEq)]
152pub enum InternalErrorKind {
153    /// An error in URL parsing.
154    Url,
155    /// An unsuccessful HTTP request or upgrade.
156    Http,
157    /// An error in serialization or deserialization.
158    SerDe,
159    /// An error in I/O.
160    Io,
161    /// An invalid message format.
162    MessageFormat,
163    /// An error parsing a string as UTF8.
164    Utf8,
165    /// Noise framework handshake.
166    NoiseHandshake,
167    /// Tailscale challenge packet.
168    Challenge,
169    /// The user's machine was not authorized to register with a Tailnet and there is no URL for
170    /// the user to authorize at.
171    MachineAuthorization,
172}
173
174impl fmt::Display for InternalErrorKind {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        match self {
177            InternalErrorKind::Url => write!(f, "URL parsing error"),
178            InternalErrorKind::Http => write!(f, "unsuccessful HTTP request or upgrade"),
179            InternalErrorKind::SerDe => write!(f, "serialization/deserialization error"),
180            InternalErrorKind::Io => write!(f, "I/O error"),
181            InternalErrorKind::MessageFormat => write!(f, "message format error"),
182            InternalErrorKind::Utf8 => write!(f, "invalid UTF8"),
183            InternalErrorKind::NoiseHandshake => write!(f, "error in Noise handshake"),
184            InternalErrorKind::Challenge => write!(f, "error with Tailscale challenge packet"),
185            InternalErrorKind::MachineAuthorization => {
186                write!(f, "machine not authorized to register with Tailnet")
187            }
188        }
189    }
190}
191
192/// The phase of connecting the control plane to a Tailnet in which an error occurs.
193#[derive(Debug, Clone, Copy, Eq, PartialEq)]
194pub enum Operation {
195    /// Requesting a net map.
196    MapRequest,
197    /// Connecting to a control server.
198    ConnectToControlServer,
199    /// Registering the user's device with a Tailnet.
200    Registration,
201}
202
203impl fmt::Display for Operation {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        match self {
206            Operation::MapRequest => write!(f, "net map request"),
207            Operation::ConnectToControlServer => write!(f, "connection to control server"),
208            Operation::Registration => write!(f, "registration"),
209        }
210    }
211}
212
213impl From<ts_http_util::Error> for Error {
214    fn from(error: ts_http_util::Error) -> Self {
215        tracing::error!(%error, "http error");
216
217        if http_error_is_recoverable(error) {
218            Error::NetworkError(Operation::ConnectToControlServer)
219        } else {
220            Error::Internal(InternalErrorKind::Http, Operation::ConnectToControlServer)
221        }
222    }
223}
224
225/// Returns true if the input io error should be classed as a network error.
226fn is_network_error(err: &std::io::Error) -> bool {
227    use std::io::ErrorKind::*;
228    matches!(
229        err.kind(),
230        ConnectionRefused
231            | ConnectionReset
232            | HostUnreachable
233            | NetworkUnreachable
234            | ConnectionAborted
235            | NotConnected
236            | TimedOut
237            | AddrNotAvailable
238            | Interrupted
239            | NetworkDown
240    )
241}
242
243/// Returns true if the error is likely to be a transient network error.
244fn http_error_is_recoverable(error: ts_http_util::Error) -> bool {
245    match error {
246        ts_http_util::Error::Io => true,
247        ts_http_util::Error::InvalidInput
248        // A TCP timeout (recoverable) should get classed as an IO error, so any other kind of
249        // timeout is probably not.
250        | ts_http_util::Error::Timeout
251        | ts_http_util::Error::InvalidResponse
252        // A peer that streamed an over-cap body is an attack/misconfig signal, not a transient
253        // blip — terminal, do not retry.
254        | ts_http_util::Error::BodyTooLarge => false,
255        // In the future, this might be recoverable with a reset.
256        ts_http_util::Error::ConnectionClosed => false,
257    }
258}