diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e64f1b6..fd9fe94c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ Records breaking or significant changes here. All dates are UTC. from the crate root to these modules. Replaces `load_key_file` with `Config::default_with_key_file`. Exports a few more types so fewer users will have to depend on internal crates. [#105](https://github.com/tailscale/tailscale-rs/pull/105). +- **Breaking** (Rust API, ts_netstack_smoltcp, ts_control): errors have been refactored, some minor + changes to APIs around errors. + [#154](https://github.com/tailscale/tailscale-rs/pull/154). +- Added (Rust API): load configuration options from environment variables. Adds `config::auth_key_from_env` + and `config::Config::default_from_env`. + [#97](https://github.com/tailscale/tailscale-rs/pull/97). +- Added (Rust API, Python, Elixir): `Device::self_node`. + [#147](https://github.com/tailscale/tailscale-rs/pull/147). +- Added (Python and Elixir bindings): optional configuration parameters. + [#140](https://github.com/tailscale/tailscale-rs/pull/140) and [#148](https://github.com/tailscale/tailscale-rs/pull/148). +- Fixed (ts_netstack_smoltcp): big improvement to TCP accept performance. + [#141](https://github.com/tailscale/tailscale-rs/pull/141). ## [0.2](https://github.com/tailscale/tailscale-rs/releases/tag/v0.2.0) - 2026-04-15 diff --git a/src/config.rs b/src/config.rs index d630fccd..9ceec204 100644 --- a/src/config.rs +++ b/src/config.rs @@ -156,7 +156,7 @@ where .await .map_err(|e| { tracing::error!(error = %e, "creating parent dirs for key file"); - crate::Error::InternalFailure + crate::Error::KeyFileWrite })?; match tokio::fs::read(path).await { @@ -167,7 +167,7 @@ where Err(e) => match bad_format_behavior { BadFormatBehavior::Error => { tracing::error!(error = %e, "parsing key file"); - return Err(crate::Error::InternalFailure); + return Err(crate::Error::KeyFileRead); } BadFormatBehavior::Overwrite => { tracing::warn!( @@ -181,7 +181,7 @@ where Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(e) => { tracing::error!(error = %e, path = %path.display(), "reading key file"); - return Err(crate::Error::InternalFailure); + return Err(crate::Error::KeyFileRead); } } @@ -190,13 +190,13 @@ where path, serde_json::to_vec(&value).map_err(|e| { tracing::error!(error = %e, "serializing key state"); - crate::Error::InternalFailure + crate::Error::KeyFileWrite })?, ) .await .map_err(|e| { tracing::error!(error = %e, "saving key state"); - crate::Error::InternalFailure + crate::Error::KeyFileWrite })?; Ok(value) diff --git a/src/error.rs b/src/error.rs index 63b84b1e..68181c62 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,36 +1,117 @@ +use std::fmt; + use crate::netstack::Error as NetstackError; /// Errors that may occur while interacting with a device. -#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, thiserror::Error, Clone, Copy, Eq, PartialEq)] pub enum Error { - /// Internal operation failed, likely a bug. - #[error("internal operation returned an error")] - InternalFailure, - - /// The runtime state was degraded: a component that we expected to be able to - /// communicate with hung up or could not be reached. + /// An operation timed-out. /// - /// This usually means that an internal component has panicked or is wedged. - #[error("runtime degraded, component unreachable")] - RuntimeDegraded, - - /// An operation timed out. - #[error("operation timed out")] + /// This error can often be handled by retrying. + #[error("operation timed-out")] Timeout, /// A connection was reset. + /// + /// This error can often be handled by retrying. #[error("connection reset")] ConnectionReset, + + /// An error reading or parsing the key file. + #[error("an error reading or parsing the key file")] + KeyFileRead, + + /// An error writing out the key file. + #[error("an error writing out the key file")] + KeyFileWrite, + + /// The environment variable `TS_RS_EXPERIMENT` was not set. + /// + /// The end-user must set `TS_RS_EXPERIMENT=this_is_unstable_software` to acknowledge that tailscale-rs + /// is early-days experimental software containing bugs, unvalidated cryptography, and no stability + /// or compatibility guarantees. + #[error("the environment variable `{}` was not set", crate::ENV_MAGIC_VAR)] + UnstableEnvVar, + + /// An error occurred which can not be anticipated or handled by a library user. + /// + /// This is likely due to a bug in our code or a rare and unexpected error. + /// + /// [`InternalErrorKind`] is intended to be informational (might be used to improve error reporting + /// in logs or to the end-user), rather then inspected during handling. + #[error("internal error ({0})")] + Internal(InternalErrorKind), +} + +/// Informational detail on the kind of internal error. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum InternalErrorKind { + /// Invalid socket state. + InvalidSocketState, + /// Response type mismatched to request type. + InternalResponseMismatch, + /// Channel closed. + InternalChannelClosed, + /// Handle to invalid TCP listener. + BadListenerHandle, + /// Handle to invalid socket. + BadSocketHandle, + /// Bad request. + BadRequest, + /// Invalid buffer. + BadBuffer, + /// Actor missing or shutdown. + Actor, +} + +impl fmt::Display for InternalErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InternalErrorKind::InvalidSocketState => write!(f, "invalid socket state"), + InternalErrorKind::InternalResponseMismatch => { + write!(f, "response type mismatched to request type") + } + InternalErrorKind::InternalChannelClosed => write!(f, "channel closed"), + InternalErrorKind::BadListenerHandle => write!(f, "handle to invalid TCP listener"), + InternalErrorKind::BadSocketHandle => write!(f, "handle to invalid socket"), + InternalErrorKind::BadRequest => write!(f, "bad request"), + InternalErrorKind::BadBuffer => write!(f, "invalid buffer"), + InternalErrorKind::Actor => write!(f, "actor missing or shutdown"), + } + } +} + +impl From for InternalErrorKind { + fn from(e: crate::netstack::InternalErrorKind) -> Self { + match e { + crate::netstack::InternalErrorKind::InvalidSocketState => { + InternalErrorKind::InvalidSocketState + } + crate::netstack::InternalErrorKind::InternalResponseMismatch => { + InternalErrorKind::InternalResponseMismatch + } + crate::netstack::InternalErrorKind::InternalChannelClosed => { + InternalErrorKind::InternalChannelClosed + } + crate::netstack::InternalErrorKind::BadListenerHandle => { + InternalErrorKind::BadListenerHandle + } + crate::netstack::InternalErrorKind::BadSocketHandle => { + InternalErrorKind::BadSocketHandle + } + _ => unreachable!(), + } + } } impl From for Error { fn from(value: ts_runtime::Error) -> Self { match value.kind { ts_runtime::ErrorKind::Timeout => Error::Timeout, - ts_runtime::ErrorKind::ActorGone => Error::RuntimeDegraded, - ts_runtime::ErrorKind::MailboxFull | ts_runtime::ErrorKind::ReplyErr => { - Error::InternalFailure - } + ts_runtime::ErrorKind::ActorGone + | ts_runtime::ErrorKind::MailboxFull + | ts_runtime::ErrorKind::ReplyErr => Error::Internal(InternalErrorKind::Actor), } } } @@ -38,13 +119,9 @@ impl From for Error { impl From for Error { fn from(value: NetstackError) -> Self { match value { - NetstackError::ChannelClosed => Error::RuntimeDegraded, - - NetstackError::WrongType - | NetstackError::BadRequest - | NetstackError::InvariantViolated => Error::InternalFailure, - - NetstackError::TcpStream(_) => Error::ConnectionReset, + NetstackError::Internal(k) => Error::Internal(k.into()), + NetstackError::ConnectionReset => Error::ConnectionReset, + NetstackError::BadRequest(_) => Error::Internal(InternalErrorKind::BadRequest), } } } diff --git a/src/lib.rs b/src/lib.rs index 921a0115..54da0e2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,7 +133,7 @@ use std::{ #[doc(inline)] pub use config::Config; #[doc(inline)] -pub use error::Error; +pub use error::{Error, InternalErrorKind}; #[doc(inline)] pub use ts_control::Node as NodeInfo; use ts_netstack_smoltcp::{CreateSocket, netcore::Channel}; @@ -190,7 +190,7 @@ impl Device { .ask(ts_runtime::control_runner::Ipv4) .await .map_err(ts_runtime::Error::from)? - .ok_or(Error::InternalFailure) + .ok_or(Error::Internal(InternalErrorKind::Actor)) } /// Get this [`Device`]'s IPv6 tailnet address. @@ -200,7 +200,7 @@ impl Device { .ask(ts_runtime::control_runner::Ipv6) .await .map_err(ts_runtime::Error::from)? - .ok_or(Error::InternalFailure) + .ok_or(Error::Internal(InternalErrorKind::Actor)) } /// Bind a UDP socket to the specified [`SocketAddr`]. @@ -242,7 +242,7 @@ impl Device { .ask(ts_runtime::control_runner::SelfNode) .await .map_err(ts_runtime::Error::from)? - .ok_or(Error::RuntimeDegraded) + .ok_or(Error::Internal(InternalErrorKind::Actor)) } /// Look up a peer by name. @@ -251,7 +251,7 @@ impl Device { .runtime .peer_tracker .upgrade() - .ok_or(Error::RuntimeDegraded)?; + .ok_or(Error::Internal(InternalErrorKind::Actor))?; pt.ask(ts_runtime::peer_tracker::PeerByName { name: name.to_string(), @@ -267,7 +267,7 @@ impl Device { .runtime .peer_tracker .upgrade() - .ok_or(Error::RuntimeDegraded)?; + .ok_or(Error::Internal(InternalErrorKind::Actor))?; pt.ask(ts_runtime::peer_tracker::PeerByTailnetIp { ip }) .await @@ -281,7 +281,7 @@ impl Device { .runtime .peer_tracker .upgrade() - .ok_or(Error::RuntimeDegraded)?; + .ok_or(Error::Internal(InternalErrorKind::Actor))?; pt.ask(ts_runtime::peer_tracker::PeerByAcceptedRoute { ip }) .await @@ -308,6 +308,8 @@ pub mod netstack { #[doc(inline)] pub use ts_netstack_smoltcp::netcore::Error; #[doc(inline)] + pub use ts_netstack_smoltcp::netcore::InternalErrorKind; + #[doc(inline)] pub use ts_netstack_smoltcp::netsock::{TcpListener, TcpStream, UdpSocket}; } @@ -336,7 +338,7 @@ guarantees. eprintln!("{}", warning.trim()); - return Err(Error::InternalFailure); + return Err(Error::UnstableEnvVar); }; Ok(()) diff --git a/ts_cli_util/src/lib.rs b/ts_cli_util/src/lib.rs index f126306f..de885b30 100644 --- a/ts_cli_util/src/lib.rs +++ b/ts_cli_util/src/lib.rs @@ -133,7 +133,7 @@ pub async fn set_closest_derp( ) }), ) - .await?; + .await; Ok((*id, map.get(id).unwrap().servers.clone())) } diff --git a/ts_control/src/control_dialer.rs b/ts_control/src/control_dialer.rs index fadc755a..02b65a77 100644 --- a/ts_control/src/control_dialer.rs +++ b/ts_control/src/control_dialer.rs @@ -11,7 +11,7 @@ use ts_capabilityversion::CapabilityVersion; use ts_http_util::{BytesBody, Http2}; use url::Url; -use crate::{DialCandidate, DialMode, DialPlan, tokio::ConnectionError}; +use crate::{DialCandidate, DialMode, DialPlan, Error, InternalErrorKind, Operation}; /// Manages state for control dial plan and handles selection of successive dial candidates. pub struct ControlDialer { @@ -189,18 +189,18 @@ impl ControlDialer { &mut self, url: &Url, machine_keys: &ts_keys::MachineKeyPair, - ) -> Result, ConnectionError> { + ) -> Result, Error> { let next = self.next_dialer(); tracing::trace!(selected_control_dialer = ?next); - let host = url.host_str().ok_or(ConnectionError::ConnectionFailed)?; + let host = url.host_str().ok_or(Error::InvalidUrl(url.clone()))?; let port = url .port_or_known_default() - .ok_or(ConnectionError::ConnectionFailed)?; + .ok_or(Error::InvalidUrl(url.clone()))?; let conn = next.dial(host, port).await.map_err(|e| { tracing::error!(error = %e, %url, %host, port, "dialing tcp"); - ConnectionError::ConnectionFailed + Error::Internal(InternalErrorKind::Io, Operation::ConnectToControlServer) })?; tracing::debug!( @@ -223,27 +223,27 @@ pub async fn complete_connection( url: &Url, machine_keys: &ts_keys::MachineKeyPair, stream: Io, -) -> Result, ConnectionError> +) -> Result, Error> where Io: AsyncRead + AsyncWrite + Send + Unpin + 'static, { let h1_client = match url.scheme() { "https" => { let conn = ts_tls_util::connect( - ts_tls_util::server_name(url).ok_or(ConnectionError::ConnectionFailed)?, + ts_tls_util::server_name(url).ok_or(Error::InvalidUrl(url.clone()))?, stream, ) .await .map_err(|e| { tracing::error!(error = %e, "establishing tls connection"); - ConnectionError::ConnectionFailed + Error::io_error(e, Operation::ConnectToControlServer) })?; ts_http_util::http1::connect(conn).await? } "http" => ts_http_util::http1::connect(stream).await?, other => { tracing::error!(invalid_scheme = other); - return Err(ConnectionError::ConnectionFailed); + return Err(Error::InvalidUrl(url.clone())); } }; let control_public_key = crate::tokio::fetch_control_key(url).await?; diff --git a/ts_control/src/lib.rs b/ts_control/src/lib.rs index a9bf4f1b..2ce3fee2 100644 --- a/ts_control/src/lib.rs +++ b/ts_control/src/lib.rs @@ -21,6 +21,8 @@ mod node; #[cfg(feature = "async_tokio")] mod tokio; +use std::fmt; + #[doc(inline)] pub use config::{Config, DEFAULT_CONTROL_SERVER}; pub use control_dialer::{ControlDialer, TcpDialer, complete_connection}; @@ -29,25 +31,133 @@ pub use dial_plan::{DialCandidate, DialMode, DialPlan}; pub use node::{Id as NodeId, Node, StableId as StableNodeId, TailnetAddress}; #[cfg(feature = "async_tokio")] -pub use crate::tokio::{AsyncControlClient, AuthResult, FilterUpdate, PeerUpdate, StateUpdate}; +pub use crate::tokio::{AsyncControlClient, FilterUpdate, PeerUpdate, StateUpdate}; -/// Errors that may occur while communicating with control. -#[derive(Debug, thiserror::Error)] +/// An error which occured while connecting to the control server or control plane. +#[derive(Debug, thiserror::Error, Clone, Eq, PartialEq)] pub enum Error { - /// Error connecting to control. - #[error(transparent)] - #[cfg(feature = "async_tokio")] - Connect(#[from] tokio::ConnectionError), - /// Error processing streaming netmap results. - #[error(transparent)] - #[cfg(feature = "async_tokio")] - MapStream(#[from] tokio::MapStreamError), - /// Error executing a ping. - #[error(transparent)] - #[cfg(feature = "async_tokio")] - Ping(#[from] tokio::PingError), - /// Error registering with control. - #[error(transparent)] - #[cfg(feature = "async_tokio")] - Register(#[from] tokio::RegistrationError), + /// A machine was not authorized by control to join tailnet; authorize via the supplied URL. + #[error("machine was not authorized by control to join tailnet")] + MachineNotAuthorized(url::Url), + + /// The user supplied an invalid URL. + #[error("invalid URL: {0}")] + InvalidUrl(url::Url), + + /// Some kind of networking error, e.g., HTTP, TLS. + /// + /// These might be addressed by retrying, or might be an unresolvable error. + /// + /// [`Operation`] is intended to be informational, rather then inspected during handling. + #[error("A networking error occurred in {0}")] + NetworkError(Operation), + + /// An internal error that users of the library are not expected to handle. + /// + /// [`InternalErrorKind`] and [`Operation`] are intended to be informational, rather then + /// inspected during handling. + #[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) + } + } +} + +/// What kind of internal error has occured. +/// +/// This is intended to be useful for reporting a crash to an end user, rather than being handled. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum InternalErrorKind { + /// An error in URL parsing. + Url, + /// An unsuccessful HTTP request or upgrade. + Http, + /// An error in serialization or deserialization. + SerDe, + /// An error in I/O. + Io, + /// An invalid message format. + MessageFormat, + /// An error parsing a string as UTF8. + Utf8, + /// Noise framework handshake. + NoiseHandshake, + /// Tailscale challenge packet. + Challenge, + /// The user's machine was not authorized to register with a Tailnet and there is no URL for + /// the user to authorize at. + 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") + } + } + } +} + +/// The phase of connecting the control plane to a Tailnet in which an error occurs. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Operation { + /// Requesting a net map. + MapRequest, + /// Connecting to a control server. + ConnectToControlServer, + /// Registering the user's device with a Tailnet. + 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 for Error { + fn from(error: ts_http_util::Error) -> Self { + tracing::error!(%error, "http error"); + Error::NetworkError(Operation::ConnectToControlServer) + } +} + +/// Returns true if the input io error should be classed as a network error. +fn is_network_error(err: &std::io::Error) -> bool { + use std::io::ErrorKind::*; + matches!( + err.kind(), + ConnectionRefused + | ConnectionReset + | HostUnreachable + | NetworkUnreachable + | ConnectionAborted + | NotConnected + | TimedOut + | AddrInUse + | AddrNotAvailable + | Interrupted + | NetworkDown + ) } diff --git a/ts_control/src/tokio/client.rs b/ts_control/src/tokio/client.rs index 46144f56..9aa664e5 100644 --- a/ts_control/src/tokio/client.rs +++ b/ts_control/src/tokio/client.rs @@ -9,10 +9,9 @@ use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use url::Url; use crate::{ - ControlDialer, + ControlDialer, Error, map_request_builder::MapRequestBuilder, tokio::{ - ConnectionError, MapStreamError, RegistrationError, map_stream::{StateUpdate, map_stream, send_map_request}, ping::handle_ping, }, @@ -34,16 +33,14 @@ impl AsyncControlClient { config: &crate::Config, node_keys: &ts_keys::NodeState, auth_key: Option<&str>, - ) -> Result { + ) -> Result<(), Error> { let control_url = &config.server_url; let h2_client = crate::tokio::connect(control_url, &node_keys.machine_keys).await?; - let register_url = control_url.join("machine/register")?; - let result = - crate::tokio::register(config, ®ister_url, auth_key, node_keys, &h2_client).await?; + crate::tokio::register(config, control_url, auth_key, node_keys, &h2_client).await?; - Ok(result) + Ok(()) } /// Connects to the control plane, registers this Tailscale node, and starts handling the @@ -61,7 +58,7 @@ impl AsyncControlClient { Self, impl Stream> + Send + Sync + use<>, ), - ConnectionError, + Error, > { let control_url = &config.server_url; let mut tasks = JoinSet::new(); @@ -69,12 +66,7 @@ impl AsyncControlClient { let h2_client = crate::tokio::connect(control_url, &node_keys.machine_keys).await?; tracing::info!("connected to control, registering"); - let register_url = control_url.join("machine/register")?; - if crate::tokio::register(config, ®ister_url, auth_key, node_keys, &h2_client).await? - != crate::AuthResult::Ok - { - return Err(RegistrationError::MachineNotAuthorized.into()); - }; + crate::tokio::register(config, control_url, auth_key, node_keys, &h2_client).await?; tracing::info!("registered, starting netmap stream"); @@ -130,12 +122,12 @@ impl AsyncControlClient { } /// Set the DERP home region for this node. - #[tracing::instrument(skip_all, fields(map_url = %self.map_url(), %region_id), err, level = "trace")] + #[tracing::instrument(skip_all, fields(map_url = %self.map_url(), %region_id), level = "trace")] pub async fn set_home_region<'c>( &mut self, region_id: ts_transport_derp::RegionId, latencies: impl IntoIterator, - ) -> Result<(), MapStreamError> { + ) { tracing::trace!(region = %region_id, "reporting home derp to control server"); if let Err(e) = self @@ -151,8 +143,6 @@ impl AsyncControlClient { { tracing::error!(error = %e, "setting home derp region"); } - - Ok(()) } /// Construct the URL that should be used to fetch the netmap. @@ -210,7 +200,7 @@ pub async fn run( } } -pub async fn run_once( +async fn run_once( state_tx: &broadcast::Sender>, command_rx: &mut mpsc::Receiver, control_url: &Url, @@ -218,17 +208,12 @@ pub async fn run_once( auth_key: Option<&str>, config: &crate::Config, control_dialer: &mut ControlDialer, -) -> Result<(), crate::Error> { +) -> Result<(), Error> { let h2_client = control_dialer .full_connect_next(control_url, &node_keys.machine_keys) .await?; - let register_url = control_url.join("machine/register").unwrap(); - if crate::tokio::register(config, ®ister_url, auth_key, node_keys, &h2_client).await? - != crate::AuthResult::Ok - { - return Err(RegistrationError::MachineNotAuthorized.into()); - }; + crate::tokio::register(config, control_url, auth_key, node_keys, &h2_client).await?; let builder = MapRequestBuilder::new(node_keys) .keep_alive(true) @@ -244,9 +229,7 @@ pub async fn run_once( let map_url = control_url.join("machine/map").unwrap(); - let reader = send_map_request(request, &map_url, &h2_client) - .await - .map_err(ConnectionError::MapStreamStartFailed)?; + let reader = send_map_request(request, &map_url, &h2_client).await?; let mut stream = core::pin::pin!(map_stream(reader)); tracing::info!("netmap stream started"); @@ -258,9 +241,7 @@ pub async fn run_once( break; }; - if let Err(e) = handle_ping(&state_update, control_url, &h2_client).await { - tracing::error!(error = %e, "handling ping request"); - } + let _ = handle_ping(&state_update, control_url, &h2_client).await; if let Some(dial_plan) = &state_update.dial_plan && control_dialer.update_dial_plan(dial_plan) diff --git a/ts_control/src/tokio/connect.rs b/ts_control/src/tokio/connect.rs index 9ad984ff..839e2acc 100644 --- a/ts_control/src/tokio/connect.rs +++ b/ts_control/src/tokio/connect.rs @@ -9,7 +9,7 @@ use ts_keys::{MachineKeyPair, MachinePublicKey}; use url::Url; use zerocopy::network_endian::U32; -use crate::tokio::{MapStreamError, RegistrationError, prefixed_reader::PrefixedReader}; +use crate::tokio::prefixed_reader::PrefixedReader; const CHALLENGE_MAGIC: [u8; 5] = [0xFF, 0xFF, 0xFF, b'T', b'S']; const HANDSHAKE_HEADER_KEY: &str = "X-Tailscale-Handshake"; @@ -22,35 +22,114 @@ lazy_static::lazy_static! { pub static ref CONTROL_PROTOCOL_VERSION: String = format!("Tailscale Control Protocol v{}", CapabilityVersion::CURRENT); } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, Clone, Copy, Eq, PartialEq)] pub enum ConnectionError { - #[error(transparent)] - BadUrl(#[from] url::ParseError), - #[error("could not read {field:?} from {stage} message: {err}")] - Io { - field: Option<&'static str>, - stage: &'static str, - err: std::io::Error, - }, - #[error("could not connect to control server")] - ConnectionFailed, - #[error(transparent)] - DeserializeFailed(#[from] serde_json::Error), - #[error("invalid challenge length ({0} bytes); must be less than {MAX_CHALLENGE_LENGTH} bytes")] - InvalidChallengeLength(usize), - #[error("invalid magic value {0:X?} (expected {CHALLENGE_MAGIC:X?})")] - InvalidChallengeMagic([u8; 5]), - #[error("failed to start map stream: {0}")] - MapStreamStartFailed(MapStreamError), - #[error(transparent)] - Noise(#[from] ts_control_noise::Error), - #[error(transparent)] - RegistrationFailed(#[from] RegistrationError), - #[error("could not upgrade control connection to TS2021 protocol")] - UpgradeFailed, - - #[error(transparent)] - Http(#[from] ts_http_util::Error), + #[error("internal error during connection: {0}")] + Internal(InternalErrorKind), + #[error("Network error")] + NetworkError, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum InternalErrorKind { + Url, + Http, + SerDe, + MessageFormat, + Io, + ChallengeLength, + NoiseHandshake, +} + +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::MessageFormat => write!(f, "message format error"), + InternalErrorKind::Io => write!(f, "I/O error"), + InternalErrorKind::ChallengeLength => write!(f, "challenge too long"), + InternalErrorKind::NoiseHandshake => write!(f, "error in Noise handshake"), + } + } +} + +impl ConnectionError { + fn io_error(field: &'static str, stage: &'static str, err: std::io::Error) -> Self { + tracing::error!("could not read {field} from {stage} message: {err}"); + + if crate::is_network_error(&err) { + ConnectionError::NetworkError + } else { + ConnectionError::Internal(InternalErrorKind::Io) + } + } +} + +impl From for ConnectionError { + fn from(error: serde_json::Error) -> Self { + tracing::error!(%error, "deserialization error"); + ConnectionError::Internal(InternalErrorKind::SerDe) + } +} + +impl From for ConnectionError { + fn from(error: ts_http_util::Error) -> Self { + tracing::error!(%error, "http error"); + ConnectionError::Internal(InternalErrorKind::Http) + } +} + +impl From for ConnectionError { + fn from(error: url::ParseError) -> Self { + tracing::error!(%error, "bad URL"); + ConnectionError::Internal(InternalErrorKind::Url) + } +} + +impl From for ConnectionError { + fn from(error: ts_control_noise::Error) -> Self { + match error { + ts_control_noise::Error::BadFormat => { + ConnectionError::Internal(InternalErrorKind::MessageFormat) + } + ts_control_noise::Error::HandshakeFailed => { + ConnectionError::Internal(InternalErrorKind::NoiseHandshake) + } + ts_control_noise::Error::Io(error) => { + tracing::error!(%error, "IO error in Noise communication"); + ConnectionError::Internal(InternalErrorKind::Io) + } + } + } +} + +impl From for crate::Error { + fn from(e: ConnectionError) -> Self { + match e { + ConnectionError::Internal(k) => { + crate::Error::Internal(k.into(), crate::Operation::ConnectToControlServer) + } + ConnectionError::NetworkError => { + crate::Error::NetworkError(crate::Operation::ConnectToControlServer) + } + } + } +} + +impl From for crate::InternalErrorKind { + fn from(e: InternalErrorKind) -> Self { + match e { + InternalErrorKind::Url => crate::InternalErrorKind::Url, + InternalErrorKind::Http => crate::InternalErrorKind::Http, + InternalErrorKind::SerDe => crate::InternalErrorKind::SerDe, + InternalErrorKind::MessageFormat => crate::InternalErrorKind::MessageFormat, + InternalErrorKind::Io => crate::InternalErrorKind::Io, + InternalErrorKind::ChallengeLength => crate::InternalErrorKind::Challenge, + InternalErrorKind::NoiseHandshake => crate::InternalErrorKind::NoiseHandshake, + } + } } #[derive(serde::Deserialize, serde::Serialize)] @@ -128,7 +207,7 @@ pub async fn fetch_control_key(control_url: &Url) -> Result MAX_CHALLENGE_LENGTH { - return Err(ConnectionError::InvalidChallengeLength(challenge_len)); + tracing::error!( + challenge_len, + "invalid challenge length; must be less than {MAX_CHALLENGE_LENGTH} bytes" + ); + return Err(ConnectionError::Internal( + InternalErrorKind::ChallengeLength, + )); } // Read and discard the challenge JSON. let mut limited = conn.take(challenge_len as _); tokio::io::copy(&mut limited, &mut tokio::io::sink()) .await - .map_err(|err| ConnectionError::Io { - field: Some("body"), - stage: "challenge", - err, - })?; + .map_err(|err| ConnectionError::io_error("body", "challenge", err))?; tracing::trace!( n_bytes = challenge_len, diff --git a/ts_control/src/tokio/map_stream.rs b/ts_control/src/tokio/map_stream.rs index 3afc1076..c9450a5b 100644 --- a/ts_control/src/tokio/map_stream.rs +++ b/ts_control/src/tokio/map_stream.rs @@ -2,10 +2,7 @@ use alloc::collections::BTreeMap; use bytes::Bytes; use futures_util::Stream; -use tokio::{ - io::{AsyncRead, AsyncReadExt}, - sync::watch, -}; +use tokio::io::{AsyncRead, AsyncReadExt}; use ts_control_serde::{MapRequest, MapResponse, PingRequest}; use ts_http_util::{BytesBody, ClientExt, Http2, ResponseExt}; use ts_packet::PacketMut; @@ -15,35 +12,40 @@ use url::Url; use crate::{DialPlan, NodeId}; -#[derive(Debug, thiserror::Error)] +// These are all internal errors (i.e., unexpected and not due to any user input). +#[derive(Debug, thiserror::Error, Clone, Copy, Eq, PartialEq)] pub enum MapStreamError { - #[error("failed to deserialize map response message: {0}")] - DeserializeFailed(serde_json::Error), - #[error("failed to forward ping request to handler; channel was not initialized")] - InvalidPingChannel, - #[error(transparent)] - JoinFailed(#[from] tokio::task::JoinError), - #[error("map stream message was missing length: {0}")] - LengthMissing(std::io::Error), - #[error("failed to register for map updates; control returned HTTP {0}")] - MapRequestFailed(u16), - #[error("failed to construct request")] - Request, - #[error("failed to serialize map request message: {0}")] - SerializeFailed(serde_json::Error), - #[error("map stream encountered unexpected EOF: {0}")] - UnexpectedEof(std::io::Error), - #[error(transparent)] - Utf8Error(#[from] core::str::Utf8Error), - #[error(transparent)] - WatchRecv(#[from] watch::error::RecvError), - #[error(transparent)] - Http(#[from] ts_http_util::Error), + #[error("serialization error")] + SerDe, + #[error("unsuccessful HTTP request or upgrade")] + Http, } -impl From for std::io::Error { - fn from(value: MapStreamError) -> Self { - std::io::Error::other(value.to_string()) +impl From for MapStreamError { + fn from(error: serde_json::Error) -> Self { + tracing::error!(%error, "serialization error sending map request"); + MapStreamError::SerDe + } +} + +impl From for MapStreamError { + fn from(error: ts_http_util::Error) -> Self { + tracing::error!(%error, "http error sending map request"); + MapStreamError::Http + } +} + +impl From for crate::Error { + fn from(e: MapStreamError) -> Self { + match e { + MapStreamError::SerDe => crate::Error::Internal( + crate::InternalErrorKind::SerDe, + crate::Operation::MapRequest, + ), + MapStreamError::Http => { + crate::Error::Internal(crate::InternalErrorKind::Http, crate::Operation::MapRequest) + } + } } } @@ -198,10 +200,10 @@ pub async fn send_map_request( tracing::debug!("sending map request to control server..."); let body = if cfg!(debug_assertions) { - serde_json::to_string_pretty(&map_request).map_err(MapStreamError::SerializeFailed) + serde_json::to_string_pretty(&map_request)? } else { - serde_json::to_string(&map_request).map_err(MapStreamError::SerializeFailed) - }?; + serde_json::to_string(&map_request)? + }; tracing::trace!( %body, "sending map request" @@ -213,7 +215,11 @@ pub async fn send_map_request( tracing::trace!(?status, "received map response"); if !status.is_success() { - return Err(MapStreamError::MapRequestFailed(status.as_u16())); + tracing::error!( + status = status.as_u16(), + "failed to register map updates with unsuccesful HTTP status code" + ); + return Err(MapStreamError::Http); } Ok(resp.into_read()) diff --git a/ts_control/src/tokio/mod.rs b/ts_control/src/tokio/mod.rs index 7baf0d18..1785a964 100644 --- a/ts_control/src/tokio/mod.rs +++ b/ts_control/src/tokio/mod.rs @@ -1,19 +1,14 @@ -mod client; pub use client::AsyncControlClient; - -mod connect; +use connect::connect; pub use connect::{ - CONTROL_PROTOCOL_VERSION, ConnectionError, connect, fetch_control_key, read_challenge_packet, - upgrade_ts2021, + CONTROL_PROTOCOL_VERSION, fetch_control_key, read_challenge_packet, upgrade_ts2021, }; +pub use map_stream::{FilterUpdate, PeerUpdate, StateUpdate}; +use register::register; +mod client; +mod connect; mod map_stream; -pub use map_stream::{FilterUpdate, MapStreamError, PeerUpdate, StateUpdate}; - mod ping; -pub use ping::PingError; - mod prefixed_reader; mod register; - -pub use register::{AuthResult, RegistrationError, register}; diff --git a/ts_control/src/tokio/ping.rs b/ts_control/src/tokio/ping.rs index 80a83f3e..14436f90 100644 --- a/ts_control/src/tokio/ping.rs +++ b/ts_control/src/tokio/ping.rs @@ -1,6 +1,5 @@ -use alloc::string::String; +use alloc::{fmt, string::String}; -use tokio::sync::watch; use ts_control_serde::PingType; use ts_http_util::{BytesBody, ClientExt, Http2, Request}; use url::Url; @@ -16,22 +15,48 @@ const C2N_PATH_UNKNOWN: &str = "HTTP/1.1 400 Bad Request\r\n\r\nunknown c2n path /// with C2N echo responses, which can append the request body. const C2N_RESPONSE_ECHO_PREAMBLE: &str = "HTTP/1.1 200 OK\r\n\r\n"; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, Clone, Copy, Eq, PartialEq)] pub enum PingError { - #[error(transparent)] - Http(#[from] ts_http_util::Error), - #[error(transparent)] - JoinFailed(#[from] tokio::task::JoinError), - #[error("C2N ping request is missing payload")] - MissingPayload, - #[error(transparent)] - WatchRecv(#[from] watch::error::RecvError), + #[error("HTTP error")] + NetworkError, + #[error("internal error during ping: {0}")] + InternalError(InternalErrorKind), +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum InternalErrorKind { + Url, + MessageFormat, +} + +impl fmt::Display for InternalErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InternalErrorKind::Url => write!(f, "URL parsing error"), + InternalErrorKind::MessageFormat => { + write!(f, "Ping request with invalid format (missing payload)") + } + } + } +} +impl From for PingError { + fn from(error: ts_http_util::Error) -> Self { + tracing::error!(%error, "Error parsing HTTP request"); + PingError::NetworkError + } +} + +impl From for PingError { + fn from(error: url::ParseError) -> Self { + tracing::error!(%error, "Error parsing URL"); + PingError::InternalError(InternalErrorKind::Url) + } } /// Parses the payload of a Control-to-Node (C2N) [`ts_control_serde::PingRequest`] as an HTTP/1.1 /// request, or returns an error. fn parse_c2n_ping(payload: &str) -> Result, PingError> { - let req = ts_http_util::http1::parse_request(payload.as_bytes()).map_err(PingError::Http)?; + let req = ts_http_util::http1::parse_request(payload.as_bytes())?; tracing::trace!( payload_len = req.body().len(), payload = req.body(), @@ -80,7 +105,7 @@ pub async fn handle_ping( let ping_request_body = ping_request .payload .as_ref() - .ok_or(PingError::MissingPayload)?; + .ok_or(PingError::InternalError(InternalErrorKind::MessageFormat))?; let c2n_request = match parse_c2n_ping(ping_request_body) { Ok(c2n_request) => { tracing::trace!(?c2n_request, "parsed c2n ping"); @@ -104,9 +129,7 @@ pub async fn handle_ping( } }; - let ping_response_url = control_url - .join(ping_request.url.path()) - .map_err(|_| PingError::MissingPayload)?; + let ping_response_url = control_url.join(ping_request.url.path())?; tracing::trace!(%ping_response_url, ?c2n_response, "posting c2n response"); let response = http2_client .post(&ping_response_url, None, c2n_response.into()) diff --git a/ts_control/src/tokio/register.rs b/ts_control/src/tokio/register.rs index 00664881..04c80c0b 100644 --- a/ts_control/src/tokio/register.rs +++ b/ts_control/src/tokio/register.rs @@ -1,3 +1,5 @@ +use std::fmt; + use bytes::Bytes; use ts_capabilityversion::CapabilityVersion; use ts_control_serde::{HostInfo, RegisterAuth, RegisterRequest, RegisterResponse}; @@ -6,43 +8,99 @@ use url::Url; const LOAD_BALANCER_HEADER_KEY: &str = "Ts-Lb"; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, Clone, Eq, PartialEq)] pub enum RegistrationError { - #[error("peer config missing auth key; needed for registration")] - AuthKeyMissing, - #[error("failed to deserialize registration response body: {0}")] - DeserializeFailed(serde_json::Error), - #[error(transparent)] - HttpError(#[from] ts_http_util::Error), #[error("machine was not authorized by control to join tailnet")] - MachineNotAuthorized, - #[error("failed to register node; control returned HTTP {0}")] - RegistrationFailed(u16), - #[error("failed to construct request")] - Request, - #[error("failed to serialize registration request body: {0}")] - SerializeFailed(serde_json::Error), - #[error(transparent)] - Utf8Error(#[from] core::str::Utf8Error), + MachineNotAuthorized(Option), + #[error("error during registration: {0}")] + Internal(InternalErrorKind), + #[error("HTTP error")] + NetworkError, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum InternalErrorKind { + Url, + SerDe, + Utf8, +} + +impl fmt::Display for InternalErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InternalErrorKind::Url => write!(f, "URL parsing error"), + InternalErrorKind::SerDe => write!(f, "serialization/deserialization error"), + InternalErrorKind::Utf8 => write!(f, "invalid UTF8"), + } + } +} + +impl From for RegistrationError { + fn from(error: url::ParseError) -> Self { + tracing::error!(%error, "bad URL"); + RegistrationError::Internal(InternalErrorKind::Url) + } +} + +impl From for RegistrationError { + fn from(error: serde_json::Error) -> Self { + tracing::error!(%error, "serialization/deserialization error in registration"); + RegistrationError::Internal(InternalErrorKind::SerDe) + } } -/// Result of authorizing with the control plane. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AuthResult { - /// Authorization succeeded. - Ok, - /// Auth failed, user should navigate to the contained URL. - AuthRequired(Url), +impl From for RegistrationError { + fn from(error: ts_http_util::Error) -> Self { + tracing::error!(%error, "http error sending registration request"); + RegistrationError::NetworkError + } +} + +impl From for RegistrationError { + fn from(error: core::str::Utf8Error) -> Self { + tracing::error!(%error, "utf8 error in registration response"); + RegistrationError::Internal(InternalErrorKind::Utf8) + } } -#[tracing::instrument(skip_all, fields(%register_url))] +impl From for crate::Error { + fn from(e: RegistrationError) -> Self { + match e { + RegistrationError::MachineNotAuthorized(Some(u)) => { + crate::Error::MachineNotAuthorized(u) + } + RegistrationError::MachineNotAuthorized(None) => crate::Error::Internal( + crate::InternalErrorKind::MachineAuthorization, + crate::Operation::Registration, + ), + RegistrationError::Internal(k) => { + crate::Error::Internal(k.into(), crate::Operation::Registration) + } + RegistrationError::NetworkError => { + crate::Error::NetworkError(crate::Operation::Registration) + } + } + } +} + +impl From for crate::InternalErrorKind { + fn from(e: InternalErrorKind) -> Self { + match e { + InternalErrorKind::Url => crate::InternalErrorKind::Url, + InternalErrorKind::SerDe => crate::InternalErrorKind::SerDe, + InternalErrorKind::Utf8 => crate::InternalErrorKind::Utf8, + } + } +} + +#[tracing::instrument(skip_all, fields(%control_url))] pub async fn register( config: &crate::Config, - register_url: &Url, + control_url: &Url, auth_key: Option<&str>, node_keystate: &ts_keys::NodeState, http2_conn: &Http2, -) -> Result { +) -> Result<(), RegistrationError> { let node_public_key = node_keystate.node_keys.public; let network_lock_public_key = node_keystate.network_lock_keys.public; @@ -62,11 +120,12 @@ pub async fn register( }; let body = if cfg!(debug_assertions) { - serde_json::to_string_pretty(®ister_req).map_err(RegistrationError::SerializeFailed) + serde_json::to_string_pretty(®ister_req)? } else { - serde_json::to_string(®ister_req).map_err(RegistrationError::SerializeFailed) - }?; + serde_json::to_string(®ister_req)? + }; + let register_url = control_url.join("machine/register")?; tracing::trace!( url = %register_url.as_str(), %body, @@ -75,7 +134,7 @@ pub async fn register( let response = http2_conn .post( - register_url, + ®ister_url, [( LOAD_BALANCER_HEADER_KEY.parse().unwrap(), node_public_key.to_string().parse().unwrap(), @@ -95,7 +154,7 @@ pub async fn register( let body = core::str::from_utf8(&body).unwrap_or(""); tracing::error!(%body, %status, "registration failed"); - return Err(RegistrationError::RegistrationFailed(status.as_u16())); + return Err(RegistrationError::NetworkError); } let body = response.collect_bytes().await?; @@ -103,21 +162,17 @@ pub async fn register( tracing::trace!(registration_response_body = %body); - let register_resp: RegisterResponse = - serde_json::from_str(body).map_err(RegistrationError::DeserializeFailed)?; + let register_resp: RegisterResponse = serde_json::from_str(body)?; if !register_resp.machine_authorized { if !register_resp.auth_url.is_empty() { - Ok(AuthResult::AuthRequired( - register_resp - .auth_url - .parse() - .map_err(|_e| RegistrationError::MachineNotAuthorized)?, + Err(RegistrationError::MachineNotAuthorized( + register_resp.auth_url.parse().ok(), )) } else { - Err(RegistrationError::MachineNotAuthorized) + Err(RegistrationError::MachineNotAuthorized(None)) } } else { - Ok(AuthResult::Ok) + Ok(()) } } diff --git a/ts_netstack_smoltcp_core/src/command/channel.rs b/ts_netstack_smoltcp_core/src/command/channel.rs index 150a14b6..cba8cd31 100644 --- a/ts_netstack_smoltcp_core/src/command/channel.rs +++ b/ts_netstack_smoltcp_core/src/command/channel.rs @@ -2,7 +2,7 @@ use core::borrow::Borrow; use smoltcp::iface::SocketHandle; -use crate::{Command, Error, Netstack, Response, command::Request}; +use crate::{ChannelClosedError, Command, Error, Netstack, Response, command::Request}; /// Channel type through which commands to the netstack flow. // NOTE(npry): command channels are weak because the netstack holds a strong sender handle @@ -31,7 +31,7 @@ pub trait HasChannel { &self, handle: Option, command: impl Into, - ) -> Result { + ) -> Result { crate::request_blocking(self.borrow_channel().borrow(), handle, command) } @@ -59,7 +59,7 @@ pub trait HasChannel { &self, handle: Option, command: impl Into, - ) -> Result<(), Error> { + ) -> Result<(), ChannelClosedError> { crate::request_nonblocking(self.borrow_channel().borrow(), handle, command) } } diff --git a/ts_netstack_smoltcp_core/src/command/error.rs b/ts_netstack_smoltcp_core/src/command/error.rs index f286e5a0..88694b96 100644 --- a/ts_netstack_smoltcp_core/src/command/error.rs +++ b/ts_netstack_smoltcp_core/src/command/error.rs @@ -1,3 +1,5 @@ +use std::fmt; + use smoltcp::socket::{ icmp::BindError as IcmpBindError, raw::BindError as RawBindError, @@ -5,39 +7,138 @@ use smoltcp::socket::{ udp::{BindError as UdpBindError, SendError as UdpSendError}, }; -use crate::{command, tcp}; +use crate::command; /// Error while interacting with the netstack. -#[derive(thiserror::Error, Debug, Copy, Clone, PartialEq, Eq)] +#[derive(thiserror::Error, Debug, Clone, Copy, Eq, PartialEq)] pub enum Error { - /// The remote end of a command or reply channel has closed. This command can't - /// complete. - /// - /// Typically, this occurs if the netstack we're talking to has been dropped. - #[error("the remote end of the channel has closed")] - ChannelClosed, - /// The request contained invalid parameters. Retrying will not resolve this issue. /// - /// Common causes for this error are that a specified address was invalid, the socket - /// a handle refers to no longer exists, or the packet to be sent was too large for the - /// socket's buffer capacity. - #[error("invalid request")] - BadRequest, + /// Common causes for this error are that a specified address was invalid, there is not enough + /// memory available in the current configuration to complete the request, or the packet to be + /// sent was too large for the socket's buffer capacity. + #[error("invalid request ({0})")] + BadRequest(BadRequestReason), - /// An error occurred related to TCP stream operations. - #[error(transparent)] - TcpStream(#[from] tcp::stream::Error), + /// An internal error occured. + /// + /// These errors are either unrecoverable, due to a bug in our code, or caused by a rare + /// and intermittent issue. + #[error("An internal error occured: {0}")] + Internal(InternalErrorKind), - /// An internal invariant was violated. + /// A TCP connection was reset. /// - /// This indicates a bug and is likely an unrecoverable failure. - #[error("invariant violation: unrecoverable")] - InvariantViolated, + /// These errors can often be handled by retrying. + #[error("connection reset")] + ConnectionReset, +} + +impl Error { + /// Error constructor for the wrong type of command response. + pub fn wrong_type() -> Self { + Error::Internal(InternalErrorKind::InternalResponseMismatch) + } + + /// Error constructor for an invalid socket state. + pub fn invalid_socket_state() -> Self { + Error::Internal(InternalErrorKind::InvalidSocketState) + } + + /// Error constructor for a missing TCP listener. + pub fn missing_listener() -> Self { + Error::Internal(InternalErrorKind::BadListenerHandle) + } + + /// Error constructor for a missing socket. + pub fn missing_socket() -> Self { + Error::Internal(InternalErrorKind::BadSocketHandle) + } + + /// Error constructor for an over-size packet. + pub fn big_packet() -> Self { + Error::BadRequest(BadRequestReason::BigPacket) + } + + /// Error constructor for out-of-memory (likely in a specific component, rather than a system OOM). + pub fn buffer_full() -> Self { + Error::Internal(InternalErrorKind::BufferFull) + } - /// Response was the wrong type for the given operation. - #[error("response was the wrong type")] - WrongType, + /// Error constructor for an unaddressable packet. + pub fn unaddressable() -> Self { + Error::BadRequest(BadRequestReason::Unaddressable) + } + + /// Error constructor for a zero-sized buffer. + pub fn zero_buffer() -> Self { + Error::BadRequest(BadRequestReason::ZeroSizeBuffer) + } +} + +/// Informational detail on the kind of internal error. +/// +/// This detail is unlikely to help in error recovery but could be used in logs or user-facing +/// reporting to add detail to an error report. +// If adding variants, be sure to add them to `tailscale::error::InternalErrorKind` too (including +// the `From` impl, which will panic otherwise). +#[non_exhaustive] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum InternalErrorKind { + /// Invalid socket state. + InvalidSocketState, + /// Response type mismatched to request type. + InternalResponseMismatch, + /// Channel closed. + InternalChannelClosed, + /// Handle to invalid TCP listener. + BadListenerHandle, + /// Handle to invalid socket. + BadSocketHandle, + /// buffer full + BufferFull, +} + +impl fmt::Display for InternalErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InternalErrorKind::InvalidSocketState => write!(f, "invalid socket state"), + InternalErrorKind::InternalResponseMismatch => { + write!(f, "response type mismatched to request type") + } + InternalErrorKind::InternalChannelClosed => write!(f, "channel closed"), + InternalErrorKind::BadListenerHandle => write!(f, "handle to invalid TCP listener"), + InternalErrorKind::BadSocketHandle => write!(f, "handle to invalid socket"), + InternalErrorKind::BufferFull => write!(f, "buffer full"), + } + } +} + +/// The reason for an [`Error::BadRequest`] error. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum BadRequestReason { + /// Packet unaddressable. + Unaddressable, + /// A packet is too large to ever fit in socket buffer; even if the buffer is emptied, this error + /// will still occur. + BigPacket, + /// The size of a user-supplied buffer is zero, so cannot store a packet. + ZeroSizeBuffer, +} + +impl fmt::Display for BadRequestReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BadRequestReason::Unaddressable => write!(f, "packet unaddressable"), + BadRequestReason::BigPacket => { + write!(f, "packet is too large to ever fit in socket buffer") + } + BadRequestReason::ZeroSizeBuffer => { + write!(f, "the size of a user-supplied buffer is zero") + } + } + } } impl From for command::Response { @@ -48,21 +149,21 @@ impl From for command::Response { impl From> for Error { fn from(_: flume::SendError) -> Self { - Error::ChannelClosed + Error::Internal(InternalErrorKind::InternalChannelClosed) } } impl From for Error { fn from(_: flume::RecvError) -> Self { - Error::ChannelClosed + Error::Internal(InternalErrorKind::InternalChannelClosed) } } impl From for Error { fn from(value: ListenError) -> Self { match value { - ListenError::InvalidState => Error::InvariantViolated, - ListenError::Unaddressable => Error::BadRequest, + ListenError::InvalidState => Error::invalid_socket_state(), + ListenError::Unaddressable => Error::unaddressable(), } } } @@ -70,8 +171,8 @@ impl From for Error { impl From for Error { fn from(value: ConnectError) -> Self { match value { - ConnectError::InvalidState => Error::InvariantViolated, - ConnectError::Unaddressable => Error::BadRequest, + ConnectError::InvalidState => Error::invalid_socket_state(), + ConnectError::Unaddressable => Error::unaddressable(), } } } @@ -79,8 +180,8 @@ impl From for Error { impl From for Error { fn from(value: UdpBindError) -> Self { match value { - UdpBindError::InvalidState => Error::InvariantViolated, - UdpBindError::Unaddressable => Error::BadRequest, + UdpBindError::InvalidState => Error::invalid_socket_state(), + UdpBindError::Unaddressable => Error::unaddressable(), } } } @@ -88,8 +189,8 @@ impl From for Error { impl From for Error { fn from(value: RawBindError) -> Self { match value { - RawBindError::InvalidState => Error::InvariantViolated, - RawBindError::Unaddressable => Error::BadRequest, + RawBindError::InvalidState => Error::invalid_socket_state(), + RawBindError::Unaddressable => Error::unaddressable(), } } } @@ -97,8 +198,8 @@ impl From for Error { impl From for Error { fn from(value: IcmpBindError) -> Self { match value { - IcmpBindError::InvalidState => Error::InvariantViolated, - IcmpBindError::Unaddressable => Error::BadRequest, + IcmpBindError::InvalidState => Error::invalid_socket_state(), + IcmpBindError::Unaddressable => Error::unaddressable(), } } } @@ -106,7 +207,7 @@ impl From for Error { impl From for Error { fn from(value: TcpSendError) -> Self { match value { - TcpSendError::InvalidState => tcp::stream::Error::Reset.into(), + TcpSendError::InvalidState => Error::ConnectionReset, } } } @@ -114,8 +215,8 @@ impl From for Error { impl From for Error { fn from(value: UdpSendError) -> Self { match value { - UdpSendError::Unaddressable => Error::BadRequest, - UdpSendError::BufferFull => Error::BadRequest, + UdpSendError::Unaddressable => Error::unaddressable(), + UdpSendError::BufferFull => Error::buffer_full(), } } } @@ -123,12 +224,10 @@ impl From for Error { #[cfg(feature = "std")] impl From for std::io::Error { fn from(value: Error) -> Self { - use std::io::{Error as StdErr, ErrorKind}; - match value { - Error::BadRequest => StdErr::new(ErrorKind::InvalidInput, Error::BadRequest), - Error::TcpStream(s) => s.into(), - other => StdErr::other(other), + Error::BadRequest(_) => std::io::ErrorKind::InvalidInput.into(), + Error::ConnectionReset => std::io::ErrorKind::ConnectionReset.into(), + other => std::io::Error::other(other), } } } diff --git a/ts_netstack_smoltcp_core/src/command/mod.rs b/ts_netstack_smoltcp_core/src/command/mod.rs index 0a4cbadb..e881bd30 100644 --- a/ts_netstack_smoltcp_core/src/command/mod.rs +++ b/ts_netstack_smoltcp_core/src/command/mod.rs @@ -14,8 +14,8 @@ pub mod tcp; pub mod udp; pub use channel::{Channel, HasChannel}; -pub use error::Error; -pub use request::{request, request_blocking, request_nonblocking}; +pub use error::{Error, InternalErrorKind}; +pub use request::{ChannelClosedError, request, request_blocking, request_nonblocking}; /// Request to a netstack bearing a command to execute. /// @@ -86,7 +86,7 @@ macro_rules! impl_try_from { match response { Response::$variant(resp) => Ok(resp), Response::Error(e) => Err(e), - _ => Err(Error::WrongType), + _ => Err(Error::wrong_type()), } } } @@ -100,13 +100,11 @@ impl_try_from!(tcp::listen::Response, TcpListen); impl Response { /// Check if this response is the `Ok` variant, returning an error if not. - /// - /// The error is [`Error::WrongType`] on a type mismatch, otherwise the contained error. pub fn to_ok(self) -> Result<(), Error> { match self { Response::Ok => Ok(()), Response::Error(e) => Err(e), - _ => Err(Error::WrongType), + _ => Err(Error::wrong_type()), } } } @@ -115,8 +113,6 @@ impl Response { /// /// The second argument is a pattern, as if it were being used in a match expression. /// -/// Traces an error and returns [`Error::WrongType`] if the response is the wrong variant. -/// /// # Example /// /// ```rust @@ -136,7 +132,7 @@ macro_rules! try_response_as { let $pat = $resp.try_into()? else { ::tracing::error!("invalid response type"); - return Err($crate::Error::WrongType.into()); + return Err($crate::Error::wrong_type().into()); }; }; } diff --git a/ts_netstack_smoltcp_core/src/command/request.rs b/ts_netstack_smoltcp_core/src/command/request.rs index a156622d..c33b783e 100644 --- a/ts_netstack_smoltcp_core/src/command/request.rs +++ b/ts_netstack_smoltcp_core/src/command/request.rs @@ -12,24 +12,36 @@ use crate::{ /// Helper trait to abstract over [`flume::Sender`] and [`flume::WeakSender`]. pub trait UpgradableChannel { /// Attempt to upgrade this value to [`flume::Sender`]. - fn upgrade(&self) -> Option>>; + fn upgrade(&self) -> Result>, ChannelClosedError>; +} + +/// An error signifying that the remote end of the channel has closed. +#[derive(Debug, thiserror::Error, Clone, Copy, Eq, PartialEq)] +#[error("the remote end of the channel has closed")] +pub struct ChannelClosedError; - /// Attempt to upgrade this value to [`flume::Sender`], returning - /// [`Error::ChannelClosed`] on failure. - fn upgrade_or_err(&self) -> Result>, Error> { - self.upgrade().ok_or(Error::ChannelClosed) +impl From for Error { + fn from(_: ChannelClosedError) -> Self { + Error::Internal(crate::InternalErrorKind::InternalChannelClosed) + } +} + +#[cfg(feature = "std")] +impl From for std::io::Error { + fn from(_: ChannelClosedError) -> Self { + std::io::ErrorKind::BrokenPipe.into() } } impl UpgradableChannel for flume::Sender { - fn upgrade(&self) -> Option>> { - Some(self) + fn upgrade(&self) -> Result>, ChannelClosedError> { + Ok(self) } } impl UpgradableChannel for flume::WeakSender { - fn upgrade(&self) -> Option>> { - self.upgrade() + fn upgrade(&self) -> Result>, ChannelClosedError> { + flume::WeakSender::::upgrade(self).ok_or(ChannelClosedError) } } @@ -37,7 +49,7 @@ impl UpgradableChannel for &T where T: UpgradableChannel + ?Sized, { - fn upgrade(&self) -> Option>> { + fn upgrade(&self) -> Result>, ChannelClosedError> { T::upgrade(self) } } @@ -49,25 +61,27 @@ pub fn request_blocking( command_tx: impl UpgradableChannel, handle: Option, command: impl Into, -) -> Result { +) -> Result { // wrapper to minimize monomorphization impact fn _request_blocking( command_tx: &flume::Sender, handle: Option, command: Command, - ) -> Result { + ) -> Result { let (resp_tx, resp_rx) = flume::bounded(1); - command_tx.send(Request { - handle, - command, - resp: resp_tx, - })?; + command_tx + .send(Request { + handle, + command, + resp: resp_tx, + }) + .map_err(|_| ChannelClosedError)?; - resp_rx.recv().map_err(Error::from) + resp_rx.recv().map_err(|_| ChannelClosedError) } - let ch = command_tx.upgrade_or_err()?; + let ch = command_tx.upgrade()?; let ch = ch.borrow(); _request_blocking(ch, handle, command.into()) @@ -102,7 +116,7 @@ pub fn request( // impl Future and the returned async block below are required to do this bit of work upgrading // the channel and converting the command outside the async context for lifetime reasons - let ch = command_tx.upgrade_or_err().map(|x| x.borrow().clone()); + let ch = command_tx.upgrade().map(|x| x.borrow().clone()); let command = command.into(); async move { @@ -113,7 +127,7 @@ pub fn request( /// Send a request without blocking on the response. /// -/// Returns an error only if the request can't be sent ([`Error::ChannelClosed`]). +/// Returns an error only if the request can't be sent. /// /// Mainly intended for use in [`Drop`] implementations, as they can't be `async` /// and it would be frequently be surprising if they blocked the calling thread. @@ -121,13 +135,13 @@ pub fn request_nonblocking( command_tx: impl UpgradableChannel, handle: Option, command: impl Into, -) -> Result<(), Error> { +) -> Result<(), ChannelClosedError> { // wrapper to minimize monomorphization impact fn _request_nonblocking( command_tx: &flume::Sender, handle: Option, command: Command, - ) -> Result<(), Error> { + ) -> Result<(), ChannelClosedError> { let (resp_tx, _resp_rx) = flume::bounded(1); match command_tx.try_send(Request { @@ -136,11 +150,11 @@ pub fn request_nonblocking( resp: resp_tx, }) { Ok(_) | Err(flume::TrySendError::Full(_)) => Ok(()), - Err(flume::TrySendError::Disconnected(_)) => Err(Error::ChannelClosed), + Err(flume::TrySendError::Disconnected(_)) => Err(ChannelClosedError), } } - let ch = command_tx.upgrade_or_err()?; + let ch = command_tx.upgrade()?; let ch = ch.borrow(); _request_nonblocking(ch, handle, command.into()) diff --git a/ts_netstack_smoltcp_core/src/command/tcp/stream.rs b/ts_netstack_smoltcp_core/src/command/tcp/stream.rs index 831d7fb4..fd53da21 100644 --- a/ts_netstack_smoltcp_core/src/command/tcp/stream.rs +++ b/ts_netstack_smoltcp_core/src/command/tcp/stream.rs @@ -10,20 +10,15 @@ use smoltcp::iface::SocketHandle; use crate::command; -/// Errors that may occur on TCP socket operations. +/// The connection was reset or closed. #[derive(thiserror::Error, Debug, Copy, Clone, PartialEq, Eq)] -pub enum Error { - /// The connection was reset or closed. - #[error("connection reset or closed")] - Reset, -} +#[error("connection reset or closed")] +pub struct ConnectionResetOrClosed; #[cfg(feature = "std")] -impl From for std::io::Error { - fn from(value: Error) -> Self { - match value { - Error::Reset => std::io::Error::new(std::io::ErrorKind::ConnectionReset, value), - } +impl From for std::io::Error { + fn from(_: ConnectionResetOrClosed) -> Self { + std::io::ErrorKind::ConnectionReset.into() } } diff --git a/ts_netstack_smoltcp_core/src/lib.rs b/ts_netstack_smoltcp_core/src/lib.rs index 879056d5..2b7fcc43 100644 --- a/ts_netstack_smoltcp_core/src/lib.rs +++ b/ts_netstack_smoltcp_core/src/lib.rs @@ -35,8 +35,8 @@ mod wake_device; #[doc(inline)] pub use command::{ - Channel, Command, Error, HasChannel, Request, Response, raw, request, request_blocking, - request_nonblocking, stack_control, tcp, udp, + Channel, ChannelClosedError, Command, Error, HasChannel, InternalErrorKind, Request, Response, + raw, request, request_blocking, request_nonblocking, stack_control, tcp, udp, }; pub use config::Config; pub use pipe::{Pipe, PipeDev}; diff --git a/ts_netstack_smoltcp_core/src/socket_impl/mod.rs b/ts_netstack_smoltcp_core/src/socket_impl/mod.rs index 7e22da00..754d1ea1 100644 --- a/ts_netstack_smoltcp_core/src/socket_impl/mod.rs +++ b/ts_netstack_smoltcp_core/src/socket_impl/mod.rs @@ -11,7 +11,7 @@ macro_rules! unwrap_handle { None => { tracing::error!(?handle, "no socket handle"); - return $crate::command::Error::BadRequest.into(); + return $crate::command::Error::missing_socket().into(); } } }}; diff --git a/ts_netstack_smoltcp_core/src/socket_impl/raw.rs b/ts_netstack_smoltcp_core/src/socket_impl/raw.rs index 406c9439..21faa746 100644 --- a/ts_netstack_smoltcp_core/src/socket_impl/raw.rs +++ b/ts_netstack_smoltcp_core/src/socket_impl/raw.rs @@ -44,7 +44,7 @@ impl Netstack { "send can never succeed, packet size is greater than socket buffer cap" ); - return Response::Error(Error::BadRequest); + return Response::Error(Error::big_packet()); } match sock.send_slice(&buf) { diff --git a/ts_netstack_smoltcp_core/src/socket_impl/tcp/listener.rs b/ts_netstack_smoltcp_core/src/socket_impl/tcp/listener.rs index 04363e4b..37c243fd 100644 --- a/ts_netstack_smoltcp_core/src/socket_impl/tcp/listener.rs +++ b/ts_netstack_smoltcp_core/src/socket_impl/tcp/listener.rs @@ -90,7 +90,7 @@ impl Netstack { TcpListenCommand::Accept { handle } => { let Some(listener) = self.tcp_listeners.get_mut(&handle) else { tracing::error!(?handle, "listener does not exist"); - return Error::BadRequest.into(); + return Error::missing_listener().into(); }; // Iterate the half-open queue, re-queueing any sockets that are still in @@ -175,7 +175,7 @@ impl Netstack { TcpListenCommand::Close { handle } => { let Some(listener) = self.tcp_listeners.remove(&handle) else { tracing::error!(?handle, "listener does not exist"); - return Error::BadRequest.into(); + return Error::missing_listener().into(); }; let sock = self diff --git a/ts_netstack_smoltcp_core/src/socket_impl/tcp/stream.rs b/ts_netstack_smoltcp_core/src/socket_impl/tcp/stream.rs index 4fcb8cb8..11b60413 100644 --- a/ts_netstack_smoltcp_core/src/socket_impl/tcp/stream.rs +++ b/ts_netstack_smoltcp_core/src/socket_impl/tcp/stream.rs @@ -5,9 +5,7 @@ use crate::{ Netstack, command::{ Error, Response, - tcp::stream::{ - Command as TcpStreamCommand, Error as TcpStreamError, Response as TcpStreamResponse, - }, + tcp::stream::{Command as TcpStreamCommand, Response as TcpStreamResponse}, }, }; @@ -67,7 +65,7 @@ impl Netstack { Ok(n) => TcpStreamResponse::Sent { n }.into(), Err(tcp::SendError::InvalidState) => { tracing::error!(state = %sock.state(), "invalid socket state for send"); - Error::InvariantViolated.into() + Response::Error(Error::invalid_socket_state()) } } } @@ -93,7 +91,7 @@ impl Netstack { Err(tcp::RecvError::Finished) => TcpStreamResponse::Finished.into(), Err(tcp::RecvError::InvalidState) => { tracing::error!(state = %sock.state(), "invalid socket state for recv"); - Error::InvariantViolated.into() + Response::Error(Error::invalid_socket_state()) } } } @@ -146,7 +144,7 @@ impl Netstack { _ => { tracing::warn!("connecting socket was reset or closed"); self.pending_tcp_closes.push(handle); - Response::Error(TcpStreamError::Reset.into()) + Response::Error(Error::ConnectionReset) } } } diff --git a/ts_netstack_smoltcp_core/src/socket_impl/udp.rs b/ts_netstack_smoltcp_core/src/socket_impl/udp.rs index 6e4928b1..63ff6b4b 100644 --- a/ts_netstack_smoltcp_core/src/socket_impl/udp.rs +++ b/ts_netstack_smoltcp_core/src/socket_impl/udp.rs @@ -23,7 +23,7 @@ impl crate::Netstack { if endpoint.port() == 0 { tracing::error!(?endpoint, "udp bind: zero port"); - return Response::Error(Error::BadRequest); + return Response::Error(Error::unaddressable()); } // The two possible failure cases for `bind` are that the port is zero or the socket @@ -49,7 +49,7 @@ impl crate::Netstack { "requested message size overflows socket capacity", ); - return Response::Error(Error::BadRequest); + return Response::Error(Error::big_packet()); } match sock.send_slice(&buf, endpoint) { @@ -63,7 +63,7 @@ impl crate::Netstack { }, Err(udp::SendError::Unaddressable) => { tracing::error!(?endpoint, "invalid endpoint"); - Response::Error(Error::BadRequest) + Response::Error(Error::unaddressable()) } } } diff --git a/ts_netstack_smoltcp_core/src/stack_control_impl.rs b/ts_netstack_smoltcp_core/src/stack_control_impl.rs index 332c52b5..9f1df0d8 100644 --- a/ts_netstack_smoltcp_core/src/stack_control_impl.rs +++ b/ts_netstack_smoltcp_core/src/stack_control_impl.rs @@ -53,7 +53,7 @@ impl Netstack { "not enough address storage space configured in smoltcp" ); - return Error::BadRequest.into(); + return Error::buffer_full().into(); } Response::Ok diff --git a/ts_netstack_smoltcp_socket/src/lib.rs b/ts_netstack_smoltcp_socket/src/lib.rs index 8171b250..41f71caf 100644 --- a/ts_netstack_smoltcp_socket/src/lib.rs +++ b/ts_netstack_smoltcp_socket/src/lib.rs @@ -19,7 +19,7 @@ macro_rules! socket_requestor_impl { fn request_blocking( &self, command: impl Into<$crate::netcore::Command>, - ) -> Result<$crate::netcore::Response, $crate::netcore::Error> { + ) -> Result<$crate::netcore::Response, $crate::netcore::ChannelClosedError> { ::netcore::HasChannel::request_blocking(&self.sender, Some(self.handle), command) } diff --git a/ts_netstack_smoltcp_socket/src/raw.rs b/ts_netstack_smoltcp_socket/src/raw.rs index 1f05ef5f..d75187e4 100644 --- a/ts_netstack_smoltcp_socket/src/raw.rs +++ b/ts_netstack_smoltcp_socket/src/raw.rs @@ -67,7 +67,7 @@ impl RawSocket { /// Receive a raw packet into `buf` from the network. pub fn recv_blocking(&self, buf: &mut [u8]) -> Result { - let len = NonZeroUsize::new(buf.len()).ok_or(netcore::Error::BadRequest)?; + let len = NonZeroUsize::new(buf.len()).ok_or(netcore::Error::zero_buffer())?; let resp = self.request_blocking(raw::Command::Recv { max_len: Some(len) })?; self._recv(buf, resp) @@ -75,7 +75,7 @@ impl RawSocket { /// Receive a raw packet into `buf` from the network. pub async fn recv(&self, buf: &mut [u8]) -> Result { - let len = NonZeroUsize::new(buf.len()).ok_or(netcore::Error::BadRequest)?; + let len = NonZeroUsize::new(buf.len()).ok_or(netcore::Error::zero_buffer())?; let resp = self .request(raw::Command::Recv { max_len: Some(len) }) .await?; diff --git a/ts_netstack_smoltcp_socket/src/tcp/stream.rs b/ts_netstack_smoltcp_socket/src/tcp/stream.rs index b76f3115..d873be13 100644 --- a/ts_netstack_smoltcp_socket/src/tcp/stream.rs +++ b/ts_netstack_smoltcp_socket/src/tcp/stream.rs @@ -190,7 +190,7 @@ impl TcpStream { match resp.try_into()? { tcp::stream::Response::Recv { buf } => Ok(buf), tcp::stream::Response::Finished => Ok(Bytes::new()), - _ => Err(netcore::Error::WrongType), + _ => Err(netcore::Error::wrong_type()), } })); } diff --git a/ts_netstack_smoltcp_socket/src/udp.rs b/ts_netstack_smoltcp_socket/src/udp.rs index 4cfe1fea..1557f436 100644 --- a/ts_netstack_smoltcp_socket/src/udp.rs +++ b/ts_netstack_smoltcp_socket/src/udp.rs @@ -55,7 +55,7 @@ impl UdpSocket { &self, buf: &mut [u8], ) -> Result<(SocketAddr, usize), netcore::Error> { - let len = NonZeroUsize::new(buf.len()).ok_or(netcore::Error::BadRequest)?; + let len = NonZeroUsize::new(buf.len()).ok_or(netcore::Error::zero_buffer())?; let resp = self.request_blocking(udp::Command::Recv { max_len: Some(len) })?; @@ -70,7 +70,7 @@ impl UdpSocket { /// Receive a packet into the given buffer. pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(SocketAddr, usize), netcore::Error> { - let len = NonZeroUsize::new(buf.len()).ok_or(netcore::Error::BadRequest)?; + let len = NonZeroUsize::new(buf.len()).ok_or(netcore::Error::zero_buffer())?; let resp = self .request(udp::Command::Recv { max_len: Some(len) }) diff --git a/ts_runtime/src/control_runner.rs b/ts_runtime/src/control_runner.rs index 14f94cbb..7948a136 100644 --- a/ts_runtime/src/control_runner.rs +++ b/ts_runtime/src/control_runner.rs @@ -12,7 +12,7 @@ use kameo::{ reply::DelegatedReply, }; use tokio::sync::watch; -use ts_control::{AsyncControlClient, Node, StateUpdate}; +use ts_control::{AsyncControlClient, Error as ControlError, Node, StateUpdate}; use crate::derp_latency::{DerpLatencyMeasurement, DerpLatencyMeasurer}; @@ -42,7 +42,7 @@ pub struct Params { #[derive(Debug, thiserror::Error)] pub enum ControlRunnerError { #[error(transparent)] - Control(#[from] ts_control::Error), + Control(#[from] ControlError), #[error(transparent)] Crate(#[from] crate::Error), @@ -54,19 +54,20 @@ impl kameo::Actor for ControlRunner { async fn on_start(params: Params, slf: ActorRef) -> Result { loop { - let ts_control::AuthResult::AuthRequired(u) = AsyncControlClient::check_auth( + match AsyncControlClient::check_auth( ¶ms.config, ¶ms.env.keys, params.auth_key.as_deref(), ) .await - .map_err(ts_control::Error::from)? - else { - break; - }; - - tracing::info!(auth_url = %u, "please authorize this machine or pass an auth key"); - tokio::time::sleep(Duration::from_secs(5)).await; + { + Ok(()) => break, + Err(ControlError::MachineNotAuthorized(u)) => { + tracing::info!(auth_url = %u, "please authorize this machine or pass an auth key"); + tokio::time::sleep(Duration::from_secs(5)).await; + } + Err(e) => return Err(e.into()), + } } let (client, stream) = AsyncControlClient::connect( @@ -74,8 +75,7 @@ impl kameo::Actor for ControlRunner { ¶ms.env.keys, params.auth_key.as_deref(), ) - .await - .map_err(ts_control::Error::from)?; + .await?; DerpLatencyMeasurer::spawn_link(&slf, params.env.clone()).await; @@ -224,8 +224,6 @@ impl Message for ControlRunner { tracing::debug!(selected_region_id = ?result.id, "updating home region"); - if let Err(e) = self.client.set_home_region(result.id, iter).await { - tracing::error!(error = %e, "setting home region"); - } + self.client.set_home_region(result.id, iter).await; } } diff --git a/ts_tls_util/src/lib.rs b/ts_tls_util/src/lib.rs index 2566f907..6e4e32f7 100644 --- a/ts_tls_util/src/lib.rs +++ b/ts_tls_util/src/lib.rs @@ -25,7 +25,7 @@ static ROOT_CERT_STORE: LazyLock> = LazyLock::new(|| { /// Establishes a TLS stream with a server over an existing connection. /// /// See module-level documentation for information on root certificates. -pub async fn connect(server_name: ServerName<'_>, io: Io) -> tokio::io::Result> +pub async fn connect(server_name: ServerName<'_>, io: Io) -> std::io::Result> where Io: AsyncRead + AsyncWrite + Unpin, {