async_snmp/
error.rs

1//! Error types for async-snmp.
2//!
3//! All errors are `#[non_exhaustive]` to allow adding new variants without breaking changes.
4
5use std::net::SocketAddr;
6use std::time::Duration;
7
8/// Result type alias using the library's Error type.
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// Authentication error kinds (SNMPv3).
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum AuthErrorKind {
14    /// No credentials configured for this operation.
15    NoCredentials,
16    /// No authentication key available.
17    NoAuthKey,
18    /// User not found in USM table.
19    NoUser,
20    /// HMAC verification failed.
21    HmacMismatch,
22    /// Authentication parameters wrong length.
23    WrongMacLength { expected: usize, actual: usize },
24    /// Could not locate auth params in message.
25    AuthParamsNotFound,
26}
27
28impl std::fmt::Display for AuthErrorKind {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            Self::NoCredentials => write!(f, "no credentials configured"),
32            Self::NoAuthKey => write!(f, "no authentication key available"),
33            Self::NoUser => write!(f, "user not found"),
34            Self::HmacMismatch => write!(f, "HMAC verification failed"),
35            Self::WrongMacLength { expected, actual } => {
36                write!(f, "wrong MAC length: expected {}, got {}", expected, actual)
37            }
38            Self::AuthParamsNotFound => write!(f, "could not locate auth params in message"),
39        }
40    }
41}
42
43/// Cryptographic error kinds (encryption/decryption).
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum CryptoErrorKind {
46    /// No privacy key available.
47    NoPrivKey,
48    /// Invalid padding in decrypted data.
49    InvalidPadding,
50    /// Invalid key length for cipher.
51    InvalidKeyLength,
52    /// Invalid IV length for cipher.
53    InvalidIvLength,
54    /// Cipher operation failed.
55    CipherError,
56    /// Unsupported privacy protocol.
57    UnsupportedProtocol,
58    /// Invalid priv params length.
59    InvalidPrivParamsLength { expected: usize, actual: usize },
60    /// Ciphertext length not a multiple of block size.
61    InvalidCiphertextLength { length: usize, block_size: usize },
62}
63
64impl std::fmt::Display for CryptoErrorKind {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        match self {
67            Self::NoPrivKey => write!(f, "no privacy key available"),
68            Self::InvalidPadding => write!(f, "invalid padding"),
69            Self::InvalidKeyLength => write!(f, "invalid key length"),
70            Self::InvalidIvLength => write!(f, "invalid IV length"),
71            Self::CipherError => write!(f, "cipher operation failed"),
72            Self::UnsupportedProtocol => write!(f, "unsupported privacy protocol"),
73            Self::InvalidPrivParamsLength { expected, actual } => {
74                write!(
75                    f,
76                    "invalid privParameters length: expected {}, got {}",
77                    expected, actual
78                )
79            }
80            Self::InvalidCiphertextLength { length, block_size } => {
81                write!(
82                    f,
83                    "ciphertext length {} not multiple of block size {}",
84                    length, block_size
85                )
86            }
87        }
88    }
89}
90
91/// BER decode error kinds.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum DecodeErrorKind {
94    /// Expected different tag.
95    UnexpectedTag { expected: u8, actual: u8 },
96    /// Data truncated unexpectedly.
97    TruncatedData,
98    /// Invalid BER length encoding.
99    InvalidLength,
100    /// Indefinite length not supported.
101    IndefiniteLength,
102    /// Integer value overflow.
103    IntegerOverflow,
104    /// Zero-length integer.
105    ZeroLengthInteger,
106    /// Invalid OID encoding.
107    InvalidOidEncoding,
108    /// Unknown SNMP version.
109    UnknownVersion(i32),
110    /// Unknown PDU type.
111    UnknownPduType(u8),
112    /// Constructed OCTET STRING not supported.
113    ConstructedOctetString,
114    /// Missing required PDU.
115    MissingPdu,
116    /// Invalid msgFlags (priv without auth).
117    InvalidMsgFlags,
118    /// Unknown security model.
119    UnknownSecurityModel(i32),
120    /// msgMaxSize below RFC 3412 minimum (484 octets).
121    MsgMaxSizeTooSmall { value: i32, minimum: i32 },
122    /// NULL with non-zero length.
123    InvalidNull,
124    /// Expected plaintext, got encrypted.
125    UnexpectedEncryption,
126    /// Expected encrypted, got plaintext.
127    ExpectedEncryption,
128    /// Invalid IP address length.
129    InvalidIpAddressLength { length: usize },
130    /// Length field too long.
131    LengthTooLong { octets: usize },
132    /// Length exceeds maximum.
133    LengthExceedsMax { length: usize, max: usize },
134    /// Integer64 too long.
135    Integer64TooLong { length: usize },
136    /// Empty response.
137    EmptyResponse,
138    /// TLV extends past end of data.
139    TlvOverflow,
140    /// Insufficient data for read.
141    InsufficientData { needed: usize, available: usize },
142    /// Invalid OID in notification varbinds.
143    InvalidOid,
144}
145
146impl std::fmt::Display for DecodeErrorKind {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        match self {
149            Self::UnexpectedTag { expected, actual } => {
150                write!(f, "expected tag 0x{:02X}, got 0x{:02X}", expected, actual)
151            }
152            Self::TruncatedData => write!(f, "unexpected end of data"),
153            Self::InvalidLength => write!(f, "invalid length encoding"),
154            Self::IndefiniteLength => write!(f, "indefinite length encoding not supported"),
155            Self::IntegerOverflow => write!(f, "integer overflow"),
156            Self::ZeroLengthInteger => write!(f, "zero-length integer"),
157            Self::InvalidOidEncoding => write!(f, "invalid OID encoding"),
158            Self::UnknownVersion(v) => write!(f, "unknown SNMP version: {}", v),
159            Self::UnknownPduType(t) => write!(f, "unknown PDU type: 0x{:02X}", t),
160            Self::ConstructedOctetString => {
161                write!(f, "constructed OCTET STRING (0x24) not supported")
162            }
163            Self::MissingPdu => write!(f, "missing PDU in message"),
164            Self::InvalidMsgFlags => write!(f, "invalid msgFlags: privacy without authentication"),
165            Self::UnknownSecurityModel(m) => write!(f, "unknown security model: {}", m),
166            Self::MsgMaxSizeTooSmall { value, minimum } => {
167                write!(f, "msgMaxSize {} below RFC 3412 minimum {}", value, minimum)
168            }
169            Self::InvalidNull => write!(f, "NULL with non-zero length"),
170            Self::UnexpectedEncryption => write!(f, "expected plaintext scoped PDU"),
171            Self::ExpectedEncryption => write!(f, "expected encrypted scoped PDU"),
172            Self::InvalidIpAddressLength { length } => {
173                write!(f, "IP address must be 4 bytes, got {}", length)
174            }
175            Self::LengthTooLong { octets } => {
176                write!(f, "length encoding too long ({} octets)", octets)
177            }
178            Self::LengthExceedsMax { length, max } => {
179                write!(f, "length {} exceeds maximum {}", length, max)
180            }
181            Self::Integer64TooLong { length } => {
182                write!(f, "integer64 too long: {} bytes", length)
183            }
184            Self::EmptyResponse => write!(f, "empty response"),
185            Self::TlvOverflow => write!(f, "TLV extends past end of data"),
186            Self::InsufficientData { needed, available } => {
187                write!(f, "need {} bytes but only {} remaining", needed, available)
188            }
189            Self::InvalidOid => write!(f, "invalid OID in notification varbinds"),
190        }
191    }
192}
193
194/// BER encode error kinds.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum EncodeErrorKind {
197    /// V3 security not configured.
198    NoSecurityConfig,
199    /// Engine not discovered.
200    EngineNotDiscovered,
201    /// Keys not derived.
202    KeysNotDerived,
203    /// Auth key not available for encoding.
204    MissingAuthKey,
205    /// Privacy key not available.
206    NoPrivKey,
207    /// Could not locate auth params position in encoded message.
208    MissingAuthParams,
209}
210
211impl std::fmt::Display for EncodeErrorKind {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        match self {
214            Self::NoSecurityConfig => write!(f, "V3 security config not set"),
215            Self::EngineNotDiscovered => write!(f, "engine not discovered"),
216            Self::KeysNotDerived => write!(f, "keys not derived"),
217            Self::MissingAuthKey => write!(f, "auth key not available for encoding"),
218            Self::NoPrivKey => write!(f, "privacy key not available"),
219            Self::MissingAuthParams => {
220                write!(f, "could not find auth params position in encoded message")
221            }
222        }
223    }
224}
225
226/// OID validation error kinds.
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub enum OidErrorKind {
229    /// Empty OID string.
230    Empty,
231    /// Invalid arc value.
232    InvalidArc,
233    /// First arc must be 0, 1, or 2.
234    InvalidFirstArc(u32),
235    /// Second arc too large for first arc value.
236    InvalidSecondArc { first: u32, second: u32 },
237    /// OID too short (minimum 2 arcs).
238    TooShort,
239    /// OID has too many arcs (exceeds MAX_OID_LEN).
240    TooManyArcs { count: usize, max: usize },
241    /// Subidentifier overflow during encoding.
242    SubidentifierOverflow,
243}
244
245impl std::fmt::Display for OidErrorKind {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        match self {
248            Self::Empty => write!(f, "empty OID"),
249            Self::InvalidArc => write!(f, "invalid arc value"),
250            Self::InvalidFirstArc(v) => write!(f, "first arc must be 0, 1, or 2, got {}", v),
251            Self::InvalidSecondArc { first, second } => {
252                write!(f, "second arc {} too large for first arc {}", second, first)
253            }
254            Self::TooShort => write!(f, "OID must have at least 2 arcs"),
255            Self::TooManyArcs { count, max } => {
256                write!(f, "OID has {} arcs, exceeds maximum {}", count, max)
257            }
258            Self::SubidentifierOverflow => write!(f, "subidentifier overflow"),
259        }
260    }
261}
262
263/// SNMP error status codes (RFC 3416).
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265#[non_exhaustive]
266pub enum ErrorStatus {
267    NoError,
268    TooBig,
269    NoSuchName,
270    BadValue,
271    ReadOnly,
272    GenErr,
273    NoAccess,
274    WrongType,
275    WrongLength,
276    WrongEncoding,
277    WrongValue,
278    NoCreation,
279    InconsistentValue,
280    ResourceUnavailable,
281    CommitFailed,
282    UndoFailed,
283    AuthorizationError,
284    NotWritable,
285    InconsistentName,
286    /// Unknown/future error status code.
287    Unknown(i32),
288}
289
290impl ErrorStatus {
291    /// Create from raw status code.
292    pub fn from_i32(value: i32) -> Self {
293        match value {
294            0 => Self::NoError,
295            1 => Self::TooBig,
296            2 => Self::NoSuchName,
297            3 => Self::BadValue,
298            4 => Self::ReadOnly,
299            5 => Self::GenErr,
300            6 => Self::NoAccess,
301            7 => Self::WrongType,
302            8 => Self::WrongLength,
303            9 => Self::WrongEncoding,
304            10 => Self::WrongValue,
305            11 => Self::NoCreation,
306            12 => Self::InconsistentValue,
307            13 => Self::ResourceUnavailable,
308            14 => Self::CommitFailed,
309            15 => Self::UndoFailed,
310            16 => Self::AuthorizationError,
311            17 => Self::NotWritable,
312            18 => Self::InconsistentName,
313            other => Self::Unknown(other),
314        }
315    }
316
317    /// Convert to raw status code.
318    pub fn as_i32(&self) -> i32 {
319        match self {
320            Self::NoError => 0,
321            Self::TooBig => 1,
322            Self::NoSuchName => 2,
323            Self::BadValue => 3,
324            Self::ReadOnly => 4,
325            Self::GenErr => 5,
326            Self::NoAccess => 6,
327            Self::WrongType => 7,
328            Self::WrongLength => 8,
329            Self::WrongEncoding => 9,
330            Self::WrongValue => 10,
331            Self::NoCreation => 11,
332            Self::InconsistentValue => 12,
333            Self::ResourceUnavailable => 13,
334            Self::CommitFailed => 14,
335            Self::UndoFailed => 15,
336            Self::AuthorizationError => 16,
337            Self::NotWritable => 17,
338            Self::InconsistentName => 18,
339            Self::Unknown(code) => *code,
340        }
341    }
342}
343
344impl std::fmt::Display for ErrorStatus {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        match self {
347            Self::NoError => write!(f, "noError"),
348            Self::TooBig => write!(f, "tooBig"),
349            Self::NoSuchName => write!(f, "noSuchName"),
350            Self::BadValue => write!(f, "badValue"),
351            Self::ReadOnly => write!(f, "readOnly"),
352            Self::GenErr => write!(f, "genErr"),
353            Self::NoAccess => write!(f, "noAccess"),
354            Self::WrongType => write!(f, "wrongType"),
355            Self::WrongLength => write!(f, "wrongLength"),
356            Self::WrongEncoding => write!(f, "wrongEncoding"),
357            Self::WrongValue => write!(f, "wrongValue"),
358            Self::NoCreation => write!(f, "noCreation"),
359            Self::InconsistentValue => write!(f, "inconsistentValue"),
360            Self::ResourceUnavailable => write!(f, "resourceUnavailable"),
361            Self::CommitFailed => write!(f, "commitFailed"),
362            Self::UndoFailed => write!(f, "undoFailed"),
363            Self::AuthorizationError => write!(f, "authorizationError"),
364            Self::NotWritable => write!(f, "notWritable"),
365            Self::InconsistentName => write!(f, "inconsistentName"),
366            Self::Unknown(code) => write!(f, "unknown({})", code),
367        }
368    }
369}
370
371/// Library error type.
372#[derive(Debug, thiserror::Error)]
373#[non_exhaustive]
374pub enum Error {
375    /// I/O error during communication.
376    #[error("I/O error{}: {source}", target.map(|t| format!(" communicating with {}", t)).unwrap_or_default())]
377    Io {
378        target: Option<SocketAddr>,
379        #[source]
380        source: std::io::Error,
381    },
382
383    /// Request timed out (after retries if configured).
384    #[error("timeout after {elapsed:?}{} (request_id={request_id}, retries={retries})", target.map(|t| format!(" waiting for {}", t)).unwrap_or_default())]
385    Timeout {
386        target: Option<SocketAddr>,
387        elapsed: Duration,
388        request_id: i32,
389        retries: u32,
390    },
391
392    /// SNMP protocol error returned by agent.
393    #[error("SNMP error{}: {status} at index {index}", target.map(|t| format!(" from {}", t)).unwrap_or_default())]
394    Snmp {
395        target: Option<SocketAddr>,
396        status: ErrorStatus,
397        index: u32,
398        oid: Option<crate::oid::Oid>,
399    },
400
401    /// Invalid OID format.
402    #[error("invalid OID: {kind}")]
403    InvalidOid {
404        kind: OidErrorKind,
405        input: Option<Box<str>>, // Only allocated when parsing string input
406    },
407
408    /// BER decoding error.
409    #[error("decode error at offset {offset}: {kind}")]
410    Decode {
411        offset: usize,
412        kind: DecodeErrorKind,
413    },
414
415    /// BER encoding error.
416    #[error("encode error: {kind}")]
417    Encode { kind: EncodeErrorKind },
418
419    /// Response request ID doesn't match.
420    #[error("request ID mismatch: expected {expected}, got {actual}")]
421    RequestIdMismatch { expected: i32, actual: i32 },
422
423    /// Response version doesn't match request.
424    #[error("version mismatch: expected {expected:?}, got {actual:?}")]
425    VersionMismatch {
426        expected: crate::version::Version,
427        actual: crate::version::Version,
428    },
429
430    /// Message exceeds maximum size.
431    #[error("message too large: {size} bytes exceeds maximum {max}")]
432    MessageTooLarge { size: usize, max: usize },
433
434    /// Unknown engine ID (SNMPv3).
435    #[error("unknown engine ID")]
436    UnknownEngineId { target: Option<SocketAddr> },
437
438    /// Message outside time window (SNMPv3).
439    #[error("message not in time window")]
440    NotInTimeWindow { target: Option<SocketAddr> },
441
442    /// Authentication failed (SNMPv3).
443    #[error("authentication failed: {kind}")]
444    AuthenticationFailed {
445        target: Option<SocketAddr>,
446        kind: AuthErrorKind,
447    },
448
449    /// Decryption failed (SNMPv3).
450    #[error("decryption failed: {kind}")]
451    DecryptionFailed {
452        target: Option<SocketAddr>,
453        kind: CryptoErrorKind,
454    },
455
456    /// Encryption failed (SNMPv3).
457    #[error("encryption failed: {kind}")]
458    EncryptionFailed {
459        target: Option<SocketAddr>,
460        kind: CryptoErrorKind,
461    },
462
463    /// Invalid community string.
464    #[error("invalid community")]
465    InvalidCommunity { target: Option<SocketAddr> },
466
467    /// Non-increasing OID detected during walk (agent misbehavior).
468    ///
469    /// Returned when a walk operation receives an OID that is not
470    /// lexicographically greater than the previous OID, which would
471    /// cause an infinite loop. This indicates a non-conformant SNMP agent.
472    ///
473    /// Only occurs with `OidOrdering::Strict` (the default).
474    #[error("walk detected non-increasing OID: {previous} >= {current}")]
475    NonIncreasingOid {
476        previous: crate::oid::Oid,
477        current: crate::oid::Oid,
478    },
479
480    /// Walk detected a cycle (same OID returned twice).
481    ///
482    /// Only occurs with `OidOrdering::AllowNonIncreasing`, which uses
483    /// a HashSet to track all seen OIDs and detect cycles.
484    #[error("walk cycle detected: OID {oid} returned twice")]
485    DuplicateOid { oid: crate::oid::Oid },
486
487    /// GETBULK not supported in SNMPv1.
488    ///
489    /// Returned when `WalkMode::GetBulk` is explicitly requested with an SNMPv1 client.
490    /// GETBULK is only available in SNMPv2c and SNMPv3.
491    #[error("GETBULK is not supported in SNMPv1")]
492    GetBulkNotSupportedInV1,
493
494    /// Configuration error.
495    ///
496    /// Returned when client configuration is invalid (e.g., privacy
497    /// without authentication, missing passwords).
498    #[error("configuration error: {0}")]
499    Config(String),
500}
501
502impl Error {
503    /// Create a decode error.
504    pub fn decode(offset: usize, kind: DecodeErrorKind) -> Self {
505        Self::Decode { offset, kind }
506    }
507
508    /// Create an encode error.
509    pub fn encode(kind: EncodeErrorKind) -> Self {
510        Self::Encode { kind }
511    }
512
513    /// Create an authentication error.
514    pub fn auth(target: Option<SocketAddr>, kind: AuthErrorKind) -> Self {
515        Self::AuthenticationFailed { target, kind }
516    }
517
518    /// Create a decryption error.
519    pub fn decrypt(target: Option<SocketAddr>, kind: CryptoErrorKind) -> Self {
520        Self::DecryptionFailed { target, kind }
521    }
522
523    /// Create an encryption error.
524    pub fn encrypt(target: Option<SocketAddr>, kind: CryptoErrorKind) -> Self {
525        Self::EncryptionFailed { target, kind }
526    }
527
528    /// Create an invalid OID error from a kind (no input string).
529    pub fn invalid_oid(kind: OidErrorKind) -> Self {
530        Self::InvalidOid { kind, input: None }
531    }
532
533    /// Create an invalid OID error with the input string that failed.
534    pub fn invalid_oid_with_input(kind: OidErrorKind, input: impl Into<Box<str>>) -> Self {
535        Self::InvalidOid {
536            kind,
537            input: Some(input.into()),
538        }
539    }
540
541    /// Get the target address if this error has one.
542    pub fn target(&self) -> Option<SocketAddr> {
543        match self {
544            Self::Io { target, .. } => *target,
545            Self::Timeout { target, .. } => *target,
546            Self::Snmp { target, .. } => *target,
547            Self::UnknownEngineId { target } => *target,
548            Self::NotInTimeWindow { target } => *target,
549            Self::AuthenticationFailed { target, .. } => *target,
550            Self::DecryptionFailed { target, .. } => *target,
551            Self::EncryptionFailed { target, .. } => *target,
552            Self::InvalidCommunity { target } => *target,
553            _ => None,
554        }
555    }
556}