async_snmp/error/mod.rs
1//! Error types for async-snmp.
2//!
3//! This module provides:
4//!
5//! - [`Error`] - The main error type (8 variants covering all failure modes)
6//! - [`ErrorStatus`] - SNMP protocol errors returned by agents (RFC 3416)
7//! - [`WalkAbortReason`] - Reasons a walk operation was aborted
8//!
9//! # Error Handling
10//!
11//! Errors are boxed for efficiency: `Result<T> = Result<T, Box<Error>>`.
12//!
13//! ```rust
14//! use async_snmp::{Error, Result};
15//!
16//! fn handle_error(result: Result<()>) {
17//! match result {
18//! Ok(()) => println!("Success"),
19//! Err(e) => match &*e {
20//! Error::Timeout { target, retries, .. } => {
21//! println!("{} unreachable after {} retries", target, retries);
22//! }
23//! Error::Auth { target } => {
24//! println!("Authentication failed for {}", target);
25//! }
26//! _ => println!("Error: {}", e),
27//! }
28//! }
29//! }
30//! ```
31
32pub(crate) mod internal;
33
34use std::net::SocketAddr;
35use std::time::Duration;
36
37use crate::oid::Oid;
38
39/// Placeholder target address used when no target is known.
40///
41/// This sentinel value (0.0.0.0:0) is used in error contexts where the
42/// target address cannot be determined (e.g., parsing failures before
43/// the source address is known).
44pub(crate) const UNKNOWN_TARGET: SocketAddr =
45 SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0);
46
47// Pattern for converting detailed internal errors to simplified public errors:
48//
49// tracing::debug!(
50// target: "async_snmp::ber", // or ::auth, ::crypto, etc.
51// { snmp.offset = 42, snmp.decode_error = "ZeroLengthInteger" },
52// "decode error details here"
53// );
54// return Err(Error::MalformedResponse { target }.boxed());
55
56/// Result type alias using the library's boxed Error type.
57pub type Result<T> = std::result::Result<T, Box<Error>>;
58
59/// Reason a walk operation was aborted.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum WalkAbortReason {
62 /// Agent returned an OID that is not greater than the previous OID.
63 NonIncreasing,
64 /// Agent returned an OID that was already seen (cycle detected).
65 Cycle,
66}
67
68impl std::fmt::Display for WalkAbortReason {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 match self {
71 Self::NonIncreasing => write!(f, "non-increasing OID"),
72 Self::Cycle => write!(f, "cycle detected"),
73 }
74 }
75}
76
77impl std::error::Error for WalkAbortReason {}
78
79/// The main error type for all async-snmp operations.
80///
81/// This enum covers all possible error conditions including network issues,
82/// protocol errors, authentication failures, and configuration problems.
83///
84/// Errors are boxed (via [`Result`]) to keep the size small on the stack.
85///
86/// # Common Patterns
87///
88/// ## Checking Error Type
89///
90/// Use pattern matching to handle specific error conditions:
91///
92/// ```
93/// use async_snmp::{Error, ErrorStatus};
94///
95/// fn is_retriable(error: &Error) -> bool {
96/// matches!(error,
97/// Error::Timeout { .. } |
98/// Error::Network { .. }
99/// )
100/// }
101///
102/// fn is_access_error(error: &Error) -> bool {
103/// matches!(error,
104/// Error::Snmp { status: ErrorStatus::NoAccess | ErrorStatus::AuthorizationError, .. } |
105/// Error::Auth { .. }
106/// )
107/// }
108/// ```
109#[derive(Debug, thiserror::Error)]
110#[non_exhaustive]
111pub enum Error {
112 /// Network failure (connection refused, unreachable, etc.)
113 #[error("network error communicating with {target}: {source}")]
114 Network {
115 target: SocketAddr,
116 #[source]
117 source: std::io::Error,
118 },
119
120 /// Request timed out after retries.
121 #[error("timeout after {elapsed:?} waiting for {target} ({retries} retries)")]
122 Timeout {
123 target: SocketAddr,
124 elapsed: Duration,
125 retries: u32,
126 },
127
128 /// SNMP protocol error from agent.
129 #[error("SNMP error from {target}: {status} at index {index}")]
130 Snmp {
131 target: SocketAddr,
132 status: ErrorStatus,
133 index: u32,
134 oid: Option<Oid>,
135 },
136
137 /// Authentication/authorization failed.
138 #[error("authentication failed for {target}")]
139 Auth { target: SocketAddr },
140
141 /// Malformed response from agent.
142 #[error("malformed response from {target}")]
143 MalformedResponse { target: SocketAddr },
144
145 /// Walk aborted due to agent misbehavior.
146 #[error("walk aborted for {target}: {reason}")]
147 WalkAborted {
148 target: SocketAddr,
149 reason: WalkAbortReason,
150 },
151
152 /// Invalid configuration.
153 #[error("configuration error: {0}")]
154 Config(Box<str>),
155
156 /// Invalid OID format.
157 #[error("invalid OID: {0}")]
158 InvalidOid(Box<str>),
159}
160
161impl Error {
162 /// Box this error (convenience for constructing boxed errors).
163 #[must_use]
164 pub fn boxed(self) -> Box<Self> {
165 Box::new(self)
166 }
167}
168
169/// SNMP protocol error status codes (RFC 3416).
170///
171/// These codes are returned by SNMP agents to indicate the result of an operation.
172/// The error status is included in the [`Error::Snmp`] variant along with an error
173/// index indicating which varbind caused the error.
174///
175/// # Error Categories
176///
177/// ## `SNMPv1` Errors (0-5)
178///
179/// - `NoError` - Operation succeeded
180/// - `TooBig` - Response too large for transport
181/// - `NoSuchName` - OID not found (v1 only; v2c+ uses exceptions)
182/// - `BadValue` - Invalid value in SET
183/// - `ReadOnly` - Attempted write to read-only object
184/// - `GenErr` - Unspecified error
185///
186/// ## SNMPv2c/v3 Errors (6-18)
187///
188/// These provide more specific error information for SET operations:
189///
190/// - `NoAccess` - Object not accessible (access control)
191/// - `WrongType` - Value has wrong ASN.1 type
192/// - `WrongLength` - Value has wrong length
193/// - `WrongValue` - Value out of range or invalid
194/// - `NotWritable` - Object does not support SET
195/// - `AuthorizationError` - Access denied by VACM
196///
197/// # Example
198///
199/// ```
200/// use async_snmp::ErrorStatus;
201///
202/// let status = ErrorStatus::from_i32(2);
203/// assert_eq!(status, ErrorStatus::NoSuchName);
204/// assert_eq!(status.as_i32(), 2);
205/// println!("Error: {}", status); // prints "noSuchName"
206/// ```
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
208#[non_exhaustive]
209pub enum ErrorStatus {
210 /// Operation completed successfully (status = 0).
211 NoError,
212 /// Response message would be too large for transport (status = 1).
213 TooBig,
214 /// Requested OID not found (status = 2). `SNMPv1` only; v2c+ uses exception values.
215 NoSuchName,
216 /// Invalid value provided in SET request (status = 3).
217 BadValue,
218 /// Attempted to SET a read-only object (status = 4).
219 ReadOnly,
220 /// Unspecified error occurred (status = 5).
221 GenErr,
222 /// Object exists but access is denied (status = 6).
223 NoAccess,
224 /// SET value has wrong ASN.1 type (status = 7).
225 WrongType,
226 /// SET value has incorrect length (status = 8).
227 WrongLength,
228 /// SET value uses wrong encoding (status = 9).
229 WrongEncoding,
230 /// SET value is out of range or otherwise invalid (status = 10).
231 WrongValue,
232 /// Object does not support row creation (status = 11).
233 NoCreation,
234 /// Value is inconsistent with other managed objects (status = 12).
235 InconsistentValue,
236 /// Resource required for SET is unavailable (status = 13).
237 ResourceUnavailable,
238 /// SET commit phase failed (status = 14).
239 CommitFailed,
240 /// SET undo phase failed (status = 15).
241 UndoFailed,
242 /// Access denied by VACM (status = 16).
243 AuthorizationError,
244 /// Object does not support modification (status = 17).
245 NotWritable,
246 /// Named object cannot be created (status = 18).
247 InconsistentName,
248 /// Unknown or future error status code.
249 Unknown(i32),
250}
251
252impl ErrorStatus {
253 /// Create from raw status code.
254 pub fn from_i32(value: i32) -> Self {
255 match value {
256 0 => Self::NoError,
257 1 => Self::TooBig,
258 2 => Self::NoSuchName,
259 3 => Self::BadValue,
260 4 => Self::ReadOnly,
261 5 => Self::GenErr,
262 6 => Self::NoAccess,
263 7 => Self::WrongType,
264 8 => Self::WrongLength,
265 9 => Self::WrongEncoding,
266 10 => Self::WrongValue,
267 11 => Self::NoCreation,
268 12 => Self::InconsistentValue,
269 13 => Self::ResourceUnavailable,
270 14 => Self::CommitFailed,
271 15 => Self::UndoFailed,
272 16 => Self::AuthorizationError,
273 17 => Self::NotWritable,
274 18 => Self::InconsistentName,
275 other => {
276 tracing::warn!(target: "async_snmp::error", { snmp.error_status = other }, "unknown SNMP error status");
277 Self::Unknown(other)
278 }
279 }
280 }
281
282 /// Convert to raw status code.
283 #[must_use]
284 pub fn as_i32(&self) -> i32 {
285 match self {
286 Self::NoError => 0,
287 Self::TooBig => 1,
288 Self::NoSuchName => 2,
289 Self::BadValue => 3,
290 Self::ReadOnly => 4,
291 Self::GenErr => 5,
292 Self::NoAccess => 6,
293 Self::WrongType => 7,
294 Self::WrongLength => 8,
295 Self::WrongEncoding => 9,
296 Self::WrongValue => 10,
297 Self::NoCreation => 11,
298 Self::InconsistentValue => 12,
299 Self::ResourceUnavailable => 13,
300 Self::CommitFailed => 14,
301 Self::UndoFailed => 15,
302 Self::AuthorizationError => 16,
303 Self::NotWritable => 17,
304 Self::InconsistentName => 18,
305 Self::Unknown(code) => *code,
306 }
307 }
308
309 /// Map a v2c+ error status to its v1 equivalent per RFC 2576 Section 4.3.
310 ///
311 /// V1-native statuses (0-5) pass through unchanged.
312 #[must_use]
313 pub fn to_v1(&self) -> Self {
314 match self {
315 // V1-native statuses
316 Self::NoError
317 | Self::TooBig
318 | Self::NoSuchName
319 | Self::BadValue
320 | Self::ReadOnly
321 | Self::GenErr => *self,
322
323 // Value errors -> BadValue
324 Self::WrongType
325 | Self::WrongLength
326 | Self::WrongEncoding
327 | Self::WrongValue
328 | Self::InconsistentValue => Self::BadValue,
329
330 // Access/creation errors -> NoSuchName
331 Self::NoAccess
332 | Self::NotWritable
333 | Self::NoCreation
334 | Self::InconsistentName
335 | Self::AuthorizationError => Self::NoSuchName,
336
337 // Resource/commit errors -> GenErr
338 Self::ResourceUnavailable | Self::CommitFailed | Self::UndoFailed => Self::GenErr,
339
340 Self::Unknown(_) => Self::GenErr,
341 }
342 }
343
344 /// Return the canonical SMI name for this status code.
345 ///
346 /// For `Unknown` variants, returns `None`; callers should format the
347 /// numeric code directly in that case.
348 #[must_use]
349 pub fn as_str(&self) -> Option<&'static str> {
350 match self {
351 Self::NoError => Some("noError"),
352 Self::TooBig => Some("tooBig"),
353 Self::NoSuchName => Some("noSuchName"),
354 Self::BadValue => Some("badValue"),
355 Self::ReadOnly => Some("readOnly"),
356 Self::GenErr => Some("genErr"),
357 Self::NoAccess => Some("noAccess"),
358 Self::WrongType => Some("wrongType"),
359 Self::WrongLength => Some("wrongLength"),
360 Self::WrongEncoding => Some("wrongEncoding"),
361 Self::WrongValue => Some("wrongValue"),
362 Self::NoCreation => Some("noCreation"),
363 Self::InconsistentValue => Some("inconsistentValue"),
364 Self::ResourceUnavailable => Some("resourceUnavailable"),
365 Self::CommitFailed => Some("commitFailed"),
366 Self::UndoFailed => Some("undoFailed"),
367 Self::AuthorizationError => Some("authorizationError"),
368 Self::NotWritable => Some("notWritable"),
369 Self::InconsistentName => Some("inconsistentName"),
370 Self::Unknown(_) => None,
371 }
372 }
373}
374
375impl std::fmt::Display for ErrorStatus {
376 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377 match self.as_str() {
378 Some(name) => f.write_str(name),
379 None => write!(f, "unknown({})", self.as_i32()),
380 }
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn walk_abort_reason_is_error() {
390 let reason = WalkAbortReason::NonIncreasing;
391 let err: &dyn std::error::Error = &reason;
392 assert_eq!(err.to_string(), "non-increasing OID");
393 }
394
395 #[test]
396 fn error_status_to_v1_mapping() {
397 // RFC 2576 Section 4.3 mappings
398 // V1 statuses (0-5) pass through unchanged
399 assert_eq!(ErrorStatus::NoError.to_v1(), ErrorStatus::NoError);
400 assert_eq!(ErrorStatus::TooBig.to_v1(), ErrorStatus::TooBig);
401 assert_eq!(ErrorStatus::NoSuchName.to_v1(), ErrorStatus::NoSuchName);
402 assert_eq!(ErrorStatus::BadValue.to_v1(), ErrorStatus::BadValue);
403 assert_eq!(ErrorStatus::ReadOnly.to_v1(), ErrorStatus::ReadOnly);
404 assert_eq!(ErrorStatus::GenErr.to_v1(), ErrorStatus::GenErr);
405
406 // WrongValue/WrongType/WrongLength/WrongEncoding/InconsistentValue -> BadValue
407 assert_eq!(ErrorStatus::WrongValue.to_v1(), ErrorStatus::BadValue);
408 assert_eq!(ErrorStatus::WrongType.to_v1(), ErrorStatus::BadValue);
409 assert_eq!(ErrorStatus::WrongLength.to_v1(), ErrorStatus::BadValue);
410 assert_eq!(ErrorStatus::WrongEncoding.to_v1(), ErrorStatus::BadValue);
411 assert_eq!(
412 ErrorStatus::InconsistentValue.to_v1(),
413 ErrorStatus::BadValue
414 );
415
416 // NoAccess/NotWritable/NoCreation/InconsistentName/AuthorizationError -> NoSuchName
417 assert_eq!(ErrorStatus::NoAccess.to_v1(), ErrorStatus::NoSuchName);
418 assert_eq!(ErrorStatus::NotWritable.to_v1(), ErrorStatus::NoSuchName);
419 assert_eq!(ErrorStatus::NoCreation.to_v1(), ErrorStatus::NoSuchName);
420 assert_eq!(
421 ErrorStatus::InconsistentName.to_v1(),
422 ErrorStatus::NoSuchName
423 );
424 assert_eq!(
425 ErrorStatus::AuthorizationError.to_v1(),
426 ErrorStatus::NoSuchName
427 );
428
429 // ResourceUnavailable/CommitFailed/UndoFailed -> GenErr
430 assert_eq!(
431 ErrorStatus::ResourceUnavailable.to_v1(),
432 ErrorStatus::GenErr
433 );
434 assert_eq!(ErrorStatus::CommitFailed.to_v1(), ErrorStatus::GenErr);
435 assert_eq!(ErrorStatus::UndoFailed.to_v1(), ErrorStatus::GenErr);
436 }
437
438 #[test]
439 fn error_size_budget() {
440 // Error size should stay bounded to avoid bloating Result types.
441 // The largest variant is Error::Snmp which contains Option<Oid>.
442 assert!(
443 std::mem::size_of::<Error>() <= 128,
444 "Error size {} exceeds 128-byte budget",
445 std::mem::size_of::<Error>()
446 );
447
448 // Result<(), Box<Error>> should be pointer-sized (8 bytes on 64-bit).
449 assert_eq!(
450 std::mem::size_of::<Result<()>>(),
451 std::mem::size_of::<*const ()>(),
452 "Result<()> should be pointer-sized"
453 );
454 }
455}