#![doc = include_str!("../README.md")]
extern crate alloc;
const PKG_VERSION: &str = if let Some(version) = option_env!("CARGO_PKG_VERSION") {
version
} else {
""
};
#[cfg(feature = "acme")]
pub mod acme;
#[cfg(feature = "async_tokio")]
mod cert;
mod config;
mod control_dialer;
mod derp;
mod dial_plan;
mod dns;
#[cfg_attr(not(feature = "async_tokio"), expect(dead_code))]
mod map_request_builder;
mod node;
#[cfg(feature = "async_tokio")]
mod serve;
mod service;
mod ssh_policy;
mod tka;
#[cfg(feature = "async_tokio")]
mod tokio;
#[cfg(feature = "identity-federation")]
pub mod wif;
use std::fmt;
#[cfg(feature = "async_tokio")]
pub use cert::{
CertError, MISSING_CERT_RPC, certified_key_from_pem, get_certificate, is_tailnet_name,
};
#[cfg(feature = "acme")]
pub use cert::{PublishTxt, SetDnsPublisher, issue_certificate_via_setdns};
#[doc(inline)]
pub use config::{
Config, DEFAULT_CONTROL_SERVER, DEFAULT_PERSISTENT_KEEPALIVE, ExitProxyConfig, ExitProxyScheme,
TransportMode, TunConfig, services_hash,
};
pub use control_dialer::{ControlDialer, TcpDialer, complete_connection};
pub use derp::{Map as DerpMap, Region as DerpRegion, convert_derp_map};
pub use dial_plan::{DialCandidate, DialMode, DialPlan};
pub use dns::{DnsConfig, ExtraRecord, Resolver as DnsResolver, ResolverTransport};
pub use node::{
ExitNodeSelector, Id as NodeId, Node, NodeCapMap, PeerChange, StableId as StableNodeId,
TailnetAddress, UserProfile, is_tailscale_ip, validate_service_name,
};
#[cfg(feature = "async_tokio")]
pub use serve::{
FunnelError, FunnelOptions, MISSING_FUNNEL_RELAY, ServeConfig, ServeState, ServeTarget,
accept_tls, funnel_access, listen_funnel, listen_tls, tls_acceptor,
};
pub use service::{ServiceError, ServiceMode, resolve_service_listen};
pub use ssh_policy::{
SshAccept, SshAction, SshConnIdentity, SshDecision, SshDenyReason, SshPolicy, SshPrincipal,
SshRule,
};
pub use tka::TkaStatus;
pub use ts_control_serde::{
Endpoint, EndpointType, TkaBootstrapRequest, TkaBootstrapResponse, TkaSyncOfferRequest,
TkaSyncOfferResponse, TkaSyncSendRequest, TkaSyncSendResponse, UserId,
};
#[cfg(feature = "identity-federation")]
pub use wif::{WifConfig, WifError, resolve_auth_key};
#[cfg(feature = "async_tokio")]
pub mod tls {
pub use tokio_rustls::{TlsAcceptor, rustls::sign::CertifiedKey, server::TlsStream};
}
#[cfg(feature = "async_tokio")]
pub use crate::tokio::{
AsyncControlClient, FilterUpdate, IdTokenError, LogoutError, LogoutInternalErrorKind,
PeerUpdate, SetDnsError, SetDnsInternalErrorKind, StateUpdate, TkaSyncError,
TkaSyncInternalErrorKind, fetch_id_token, logout, set_dns, tka_bootstrap, tka_sync_offer,
tka_sync_send,
};
#[derive(Debug, thiserror::Error, Clone, Eq, PartialEq)]
pub enum Error {
#[error("machine was not authorized by control to join tailnet, authorize at {0}")]
MachineNotAuthorized(url::Url),
#[error("invalid URL: {0}")]
InvalidUrl(url::Url),
#[error("control rejected registration: {0}")]
Registration(String),
#[error("a networking error occurred in {0}")]
NetworkError(Operation),
#[error("{0} error in {1}")]
Internal(InternalErrorKind, Operation),
}
impl Error {
fn io_error(err: std::io::Error, op: Operation) -> Self {
if crate::is_network_error(&err) {
Error::NetworkError(op)
} else {
Error::Internal(InternalErrorKind::Io, op)
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum InternalErrorKind {
Url,
Http,
SerDe,
Io,
MessageFormat,
Utf8,
NoiseHandshake,
Challenge,
MachineAuthorization,
}
impl fmt::Display for InternalErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InternalErrorKind::Url => write!(f, "URL parsing error"),
InternalErrorKind::Http => write!(f, "unsuccessful HTTP request or upgrade"),
InternalErrorKind::SerDe => write!(f, "serialization/deserialization error"),
InternalErrorKind::Io => write!(f, "I/O error"),
InternalErrorKind::MessageFormat => write!(f, "message format error"),
InternalErrorKind::Utf8 => write!(f, "invalid UTF8"),
InternalErrorKind::NoiseHandshake => write!(f, "error in Noise handshake"),
InternalErrorKind::Challenge => write!(f, "error with Tailscale challenge packet"),
InternalErrorKind::MachineAuthorization => {
write!(f, "machine not authorized to register with Tailnet")
}
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Operation {
MapRequest,
ConnectToControlServer,
Registration,
}
impl fmt::Display for Operation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Operation::MapRequest => write!(f, "net map request"),
Operation::ConnectToControlServer => write!(f, "connection to control server"),
Operation::Registration => write!(f, "registration"),
}
}
}
impl From<ts_http_util::Error> for Error {
fn from(error: ts_http_util::Error) -> Self {
tracing::error!(%error, "http error");
if http_error_is_recoverable(error) {
Error::NetworkError(Operation::ConnectToControlServer)
} else {
Error::Internal(InternalErrorKind::Http, Operation::ConnectToControlServer)
}
}
}
fn is_network_error(err: &std::io::Error) -> bool {
use std::io::ErrorKind::*;
matches!(
err.kind(),
ConnectionRefused
| ConnectionReset
| HostUnreachable
| NetworkUnreachable
| ConnectionAborted
| NotConnected
| TimedOut
| AddrNotAvailable
| Interrupted
| NetworkDown
)
}
fn http_error_is_recoverable(error: ts_http_util::Error) -> bool {
match error {
ts_http_util::Error::Io => true,
ts_http_util::Error::InvalidInput
| ts_http_util::Error::Timeout
| ts_http_util::Error::InvalidResponse => false,
ts_http_util::Error::ConnectionClosed => false,
}
}