Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 5 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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!(
Expand All @@ -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);
}
}

Expand All @@ -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)
Expand Down
125 changes: 101 additions & 24 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,50 +1,127 @@
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<crate::netstack::InternalErrorKind> 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<ts_runtime::Error> 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),
}
}
}

impl From<NetstackError> 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),
}
}
}
18 changes: 10 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
Expand All @@ -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`].
Expand Down Expand Up @@ -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.
Expand All @@ -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(),
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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};
}

Expand Down Expand Up @@ -336,7 +338,7 @@ guarantees.

eprintln!("{}", warning.trim());

return Err(Error::InternalFailure);
return Err(Error::UnstableEnvVar);
};

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion ts_cli_util/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ pub async fn set_closest_derp(
)
}),
)
.await?;
.await;

Ok((*id, map.get(id).unwrap().servers.clone()))
}
18 changes: 9 additions & 9 deletions ts_control/src/control_dialer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -189,18 +189,18 @@ impl ControlDialer {
&mut self,
url: &Url,
machine_keys: &ts_keys::MachineKeyPair,
) -> Result<Http2<BytesBody>, ConnectionError> {
) -> Result<Http2<BytesBody>, 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()))?;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ok_or_else to skip the clone on the happy path


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!(
Expand All @@ -223,27 +223,27 @@ pub async fn complete_connection<Io>(
url: &Url,
machine_keys: &ts_keys::MachineKeyPair,
stream: Io,
) -> Result<Http2<BytesBody>, ConnectionError>
) -> Result<Http2<BytesBody>, 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()))?,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

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?;
Expand Down
Loading