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