use thiserror::Error;
#[derive(Debug, Error)]
pub enum NetError {
#[error("packet too short: need {need} bytes, got {got}")]
PacketTooShort {
need: usize,
got: usize,
},
#[error("invalid ethertype: {0:#06x}")]
InvalidEtherType(u16),
#[error("invalid IP version: {0}")]
InvalidIpVersion(u8),
#[error("unsupported protocol: {0}")]
UnsupportedProtocol(&'static str),
#[error("invalid packet: {reason}")]
InvalidPacket {
reason: &'static str,
},
#[cfg(all(feature = "linux", target_os = "linux"))]
#[error("descriptor error: {0}")]
Descriptor(#[from] zenith_linux::error::DescriptorError),
#[cfg(all(feature = "linux", target_os = "linux"))]
#[error("ring error: {0}")]
Ring(#[from] zenith_linux::error::RingError),
#[cfg(all(feature = "linux", target_os = "linux"))]
#[error("xsk socket error: {0}")]
XskSocket(#[from] zenith_linux::error::XskError),
#[cfg(all(feature = "linux", target_os = "linux"))]
#[error("umem error: {0}")]
Umem(#[from] zenith_linux::error::UmemError),
#[error("core error: {0}")]
Core(#[from] zenith_foundation::CoreError),
#[error("admission rejected: {reason}")]
AdmissionRejected {
reason: &'static str,
},
#[error("worker state error: {reason}")]
WorkerState {
reason: &'static str,
},
#[cfg(all(feature = "linux", target_os = "linux"))]
#[error("linux error: {0}")]
Linux(#[from] zenith_linux::error::LinuxError),
#[cfg(all(feature = "linux", target_os = "linux"))]
#[error("ebpf map error: {0}")]
Ebpf(#[from] zenith_ebpf::MapError),
#[error("internal error: {0}")]
Internal(&'static str),
#[error("connection table full: capacity={capacity}")]
ConnectionTableFull {
capacity: usize,
},
#[error("connection already exists: {key}")]
ConnectionExists {
key: String,
},
#[error("hash table full")]
HashTableFull,
#[error("invalid operation: {reason}")]
InvalidOperation {
reason: String,
},
#[error("resource limit exceeded: {0}")]
ResourceLimit(String),
#[error("quic protocol error: {reason}")]
QuicError {
reason: String,
},
}
impl NetError {
#[inline]
pub fn is_recoverable(&self) -> bool {
matches!(
self,
Self::PacketTooShort { .. }
| Self::InvalidEtherType(_)
| Self::InvalidIpVersion(_)
| Self::UnsupportedProtocol(_)
| Self::InvalidPacket { .. }
| Self::AdmissionRejected { .. }
)
}
#[inline]
pub fn is_fatal(&self) -> bool {
#[cfg(all(feature = "linux", target_os = "linux"))]
{
matches!(
self,
Self::Descriptor(_)
| Self::Ring(_)
| Self::XskSocket(_)
| Self::Umem(_)
| Self::Core(_)
| Self::WorkerState { .. }
| Self::Linux(_)
| Self::Internal(_)
)
}
#[cfg(not(all(feature = "linux", target_os = "linux")))]
{
matches!(
self,
Self::Core(_) | Self::WorkerState { .. } | Self::Internal(_)
)
}
}
}
pub type Result<T> = std::result::Result<T, NetError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_packet_too_short_error() {
let err = NetError::PacketTooShort { need: 20, got: 10 };
let msg = format!("{}", err);
assert!(msg.contains("packet too short"));
assert!(msg.contains("20"));
assert!(msg.contains("10"));
assert!(err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_invalid_ethertype_error() {
let err = NetError::InvalidEtherType(0x1234);
let msg = format!("{}", err);
assert!(msg.contains("invalid ethertype"));
assert!(msg.contains("0x1234"));
assert!(err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_invalid_ip_version_error() {
let err = NetError::InvalidIpVersion(6);
let msg = format!("{}", err);
assert!(msg.contains("invalid IP version"));
assert!(msg.contains("6"));
assert!(err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_unsupported_protocol_error() {
let err = NetError::UnsupportedProtocol("SCTP");
let msg = format!("{}", err);
assert!(msg.contains("unsupported protocol"));
assert!(msg.contains("SCTP"));
assert!(err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_invalid_packet_error() {
let err = NetError::InvalidPacket {
reason: "bad checksum",
};
let msg = format!("{}", err);
assert!(msg.contains("invalid packet"));
assert!(msg.contains("bad checksum"));
assert!(err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_admission_rejected_error() {
let err = NetError::AdmissionRejected {
reason: "blacklisted IP",
};
let msg = format!("{}", err);
assert!(msg.contains("admission rejected"));
assert!(msg.contains("blacklisted IP"));
assert!(err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_worker_state_error() {
let err = NetError::WorkerState {
reason: "already running",
};
let msg = format!("{}", err);
assert!(msg.contains("worker state error"));
assert!(msg.contains("already running"));
assert!(!err.is_recoverable());
assert!(err.is_fatal());
}
#[test]
fn test_internal_error() {
let err = NetError::Internal("out of memory");
let msg = format!("{}", err);
assert!(msg.contains("internal error"));
assert!(msg.contains("out of memory"));
assert!(!err.is_recoverable());
assert!(err.is_fatal());
}
#[test]
fn test_connection_table_full_error() {
let err = NetError::ConnectionTableFull { capacity: 1024 };
let msg = format!("{}", err);
assert!(msg.contains("connection table full"));
assert!(msg.contains("1024"));
assert!(!err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_connection_exists_error() {
let err = NetError::ConnectionExists {
key: "10.0.0.1:8080".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("connection already exists"));
assert!(msg.contains("10.0.0.1:8080"));
assert!(!err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_hash_table_full_error() {
let err = NetError::HashTableFull;
let msg = format!("{}", err);
assert!(msg.contains("hash table full"));
assert!(!err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_invalid_operation_error() {
let err = NetError::InvalidOperation {
reason: "cannot close closed connection".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("invalid operation"));
assert!(msg.contains("cannot close closed connection"));
assert!(!err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_resource_limit_error() {
let err = NetError::ResourceLimit("too many open files".to_string());
let msg = format!("{}", err);
assert!(msg.contains("resource limit exceeded"));
assert!(msg.contains("too many open files"));
assert!(!err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_quic_error() {
let err = NetError::QuicError {
reason: "invalid packet".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("quic protocol error"));
assert!(msg.contains("invalid packet"));
assert!(!err.is_recoverable());
assert!(!err.is_fatal());
}
#[test]
fn test_error_classification_recoverable() {
let recoverable_errors = vec![
NetError::PacketTooShort { need: 10, got: 5 },
NetError::InvalidEtherType(0x0000),
NetError::InvalidIpVersion(0),
NetError::UnsupportedProtocol("test"),
NetError::InvalidPacket { reason: "test" },
NetError::AdmissionRejected { reason: "test" },
];
for err in &recoverable_errors {
assert!(err.is_recoverable(), "should be recoverable: {:?}", err);
assert!(!err.is_fatal(), "should not be fatal: {:?}", err);
}
}
#[test]
fn test_error_classification_fatal() {
let fatal_errors = vec![
NetError::WorkerState { reason: "test" },
NetError::Internal("test"),
];
for err in &fatal_errors {
assert!(err.is_fatal(), "should be fatal: {:?}", err);
assert!(!err.is_recoverable(), "should not be recoverable: {:?}", err);
}
}
#[test]
fn test_error_debug_format() {
let err = NetError::PacketTooShort { need: 20, got: 10 };
let debug = format!("{:?}", err);
assert!(debug.contains("PacketTooShort"));
assert!(debug.contains("need: 20"));
assert!(debug.contains("got: 10"));
}
#[test]
fn test_result_type_alias() {
let ok: usize = 42;
assert_eq!(ok, 42);
let err: Result<usize> = Err(NetError::Internal("test"));
assert!(err.is_err());
}
}