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