- error.rs: Structured error types with context for all subsystems (transport, routing, crypto, protocol, store, config) - config.rs: Runtime configuration with TOML parsing and validation - metrics.rs: Counter/gauge/histogram metrics with transport-specific tracking and JSON-serializable snapshots - rate_limit.rs: Token bucket rate limiting with per-peer tracking, duty cycle enforcement for LoRa, and backpressure control These modules provide the foundation for production deployment.
355 lines
9.4 KiB
Rust
355 lines
9.4 KiB
Rust
//! Production-ready error types for the mesh P2P layer.
|
|
//!
|
|
//! This module provides structured error types with context for debugging
|
|
//! and recovery. Errors are categorized by subsystem for easier handling.
|
|
|
|
use std::fmt;
|
|
|
|
use thiserror::Error;
|
|
|
|
use crate::address::MeshAddress;
|
|
use crate::transport::TransportAddr;
|
|
|
|
/// Top-level mesh error type.
|
|
#[derive(Debug, Error)]
|
|
pub enum MeshError {
|
|
/// Transport layer errors.
|
|
#[error("transport error: {0}")]
|
|
Transport(#[from] TransportError),
|
|
|
|
/// Routing errors.
|
|
#[error("routing error: {0}")]
|
|
Routing(#[from] RoutingError),
|
|
|
|
/// Crypto/encryption errors.
|
|
#[error("crypto error: {0}")]
|
|
Crypto(#[from] CryptoError),
|
|
|
|
/// Protocol errors (malformed messages, version mismatch).
|
|
#[error("protocol error: {0}")]
|
|
Protocol(#[from] ProtocolError),
|
|
|
|
/// Store/cache errors.
|
|
#[error("store error: {0}")]
|
|
Store(#[from] StoreError),
|
|
|
|
/// Configuration errors.
|
|
#[error("config error: {0}")]
|
|
Config(#[from] ConfigError),
|
|
|
|
/// Internal errors (bugs, invariant violations).
|
|
#[error("internal error: {0}")]
|
|
Internal(String),
|
|
}
|
|
|
|
/// Transport layer errors.
|
|
#[derive(Debug, Error)]
|
|
pub enum TransportError {
|
|
/// Failed to send data.
|
|
#[error("send failed to {dest}: {reason}")]
|
|
SendFailed { dest: String, reason: String },
|
|
|
|
/// Failed to receive data.
|
|
#[error("receive failed: {0}")]
|
|
ReceiveFailed(String),
|
|
|
|
/// Connection failed or lost.
|
|
#[error("connection to {dest} failed: {reason}")]
|
|
ConnectionFailed { dest: String, reason: String },
|
|
|
|
/// Transport not available.
|
|
#[error("transport '{name}' not available")]
|
|
NotAvailable { name: String },
|
|
|
|
/// No transports registered.
|
|
#[error("no transports registered")]
|
|
NoTransports,
|
|
|
|
/// MTU exceeded.
|
|
#[error("payload {size} bytes exceeds MTU {mtu} bytes")]
|
|
MtuExceeded { size: usize, mtu: usize },
|
|
|
|
/// Duty cycle limit reached.
|
|
#[error("duty cycle limit reached: {used_ms}ms used of {limit_ms}ms allowed")]
|
|
DutyCycleExceeded { used_ms: u64, limit_ms: u64 },
|
|
|
|
/// Timeout waiting for response.
|
|
#[error("timeout waiting for response from {dest}")]
|
|
Timeout { dest: String },
|
|
|
|
/// I/O error.
|
|
#[error("I/O error: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
|
|
/// Routing errors.
|
|
#[derive(Debug, Error)]
|
|
pub enum RoutingError {
|
|
/// No route to destination.
|
|
#[error("no route to {0}")]
|
|
NoRoute(String),
|
|
|
|
/// Route expired.
|
|
#[error("route to {dest} expired (last seen {age_secs}s ago)")]
|
|
RouteExpired { dest: String, age_secs: u64 },
|
|
|
|
/// Too many hops.
|
|
#[error("max hops ({max}) exceeded for message to {dest}")]
|
|
MaxHopsExceeded { dest: String, max: u8 },
|
|
|
|
/// Message expired.
|
|
#[error("message expired (TTL {ttl_secs}s, age {age_secs}s)")]
|
|
MessageExpired { ttl_secs: u32, age_secs: u64 },
|
|
|
|
/// Duplicate message (dedup).
|
|
#[error("duplicate message ID {0}")]
|
|
Duplicate(String),
|
|
|
|
/// Routing table full.
|
|
#[error("routing table full ({capacity} entries)")]
|
|
TableFull { capacity: usize },
|
|
}
|
|
|
|
/// Crypto/encryption errors.
|
|
#[derive(Debug, Error)]
|
|
pub enum CryptoError {
|
|
/// Signature verification failed.
|
|
#[error("signature verification failed for {context}")]
|
|
SignatureInvalid { context: String },
|
|
|
|
/// Decryption failed.
|
|
#[error("decryption failed: {0}")]
|
|
DecryptionFailed(String),
|
|
|
|
/// Key not found.
|
|
#[error("key not found for {0}")]
|
|
KeyNotFound(String),
|
|
|
|
/// KeyPackage invalid or expired.
|
|
#[error("KeyPackage invalid: {0}")]
|
|
KeyPackageInvalid(String),
|
|
|
|
/// Replay attack detected.
|
|
#[error("replay detected: sequence {seq} already seen from {sender}")]
|
|
ReplayDetected { sender: String, seq: u32 },
|
|
|
|
/// Wrong epoch.
|
|
#[error("wrong epoch: expected {expected}, got {got}")]
|
|
WrongEpoch { expected: u16, got: u16 },
|
|
|
|
/// MLS error (from openmls).
|
|
#[error("MLS error: {0}")]
|
|
Mls(String),
|
|
}
|
|
|
|
/// Protocol errors.
|
|
#[derive(Debug, Error)]
|
|
pub enum ProtocolError {
|
|
/// Unknown message type.
|
|
#[error("unknown message type: 0x{0:02x}")]
|
|
UnknownMessageType(u8),
|
|
|
|
/// Invalid message format.
|
|
#[error("invalid message format: {0}")]
|
|
InvalidFormat(String),
|
|
|
|
/// Version mismatch.
|
|
#[error("protocol version mismatch: expected {expected}, got {got}")]
|
|
VersionMismatch { expected: u8, got: u8 },
|
|
|
|
/// Required field missing.
|
|
#[error("required field missing: {0}")]
|
|
MissingField(String),
|
|
|
|
/// CBOR decode error.
|
|
#[error("CBOR decode error: {0}")]
|
|
CborDecode(String),
|
|
|
|
/// CBOR encode error.
|
|
#[error("CBOR encode error: {0}")]
|
|
CborEncode(String),
|
|
|
|
/// Message too large.
|
|
#[error("message too large: {size} bytes (max {max})")]
|
|
MessageTooLarge { size: usize, max: usize },
|
|
}
|
|
|
|
/// Store/cache errors.
|
|
#[derive(Debug, Error)]
|
|
pub enum StoreError {
|
|
/// Store is full.
|
|
#[error("store full: {current}/{capacity} items")]
|
|
Full { current: usize, capacity: usize },
|
|
|
|
/// Item not found.
|
|
#[error("item not found: {0}")]
|
|
NotFound(String),
|
|
|
|
/// Persistence error.
|
|
#[error("persistence error: {0}")]
|
|
Persistence(String),
|
|
|
|
/// Serialization error.
|
|
#[error("serialization error: {0}")]
|
|
Serialization(String),
|
|
}
|
|
|
|
/// Configuration errors.
|
|
#[derive(Debug, Error)]
|
|
pub enum ConfigError {
|
|
/// Invalid configuration value.
|
|
#[error("invalid config value for '{key}': {reason}")]
|
|
InvalidValue { key: String, reason: String },
|
|
|
|
/// Missing required configuration.
|
|
#[error("missing required config: {0}")]
|
|
Missing(String),
|
|
|
|
/// Configuration parse error.
|
|
#[error("config parse error: {0}")]
|
|
Parse(String),
|
|
}
|
|
|
|
/// Result type alias for mesh operations.
|
|
pub type MeshResult<T> = Result<T, MeshError>;
|
|
|
|
/// Error context extension trait for adding context to errors.
|
|
pub trait ErrorContext<T> {
|
|
/// Add context to an error.
|
|
fn context(self, context: impl Into<String>) -> MeshResult<T>;
|
|
|
|
/// Add context with a closure (lazy evaluation).
|
|
fn with_context<F>(self, f: F) -> MeshResult<T>
|
|
where
|
|
F: FnOnce() -> String;
|
|
}
|
|
|
|
impl<T, E: Into<MeshError>> ErrorContext<T> for Result<T, E> {
|
|
fn context(self, context: impl Into<String>) -> MeshResult<T> {
|
|
self.map_err(|e| {
|
|
let err = e.into();
|
|
MeshError::Internal(format!("{}: {}", context.into(), err))
|
|
})
|
|
}
|
|
|
|
fn with_context<F>(self, f: F) -> MeshResult<T>
|
|
where
|
|
F: FnOnce() -> String,
|
|
{
|
|
self.map_err(|e| {
|
|
let err = e.into();
|
|
MeshError::Internal(format!("{}: {}", f(), err))
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Convert anyhow errors to MeshError.
|
|
impl From<anyhow::Error> for MeshError {
|
|
fn from(e: anyhow::Error) -> Self {
|
|
MeshError::Internal(e.to_string())
|
|
}
|
|
}
|
|
|
|
/// Helper to create transport send errors.
|
|
impl TransportError {
|
|
pub fn send_failed(dest: &TransportAddr, reason: impl Into<String>) -> Self {
|
|
Self::SendFailed {
|
|
dest: dest.to_string(),
|
|
reason: reason.into(),
|
|
}
|
|
}
|
|
|
|
pub fn connection_failed(dest: &TransportAddr, reason: impl Into<String>) -> Self {
|
|
Self::ConnectionFailed {
|
|
dest: dest.to_string(),
|
|
reason: reason.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper to create routing errors.
|
|
impl RoutingError {
|
|
pub fn no_route(addr: &MeshAddress) -> Self {
|
|
Self::NoRoute(format!("{}", addr))
|
|
}
|
|
|
|
pub fn no_route_bytes(addr: &[u8]) -> Self {
|
|
Self::NoRoute(hex::encode(&addr[..8.min(addr.len())]))
|
|
}
|
|
}
|
|
|
|
/// Helper to create crypto errors.
|
|
impl CryptoError {
|
|
pub fn signature_invalid(context: impl Into<String>) -> Self {
|
|
Self::SignatureInvalid {
|
|
context: context.into(),
|
|
}
|
|
}
|
|
|
|
pub fn replay(sender: &MeshAddress, seq: u32) -> Self {
|
|
Self::ReplayDetected {
|
|
sender: format!("{}", sender),
|
|
seq,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper to create protocol errors.
|
|
impl ProtocolError {
|
|
pub fn cbor_decode(e: impl fmt::Display) -> Self {
|
|
Self::CborDecode(e.to_string())
|
|
}
|
|
|
|
pub fn cbor_encode(e: impl fmt::Display) -> Self {
|
|
Self::CborEncode(e.to_string())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn error_display() {
|
|
let err = TransportError::SendFailed {
|
|
dest: "tcp:127.0.0.1:8080".to_string(),
|
|
reason: "connection refused".to_string(),
|
|
};
|
|
assert!(err.to_string().contains("tcp:127.0.0.1:8080"));
|
|
assert!(err.to_string().contains("connection refused"));
|
|
}
|
|
|
|
#[test]
|
|
fn error_conversion() {
|
|
let transport_err = TransportError::NoTransports;
|
|
let mesh_err: MeshError = transport_err.into();
|
|
assert!(matches!(mesh_err, MeshError::Transport(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn routing_error_helpers() {
|
|
let addr = MeshAddress::from_bytes([0xAB; 16]);
|
|
let err = RoutingError::no_route(&addr);
|
|
assert!(err.to_string().contains("no route"));
|
|
}
|
|
|
|
#[test]
|
|
fn crypto_error_helpers() {
|
|
let addr = MeshAddress::from_bytes([0xCD; 16]);
|
|
let err = CryptoError::replay(&addr, 42);
|
|
assert!(err.to_string().contains("42"));
|
|
}
|
|
|
|
#[test]
|
|
fn context_extension() {
|
|
fn fallible() -> Result<(), TransportError> {
|
|
Err(TransportError::NoTransports)
|
|
}
|
|
|
|
let result: MeshResult<()> = fallible().context("during startup");
|
|
assert!(result.is_err());
|
|
let err_str = result.unwrap_err().to_string();
|
|
assert!(err_str.contains("during startup"));
|
|
}
|
|
}
|