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