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