1#![doc = include_str!("../README.md")]
2
3extern crate alloc;
4
5const PKG_VERSION: &str = if let Some(version) = option_env!("CARGO_PKG_VERSION") {
9 version
10} else {
11 ""
12};
13
14pub(crate) const MAX_CONTROL_RESPONSE: usize = 1024 * 1024;
24
25#[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#[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#[derive(Debug, thiserror::Error, Clone, Eq, PartialEq)]
109pub enum Error {
110 #[error("machine was not authorized by control to join tailnet, authorize at {0}")]
112 MachineNotAuthorized(url::Url),
113
114 #[error("invalid URL: {0}")]
116 InvalidUrl(url::Url),
117
118 #[error("control rejected registration: {0}")]
121 Registration(String),
122
123 #[error("a networking error occurred in {0}")]
129 NetworkError(Operation),
130
131 #[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#[non_exhaustive]
153#[derive(Debug, Clone, Copy, Eq, PartialEq)]
154pub enum InternalErrorKind {
155 Url,
157 Http,
159 SerDe,
161 Io,
163 MessageFormat,
165 Utf8,
167 NoiseHandshake,
169 Challenge,
171 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#[derive(Debug, Clone, Copy, Eq, PartialEq)]
196pub enum Operation {
197 MapRequest,
199 ConnectToControlServer,
201 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
227fn 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
245fn 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 | ts_http_util::Error::Timeout
253 | ts_http_util::Error::InvalidResponse
254 | ts_http_util::Error::BodyTooLarge => false,
257 ts_http_util::Error::ConnectionClosed => false,
259 }
260}