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, HandlerResult, 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, HandlerResult<GetResult>> {
27//!         Box::pin(async move {
28//!             // sysDescr.0
29//!             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {
30//!                 return Ok(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 Ok(GetResult::Value(Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 99999))));
35//!             }
36//!             Ok(GetResult::NoSuchObject)
37//!         })
38//!     }
39//!
40//!     fn get_next<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, HandlerResult<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 Ok(GetNextResult::Value(VarBind::new(sys_descr, Value::OctetString("My SNMP Agent".into()))));
48//!             }
49//!             if oid < &sys_object_id {
50//!                 return Ok(GetNextResult::Value(VarBind::new(sys_object_id, Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 99999)))));
51//!             }
52//!             Ok(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 notification::{NotificationOutcome, SinkOutcome};
78pub use vacm::{SecurityModel, VacmBuilder, VacmConfig, View, ViewCheckResult, ViewSubtree};
79
80use std::collections::{HashMap, HashSet};
81use std::net::SocketAddr;
82use std::sync::Arc;
83use std::sync::atomic::{AtomicU32, Ordering};
84use std::time::{Duration, Instant};
85
86use bytes::Bytes;
87use subtle::ConstantTimeEq;
88use tokio::net::UdpSocket;
89use tokio::sync::Semaphore;
90use tokio_util::sync::CancellationToken;
91use tracing::instrument;
92
93use std::io::IoSliceMut;
94
95use quinn_udp::{RecvMeta, Transmit, UdpSockRef, UdpSocketState};
96
97use crate::error::{Error, ErrorStatus, Result};
98use crate::handler::{GetNextResult, GetResult, HandlerResult, MibHandler, RequestContext};
99use crate::notification::UsmConfig;
100use crate::oid;
101use crate::oid::Oid;
102use crate::pdu::{Pdu, PduType};
103use crate::util::bind_udp_socket;
104use crate::v3::process::UsmStats;
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/// Base overhead for SNMP message encoding: the v1/v2c community wrapper plus
114/// the fixed BER framing shared by every response (message and PDU sequence
115/// headers, request-id / error-status / error-index integers, and, for v3, the
116/// msgGlobalData, USM, and scopedPDU framing). The variable-length community
117/// string (v1/v2c), variable-length v3 fields, and the auth/priv material are
118/// added on top in [`Agent::response_overhead`].
119const RESPONSE_OVERHEAD: usize = 100;
120
121/// Additional v3 overhead when the message is authenticated:
122/// msgAuthenticationParameters carries up to a 48-octet HMAC (SHA-512).
123const V3_AUTH_OVERHEAD: usize = 48;
124
125/// Additional v3 overhead when the message is encrypted: the 8-octet salt in
126/// msgPrivacyParameters, the OCTET STRING wrapper around the encrypted
127/// scopedPDU, and up to a full DES/AES block of CBC padding.
128const V3_PRIV_OVERHEAD: usize = 20;
129
130/// Maximum number of VACM-denied OIDs skipped while advancing a single GETNEXT
131/// step before giving up and reporting end-of-MIB for that varbind. Without a
132/// cap, a request spanning a large denied range forces O(range) backing-store
133/// lookups per step, a CPU-DoS shape. When the cap is hit the scan for that
134/// varbind ends rather than continuing to probe.
135const MAX_VACM_SKIP_ITERATIONS: usize = 1000;
136
137/// RFC 2576 Section 4.1.2.3: SNMPv1 has no Counter64 type, so a Counter64
138/// value cannot be carried in a v1 response varbind. GET responds with
139/// noSuchName; GETNEXT/GETBULK skip the offending varbind.
140fn v1_rejects_counter64(version: Version, value: &Value) -> bool {
141    version == Version::V1 && matches!(value, Value::Counter64(_))
142}
143
144/// Built-in MIB handler groups that the agent registers automatically.
145///
146/// By default, the agent registers handlers for standard SNMP MIB objects
147/// (engine parameters, USM statistics, MPD statistics). Use
148/// [`AgentBuilder::without_builtin_handler`] to disable specific groups
149/// or [`AgentBuilder::without_builtin_handlers`] to disable all of them.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
151pub enum BuiltinMib {
152    /// snmpEngine scalars (1.3.6.1.6.3.10.2.1).
153    ///
154    /// Provides snmpEngineID, snmpEngineBoots, snmpEngineTime,
155    /// and snmpEngineMaxMessageSize.
156    SnmpEngine,
157    /// USM statistics (1.3.6.1.6.3.15.1.1).
158    ///
159    /// Provides the six usmStats counters (unsupportedSecLevels,
160    /// notInTimeWindows, unknownUserNames, unknownEngineIDs,
161    /// wrongDigests, decryptionErrors).
162    UsmStats,
163    /// MPD statistics (1.3.6.1.6.3.11.2.1).
164    ///
165    /// Provides snmpUnknownSecurityModels and snmpInvalidMsgs.
166    MpdStats,
167}
168
169/// Registered handler with its OID prefix.
170pub(crate) struct RegisteredHandler {
171    pub(crate) prefix: Oid,
172    pub(crate) handler: Arc<dyn MibHandler>,
173}
174
175/// Builder for [`Agent`].
176///
177/// Use this builder to configure and construct an SNMP agent. The builder
178/// pattern allows you to chain configuration methods before calling
179/// [`build()`](AgentBuilder::build) to create the agent.
180///
181/// # Access Control
182///
183/// By default, the agent operates in **permissive mode**: any authenticated
184/// request (valid community string for v1/v2c, valid USM credentials for v3)
185/// has full read and write access to all registered handlers.
186///
187/// For production deployments, use the [`vacm()`](AgentBuilder::vacm) method
188/// to configure View-based Access Control (RFC 3415), which allows fine-grained
189/// control over which security names can access which OID subtrees.
190///
191/// # Minimal Example
192///
193/// ```rust,no_run
194/// use async_snmp::agent::Agent;
195/// use async_snmp::handler::{MibHandler, RequestContext, GetResult, GetNextResult, HandlerResult, BoxFuture};
196/// use async_snmp::{Oid, Value, VarBind, oid};
197/// use std::sync::Arc;
198///
199/// struct MyHandler;
200/// impl MibHandler for MyHandler {
201///     fn get<'a>(&'a self, _: &'a RequestContext, _: &'a Oid) -> BoxFuture<'a, HandlerResult<GetResult>> {
202///         Box::pin(async { Ok(GetResult::NoSuchObject) })
203///     }
204///     fn get_next<'a>(&'a self, _: &'a RequestContext, _: &'a Oid) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
205///         Box::pin(async { Ok(GetNextResult::EndOfMibView) })
206///     }
207/// }
208///
209/// # async fn example() -> Result<(), Box<async_snmp::Error>> {
210/// let agent = Agent::builder()
211///     .bind("0.0.0.0:1161")  // Use non-privileged port
212///     .community(b"public")
213///     .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(MyHandler))
214///     .build()
215///     .await?;
216/// # Ok(())
217/// # }
218/// ```
219pub struct AgentBuilder {
220    bind_addr: String,
221    communities: Vec<Vec<u8>>,
222    usm_users: HashMap<Bytes, UsmConfig>,
223    handlers: Vec<RegisteredHandler>,
224    engine_id: Option<Vec<u8>>,
225    engine_boots: u32,
226    max_message_size: usize,
227    max_concurrent_requests: Option<usize>,
228    recv_buffer_size: Option<usize>,
229    vacm: Option<VacmConfig>,
230    cancel: Option<CancellationToken>,
231    trap_sinks: Vec<(String, crate::client::Auth)>,
232    inform_timeout: Duration,
233    inform_retry: crate::client::Retry,
234    disabled_builtins: HashSet<BuiltinMib>,
235}
236
237impl AgentBuilder {
238    /// Create a new builder with default settings.
239    ///
240    /// Defaults:
241    /// - Bind address: `0.0.0.0:161` (UDP)
242    /// - Max message size: 1472 bytes (Ethernet MTU - IP/UDP headers)
243    /// - Max concurrent requests: 1000
244    /// - Receive buffer size: 4MB (requested from kernel)
245    /// - No communities or USM users (all requests rejected)
246    /// - No handlers registered
247    #[must_use]
248    pub fn new() -> Self {
249        Self {
250            bind_addr: "0.0.0.0:161".to_string(),
251            communities: Vec::new(),
252            usm_users: HashMap::new(),
253            handlers: Vec::new(),
254            engine_id: None,
255            engine_boots: 1,
256            max_message_size: DEFAULT_MAX_MESSAGE_SIZE,
257            max_concurrent_requests: Some(1000),
258            recv_buffer_size: Some(4 * 1024 * 1024), // 4MB
259            vacm: None,
260            cancel: None,
261            trap_sinks: Vec::new(),
262            inform_timeout: Duration::from_secs(5),
263            inform_retry: crate::client::Retry::default(),
264            disabled_builtins: HashSet::new(),
265        }
266    }
267
268    /// Set the UDP bind address.
269    ///
270    /// Default is `0.0.0.0:161` (standard SNMP agent port). Note that binding
271    /// to UDP port 161 typically requires root/administrator privileges.
272    ///
273    /// # IPv4 Examples
274    ///
275    /// ```rust,no_run
276    /// use async_snmp::agent::Agent;
277    ///
278    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
279    /// // Bind to all IPv4 interfaces on standard port (requires privileges)
280    /// let agent = Agent::builder().bind("0.0.0.0:161").community(b"public").build().await?;
281    ///
282    /// // Bind to localhost only on non-privileged port
283    /// let agent = Agent::builder().bind("127.0.0.1:1161").community(b"public").build().await?;
284    ///
285    /// // Bind to specific interface
286    /// let agent = Agent::builder().bind("192.168.1.100:161").community(b"public").build().await?;
287    /// # Ok(())
288    /// # }
289    /// ```
290    ///
291    /// # IPv6 / Dual-Stack Examples
292    ///
293    /// ```rust,no_run
294    /// use async_snmp::agent::Agent;
295    ///
296    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
297    /// // Bind to all interfaces (IPv6, with dual-stack on Linux)
298    /// let agent = Agent::builder().bind("[::]:161").community(b"public").build().await?;
299    ///
300    /// // Bind to IPv6 localhost only
301    /// let agent = Agent::builder().bind("[::1]:1161").community(b"public").build().await?;
302    /// # Ok(())
303    /// # }
304    /// ```
305    #[must_use]
306    pub fn bind(mut self, addr: impl Into<String>) -> Self {
307        self.bind_addr = addr.into();
308        self
309    }
310
311    /// Add an accepted community string for v1/v2c requests.
312    ///
313    /// Multiple communities can be added. If none are added,
314    /// all v1/v2c requests are rejected.
315    ///
316    /// # Example
317    ///
318    /// ```rust,no_run
319    /// use async_snmp::agent::Agent;
320    ///
321    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
322    /// let agent = Agent::builder()
323    ///     .bind("0.0.0.0:1161")
324    ///     .community(b"public")   // Read-only access
325    ///     .community(b"private")  // Read-write access (with VACM)
326    ///     .build()
327    ///     .await?;
328    /// # Ok(())
329    /// # }
330    /// ```
331    #[must_use]
332    pub fn community(mut self, community: &[u8]) -> Self {
333        self.communities.push(community.to_vec());
334        self
335    }
336
337    /// Add multiple community strings.
338    ///
339    /// # Example
340    ///
341    /// ```rust,no_run
342    /// use async_snmp::agent::Agent;
343    ///
344    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
345    /// let communities = ["public", "private", "monitor"];
346    /// let agent = Agent::builder()
347    ///     .bind("0.0.0.0:1161")
348    ///     .communities(communities)
349    ///     .build()
350    ///     .await?;
351    /// # Ok(())
352    /// # }
353    /// ```
354    #[must_use]
355    pub fn communities<I, C>(mut self, communities: I) -> Self
356    where
357        I: IntoIterator<Item = C>,
358        C: AsRef<[u8]>,
359    {
360        for c in communities {
361            self.communities.push(c.as_ref().to_vec());
362        }
363        self
364    }
365
366    /// Add a USM user for `SNMPv3` authentication.
367    ///
368    /// Configure authentication and privacy settings using the closure.
369    /// Multiple users can be added with different security levels.
370    ///
371    /// # Security Levels
372    ///
373    /// - **noAuthNoPriv**: No authentication or encryption
374    /// - **authNoPriv**: Authentication only (HMAC verification)
375    /// - **authPriv**: Authentication and encryption
376    ///
377    /// # Example
378    ///
379    /// ```rust,no_run
380    /// use async_snmp::agent::Agent;
381    /// use async_snmp::{AuthProtocol, PrivProtocol};
382    ///
383    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
384    /// let agent = Agent::builder()
385    ///     .bind("0.0.0.0:1161")
386    ///     // Read-only user with authentication only
387    ///     .usm_user("monitor", |u| {
388    ///         u.auth(AuthProtocol::Sha256, b"monitorpass123")
389    ///     })
390    ///     // Admin user with full encryption
391    ///     .usm_user("admin", |u| {
392    ///         u.auth(AuthProtocol::Sha256, b"adminauth123")
393    ///          .privacy(PrivProtocol::Aes128, b"adminpriv123")
394    ///     })
395    ///     .build()
396    ///     .await?;
397    /// # Ok(())
398    /// # }
399    /// ```
400    #[must_use]
401    pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
402    where
403        F: FnOnce(UsmConfig) -> UsmConfig,
404    {
405        let username_bytes: Bytes = username.into();
406        let config = configure(UsmConfig::new(username_bytes.clone()));
407        self.usm_users.insert(username_bytes, config);
408        self
409    }
410
411    /// Set the engine ID for `SNMPv3`.
412    ///
413    /// If not set, a default engine ID will be generated based on the
414    /// RFC 3411 format using enterprise number and timestamp.
415    ///
416    /// # Example
417    ///
418    /// ```rust,no_run
419    /// use async_snmp::agent::Agent;
420    ///
421    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
422    /// let agent = Agent::builder()
423    ///     .bind("0.0.0.0:1161")
424    ///     .engine_id(b"\x80\x00\x00\x00\x01MyEngine".to_vec())
425    ///     .community(b"public")
426    ///     .build()
427    ///     .await?;
428    /// # Ok(())
429    /// # }
430    /// ```
431    #[must_use]
432    pub fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
433        self.engine_id = Some(engine_id.into());
434        self
435    }
436
437    /// Set the initial engine boots value.
438    ///
439    /// Per RFC 3414 Section 2.3, snmpEngineBoots must be monotonically
440    /// increasing across restarts. The application is responsible for
441    /// persisting and restoring this value. If not set, defaults to 1.
442    ///
443    /// # Example
444    ///
445    /// ```rust,no_run
446    /// use async_snmp::agent::Agent;
447    ///
448    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
449    /// // Load persisted value (e.g. from file or database)
450    /// let persisted_boots: u32 = 42;
451    ///
452    /// let agent = Agent::builder()
453    ///     .bind("0.0.0.0:1161")
454    ///     .engine_boots(persisted_boots)
455    ///     .community(b"public")
456    ///     .build()
457    ///     .await?;
458    /// # Ok(())
459    /// # }
460    /// ```
461    #[must_use]
462    pub fn engine_boots(mut self, boots: u32) -> Self {
463        self.engine_boots = boots;
464        self
465    }
466
467    /// Set the maximum message size for responses.
468    ///
469    /// Default is 1472 octets (fits Ethernet MTU minus IP/UDP headers).
470    /// GETBULK responses will be truncated to fit within this limit.
471    ///
472    /// For `SNMPv3` requests, the agent uses the minimum of this value
473    /// and the msgMaxSize from the request.
474    #[must_use]
475    pub fn max_message_size(mut self, size: usize) -> Self {
476        self.max_message_size = size;
477        self
478    }
479
480    /// Set the maximum number of concurrent requests the agent will process.
481    ///
482    /// Default is 1000. Requests beyond this limit will queue until a slot
483    /// becomes available. Set to `None` for unbounded concurrency.
484    ///
485    /// This controls memory usage under high load while still allowing
486    /// parallel request processing.
487    ///
488    /// A limit of `Some(0)` is invalid (it would permit no requests and wedge
489    /// the agent) and is rejected by [`AgentBuilder::build`].
490    #[must_use]
491    pub fn max_concurrent_requests(mut self, limit: Option<usize>) -> Self {
492        self.max_concurrent_requests = limit;
493        self
494    }
495
496    /// Set the UDP socket receive buffer size.
497    ///
498    /// Default is 4MB. The kernel may cap this at `net.core.rmem_max`.
499    /// A larger buffer prevents packet loss during request bursts.
500    ///
501    /// Set to `None` to use the kernel default.
502    #[must_use]
503    pub fn recv_buffer_size(mut self, size: Option<usize>) -> Self {
504        self.recv_buffer_size = size;
505        self
506    }
507
508    /// Register a MIB handler for an OID subtree.
509    ///
510    /// Handlers are matched by longest prefix. When a request comes in,
511    /// the handler with the longest matching prefix is used.
512    ///
513    /// # Example
514    ///
515    /// ```rust,no_run
516    /// use async_snmp::agent::Agent;
517    /// use async_snmp::handler::{MibHandler, RequestContext, GetResult, GetNextResult, HandlerResult, BoxFuture};
518    /// use async_snmp::{Oid, Value, VarBind, oid};
519    /// use std::sync::Arc;
520    ///
521    /// struct SystemHandler;
522    /// impl MibHandler for SystemHandler {
523    ///     fn get<'a>(&'a self, _: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, HandlerResult<GetResult>> {
524    ///         Box::pin(async move {
525    ///             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {
526    ///                 Ok(GetResult::Value(Value::OctetString("My Agent".into())))
527    ///             } else {
528    ///                 Ok(GetResult::NoSuchObject)
529    ///             }
530    ///         })
531    ///     }
532    ///     fn get_next<'a>(&'a self, _: &'a RequestContext, _: &'a Oid) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
533    ///         Box::pin(async { Ok(GetNextResult::EndOfMibView) })
534    ///     }
535    /// }
536    ///
537    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
538    /// let agent = Agent::builder()
539    ///     .bind("0.0.0.0:1161")
540    ///     .community(b"public")
541    ///     // Register handler for system MIB subtree
542    ///     .handler(oid!(1, 3, 6, 1, 2, 1, 1), Arc::new(SystemHandler))
543    ///     .build()
544    ///     .await?;
545    /// # Ok(())
546    /// # }
547    /// ```
548    #[must_use]
549    pub fn handler(mut self, prefix: Oid, handler: Arc<dyn MibHandler>) -> Self {
550        self.handlers.push(RegisteredHandler { prefix, handler });
551        self
552    }
553
554    /// Configure VACM (View-based Access Control Model) using a builder function.
555    ///
556    /// When VACM is configured, all requests are checked against the configured
557    /// access control rules. Requests that don't have proper access are rejected
558    /// with `noAccess` error (v2c/v3) or `noSuchName` (v1).
559    ///
560    /// **Without VACM configuration, the agent operates in permissive mode**:
561    /// any authenticated request has full read/write access to all handlers.
562    ///
563    /// # Example
564    ///
565    /// ```rust,no_run
566    /// use async_snmp::agent::{Agent, SecurityModel, VacmBuilder};
567    /// use async_snmp::message::SecurityLevel;
568    /// use async_snmp::oid;
569    ///
570    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
571    /// let agent = Agent::builder()
572    ///     .bind("0.0.0.0:161")
573    ///     .community(b"public")
574    ///     .community(b"private")
575    ///     .vacm(|v| v
576    ///         .group("public", SecurityModel::V2c, "readonly_group")
577    ///         .group("private", SecurityModel::V2c, "readwrite_group")
578    ///         .access("readonly_group", |a| a
579    ///             .read_view("full_view"))
580    ///         .access("readwrite_group", |a| a
581    ///             .read_view("full_view")
582    ///             .write_view("write_view"))
583    ///         .view("full_view", |v| v
584    ///             .include(oid!(1, 3, 6, 1)))
585    ///         .view("write_view", |v| v
586    ///             .include(oid!(1, 3, 6, 1, 2, 1, 1))))
587    ///     .build()
588    ///     .await?;
589    /// # Ok(())
590    /// # }
591    /// ```
592    #[must_use]
593    pub fn vacm<F>(mut self, configure: F) -> Self
594    where
595        F: FnOnce(VacmBuilder) -> VacmBuilder,
596    {
597        let builder = VacmBuilder::new();
598        self.vacm = Some(configure(builder).build());
599        self
600    }
601
602    /// Set a cancellation token for graceful shutdown.
603    ///
604    /// If not set, the agent creates its own token accessible via `Agent::cancel()`.
605    #[must_use]
606    pub fn cancel(mut self, token: CancellationToken) -> Self {
607        self.cancel = Some(token);
608        self
609    }
610
611    /// Add a trap/inform destination.
612    ///
613    /// The agent will send notifications to all configured trap sinks when
614    /// [`Agent::send_trap()`] or [`Agent::send_inform()`] is called.
615    ///
616    /// # Example
617    ///
618    /// ```rust,no_run
619    /// use async_snmp::agent::Agent;
620    /// use async_snmp::{Auth, AuthProtocol, PrivProtocol};
621    ///
622    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
623    /// let agent = Agent::builder()
624    ///     .bind("0.0.0.0:1161")
625    ///     .community(b"public")
626    ///     .trap_sink("192.168.1.100:162", Auth::v2c("public"))
627    ///     .trap_sink("10.0.0.1:162", Auth::usm("trapuser")
628    ///         .auth(AuthProtocol::Sha256, "authpass")
629    ///         .privacy(PrivProtocol::Aes128, "privpass"))
630    ///     .build()
631    ///     .await?;
632    /// # Ok(())
633    /// # }
634    /// ```
635    #[must_use]
636    pub fn trap_sink(
637        mut self,
638        dest: impl Into<String>,
639        auth: impl Into<crate::client::Auth>,
640    ) -> Self {
641        self.trap_sinks.push((dest.into(), auth.into()));
642        self
643    }
644
645    /// Set the timeout for inform requests sent to trap sinks.
646    ///
647    /// Default is 5 seconds. Only affects `send_inform`, not `send_trap`.
648    #[must_use]
649    pub fn inform_timeout(mut self, timeout: Duration) -> Self {
650        self.inform_timeout = timeout;
651        self
652    }
653
654    /// Set the retry policy for inform requests sent to trap sinks.
655    ///
656    /// Default is `Retry::default()` (3 retries with 1-second delay).
657    /// Only affects `send_inform`, not `send_trap`.
658    #[must_use]
659    pub fn inform_retry(mut self, retry: crate::client::Retry) -> Self {
660        self.inform_retry = retry;
661        self
662    }
663
664    /// Disable a specific built-in MIB handler group.
665    ///
666    /// By default, the agent registers handlers for snmpEngine, USM stats,
667    /// and MPD stats. Call this to prevent registration of a specific group,
668    /// e.g., if you want to provide your own handler for those OIDs.
669    #[must_use]
670    pub fn without_builtin_handler(mut self, mib: BuiltinMib) -> Self {
671        self.disabled_builtins.insert(mib);
672        self
673    }
674
675    /// Disable all built-in MIB handlers.
676    ///
677    /// The agent will not register any internal handlers for snmpEngine,
678    /// USM stats, or MPD stats. You can still query the counter values
679    /// via accessor methods like [`Agent::usm_unknown_engine_ids()`].
680    #[must_use]
681    pub fn without_builtin_handlers(mut self) -> Self {
682        self.disabled_builtins.insert(BuiltinMib::SnmpEngine);
683        self.disabled_builtins.insert(BuiltinMib::UsmStats);
684        self.disabled_builtins.insert(BuiltinMib::MpdStats);
685        self
686    }
687
688    /// Build the agent.
689    pub async fn build(mut self) -> Result<Agent> {
690        // Reject any USM user configured with privacy but no authentication,
691        // and precompute master keys so the expensive password expansion runs
692        // once here instead of on every inbound packet (CPU amplification).
693        for config in self.usm_users.values_mut() {
694            config.validate()?;
695            config.precompute_master_keys();
696        }
697
698        let bind_addr: std::net::SocketAddr = self.bind_addr.parse().map_err(|_| {
699            Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
700        })?;
701
702        let socket = bind_udp_socket(bind_addr, self.recv_buffer_size, None, false)
703            .await
704            .map_err(|e| Error::Network {
705                target: bind_addr,
706                source: e,
707            })?;
708
709        let local_addr = socket.local_addr().map_err(|e| Error::Network {
710            target: bind_addr,
711            source: e,
712        })?;
713
714        let socket_state =
715            UdpSocketState::new(UdpSockRef::from(&socket)).map_err(|e| Error::Network {
716                target: bind_addr,
717                source: e,
718            })?;
719
720        // Validate a user-supplied engine ID, or generate a valid random one.
721        let engine_id: Bytes = match self.engine_id {
722            Some(id) => {
723                crate::v3::validate_engine_id(&id)?;
724                Bytes::from(id)
725            }
726            None => crate::v3::generate_engine_id(),
727        };
728
729        let cancel = self.cancel.unwrap_or_default();
730
731        // Create concurrency limiter if configured. A zero-permit semaphore
732        // would never grant a permit and wedge the agent, so reject it.
733        if self.max_concurrent_requests == Some(0) {
734            return Err(
735                Error::Config("max_concurrent_requests must be greater than 0".into()).into(),
736            );
737        }
738        let concurrency_limit = self
739            .max_concurrent_requests
740            .map(|n| Arc::new(Semaphore::new(n)));
741
742        // Resolve trap sink addresses
743        let mut trap_sinks = Vec::with_capacity(self.trap_sinks.len());
744        for (dest_str, auth) in self.trap_sinks {
745            let dest: SocketAddr = dest_str.parse().map_err(|_| {
746                Error::Config(format!("invalid trap sink address: {dest_str}").into())
747            })?;
748            trap_sinks.push(notification::TrapSink::new(
749                dest,
750                auth,
751                self.inform_timeout,
752                self.inform_retry.clone(),
753            ));
754        }
755
756        let state = Arc::new(AgentState {
757            engine_id,
758            engine_boots: AtomicU32::new(self.engine_boots),
759            engine_time: AtomicU32::new(0),
760            engine_start: Instant::now(),
761            engine_boots_base: self.engine_boots,
762            max_message_size: self.max_message_size,
763            snmp_invalid_msgs: AtomicU32::new(0),
764            snmp_unknown_security_models: AtomicU32::new(0),
765            snmp_silent_drops: AtomicU32::new(0),
766            snmp_unknown_contexts: AtomicU32::new(0),
767            usm_stats: UsmStats::default(),
768        });
769
770        // Register built-in handlers for any not disabled
771        if !self.disabled_builtins.contains(&BuiltinMib::SnmpEngine) {
772            self.handlers.push(RegisteredHandler {
773                prefix: oid!(1, 3, 6, 1, 6, 3, 10, 2, 1),
774                handler: Arc::new(builtins::SnmpEngineHandler {
775                    state: Arc::clone(&state),
776                }),
777            });
778        }
779        if !self.disabled_builtins.contains(&BuiltinMib::UsmStats) {
780            self.handlers.push(RegisteredHandler {
781                prefix: oid!(1, 3, 6, 1, 6, 3, 15, 1, 1),
782                handler: Arc::new(builtins::UsmStatsHandler {
783                    state: Arc::clone(&state),
784                }),
785            });
786        }
787        if !self.disabled_builtins.contains(&BuiltinMib::MpdStats) {
788            self.handlers.push(RegisteredHandler {
789                prefix: oid!(1, 3, 6, 1, 6, 3, 11, 2, 1),
790                handler: Arc::new(builtins::MpdStatsHandler {
791                    state: Arc::clone(&state),
792                }),
793            });
794        }
795
796        // Sort handlers by prefix length (longest first) for matching
797        self.handlers
798            .sort_by_key(|h| std::cmp::Reverse(h.prefix.len()));
799
800        Ok(Agent {
801            inner: Arc::new(AgentInner {
802                socket: Arc::new(socket),
803                socket_state,
804                local_addr,
805                communities: self.communities,
806                usm_users: self.usm_users,
807                handlers: self.handlers,
808                state,
809                salt_counter: SaltCounter::new(),
810                concurrency_limit,
811                vacm: self.vacm,
812                cancel,
813                trap_sinks,
814                notification_id: std::sync::atomic::AtomicI32::new(1),
815            }),
816        })
817    }
818}
819
820impl Default for AgentBuilder {
821    fn default() -> Self {
822        Self::new()
823    }
824}
825
826/// Engine state and counters shared across agent clones and (future) built-in handlers.
827pub(crate) struct AgentState {
828    pub(crate) engine_id: Bytes,
829    pub(crate) engine_boots: AtomicU32,
830    pub(crate) engine_time: AtomicU32,
831    pub(crate) engine_start: Instant,
832    /// Initial `engine_boots` value at startup, used to compute overflow-adjusted boots.
833    pub(crate) engine_boots_base: u32,
834    pub(crate) max_message_size: usize,
835    // RFC 3412 statistics counters
836    /// snmpInvalidMsgs (1.3.6.1.6.3.11.2.1.2) - messages with invalid msgFlags
837    /// (e.g., privacy without authentication)
838    pub(crate) snmp_invalid_msgs: AtomicU32,
839    /// snmpUnknownSecurityModels (1.3.6.1.6.3.11.2.1.1) - messages with
840    /// unrecognized security model
841    pub(crate) snmp_unknown_security_models: AtomicU32,
842    /// snmpSilentDrops (1.3.6.1.6.3.11.2.1.3) - confirmed-class PDUs silently
843    /// dropped because even an empty response would exceed max message size
844    pub(crate) snmp_silent_drops: AtomicU32,
845    /// snmpUnknownContexts (1.3.6.1.6.3.12.1.5) - requests whose scopedPDU
846    /// contextEngineID did not name a context served by this engine
847    pub(crate) snmp_unknown_contexts: AtomicU32,
848    /// RFC 3414 usmStats counters
849    pub(crate) usm_stats: UsmStats,
850}
851
852/// Inner state shared across agent clones.
853pub(crate) struct AgentInner {
854    pub(crate) socket: Arc<UdpSocket>,
855    pub(crate) socket_state: UdpSocketState,
856    pub(crate) local_addr: SocketAddr,
857    pub(crate) communities: Vec<Vec<u8>>,
858    pub(crate) usm_users: HashMap<Bytes, UsmConfig>,
859    pub(crate) handlers: Vec<RegisteredHandler>,
860    pub(crate) state: Arc<AgentState>,
861    pub(crate) salt_counter: SaltCounter,
862    pub(crate) concurrency_limit: Option<Arc<Semaphore>>,
863    pub(crate) vacm: Option<VacmConfig>,
864    /// Cancellation token for graceful shutdown.
865    pub(crate) cancel: CancellationToken,
866    /// Configured trap/inform destinations.
867    pub(crate) trap_sinks: Vec<notification::TrapSink>,
868    /// Per-agent monotonic counter for trap request-ids and v3 notification msgIDs.
869    pub(crate) notification_id: std::sync::atomic::AtomicI32,
870}
871
872/// SNMP Agent.
873///
874/// Listens for and responds to SNMP requests (GET, GETNEXT, GETBULK, SET).
875///
876/// # Example
877///
878/// ```rust,no_run
879/// use async_snmp::agent::Agent;
880/// use async_snmp::oid;
881///
882/// # async fn example() -> Result<(), Box<async_snmp::Error>> {
883/// let agent = Agent::builder()
884///     .bind("0.0.0.0:161")
885///     .community(b"public")
886///     .build()
887///     .await?;
888///
889/// agent.run().await
890/// # }
891/// ```
892pub struct Agent {
893    pub(crate) inner: Arc<AgentInner>,
894}
895
896impl Agent {
897    /// Create a builder for configuring the agent.
898    #[must_use]
899    pub fn builder() -> AgentBuilder {
900        AgentBuilder::new()
901    }
902
903    /// Get the local address the agent is bound to.
904    #[must_use]
905    pub fn local_addr(&self) -> SocketAddr {
906        self.inner.local_addr
907    }
908
909    /// Get the engine ID.
910    #[must_use]
911    pub fn engine_id(&self) -> &[u8] {
912        &self.inner.state.engine_id
913    }
914
915    /// Get the current engine boots value.
916    ///
917    /// Useful for persisting across restarts per RFC 3414 Section 2.3.
918    /// The persisted value should be passed to `AgentBuilder::engine_boots()`
919    /// on the next startup.
920    #[must_use]
921    pub fn engine_boots(&self) -> u32 {
922        self.inner.state.engine_boots.load(Ordering::Relaxed)
923    }
924
925    /// Get the current engine time value.
926    #[must_use]
927    pub fn engine_time(&self) -> u32 {
928        self.inner.state.engine_time.load(Ordering::Relaxed)
929    }
930
931    /// Get the cancellation token for this agent.
932    ///
933    /// Call `token.cancel()` to initiate graceful shutdown.
934    #[must_use]
935    pub fn cancel(&self) -> CancellationToken {
936        self.inner.cancel.clone()
937    }
938
939    /// Get the snmpInvalidMsgs counter value.
940    ///
941    /// This counter tracks messages with invalid msgFlags, such as
942    /// privacy-without-authentication (RFC 3412 Section 7.2 Step 5d).
943    ///
944    /// OID: 1.3.6.1.6.3.11.2.1.2
945    #[must_use]
946    pub fn snmp_invalid_msgs(&self) -> u32 {
947        self.inner.state.snmp_invalid_msgs.load(Ordering::Relaxed)
948    }
949
950    /// Get the snmpUnknownSecurityModels counter value.
951    ///
952    /// This counter tracks messages with unrecognized security models
953    /// (RFC 3412 Section 7.2 Step 2).
954    ///
955    /// OID: 1.3.6.1.6.3.11.2.1.1
956    #[must_use]
957    pub fn snmp_unknown_security_models(&self) -> u32 {
958        self.inner
959            .state
960            .snmp_unknown_security_models
961            .load(Ordering::Relaxed)
962    }
963
964    /// Get the snmpSilentDrops counter value.
965    ///
966    /// This counter tracks confirmed-class PDUs (`GetRequest`, `GetNextRequest`,
967    /// `GetBulkRequest`, `SetRequest`, `InformRequest`) that were silently dropped
968    /// because even an empty Response-PDU would exceed the maximum message
969    /// size constraint (RFC 3412 Section 7.1).
970    ///
971    /// OID: 1.3.6.1.6.3.11.2.1.3
972    #[must_use]
973    pub fn snmp_silent_drops(&self) -> u32 {
974        self.inner.state.snmp_silent_drops.load(Ordering::Relaxed)
975    }
976
977    /// Get the snmpUnknownContexts counter value.
978    ///
979    /// This counter tracks requests whose scopedPDU contextEngineID did not
980    /// name a context served by this engine (RFC 3413 Section 3.2). Such
981    /// requests are answered with a Report PDU rather than dispatched against
982    /// the local MIB.
983    ///
984    /// OID: 1.3.6.1.6.3.12.1.5
985    #[must_use]
986    pub fn snmp_unknown_contexts(&self) -> u32 {
987        self.inner
988            .state
989            .snmp_unknown_contexts
990            .load(Ordering::Relaxed)
991    }
992
993    /// Get the usmStatsUnknownEngineIDs counter value.
994    ///
995    /// This counter tracks messages with unknown engine IDs.
996    /// Incremented when a non-discovery request arrives with an engine ID that
997    /// does not match the local engine (RFC 3414 Section 3.2 Step 3).
998    ///
999    /// OID: 1.3.6.1.6.3.15.1.1.4
1000    #[must_use]
1001    pub fn usm_unknown_engine_ids(&self) -> u32 {
1002        self.inner
1003            .state
1004            .usm_stats
1005            .unknown_engine_ids
1006            .load(Ordering::Relaxed)
1007    }
1008
1009    /// Get the usmStatsUnknownUserNames counter value.
1010    ///
1011    /// This counter tracks messages with unknown user names.
1012    /// Incremented when a message arrives with a user name not in the local
1013    /// user database (RFC 3414 Section 3.2 Step 1).
1014    ///
1015    /// OID: 1.3.6.1.6.3.15.1.1.3
1016    #[must_use]
1017    pub fn usm_unknown_usernames(&self) -> u32 {
1018        self.inner
1019            .state
1020            .usm_stats
1021            .unknown_usernames
1022            .load(Ordering::Relaxed)
1023    }
1024
1025    /// Get the usmStatsWrongDigests counter value.
1026    ///
1027    /// This counter tracks messages with incorrect authentication digests.
1028    /// (RFC 3414 Section 3.2 Step 6).
1029    ///
1030    /// OID: 1.3.6.1.6.3.15.1.1.5
1031    #[must_use]
1032    pub fn usm_wrong_digests(&self) -> u32 {
1033        self.inner
1034            .state
1035            .usm_stats
1036            .wrong_digests
1037            .load(Ordering::Relaxed)
1038    }
1039
1040    /// Get the usmStatsNotInTimeWindows counter value.
1041    ///
1042    /// This counter tracks messages requesting an authenticated security
1043    /// level that fail the time window check (RFC 3414 Section 3.2 Step 7a):
1044    /// engine boots mismatch, boots latched at the maximum (checked before
1045    /// digest verification), or message time differing from the local time
1046    /// by more than 150 seconds.
1047    ///
1048    /// OID: 1.3.6.1.6.3.15.1.1.2
1049    #[must_use]
1050    pub fn usm_not_in_time_windows(&self) -> u32 {
1051        self.inner
1052            .state
1053            .usm_stats
1054            .not_in_time_windows
1055            .load(Ordering::Relaxed)
1056    }
1057
1058    /// Get the usmStatsUnsupportedSecLevels counter value.
1059    ///
1060    /// This counter tracks messages where the user does not support
1061    /// the requested security level (e.g., auth required but user
1062    /// has no auth key configured). RFC 3414 Section 3.2.
1063    ///
1064    /// OID: 1.3.6.1.6.3.15.1.1.1
1065    #[must_use]
1066    pub fn usm_unsupported_sec_levels(&self) -> u32 {
1067        self.inner
1068            .state
1069            .usm_stats
1070            .unsupported_sec_levels
1071            .load(Ordering::Relaxed)
1072    }
1073
1074    /// Get the usmStatsDecryptionErrors counter value.
1075    ///
1076    /// This counter tracks messages where decryption failed (the user
1077    /// has a privacy key but the decrypt operation returned an error).
1078    /// RFC 3414 Section 3.2.
1079    ///
1080    /// OID: 1.3.6.1.6.3.15.1.1.6
1081    #[must_use]
1082    pub fn usm_decryption_errors(&self) -> u32 {
1083        self.inner
1084            .state
1085            .usm_stats
1086            .decryption_errors
1087            .load(Ordering::Relaxed)
1088    }
1089
1090    /// Returns agent uptime in hundredths of a second (centiseconds).
1091    ///
1092    /// Use this in your system MIB handler to provide sysUpTime.0
1093    /// (1.3.6.1.2.1.1.3.0) as a `Value::TimeTicks` value.
1094    #[must_use]
1095    pub fn uptime_hundredths(&self) -> u32 {
1096        let elapsed = self.inner.state.engine_start.elapsed();
1097        let centisecs = elapsed.as_millis() / 10;
1098        centisecs.min(u128::from(u32::MAX)) as u32
1099    }
1100
1101    /// Run the agent, processing requests concurrently.
1102    ///
1103    /// Requests are processed in parallel up to the configured
1104    /// `max_concurrent_requests` limit (default: 1000). This method runs
1105    /// until the cancellation token is triggered.
1106    #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
1107    pub async fn run(&self) -> Result<()> {
1108        let mut buf = vec![0u8; 65535];
1109
1110        loop {
1111            let recv_meta = tokio::select! {
1112                result = self.recv_packet(&mut buf) => {
1113                    result?
1114                }
1115                () = self.inner.cancel.cancelled() => {
1116                    tracing::info!(target: "async_snmp::agent", "agent shutdown requested");
1117                    return Ok(());
1118                }
1119            };
1120
1121            let data = Bytes::copy_from_slice(&buf[..recv_meta.len]);
1122            let agent = self.clone();
1123
1124            let permit = if let Some(ref sem) = self.inner.concurrency_limit {
1125                tokio::select! {
1126                    result = sem.clone().acquire_owned() => {
1127                        Some(result.expect("semaphore closed"))
1128                    }
1129                    () = self.inner.cancel.cancelled() => {
1130                        tracing::info!(target: "async_snmp::agent", "agent shutdown requested");
1131                        return Ok(());
1132                    }
1133                }
1134            } else {
1135                None
1136            };
1137
1138            tokio::spawn(async move {
1139                agent.update_engine_time();
1140
1141                match agent.handle_request(data, recv_meta.addr).await {
1142                    Ok(Some(response_bytes)) => {
1143                        // Per RFC 3416 Section 4.2 the GET/GETNEXT/SET handlers
1144                        // already emit a tooBig Response when their result would
1145                        // not fit (and GETBULK Section 4.2.3 truncates or emits
1146                        // tooBig). This drop is the final fallback for when even
1147                        // that empty tooBig Response still exceeds the limit; the
1148                        // packet is then silently dropped (snmpSilentDrops).
1149                        if response_bytes.len() > agent.inner.state.max_message_size {
1150                            agent
1151                                .inner
1152                                .state
1153                                .snmp_silent_drops
1154                                .fetch_add(1, Ordering::Relaxed);
1155                            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");
1156                        } else if let Err(e) =
1157                            agent.send_response(&response_bytes, &recv_meta).await
1158                        {
1159                            tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "failed to send response");
1160                        }
1161                    }
1162                    Ok(None) => {}
1163                    Err(e) => {
1164                        tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "error handling request");
1165                    }
1166                }
1167
1168                drop(permit);
1169            });
1170        }
1171    }
1172
1173    async fn recv_packet(&self, buf: &mut [u8]) -> Result<RecvMeta> {
1174        let mut iov = [IoSliceMut::new(buf)];
1175        let mut meta = [RecvMeta::default()];
1176
1177        loop {
1178            self.inner
1179                .socket
1180                .readable()
1181                .await
1182                .map_err(|e| Error::Network {
1183                    target: self.inner.local_addr,
1184                    source: e,
1185                })?;
1186
1187            let result = self.inner.socket.try_io(tokio::io::Interest::READABLE, || {
1188                let sref = UdpSockRef::from(&*self.inner.socket);
1189                self.inner.socket_state.recv(sref, &mut iov, &mut meta)
1190            });
1191
1192            match result {
1193                Ok(n) if n > 0 => return Ok(meta[0]),
1194                Ok(_) => { /* fall thru to next `loop {}` iteration */ }
1195                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { /* fall thru to next `loop {}` iteration */
1196                }
1197                Err(e) => {
1198                    return Err(Error::Network {
1199                        target: self.inner.local_addr,
1200                        source: e,
1201                    }
1202                    .boxed());
1203                }
1204            }
1205        }
1206    }
1207
1208    async fn send_response(&self, data: &[u8], recv_meta: &RecvMeta) -> std::io::Result<()> {
1209        let transmit = Transmit {
1210            destination: recv_meta.addr,
1211            ecn: None,
1212            contents: data,
1213            segment_size: None,
1214            src_ip: recv_meta.dst_ip,
1215        };
1216
1217        loop {
1218            self.inner.socket.writable().await?;
1219
1220            let result = self.inner.socket.try_io(tokio::io::Interest::WRITABLE, || {
1221                let sref = UdpSockRef::from(&*self.inner.socket);
1222                self.inner.socket_state.try_send(sref, &transmit)
1223            });
1224
1225            match result {
1226                Ok(()) => return Ok(()),
1227                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { /* fall thru to next `loop {}` iteration */
1228                }
1229                Err(e) => return Err(e),
1230            }
1231        }
1232    }
1233
1234    /// Process a single request and return the response bytes.
1235    ///
1236    /// Returns `None` if no response should be sent.
1237    async fn handle_request(&self, data: Bytes, source: SocketAddr) -> Result<Option<Bytes>> {
1238        match crate::message::peek_version(data.clone(), source)? {
1239            Version::V1 => self.handle_v1(data, source).await,
1240            Version::V2c => self.handle_v2c(data, source).await,
1241            Version::V3 => self.handle_v3(data, source).await,
1242        }
1243    }
1244
1245    /// Update engine boots and time based on elapsed time since start.
1246    ///
1247    /// Per RFC 3414 Section 2.3, when snmpEngineTime reaches `MAX_ENGINE_TIME`
1248    /// (2^31-1), snmpEngineBoots is incremented and snmpEngineTime resets to
1249    /// zero. The boots/time pair is derived from total elapsed seconds and
1250    /// the base boots value at startup, so no mutable state beyond the
1251    /// atomics is needed.
1252    fn update_engine_time(&self) {
1253        let total_secs = self.inner.state.engine_start.elapsed().as_secs();
1254        let (boots, time) =
1255            compute_engine_boots_time(self.inner.state.engine_boots_base, total_secs);
1256
1257        if boots != self.inner.state.engine_boots.load(Ordering::Relaxed)
1258            && boots > self.inner.state.engine_boots_base
1259        {
1260            tracing::warn!(
1261                target: "async_snmp::agent",
1262                engine_boots = boots,
1263                "engine time wrapped past MAX_ENGINE_TIME, incrementing engine boots"
1264            );
1265        }
1266
1267        self.inner
1268            .state
1269            .engine_boots
1270            .store(boots, Ordering::Relaxed);
1271        self.inner.state.engine_time.store(time, Ordering::Relaxed);
1272    }
1273
1274    /// Validate community string using constant-time comparison.
1275    ///
1276    /// Uses constant-time comparison to prevent timing attacks that could
1277    /// be used to guess valid community strings character by character.
1278    pub(crate) fn validate_community(&self, community: &[u8]) -> bool {
1279        if self.inner.communities.is_empty() {
1280            // No communities configured = reject all
1281            return false;
1282        }
1283        // Use constant-time comparison for each community string.
1284        // We compare against all configured communities regardless of
1285        // early matches to maintain constant-time behavior.
1286        let mut valid = false;
1287        for configured in &self.inner.communities {
1288            // ct_eq returns a Choice, which we convert to bool after comparison
1289            if configured.len() == community.len()
1290                && bool::from(configured.as_slice().ct_eq(community))
1291            {
1292                valid = true;
1293            }
1294        }
1295        valid
1296    }
1297
1298    /// Dispatch a request to the appropriate handler.
1299    async fn dispatch_request(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1300        match pdu.pdu_type {
1301            PduType::GetRequest => self.handle_get(ctx, pdu).await,
1302            PduType::GetNextRequest => self.handle_get_next(ctx, pdu).await,
1303            PduType::GetBulkRequest => {
1304                // SNMPv1 does not support GETBULK
1305                if ctx.version == Version::V1 {
1306                    return Ok(pdu.to_error_response(ErrorStatus::GenErr, 0));
1307                }
1308                self.handle_get_bulk(ctx, pdu).await
1309            }
1310            PduType::SetRequest => self.handle_set(ctx, pdu).await,
1311            PduType::InformRequest => self.handle_inform(ctx, pdu),
1312            _ => {
1313                // Should not happen - filtered earlier
1314                Ok(pdu.to_error_response(ErrorStatus::GenErr, 0))
1315            }
1316        }
1317    }
1318
1319    /// Handle `InformRequest` PDU.
1320    ///
1321    /// Per RFC 3416 Section 4.2.7, an `InformRequest` is a confirmed-class PDU
1322    /// that the receiver acknowledges by returning a Response with the same
1323    /// request-id and varbind list.
1324    #[allow(
1325        clippy::unnecessary_wraps,
1326        reason = "TODO store received informs, which may be a fallible operation"
1327    )]
1328    fn handle_inform(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1329        // Acknowledge by echoing the same varbinds in a Response.
1330        //
1331        // RFC 3416 Section 4.2.7: an InformRequest is a confirmed-class PDU. If
1332        // the echoed Response would exceed the message-size limit, return a
1333        // tooBig Response with an empty variable-bindings list rather than
1334        // letting the oversized Response be silently dropped. A confirmed-class
1335        // sender that never receives a fitting acknowledgement would otherwise
1336        // retry indefinitely.
1337        if !Self::response_fits(
1338            &pdu.varbinds,
1339            self.response_overhead(ctx),
1340            self.effective_max_size(ctx),
1341        ) {
1342            return Ok(Self::too_big_response(ctx.version, pdu));
1343        }
1344
1345        Ok(pdu.to_response())
1346    }
1347
1348    /// Effective maximum response message size for a request: the smaller of
1349    /// the agent's configured limit and the client's advertised `msgMaxSize`
1350    /// (v3). v1/v2c requests carry no `msg_max_size`, so the agent limit applies.
1351    fn effective_max_size(&self, ctx: &RequestContext) -> usize {
1352        let agent_max = self.inner.state.max_message_size;
1353        match ctx.msg_max_size {
1354            Some(client_max) => agent_max.min(client_max as usize),
1355            None => agent_max,
1356        }
1357    }
1358
1359    /// Upper-bound overhead (the non-varbind bytes) of the encoded Response for
1360    /// this request, used to budget how many varbinds fit within the size limit.
1361    ///
1362    /// For v1/v2c the fixed [`RESPONSE_OVERHEAD`] covers the community wrapper.
1363    /// The v3 USM/scopedPDU wrapper is materially larger and grows with the
1364    /// security level, so the v3 estimate adds the engine ID (carried twice, as
1365    /// the authoritative engine ID in the security parameters and the context
1366    /// engine ID in the scopedPDU), the user name, the context name, and the
1367    /// auth/priv material. The result is deliberately a conservative upper
1368    /// bound: a slight over-estimate only trims a varbind or two, whereas an
1369    /// under-estimate would let a Response exceed the client's msgMaxSize (sent
1370    /// anyway) or the agent limit (silently dropped) instead of returning
1371    /// tooBig.
1372    fn response_overhead(&self, ctx: &RequestContext) -> usize {
1373        if ctx.version != Version::V3 {
1374            // v1/v2c echo the request's community string in the response
1375            // wrapper. A long, operator-configured community can otherwise
1376            // push the encoded Response past the size limit after
1377            // response_fits has already accepted it.
1378            return RESPONSE_OVERHEAD + ctx.security_name.len();
1379        }
1380        let mut overhead = RESPONSE_OVERHEAD
1381            + 2 * self.inner.state.engine_id.len()
1382            + ctx.security_name.len()
1383            + ctx.context_name.len();
1384        if ctx.security_level.requires_auth() {
1385            overhead += V3_AUTH_OVERHEAD;
1386        }
1387        if ctx.security_level.requires_priv() {
1388            overhead += V3_PRIV_OVERHEAD;
1389        }
1390        overhead
1391    }
1392
1393    /// Estimate whether a Response carrying `varbinds` fits within `max_size`,
1394    /// using the same estimate as GETBULK: `overhead` (from
1395    /// [`Agent::response_overhead`]) plus the encoded size of each varbind.
1396    fn response_fits(varbinds: &[VarBind], overhead: usize, max_size: usize) -> bool {
1397        let size = overhead + varbinds.iter().map(VarBind::encoded_size).sum::<usize>();
1398        size <= max_size
1399    }
1400
1401    /// Build the `tooBig` Response for `pdu`: error-status `tooBig`, error-index
1402    /// zero, per RFC 3416 Section 4.2.
1403    ///
1404    /// RFC 3416 clears the variable-bindings field for v2c/v3. SNMPv1 predates
1405    /// that rule: RFC 1157 Sections 4.1.2-4.1.4 specify that on a tooBig error
1406    /// the Response echoes the original request's variable bindings unchanged,
1407    /// so v1 tooBig Responses carry the request varbinds.
1408    pub(super) fn too_big_response(version: Version, pdu: &Pdu) -> Pdu {
1409        let varbinds = if version == Version::V1 {
1410            pdu.varbinds.clone()
1411        } else {
1412            Vec::new()
1413        };
1414        Pdu {
1415            pdu_type: PduType::Response,
1416            request_id: pdu.request_id,
1417            error_status: ErrorStatus::TooBig.as_i32(),
1418            error_index: 0,
1419            varbinds,
1420        }
1421    }
1422
1423    /// Handle GET request.
1424    async fn handle_get(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1425        let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
1426
1427        for (index, vb) in pdu.varbinds.iter().enumerate() {
1428            // VACM read access check
1429            if let Some(ref vacm) = self.inner.vacm
1430                && !vacm.check_access(ctx.read_view.as_ref(), &vb.oid)
1431            {
1432                // v1: noSuchName, v2c/v3: noAccess or NoSuchObject
1433                if ctx.version == Version::V1 {
1434                    return Ok(pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32));
1435                }
1436                // For GET, return NoSuchObject for inaccessible OIDs per RFC 3415
1437                response_varbinds.push(VarBind::new(vb.oid.clone(), Value::NoSuchObject));
1438                continue;
1439            }
1440
1441            let result = if let Some(handler) = self.find_handler(&vb.oid) {
1442                match handler.handler.get(ctx, &vb.oid).await {
1443                    Ok(result) => result,
1444                    Err(err) => {
1445                        // RFC 3416 Section 4.2.1: a varbind whose processing
1446                        // fails yields a genErr Response naming its index.
1447                        tracing::warn!(
1448                            target: "async_snmp::agent",
1449                            oid = %vb.oid,
1450                            error = %err,
1451                            "handler GET failed; responding genErr"
1452                        );
1453                        return Ok(pdu.to_error_response(ErrorStatus::GenErr, (index + 1) as i32));
1454                    }
1455                }
1456            } else {
1457                GetResult::NoSuchObject
1458            };
1459
1460            let response_value = match result {
1461                GetResult::Value(v) => {
1462                    if v1_rejects_counter64(ctx.version, &v) {
1463                        return Ok(
1464                            pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1465                        );
1466                    }
1467                    v
1468                }
1469                GetResult::NoSuchObject => {
1470                    // v1 returns noSuchName error, v2c/v3 returns NoSuchObject exception
1471                    if ctx.version == Version::V1 {
1472                        return Ok(
1473                            pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1474                        );
1475                    }
1476                    Value::NoSuchObject
1477                }
1478                GetResult::NoSuchInstance => {
1479                    // v1 returns noSuchName error, v2c/v3 returns NoSuchInstance exception
1480                    if ctx.version == Version::V1 {
1481                        return Ok(
1482                            pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1483                        );
1484                    }
1485                    Value::NoSuchInstance
1486                }
1487            };
1488
1489            response_varbinds.push(VarBind::new(vb.oid.clone(), response_value));
1490        }
1491
1492        // RFC 3416 Section 4.2.1: if the Response would exceed the message-size
1493        // limit, return a tooBig Response with an empty variable-bindings list.
1494        if !Self::response_fits(
1495            &response_varbinds,
1496            self.response_overhead(ctx),
1497            self.effective_max_size(ctx),
1498        ) {
1499            return Ok(Self::too_big_response(ctx.version, pdu));
1500        }
1501
1502        Ok(Pdu {
1503            pdu_type: PduType::Response,
1504            request_id: pdu.request_id,
1505            error_status: 0,
1506            error_index: 0,
1507            varbinds: response_varbinds,
1508        })
1509    }
1510
1511    /// Handle GETNEXT request.
1512    async fn handle_get_next(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1513        let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
1514
1515        for (index, vb) in pdu.varbinds.iter().enumerate() {
1516            // Try to find the next OID from any handler, skipping OIDs denied by
1517            // VACM. RFC 3413 classifies GETNEXT as Read-Class and requires
1518            // continuing the walk until an accessible OID is found.
1519            let next = match self.get_next_accessible_oid(ctx, &vb.oid).await {
1520                Ok(next) => next,
1521                Err(err) => {
1522                    tracing::warn!(
1523                        target: "async_snmp::agent",
1524                        oid = %vb.oid,
1525                        error = %err,
1526                        "handler GETNEXT failed; responding genErr"
1527                    );
1528                    return Ok(pdu.to_error_response(ErrorStatus::GenErr, (index + 1) as i32));
1529                }
1530            };
1531
1532            if let Some(next_vb) = next {
1533                response_varbinds.push(next_vb);
1534            } else {
1535                // v1 returns noSuchName, v2c/v3 returns endOfMibView
1536                if ctx.version == Version::V1 {
1537                    return Ok(pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32));
1538                }
1539                response_varbinds.push(VarBind::new(vb.oid.clone(), Value::EndOfMibView));
1540            }
1541        }
1542
1543        // RFC 3416 Section 4.2.2: if the Response would exceed the message-size
1544        // limit, return a tooBig Response with an empty variable-bindings list.
1545        if !Self::response_fits(
1546            &response_varbinds,
1547            self.response_overhead(ctx),
1548            self.effective_max_size(ctx),
1549        ) {
1550            return Ok(Self::too_big_response(ctx.version, pdu));
1551        }
1552
1553        Ok(Pdu {
1554            pdu_type: PduType::Response,
1555            request_id: pdu.request_id,
1556            error_status: 0,
1557            error_index: 0,
1558            varbinds: response_varbinds,
1559        })
1560    }
1561
1562    /// Handle GETBULK request.
1563    ///
1564    /// Per RFC 3416 Section 4.2.3, if the response would exceed the message
1565    /// size limit, we return fewer variable bindings rather than all of them.
1566    async fn handle_get_bulk(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1567        // For GETBULK, error_status is non_repeaters and error_index is max_repetitions
1568        let non_repeaters = pdu.error_status.try_into().unwrap_or(0);
1569        let max_repetitions = pdu.error_index.max(0);
1570
1571        let mut response_varbinds = Vec::new();
1572        let mut current_size: usize = self.response_overhead(ctx);
1573        let max_size = self.effective_max_size(ctx);
1574
1575        // Helper to check if we can add a varbind
1576        let can_add = |vb: &VarBind, current_size: usize| -> bool {
1577            current_size + vb.encoded_size() <= max_size
1578        };
1579
1580        // Handle non-repeaters (first N varbinds get one GETNEXT each)
1581        for (index, vb) in pdu.varbinds.iter().take(non_repeaters).enumerate() {
1582            let next = match self.get_next_accessible_oid(ctx, &vb.oid).await {
1583                Ok(next) => next,
1584                Err(err) => {
1585                    // RFC 3416 Section 4.2.3: error-index names the varbind in
1586                    // the received request.
1587                    tracing::warn!(
1588                        target: "async_snmp::agent",
1589                        oid = %vb.oid,
1590                        error = %err,
1591                        "handler GETBULK failed; responding genErr"
1592                    );
1593                    return Ok(pdu.to_error_response(ErrorStatus::GenErr, (index + 1) as i32));
1594                }
1595            };
1596
1597            let next_vb = match next {
1598                Some(next_vb) => next_vb,
1599                None => VarBind::new(vb.oid.clone(), Value::EndOfMibView),
1600            };
1601
1602            if !can_add(&next_vb, current_size) {
1603                // Can't fit even non-repeaters, return tooBig if we have nothing
1604                if response_varbinds.is_empty() {
1605                    return Ok(Self::too_big_response(ctx.version, pdu));
1606                }
1607                // RFC 3416 Section 4.2.3: truncation removes variable bindings
1608                // from the END of the positional set. All repeaters are
1609                // positionally after every non-repeater, so once a non-repeater
1610                // is dropped, no later binding may appear. Return the
1611                // non-repeater prefix collected so far without running the
1612                // repeater loop (falling through would emit repeater varbinds
1613                // into the dropped non-repeater's slot).
1614                return Ok(Pdu {
1615                    pdu_type: PduType::Response,
1616                    request_id: pdu.request_id,
1617                    error_status: 0,
1618                    error_index: 0,
1619                    varbinds: response_varbinds,
1620                });
1621            }
1622
1623            current_size += next_vb.encoded_size();
1624            response_varbinds.push(next_vb);
1625        }
1626
1627        // Handle repeaters
1628        if non_repeaters < pdu.varbinds.len() {
1629            let repeaters = &pdu.varbinds[non_repeaters..];
1630            let mut current_oids: Vec<Oid> = repeaters.iter().map(|vb| vb.oid.clone()).collect();
1631            let mut all_done = vec![false; repeaters.len()];
1632
1633            'outer: for _ in 0..max_repetitions {
1634                let mut row_complete = true;
1635                for (i, oid) in current_oids.iter_mut().enumerate() {
1636                    let next_vb = if all_done[i] {
1637                        VarBind::new(oid.clone(), Value::EndOfMibView)
1638                    } else {
1639                        let next = match self.get_next_accessible_oid(ctx, oid).await {
1640                            Ok(next) => next,
1641                            Err(err) => {
1642                                // error-index refers to the repeater's position
1643                                // in the received request, whatever the
1644                                // repetition it failed on (RFC 3416
1645                                // Section 4.2.3).
1646                                tracing::warn!(
1647                                    target: "async_snmp::agent",
1648                                    oid = %oid,
1649                                    error = %err,
1650                                    "handler GETBULK failed; responding genErr"
1651                                );
1652                                return Ok(pdu.to_error_response(
1653                                    ErrorStatus::GenErr,
1654                                    (non_repeaters + i + 1) as i32,
1655                                ));
1656                            }
1657                        };
1658
1659                        if let Some(next_vb) = next {
1660                            *oid = next_vb.oid.clone();
1661                            row_complete = false;
1662                            next_vb
1663                        } else {
1664                            all_done[i] = true;
1665                            VarBind::new(oid.clone(), Value::EndOfMibView)
1666                        }
1667                    };
1668
1669                    // Check size before adding
1670                    if !can_add(&next_vb, current_size) {
1671                        // RFC 3416 Section 4.2.3 / net-snmp: if nothing has fit
1672                        // yet (common non_repeaters == 0 shape where the first
1673                        // repeater varbind is oversized), return tooBig with
1674                        // empty varbinds. Mirrors the non-repeater tooBig guard
1675                        // above; a bare noError+empty response is
1676                        // indistinguishable from end-of-MIB and silently ends a
1677                        // manager's walk instead of prompting a retry with a
1678                        // smaller max-repetitions.
1679                        if response_varbinds.is_empty() {
1680                            return Ok(Self::too_big_response(ctx.version, pdu));
1681                        }
1682                        // Some varbinds already fit: truncate (partial response).
1683                        break 'outer;
1684                    }
1685
1686                    current_size += next_vb.encoded_size();
1687                    response_varbinds.push(next_vb);
1688                }
1689
1690                if row_complete {
1691                    break;
1692                }
1693            }
1694        }
1695
1696        Ok(Pdu {
1697            pdu_type: PduType::Response,
1698            request_id: pdu.request_id,
1699            error_status: 0,
1700            error_index: 0,
1701            varbinds: response_varbinds,
1702        })
1703    }
1704
1705    /// Find the handler for a given OID.
1706    pub(crate) fn find_handler(&self, oid: &Oid) -> Option<&RegisteredHandler> {
1707        // Handlers are sorted by prefix length (longest first)
1708        self.inner
1709            .handlers
1710            .iter()
1711            .find(|&handler| handler.handler.handles(&handler.prefix, oid))
1712            .map(|v| v as _)
1713    }
1714
1715    /// Find the next OID accessible under VACM, skipping denied OIDs by
1716    /// continuing the walk. Returns None when end-of-MIB is reached or all
1717    /// remaining candidates are denied. A handler processing failure
1718    /// propagates as Err (mapped to genErr by the caller).
1719    async fn get_next_accessible_oid(
1720        &self,
1721        ctx: &RequestContext,
1722        from_oid: &Oid,
1723    ) -> HandlerResult<Option<VarBind>> {
1724        let mut search_from = from_oid.clone();
1725        for _ in 0..MAX_VACM_SKIP_ITERATIONS {
1726            let candidate = self.get_next_oid(ctx, &search_from).await?;
1727            match candidate {
1728                None => return Ok(None),
1729                Some(ref next_vb) => {
1730                    if next_vb.oid <= search_from {
1731                        tracing::error!(
1732                            target: "async_snmp::agent",
1733                            from = %search_from,
1734                            got = %next_vb.oid,
1735                            "handler returned non-increasing OID in GETNEXT"
1736                        );
1737                        return Ok(None);
1738                    }
1739                    if v1_rejects_counter64(ctx.version, &next_vb.value) {
1740                        search_from = next_vb.oid.clone();
1741                        continue;
1742                    }
1743                    if let Some(ref vacm) = self.inner.vacm {
1744                        if vacm.check_access(ctx.read_view.as_ref(), &next_vb.oid) {
1745                            return Ok(candidate);
1746                        }
1747                        search_from = next_vb.oid.clone();
1748                    } else {
1749                        return Ok(candidate);
1750                    }
1751                }
1752            }
1753        }
1754        // Skip cap reached: treat as end-of-MIB for this varbind rather than
1755        // continuing to probe an unboundedly large denied range.
1756        tracing::warn!(
1757            target: "async_snmp::agent",
1758            from = %from_oid,
1759            cap = MAX_VACM_SKIP_ITERATIONS,
1760            "VACM skip cap reached in GETNEXT; ending scan for this varbind"
1761        );
1762        Ok(None)
1763    }
1764
1765    /// Get the next OID from any handler.
1766    async fn get_next_oid(
1767        &self,
1768        ctx: &RequestContext,
1769        oid: &Oid,
1770    ) -> HandlerResult<Option<VarBind>> {
1771        // Find the first handler that can provide a next OID.
1772        //
1773        // A handler can only return an OID > oid if:
1774        //   - oid falls within the handler's subtree (oid starts with handler prefix), OR
1775        //   - the handler's entire subtree is after oid (handler prefix > oid)
1776        //
1777        // Handlers whose prefix is <= oid and whose subtree does not contain oid
1778        // cannot return anything useful and are skipped.
1779        let mut best_result: Option<VarBind> = None;
1780
1781        for handler in &self.inner.handlers {
1782            let prefix = &handler.prefix;
1783            if prefix <= oid && !oid.starts_with(prefix) {
1784                continue;
1785            }
1786            if let GetNextResult::Value(next) = handler.handler.get_next(ctx, oid).await? {
1787                // Must be lexicographically greater than the request OID
1788                if next.oid > *oid {
1789                    match &best_result {
1790                        None => best_result = Some(next),
1791                        Some(current) if next.oid < current.oid => best_result = Some(next),
1792                        _ => {}
1793                    }
1794                }
1795            }
1796        }
1797
1798        Ok(best_result)
1799    }
1800}
1801
1802impl Clone for Agent {
1803    fn clone(&self) -> Self {
1804        Self {
1805            inner: Arc::clone(&self.inner),
1806        }
1807    }
1808}
1809
1810#[cfg(test)]
1811mod tests {
1812    use super::*;
1813    use crate::handler::{
1814        BoxFuture, GetNextResult, GetResult, HandlerError, HandlerResult, MibHandler,
1815        RequestContext, SecurityModel, SetResult,
1816    };
1817    use crate::message::SecurityLevel;
1818    use crate::oid;
1819
1820    struct TestHandler;
1821
1822    impl MibHandler for TestHandler {
1823        fn get<'a>(
1824            &'a self,
1825            _ctx: &'a RequestContext,
1826            oid: &'a Oid,
1827        ) -> BoxFuture<'a, HandlerResult<GetResult>> {
1828            Box::pin(async move {
1829                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
1830                    return Ok(GetResult::Value(Value::Integer(42)));
1831                }
1832                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
1833                    return Ok(GetResult::Value(Value::OctetString(Bytes::from_static(
1834                        b"test",
1835                    ))));
1836                }
1837                Ok(GetResult::NoSuchObject)
1838            })
1839        }
1840
1841        fn get_next<'a>(
1842            &'a self,
1843            _ctx: &'a RequestContext,
1844            oid: &'a Oid,
1845        ) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
1846            Box::pin(async move {
1847                let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
1848                let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
1849
1850                if oid < &oid1 {
1851                    return Ok(GetNextResult::Value(VarBind::new(oid1, Value::Integer(42))));
1852                }
1853                if oid < &oid2 {
1854                    return Ok(GetNextResult::Value(VarBind::new(
1855                        oid2,
1856                        Value::OctetString(Bytes::from_static(b"test")),
1857                    )));
1858                }
1859                Ok(GetNextResult::EndOfMibView)
1860            })
1861        }
1862    }
1863
1864    fn test_ctx() -> RequestContext {
1865        RequestContext {
1866            source: "127.0.0.1:12345".parse().unwrap(),
1867            version: Version::V2c,
1868            security_model: SecurityModel::V2c,
1869            security_name: Bytes::from_static(b"public"),
1870            security_level: SecurityLevel::NoAuthNoPriv,
1871            context_name: Bytes::new(),
1872            request_id: 1,
1873            pdu_type: PduType::GetRequest,
1874            group_name: None,
1875            read_view: None,
1876            write_view: None,
1877            msg_max_size: None,
1878        }
1879    }
1880
1881    #[test]
1882    fn test_agent_builder_defaults() {
1883        let builder = AgentBuilder::new();
1884        assert_eq!(builder.bind_addr, "0.0.0.0:161");
1885        assert!(builder.communities.is_empty());
1886        assert!(builder.usm_users.is_empty());
1887        assert!(builder.handlers.is_empty());
1888    }
1889
1890    #[test]
1891    fn test_agent_builder_community() {
1892        let builder = AgentBuilder::new()
1893            .community(b"public")
1894            .community(b"private");
1895        assert_eq!(builder.communities.len(), 2);
1896    }
1897
1898    #[test]
1899    fn test_agent_builder_communities() {
1900        let builder = AgentBuilder::new().communities(["public", "private"]);
1901        assert_eq!(builder.communities.len(), 2);
1902    }
1903
1904    #[test]
1905    fn test_agent_builder_handler() {
1906        let builder =
1907            AgentBuilder::new().handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler));
1908        assert_eq!(builder.handlers.len(), 1);
1909    }
1910
1911    #[tokio::test]
1912    async fn test_agent_builder_rejects_privacy_without_auth() {
1913        let result = AgentBuilder::new()
1914            .bind("127.0.0.1:0")
1915            .usm_user("noauth", |u| {
1916                u.privacy(crate::v3::PrivProtocol::Aes128, b"privpass")
1917            })
1918            .build()
1919            .await;
1920        match result {
1921            Err(err) => assert!(
1922                matches!(*err, Error::Config(_)),
1923                "expected Config error, got {err:?}"
1924            ),
1925            Ok(_) => panic!("privacy without auth must be rejected"),
1926        }
1927    }
1928
1929    #[tokio::test]
1930    async fn test_mib_handler_default_set() {
1931        let handler = TestHandler;
1932        let mut ctx = test_ctx();
1933        ctx.pdu_type = PduType::SetRequest;
1934
1935        let result = handler
1936            .test_set(&ctx, &oid!(1, 3, 6, 1), &Value::Integer(1))
1937            .await;
1938        assert_eq!(result, SetResult::NotWritable);
1939    }
1940
1941    #[test]
1942    fn test_mib_handler_handles() {
1943        let handler = TestHandler;
1944        let prefix = oid!(1, 3, 6, 1, 4, 1, 99_999);
1945
1946        // OID within prefix
1947        assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_999, 1, 0)));
1948
1949        // Exact prefix match
1950        assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_999)));
1951
1952        // OID before prefix - should NOT be handled (GET/SET routing must not claim
1953        // OIDs outside the registered subtree)
1954        assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_998)));
1955
1956        // OID after prefix (not handled)
1957        assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 100_000)));
1958    }
1959
1960    #[tokio::test]
1961    async fn test_test_handler_get() {
1962        let handler = TestHandler;
1963        let ctx = test_ctx();
1964
1965        // Existing OID
1966        let result = handler
1967            .get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
1968            .await
1969            .unwrap();
1970        assert!(matches!(result, GetResult::Value(Value::Integer(42))));
1971
1972        // Non-existing OID
1973        let result = handler
1974            .get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 99, 0))
1975            .await
1976            .unwrap();
1977        assert!(matches!(result, GetResult::NoSuchObject));
1978    }
1979
1980    #[tokio::test]
1981    async fn test_test_handler_get_next() {
1982        let handler = TestHandler;
1983        let mut ctx = test_ctx();
1984        ctx.pdu_type = PduType::GetNextRequest;
1985
1986        // Before first OID
1987        let next = handler
1988            .get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999))
1989            .await
1990            .unwrap();
1991        assert!(next.is_value());
1992        if let GetNextResult::Value(vb) = next {
1993            assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0));
1994        }
1995
1996        // Between OIDs
1997        let next = handler
1998            .get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
1999            .await
2000            .unwrap();
2001        assert!(next.is_value());
2002        if let GetNextResult::Value(vb) = next {
2003            assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0));
2004        }
2005
2006        // After last OID
2007        let next = handler
2008            .get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
2009            .await
2010            .unwrap();
2011        assert!(next.is_end_of_mib_view());
2012    }
2013
2014    // Serves .99999.1.0 and fails everything past it, simulating a backing
2015    // store that is reachable for the first object and down for the rest.
2016    struct FailingBackendHandler;
2017
2018    impl MibHandler for FailingBackendHandler {
2019        fn get<'a>(
2020            &'a self,
2021            _ctx: &'a RequestContext,
2022            oid: &'a Oid,
2023        ) -> BoxFuture<'a, HandlerResult<GetResult>> {
2024            Box::pin(async move {
2025                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
2026                    return Ok(GetResult::Value(Value::Integer(1)));
2027                }
2028                Err(HandlerError::new("backing store unavailable"))
2029            })
2030        }
2031
2032        fn get_next<'a>(
2033            &'a self,
2034            _ctx: &'a RequestContext,
2035            oid: &'a Oid,
2036        ) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
2037            Box::pin(async move {
2038                let first = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
2039                if oid < &first {
2040                    return Ok(GetNextResult::Value(VarBind::new(first, Value::Integer(1))));
2041                }
2042                Err(HandlerError::new("backing store unavailable"))
2043            })
2044        }
2045    }
2046
2047    async fn failing_backend_agent() -> Agent {
2048        AgentBuilder::new()
2049            .bind("127.0.0.1:0")
2050            .community(b"public")
2051            .handler(
2052                oid!(1, 3, 6, 1, 4, 1, 99999),
2053                Arc::new(FailingBackendHandler),
2054            )
2055            .build()
2056            .await
2057            .unwrap()
2058    }
2059
2060    #[tokio::test]
2061    async fn test_get_handler_error_maps_to_generr() {
2062        let agent = failing_backend_agent().await;
2063        let ctx = test_ctx();
2064
2065        // First varbind succeeds, second hits the failing backend: RFC 3416
2066        // Section 4.2.1 requires genErr with error-index of the failing varbind
2067        // and the request varbinds echoed.
2068        let pdu = Pdu {
2069            pdu_type: PduType::GetRequest,
2070            request_id: 1,
2071            error_status: 0,
2072            error_index: 0,
2073            varbinds: vec![
2074                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0), Value::Null),
2075                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0), Value::Null),
2076            ],
2077        };
2078
2079        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2080        assert_eq!(response.error_status, ErrorStatus::GenErr.as_i32());
2081        assert_eq!(response.error_index, 2);
2082        assert_eq!(response.varbinds.len(), 2);
2083        assert_eq!(response.varbinds[0].oid, pdu.varbinds[0].oid);
2084    }
2085
2086    #[tokio::test]
2087    async fn test_get_v1_handler_error_maps_to_generr() {
2088        let agent = failing_backend_agent().await;
2089        let mut ctx = test_ctx();
2090        ctx.version = Version::V1;
2091
2092        let pdu = Pdu {
2093            pdu_type: PduType::GetRequest,
2094            request_id: 2,
2095            error_status: 0,
2096            error_index: 0,
2097            varbinds: vec![VarBind::new(
2098                oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0),
2099                Value::Null,
2100            )],
2101        };
2102
2103        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2104        assert_eq!(response.error_status, ErrorStatus::GenErr.as_i32());
2105        assert_eq!(response.error_index, 1);
2106    }
2107
2108    #[tokio::test]
2109    async fn test_getnext_handler_error_maps_to_generr() {
2110        let agent = failing_backend_agent().await;
2111        let mut ctx = test_ctx();
2112        ctx.pdu_type = PduType::GetNextRequest;
2113
2114        let pdu = Pdu {
2115            pdu_type: PduType::GetNextRequest,
2116            request_id: 3,
2117            error_status: 0,
2118            error_index: 0,
2119            varbinds: vec![VarBind::new(
2120                oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2121                Value::Null,
2122            )],
2123        };
2124
2125        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2126        assert_eq!(response.error_status, ErrorStatus::GenErr.as_i32());
2127        assert_eq!(response.error_index, 1);
2128    }
2129
2130    #[tokio::test]
2131    async fn test_getbulk_handler_error_maps_to_generr() {
2132        let agent = failing_backend_agent().await;
2133        let mut ctx = test_ctx();
2134        ctx.pdu_type = PduType::GetBulkRequest;
2135
2136        // Non-repeater resolves to .1.0; the repeater's first GETNEXT fails.
2137        // error-index refers to the varbind position in the received request
2138        // (RFC 3416 Section 4.2.3), here 2, regardless of repetition count.
2139        let pdu = Pdu {
2140            pdu_type: PduType::GetBulkRequest,
2141            request_id: 4,
2142            error_status: 1, // non_repeaters
2143            error_index: 5,  // max_repetitions
2144            varbinds: vec![
2145                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null),
2146                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0), Value::Null),
2147            ],
2148        };
2149
2150        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2151        assert_eq!(response.error_status, ErrorStatus::GenErr.as_i32());
2152        assert_eq!(response.error_index, 2);
2153    }
2154
2155    // FiveOidHandler has OIDs at .99999.{1,2,3,4,5}.0 with integer values 1-5.
2156    struct FiveOidHandler;
2157
2158    impl MibHandler for FiveOidHandler {
2159        fn get<'a>(
2160            &'a self,
2161            _ctx: &'a RequestContext,
2162            oid: &'a Oid,
2163        ) -> BoxFuture<'a, HandlerResult<GetResult>> {
2164            Box::pin(async move {
2165                for i in 1u16..=5 {
2166                    if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, i.into(), 0) {
2167                        return Ok(GetResult::Value(Value::Integer(i.into())));
2168                    }
2169                }
2170                Ok(GetResult::NoSuchObject)
2171            })
2172        }
2173
2174        fn get_next<'a>(
2175            &'a self,
2176            _ctx: &'a RequestContext,
2177            oid: &'a Oid,
2178        ) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
2179            Box::pin(async move {
2180                for i in 1u32..=5 {
2181                    let candidate = oid!(1, 3, 6, 1, 4, 1, 99999, i, 0);
2182                    if oid < &candidate {
2183                        return Ok(GetNextResult::Value(VarBind::new(
2184                            candidate,
2185                            Value::Integer(i as i32),
2186                        )));
2187                    }
2188                }
2189                Ok(GetNextResult::EndOfMibView)
2190            })
2191        }
2192    }
2193
2194    /// Build an agent bound to a random port for testing, with a VACM view
2195    /// that only permits reading OIDs under .99999.2 and .99999.4 (odd OIDs
2196    /// 1, 3, 5 are denied). This exercises the VACM walk-past logic.
2197    async fn test_agent_with_restricted_vacm() -> Agent {
2198        Agent::builder()
2199            .bind("127.0.0.1:0")
2200            .community(b"public")
2201            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
2202            .vacm(|v| {
2203                v.group("public", SecurityModel::V2c, "readers")
2204                    .access("readers", |a| a.read_view("restricted"))
2205                    .view("restricted", |v| {
2206                        v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 2))
2207                            .include(oid!(1, 3, 6, 1, 4, 1, 99999, 4))
2208                    })
2209            })
2210            .build()
2211            .await
2212            .unwrap()
2213    }
2214
2215    #[tokio::test]
2216    async fn test_getbulk_vacm_filters_inaccessible_oids() {
2217        let agent = test_agent_with_restricted_vacm().await;
2218
2219        let mut ctx = test_ctx();
2220        ctx.pdu_type = PduType::GetBulkRequest;
2221        ctx.read_view = Some(Bytes::from_static(b"restricted"));
2222
2223        // GETBULK starting before the handler prefix, requesting up to 10 repeats.
2224        // The handler has OIDs {1,2,3,4,5}.0 but only {2,4} are in the view.
2225        // The walk must skip denied OIDs and continue, returning both 2 and 4.
2226        let pdu = Pdu {
2227            pdu_type: PduType::GetBulkRequest,
2228            request_id: 1,
2229            error_status: 0, // non_repeaters
2230            error_index: 10, // max_repetitions
2231            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2232        };
2233
2234        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2235
2236        // Collect the OIDs returned (excluding EndOfMibView sentinels)
2237        let returned_oids: Vec<&Oid> = response
2238            .varbinds
2239            .iter()
2240            .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2241            .map(|vb| &vb.oid)
2242            .collect();
2243
2244        // Both accessible OIDs must appear - the walk must not stop at the first one
2245        assert!(
2246            returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)),
2247            "expected .99999.2.0 in response, got: {returned_oids:?}"
2248        );
2249        assert!(
2250            returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0)),
2251            "expected .99999.4.0 in response (walk must continue past denied OIDs), got: {returned_oids:?}"
2252        );
2253
2254        // Denied OIDs must not appear
2255        for &oid in &[
2256            &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2257            &oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
2258            &oid!(1, 3, 6, 1, 4, 1, 99999, 5, 0),
2259        ] {
2260            assert!(
2261                !returned_oids.contains(&oid),
2262                "GETBULK returned OID outside read view: {oid:?}"
2263            );
2264        }
2265    }
2266
2267    #[tokio::test]
2268    async fn test_getbulk_non_repeaters_vacm_filtered() {
2269        let agent = test_agent_with_restricted_vacm().await;
2270
2271        let mut ctx = test_ctx();
2272        ctx.pdu_type = PduType::GetBulkRequest;
2273        ctx.read_view = Some(Bytes::from_static(b"restricted"));
2274
2275        // GETBULK with non_repeaters=2, max_repetitions=0.
2276        // First varbind starts before the subtree: walks past denied .99999.1.0
2277        // and returns the first accessible .99999.2.0.
2278        // Second varbind starts at .99999.4.0 (the last accessible OID): walks
2279        // to .99999.5.0 (denied) and then hits end-of-MIB, returning EndOfMibView.
2280        let pdu = Pdu {
2281            pdu_type: PduType::GetBulkRequest,
2282            request_id: 2,
2283            error_status: 2, // non_repeaters
2284            error_index: 0,  // max_repetitions
2285            varbinds: vec![
2286                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null),
2287                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0), Value::Null),
2288            ],
2289        };
2290
2291        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2292
2293        // First non-repeater skips denied .99999.1.0 and returns accessible .99999.2.0
2294        assert_eq!(
2295            response.varbinds[0].oid,
2296            oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)
2297        );
2298        assert!(matches!(response.varbinds[0].value, Value::Integer(2)));
2299
2300        // Second non-repeater walks to .99999.5.0 (denied), then end-of-MIB
2301        assert_eq!(response.varbinds[1].value, Value::EndOfMibView);
2302    }
2303
2304    /// Handler exposing an effectively unbounded range of OIDs under
2305    /// .99999.1.<n>, counting every `get_next` call. Used to exercise the VACM
2306    /// skip cap: every OID it returns is denied by the accompanying view, so a
2307    /// single GETNEXT step would loop forever without the bound.
2308    struct CountingRangeHandler {
2309        calls: Arc<std::sync::atomic::AtomicUsize>,
2310    }
2311
2312    impl MibHandler for CountingRangeHandler {
2313        fn get<'a>(
2314            &'a self,
2315            _ctx: &'a RequestContext,
2316            _oid: &'a Oid,
2317        ) -> BoxFuture<'a, HandlerResult<GetResult>> {
2318            Box::pin(async move { Ok(GetResult::NoSuchObject) })
2319        }
2320
2321        fn get_next<'a>(
2322            &'a self,
2323            _ctx: &'a RequestContext,
2324            _oid: &'a Oid,
2325        ) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
2326            Box::pin(async move {
2327                // Nth call returns .99999.1.N; N strictly increases each call, so
2328                // the returned OID is always greater than the previous one (the
2329                // current search cursor), keeping the walk monotonically advancing.
2330                let n = self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
2331                let next = Oid::from_slice(&[1, 3, 6, 1, 4, 1, 99999, 1]).child(n as u32);
2332                Ok(GetNextResult::Value(VarBind::new(next, Value::Integer(1))))
2333            })
2334        }
2335    }
2336
2337    // Regression: a GETNEXT over a large denied range must not make an unbounded
2338    // number of backing-store lookups. The skip loop is capped, so the handler
2339    // is called at most MAX_VACM_SKIP_ITERATIONS times per varbind and the step
2340    // resolves to end-of-MIB instead of looping.
2341    #[tokio::test]
2342    async fn test_getnext_vacm_denied_range_is_capped() {
2343        let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2344        let agent = Agent::builder()
2345            .bind("127.0.0.1:0")
2346            .community(b"public")
2347            .handler(
2348                oid!(1, 3, 6, 1, 4, 1, 99999),
2349                Arc::new(CountingRangeHandler {
2350                    calls: calls.clone(),
2351                }),
2352            )
2353            // View includes an unrelated subtree only, so every OID the handler
2354            // returns under .99999 is denied.
2355            .vacm(|v| {
2356                v.group("public", SecurityModel::V2c, "readers")
2357                    .access("readers", |a| a.read_view("restricted"))
2358                    .view("restricted", |v| v.include(oid!(1, 3, 6, 1, 4, 1, 88888)))
2359            })
2360            .build()
2361            .await
2362            .unwrap();
2363
2364        let mut ctx = test_ctx();
2365        ctx.pdu_type = PduType::GetNextRequest;
2366        ctx.read_view = Some(Bytes::from_static(b"restricted"));
2367
2368        let pdu = Pdu {
2369            pdu_type: PduType::GetNextRequest,
2370            request_id: 1,
2371            error_status: 0,
2372            error_index: 0,
2373            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2374        };
2375
2376        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2377
2378        // The step resolves to end-of-MIB rather than returning a denied OID.
2379        assert_eq!(response.varbinds.len(), 1);
2380        assert_eq!(response.varbinds[0].value, Value::EndOfMibView);
2381
2382        // The skip loop is bounded: the handler is not called unboundedly.
2383        let total = calls.load(std::sync::atomic::Ordering::SeqCst);
2384        assert!(
2385            total <= MAX_VACM_SKIP_ITERATIONS,
2386            "handler called {total} times, expected <= {MAX_VACM_SKIP_ITERATIONS}"
2387        );
2388    }
2389
2390    // TestHandler with three OIDs: .99999.1.0, .99999.2.0, .99999.3.0
2391    struct ThreeOidHandler;
2392
2393    impl MibHandler for ThreeOidHandler {
2394        fn get<'a>(
2395            &'a self,
2396            _ctx: &'a RequestContext,
2397            oid: &'a Oid,
2398        ) -> BoxFuture<'a, HandlerResult<GetResult>> {
2399            Box::pin(async move {
2400                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
2401                    return Ok(GetResult::Value(Value::Integer(1)));
2402                }
2403                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
2404                    return Ok(GetResult::Value(Value::Integer(2)));
2405                }
2406                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0) {
2407                    return Ok(GetResult::Value(Value::Integer(3)));
2408                }
2409                Ok(GetResult::NoSuchObject)
2410            })
2411        }
2412
2413        fn get_next<'a>(
2414            &'a self,
2415            _ctx: &'a RequestContext,
2416            oid: &'a Oid,
2417        ) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
2418            Box::pin(async move {
2419                let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
2420                let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
2421                let oid3 = oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0);
2422
2423                if oid < &oid1 {
2424                    return Ok(GetNextResult::Value(VarBind::new(oid1, Value::Integer(1))));
2425                }
2426                if oid < &oid2 {
2427                    return Ok(GetNextResult::Value(VarBind::new(oid2, Value::Integer(2))));
2428                }
2429                if oid < &oid3 {
2430                    return Ok(GetNextResult::Value(VarBind::new(oid3, Value::Integer(3))));
2431                }
2432                Ok(GetNextResult::EndOfMibView)
2433            })
2434        }
2435    }
2436
2437    /// Build an agent with `ThreeOidHandler` and a VACM view that includes
2438    /// .99999.1 and .99999.3 but excludes .99999.2.
2439    async fn test_agent_with_gap_vacm() -> Agent {
2440        Agent::builder()
2441            .bind("127.0.0.1:0")
2442            .community(b"public")
2443            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(ThreeOidHandler))
2444            .vacm(|v| {
2445                v.group("public", SecurityModel::V2c, "readers")
2446                    .access("readers", |a| a.read_view("gap"))
2447                    .view("gap", |v| {
2448                        v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 1))
2449                            .include(oid!(1, 3, 6, 1, 4, 1, 99999, 3))
2450                    })
2451            })
2452            .build()
2453            .await
2454            .unwrap()
2455    }
2456
2457    #[tokio::test]
2458    async fn test_getnext_vacm_skips_inaccessible_continues_walk() {
2459        // GETNEXT must continue past denied OIDs to find the next accessible one.
2460        // .99999.2.0 is excluded from the view; .99999.3.0 is included.
2461        // GETNEXT from .99999.1.0 should skip .99999.2.0 and return .99999.3.0.
2462        let agent = test_agent_with_gap_vacm().await;
2463
2464        let mut ctx = test_ctx();
2465        ctx.pdu_type = PduType::GetNextRequest;
2466        ctx.read_view = Some(Bytes::from_static(b"gap"));
2467
2468        let pdu = Pdu {
2469            pdu_type: PduType::GetNextRequest,
2470            request_id: 1,
2471            error_status: 0,
2472            error_index: 0,
2473            varbinds: vec![VarBind::new(
2474                oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2475                Value::Null,
2476            )],
2477        };
2478
2479        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2480        assert_eq!(response.varbinds.len(), 1);
2481        assert_eq!(
2482            response.varbinds[0].oid,
2483            oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
2484            "GETNEXT should skip denied .99999.2.0 and return accessible .99999.3.0"
2485        );
2486        assert!(matches!(response.varbinds[0].value, Value::Integer(3)));
2487    }
2488
2489    #[tokio::test]
2490    async fn test_getnext_vacm_all_remaining_denied_returns_end_of_mib() {
2491        // When all remaining OIDs are denied, GETNEXT should return EndOfMibView.
2492        // Start at .99999.4.0 (the last accessible OID). The only OID after it
2493        // is .99999.5.0 which is denied, so the walk reaches end-of-MIB.
2494        let agent = test_agent_with_restricted_vacm().await;
2495
2496        let mut ctx = test_ctx();
2497        ctx.pdu_type = PduType::GetNextRequest;
2498        ctx.read_view = Some(Bytes::from_static(b"restricted"));
2499
2500        let pdu = Pdu {
2501            pdu_type: PduType::GetNextRequest,
2502            request_id: 1,
2503            error_status: 0,
2504            error_index: 0,
2505            varbinds: vec![VarBind::new(
2506                oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0),
2507                Value::Null,
2508            )],
2509        };
2510
2511        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2512        assert_eq!(response.varbinds.len(), 1);
2513        assert_eq!(
2514            response.varbinds[0].value,
2515            Value::EndOfMibView,
2516            "GETNEXT should return EndOfMibView when all remaining OIDs are denied"
2517        );
2518    }
2519
2520    #[tokio::test]
2521    async fn test_getbulk_without_vacm_returns_all_oids() {
2522        // Sanity check: without VACM, both OIDs should be returned
2523        let agent = Agent::builder()
2524            .bind("127.0.0.1:0")
2525            .community(b"public")
2526            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler))
2527            .build()
2528            .await
2529            .unwrap();
2530
2531        let mut ctx = test_ctx();
2532        ctx.pdu_type = PduType::GetBulkRequest;
2533
2534        let pdu = Pdu {
2535            pdu_type: PduType::GetBulkRequest,
2536            request_id: 1,
2537            error_status: 0,
2538            error_index: 10,
2539            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2540        };
2541
2542        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2543
2544        // Both OIDs should appear
2545        assert!(
2546            response
2547                .varbinds
2548                .iter()
2549                .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
2550        );
2551        assert!(
2552            response
2553                .varbinds
2554                .iter()
2555                .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
2556        );
2557    }
2558
2559    #[tokio::test]
2560    async fn test_v1_getbulk_rejected() {
2561        // SNMPv1 does not support GETBULK. Should return GenErr.
2562        let agent = Agent::builder()
2563            .bind("127.0.0.1:0")
2564            .community(b"public")
2565            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler))
2566            .build()
2567            .await
2568            .unwrap();
2569
2570        let mut ctx = test_ctx();
2571        ctx.version = Version::V1;
2572        ctx.security_model = SecurityModel::V1;
2573        ctx.pdu_type = PduType::GetBulkRequest;
2574
2575        let pdu = Pdu {
2576            pdu_type: PduType::GetBulkRequest,
2577            request_id: 1,
2578            error_status: 0,
2579            error_index: 10,
2580            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2581        };
2582
2583        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2584        assert_eq!(
2585            ErrorStatus::from_i32(response.error_status),
2586            ErrorStatus::GenErr,
2587            "v1 GETBULK should be rejected"
2588        );
2589    }
2590
2591    /// Handler returning Counter64 at .99999.1.0, Integer at .99999.2.0
2592    struct Counter64Handler;
2593
2594    impl MibHandler for Counter64Handler {
2595        fn get<'a>(
2596            &'a self,
2597            _ctx: &'a RequestContext,
2598            oid: &'a Oid,
2599        ) -> BoxFuture<'a, HandlerResult<GetResult>> {
2600            Box::pin(async move {
2601                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
2602                    return Ok(GetResult::Value(Value::Counter64(1_000_000_000_000)));
2603                }
2604                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
2605                    return Ok(GetResult::Value(Value::Integer(42)));
2606                }
2607                Ok(GetResult::NoSuchObject)
2608            })
2609        }
2610
2611        fn get_next<'a>(
2612            &'a self,
2613            _ctx: &'a RequestContext,
2614            oid: &'a Oid,
2615        ) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
2616            Box::pin(async move {
2617                let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
2618                let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
2619
2620                if oid < &oid1 {
2621                    return Ok(GetNextResult::Value(VarBind::new(
2622                        oid1,
2623                        Value::Counter64(1_000_000_000_000),
2624                    )));
2625                }
2626                if oid < &oid2 {
2627                    return Ok(GetNextResult::Value(VarBind::new(oid2, Value::Integer(42))));
2628                }
2629                Ok(GetNextResult::EndOfMibView)
2630            })
2631        }
2632    }
2633
2634    async fn test_agent_with_counter64() -> Agent {
2635        Agent::builder()
2636            .bind("127.0.0.1:0")
2637            .community(b"public")
2638            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(Counter64Handler))
2639            .build()
2640            .await
2641            .unwrap()
2642    }
2643
2644    #[tokio::test]
2645    async fn test_v1_get_filters_counter64() {
2646        // RFC 2576 Section 4.1.2.3: Counter64 not valid in v1 GET responses.
2647        // Should return noSuchName for the Counter64 varbind.
2648        let agent = test_agent_with_counter64().await;
2649
2650        let mut ctx = test_ctx();
2651        ctx.version = Version::V1;
2652        ctx.security_model = SecurityModel::V1;
2653        ctx.pdu_type = PduType::GetRequest;
2654
2655        let pdu = Pdu {
2656            pdu_type: PduType::GetRequest,
2657            request_id: 1,
2658            error_status: 0,
2659            error_index: 0,
2660            varbinds: vec![VarBind::new(
2661                oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2662                Value::Null,
2663            )],
2664        };
2665
2666        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2667        assert_eq!(
2668            ErrorStatus::from_i32(response.error_status),
2669            ErrorStatus::NoSuchName,
2670            "v1 GET of Counter64 should return noSuchName"
2671        );
2672    }
2673
2674    #[tokio::test]
2675    async fn test_v2c_get_allows_counter64() {
2676        // v2c should return Counter64 normally
2677        let agent = test_agent_with_counter64().await;
2678
2679        let ctx = test_ctx(); // v2c by default
2680
2681        let pdu = Pdu {
2682            pdu_type: PduType::GetRequest,
2683            request_id: 1,
2684            error_status: 0,
2685            error_index: 0,
2686            varbinds: vec![VarBind::new(
2687                oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2688                Value::Null,
2689            )],
2690        };
2691
2692        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2693        assert_eq!(response.error_status, 0);
2694        assert!(matches!(response.varbinds[0].value, Value::Counter64(_)));
2695    }
2696
2697    #[tokio::test]
2698    async fn test_getbulk_respects_v3_msg_max_size() {
2699        // When msg_max_size is set (V3 request), GETBULK should limit the
2700        // response to fit within min(agent_max, client_msg_max_size).
2701        // The agent has a large max_message_size, but the client advertises
2702        // a small msgMaxSize that can only fit a few varbinds.
2703        let agent = Agent::builder()
2704            .bind("127.0.0.1:0")
2705            .community(b"public")
2706            .max_message_size(65507) // agent allows large responses
2707            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
2708            .build()
2709            .await
2710            .unwrap();
2711
2712        // First, get the full response without msg_max_size limit
2713        let mut ctx_unlimited = test_ctx();
2714        ctx_unlimited.pdu_type = PduType::GetBulkRequest;
2715        ctx_unlimited.msg_max_size = None;
2716
2717        let pdu = Pdu {
2718            pdu_type: PduType::GetBulkRequest,
2719            request_id: 1,
2720            error_status: 0, // non_repeaters
2721            error_index: 10, // max_repetitions
2722            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2723        };
2724
2725        let full_response = agent.dispatch_request(&ctx_unlimited, &pdu).await.unwrap();
2726        let full_count = full_response
2727            .varbinds
2728            .iter()
2729            .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2730            .count();
2731        assert!(
2732            full_count >= 3,
2733            "expected at least 3 data varbinds without limit, got {full_count}"
2734        );
2735
2736        // Now set a small msg_max_size that limits the response.
2737        // RESPONSE_OVERHEAD is 100, and each varbind for OIDs like
2738        // .1.3.6.1.4.1.99999.N.0 with Integer value is ~22 bytes.
2739        // Set msg_max_size to fit overhead + ~2 varbinds but not all 5.
2740        let mut ctx_limited = test_ctx();
2741        ctx_limited.pdu_type = PduType::GetBulkRequest;
2742        ctx_limited.msg_max_size = Some(150); // overhead(100) + room for ~2 varbinds
2743
2744        let limited_response = agent.dispatch_request(&ctx_limited, &pdu).await.unwrap();
2745        let limited_count = limited_response
2746            .varbinds
2747            .iter()
2748            .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2749            .count();
2750
2751        assert!(
2752            limited_count < full_count,
2753            "V3 msg_max_size should limit response: got {limited_count} varbinds (unlimited: {full_count})"
2754        );
2755        assert!(
2756            limited_count > 0,
2757            "should still return at least one varbind"
2758        );
2759    }
2760
2761    #[tokio::test]
2762    async fn test_response_overhead_scales_with_v3_security_level() {
2763        // A 17-octet engine ID is carried twice (authoritative + context).
2764        let engine_id = vec![0x11u8; 17];
2765        let agent = Agent::builder()
2766            .bind("127.0.0.1:0")
2767            .community(b"public")
2768            .engine_id(engine_id.clone())
2769            .build()
2770            .await
2771            .unwrap();
2772
2773        // v1/v2c: base overhead plus the echoed community string, unaffected by
2774        // the security level field.
2775        let v2c = test_ctx();
2776        assert_eq!(
2777            agent.response_overhead(&v2c),
2778            RESPONSE_OVERHEAD + v2c.security_name.len()
2779        );
2780
2781        let username = Bytes::from_static(b"user");
2782        let variable = 2 * engine_id.len() + username.len(); // context name empty
2783
2784        let mut noauth = test_ctx();
2785        noauth.version = Version::V3;
2786        noauth.security_level = SecurityLevel::NoAuthNoPriv;
2787        noauth.security_name = username.clone();
2788        assert_eq!(
2789            agent.response_overhead(&noauth),
2790            RESPONSE_OVERHEAD + variable
2791        );
2792
2793        let mut authnopriv = noauth.clone();
2794        authnopriv.security_level = SecurityLevel::AuthNoPriv;
2795        assert_eq!(
2796            agent.response_overhead(&authnopriv),
2797            RESPONSE_OVERHEAD + variable + V3_AUTH_OVERHEAD
2798        );
2799
2800        let mut authpriv = noauth.clone();
2801        authpriv.security_level = SecurityLevel::AuthPriv;
2802        assert_eq!(
2803            agent.response_overhead(&authpriv),
2804            RESPONSE_OVERHEAD + variable + V3_AUTH_OVERHEAD + V3_PRIV_OVERHEAD
2805        );
2806
2807        // Overhead is monotonic in the wrapper cost.
2808        assert!(agent.response_overhead(&v2c) < agent.response_overhead(&noauth));
2809        assert!(agent.response_overhead(&noauth) < agent.response_overhead(&authnopriv));
2810        assert!(agent.response_overhead(&authnopriv) < agent.response_overhead(&authpriv));
2811    }
2812
2813    #[tokio::test]
2814    async fn test_response_overhead_counts_community_length() {
2815        let agent = Agent::builder()
2816            .bind("127.0.0.1:0")
2817            .community(b"public")
2818            .build()
2819            .await
2820            .unwrap();
2821
2822        // A long, operator-configured community is echoed in the v1/v2c
2823        // response wrapper and must be reflected in the overhead estimate so
2824        // response_fits does not accept a Response that then exceeds the size
2825        // limit (silent drop) instead of returning tooBig.
2826        let short = test_ctx();
2827        let mut long = test_ctx();
2828        long.security_name = Bytes::from(vec![b'x'; 200]);
2829
2830        assert_eq!(
2831            agent.response_overhead(&long) - agent.response_overhead(&short),
2832            long.security_name.len() - short.security_name.len()
2833        );
2834
2835        // With a single varbind sized to fit only when the community length is
2836        // ignored, the long community must flip response_fits to false.
2837        let vb = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::Integer(0));
2838        let max = RESPONSE_OVERHEAD + short.security_name.len() + vb.encoded_size();
2839        assert!(Agent::response_fits(
2840            std::slice::from_ref(&vb),
2841            agent.response_overhead(&short),
2842            max
2843        ));
2844        assert!(!Agent::response_fits(
2845            std::slice::from_ref(&vb),
2846            agent.response_overhead(&long),
2847            max
2848        ));
2849    }
2850
2851    #[tokio::test]
2852    async fn test_getbulk_authpriv_budgets_for_wrapper() {
2853        // For the same advertised msgMaxSize, an authPriv v3 request must
2854        // reserve more space for the USM/scopedPDU wrapper than a v2c request,
2855        // so it fits strictly fewer varbinds. Under the old fixed overhead both
2856        // budgeted identically and the authPriv Response could exceed the limit.
2857        let agent = Agent::builder()
2858            .bind("127.0.0.1:0")
2859            .community(b"public")
2860            .max_message_size(65507)
2861            .engine_id(vec![0x11u8; 17])
2862            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
2863            .build()
2864            .await
2865            .unwrap();
2866
2867        let pdu = Pdu {
2868            pdu_type: PduType::GetBulkRequest,
2869            request_id: 1,
2870            error_status: 0, // non_repeaters
2871            error_index: 10, // max_repetitions
2872            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2873        };
2874
2875        // A limit large enough to expose the difference: v2c fits more varbinds
2876        // than authPriv because authPriv's overhead is larger.
2877        let limit = 200;
2878
2879        let mut v2c = test_ctx();
2880        v2c.pdu_type = PduType::GetBulkRequest;
2881        v2c.msg_max_size = Some(limit);
2882        let v2c_count = agent
2883            .dispatch_request(&v2c, &pdu)
2884            .await
2885            .unwrap()
2886            .varbinds
2887            .iter()
2888            .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2889            .count();
2890
2891        let mut authpriv = test_ctx();
2892        authpriv.version = Version::V3;
2893        authpriv.security_level = SecurityLevel::AuthPriv;
2894        authpriv.security_name = Bytes::from_static(b"user");
2895        authpriv.pdu_type = PduType::GetBulkRequest;
2896        authpriv.msg_max_size = Some(limit);
2897        let authpriv_count = agent
2898            .dispatch_request(&authpriv, &pdu)
2899            .await
2900            .unwrap()
2901            .varbinds
2902            .iter()
2903            .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2904            .count();
2905
2906        assert!(
2907            authpriv_count < v2c_count,
2908            "authPriv should budget fewer varbinds than v2c for the same \
2909             msgMaxSize: authpriv={authpriv_count}, v2c={v2c_count}"
2910        );
2911    }
2912
2913    // Handler with two large non-repeater values under .99999.1.0 and
2914    // .99999.2.0, and a small repeater value under .99999.9.0.
2915    struct MixedSizeHandler;
2916
2917    impl MibHandler for MixedSizeHandler {
2918        fn get<'a>(
2919            &'a self,
2920            _ctx: &'a RequestContext,
2921            oid: &'a Oid,
2922        ) -> BoxFuture<'a, HandlerResult<GetResult>> {
2923            Box::pin(async move {
2924                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0)
2925                    || oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)
2926                {
2927                    return Ok(GetResult::Value(Value::OctetString(Bytes::from(vec![
2928                        0xAB;
2929                        200
2930                    ]))));
2931                }
2932                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 9, 0) {
2933                    return Ok(GetResult::Value(Value::Integer(7)));
2934                }
2935                Ok(GetResult::NoSuchObject)
2936            })
2937        }
2938
2939        fn get_next<'a>(
2940            &'a self,
2941            _ctx: &'a RequestContext,
2942            oid: &'a Oid,
2943        ) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
2944            Box::pin(async move {
2945                let big1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
2946                let big2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
2947                let small = oid!(1, 3, 6, 1, 4, 1, 99999, 9, 0);
2948                if oid < &big1 {
2949                    return Ok(GetNextResult::Value(VarBind::new(
2950                        big1,
2951                        Value::OctetString(Bytes::from(vec![0xAB; 200])),
2952                    )));
2953                }
2954                if oid < &big2 {
2955                    return Ok(GetNextResult::Value(VarBind::new(
2956                        big2,
2957                        Value::OctetString(Bytes::from(vec![0xAB; 200])),
2958                    )));
2959                }
2960                if oid < &small {
2961                    return Ok(GetNextResult::Value(VarBind::new(small, Value::Integer(7))));
2962                }
2963                Ok(GetNextResult::EndOfMibView)
2964            })
2965        }
2966    }
2967
2968    #[tokio::test]
2969    async fn test_getbulk_dropped_non_repeater_omits_repeaters() {
2970        // RFC 3416 Section 4.2.3: truncation removes variable bindings from the
2971        // END of the positional set. Repeaters are positionally after all
2972        // non-repeaters, so if a non-repeater does not fit, no repeater binding
2973        // may appear in the response. Regression test for the fall-through bug
2974        // where a dropped non-repeater let repeater varbinds bleed into its slot.
2975        let agent = Agent::builder()
2976            .bind("127.0.0.1:0")
2977            .community(b"public")
2978            .max_message_size(65507)
2979            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(MixedSizeHandler))
2980            .without_builtin_handlers()
2981            .build()
2982            .await
2983            .unwrap();
2984
2985        // Size the limit so the first (big) non-repeater fits, the second (big)
2986        // does not, but a small repeater varbind WOULD fit if it were reached.
2987        let big_vb = VarBind::new(
2988            oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2989            Value::OctetString(Bytes::from(vec![0xAB; 200])),
2990        );
2991        let small_vb = VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 9, 0), Value::Integer(7));
2992        let max = RESPONSE_OVERHEAD + big_vb.encoded_size() + small_vb.encoded_size();
2993
2994        let mut ctx = test_ctx();
2995        ctx.pdu_type = PduType::GetBulkRequest;
2996        ctx.msg_max_size = Some(max as u32);
2997
2998        let pdu = Pdu {
2999            pdu_type: PduType::GetBulkRequest,
3000            request_id: 1,
3001            error_status: 2, // non_repeaters
3002            error_index: 2,  // max_repetitions
3003            varbinds: vec![
3004                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 1), Value::Null),
3005                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 2), Value::Null),
3006                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 9), Value::Null),
3007            ],
3008        };
3009
3010        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3011
3012        // Only the first non-repeater fit; the response is exactly that prefix.
3013        assert_eq!(
3014            response.varbinds.len(),
3015            1,
3016            "expected exactly the non-repeater prefix, got {:?}",
3017            response
3018                .varbinds
3019                .iter()
3020                .map(|vb| &vb.oid)
3021                .collect::<Vec<_>>()
3022        );
3023        assert_eq!(
3024            response.varbinds[0].oid,
3025            oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0)
3026        );
3027        // The repeater varbind must not have bled into the dropped slot.
3028        assert!(
3029            !response
3030                .varbinds
3031                .iter()
3032                .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 9, 0)),
3033            "repeater varbind leaked into response after a dropped non-repeater"
3034        );
3035    }
3036
3037    #[tokio::test]
3038    async fn test_getbulk_too_big_has_empty_varbinds() {
3039        // RFC 3416 Section 4.2: a tooBig Response has an empty variable-bindings
3040        // field. When not even the first GETBULK varbind fits, respond tooBig.
3041        let agent = Agent::builder()
3042            .bind("127.0.0.1:0")
3043            .community(b"public")
3044            .max_message_size(65507)
3045            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(MixedSizeHandler))
3046            .without_builtin_handlers()
3047            .build()
3048            .await
3049            .unwrap();
3050
3051        let mut ctx = test_ctx();
3052        ctx.pdu_type = PduType::GetBulkRequest;
3053        // Below RESPONSE_OVERHEAD, so even the first varbind cannot fit.
3054        ctx.msg_max_size = Some((RESPONSE_OVERHEAD - 1) as u32);
3055
3056        let pdu = Pdu {
3057            pdu_type: PduType::GetBulkRequest,
3058            request_id: 1,
3059            error_status: 2, // non_repeaters
3060            error_index: 2,  // max_repetitions
3061            varbinds: vec![
3062                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 1), Value::Null),
3063                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 2), Value::Null),
3064                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 9), Value::Null),
3065            ],
3066        };
3067
3068        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3069
3070        assert_eq!(response.error_status, ErrorStatus::TooBig.as_i32());
3071        assert!(
3072            response.varbinds.is_empty(),
3073            "tooBig Response must have empty varbinds, got {}",
3074            response.varbinds.len()
3075        );
3076    }
3077
3078    #[tokio::test]
3079    async fn test_getbulk_too_big_zero_non_repeaters_first_repeater_oversized() {
3080        // RFC 3416 Section 4.2.3 / net-snmp: for the common GETBULK shape
3081        // non_repeaters == 0, when the FIRST repeater varbind does not fit the
3082        // size limit, respond tooBig with empty varbinds (not a bare
3083        // noError+empty response, which a manager cannot distinguish from
3084        // end-of-MIB). Regression test for the repeater-loop `break 'outer`
3085        // path that returned error_status 0 with empty varbinds.
3086        let agent = Agent::builder()
3087            .bind("127.0.0.1:0")
3088            .community(b"public")
3089            .max_message_size(65507)
3090            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(MixedSizeHandler))
3091            .without_builtin_handlers()
3092            .build()
3093            .await
3094            .unwrap();
3095
3096        // The first repeater get_next from .99999.1 returns big1 (200-byte
3097        // OctetString). Size the limit above RESPONSE_OVERHEAD (so this is not
3098        // the trivial below-overhead case) but below what big1 needs, so big1
3099        // is the first varbind and does not fit.
3100        let big_vb = VarBind::new(
3101            oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
3102            Value::OctetString(Bytes::from(vec![0xAB; 200])),
3103        );
3104        let max = RESPONSE_OVERHEAD + big_vb.encoded_size() - 1;
3105
3106        let mut ctx = test_ctx();
3107        ctx.pdu_type = PduType::GetBulkRequest;
3108        ctx.msg_max_size = Some(max as u32);
3109
3110        let pdu = Pdu {
3111            pdu_type: PduType::GetBulkRequest,
3112            request_id: 1,
3113            error_status: 0, // non_repeaters == 0
3114            error_index: 5,  // max_repetitions
3115            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 1), Value::Null)],
3116        };
3117
3118        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3119
3120        assert_eq!(
3121            response.error_status,
3122            ErrorStatus::TooBig.as_i32(),
3123            "first oversized repeater varbind (non_repeaters == 0) must yield tooBig"
3124        );
3125        assert!(
3126            response.varbinds.is_empty(),
3127            "tooBig Response must have empty varbinds, got {}",
3128            response.varbinds.len()
3129        );
3130    }
3131
3132    #[tokio::test]
3133    async fn test_getbulk_msg_max_size_none_uses_agent_max() {
3134        // Without msg_max_size (v1/v2c), the agent's own max_message_size is used.
3135        // With a large agent max, all 5 OIDs should be returned.
3136        let agent = Agent::builder()
3137            .bind("127.0.0.1:0")
3138            .community(b"public")
3139            .max_message_size(65507)
3140            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
3141            .without_builtin_handlers()
3142            .build()
3143            .await
3144            .unwrap();
3145
3146        let mut ctx = test_ctx();
3147        ctx.pdu_type = PduType::GetBulkRequest;
3148        ctx.msg_max_size = None; // v2c, no client limit
3149
3150        let pdu = Pdu {
3151            pdu_type: PduType::GetBulkRequest,
3152            request_id: 1,
3153            error_status: 0,
3154            error_index: 10,
3155            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
3156        };
3157
3158        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3159        let data_count = response
3160            .varbinds
3161            .iter()
3162            .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
3163            .count();
3164        assert_eq!(
3165            data_count, 5,
3166            "all 5 OIDs should be returned without msg_max_size limit"
3167        );
3168    }
3169
3170    #[tokio::test]
3171    async fn test_v1_getnext_skips_counter64() {
3172        // RFC 2576 Section 4.1.2.3: Counter64 skipped in v1 GETNEXT.
3173        // Walking from .99999 should skip the Counter64 at .99999.1.0
3174        // and return the Integer at .99999.2.0.
3175        let agent = test_agent_with_counter64().await;
3176
3177        let mut ctx = test_ctx();
3178        ctx.version = Version::V1;
3179        ctx.security_model = SecurityModel::V1;
3180        ctx.pdu_type = PduType::GetNextRequest;
3181
3182        let pdu = Pdu {
3183            pdu_type: PduType::GetNextRequest,
3184            request_id: 1,
3185            error_status: 0,
3186            error_index: 0,
3187            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
3188        };
3189
3190        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3191        assert_eq!(response.error_status, 0, "should succeed");
3192        assert_eq!(
3193            response.varbinds[0].oid,
3194            oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0),
3195            "should skip Counter64 and return next non-Counter64 OID"
3196        );
3197        assert!(matches!(response.varbinds[0].value, Value::Integer(42)));
3198    }
3199
3200    #[test]
3201    fn test_engine_time_no_overflow() {
3202        // Normal operation: elapsed < MAX_ENGINE_TIME, boots stays at base
3203        let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
3204        assert_eq!(boots, 1);
3205        assert_eq!(time, 1000);
3206    }
3207
3208    #[test]
3209    fn test_engine_time_zero_elapsed() {
3210        let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
3211        assert_eq!(boots, 1);
3212        assert_eq!(time, 0);
3213    }
3214
3215    #[test]
3216    fn test_engine_time_just_below_max() {
3217        let max = crate::v3::MAX_ENGINE_TIME;
3218        let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max) - 1);
3219        assert_eq!(boots, 1);
3220        assert_eq!(time, max - 1);
3221    }
3222
3223    #[test]
3224    fn test_engine_time_at_max_wraps() {
3225        // Exactly at MAX_ENGINE_TIME seconds: boots increments, time resets to 0
3226        let max = crate::v3::MAX_ENGINE_TIME;
3227        let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max));
3228        assert_eq!(
3229            boots, 2,
3230            "boots should increment when elapsed reaches MAX_ENGINE_TIME"
3231        );
3232        assert_eq!(time, 0, "time should wrap to 0");
3233    }
3234
3235    #[test]
3236    fn test_engine_time_past_max() {
3237        // 500 seconds past the first wrap
3238        let max = crate::v3::MAX_ENGINE_TIME;
3239        let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max) + 500);
3240        assert_eq!(boots, 2);
3241        assert_eq!(time, 500);
3242    }
3243
3244    #[test]
3245    fn test_engine_time_multiple_wraps() {
3246        // Three full cycles
3247        let max = crate::v3::MAX_ENGINE_TIME;
3248        let elapsed = u64::from(max) * 3 + 42;
3249        let (boots, time) = crate::v3::compute_engine_boots_time(1, elapsed);
3250        assert_eq!(boots, 4, "base 1 + 3 wraps = 4");
3251        assert_eq!(time, 42);
3252    }
3253
3254    #[test]
3255    fn test_engine_time_boots_capped_at_max() {
3256        // If enough wraps happen that boots would exceed MAX_ENGINE_TIME, cap it
3257        let max = crate::v3::MAX_ENGINE_TIME;
3258        let elapsed = u64::from(max) * u64::from(max); // way more wraps than max allows
3259        let (boots, _time) = crate::v3::compute_engine_boots_time(1, elapsed);
3260        assert_eq!(boots, max, "boots should be capped at MAX_ENGINE_TIME");
3261    }
3262
3263    #[test]
3264    fn test_engine_time_base_boots_preserved() {
3265        // A non-1 base boots (e.g. from persistence) is respected
3266        let max = crate::v3::MAX_ENGINE_TIME;
3267        let (boots, time) = crate::v3::compute_engine_boots_time(5, u64::from(max) + 100);
3268        assert_eq!(boots, 6, "base 5 + 1 wrap = 6");
3269        assert_eq!(time, 100);
3270    }
3271
3272    #[test]
3273    fn test_engine_time_high_base_boots_capped() {
3274        // Base boots near MAX_ENGINE_TIME with a wrap should cap
3275        let max = crate::v3::MAX_ENGINE_TIME;
3276        let (boots, _time) = crate::v3::compute_engine_boots_time(max - 1, u64::from(max) * 2);
3277        assert_eq!(boots, max, "should cap at MAX_ENGINE_TIME, not overflow");
3278    }
3279
3280    #[tokio::test]
3281    async fn test_engine_boots_builder() {
3282        // engine_boots builder method sets the initial boots value
3283        let agent = Agent::builder()
3284            .bind("127.0.0.1:0")
3285            .community(b"public")
3286            .engine_boots(42)
3287            .build()
3288            .await
3289            .unwrap();
3290
3291        assert_eq!(agent.engine_boots(), 42);
3292    }
3293
3294    #[tokio::test]
3295    async fn test_zero_max_concurrent_requests_rejected() {
3296        // A zero-permit concurrency limit would never grant a permit and wedge
3297        // the agent on the first packet, so the builder must reject it.
3298        let result = Agent::builder()
3299            .bind("127.0.0.1:0")
3300            .community(b"public")
3301            .max_concurrent_requests(Some(0))
3302            .build()
3303            .await;
3304
3305        let err = result.err().expect("expected build to fail");
3306        assert!(matches!(*err, Error::Config(_)));
3307    }
3308
3309    #[tokio::test]
3310    async fn test_engine_boots_default() {
3311        // Default engine_boots is 1
3312        let agent = Agent::builder()
3313            .bind("127.0.0.1:0")
3314            .community(b"public")
3315            .build()
3316            .await
3317            .unwrap();
3318
3319        assert_eq!(agent.engine_boots(), 1);
3320    }
3321
3322    #[tokio::test]
3323    async fn test_usm_counter_accessors_default_zero() {
3324        let agent = Agent::builder()
3325            .bind("127.0.0.1:0")
3326            .community(b"public")
3327            .build()
3328            .await
3329            .unwrap();
3330
3331        assert_eq!(agent.usm_unsupported_sec_levels(), 0);
3332        assert_eq!(agent.usm_decryption_errors(), 0);
3333    }
3334
3335    #[test]
3336    fn test_builtin_mib_without_single() {
3337        let builder = AgentBuilder::new().without_builtin_handler(BuiltinMib::UsmStats);
3338        assert!(builder.disabled_builtins.contains(&BuiltinMib::UsmStats));
3339        assert!(!builder.disabled_builtins.contains(&BuiltinMib::SnmpEngine));
3340        assert!(!builder.disabled_builtins.contains(&BuiltinMib::MpdStats));
3341    }
3342
3343    #[test]
3344    fn test_builtin_mib_without_all() {
3345        let builder = AgentBuilder::new().without_builtin_handlers();
3346        assert!(builder.disabled_builtins.contains(&BuiltinMib::SnmpEngine));
3347        assert!(builder.disabled_builtins.contains(&BuiltinMib::UsmStats));
3348        assert!(builder.disabled_builtins.contains(&BuiltinMib::MpdStats));
3349    }
3350
3351    #[tokio::test]
3352    async fn test_uptime_hundredths() {
3353        let agent = Agent::builder()
3354            .bind("127.0.0.1:0")
3355            .community(b"public")
3356            .build()
3357            .await
3358            .unwrap();
3359
3360        let uptime = agent.uptime_hundredths();
3361        assert!(
3362            uptime < 100,
3363            "uptime should be less than 1 second, got {uptime}"
3364        );
3365
3366        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3367        let uptime2 = agent.uptime_hundredths();
3368        assert!(uptime2 > uptime, "uptime should increase after delay");
3369    }
3370
3371    #[tokio::test]
3372    async fn test_builtin_handlers_registered_by_default() {
3373        let agent = Agent::builder()
3374            .bind("127.0.0.1:0")
3375            .community(b"public")
3376            .build()
3377            .await
3378            .unwrap();
3379
3380        let ctx = test_ctx();
3381
3382        // snmpEngineMaxMessageSize.0 should be queryable
3383        let handler = agent
3384            .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 4, 0))
3385            .expect("snmpEngine handler should be registered");
3386        let get_result = handler
3387            .handler
3388            .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 4, 0))
3389            .await
3390            .unwrap();
3391        assert!(matches!(get_result, GetResult::Value(Value::Integer(_))));
3392
3393        // usmStatsWrongDigests.0 should be queryable
3394        let handler = agent
3395            .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0))
3396            .expect("USM stats handler should be registered");
3397        let get_result = handler
3398            .handler
3399            .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0))
3400            .await
3401            .unwrap();
3402        assert!(matches!(get_result, GetResult::Value(Value::Counter32(0))));
3403
3404        // snmpUnknownSecurityModels.0 should be queryable
3405        let handler = agent
3406            .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
3407            .expect("MPD stats handler should be registered");
3408        let get_result = handler
3409            .handler
3410            .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
3411            .await
3412            .unwrap();
3413        assert!(matches!(get_result, GetResult::Value(Value::Counter32(0))));
3414    }
3415
3416    #[tokio::test]
3417    async fn test_builtin_handlers_disabled() {
3418        let agent = Agent::builder()
3419            .bind("127.0.0.1:0")
3420            .community(b"public")
3421            .without_builtin_handlers()
3422            .build()
3423            .await
3424            .unwrap();
3425
3426        assert!(
3427            agent
3428                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0))
3429                .is_none()
3430        );
3431        assert!(
3432            agent
3433                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0))
3434                .is_none()
3435        );
3436        assert!(
3437            agent
3438                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
3439                .is_none()
3440        );
3441    }
3442
3443    #[tokio::test]
3444    async fn test_builtin_handler_selective_disable() {
3445        let agent = Agent::builder()
3446            .bind("127.0.0.1:0")
3447            .community(b"public")
3448            .without_builtin_handler(BuiltinMib::UsmStats)
3449            .build()
3450            .await
3451            .unwrap();
3452
3453        assert!(
3454            agent
3455                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0))
3456                .is_some()
3457        );
3458        assert!(
3459            agent
3460                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0))
3461                .is_none()
3462        );
3463        assert!(
3464            agent
3465                .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
3466                .is_some()
3467        );
3468    }
3469
3470    // Build an agent whose effective response size limit only fits a couple of
3471    // varbinds, used to exercise the RFC 3416 tooBig paths for GET/GETNEXT.
3472    async fn small_limit_agent() -> Agent {
3473        Agent::builder()
3474            .bind("127.0.0.1:0")
3475            .community(b"public")
3476            .max_message_size(150)
3477            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
3478            .without_builtin_handlers()
3479            .build()
3480            .await
3481            .unwrap()
3482    }
3483
3484    fn five_varbinds() -> Vec<VarBind> {
3485        (1u32..=5)
3486            .map(|i| VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, i, 0), Value::Null))
3487            .collect()
3488    }
3489
3490    #[tokio::test]
3491    async fn test_get_too_big_returns_toobig_response() {
3492        let agent = small_limit_agent().await;
3493        let ctx = test_ctx();
3494
3495        // GET for all five OIDs; the response cannot fit within the 150-byte
3496        // effective limit, so RFC 3416 Section 4.2.1 requires a tooBig Response.
3497        let pdu = Pdu {
3498            pdu_type: PduType::GetRequest,
3499            request_id: 1,
3500            error_status: 0,
3501            error_index: 0,
3502            varbinds: five_varbinds(),
3503        };
3504
3505        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3506        assert_eq!(response.error_status, ErrorStatus::TooBig.as_i32());
3507        assert_eq!(response.error_index, 0);
3508        assert!(response.varbinds.is_empty());
3509    }
3510
3511    #[tokio::test]
3512    async fn test_get_too_big_v1_echoes_request_varbinds() {
3513        let agent = small_limit_agent().await;
3514
3515        // SNMPv1 (RFC 1157 Sections 4.1.2-4.1.4): a tooBig Response echoes the
3516        // original request's variable bindings, unlike v2c/v3 which clear them.
3517        let mut ctx = test_ctx();
3518        ctx.version = Version::V1;
3519        ctx.security_model = SecurityModel::V1;
3520
3521        let request_varbinds = five_varbinds();
3522        let pdu = Pdu {
3523            pdu_type: PduType::GetRequest,
3524            request_id: 1,
3525            error_status: 0,
3526            error_index: 0,
3527            varbinds: request_varbinds.clone(),
3528        };
3529
3530        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3531        assert_eq!(response.error_status, ErrorStatus::TooBig.as_i32());
3532        assert_eq!(response.error_index, 0);
3533        assert_eq!(response.varbinds, request_varbinds);
3534
3535        // The same oversized request under v2c must still clear the varbinds.
3536        let v2c_response = agent.dispatch_request(&test_ctx(), &pdu).await.unwrap();
3537        assert_eq!(v2c_response.error_status, ErrorStatus::TooBig.as_i32());
3538        assert!(v2c_response.varbinds.is_empty());
3539    }
3540
3541    #[tokio::test]
3542    async fn test_get_within_limit_returns_response() {
3543        let agent = small_limit_agent().await;
3544        let ctx = test_ctx();
3545
3546        // A single varbind fits comfortably; the tooBig check must not fire.
3547        let pdu = Pdu {
3548            pdu_type: PduType::GetRequest,
3549            request_id: 1,
3550            error_status: 0,
3551            error_index: 0,
3552            varbinds: vec![VarBind::new(
3553                oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
3554                Value::Null,
3555            )],
3556        };
3557
3558        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3559        assert_eq!(response.error_status, 0);
3560        assert_eq!(response.varbinds.len(), 1);
3561        assert!(matches!(response.varbinds[0].value, Value::Integer(1)));
3562    }
3563
3564    #[tokio::test]
3565    async fn test_getnext_too_big_returns_toobig_response() {
3566        let agent = small_limit_agent().await;
3567        let mut ctx = test_ctx();
3568        ctx.pdu_type = PduType::GetNextRequest;
3569
3570        let pdu = Pdu {
3571            pdu_type: PduType::GetNextRequest,
3572            request_id: 1,
3573            error_status: 0,
3574            error_index: 0,
3575            varbinds: five_varbinds(),
3576        };
3577
3578        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3579        assert_eq!(response.error_status, ErrorStatus::TooBig.as_i32());
3580        assert_eq!(response.error_index, 0);
3581        assert!(response.varbinds.is_empty());
3582    }
3583
3584    #[tokio::test]
3585    async fn test_inform_too_big_returns_toobig_response() {
3586        let agent = small_limit_agent().await;
3587        let mut ctx = test_ctx();
3588        ctx.pdu_type = PduType::InformRequest;
3589
3590        // An InformRequest whose echoed Response would exceed the 150-byte
3591        // effective limit. RFC 3416 Section 4.2.7 (confirmed-class) requires a
3592        // fitting tooBig acknowledgement rather than silently dropping the
3593        // oversized echo, which would make a confirmed-class sender retry
3594        // indefinitely.
3595        let big = Value::OctetString(Bytes::from(vec![0xABu8; 256]));
3596        let pdu = Pdu {
3597            pdu_type: PduType::InformRequest,
3598            request_id: 1,
3599            error_status: 0,
3600            error_index: 0,
3601            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0), big)],
3602        };
3603
3604        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3605        assert_eq!(response.error_status, ErrorStatus::TooBig.as_i32());
3606        assert_eq!(response.error_index, 0);
3607        assert!(response.varbinds.is_empty());
3608    }
3609
3610    #[tokio::test]
3611    async fn test_inform_within_limit_echoes_varbinds() {
3612        let agent = small_limit_agent().await;
3613        let mut ctx = test_ctx();
3614        ctx.pdu_type = PduType::InformRequest;
3615
3616        // A small Inform fits within the limit and is acknowledged by echoing
3617        // the same varbinds in a Response.
3618        let pdu = Pdu {
3619            pdu_type: PduType::InformRequest,
3620            request_id: 7,
3621            error_status: 0,
3622            error_index: 0,
3623            varbinds: vec![VarBind::new(
3624                oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
3625                Value::Integer(42),
3626            )],
3627        };
3628
3629        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3630        assert_eq!(response.pdu_type, PduType::Response);
3631        assert_eq!(response.error_status, 0);
3632        assert_eq!(response.request_id, 7);
3633        assert_eq!(response.varbinds.len(), 1);
3634        assert!(matches!(response.varbinds[0].value, Value::Integer(42)));
3635    }
3636
3637    #[tokio::test]
3638    async fn test_getnext_within_limit_returns_response() {
3639        let agent = small_limit_agent().await;
3640        let mut ctx = test_ctx();
3641        ctx.pdu_type = PduType::GetNextRequest;
3642
3643        let pdu = Pdu {
3644            pdu_type: PduType::GetNextRequest,
3645            request_id: 1,
3646            error_status: 0,
3647            error_index: 0,
3648            varbinds: vec![VarBind::new(
3649                oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
3650                Value::Null,
3651            )],
3652        };
3653
3654        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3655        assert_eq!(response.error_status, 0);
3656        assert_eq!(response.varbinds.len(), 1);
3657        assert_eq!(
3658            response.varbinds[0].oid,
3659            oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)
3660        );
3661    }
3662}