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