Skip to main content

async_snmp/agent/
mod.rs

1//! SNMP Agent (RFC 3413).
2//!
3//! This module provides SNMP agent functionality for responding to
4//! GET, GETNEXT, GETBULK, and SET requests, and for sending traps and informs.
5//!
6//! # Features
7//!
8//! - **Async handlers**: All handler methods are async for database queries, network calls, etc.
9//! - **Atomic SET**: Two-phase commit protocol (test/commit/undo/free) per RFC 3416
10//! - **VACM support**: Optional View-based Access Control Model (RFC 3415)
11//! - **Trap/inform sending**: Send notifications to configured trap sinks via [`Agent::send_trap`] and [`Agent::send_inform`]
12//! - **Built-in MIB handlers**: Automatic read-only handlers for snmpEngine, usmStats, and mpdStats groups (see [`BuiltinMib`])
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! use async_snmp::agent::Agent;
18//! use async_snmp::handler::{MibHandler, RequestContext, GetResult, GetNextResult, BoxFuture};
19//! use async_snmp::{Oid, Value, VarBind, oid};
20//! use std::sync::Arc;
21//!
22//! // Define a simple handler for the system MIB subtree
23//! struct SystemMibHandler;
24//!
25//! impl MibHandler for SystemMibHandler {
26//!     fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
27//!         Box::pin(async move {
28//!             // sysDescr.0
29//!             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {
30//!                 return GetResult::Value(Value::OctetString("My SNMP Agent".into()));
31//!             }
32//!             // sysObjectID.0
33//!             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 2, 0) {
34//!                 return GetResult::Value(Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 99999)));
35//!             }
36//!             GetResult::NoSuchObject
37//!         })
38//!     }
39//!
40//!     fn get_next<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetNextResult> {
41//!         Box::pin(async move {
42//!             // Return the lexicographically next OID after the given one
43//!             let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
44//!             let sys_object_id = oid!(1, 3, 6, 1, 2, 1, 1, 2, 0);
45//!
46//!             if oid < &sys_descr {
47//!                 return GetNextResult::Value(VarBind::new(sys_descr, Value::OctetString("My SNMP Agent".into())));
48//!             }
49//!             if oid < &sys_object_id {
50//!                 return GetNextResult::Value(VarBind::new(sys_object_id, Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 99999))));
51//!             }
52//!             GetNextResult::EndOfMibView
53//!         })
54//!     }
55//! }
56//!
57//! #[tokio::main]
58//! async fn main() -> Result<(), Box<async_snmp::Error>> {
59//!     let agent = Agent::builder()
60//!         .bind("0.0.0.0:161")
61//!         .community(b"public")
62//!         .handler(oid!(1, 3, 6, 1, 2, 1, 1), Arc::new(SystemMibHandler))
63//!         .build()
64//!         .await?;
65//!
66//!     agent.run().await
67//! }
68//! ```
69
70mod builtins;
71mod notification;
72mod request;
73mod response;
74mod set_handler;
75pub mod vacm;
76
77pub use vacm::{SecurityModel, VacmBuilder, VacmConfig, View, ViewCheckResult, ViewSubtree};
78
79use std::collections::{HashMap, HashSet};
80use std::net::SocketAddr;
81use std::sync::Arc;
82use std::sync::atomic::{AtomicU32, Ordering};
83use std::time::{Duration, Instant};
84
85use bytes::Bytes;
86use subtle::ConstantTimeEq;
87use tokio::net::UdpSocket;
88use tokio::sync::Semaphore;
89use tokio_util::sync::CancellationToken;
90use tracing::instrument;
91
92use std::io::IoSliceMut;
93
94use quinn_udp::{RecvMeta, Transmit, UdpSockRef, UdpSocketState};
95
96use crate::ber::Decoder;
97use crate::error::internal::DecodeErrorKind;
98use crate::error::{Error, ErrorStatus, Result};
99use crate::handler::{GetNextResult, GetResult, MibHandler, RequestContext};
100use crate::notification::UsmConfig;
101use crate::oid;
102use crate::oid::Oid;
103use crate::pdu::{Pdu, PduType};
104use crate::util::bind_udp_socket;
105use crate::v3::{SaltCounter, compute_engine_boots_time};
106use crate::value::Value;
107use crate::varbind::VarBind;
108use crate::version::Version;
109
110/// Default maximum message size for UDP (RFC 3417 recommendation).
111const DEFAULT_MAX_MESSAGE_SIZE: usize = 1472;
112
113/// Overhead for SNMP message encoding (approximate conservative estimate).
114/// This accounts for version, community/USM, PDU headers, etc.
115const RESPONSE_OVERHEAD: usize = 100;
116
117/// Built-in MIB handler groups that the agent registers automatically.
118///
119/// By default, the agent registers handlers for standard SNMP MIB objects
120/// (engine parameters, USM statistics, MPD statistics). Use
121/// [`AgentBuilder::without_builtin_handler`] to disable specific groups
122/// or [`AgentBuilder::without_builtin_handlers`] to disable all of them.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
124pub enum BuiltinMib {
125    /// snmpEngine scalars (1.3.6.1.6.3.10.2.1).
126    ///
127    /// Provides snmpEngineID, snmpEngineBoots, snmpEngineTime,
128    /// and snmpEngineMaxMessageSize.
129    SnmpEngine,
130    /// USM statistics (1.3.6.1.6.3.15.1.1).
131    ///
132    /// Provides the six usmStats counters (unsupportedSecLevels,
133    /// notInTimeWindows, unknownUserNames, unknownEngineIDs,
134    /// wrongDigests, decryptionErrors).
135    UsmStats,
136    /// MPD statistics (1.3.6.1.6.3.11.2.1).
137    ///
138    /// Provides snmpUnknownSecurityModels and snmpInvalidMsgs.
139    MpdStats,
140}
141
142/// Registered handler with its OID prefix.
143pub(crate) struct RegisteredHandler {
144    pub(crate) prefix: Oid,
145    pub(crate) handler: Arc<dyn MibHandler>,
146}
147
148/// Builder for [`Agent`].
149///
150/// Use this builder to configure and construct an SNMP agent. The builder
151/// pattern allows you to chain configuration methods before calling
152/// [`build()`](AgentBuilder::build) to create the agent.
153///
154/// # Access Control
155///
156/// By default, the agent operates in **permissive mode**: any authenticated
157/// request (valid community string for v1/v2c, valid USM credentials for v3)
158/// has full read and write access to all registered handlers.
159///
160/// For production deployments, use the [`vacm()`](AgentBuilder::vacm) method
161/// to configure View-based Access Control (RFC 3415), which allows fine-grained
162/// control over which security names can access which OID subtrees.
163///
164/// # Minimal Example
165///
166/// ```rust,no_run
167/// use async_snmp::agent::Agent;
168/// use async_snmp::handler::{MibHandler, RequestContext, GetResult, GetNextResult, BoxFuture};
169/// use async_snmp::{Oid, Value, VarBind, oid};
170/// use std::sync::Arc;
171///
172/// struct MyHandler;
173/// impl MibHandler for MyHandler {
174///     fn get<'a>(&'a self, _: &'a RequestContext, _: &'a Oid) -> BoxFuture<'a, GetResult> {
175///         Box::pin(async { GetResult::NoSuchObject })
176///     }
177///     fn get_next<'a>(&'a self, _: &'a RequestContext, _: &'a Oid) -> BoxFuture<'a, GetNextResult> {
178///         Box::pin(async { GetNextResult::EndOfMibView })
179///     }
180/// }
181///
182/// # async fn example() -> Result<(), Box<async_snmp::Error>> {
183/// let agent = Agent::builder()
184///     .bind("0.0.0.0:1161")  // Use non-privileged port
185///     .community(b"public")
186///     .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(MyHandler))
187///     .build()
188///     .await?;
189/// # Ok(())
190/// # }
191/// ```
192pub struct AgentBuilder {
193    bind_addr: String,
194    communities: Vec<Vec<u8>>,
195    usm_users: HashMap<Bytes, UsmConfig>,
196    handlers: Vec<RegisteredHandler>,
197    engine_id: Option<Vec<u8>>,
198    engine_boots: u32,
199    max_message_size: usize,
200    max_concurrent_requests: Option<usize>,
201    recv_buffer_size: Option<usize>,
202    vacm: Option<VacmConfig>,
203    cancel: Option<CancellationToken>,
204    trap_sinks: Vec<(String, crate::client::Auth)>,
205    inform_timeout: Duration,
206    inform_retry: crate::client::Retry,
207    disabled_builtins: HashSet<BuiltinMib>,
208}
209
210impl AgentBuilder {
211    /// Create a new builder with default settings.
212    ///
213    /// Defaults:
214    /// - Bind address: `0.0.0.0:161` (UDP)
215    /// - Max message size: 1472 bytes (Ethernet MTU - IP/UDP headers)
216    /// - Max concurrent requests: 1000
217    /// - Receive buffer size: 4MB (requested from kernel)
218    /// - No communities or USM users (all requests rejected)
219    /// - No handlers registered
220    #[must_use]
221    pub fn new() -> Self {
222        Self {
223            bind_addr: "0.0.0.0:161".to_string(),
224            communities: Vec::new(),
225            usm_users: HashMap::new(),
226            handlers: Vec::new(),
227            engine_id: None,
228            engine_boots: 1,
229            max_message_size: DEFAULT_MAX_MESSAGE_SIZE,
230            max_concurrent_requests: Some(1000),
231            recv_buffer_size: Some(4 * 1024 * 1024), // 4MB
232            vacm: None,
233            cancel: None,
234            trap_sinks: Vec::new(),
235            inform_timeout: Duration::from_secs(5),
236            inform_retry: crate::client::Retry::default(),
237            disabled_builtins: HashSet::new(),
238        }
239    }
240
241    /// Set the UDP bind address.
242    ///
243    /// Default is `0.0.0.0:161` (standard SNMP agent port). Note that binding
244    /// to UDP port 161 typically requires root/administrator privileges.
245    ///
246    /// # IPv4 Examples
247    ///
248    /// ```rust,no_run
249    /// use async_snmp::agent::Agent;
250    ///
251    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
252    /// // Bind to all IPv4 interfaces on standard port (requires privileges)
253    /// let agent = Agent::builder().bind("0.0.0.0:161").community(b"public").build().await?;
254    ///
255    /// // Bind to localhost only on non-privileged port
256    /// let agent = Agent::builder().bind("127.0.0.1:1161").community(b"public").build().await?;
257    ///
258    /// // Bind to specific interface
259    /// let agent = Agent::builder().bind("192.168.1.100:161").community(b"public").build().await?;
260    /// # Ok(())
261    /// # }
262    /// ```
263    ///
264    /// # IPv6 / Dual-Stack Examples
265    ///
266    /// ```rust,no_run
267    /// use async_snmp::agent::Agent;
268    ///
269    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
270    /// // Bind to all interfaces (IPv6, with dual-stack on Linux)
271    /// let agent = Agent::builder().bind("[::]:161").community(b"public").build().await?;
272    ///
273    /// // Bind to IPv6 localhost only
274    /// let agent = Agent::builder().bind("[::1]:1161").community(b"public").build().await?;
275    /// # Ok(())
276    /// # }
277    /// ```
278    #[must_use]
279    pub fn bind(mut self, addr: impl Into<String>) -> Self {
280        self.bind_addr = addr.into();
281        self
282    }
283
284    /// Add an accepted community string for v1/v2c requests.
285    ///
286    /// Multiple communities can be added. If none are added,
287    /// all v1/v2c requests are rejected.
288    ///
289    /// # Example
290    ///
291    /// ```rust,no_run
292    /// use async_snmp::agent::Agent;
293    ///
294    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
295    /// let agent = Agent::builder()
296    ///     .bind("0.0.0.0:1161")
297    ///     .community(b"public")   // Read-only access
298    ///     .community(b"private")  // Read-write access (with VACM)
299    ///     .build()
300    ///     .await?;
301    /// # Ok(())
302    /// # }
303    /// ```
304    #[must_use]
305    pub fn community(mut self, community: &[u8]) -> Self {
306        self.communities.push(community.to_vec());
307        self
308    }
309
310    /// Add multiple community strings.
311    ///
312    /// # Example
313    ///
314    /// ```rust,no_run
315    /// use async_snmp::agent::Agent;
316    ///
317    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
318    /// let communities = ["public", "private", "monitor"];
319    /// let agent = Agent::builder()
320    ///     .bind("0.0.0.0:1161")
321    ///     .communities(communities)
322    ///     .build()
323    ///     .await?;
324    /// # Ok(())
325    /// # }
326    /// ```
327    #[must_use]
328    pub fn communities<I, C>(mut self, communities: I) -> Self
329    where
330        I: IntoIterator<Item = C>,
331        C: AsRef<[u8]>,
332    {
333        for c in communities {
334            self.communities.push(c.as_ref().to_vec());
335        }
336        self
337    }
338
339    /// Add a USM user for `SNMPv3` authentication.
340    ///
341    /// Configure authentication and privacy settings using the closure.
342    /// Multiple users can be added with different security levels.
343    ///
344    /// # Security Levels
345    ///
346    /// - **noAuthNoPriv**: No authentication or encryption
347    /// - **authNoPriv**: Authentication only (HMAC verification)
348    /// - **authPriv**: Authentication and encryption
349    ///
350    /// # Example
351    ///
352    /// ```rust,no_run
353    /// use async_snmp::agent::Agent;
354    /// use async_snmp::{AuthProtocol, PrivProtocol};
355    ///
356    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
357    /// let agent = Agent::builder()
358    ///     .bind("0.0.0.0:1161")
359    ///     // Read-only user with authentication only
360    ///     .usm_user("monitor", |u| {
361    ///         u.auth(AuthProtocol::Sha256, b"monitorpass123")
362    ///     })
363    ///     // Admin user with full encryption
364    ///     .usm_user("admin", |u| {
365    ///         u.auth(AuthProtocol::Sha256, b"adminauth123")
366    ///          .privacy(PrivProtocol::Aes128, b"adminpriv123")
367    ///     })
368    ///     .build()
369    ///     .await?;
370    /// # Ok(())
371    /// # }
372    /// ```
373    #[must_use]
374    pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
375    where
376        F: FnOnce(UsmConfig) -> UsmConfig,
377    {
378        let username_bytes: Bytes = username.into();
379        let config = configure(UsmConfig::new(username_bytes.clone()));
380        self.usm_users.insert(username_bytes, config);
381        self
382    }
383
384    /// Set the engine ID for `SNMPv3`.
385    ///
386    /// If not set, a default engine ID will be generated based on the
387    /// RFC 3411 format using enterprise number and timestamp.
388    ///
389    /// # Example
390    ///
391    /// ```rust,no_run
392    /// use async_snmp::agent::Agent;
393    ///
394    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
395    /// let agent = Agent::builder()
396    ///     .bind("0.0.0.0:1161")
397    ///     .engine_id(b"\x80\x00\x00\x00\x01MyEngine".to_vec())
398    ///     .community(b"public")
399    ///     .build()
400    ///     .await?;
401    /// # Ok(())
402    /// # }
403    /// ```
404    #[must_use]
405    pub fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
406        self.engine_id = Some(engine_id.into());
407        self
408    }
409
410    /// Set the initial engine boots value.
411    ///
412    /// Per RFC 3414 Section 2.3, snmpEngineBoots must be monotonically
413    /// increasing across restarts. The application is responsible for
414    /// persisting and restoring this value. If not set, defaults to 1.
415    ///
416    /// # Example
417    ///
418    /// ```rust,no_run
419    /// use async_snmp::agent::Agent;
420    ///
421    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
422    /// // Load persisted value (e.g. from file or database)
423    /// let persisted_boots: u32 = 42;
424    ///
425    /// let agent = Agent::builder()
426    ///     .bind("0.0.0.0:1161")
427    ///     .engine_boots(persisted_boots)
428    ///     .community(b"public")
429    ///     .build()
430    ///     .await?;
431    /// # Ok(())
432    /// # }
433    /// ```
434    #[must_use]
435    pub fn engine_boots(mut self, boots: u32) -> Self {
436        self.engine_boots = boots;
437        self
438    }
439
440    /// Set the maximum message size for responses.
441    ///
442    /// Default is 1472 octets (fits Ethernet MTU minus IP/UDP headers).
443    /// GETBULK responses will be truncated to fit within this limit.
444    ///
445    /// For `SNMPv3` requests, the agent uses the minimum of this value
446    /// and the msgMaxSize from the request.
447    #[must_use]
448    pub fn max_message_size(mut self, size: usize) -> Self {
449        self.max_message_size = size;
450        self
451    }
452
453    /// Set the maximum number of concurrent requests the agent will process.
454    ///
455    /// Default is 1000. Requests beyond this limit will queue until a slot
456    /// becomes available. Set to `None` for unbounded concurrency.
457    ///
458    /// This controls memory usage under high load while still allowing
459    /// parallel request processing.
460    #[must_use]
461    pub fn max_concurrent_requests(mut self, limit: Option<usize>) -> Self {
462        self.max_concurrent_requests = limit;
463        self
464    }
465
466    /// Set the UDP socket receive buffer size.
467    ///
468    /// Default is 4MB. The kernel may cap this at `net.core.rmem_max`.
469    /// A larger buffer prevents packet loss during request bursts.
470    ///
471    /// Set to `None` to use the kernel default.
472    #[must_use]
473    pub fn recv_buffer_size(mut self, size: Option<usize>) -> Self {
474        self.recv_buffer_size = size;
475        self
476    }
477
478    /// Register a MIB handler for an OID subtree.
479    ///
480    /// Handlers are matched by longest prefix. When a request comes in,
481    /// the handler with the longest matching prefix is used.
482    ///
483    /// # Example
484    ///
485    /// ```rust,no_run
486    /// use async_snmp::agent::Agent;
487    /// use async_snmp::handler::{MibHandler, RequestContext, GetResult, GetNextResult, BoxFuture};
488    /// use async_snmp::{Oid, Value, VarBind, oid};
489    /// use std::sync::Arc;
490    ///
491    /// struct SystemHandler;
492    /// impl MibHandler for SystemHandler {
493    ///     fn get<'a>(&'a self, _: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
494    ///         Box::pin(async move {
495    ///             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {
496    ///                 GetResult::Value(Value::OctetString("My Agent".into()))
497    ///             } else {
498    ///                 GetResult::NoSuchObject
499    ///             }
500    ///         })
501    ///     }
502    ///     fn get_next<'a>(&'a self, _: &'a RequestContext, _: &'a Oid) -> BoxFuture<'a, GetNextResult> {
503    ///         Box::pin(async { GetNextResult::EndOfMibView })
504    ///     }
505    /// }
506    ///
507    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
508    /// let agent = Agent::builder()
509    ///     .bind("0.0.0.0:1161")
510    ///     .community(b"public")
511    ///     // Register handler for system MIB subtree
512    ///     .handler(oid!(1, 3, 6, 1, 2, 1, 1), Arc::new(SystemHandler))
513    ///     .build()
514    ///     .await?;
515    /// # Ok(())
516    /// # }
517    /// ```
518    #[must_use]
519    pub fn handler(mut self, prefix: Oid, handler: Arc<dyn MibHandler>) -> Self {
520        self.handlers.push(RegisteredHandler { prefix, handler });
521        self
522    }
523
524    /// Configure VACM (View-based Access Control Model) using a builder function.
525    ///
526    /// When VACM is configured, all requests are checked against the configured
527    /// access control rules. Requests that don't have proper access are rejected
528    /// with `noAccess` error (v2c/v3) or `noSuchName` (v1).
529    ///
530    /// **Without VACM configuration, the agent operates in permissive mode**:
531    /// any authenticated request has full read/write access to all handlers.
532    ///
533    /// # Example
534    ///
535    /// ```rust,no_run
536    /// use async_snmp::agent::{Agent, SecurityModel, VacmBuilder};
537    /// use async_snmp::message::SecurityLevel;
538    /// use async_snmp::oid;
539    ///
540    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
541    /// let agent = Agent::builder()
542    ///     .bind("0.0.0.0:161")
543    ///     .community(b"public")
544    ///     .community(b"private")
545    ///     .vacm(|v| v
546    ///         .group("public", SecurityModel::V2c, "readonly_group")
547    ///         .group("private", SecurityModel::V2c, "readwrite_group")
548    ///         .access("readonly_group", |a| a
549    ///             .read_view("full_view"))
550    ///         .access("readwrite_group", |a| a
551    ///             .read_view("full_view")
552    ///             .write_view("write_view"))
553    ///         .view("full_view", |v| v
554    ///             .include(oid!(1, 3, 6, 1)))
555    ///         .view("write_view", |v| v
556    ///             .include(oid!(1, 3, 6, 1, 2, 1, 1))))
557    ///     .build()
558    ///     .await?;
559    /// # Ok(())
560    /// # }
561    /// ```
562    #[must_use]
563    pub fn vacm<F>(mut self, configure: F) -> Self
564    where
565        F: FnOnce(VacmBuilder) -> VacmBuilder,
566    {
567        let builder = VacmBuilder::new();
568        self.vacm = Some(configure(builder).build());
569        self
570    }
571
572    /// Set a cancellation token for graceful shutdown.
573    ///
574    /// If not set, the agent creates its own token accessible via `Agent::cancel()`.
575    #[must_use]
576    pub fn cancel(mut self, token: CancellationToken) -> Self {
577        self.cancel = Some(token);
578        self
579    }
580
581    /// Add a trap/inform destination.
582    ///
583    /// The agent will send notifications to all configured trap sinks when
584    /// [`Agent::send_trap()`] or [`Agent::send_inform()`] is called.
585    ///
586    /// # Example
587    ///
588    /// ```rust,no_run
589    /// use async_snmp::agent::Agent;
590    /// use async_snmp::{Auth, AuthProtocol, PrivProtocol};
591    ///
592    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
593    /// let agent = Agent::builder()
594    ///     .bind("0.0.0.0:1161")
595    ///     .community(b"public")
596    ///     .trap_sink("192.168.1.100:162", Auth::v2c("public"))
597    ///     .trap_sink("10.0.0.1:162", Auth::usm("trapuser")
598    ///         .auth(AuthProtocol::Sha256, "authpass")
599    ///         .privacy(PrivProtocol::Aes128, "privpass"))
600    ///     .build()
601    ///     .await?;
602    /// # Ok(())
603    /// # }
604    /// ```
605    #[must_use]
606    pub fn trap_sink(
607        mut self,
608        dest: impl Into<String>,
609        auth: impl Into<crate::client::Auth>,
610    ) -> Self {
611        self.trap_sinks.push((dest.into(), auth.into()));
612        self
613    }
614
615    /// Set the timeout for inform requests sent to trap sinks.
616    ///
617    /// Default is 5 seconds. Only affects `send_inform`, not `send_trap`.
618    #[must_use]
619    pub fn inform_timeout(mut self, timeout: Duration) -> Self {
620        self.inform_timeout = timeout;
621        self
622    }
623
624    /// Set the retry policy for inform requests sent to trap sinks.
625    ///
626    /// Default is `Retry::default()` (3 retries with 1-second delay).
627    /// Only affects `send_inform`, not `send_trap`.
628    #[must_use]
629    pub fn inform_retry(mut self, retry: crate::client::Retry) -> Self {
630        self.inform_retry = retry;
631        self
632    }
633
634    /// Disable a specific built-in MIB handler group.
635    ///
636    /// By default, the agent registers handlers for snmpEngine, USM stats,
637    /// and MPD stats. Call this to prevent registration of a specific group,
638    /// e.g., if you want to provide your own handler for those OIDs.
639    #[must_use]
640    pub fn without_builtin_handler(mut self, mib: BuiltinMib) -> Self {
641        self.disabled_builtins.insert(mib);
642        self
643    }
644
645    /// Disable all built-in MIB handlers.
646    ///
647    /// The agent will not register any internal handlers for snmpEngine,
648    /// USM stats, or MPD stats. You can still query the counter values
649    /// via accessor methods like [`Agent::usm_unknown_engine_ids()`].
650    #[must_use]
651    pub fn without_builtin_handlers(mut self) -> Self {
652        self.disabled_builtins.insert(BuiltinMib::SnmpEngine);
653        self.disabled_builtins.insert(BuiltinMib::UsmStats);
654        self.disabled_builtins.insert(BuiltinMib::MpdStats);
655        self
656    }
657
658    /// Build the agent.
659    pub async fn build(mut self) -> Result<Agent> {
660        let bind_addr: std::net::SocketAddr = self.bind_addr.parse().map_err(|_| {
661            Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
662        })?;
663
664        let socket = bind_udp_socket(bind_addr, self.recv_buffer_size, None, false)
665            .await
666            .map_err(|e| Error::Network {
667                target: bind_addr,
668                source: e,
669            })?;
670
671        let local_addr = socket.local_addr().map_err(|e| Error::Network {
672            target: bind_addr,
673            source: e,
674        })?;
675
676        let socket_state =
677            UdpSocketState::new(UdpSockRef::from(&socket)).map_err(|e| Error::Network {
678                target: bind_addr,
679                source: e,
680            })?;
681
682        // Generate default engine ID if not provided
683        let engine_id: Bytes = self.engine_id.map_or_else(
684            || {
685                // RFC 3411 format: enterprise number + format + local identifier
686                // Use a simple format: 0x80 (local) + timestamp + random
687                let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01]; // Enterprise format indicator
688                let timestamp = std::time::SystemTime::now()
689                    .duration_since(std::time::UNIX_EPOCH)
690                    .unwrap_or_default()
691                    .as_secs();
692                id.extend_from_slice(&timestamp.to_be_bytes());
693                Bytes::from(id)
694            },
695            Bytes::from,
696        );
697
698        let cancel = self.cancel.unwrap_or_default();
699
700        // Create concurrency limiter if configured
701        let concurrency_limit = self
702            .max_concurrent_requests
703            .map(|n| Arc::new(Semaphore::new(n)));
704
705        // Resolve trap sink addresses
706        let mut trap_sinks = Vec::with_capacity(self.trap_sinks.len());
707        for (dest_str, auth) in self.trap_sinks {
708            let dest: SocketAddr = dest_str.parse().map_err(|_| {
709                Error::Config(format!("invalid trap sink address: {dest_str}").into())
710            })?;
711            trap_sinks.push(notification::TrapSink::new(
712                dest,
713                auth,
714                self.inform_timeout,
715                self.inform_retry.clone(),
716            ));
717        }
718
719        let state = Arc::new(AgentState {
720            engine_id,
721            engine_boots: AtomicU32::new(self.engine_boots),
722            engine_time: AtomicU32::new(0),
723            engine_start: Instant::now(),
724            engine_boots_base: self.engine_boots,
725            max_message_size: self.max_message_size,
726            snmp_invalid_msgs: AtomicU32::new(0),
727            snmp_unknown_security_models: AtomicU32::new(0),
728            snmp_silent_drops: AtomicU32::new(0),
729            usm_unknown_engine_ids: AtomicU32::new(0),
730            usm_unknown_usernames: AtomicU32::new(0),
731            usm_wrong_digests: AtomicU32::new(0),
732            usm_not_in_time_windows: AtomicU32::new(0),
733            usm_unsupported_sec_levels: AtomicU32::new(0),
734            usm_decryption_errors: AtomicU32::new(0),
735        });
736
737        // Register built-in handlers for any not disabled
738        if !self.disabled_builtins.contains(&BuiltinMib::SnmpEngine) {
739            self.handlers.push(RegisteredHandler {
740                prefix: oid!(1, 3, 6, 1, 6, 3, 10, 2, 1),
741                handler: Arc::new(builtins::SnmpEngineHandler {
742                    state: Arc::clone(&state),
743                }),
744            });
745        }
746        if !self.disabled_builtins.contains(&BuiltinMib::UsmStats) {
747            self.handlers.push(RegisteredHandler {
748                prefix: oid!(1, 3, 6, 1, 6, 3, 15, 1, 1),
749                handler: Arc::new(builtins::UsmStatsHandler {
750                    state: Arc::clone(&state),
751                }),
752            });
753        }
754        if !self.disabled_builtins.contains(&BuiltinMib::MpdStats) {
755            self.handlers.push(RegisteredHandler {
756                prefix: oid!(1, 3, 6, 1, 6, 3, 11, 2, 1),
757                handler: Arc::new(builtins::MpdStatsHandler {
758                    state: Arc::clone(&state),
759                }),
760            });
761        }
762
763        // Sort handlers by prefix length (longest first) for matching
764        self.handlers
765            .sort_by_key(|h| std::cmp::Reverse(h.prefix.len()));
766
767        Ok(Agent {
768            inner: Arc::new(AgentInner {
769                socket: Arc::new(socket),
770                socket_state,
771                local_addr,
772                communities: self.communities,
773                usm_users: self.usm_users,
774                handlers: self.handlers,
775                state,
776                salt_counter: SaltCounter::new(),
777                concurrency_limit,
778                vacm: self.vacm,
779                cancel,
780                trap_sinks,
781            }),
782        })
783    }
784}
785
786impl Default for AgentBuilder {
787    fn default() -> Self {
788        Self::new()
789    }
790}
791
792/// Engine state and counters shared across agent clones and (future) built-in handlers.
793pub(crate) struct AgentState {
794    pub(crate) engine_id: Bytes,
795    pub(crate) engine_boots: AtomicU32,
796    pub(crate) engine_time: AtomicU32,
797    pub(crate) engine_start: Instant,
798    /// Initial `engine_boots` value at startup, used to compute overflow-adjusted boots.
799    pub(crate) engine_boots_base: u32,
800    pub(crate) max_message_size: usize,
801    // RFC 3412 statistics counters
802    /// snmpInvalidMsgs (1.3.6.1.6.3.11.2.1.2) - messages with invalid msgFlags
803    /// (e.g., privacy without authentication)
804    pub(crate) snmp_invalid_msgs: AtomicU32,
805    /// snmpUnknownSecurityModels (1.3.6.1.6.3.11.2.1.1) - messages with
806    /// unrecognized security model
807    pub(crate) snmp_unknown_security_models: AtomicU32,
808    /// snmpSilentDrops (1.3.6.1.6.3.11.2.1.3) - confirmed-class PDUs silently
809    /// dropped because even an empty response would exceed max message size
810    pub(crate) snmp_silent_drops: AtomicU32,
811    // RFC 3414 USM statistics counters
812    /// usmStatsUnknownEngineIDs (1.3.6.1.6.3.15.1.1.4) - messages with
813    /// unknown engine ID
814    pub(crate) usm_unknown_engine_ids: AtomicU32,
815    /// usmStatsUnknownUserNames (1.3.6.1.6.3.15.1.1.3) - messages with
816    /// unknown user name
817    pub(crate) usm_unknown_usernames: AtomicU32,
818    /// usmStatsWrongDigests (1.3.6.1.6.3.15.1.1.5) - messages with incorrect
819    /// authentication digest
820    pub(crate) usm_wrong_digests: AtomicU32,
821    /// usmStatsNotInTimeWindows (1.3.6.1.6.3.15.1.1.2) - messages outside
822    /// the time window
823    pub(crate) usm_not_in_time_windows: AtomicU32,
824    /// usmStatsUnsupportedSecLevels (1.3.6.1.6.3.15.1.1.1) - messages where
825    /// the user does not support the requested security level
826    pub(crate) usm_unsupported_sec_levels: AtomicU32,
827    /// usmStatsDecryptionErrors (1.3.6.1.6.3.15.1.1.6) - messages where
828    /// decryption failed
829    pub(crate) usm_decryption_errors: AtomicU32,
830}
831
832/// Inner state shared across agent clones.
833pub(crate) struct AgentInner {
834    pub(crate) socket: Arc<UdpSocket>,
835    pub(crate) socket_state: UdpSocketState,
836    pub(crate) local_addr: SocketAddr,
837    pub(crate) communities: Vec<Vec<u8>>,
838    pub(crate) usm_users: HashMap<Bytes, UsmConfig>,
839    pub(crate) handlers: Vec<RegisteredHandler>,
840    pub(crate) state: Arc<AgentState>,
841    pub(crate) salt_counter: SaltCounter,
842    pub(crate) concurrency_limit: Option<Arc<Semaphore>>,
843    pub(crate) vacm: Option<VacmConfig>,
844    /// Cancellation token for graceful shutdown.
845    pub(crate) cancel: CancellationToken,
846    /// Configured trap/inform destinations.
847    pub(crate) trap_sinks: Vec<notification::TrapSink>,
848}
849
850/// SNMP Agent.
851///
852/// Listens for and responds to SNMP requests (GET, GETNEXT, GETBULK, SET).
853///
854/// # Example
855///
856/// ```rust,no_run
857/// use async_snmp::agent::Agent;
858/// use async_snmp::oid;
859///
860/// # async fn example() -> Result<(), Box<async_snmp::Error>> {
861/// let agent = Agent::builder()
862///     .bind("0.0.0.0:161")
863///     .community(b"public")
864///     .build()
865///     .await?;
866///
867/// agent.run().await
868/// # }
869/// ```
870pub struct Agent {
871    pub(crate) inner: Arc<AgentInner>,
872}
873
874impl Agent {
875    /// Create a builder for configuring the agent.
876    #[must_use]
877    pub fn builder() -> AgentBuilder {
878        AgentBuilder::new()
879    }
880
881    /// Get the local address the agent is bound to.
882    #[must_use]
883    pub fn local_addr(&self) -> SocketAddr {
884        self.inner.local_addr
885    }
886
887    /// Get the engine ID.
888    #[must_use]
889    pub fn engine_id(&self) -> &[u8] {
890        &self.inner.state.engine_id
891    }
892
893    /// Get the current engine boots value.
894    ///
895    /// Useful for persisting across restarts per RFC 3414 Section 2.3.
896    /// The persisted value should be passed to `AgentBuilder::engine_boots()`
897    /// on the next startup.
898    #[must_use]
899    pub fn engine_boots(&self) -> u32 {
900        self.inner.state.engine_boots.load(Ordering::Relaxed)
901    }
902
903    /// Get the current engine time value.
904    #[must_use]
905    pub fn engine_time(&self) -> u32 {
906        self.inner.state.engine_time.load(Ordering::Relaxed)
907    }
908
909    /// Get the cancellation token for this agent.
910    ///
911    /// Call `token.cancel()` to initiate graceful shutdown.
912    #[must_use]
913    pub fn cancel(&self) -> CancellationToken {
914        self.inner.cancel.clone()
915    }
916
917    /// Get the snmpInvalidMsgs counter value.
918    ///
919    /// This counter tracks messages with invalid msgFlags, such as
920    /// privacy-without-authentication (RFC 3412 Section 7.2 Step 5d).
921    ///
922    /// OID: 1.3.6.1.6.3.11.2.1.2
923    #[must_use]
924    pub fn snmp_invalid_msgs(&self) -> u32 {
925        self.inner.state.snmp_invalid_msgs.load(Ordering::Relaxed)
926    }
927
928    /// Get the snmpUnknownSecurityModels counter value.
929    ///
930    /// This counter tracks messages with unrecognized security models
931    /// (RFC 3412 Section 7.2 Step 2).
932    ///
933    /// OID: 1.3.6.1.6.3.11.2.1.1
934    #[must_use]
935    pub fn snmp_unknown_security_models(&self) -> u32 {
936        self.inner
937            .state
938            .snmp_unknown_security_models
939            .load(Ordering::Relaxed)
940    }
941
942    /// Get the snmpSilentDrops counter value.
943    ///
944    /// This counter tracks confirmed-class PDUs (`GetRequest`, `GetNextRequest`,
945    /// `GetBulkRequest`, `SetRequest`, `InformRequest`) that were silently dropped
946    /// because even an empty Response-PDU would exceed the maximum message
947    /// size constraint (RFC 3412 Section 7.1).
948    ///
949    /// OID: 1.3.6.1.6.3.11.2.1.3
950    #[must_use]
951    pub fn snmp_silent_drops(&self) -> u32 {
952        self.inner.state.snmp_silent_drops.load(Ordering::Relaxed)
953    }
954
955    /// Get the usmStatsUnknownEngineIDs counter value.
956    ///
957    /// This counter tracks messages with unknown engine IDs.
958    /// Incremented when a non-discovery request arrives with an engine ID that
959    /// does not match the local engine (RFC 3414 Section 3.2 Step 3).
960    ///
961    /// OID: 1.3.6.1.6.3.15.1.1.4
962    #[must_use]
963    pub fn usm_unknown_engine_ids(&self) -> u32 {
964        self.inner
965            .state
966            .usm_unknown_engine_ids
967            .load(Ordering::Relaxed)
968    }
969
970    /// Get the usmStatsUnknownUserNames counter value.
971    ///
972    /// This counter tracks messages with unknown user names.
973    /// Incremented when a message arrives with a user name not in the local
974    /// user database (RFC 3414 Section 3.2 Step 1).
975    ///
976    /// OID: 1.3.6.1.6.3.15.1.1.3
977    #[must_use]
978    pub fn usm_unknown_usernames(&self) -> u32 {
979        self.inner
980            .state
981            .usm_unknown_usernames
982            .load(Ordering::Relaxed)
983    }
984
985    /// Get the usmStatsWrongDigests counter value.
986    ///
987    /// This counter tracks messages with incorrect authentication digests.
988    /// (RFC 3414 Section 3.2 Step 6).
989    ///
990    /// OID: 1.3.6.1.6.3.15.1.1.5
991    #[must_use]
992    pub fn usm_wrong_digests(&self) -> u32 {
993        self.inner.state.usm_wrong_digests.load(Ordering::Relaxed)
994    }
995
996    /// Get the usmStatsNotInTimeWindows counter value.
997    ///
998    /// This counter tracks messages requesting an authenticated security
999    /// level that fail the time window check (RFC 3414 Section 3.2 Step 7a):
1000    /// engine boots mismatch, boots latched at the maximum (checked before
1001    /// digest verification), or message time differing from the local time
1002    /// by more than 150 seconds.
1003    ///
1004    /// OID: 1.3.6.1.6.3.15.1.1.2
1005    #[must_use]
1006    pub fn usm_not_in_time_windows(&self) -> u32 {
1007        self.inner
1008            .state
1009            .usm_not_in_time_windows
1010            .load(Ordering::Relaxed)
1011    }
1012
1013    /// Get the usmStatsUnsupportedSecLevels counter value.
1014    ///
1015    /// This counter tracks messages where the user does not support
1016    /// the requested security level (e.g., auth required but user
1017    /// has no auth key configured). RFC 3414 Section 3.2.
1018    ///
1019    /// OID: 1.3.6.1.6.3.15.1.1.1
1020    #[must_use]
1021    pub fn usm_unsupported_sec_levels(&self) -> u32 {
1022        self.inner
1023            .state
1024            .usm_unsupported_sec_levels
1025            .load(Ordering::Relaxed)
1026    }
1027
1028    /// Get the usmStatsDecryptionErrors counter value.
1029    ///
1030    /// This counter tracks messages where decryption failed (the user
1031    /// has a privacy key but the decrypt operation returned an error).
1032    /// RFC 3414 Section 3.2.
1033    ///
1034    /// OID: 1.3.6.1.6.3.15.1.1.6
1035    #[must_use]
1036    pub fn usm_decryption_errors(&self) -> u32 {
1037        self.inner
1038            .state
1039            .usm_decryption_errors
1040            .load(Ordering::Relaxed)
1041    }
1042
1043    /// Returns agent uptime in hundredths of a second (centiseconds).
1044    ///
1045    /// Use this in your system MIB handler to provide sysUpTime.0
1046    /// (1.3.6.1.2.1.1.3.0) as a `Value::TimeTicks` value.
1047    #[must_use]
1048    pub fn uptime_hundredths(&self) -> u32 {
1049        let elapsed = self.inner.state.engine_start.elapsed();
1050        let centisecs = elapsed.as_millis() / 10;
1051        centisecs.min(u128::from(u32::MAX)) as u32
1052    }
1053
1054    /// Run the agent, processing requests concurrently.
1055    ///
1056    /// Requests are processed in parallel up to the configured
1057    /// `max_concurrent_requests` limit (default: 1000). This method runs
1058    /// until the cancellation token is triggered.
1059    #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
1060    pub async fn run(&self) -> Result<()> {
1061        let mut buf = vec![0u8; 65535];
1062
1063        loop {
1064            let recv_meta = tokio::select! {
1065                result = self.recv_packet(&mut buf) => {
1066                    result?
1067                }
1068                () = self.inner.cancel.cancelled() => {
1069                    tracing::info!(target: "async_snmp::agent", "agent shutdown requested");
1070                    return Ok(());
1071                }
1072            };
1073
1074            let data = Bytes::copy_from_slice(&buf[..recv_meta.len]);
1075            let agent = self.clone();
1076
1077            let permit = if let Some(ref sem) = self.inner.concurrency_limit {
1078                Some(sem.clone().acquire_owned().await.expect("semaphore closed"))
1079            } else {
1080                None
1081            };
1082
1083            tokio::spawn(async move {
1084                agent.update_engine_time();
1085
1086                match agent.handle_request(data, recv_meta.addr).await {
1087                    Ok(Some(response_bytes)) => {
1088                        // RFC 3413 Section 3.2 step 4: if the encoded response
1089                        // exceeds the max message size, silently drop it.
1090                        if response_bytes.len() > agent.inner.state.max_message_size {
1091                            agent
1092                                .inner
1093                                .state
1094                                .snmp_silent_drops
1095                                .fetch_add(1, Ordering::Relaxed);
1096                            tracing::debug!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, response_size = response_bytes.len(), max_size = agent.inner.state.max_message_size }, "response exceeds max message size, silently dropped");
1097                        } else if let Err(e) =
1098                            agent.send_response(&response_bytes, &recv_meta).await
1099                        {
1100                            tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "failed to send response");
1101                        }
1102                    }
1103                    Ok(None) => {}
1104                    Err(e) => {
1105                        tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "error handling request");
1106                    }
1107                }
1108
1109                drop(permit);
1110            });
1111        }
1112    }
1113
1114    async fn recv_packet(&self, buf: &mut [u8]) -> Result<RecvMeta> {
1115        let mut iov = [IoSliceMut::new(buf)];
1116        let mut meta = [RecvMeta::default()];
1117
1118        loop {
1119            self.inner
1120                .socket
1121                .readable()
1122                .await
1123                .map_err(|e| Error::Network {
1124                    target: self.inner.local_addr,
1125                    source: e,
1126                })?;
1127
1128            let result = self.inner.socket.try_io(tokio::io::Interest::READABLE, || {
1129                let sref = UdpSockRef::from(&*self.inner.socket);
1130                self.inner.socket_state.recv(sref, &mut iov, &mut meta)
1131            });
1132
1133            match result {
1134                Ok(n) if n > 0 => return Ok(meta[0]),
1135                Ok(_) => { /* fall thru to next `loop {}` iteration */ }
1136                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { /* fall thru to next `loop {}` iteration */
1137                }
1138                Err(e) => {
1139                    return Err(Error::Network {
1140                        target: self.inner.local_addr,
1141                        source: e,
1142                    }
1143                    .boxed());
1144                }
1145            }
1146        }
1147    }
1148
1149    async fn send_response(&self, data: &[u8], recv_meta: &RecvMeta) -> std::io::Result<()> {
1150        let transmit = Transmit {
1151            destination: recv_meta.addr,
1152            ecn: None,
1153            contents: data,
1154            segment_size: None,
1155            src_ip: recv_meta.dst_ip,
1156        };
1157
1158        loop {
1159            self.inner.socket.writable().await?;
1160
1161            let result = self.inner.socket.try_io(tokio::io::Interest::WRITABLE, || {
1162                let sref = UdpSockRef::from(&*self.inner.socket);
1163                self.inner.socket_state.try_send(sref, &transmit)
1164            });
1165
1166            match result {
1167                Ok(()) => return Ok(()),
1168                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { /* fall thru to next `loop {}` iteration */
1169                }
1170                Err(e) => return Err(e),
1171            }
1172        }
1173    }
1174
1175    /// Process a single request and return the response bytes.
1176    ///
1177    /// Returns `None` if no response should be sent.
1178    async fn handle_request(&self, data: Bytes, source: SocketAddr) -> Result<Option<Bytes>> {
1179        // Peek at version
1180        let mut decoder = Decoder::with_target(data.clone(), source);
1181        let mut seq = decoder.read_sequence()?;
1182        let version_num = seq.read_integer()?;
1183        let version = Version::from_i32(version_num).ok_or_else(|| {
1184            tracing::debug!(target: "async_snmp::agent", { source = %source, kind = %DecodeErrorKind::UnknownVersion(version_num) }, "unknown SNMP version");
1185            Error::MalformedResponse { target: source }.boxed()
1186        })?;
1187        drop(seq);
1188        drop(decoder);
1189
1190        match version {
1191            Version::V1 => self.handle_v1(data, source).await,
1192            Version::V2c => self.handle_v2c(data, source).await,
1193            Version::V3 => self.handle_v3(data, source).await,
1194        }
1195    }
1196
1197    /// Update engine boots and time based on elapsed time since start.
1198    ///
1199    /// Per RFC 3414 Section 2.3, when snmpEngineTime reaches `MAX_ENGINE_TIME`
1200    /// (2^31-1), snmpEngineBoots is incremented and snmpEngineTime resets to
1201    /// zero. The boots/time pair is derived from total elapsed seconds and
1202    /// the base boots value at startup, so no mutable state beyond the
1203    /// atomics is needed.
1204    fn update_engine_time(&self) {
1205        let total_secs = self.inner.state.engine_start.elapsed().as_secs();
1206        let (boots, time) =
1207            compute_engine_boots_time(self.inner.state.engine_boots_base, total_secs);
1208
1209        if boots != self.inner.state.engine_boots.load(Ordering::Relaxed)
1210            && boots > self.inner.state.engine_boots_base
1211        {
1212            tracing::warn!(
1213                target: "async_snmp::agent",
1214                engine_boots = boots,
1215                "engine time wrapped past MAX_ENGINE_TIME, incrementing engine boots"
1216            );
1217        }
1218
1219        self.inner
1220            .state
1221            .engine_boots
1222            .store(boots, Ordering::Relaxed);
1223        self.inner.state.engine_time.store(time, Ordering::Relaxed);
1224    }
1225
1226    /// Validate community string using constant-time comparison.
1227    ///
1228    /// Uses constant-time comparison to prevent timing attacks that could
1229    /// be used to guess valid community strings character by character.
1230    pub(crate) fn validate_community(&self, community: &[u8]) -> bool {
1231        if self.inner.communities.is_empty() {
1232            // No communities configured = reject all
1233            return false;
1234        }
1235        // Use constant-time comparison for each community string.
1236        // We compare against all configured communities regardless of
1237        // early matches to maintain constant-time behavior.
1238        let mut valid = false;
1239        for configured in &self.inner.communities {
1240            // ct_eq returns a Choice, which we convert to bool after comparison
1241            if configured.len() == community.len()
1242                && bool::from(configured.as_slice().ct_eq(community))
1243            {
1244                valid = true;
1245            }
1246        }
1247        valid
1248    }
1249
1250    /// Dispatch a request to the appropriate handler.
1251    async fn dispatch_request(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1252        match pdu.pdu_type {
1253            PduType::GetRequest => self.handle_get(ctx, pdu).await,
1254            PduType::GetNextRequest => self.handle_get_next(ctx, pdu).await,
1255            PduType::GetBulkRequest => {
1256                // SNMPv1 does not support GETBULK
1257                if ctx.version == Version::V1 {
1258                    return Ok(pdu.to_error_response(ErrorStatus::GenErr, 0));
1259                }
1260                self.handle_get_bulk(ctx, pdu).await
1261            }
1262            PduType::SetRequest => self.handle_set(ctx, pdu).await,
1263            PduType::InformRequest => self.handle_inform(pdu),
1264            _ => {
1265                // Should not happen - filtered earlier
1266                Ok(pdu.to_error_response(ErrorStatus::GenErr, 0))
1267            }
1268        }
1269    }
1270
1271    /// Handle `InformRequest` PDU.
1272    ///
1273    /// Per RFC 3416 Section 4.2.7, an `InformRequest` is a confirmed-class PDU
1274    /// that the receiver acknowledges by returning a Response with the same
1275    /// request-id and varbind list.
1276    #[allow(
1277        clippy::unnecessary_wraps,
1278        reason = "TODO store received informs, which may be a fallible operation"
1279    )]
1280    #[allow(
1281        clippy::unused_self,
1282        reason = "TODO store received informs, which may require self"
1283    )]
1284    fn handle_inform(&self, pdu: &Pdu) -> Result<Pdu> {
1285        // Simply acknowledge by returning the same varbinds in a Response
1286        Ok(pdu.to_response())
1287    }
1288
1289    /// Handle GET request.
1290    async fn handle_get(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1291        let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
1292
1293        for (index, vb) in pdu.varbinds.iter().enumerate() {
1294            // VACM read access check
1295            if let Some(ref vacm) = self.inner.vacm
1296                && !vacm.check_access(ctx.read_view.as_ref(), &vb.oid)
1297            {
1298                // v1: noSuchName, v2c/v3: noAccess or NoSuchObject
1299                if ctx.version == Version::V1 {
1300                    return Ok(pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32));
1301                }
1302                // For GET, return NoSuchObject for inaccessible OIDs per RFC 3415
1303                response_varbinds.push(VarBind::new(vb.oid.clone(), Value::NoSuchObject));
1304                continue;
1305            }
1306
1307            let result = if let Some(handler) = self.find_handler(&vb.oid) {
1308                handler.handler.get(ctx, &vb.oid).await
1309            } else {
1310                GetResult::NoSuchObject
1311            };
1312
1313            let response_value = match result {
1314                GetResult::Value(v) => {
1315                    // RFC 2576 Section 4.1.2.3: Counter64 not valid in v1
1316                    if ctx.version == Version::V1 && matches!(v, Value::Counter64(_)) {
1317                        return Ok(
1318                            pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1319                        );
1320                    }
1321                    v
1322                }
1323                GetResult::NoSuchObject => {
1324                    // v1 returns noSuchName error, v2c/v3 returns NoSuchObject exception
1325                    if ctx.version == Version::V1 {
1326                        return Ok(
1327                            pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1328                        );
1329                    }
1330                    Value::NoSuchObject
1331                }
1332                GetResult::NoSuchInstance => {
1333                    // v1 returns noSuchName error, v2c/v3 returns NoSuchInstance exception
1334                    if ctx.version == Version::V1 {
1335                        return Ok(
1336                            pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1337                        );
1338                    }
1339                    Value::NoSuchInstance
1340                }
1341            };
1342
1343            response_varbinds.push(VarBind::new(vb.oid.clone(), response_value));
1344        }
1345
1346        Ok(Pdu {
1347            pdu_type: PduType::Response,
1348            request_id: pdu.request_id,
1349            error_status: 0,
1350            error_index: 0,
1351            varbinds: response_varbinds,
1352        })
1353    }
1354
1355    /// Handle GETNEXT request.
1356    async fn handle_get_next(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1357        let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
1358
1359        for (index, vb) in pdu.varbinds.iter().enumerate() {
1360            // Try to find the next OID from any handler, skipping OIDs denied by
1361            // VACM. RFC 3413 classifies GETNEXT as Read-Class and requires
1362            // continuing the walk until an accessible OID is found.
1363            let next = self.get_next_accessible_oid(ctx, &vb.oid).await;
1364
1365            if let Some(next_vb) = next {
1366                response_varbinds.push(next_vb);
1367            } else {
1368                // v1 returns noSuchName, v2c/v3 returns endOfMibView
1369                if ctx.version == Version::V1 {
1370                    return Ok(pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32));
1371                }
1372                response_varbinds.push(VarBind::new(vb.oid.clone(), Value::EndOfMibView));
1373            }
1374        }
1375
1376        Ok(Pdu {
1377            pdu_type: PduType::Response,
1378            request_id: pdu.request_id,
1379            error_status: 0,
1380            error_index: 0,
1381            varbinds: response_varbinds,
1382        })
1383    }
1384
1385    /// Handle GETBULK request.
1386    ///
1387    /// Per RFC 3416 Section 4.2.3, if the response would exceed the message
1388    /// size limit, we return fewer variable bindings rather than all of them.
1389    async fn handle_get_bulk(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1390        // For GETBULK, error_status is non_repeaters and error_index is max_repetitions
1391        let non_repeaters = pdu.error_status.try_into().unwrap_or(0);
1392        let max_repetitions = pdu.error_index.max(0);
1393
1394        let mut response_varbinds = Vec::new();
1395        let mut current_size: usize = RESPONSE_OVERHEAD;
1396        let agent_max = self.inner.state.max_message_size;
1397        let max_size = match ctx.msg_max_size {
1398            Some(client_max) => agent_max.min(client_max as usize),
1399            None => agent_max,
1400        };
1401
1402        // Helper to check if we can add a varbind
1403        let can_add = |vb: &VarBind, current_size: usize| -> bool {
1404            current_size + vb.encoded_size() <= max_size
1405        };
1406
1407        // Handle non-repeaters (first N varbinds get one GETNEXT each)
1408        for vb in pdu.varbinds.iter().take(non_repeaters) {
1409            let next = self.get_next_accessible_oid(ctx, &vb.oid).await;
1410
1411            let next_vb = match next {
1412                Some(next_vb) => next_vb,
1413                None => VarBind::new(vb.oid.clone(), Value::EndOfMibView),
1414            };
1415
1416            if !can_add(&next_vb, current_size) {
1417                // Can't fit even non-repeaters, return tooBig if we have nothing
1418                if response_varbinds.is_empty() {
1419                    return Ok(pdu.to_error_response(ErrorStatus::TooBig, 0));
1420                }
1421                // Otherwise return what we have
1422                break;
1423            }
1424
1425            current_size += next_vb.encoded_size();
1426            response_varbinds.push(next_vb);
1427        }
1428
1429        // Handle repeaters
1430        if non_repeaters < pdu.varbinds.len() {
1431            let repeaters = &pdu.varbinds[non_repeaters..];
1432            let mut current_oids: Vec<Oid> = repeaters.iter().map(|vb| vb.oid.clone()).collect();
1433            let mut all_done = vec![false; repeaters.len()];
1434
1435            'outer: for _ in 0..max_repetitions {
1436                let mut row_complete = true;
1437                for (i, oid) in current_oids.iter_mut().enumerate() {
1438                    let next_vb = if all_done[i] {
1439                        VarBind::new(oid.clone(), Value::EndOfMibView)
1440                    } else {
1441                        let next = self.get_next_accessible_oid(ctx, oid).await;
1442
1443                        if let Some(next_vb) = next {
1444                            *oid = next_vb.oid.clone();
1445                            row_complete = false;
1446                            next_vb
1447                        } else {
1448                            all_done[i] = true;
1449                            VarBind::new(oid.clone(), Value::EndOfMibView)
1450                        }
1451                    };
1452
1453                    // Check size before adding
1454                    if !can_add(&next_vb, current_size) {
1455                        // Can't fit more, return what we have
1456                        break 'outer;
1457                    }
1458
1459                    current_size += next_vb.encoded_size();
1460                    response_varbinds.push(next_vb);
1461                }
1462
1463                if row_complete {
1464                    break;
1465                }
1466            }
1467        }
1468
1469        Ok(Pdu {
1470            pdu_type: PduType::Response,
1471            request_id: pdu.request_id,
1472            error_status: 0,
1473            error_index: 0,
1474            varbinds: response_varbinds,
1475        })
1476    }
1477
1478    /// Find the handler for a given OID.
1479    pub(crate) fn find_handler(&self, oid: &Oid) -> Option<&RegisteredHandler> {
1480        // Handlers are sorted by prefix length (longest first)
1481        self.inner
1482            .handlers
1483            .iter()
1484            .find(|&handler| handler.handler.handles(&handler.prefix, oid))
1485            .map(|v| v as _)
1486    }
1487
1488    /// Find the next OID accessible under VACM, skipping denied OIDs by
1489    /// continuing the walk. Returns None when end-of-MIB is reached or all
1490    /// remaining candidates are denied.
1491    async fn get_next_accessible_oid(
1492        &self,
1493        ctx: &RequestContext,
1494        from_oid: &Oid,
1495    ) -> Option<VarBind> {
1496        let mut search_from = from_oid.clone();
1497        loop {
1498            let candidate = self.get_next_oid(ctx, &search_from).await;
1499            match candidate {
1500                None => return None,
1501                Some(ref next_vb) => {
1502                    if next_vb.oid <= search_from {
1503                        tracing::error!(
1504                            target: "async_snmp::agent",
1505                            from = %search_from,
1506                            got = %next_vb.oid,
1507                            "handler returned non-increasing OID in GETNEXT"
1508                        );
1509                        return None;
1510                    }
1511                    // RFC 2576 Section 4.1.2.3: skip Counter64 for v1
1512                    if ctx.version == Version::V1 && matches!(next_vb.value, Value::Counter64(_)) {
1513                        search_from = next_vb.oid.clone();
1514                        continue;
1515                    }
1516                    if let Some(ref vacm) = self.inner.vacm {
1517                        if vacm.check_access(ctx.read_view.as_ref(), &next_vb.oid) {
1518                            return candidate;
1519                        }
1520                        search_from = next_vb.oid.clone();
1521                    } else {
1522                        return candidate;
1523                    }
1524                }
1525            }
1526        }
1527    }
1528
1529    /// Get the next OID from any handler.
1530    async fn get_next_oid(&self, ctx: &RequestContext, oid: &Oid) -> Option<VarBind> {
1531        // Find the first handler that can provide a next OID.
1532        //
1533        // A handler can only return an OID > oid if:
1534        //   - oid falls within the handler's subtree (oid starts with handler prefix), OR
1535        //   - the handler's entire subtree is after oid (handler prefix > oid)
1536        //
1537        // Handlers whose prefix is <= oid and whose subtree does not contain oid
1538        // cannot return anything useful and are skipped.
1539        let mut best_result: Option<VarBind> = None;
1540
1541        for handler in &self.inner.handlers {
1542            let prefix = &handler.prefix;
1543            if prefix <= oid && !oid.starts_with(prefix) {
1544                continue;
1545            }
1546            if let GetNextResult::Value(next) = handler.handler.get_next(ctx, oid).await {
1547                // Must be lexicographically greater than the request OID
1548                if next.oid > *oid {
1549                    match &best_result {
1550                        None => best_result = Some(next),
1551                        Some(current) if next.oid < current.oid => best_result = Some(next),
1552                        _ => {}
1553                    }
1554                }
1555            }
1556        }
1557
1558        best_result
1559    }
1560}
1561
1562impl Clone for Agent {
1563    fn clone(&self) -> Self {
1564        Self {
1565            inner: Arc::clone(&self.inner),
1566        }
1567    }
1568}
1569
1570#[cfg(test)]
1571mod tests {
1572    use super::*;
1573    use crate::handler::{
1574        BoxFuture, GetNextResult, GetResult, MibHandler, RequestContext, SecurityModel, SetResult,
1575    };
1576    use crate::message::SecurityLevel;
1577    use crate::oid;
1578
1579    struct TestHandler;
1580
1581    impl MibHandler for TestHandler {
1582        fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1583            Box::pin(async move {
1584                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
1585                    return GetResult::Value(Value::Integer(42));
1586                }
1587                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
1588                    return GetResult::Value(Value::OctetString(Bytes::from_static(b"test")));
1589                }
1590                GetResult::NoSuchObject
1591            })
1592        }
1593
1594        fn get_next<'a>(
1595            &'a self,
1596            _ctx: &'a RequestContext,
1597            oid: &'a Oid,
1598        ) -> BoxFuture<'a, GetNextResult> {
1599            Box::pin(async move {
1600                let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
1601                let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
1602
1603                if oid < &oid1 {
1604                    return GetNextResult::Value(VarBind::new(oid1, Value::Integer(42)));
1605                }
1606                if oid < &oid2 {
1607                    return GetNextResult::Value(VarBind::new(
1608                        oid2,
1609                        Value::OctetString(Bytes::from_static(b"test")),
1610                    ));
1611                }
1612                GetNextResult::EndOfMibView
1613            })
1614        }
1615    }
1616
1617    fn test_ctx() -> RequestContext {
1618        RequestContext {
1619            source: "127.0.0.1:12345".parse().unwrap(),
1620            version: Version::V2c,
1621            security_model: SecurityModel::V2c,
1622            security_name: Bytes::from_static(b"public"),
1623            security_level: SecurityLevel::NoAuthNoPriv,
1624            context_name: Bytes::new(),
1625            request_id: 1,
1626            pdu_type: PduType::GetRequest,
1627            group_name: None,
1628            read_view: None,
1629            write_view: None,
1630            msg_max_size: None,
1631        }
1632    }
1633
1634    #[test]
1635    fn test_agent_builder_defaults() {
1636        let builder = AgentBuilder::new();
1637        assert_eq!(builder.bind_addr, "0.0.0.0:161");
1638        assert!(builder.communities.is_empty());
1639        assert!(builder.usm_users.is_empty());
1640        assert!(builder.handlers.is_empty());
1641    }
1642
1643    #[test]
1644    fn test_agent_builder_community() {
1645        let builder = AgentBuilder::new()
1646            .community(b"public")
1647            .community(b"private");
1648        assert_eq!(builder.communities.len(), 2);
1649    }
1650
1651    #[test]
1652    fn test_agent_builder_communities() {
1653        let builder = AgentBuilder::new().communities(["public", "private"]);
1654        assert_eq!(builder.communities.len(), 2);
1655    }
1656
1657    #[test]
1658    fn test_agent_builder_handler() {
1659        let builder =
1660            AgentBuilder::new().handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler));
1661        assert_eq!(builder.handlers.len(), 1);
1662    }
1663
1664    #[tokio::test]
1665    async fn test_mib_handler_default_set() {
1666        let handler = TestHandler;
1667        let mut ctx = test_ctx();
1668        ctx.pdu_type = PduType::SetRequest;
1669
1670        let result = handler
1671            .test_set(&ctx, &oid!(1, 3, 6, 1), &Value::Integer(1))
1672            .await;
1673        assert_eq!(result, SetResult::NotWritable);
1674    }
1675
1676    #[test]
1677    fn test_mib_handler_handles() {
1678        let handler = TestHandler;
1679        let prefix = oid!(1, 3, 6, 1, 4, 1, 99_999);
1680
1681        // OID within prefix
1682        assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_999, 1, 0)));
1683
1684        // Exact prefix match
1685        assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_999)));
1686
1687        // OID before prefix - should NOT be handled (GET/SET routing must not claim
1688        // OIDs outside the registered subtree)
1689        assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_998)));
1690
1691        // OID after prefix (not handled)
1692        assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 100_000)));
1693    }
1694
1695    #[tokio::test]
1696    async fn test_test_handler_get() {
1697        let handler = TestHandler;
1698        let ctx = test_ctx();
1699
1700        // Existing OID
1701        let result = handler
1702            .get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
1703            .await;
1704        assert!(matches!(result, GetResult::Value(Value::Integer(42))));
1705
1706        // Non-existing OID
1707        let result = handler
1708            .get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 99, 0))
1709            .await;
1710        assert!(matches!(result, GetResult::NoSuchObject));
1711    }
1712
1713    #[tokio::test]
1714    async fn test_test_handler_get_next() {
1715        let handler = TestHandler;
1716        let mut ctx = test_ctx();
1717        ctx.pdu_type = PduType::GetNextRequest;
1718
1719        // Before first OID
1720        let next = handler.get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999)).await;
1721        assert!(next.is_value());
1722        if let GetNextResult::Value(vb) = next {
1723            assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0));
1724        }
1725
1726        // Between OIDs
1727        let next = handler
1728            .get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
1729            .await;
1730        assert!(next.is_value());
1731        if let GetNextResult::Value(vb) = next {
1732            assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0));
1733        }
1734
1735        // After last OID
1736        let next = handler
1737            .get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
1738            .await;
1739        assert!(next.is_end_of_mib_view());
1740    }
1741
1742    // FiveOidHandler has OIDs at .99999.{1,2,3,4,5}.0 with integer values 1-5.
1743    struct FiveOidHandler;
1744
1745    impl MibHandler for FiveOidHandler {
1746        fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1747            Box::pin(async move {
1748                for i in 1u16..=5 {
1749                    if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, i.into(), 0) {
1750                        return GetResult::Value(Value::Integer(i.into()));
1751                    }
1752                }
1753                GetResult::NoSuchObject
1754            })
1755        }
1756
1757        fn get_next<'a>(
1758            &'a self,
1759            _ctx: &'a RequestContext,
1760            oid: &'a Oid,
1761        ) -> BoxFuture<'a, GetNextResult> {
1762            Box::pin(async move {
1763                for i in 1u32..=5 {
1764                    let candidate = oid!(1, 3, 6, 1, 4, 1, 99999, i, 0);
1765                    if oid < &candidate {
1766                        return GetNextResult::Value(VarBind::new(
1767                            candidate,
1768                            Value::Integer(i as i32),
1769                        ));
1770                    }
1771                }
1772                GetNextResult::EndOfMibView
1773            })
1774        }
1775    }
1776
1777    /// Build an agent bound to a random port for testing, with a VACM view
1778    /// that only permits reading OIDs under .99999.2 and .99999.4 (odd OIDs
1779    /// 1, 3, 5 are denied). This exercises the VACM walk-past logic.
1780    async fn test_agent_with_restricted_vacm() -> Agent {
1781        Agent::builder()
1782            .bind("127.0.0.1:0")
1783            .community(b"public")
1784            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
1785            .vacm(|v| {
1786                v.group("public", SecurityModel::V2c, "readers")
1787                    .access("readers", |a| a.read_view("restricted"))
1788                    .view("restricted", |v| {
1789                        v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 2))
1790                            .include(oid!(1, 3, 6, 1, 4, 1, 99999, 4))
1791                    })
1792            })
1793            .build()
1794            .await
1795            .unwrap()
1796    }
1797
1798    #[tokio::test]
1799    async fn test_getbulk_vacm_filters_inaccessible_oids() {
1800        let agent = test_agent_with_restricted_vacm().await;
1801
1802        let mut ctx = test_ctx();
1803        ctx.pdu_type = PduType::GetBulkRequest;
1804        ctx.read_view = Some(Bytes::from_static(b"restricted"));
1805
1806        // GETBULK starting before the handler prefix, requesting up to 10 repeats.
1807        // The handler has OIDs {1,2,3,4,5}.0 but only {2,4} are in the view.
1808        // The walk must skip denied OIDs and continue, returning both 2 and 4.
1809        let pdu = Pdu {
1810            pdu_type: PduType::GetBulkRequest,
1811            request_id: 1,
1812            error_status: 0, // non_repeaters
1813            error_index: 10, // max_repetitions
1814            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
1815        };
1816
1817        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1818
1819        // Collect the OIDs returned (excluding EndOfMibView sentinels)
1820        let returned_oids: Vec<&Oid> = response
1821            .varbinds
1822            .iter()
1823            .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
1824            .map(|vb| &vb.oid)
1825            .collect();
1826
1827        // Both accessible OIDs must appear - the walk must not stop at the first one
1828        assert!(
1829            returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)),
1830            "expected .99999.2.0 in response, got: {returned_oids:?}"
1831        );
1832        assert!(
1833            returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0)),
1834            "expected .99999.4.0 in response (walk must continue past denied OIDs), got: {returned_oids:?}"
1835        );
1836
1837        // Denied OIDs must not appear
1838        for &oid in &[
1839            &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
1840            &oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
1841            &oid!(1, 3, 6, 1, 4, 1, 99999, 5, 0),
1842        ] {
1843            assert!(
1844                !returned_oids.contains(&oid),
1845                "GETBULK returned OID outside read view: {oid:?}"
1846            );
1847        }
1848    }
1849
1850    #[tokio::test]
1851    async fn test_getbulk_non_repeaters_vacm_filtered() {
1852        let agent = test_agent_with_restricted_vacm().await;
1853
1854        let mut ctx = test_ctx();
1855        ctx.pdu_type = PduType::GetBulkRequest;
1856        ctx.read_view = Some(Bytes::from_static(b"restricted"));
1857
1858        // GETBULK with non_repeaters=2, max_repetitions=0.
1859        // First varbind starts before the subtree: walks past denied .99999.1.0
1860        // and returns the first accessible .99999.2.0.
1861        // Second varbind starts at .99999.4.0 (the last accessible OID): walks
1862        // to .99999.5.0 (denied) and then hits end-of-MIB, returning EndOfMibView.
1863        let pdu = Pdu {
1864            pdu_type: PduType::GetBulkRequest,
1865            request_id: 2,
1866            error_status: 2, // non_repeaters
1867            error_index: 0,  // max_repetitions
1868            varbinds: vec![
1869                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null),
1870                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0), Value::Null),
1871            ],
1872        };
1873
1874        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1875
1876        // First non-repeater skips denied .99999.1.0 and returns accessible .99999.2.0
1877        assert_eq!(
1878            response.varbinds[0].oid,
1879            oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)
1880        );
1881        assert!(matches!(response.varbinds[0].value, Value::Integer(2)));
1882
1883        // Second non-repeater walks to .99999.5.0 (denied), then end-of-MIB
1884        assert_eq!(response.varbinds[1].value, Value::EndOfMibView);
1885    }
1886
1887    // TestHandler with three OIDs: .99999.1.0, .99999.2.0, .99999.3.0
1888    struct ThreeOidHandler;
1889
1890    impl MibHandler for ThreeOidHandler {
1891        fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1892            Box::pin(async move {
1893                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
1894                    return GetResult::Value(Value::Integer(1));
1895                }
1896                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
1897                    return GetResult::Value(Value::Integer(2));
1898                }
1899                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0) {
1900                    return GetResult::Value(Value::Integer(3));
1901                }
1902                GetResult::NoSuchObject
1903            })
1904        }
1905
1906        fn get_next<'a>(
1907            &'a self,
1908            _ctx: &'a RequestContext,
1909            oid: &'a Oid,
1910        ) -> BoxFuture<'a, GetNextResult> {
1911            Box::pin(async move {
1912                let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
1913                let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
1914                let oid3 = oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0);
1915
1916                if oid < &oid1 {
1917                    return GetNextResult::Value(VarBind::new(oid1, Value::Integer(1)));
1918                }
1919                if oid < &oid2 {
1920                    return GetNextResult::Value(VarBind::new(oid2, Value::Integer(2)));
1921                }
1922                if oid < &oid3 {
1923                    return GetNextResult::Value(VarBind::new(oid3, Value::Integer(3)));
1924                }
1925                GetNextResult::EndOfMibView
1926            })
1927        }
1928    }
1929
1930    /// Build an agent with `ThreeOidHandler` and a VACM view that includes
1931    /// .99999.1 and .99999.3 but excludes .99999.2.
1932    async fn test_agent_with_gap_vacm() -> Agent {
1933        Agent::builder()
1934            .bind("127.0.0.1:0")
1935            .community(b"public")
1936            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(ThreeOidHandler))
1937            .vacm(|v| {
1938                v.group("public", SecurityModel::V2c, "readers")
1939                    .access("readers", |a| a.read_view("gap"))
1940                    .view("gap", |v| {
1941                        v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 1))
1942                            .include(oid!(1, 3, 6, 1, 4, 1, 99999, 3))
1943                    })
1944            })
1945            .build()
1946            .await
1947            .unwrap()
1948    }
1949
1950    #[tokio::test]
1951    async fn test_getnext_vacm_skips_inaccessible_continues_walk() {
1952        // GETNEXT must continue past denied OIDs to find the next accessible one.
1953        // .99999.2.0 is excluded from the view; .99999.3.0 is included.
1954        // GETNEXT from .99999.1.0 should skip .99999.2.0 and return .99999.3.0.
1955        let agent = test_agent_with_gap_vacm().await;
1956
1957        let mut ctx = test_ctx();
1958        ctx.pdu_type = PduType::GetNextRequest;
1959        ctx.read_view = Some(Bytes::from_static(b"gap"));
1960
1961        let pdu = Pdu {
1962            pdu_type: PduType::GetNextRequest,
1963            request_id: 1,
1964            error_status: 0,
1965            error_index: 0,
1966            varbinds: vec![VarBind::new(
1967                oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
1968                Value::Null,
1969            )],
1970        };
1971
1972        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1973        assert_eq!(response.varbinds.len(), 1);
1974        assert_eq!(
1975            response.varbinds[0].oid,
1976            oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
1977            "GETNEXT should skip denied .99999.2.0 and return accessible .99999.3.0"
1978        );
1979        assert!(matches!(response.varbinds[0].value, Value::Integer(3)));
1980    }
1981
1982    #[tokio::test]
1983    async fn test_getnext_vacm_all_remaining_denied_returns_end_of_mib() {
1984        // When all remaining OIDs are denied, GETNEXT should return EndOfMibView.
1985        // Start at .99999.4.0 (the last accessible OID). The only OID after it
1986        // is .99999.5.0 which is denied, so the walk reaches end-of-MIB.
1987        let agent = test_agent_with_restricted_vacm().await;
1988
1989        let mut ctx = test_ctx();
1990        ctx.pdu_type = PduType::GetNextRequest;
1991        ctx.read_view = Some(Bytes::from_static(b"restricted"));
1992
1993        let pdu = Pdu {
1994            pdu_type: PduType::GetNextRequest,
1995            request_id: 1,
1996            error_status: 0,
1997            error_index: 0,
1998            varbinds: vec![VarBind::new(
1999                oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0),
2000                Value::Null,
2001            )],
2002        };
2003
2004        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2005        assert_eq!(response.varbinds.len(), 1);
2006        assert_eq!(
2007            response.varbinds[0].value,
2008            Value::EndOfMibView,
2009            "GETNEXT should return EndOfMibView when all remaining OIDs are denied"
2010        );
2011    }
2012
2013    #[tokio::test]
2014    async fn test_getbulk_without_vacm_returns_all_oids() {
2015        // Sanity check: without VACM, both OIDs should be returned
2016        let agent = Agent::builder()
2017            .bind("127.0.0.1:0")
2018            .community(b"public")
2019            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler))
2020            .build()
2021            .await
2022            .unwrap();
2023
2024        let mut ctx = test_ctx();
2025        ctx.pdu_type = PduType::GetBulkRequest;
2026
2027        let pdu = Pdu {
2028            pdu_type: PduType::GetBulkRequest,
2029            request_id: 1,
2030            error_status: 0,
2031            error_index: 10,
2032            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2033        };
2034
2035        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2036
2037        // Both OIDs should appear
2038        assert!(
2039            response
2040                .varbinds
2041                .iter()
2042                .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
2043        );
2044        assert!(
2045            response
2046                .varbinds
2047                .iter()
2048                .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
2049        );
2050    }
2051
2052    #[tokio::test]
2053    async fn test_v1_getbulk_rejected() {
2054        // SNMPv1 does not support GETBULK. Should return GenErr.
2055        let agent = Agent::builder()
2056            .bind("127.0.0.1:0")
2057            .community(b"public")
2058            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler))
2059            .build()
2060            .await
2061            .unwrap();
2062
2063        let mut ctx = test_ctx();
2064        ctx.version = Version::V1;
2065        ctx.security_model = SecurityModel::V1;
2066        ctx.pdu_type = PduType::GetBulkRequest;
2067
2068        let pdu = Pdu {
2069            pdu_type: PduType::GetBulkRequest,
2070            request_id: 1,
2071            error_status: 0,
2072            error_index: 10,
2073            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2074        };
2075
2076        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2077        assert_eq!(
2078            ErrorStatus::from_i32(response.error_status),
2079            ErrorStatus::GenErr,
2080            "v1 GETBULK should be rejected"
2081        );
2082    }
2083
2084    /// Handler returning Counter64 at .99999.1.0, Integer at .99999.2.0
2085    struct Counter64Handler;
2086
2087    impl MibHandler for Counter64Handler {
2088        fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
2089            Box::pin(async move {
2090                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
2091                    return GetResult::Value(Value::Counter64(1_000_000_000_000));
2092                }
2093                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
2094                    return GetResult::Value(Value::Integer(42));
2095                }
2096                GetResult::NoSuchObject
2097            })
2098        }
2099
2100        fn get_next<'a>(
2101            &'a self,
2102            _ctx: &'a RequestContext,
2103            oid: &'a Oid,
2104        ) -> BoxFuture<'a, GetNextResult> {
2105            Box::pin(async move {
2106                let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
2107                let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
2108
2109                if oid < &oid1 {
2110                    return GetNextResult::Value(VarBind::new(
2111                        oid1,
2112                        Value::Counter64(1_000_000_000_000),
2113                    ));
2114                }
2115                if oid < &oid2 {
2116                    return GetNextResult::Value(VarBind::new(oid2, Value::Integer(42)));
2117                }
2118                GetNextResult::EndOfMibView
2119            })
2120        }
2121    }
2122
2123    async fn test_agent_with_counter64() -> Agent {
2124        Agent::builder()
2125            .bind("127.0.0.1:0")
2126            .community(b"public")
2127            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(Counter64Handler))
2128            .build()
2129            .await
2130            .unwrap()
2131    }
2132
2133    #[tokio::test]
2134    async fn test_v1_get_filters_counter64() {
2135        // RFC 2576 Section 4.1.2.3: Counter64 not valid in v1 GET responses.
2136        // Should return noSuchName for the Counter64 varbind.
2137        let agent = test_agent_with_counter64().await;
2138
2139        let mut ctx = test_ctx();
2140        ctx.version = Version::V1;
2141        ctx.security_model = SecurityModel::V1;
2142        ctx.pdu_type = PduType::GetRequest;
2143
2144        let pdu = Pdu {
2145            pdu_type: PduType::GetRequest,
2146            request_id: 1,
2147            error_status: 0,
2148            error_index: 0,
2149            varbinds: vec![VarBind::new(
2150                oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2151                Value::Null,
2152            )],
2153        };
2154
2155        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2156        assert_eq!(
2157            ErrorStatus::from_i32(response.error_status),
2158            ErrorStatus::NoSuchName,
2159            "v1 GET of Counter64 should return noSuchName"
2160        );
2161    }
2162
2163    #[tokio::test]
2164    async fn test_v2c_get_allows_counter64() {
2165        // v2c should return Counter64 normally
2166        let agent = test_agent_with_counter64().await;
2167
2168        let ctx = test_ctx(); // v2c by default
2169
2170        let pdu = Pdu {
2171            pdu_type: PduType::GetRequest,
2172            request_id: 1,
2173            error_status: 0,
2174            error_index: 0,
2175            varbinds: vec![VarBind::new(
2176                oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2177                Value::Null,
2178            )],
2179        };
2180
2181        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2182        assert_eq!(response.error_status, 0);
2183        assert!(matches!(response.varbinds[0].value, Value::Counter64(_)));
2184    }
2185
2186    #[tokio::test]
2187    async fn test_getbulk_respects_v3_msg_max_size() {
2188        // When msg_max_size is set (V3 request), GETBULK should limit the
2189        // response to fit within min(agent_max, client_msg_max_size).
2190        // The agent has a large max_message_size, but the client advertises
2191        // a small msgMaxSize that can only fit a few varbinds.
2192        let agent = Agent::builder()
2193            .bind("127.0.0.1:0")
2194            .community(b"public")
2195            .max_message_size(65507) // agent allows large responses
2196            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
2197            .build()
2198            .await
2199            .unwrap();
2200
2201        // First, get the full response without msg_max_size limit
2202        let mut ctx_unlimited = test_ctx();
2203        ctx_unlimited.pdu_type = PduType::GetBulkRequest;
2204        ctx_unlimited.msg_max_size = None;
2205
2206        let pdu = Pdu {
2207            pdu_type: PduType::GetBulkRequest,
2208            request_id: 1,
2209            error_status: 0, // non_repeaters
2210            error_index: 10, // max_repetitions
2211            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2212        };
2213
2214        let full_response = agent.dispatch_request(&ctx_unlimited, &pdu).await.unwrap();
2215        let full_count = full_response
2216            .varbinds
2217            .iter()
2218            .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2219            .count();
2220        assert!(
2221            full_count >= 3,
2222            "expected at least 3 data varbinds without limit, got {full_count}"
2223        );
2224
2225        // Now set a small msg_max_size that limits the response.
2226        // RESPONSE_OVERHEAD is 100, and each varbind for OIDs like
2227        // .1.3.6.1.4.1.99999.N.0 with Integer value is ~22 bytes.
2228        // Set msg_max_size to fit overhead + ~2 varbinds but not all 5.
2229        let mut ctx_limited = test_ctx();
2230        ctx_limited.pdu_type = PduType::GetBulkRequest;
2231        ctx_limited.msg_max_size = Some(150); // overhead(100) + room for ~2 varbinds
2232
2233        let limited_response = agent.dispatch_request(&ctx_limited, &pdu).await.unwrap();
2234        let limited_count = limited_response
2235            .varbinds
2236            .iter()
2237            .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2238            .count();
2239
2240        assert!(
2241            limited_count < full_count,
2242            "V3 msg_max_size should limit response: got {limited_count} varbinds (unlimited: {full_count})"
2243        );
2244        assert!(
2245            limited_count > 0,
2246            "should still return at least one varbind"
2247        );
2248    }
2249
2250    #[tokio::test]
2251    async fn test_getbulk_msg_max_size_none_uses_agent_max() {
2252        // Without msg_max_size (v1/v2c), the agent's own max_message_size is used.
2253        // With a large agent max, all 5 OIDs should be returned.
2254        let agent = Agent::builder()
2255            .bind("127.0.0.1:0")
2256            .community(b"public")
2257            .max_message_size(65507)
2258            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
2259            .without_builtin_handlers()
2260            .build()
2261            .await
2262            .unwrap();
2263
2264        let mut ctx = test_ctx();
2265        ctx.pdu_type = PduType::GetBulkRequest;
2266        ctx.msg_max_size = None; // v2c, no client limit
2267
2268        let pdu = Pdu {
2269            pdu_type: PduType::GetBulkRequest,
2270            request_id: 1,
2271            error_status: 0,
2272            error_index: 10,
2273            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2274        };
2275
2276        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2277        let data_count = response
2278            .varbinds
2279            .iter()
2280            .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2281            .count();
2282        assert_eq!(
2283            data_count, 5,
2284            "all 5 OIDs should be returned without msg_max_size limit"
2285        );
2286    }
2287
2288    #[tokio::test]
2289    async fn test_v1_getnext_skips_counter64() {
2290        // RFC 2576 Section 4.1.2.3: Counter64 skipped in v1 GETNEXT.
2291        // Walking from .99999 should skip the Counter64 at .99999.1.0
2292        // and return the Integer at .99999.2.0.
2293        let agent = test_agent_with_counter64().await;
2294
2295        let mut ctx = test_ctx();
2296        ctx.version = Version::V1;
2297        ctx.security_model = SecurityModel::V1;
2298        ctx.pdu_type = PduType::GetNextRequest;
2299
2300        let pdu = Pdu {
2301            pdu_type: PduType::GetNextRequest,
2302            request_id: 1,
2303            error_status: 0,
2304            error_index: 0,
2305            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2306        };
2307
2308        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2309        assert_eq!(response.error_status, 0, "should succeed");
2310        assert_eq!(
2311            response.varbinds[0].oid,
2312            oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0),
2313            "should skip Counter64 and return next non-Counter64 OID"
2314        );
2315        assert!(matches!(response.varbinds[0].value, Value::Integer(42)));
2316    }
2317
2318    #[test]
2319    fn test_engine_time_no_overflow() {
2320        // Normal operation: elapsed < MAX_ENGINE_TIME, boots stays at base
2321        let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
2322        assert_eq!(boots, 1);
2323        assert_eq!(time, 1000);
2324    }
2325
2326    #[test]
2327    fn test_engine_time_zero_elapsed() {
2328        let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
2329        assert_eq!(boots, 1);
2330        assert_eq!(time, 0);
2331    }
2332
2333    #[test]
2334    fn test_engine_time_just_below_max() {
2335        let max = crate::v3::MAX_ENGINE_TIME;
2336        let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max) - 1);
2337        assert_eq!(boots, 1);
2338        assert_eq!(time, max - 1);
2339    }
2340
2341    #[test]
2342    fn test_engine_time_at_max_wraps() {
2343        // Exactly at MAX_ENGINE_TIME seconds: boots increments, time resets to 0
2344        let max = crate::v3::MAX_ENGINE_TIME;
2345        let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max));
2346        assert_eq!(
2347            boots, 2,
2348            "boots should increment when elapsed reaches MAX_ENGINE_TIME"
2349        );
2350        assert_eq!(time, 0, "time should wrap to 0");
2351    }
2352
2353    #[test]
2354    fn test_engine_time_past_max() {
2355        // 500 seconds past the first wrap
2356        let max = crate::v3::MAX_ENGINE_TIME;
2357        let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max) + 500);
2358        assert_eq!(boots, 2);
2359        assert_eq!(time, 500);
2360    }
2361
2362    #[test]
2363    fn test_engine_time_multiple_wraps() {
2364        // Three full cycles
2365        let max = crate::v3::MAX_ENGINE_TIME;
2366        let elapsed = u64::from(max) * 3 + 42;
2367        let (boots, time) = crate::v3::compute_engine_boots_time(1, elapsed);
2368        assert_eq!(boots, 4, "base 1 + 3 wraps = 4");
2369        assert_eq!(time, 42);
2370    }
2371
2372    #[test]
2373    fn test_engine_time_boots_capped_at_max() {
2374        // If enough wraps happen that boots would exceed MAX_ENGINE_TIME, cap it
2375        let max = crate::v3::MAX_ENGINE_TIME;
2376        let elapsed = u64::from(max) * u64::from(max); // way more wraps than max allows
2377        let (boots, _time) = crate::v3::compute_engine_boots_time(1, elapsed);
2378        assert_eq!(boots, max, "boots should be capped at MAX_ENGINE_TIME");
2379    }
2380
2381    #[test]
2382    fn test_engine_time_base_boots_preserved() {
2383        // A non-1 base boots (e.g. from persistence) is respected
2384        let max = crate::v3::MAX_ENGINE_TIME;
2385        let (boots, time) = crate::v3::compute_engine_boots_time(5, u64::from(max) + 100);
2386        assert_eq!(boots, 6, "base 5 + 1 wrap = 6");
2387        assert_eq!(time, 100);
2388    }
2389
2390    #[test]
2391    fn test_engine_time_high_base_boots_capped() {
2392        // Base boots near MAX_ENGINE_TIME with a wrap should cap
2393        let max = crate::v3::MAX_ENGINE_TIME;
2394        let (boots, _time) = crate::v3::compute_engine_boots_time(max - 1, u64::from(max) * 2);
2395        assert_eq!(boots, max, "should cap at MAX_ENGINE_TIME, not overflow");
2396    }
2397
2398    #[tokio::test]
2399    async fn test_engine_boots_builder() {
2400        // engine_boots builder method sets the initial boots value
2401        let agent = Agent::builder()
2402            .bind("127.0.0.1:0")
2403            .community(b"public")
2404            .engine_boots(42)
2405            .build()
2406            .await
2407            .unwrap();
2408
2409        assert_eq!(agent.engine_boots(), 42);
2410    }
2411
2412    #[tokio::test]
2413    async fn test_engine_boots_default() {
2414        // Default engine_boots is 1
2415        let agent = Agent::builder()
2416            .bind("127.0.0.1:0")
2417            .community(b"public")
2418            .build()
2419            .await
2420            .unwrap();
2421
2422        assert_eq!(agent.engine_boots(), 1);
2423    }
2424
2425    #[tokio::test]
2426    async fn test_usm_counter_accessors_default_zero() {
2427        let agent = Agent::builder()
2428            .bind("127.0.0.1:0")
2429            .community(b"public")
2430            .build()
2431            .await
2432            .unwrap();
2433
2434        assert_eq!(agent.usm_unsupported_sec_levels(), 0);
2435        assert_eq!(agent.usm_decryption_errors(), 0);
2436    }
2437
2438    #[test]
2439    fn test_builtin_mib_without_single() {
2440        let builder = AgentBuilder::new().without_builtin_handler(BuiltinMib::UsmStats);
2441        assert!(builder.disabled_builtins.contains(&BuiltinMib::UsmStats));
2442        assert!(!builder.disabled_builtins.contains(&BuiltinMib::SnmpEngine));
2443        assert!(!builder.disabled_builtins.contains(&BuiltinMib::MpdStats));
2444    }
2445
2446    #[test]
2447    fn test_builtin_mib_without_all() {
2448        let builder = AgentBuilder::new().without_builtin_handlers();
2449        assert!(builder.disabled_builtins.contains(&BuiltinMib::SnmpEngine));
2450        assert!(builder.disabled_builtins.contains(&BuiltinMib::UsmStats));
2451        assert!(builder.disabled_builtins.contains(&BuiltinMib::MpdStats));
2452    }
2453
2454    #[tokio::test]
2455    async fn test_uptime_hundredths() {
2456        let agent = Agent::builder()
2457            .bind("127.0.0.1:0")
2458            .community(b"public")
2459            .build()
2460            .await
2461            .unwrap();
2462
2463        let uptime = agent.uptime_hundredths();
2464        assert!(
2465            uptime < 100,
2466            "uptime should be less than 1 second, got {uptime}"
2467        );
2468
2469        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2470        let uptime2 = agent.uptime_hundredths();
2471        assert!(uptime2 > uptime, "uptime should increase after delay");
2472    }
2473
2474    #[tokio::test]
2475    async fn test_builtin_handlers_registered_by_default() {
2476        let agent = Agent::builder()
2477            .bind("127.0.0.1:0")
2478            .community(b"public")
2479            .build()
2480            .await
2481            .unwrap();
2482
2483        let ctx = test_ctx();
2484
2485        // snmpEngineMaxMessageSize.0 should be queryable
2486        let handler = agent
2487            .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 4, 0))
2488            .expect("snmpEngine handler should be registered");
2489        let get_result = handler
2490            .handler
2491            .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 4, 0))
2492            .await;
2493        assert!(matches!(get_result, GetResult::Value(Value::Integer(_))));
2494
2495        // usmStatsWrongDigests.0 should be queryable
2496        let handler = agent
2497            .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0))
2498            .expect("USM stats handler should be registered");
2499        let get_result = handler
2500            .handler
2501            .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0))
2502            .await;
2503        assert!(matches!(get_result, GetResult::Value(Value::Counter32(0))));
2504
2505        // snmpUnknownSecurityModels.0 should be queryable
2506        let handler = agent
2507            .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2508            .expect("MPD stats handler should be registered");
2509        let get_result = handler
2510            .handler
2511            .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2512            .await;
2513        assert!(matches!(get_result, GetResult::Value(Value::Counter32(0))));
2514    }
2515
2516    #[tokio::test]
2517    async fn test_builtin_handlers_disabled() {
2518        let agent = Agent::builder()
2519            .bind("127.0.0.1:0")
2520            .community(b"public")
2521            .without_builtin_handlers()
2522            .build()
2523            .await
2524            .unwrap();
2525
2526        assert!(
2527            agent
2528                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0))
2529                .is_none()
2530        );
2531        assert!(
2532            agent
2533                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0))
2534                .is_none()
2535        );
2536        assert!(
2537            agent
2538                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2539                .is_none()
2540        );
2541    }
2542
2543    #[tokio::test]
2544    async fn test_builtin_handler_selective_disable() {
2545        let agent = Agent::builder()
2546            .bind("127.0.0.1:0")
2547            .community(b"public")
2548            .without_builtin_handler(BuiltinMib::UsmStats)
2549            .build()
2550            .await
2551            .unwrap();
2552
2553        assert!(
2554            agent
2555                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0))
2556                .is_some()
2557        );
2558        assert!(
2559            agent
2560                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0))
2561                .is_none()
2562        );
2563        assert!(
2564            agent
2565                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2566                .is_some()
2567        );
2568    }
2569}