Protocol selection previously belonged to tsng up and therefore to the service command line. A blocked or degraded carrier could only be removed by editing the service and restarting the whole agent. Make the ordered selection a local per-network setting instead. Add Agent::set_protocols and a local control request shared by CLI and tray. Keep tsng protocols without arguments as the protocol catalog; naming a network shows its selection and supplying a comma-separated list replaces it. Accept exact names, full ids and unique prefixes using shared selector rules; ambiguous selectors and invalid lists change nothing. An explicit none selects control-only operation. Offline edits take the directory lock and apply on the next start; live edits never write behind the daemon. Register every built-in plugin and transport in the CLI. Treat up --protocol as ordered initial defaults for networks without an override, allowing a protocol absent from the unit to be enabled later. Retain the targeting of unqualified startup options and allow explicitly scoped options to configure initially disabled protocols. Persist NULL for inherited preferences and an empty selection distinctly in SQLite schema 8; migration preserves existing identities, secrets, signed records and counters. Serialize changes with network lifecycle operations and commit preferences before applying or announcing them. Retire only the affected network's data links and E2E channels, cancel pending opens, and reject obsolete completion results by attempt id. Clear peer data channels when authenticated protocol capabilities change while retaining control sessions and topology. Replay allocations and peer identities after asynchronously prepared plugins become ready. A control-only start defers creation of the agent's single interface until data is enabled; plugins receive a sink that can use it later. Automatic carrier fallback continues to preserve E2E state; explicit policy changes renegotiate it. Identity derivation and peer wire formats are unchanged. Add a collapsible protocols row to each tray network tile, with a catalog provided by the daemon, descriptions, immediately applied checkboxes and preference-order buttons. Keep editing disabled until the worker refreshes status so a second click cannot overwrite an edit using a stale snapshot. Local IPC wire version is 20 for the new request and report fields; upgrade and restart agent, CLI and tray together. Crate release versions remain unchanged; the requested RC tag is v0.1.0-rc.30. Validation on Linux through nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets: 509 passed, 2 existing ignored tests, with inherited TSUNAGI_CONTROL_SOCKET and TSUNAGI_CONTROL_GROUP removed Tests use real iroh membership, SQLite, UDP, AWG and WireGuard encryption, QUIC carriers and memory TUNs. Cover either endpoint-id role, full-size packet delivery after switching, an unaffected second active network, saved selection after restart, empty/invalid selections, first enablement without restart, rapid changes racing old opens, live and offline CLI changes, startup-absent protocols and preservation of signed state through migration. Earlier compile and focused-test iterations caught initializer mistakes, a short test secret, asynchronous plugin allocation readiness, and an inherited system socket path; corrected before the successful final checks. Plain cargo was unavailable in the host shell, so validation used the Nix toolchain. Manual tray interaction, macOS/Windows, privileged host TUNs and public-network traversal were not run.
225 lines
7.4 KiB
Rust
225 lines
7.4 KiB
Rust
//! Error types for the whole library.
|
|
//!
|
|
//! The library never panics on untrusted network input: every decoding and
|
|
//! validation failure is represented as a [`ProtocolError`] and surfaced as a
|
|
//! rejected message or session, never as a process abort.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use crate::identity::NetworkId;
|
|
|
|
/// Convenient result alias used across the crate.
|
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|
|
|
/// Top level error type of the library.
|
|
#[derive(Debug, thiserror::Error)]
|
|
#[non_exhaustive]
|
|
pub enum Error {
|
|
/// An ordered local data-plane protocol selection is invalid.
|
|
#[error("invalid protocol selection: {0}")]
|
|
InvalidProtocols(String),
|
|
/// The supplied network name does not satisfy the documented rules.
|
|
#[error("invalid network name: {0}")]
|
|
InvalidNetworkName(&'static str),
|
|
|
|
/// The supplied network secret does not satisfy the documented rules.
|
|
///
|
|
/// The secret itself is never included in the message.
|
|
#[error("invalid network secret: {0}")]
|
|
InvalidNetworkSecret(&'static str),
|
|
|
|
/// A textual identifier could not be parsed.
|
|
#[error("invalid {kind}: {reason}")]
|
|
InvalidEncoding {
|
|
/// What was being parsed, e.g. `network id`.
|
|
kind: &'static str,
|
|
/// Why parsing failed.
|
|
reason: &'static str,
|
|
},
|
|
|
|
/// The mandatory state directory is already owned by another live agent.
|
|
///
|
|
/// This is an ownership lock, not a "file exists" check.
|
|
#[error("state directory {path} is owned by another running agent instance")]
|
|
StateLocked {
|
|
/// Directory that could not be locked.
|
|
path: PathBuf,
|
|
},
|
|
|
|
/// The mandatory state store is unusable. It is never silently recreated.
|
|
#[error("mandatory state store at {path} is unusable and was NOT reset: {reason}")]
|
|
StateCorrupted {
|
|
/// Path of the unusable store.
|
|
path: PathBuf,
|
|
/// Human readable reason, free of secrets.
|
|
reason: String,
|
|
},
|
|
|
|
/// The mandatory state store has a schema this build cannot handle.
|
|
#[error(
|
|
"state store schema version {found} is not supported by this build (supported: {supported})"
|
|
)]
|
|
UnsupportedSchema {
|
|
/// Version found on disk.
|
|
found: i64,
|
|
/// Version this build writes.
|
|
supported: i64,
|
|
},
|
|
|
|
/// A storage operation failed.
|
|
#[error("storage error: {0}")]
|
|
Storage(String),
|
|
|
|
/// A filesystem operation failed.
|
|
#[error("io error at {path}: {source}")]
|
|
Io {
|
|
/// Path involved in the failure.
|
|
path: PathBuf,
|
|
/// Underlying error.
|
|
#[source]
|
|
source: std::io::Error,
|
|
},
|
|
|
|
/// Binding or driving the iroh endpoint failed.
|
|
#[error("iroh endpoint error: {0}")]
|
|
Endpoint(String),
|
|
|
|
/// A control protocol violation.
|
|
#[error(transparent)]
|
|
Protocol(#[from] ProtocolError),
|
|
|
|
/// The requested network is not currently active on this agent.
|
|
#[error("network {0} is not active")]
|
|
NetworkNotActive(NetworkId),
|
|
|
|
/// The requested network is already active on this agent.
|
|
#[error("network {0} is already active")]
|
|
NetworkAlreadyActive(NetworkId),
|
|
|
|
/// The requested network is not configured in the state store.
|
|
#[error("network {0} is not configured")]
|
|
NetworkUnknown(NetworkId),
|
|
|
|
/// An overlay range overlaps networks already active on this agent.
|
|
#[error(
|
|
"network {network} cannot start: {range} overlaps active networks {conflicts:?}; switch to it to suspend those networks"
|
|
)]
|
|
NetworkRangeConflict {
|
|
/// Network that could not start.
|
|
network: NetworkId,
|
|
/// Its agreed or proposed range.
|
|
range: crate::state::Ipv4Range,
|
|
/// Active networks that must be suspended first.
|
|
conflicts: Vec<NetworkId>,
|
|
},
|
|
|
|
/// No session with that peer exists in the given network.
|
|
#[error("no authenticated session with peer {peer} in network {network}")]
|
|
NoSuchPeer {
|
|
/// Network the lookup was scoped to.
|
|
network: NetworkId,
|
|
/// Peer that was looked up, short form.
|
|
peer: String,
|
|
},
|
|
|
|
/// The agent is shutting down or already stopped.
|
|
#[error("agent is stopped")]
|
|
Stopped,
|
|
|
|
/// Something that had to answer in bounded time did not.
|
|
///
|
|
/// Reported instead of waited out: an agent that has wedged must not
|
|
/// leave a command hanging with nothing on screen and no way out.
|
|
#[error("{what} did not answer in time")]
|
|
Timeout {
|
|
/// What was asked, phrased so the message stands on its own.
|
|
what: String,
|
|
},
|
|
|
|
/// The request is understood and cannot be done as things stand.
|
|
#[error("{reason}")]
|
|
Rejected {
|
|
/// Why, phrased so the message stands on its own.
|
|
reason: String,
|
|
},
|
|
|
|
/// Discovery backend failure. Never fatal for the agent.
|
|
#[error("discovery error: {0}")]
|
|
Discovery(String),
|
|
}
|
|
|
|
/// Errors produced while speaking the control protocol.
|
|
///
|
|
/// These always result in rejecting a single message or a single session. They
|
|
/// never stop other networks, other peers, or the agent itself.
|
|
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
|
#[non_exhaustive]
|
|
pub enum ProtocolError {
|
|
/// The peer announced an unsupported control protocol version.
|
|
#[error("unsupported control protocol version {found} (this build speaks {supported})")]
|
|
UnsupportedVersion {
|
|
/// Version announced by the peer.
|
|
found: u16,
|
|
/// Version this build speaks.
|
|
supported: u16,
|
|
},
|
|
|
|
/// A frame header announced more bytes than the configured limit allows.
|
|
///
|
|
/// Checked *before* any buffer of that size is allocated.
|
|
#[error("frame of {announced} bytes exceeds the {limit} byte limit")]
|
|
FrameTooLarge {
|
|
/// Length announced in the frame header.
|
|
announced: u64,
|
|
/// Configured limit.
|
|
limit: usize,
|
|
},
|
|
|
|
/// A frame could not be decoded.
|
|
#[error("malformed frame: {0}")]
|
|
Malformed(&'static str),
|
|
|
|
/// The stream ended before a complete frame was read.
|
|
#[error("stream closed while reading a frame")]
|
|
StreamClosed,
|
|
|
|
/// An underlying stream read or write failed.
|
|
#[error("stream error: {0}")]
|
|
Stream(String),
|
|
|
|
/// The peer failed to prove knowledge of the derived network secret.
|
|
#[error("network authentication failed")]
|
|
AuthenticationFailed,
|
|
|
|
/// The peer asked for a network this agent does not have active.
|
|
#[error("peer requested an unknown or inactive network")]
|
|
UnknownNetwork,
|
|
|
|
/// A message carried a network id different from the session's network.
|
|
#[error("message network id does not match the authenticated session network")]
|
|
NetworkMismatch,
|
|
|
|
/// A regular control message arrived before the handshake completed.
|
|
#[error("control message received before authentication completed")]
|
|
NotAuthenticated,
|
|
|
|
/// A handshake step did not complete within the configured timeout.
|
|
#[error("handshake timed out")]
|
|
HandshakeTimeout,
|
|
|
|
/// A field exceeded its configured bound.
|
|
#[error("field `{field}` exceeds its limit ({len} > {limit})")]
|
|
FieldTooLarge {
|
|
/// Name of the offending field.
|
|
field: &'static str,
|
|
/// Observed length.
|
|
len: usize,
|
|
/// Configured limit.
|
|
limit: usize,
|
|
},
|
|
|
|
/// The connection is not usable for deriving channel binding material.
|
|
#[error("connection does not provide TLS exporter material: {0}")]
|
|
NoChannelBinding(String),
|
|
}
|