Skip to main content

async_snmp/agent/
mod.rs

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