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, TkaDisableRequest,
84    TkaDisableResponse, TkaInitBeginRequest, TkaInitBeginResponse, TkaInitFinishRequest,
85    TkaInitFinishResponse, TkaSignInfo, TkaSubmitSignatureRequest, TkaSubmitSignatureResponse,
86    TkaSyncOfferRequest, TkaSyncOfferResponse, TkaSyncSendRequest, TkaSyncSendResponse, UserId,
87};
88#[cfg(feature = "identity-federation")]
89pub use wif::{WifConfig, WifError, resolve_auth_key};
90
91/// Re-exported TLS types from the `tokio-rustls`/`ring` stack used by `cert`/`serve`, so
92/// embedders can name [`get_certificate`]/[`listen_tls`] return types without taking their own
93/// direct `tokio-rustls` dependency (and risking a second, mismatched crypto provider).
94#[cfg(feature = "async_tokio")]
95pub mod tls {
96    pub use tokio_rustls::{TlsAcceptor, rustls::sign::CertifiedKey, server::TlsStream};
97}
98
99#[cfg(feature = "async_tokio")]
100pub use crate::tokio::{
101    AsyncControlClient, FilterUpdate, IdTokenError, LogoutError, LogoutInternalErrorKind,
102    PeerUpdate, SetDnsError, SetDnsInternalErrorKind, StateUpdate, TkaSyncError,
103    TkaSyncInternalErrorKind, fetch_id_token, logout, set_dns, tka_bootstrap, tka_disable,
104    tka_init_begin, tka_init_finish, tka_submit_signature, tka_sync_offer, tka_sync_send,
105};
106
107/// An error which occurred while connecting to the control server or control plane.
108#[derive(Debug, thiserror::Error, Clone, Eq, PartialEq)]
109pub enum Error {
110    /// A machine was not authorized by control to join tailnet; authorize via the supplied URL.
111    #[error("machine was not authorized by control to join tailnet, authorize at {0}")]
112    MachineNotAuthorized(url::Url),
113
114    /// The user supplied an invalid URL.
115    #[error("invalid URL: {0}")]
116    InvalidUrl(url::Url),
117
118    /// Control rejected registration with a specific reason (e.g. a bad/expired/unknown auth key).
119    /// The string is control's verbatim `RegisterResponse.Error` message.
120    #[error("control rejected registration: {0}")]
121    Registration(String),
122
123    /// Some kind of networking error.
124    ///
125    /// These might be addressed by retrying, or might be an unresolvable error.
126    ///
127    /// [`Operation`] is intended to be informational, rather then inspected during handling.
128    #[error("a networking error occurred in {0}")]
129    NetworkError(Operation),
130
131    /// An internal error that users of the library are not expected to handle.
132    ///
133    /// [`InternalErrorKind`] and [`Operation`] are intended to be informational, rather then
134    /// inspected during handling.
135    #[error("{0} error in {1}")]
136    Internal(InternalErrorKind, Operation),
137}
138
139impl Error {
140    fn io_error(err: std::io::Error, op: Operation) -> Self {
141        if crate::is_network_error(&err) {
142            Error::NetworkError(op)
143        } else {
144            Error::Internal(InternalErrorKind::Io, op)
145        }
146    }
147}
148
149/// What kind of internal error has occurred.
150///
151/// This is intended to be useful for reporting a crash to an end user, rather than being handled.
152#[non_exhaustive]
153#[derive(Debug, Clone, Copy, Eq, PartialEq)]
154pub enum InternalErrorKind {
155    /// An error in URL parsing.
156    Url,
157    /// An unsuccessful HTTP request or upgrade.
158    Http,
159    /// An error in serialization or deserialization.
160    SerDe,
161    /// An error in I/O.
162    Io,
163    /// An invalid message format.
164    MessageFormat,
165    /// An error parsing a string as UTF8.
166    Utf8,
167    /// Noise framework handshake.
168    NoiseHandshake,
169    /// Tailscale challenge packet.
170    Challenge,
171    /// The user's machine was not authorized to register with a Tailnet and there is no URL for
172    /// the user to authorize at.
173    MachineAuthorization,
174}
175
176impl fmt::Display for InternalErrorKind {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        match self {
179            InternalErrorKind::Url => write!(f, "URL parsing error"),
180            InternalErrorKind::Http => write!(f, "unsuccessful HTTP request or upgrade"),
181            InternalErrorKind::SerDe => write!(f, "serialization/deserialization error"),
182            InternalErrorKind::Io => write!(f, "I/O error"),
183            InternalErrorKind::MessageFormat => write!(f, "message format error"),
184            InternalErrorKind::Utf8 => write!(f, "invalid UTF8"),
185            InternalErrorKind::NoiseHandshake => write!(f, "error in Noise handshake"),
186            InternalErrorKind::Challenge => write!(f, "error with Tailscale challenge packet"),
187            InternalErrorKind::MachineAuthorization => {
188                write!(f, "machine not authorized to register with Tailnet")
189            }
190        }
191    }
192}
193
194/// The phase of connecting the control plane to a Tailnet in which an error occurs.
195#[derive(Debug, Clone, Copy, Eq, PartialEq)]
196pub enum Operation {
197    /// Requesting a net map.
198    MapRequest,
199    /// Connecting to a control server.
200    ConnectToControlServer,
201    /// Registering the user's device with a Tailnet.
202    Registration,
203}
204
205impl fmt::Display for Operation {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        match self {
208            Operation::MapRequest => write!(f, "net map request"),
209            Operation::ConnectToControlServer => write!(f, "connection to control server"),
210            Operation::Registration => write!(f, "registration"),
211        }
212    }
213}
214
215impl From<ts_http_util::Error> for Error {
216    fn from(error: ts_http_util::Error) -> Self {
217        tracing::error!(%error, "http error");
218
219        if http_error_is_recoverable(error) {
220            Error::NetworkError(Operation::ConnectToControlServer)
221        } else {
222            Error::Internal(InternalErrorKind::Http, Operation::ConnectToControlServer)
223        }
224    }
225}
226
227/// Returns true if the input io error should be classed as a network error.
228fn is_network_error(err: &std::io::Error) -> bool {
229    use std::io::ErrorKind::*;
230    matches!(
231        err.kind(),
232        ConnectionRefused
233            | ConnectionReset
234            | HostUnreachable
235            | NetworkUnreachable
236            | ConnectionAborted
237            | NotConnected
238            | TimedOut
239            | AddrNotAvailable
240            | Interrupted
241            | NetworkDown
242    )
243}
244
245/// Returns true if the error is likely to be a transient network error.
246fn http_error_is_recoverable(error: ts_http_util::Error) -> bool {
247    match error {
248        ts_http_util::Error::Io => true,
249        ts_http_util::Error::InvalidInput
250        // A TCP timeout (recoverable) should get classed as an IO error, so any other kind of
251        // timeout is probably not.
252        | ts_http_util::Error::Timeout
253        | ts_http_util::Error::InvalidResponse
254        // A peer that streamed an over-cap body is an attack/misconfig signal, not a transient
255        // blip — terminal, do not retry.
256        | ts_http_util::Error::BodyTooLarge => false,
257        // In the future, this might be recoverable with a reset.
258        ts_http_util::Error::ConnectionClosed => false,
259    }
260}