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