Skip to main content

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}