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