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.
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) per RFC 3416
10//! - **VACM support**: Optional View-based Access Control Model (RFC 3415)
11//!
12//! # Example
13//!
14//! ```rust,no_run
15//! use async_snmp::agent::Agent;
16//! use async_snmp::handler::{MibHandler, RequestContext, GetResult, GetNextResult, BoxFuture};
17//! use async_snmp::{Oid, Value, VarBind, oid};
18//! use std::sync::Arc;
19//!
20//! // Define a simple handler for the system MIB subtree
21//! struct SystemMibHandler;
22//!
23//! impl MibHandler for SystemMibHandler {
24//!     fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
25//!         Box::pin(async move {
26//!             // sysDescr.0
27//!             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {
28//!                 return GetResult::Value(Value::OctetString("My SNMP Agent".into()));
29//!             }
30//!             // sysObjectID.0
31//!             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 2, 0) {
32//!                 return GetResult::Value(Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 99999)));
33//!             }
34//!             GetResult::NoSuchObject
35//!         })
36//!     }
37//!
38//!     fn get_next<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetNextResult> {
39//!         Box::pin(async move {
40//!             // Return the lexicographically next OID after the given one
41//!             let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
42//!             let sys_object_id = oid!(1, 3, 6, 1, 2, 1, 1, 2, 0);
43//!
44//!             if oid < &sys_descr {
45//!                 return GetNextResult::Value(VarBind::new(sys_descr, Value::OctetString("My SNMP Agent".into())));
46//!             }
47//!             if oid < &sys_object_id {
48//!                 return GetNextResult::Value(VarBind::new(sys_object_id, Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 99999))));
49//!             }
50//!             GetNextResult::EndOfMibView
51//!         })
52//!     }
53//! }
54//!
55//! #[tokio::main]
56//! async fn main() -> Result<(), Box<async_snmp::Error>> {
57//!     let agent = Agent::builder()
58//!         .bind("0.0.0.0:161")
59//!         .community(b"public")
60//!         .handler(oid!(1, 3, 6, 1, 2, 1, 1), Arc::new(SystemMibHandler))
61//!         .build()
62//!         .await?;
63//!
64//!     agent.run().await
65//! }
66//! ```
67
68mod request;
69mod response;
70mod set_handler;
71pub mod vacm;
72
73pub use vacm::{SecurityModel, VacmBuilder, VacmConfig, View, ViewCheckResult, ViewSubtree};
74
75use std::collections::HashMap;
76use std::net::SocketAddr;
77use std::sync::Arc;
78use std::sync::atomic::{AtomicU32, Ordering};
79use std::time::Instant;
80
81use bytes::Bytes;
82use subtle::ConstantTimeEq;
83use tokio::net::UdpSocket;
84use tokio::sync::Semaphore;
85use tokio_util::sync::CancellationToken;
86use tracing::instrument;
87
88use std::io::IoSliceMut;
89
90use quinn_udp::{RecvMeta, Transmit, UdpSockRef, UdpSocketState};
91
92use crate::ber::Decoder;
93use crate::error::internal::DecodeErrorKind;
94use crate::error::{Error, ErrorStatus, Result};
95use crate::handler::{GetNextResult, GetResult, MibHandler, RequestContext};
96use crate::notification::UsmConfig;
97use crate::oid::Oid;
98use crate::pdu::{Pdu, PduType};
99use crate::util::bind_udp_socket;
100use crate::v3::SaltCounter;
101use crate::value::Value;
102use crate::varbind::VarBind;
103use crate::version::Version;
104
105/// Default maximum message size for UDP (RFC 3417 recommendation).
106const DEFAULT_MAX_MESSAGE_SIZE: usize = 1472;
107
108/// Overhead for SNMP message encoding (approximate conservative estimate).
109/// This accounts for version, community/USM, PDU headers, etc.
110const RESPONSE_OVERHEAD: usize = 100;
111
112/// Registered handler with its OID prefix.
113pub(crate) struct RegisteredHandler {
114    pub(crate) prefix: Oid,
115    pub(crate) handler: Arc<dyn MibHandler>,
116}
117
118/// Builder for [`Agent`].
119///
120/// Use this builder to configure and construct an SNMP agent. The builder
121/// pattern allows you to chain configuration methods before calling
122/// [`build()`](AgentBuilder::build) to create the agent.
123///
124/// # Access Control
125///
126/// By default, the agent operates in **permissive mode**: any authenticated
127/// request (valid community string for v1/v2c, valid USM credentials for v3)
128/// has full read and write access to all registered handlers.
129///
130/// For production deployments, use the [`vacm()`](AgentBuilder::vacm) method
131/// to configure View-based Access Control (RFC 3415), which allows fine-grained
132/// control over which security names can access which OID subtrees.
133///
134/// # Minimal Example
135///
136/// ```rust,no_run
137/// use async_snmp::agent::Agent;
138/// use async_snmp::handler::{MibHandler, RequestContext, GetResult, GetNextResult, BoxFuture};
139/// use async_snmp::{Oid, Value, VarBind, oid};
140/// use std::sync::Arc;
141///
142/// struct MyHandler;
143/// impl MibHandler for MyHandler {
144///     fn get<'a>(&'a self, _: &'a RequestContext, _: &'a Oid) -> BoxFuture<'a, GetResult> {
145///         Box::pin(async { GetResult::NoSuchObject })
146///     }
147///     fn get_next<'a>(&'a self, _: &'a RequestContext, _: &'a Oid) -> BoxFuture<'a, GetNextResult> {
148///         Box::pin(async { GetNextResult::EndOfMibView })
149///     }
150/// }
151///
152/// # async fn example() -> Result<(), Box<async_snmp::Error>> {
153/// let agent = Agent::builder()
154///     .bind("0.0.0.0:1161")  // Use non-privileged port
155///     .community(b"public")
156///     .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(MyHandler))
157///     .build()
158///     .await?;
159/// # Ok(())
160/// # }
161/// ```
162pub struct AgentBuilder {
163    bind_addr: String,
164    communities: Vec<Vec<u8>>,
165    usm_users: HashMap<Bytes, UsmConfig>,
166    handlers: Vec<RegisteredHandler>,
167    engine_id: Option<Vec<u8>>,
168    max_message_size: usize,
169    max_concurrent_requests: Option<usize>,
170    recv_buffer_size: Option<usize>,
171    vacm: Option<VacmConfig>,
172    cancel: Option<CancellationToken>,
173}
174
175impl AgentBuilder {
176    /// Create a new builder with default settings.
177    ///
178    /// Defaults:
179    /// - Bind address: `0.0.0.0:161` (UDP)
180    /// - Max message size: 1472 bytes (Ethernet MTU - IP/UDP headers)
181    /// - Max concurrent requests: 1000
182    /// - Receive buffer size: 4MB (requested from kernel)
183    /// - No communities or USM users (all requests rejected)
184    /// - No handlers registered
185    pub fn new() -> Self {
186        Self {
187            bind_addr: "0.0.0.0:161".to_string(),
188            communities: Vec::new(),
189            usm_users: HashMap::new(),
190            handlers: Vec::new(),
191            engine_id: None,
192            max_message_size: DEFAULT_MAX_MESSAGE_SIZE,
193            max_concurrent_requests: Some(1000),
194            recv_buffer_size: Some(4 * 1024 * 1024), // 4MB
195            vacm: None,
196            cancel: None,
197        }
198    }
199
200    /// Set the UDP bind address.
201    ///
202    /// Default is `0.0.0.0:161` (standard SNMP agent port). Note that binding
203    /// to UDP port 161 typically requires root/administrator privileges.
204    ///
205    /// # IPv4 Examples
206    ///
207    /// ```rust,no_run
208    /// use async_snmp::agent::Agent;
209    ///
210    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
211    /// // Bind to all IPv4 interfaces on standard port (requires privileges)
212    /// let agent = Agent::builder().bind("0.0.0.0:161").community(b"public").build().await?;
213    ///
214    /// // Bind to localhost only on non-privileged port
215    /// let agent = Agent::builder().bind("127.0.0.1:1161").community(b"public").build().await?;
216    ///
217    /// // Bind to specific interface
218    /// let agent = Agent::builder().bind("192.168.1.100:161").community(b"public").build().await?;
219    /// # Ok(())
220    /// # }
221    /// ```
222    ///
223    /// # IPv6 / Dual-Stack Examples
224    ///
225    /// ```rust,no_run
226    /// use async_snmp::agent::Agent;
227    ///
228    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
229    /// // Bind to all interfaces (IPv6, with dual-stack on Linux)
230    /// let agent = Agent::builder().bind("[::]:161").community(b"public").build().await?;
231    ///
232    /// // Bind to IPv6 localhost only
233    /// let agent = Agent::builder().bind("[::1]:1161").community(b"public").build().await?;
234    /// # Ok(())
235    /// # }
236    /// ```
237    pub fn bind(mut self, addr: impl Into<String>) -> Self {
238        self.bind_addr = addr.into();
239        self
240    }
241
242    /// Add an accepted community string for v1/v2c requests.
243    ///
244    /// Multiple communities can be added. If none are added,
245    /// all v1/v2c requests are rejected.
246    ///
247    /// # Example
248    ///
249    /// ```rust,no_run
250    /// use async_snmp::agent::Agent;
251    ///
252    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
253    /// let agent = Agent::builder()
254    ///     .bind("0.0.0.0:1161")
255    ///     .community(b"public")   // Read-only access
256    ///     .community(b"private")  // Read-write access (with VACM)
257    ///     .build()
258    ///     .await?;
259    /// # Ok(())
260    /// # }
261    /// ```
262    pub fn community(mut self, community: &[u8]) -> Self {
263        self.communities.push(community.to_vec());
264        self
265    }
266
267    /// Add multiple community strings.
268    ///
269    /// # Example
270    ///
271    /// ```rust,no_run
272    /// use async_snmp::agent::Agent;
273    ///
274    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
275    /// let communities = ["public", "private", "monitor"];
276    /// let agent = Agent::builder()
277    ///     .bind("0.0.0.0:1161")
278    ///     .communities(communities)
279    ///     .build()
280    ///     .await?;
281    /// # Ok(())
282    /// # }
283    /// ```
284    pub fn communities<I, C>(mut self, communities: I) -> Self
285    where
286        I: IntoIterator<Item = C>,
287        C: AsRef<[u8]>,
288    {
289        for c in communities {
290            self.communities.push(c.as_ref().to_vec());
291        }
292        self
293    }
294
295    /// Add a USM user for SNMPv3 authentication.
296    ///
297    /// Configure authentication and privacy settings using the closure.
298    /// Multiple users can be added with different security levels.
299    ///
300    /// # Security Levels
301    ///
302    /// - **noAuthNoPriv**: No authentication or encryption
303    /// - **authNoPriv**: Authentication only (HMAC verification)
304    /// - **authPriv**: Authentication and encryption
305    ///
306    /// # Example
307    ///
308    /// ```rust,no_run
309    /// use async_snmp::agent::Agent;
310    /// use async_snmp::{AuthProtocol, PrivProtocol};
311    ///
312    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
313    /// let agent = Agent::builder()
314    ///     .bind("0.0.0.0:1161")
315    ///     // Read-only user with authentication only
316    ///     .usm_user("monitor", |u| {
317    ///         u.auth(AuthProtocol::Sha256, b"monitorpass123")
318    ///     })
319    ///     // Admin user with full encryption
320    ///     .usm_user("admin", |u| {
321    ///         u.auth(AuthProtocol::Sha256, b"adminauth123")
322    ///          .privacy(PrivProtocol::Aes128, b"adminpriv123")
323    ///     })
324    ///     .build()
325    ///     .await?;
326    /// # Ok(())
327    /// # }
328    /// ```
329    pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
330    where
331        F: FnOnce(UsmConfig) -> UsmConfig,
332    {
333        let username_bytes: Bytes = username.into();
334        let config = configure(UsmConfig::new(username_bytes.clone()));
335        self.usm_users.insert(username_bytes, config);
336        self
337    }
338
339    /// Set the engine ID for SNMPv3.
340    ///
341    /// If not set, a default engine ID will be generated based on the
342    /// RFC 3411 format using enterprise number and timestamp.
343    ///
344    /// # Example
345    ///
346    /// ```rust,no_run
347    /// use async_snmp::agent::Agent;
348    ///
349    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
350    /// let agent = Agent::builder()
351    ///     .bind("0.0.0.0:1161")
352    ///     .engine_id(b"\x80\x00\x00\x00\x01MyEngine".to_vec())
353    ///     .community(b"public")
354    ///     .build()
355    ///     .await?;
356    /// # Ok(())
357    /// # }
358    /// ```
359    pub fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
360        self.engine_id = Some(engine_id.into());
361        self
362    }
363
364    /// Set the maximum message size for responses.
365    ///
366    /// Default is 1472 octets (fits Ethernet MTU minus IP/UDP headers).
367    /// GETBULK responses will be truncated to fit within this limit.
368    ///
369    /// For SNMPv3 requests, the agent uses the minimum of this value
370    /// and the msgMaxSize from the request.
371    pub fn max_message_size(mut self, size: usize) -> Self {
372        self.max_message_size = size;
373        self
374    }
375
376    /// Set the maximum number of concurrent requests the agent will process.
377    ///
378    /// Default is 1000. Requests beyond this limit will queue until a slot
379    /// becomes available. Set to `None` for unbounded concurrency.
380    ///
381    /// This controls memory usage under high load while still allowing
382    /// parallel request processing.
383    pub fn max_concurrent_requests(mut self, limit: Option<usize>) -> Self {
384        self.max_concurrent_requests = limit;
385        self
386    }
387
388    /// Set the UDP socket receive buffer size.
389    ///
390    /// Default is 4MB. The kernel may cap this at `net.core.rmem_max`.
391    /// A larger buffer prevents packet loss during request bursts.
392    ///
393    /// Set to `None` to use the kernel default.
394    pub fn recv_buffer_size(mut self, size: Option<usize>) -> Self {
395        self.recv_buffer_size = size;
396        self
397    }
398
399    /// Register a MIB handler for an OID subtree.
400    ///
401    /// Handlers are matched by longest prefix. When a request comes in,
402    /// the handler with the longest matching prefix is used.
403    ///
404    /// # Example
405    ///
406    /// ```rust,no_run
407    /// use async_snmp::agent::Agent;
408    /// use async_snmp::handler::{MibHandler, RequestContext, GetResult, GetNextResult, BoxFuture};
409    /// use async_snmp::{Oid, Value, VarBind, oid};
410    /// use std::sync::Arc;
411    ///
412    /// struct SystemHandler;
413    /// impl MibHandler for SystemHandler {
414    ///     fn get<'a>(&'a self, _: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
415    ///         Box::pin(async move {
416    ///             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {
417    ///                 GetResult::Value(Value::OctetString("My Agent".into()))
418    ///             } else {
419    ///                 GetResult::NoSuchObject
420    ///             }
421    ///         })
422    ///     }
423    ///     fn get_next<'a>(&'a self, _: &'a RequestContext, _: &'a Oid) -> BoxFuture<'a, GetNextResult> {
424    ///         Box::pin(async { GetNextResult::EndOfMibView })
425    ///     }
426    /// }
427    ///
428    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
429    /// let agent = Agent::builder()
430    ///     .bind("0.0.0.0:1161")
431    ///     .community(b"public")
432    ///     // Register handler for system MIB subtree
433    ///     .handler(oid!(1, 3, 6, 1, 2, 1, 1), Arc::new(SystemHandler))
434    ///     .build()
435    ///     .await?;
436    /// # Ok(())
437    /// # }
438    /// ```
439    pub fn handler(mut self, prefix: Oid, handler: Arc<dyn MibHandler>) -> Self {
440        self.handlers.push(RegisteredHandler { prefix, handler });
441        self
442    }
443
444    /// Configure VACM (View-based Access Control Model) using a builder function.
445    ///
446    /// When VACM is configured, all requests are checked against the configured
447    /// access control rules. Requests that don't have proper access are rejected
448    /// with `noAccess` error (v2c/v3) or `noSuchName` (v1).
449    ///
450    /// **Without VACM configuration, the agent operates in permissive mode**:
451    /// any authenticated request has full read/write access to all handlers.
452    ///
453    /// # Example
454    ///
455    /// ```rust,no_run
456    /// use async_snmp::agent::{Agent, SecurityModel, VacmBuilder};
457    /// use async_snmp::message::SecurityLevel;
458    /// use async_snmp::oid;
459    ///
460    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
461    /// let agent = Agent::builder()
462    ///     .bind("0.0.0.0:161")
463    ///     .community(b"public")
464    ///     .community(b"private")
465    ///     .vacm(|v| v
466    ///         .group("public", SecurityModel::V2c, "readonly_group")
467    ///         .group("private", SecurityModel::V2c, "readwrite_group")
468    ///         .access("readonly_group", |a| a
469    ///             .read_view("full_view"))
470    ///         .access("readwrite_group", |a| a
471    ///             .read_view("full_view")
472    ///             .write_view("write_view"))
473    ///         .view("full_view", |v| v
474    ///             .include(oid!(1, 3, 6, 1)))
475    ///         .view("write_view", |v| v
476    ///             .include(oid!(1, 3, 6, 1, 2, 1, 1))))
477    ///     .build()
478    ///     .await?;
479    /// # Ok(())
480    /// # }
481    /// ```
482    pub fn vacm<F>(mut self, configure: F) -> Self
483    where
484        F: FnOnce(VacmBuilder) -> VacmBuilder,
485    {
486        let builder = VacmBuilder::new();
487        self.vacm = Some(configure(builder).build());
488        self
489    }
490
491    /// Set a cancellation token for graceful shutdown.
492    ///
493    /// If not set, the agent creates its own token accessible via `Agent::cancel()`.
494    pub fn cancel(mut self, token: CancellationToken) -> Self {
495        self.cancel = Some(token);
496        self
497    }
498
499    /// Build the agent.
500    pub async fn build(mut self) -> Result<Agent> {
501        let bind_addr: std::net::SocketAddr = self.bind_addr.parse().map_err(|_| {
502            Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
503        })?;
504
505        let socket = bind_udp_socket(bind_addr, self.recv_buffer_size)
506            .await
507            .map_err(|e| Error::Network {
508                target: bind_addr,
509                source: e,
510            })?;
511
512        let local_addr = socket.local_addr().map_err(|e| Error::Network {
513            target: bind_addr,
514            source: e,
515        })?;
516
517        let socket_state =
518            UdpSocketState::new(UdpSockRef::from(&socket)).map_err(|e| Error::Network {
519                target: bind_addr,
520                source: e,
521            })?;
522
523        // Generate default engine ID if not provided
524        let engine_id: Bytes = self.engine_id.map(Bytes::from).unwrap_or_else(|| {
525            // RFC 3411 format: enterprise number + format + local identifier
526            // Use a simple format: 0x80 (local) + timestamp + random
527            let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01]; // Enterprise format indicator
528            let timestamp = std::time::SystemTime::now()
529                .duration_since(std::time::UNIX_EPOCH)
530                .unwrap_or_default()
531                .as_secs();
532            id.extend_from_slice(&timestamp.to_be_bytes());
533            Bytes::from(id)
534        });
535
536        // Sort handlers by prefix length (longest first) for matching
537        self.handlers
538            .sort_by(|a, b| b.prefix.len().cmp(&a.prefix.len()));
539
540        let cancel = self.cancel.unwrap_or_default();
541
542        // Create concurrency limiter if configured
543        let concurrency_limit = self
544            .max_concurrent_requests
545            .map(|n| Arc::new(Semaphore::new(n)));
546
547        Ok(Agent {
548            inner: Arc::new(AgentInner {
549                socket: Arc::new(socket),
550                socket_state,
551                local_addr,
552                communities: self.communities,
553                usm_users: self.usm_users,
554                handlers: self.handlers,
555                engine_id,
556                engine_boots: AtomicU32::new(1),
557                engine_time: AtomicU32::new(0),
558                engine_start: Instant::now(),
559                salt_counter: SaltCounter::new(),
560                max_message_size: self.max_message_size,
561                concurrency_limit,
562                vacm: self.vacm,
563                snmp_invalid_msgs: AtomicU32::new(0),
564                snmp_unknown_security_models: AtomicU32::new(0),
565                snmp_silent_drops: AtomicU32::new(0),
566                usm_unknown_engine_ids: AtomicU32::new(0),
567                usm_unknown_usernames: AtomicU32::new(0),
568                usm_wrong_digests: AtomicU32::new(0),
569                usm_not_in_time_windows: AtomicU32::new(0),
570                cancel,
571            }),
572        })
573    }
574}
575
576impl Default for AgentBuilder {
577    fn default() -> Self {
578        Self::new()
579    }
580}
581
582/// Inner state shared across agent clones.
583pub(crate) struct AgentInner {
584    pub(crate) socket: Arc<UdpSocket>,
585    pub(crate) socket_state: UdpSocketState,
586    pub(crate) local_addr: SocketAddr,
587    pub(crate) communities: Vec<Vec<u8>>,
588    pub(crate) usm_users: HashMap<Bytes, UsmConfig>,
589    pub(crate) handlers: Vec<RegisteredHandler>,
590    pub(crate) engine_id: Bytes,
591    pub(crate) engine_boots: AtomicU32,
592    pub(crate) engine_time: AtomicU32,
593    pub(crate) engine_start: Instant,
594    pub(crate) salt_counter: SaltCounter,
595    pub(crate) max_message_size: usize,
596    pub(crate) concurrency_limit: Option<Arc<Semaphore>>,
597    pub(crate) vacm: Option<VacmConfig>,
598    // RFC 3412 statistics counters
599    /// snmpInvalidMsgs (1.3.6.1.6.3.11.2.1.2) - messages with invalid msgFlags
600    /// (e.g., privacy without authentication)
601    pub(crate) snmp_invalid_msgs: AtomicU32,
602    /// snmpUnknownSecurityModels (1.3.6.1.6.3.11.2.1.1) - messages with
603    /// unrecognized security model
604    pub(crate) snmp_unknown_security_models: AtomicU32,
605    /// snmpSilentDrops (1.3.6.1.6.3.11.2.1.3) - confirmed-class PDUs silently
606    /// dropped because even an empty response would exceed max message size
607    pub(crate) snmp_silent_drops: AtomicU32,
608    // RFC 3414 USM statistics counters
609    /// usmStatsUnknownEngineIDs (1.3.6.1.6.3.15.1.1.4) - messages with
610    /// unknown engine ID
611    pub(crate) usm_unknown_engine_ids: AtomicU32,
612    /// usmStatsUnknownUserNames (1.3.6.1.6.3.15.1.1.3) - messages with
613    /// unknown user name
614    pub(crate) usm_unknown_usernames: AtomicU32,
615    /// usmStatsWrongDigests (1.3.6.1.6.3.15.1.1.5) - messages with incorrect
616    /// authentication digest
617    pub(crate) usm_wrong_digests: AtomicU32,
618    /// usmStatsNotInTimeWindows (1.3.6.1.6.3.15.1.1.2) - messages outside
619    /// the time window
620    pub(crate) usm_not_in_time_windows: AtomicU32,
621    /// Cancellation token for graceful shutdown.
622    pub(crate) cancel: CancellationToken,
623}
624
625/// SNMP Agent.
626///
627/// Listens for and responds to SNMP requests (GET, GETNEXT, GETBULK, SET).
628///
629/// # Example
630///
631/// ```rust,no_run
632/// use async_snmp::agent::Agent;
633/// use async_snmp::oid;
634///
635/// # async fn example() -> Result<(), Box<async_snmp::Error>> {
636/// let agent = Agent::builder()
637///     .bind("0.0.0.0:161")
638///     .community(b"public")
639///     .build()
640///     .await?;
641///
642/// agent.run().await
643/// # }
644/// ```
645pub struct Agent {
646    pub(crate) inner: Arc<AgentInner>,
647}
648
649impl Agent {
650    /// Create a builder for configuring the agent.
651    pub fn builder() -> AgentBuilder {
652        AgentBuilder::new()
653    }
654
655    /// Get the local address the agent is bound to.
656    pub fn local_addr(&self) -> SocketAddr {
657        self.inner.local_addr
658    }
659
660    /// Get the engine ID.
661    pub fn engine_id(&self) -> &[u8] {
662        &self.inner.engine_id
663    }
664
665    /// Get the cancellation token for this agent.
666    ///
667    /// Call `token.cancel()` to initiate graceful shutdown.
668    pub fn cancel(&self) -> CancellationToken {
669        self.inner.cancel.clone()
670    }
671
672    /// Get the snmpInvalidMsgs counter value.
673    ///
674    /// This counter tracks messages with invalid msgFlags, such as
675    /// privacy-without-authentication (RFC 3412 Section 7.2 Step 5d).
676    ///
677    /// OID: 1.3.6.1.6.3.11.2.1.2
678    pub fn snmp_invalid_msgs(&self) -> u32 {
679        self.inner.snmp_invalid_msgs.load(Ordering::Relaxed)
680    }
681
682    /// Get the snmpUnknownSecurityModels counter value.
683    ///
684    /// This counter tracks messages with unrecognized security models
685    /// (RFC 3412 Section 7.2 Step 2).
686    ///
687    /// OID: 1.3.6.1.6.3.11.2.1.1
688    pub fn snmp_unknown_security_models(&self) -> u32 {
689        self.inner
690            .snmp_unknown_security_models
691            .load(Ordering::Relaxed)
692    }
693
694    /// Get the snmpSilentDrops counter value.
695    ///
696    /// This counter tracks confirmed-class PDUs (GetRequest, GetNextRequest,
697    /// GetBulkRequest, SetRequest, InformRequest) that were silently dropped
698    /// because even an empty Response-PDU would exceed the maximum message
699    /// size constraint (RFC 3412 Section 7.1).
700    ///
701    /// OID: 1.3.6.1.6.3.11.2.1.3
702    pub fn snmp_silent_drops(&self) -> u32 {
703        self.inner.snmp_silent_drops.load(Ordering::Relaxed)
704    }
705
706    /// Get the usmStatsUnknownEngineIDs counter value.
707    ///
708    /// This counter tracks messages with unknown engine IDs.
709    /// Incremented when a non-discovery request arrives with an engine ID that
710    /// does not match the local engine (RFC 3414 Section 3.2 Step 3).
711    ///
712    /// OID: 1.3.6.1.6.3.15.1.1.4
713    pub fn usm_unknown_engine_ids(&self) -> u32 {
714        self.inner.usm_unknown_engine_ids.load(Ordering::Relaxed)
715    }
716
717    /// Get the usmStatsUnknownUserNames counter value.
718    ///
719    /// This counter tracks messages with unknown user names.
720    /// Incremented when a message arrives with a user name not in the local
721    /// user database (RFC 3414 Section 3.2 Step 1).
722    ///
723    /// OID: 1.3.6.1.6.3.15.1.1.3
724    pub fn usm_unknown_usernames(&self) -> u32 {
725        self.inner.usm_unknown_usernames.load(Ordering::Relaxed)
726    }
727
728    /// Get the usmStatsWrongDigests counter value.
729    ///
730    /// This counter tracks messages with incorrect authentication digests,
731    /// as well as messages where the user has no auth key configured.
732    /// (RFC 3414 Section 3.2 Steps 6 and 7).
733    ///
734    /// OID: 1.3.6.1.6.3.15.1.1.5
735    pub fn usm_wrong_digests(&self) -> u32 {
736        self.inner.usm_wrong_digests.load(Ordering::Relaxed)
737    }
738
739    /// Get the usmStatsNotInTimeWindows counter value.
740    ///
741    /// This counter tracks messages that fall outside the time window.
742    /// Incremented when the message time differs from the local time by
743    /// more than 150 seconds (RFC 3414 Section 3.2 Step 8).
744    ///
745    /// OID: 1.3.6.1.6.3.15.1.1.2
746    pub fn usm_not_in_time_windows(&self) -> u32 {
747        self.inner.usm_not_in_time_windows.load(Ordering::Relaxed)
748    }
749
750    /// Run the agent, processing requests concurrently.
751    ///
752    /// Requests are processed in parallel up to the configured
753    /// `max_concurrent_requests` limit (default: 1000). This method runs
754    /// until the cancellation token is triggered.
755    #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
756    pub async fn run(&self) -> Result<()> {
757        let mut buf = vec![0u8; 65535];
758
759        loop {
760            let recv_meta = tokio::select! {
761                result = self.recv_packet(&mut buf) => {
762                    result?
763                }
764                _ = self.inner.cancel.cancelled() => {
765                    tracing::info!(target: "async_snmp::agent", "agent shutdown requested");
766                    return Ok(());
767                }
768            };
769
770            let data = Bytes::copy_from_slice(&buf[..recv_meta.len]);
771            let agent = self.clone();
772
773            let permit = if let Some(ref sem) = self.inner.concurrency_limit {
774                Some(sem.clone().acquire_owned().await.expect("semaphore closed"))
775            } else {
776                None
777            };
778
779            tokio::spawn(async move {
780                agent.update_engine_time();
781
782                match agent.handle_request(data, recv_meta.addr).await {
783                    Ok(Some(response_bytes)) => {
784                        if let Err(e) = agent.send_response(&response_bytes, &recv_meta).await {
785                            tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "failed to send response");
786                        }
787                    }
788                    Ok(None) => {}
789                    Err(e) => {
790                        tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "error handling request");
791                    }
792                }
793
794                drop(permit);
795            });
796        }
797    }
798
799    async fn recv_packet(&self, buf: &mut [u8]) -> Result<RecvMeta> {
800        let mut iov = [IoSliceMut::new(buf)];
801        let mut meta = [RecvMeta::default()];
802
803        loop {
804            self.inner
805                .socket
806                .readable()
807                .await
808                .map_err(|e| Error::Network {
809                    target: self.inner.local_addr,
810                    source: e,
811                })?;
812
813            let result = self.inner.socket.try_io(tokio::io::Interest::READABLE, || {
814                let sref = UdpSockRef::from(&*self.inner.socket);
815                self.inner.socket_state.recv(sref, &mut iov, &mut meta)
816            });
817
818            match result {
819                Ok(n) if n > 0 => return Ok(meta[0]),
820                Ok(_) => continue,
821                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
822                Err(e) => {
823                    return Err(Error::Network {
824                        target: self.inner.local_addr,
825                        source: e,
826                    }
827                    .boxed());
828                }
829            }
830        }
831    }
832
833    async fn send_response(&self, data: &[u8], recv_meta: &RecvMeta) -> std::io::Result<()> {
834        let transmit = Transmit {
835            destination: recv_meta.addr,
836            ecn: None,
837            contents: data,
838            segment_size: None,
839            src_ip: recv_meta.dst_ip,
840        };
841
842        loop {
843            self.inner.socket.writable().await?;
844
845            let result = self.inner.socket.try_io(tokio::io::Interest::WRITABLE, || {
846                let sref = UdpSockRef::from(&*self.inner.socket);
847                self.inner.socket_state.try_send(sref, &transmit)
848            });
849
850            match result {
851                Ok(()) => return Ok(()),
852                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
853                Err(e) => return Err(e),
854            }
855        }
856    }
857
858    /// Process a single request and return the response bytes.
859    ///
860    /// Returns `None` if no response should be sent.
861    async fn handle_request(&self, data: Bytes, source: SocketAddr) -> Result<Option<Bytes>> {
862        // Peek at version
863        let mut decoder = Decoder::with_target(data.clone(), source);
864        let mut seq = decoder.read_sequence()?;
865        let version_num = seq.read_integer()?;
866        let version = Version::from_i32(version_num).ok_or_else(|| {
867            tracing::debug!(target: "async_snmp::agent", { source = %source, kind = %DecodeErrorKind::UnknownVersion(version_num) }, "unknown SNMP version");
868            Error::MalformedResponse { target: source }.boxed()
869        })?;
870        drop(seq);
871        drop(decoder);
872
873        match version {
874            Version::V1 => self.handle_v1(data, source).await,
875            Version::V2c => self.handle_v2c(data, source).await,
876            Version::V3 => self.handle_v3(data, source).await,
877        }
878    }
879
880    /// Update engine time based on elapsed time since start.
881    fn update_engine_time(&self) {
882        let elapsed = self.inner.engine_start.elapsed().as_secs() as u32;
883        self.inner.engine_time.store(elapsed, Ordering::Relaxed);
884    }
885
886    /// Validate community string using constant-time comparison.
887    ///
888    /// Uses constant-time comparison to prevent timing attacks that could
889    /// be used to guess valid community strings character by character.
890    pub(crate) fn validate_community(&self, community: &[u8]) -> bool {
891        if self.inner.communities.is_empty() {
892            // No communities configured = reject all
893            return false;
894        }
895        // Use constant-time comparison for each community string.
896        // We compare against all configured communities regardless of
897        // early matches to maintain constant-time behavior.
898        let mut valid = false;
899        for configured in &self.inner.communities {
900            // ct_eq returns a Choice, which we convert to bool after comparison
901            if configured.len() == community.len()
902                && bool::from(configured.as_slice().ct_eq(community))
903            {
904                valid = true;
905            }
906        }
907        valid
908    }
909
910    /// Dispatch a request to the appropriate handler.
911    async fn dispatch_request(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
912        match pdu.pdu_type {
913            PduType::GetRequest => self.handle_get(ctx, pdu).await,
914            PduType::GetNextRequest => self.handle_get_next(ctx, pdu).await,
915            PduType::GetBulkRequest => self.handle_get_bulk(ctx, pdu).await,
916            PduType::SetRequest => self.handle_set(ctx, pdu).await,
917            PduType::InformRequest => self.handle_inform(pdu),
918            _ => {
919                // Should not happen - filtered earlier
920                Ok(pdu.to_error_response(ErrorStatus::GenErr, 0))
921            }
922        }
923    }
924
925    /// Handle InformRequest PDU.
926    ///
927    /// Per RFC 3416 Section 4.2.7, an InformRequest is a confirmed-class PDU
928    /// that the receiver acknowledges by returning a Response with the same
929    /// request-id and varbind list.
930    fn handle_inform(&self, pdu: &Pdu) -> Result<Pdu> {
931        // Simply acknowledge by returning the same varbinds in a Response
932        Ok(pdu.to_response())
933    }
934
935    /// Handle GET request.
936    async fn handle_get(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
937        let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
938
939        for (index, vb) in pdu.varbinds.iter().enumerate() {
940            // VACM read access check
941            if let Some(ref vacm) = self.inner.vacm
942                && !vacm.check_access(ctx.read_view.as_ref(), &vb.oid)
943            {
944                // v1: noSuchName, v2c/v3: noAccess or NoSuchObject
945                if ctx.version == Version::V1 {
946                    return Ok(pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32));
947                } else {
948                    // For GET, return NoSuchObject for inaccessible OIDs per RFC 3415
949                    response_varbinds.push(VarBind::new(vb.oid.clone(), Value::NoSuchObject));
950                    continue;
951                }
952            }
953
954            let result = if let Some(handler) = self.find_handler(&vb.oid) {
955                handler.handler.get(ctx, &vb.oid).await
956            } else {
957                GetResult::NoSuchObject
958            };
959
960            let response_value = match result {
961                GetResult::Value(v) => v,
962                GetResult::NoSuchObject => {
963                    // v1 returns noSuchName error, v2c/v3 returns NoSuchObject exception
964                    if ctx.version == Version::V1 {
965                        return Ok(
966                            pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
967                        );
968                    } else {
969                        Value::NoSuchObject
970                    }
971                }
972                GetResult::NoSuchInstance => {
973                    // v1 returns noSuchName error, v2c/v3 returns NoSuchInstance exception
974                    if ctx.version == Version::V1 {
975                        return Ok(
976                            pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
977                        );
978                    } else {
979                        Value::NoSuchInstance
980                    }
981                }
982            };
983
984            response_varbinds.push(VarBind::new(vb.oid.clone(), response_value));
985        }
986
987        Ok(Pdu {
988            pdu_type: PduType::Response,
989            request_id: pdu.request_id,
990            error_status: 0,
991            error_index: 0,
992            varbinds: response_varbinds,
993        })
994    }
995
996    /// Handle GETNEXT request.
997    async fn handle_get_next(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
998        let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
999
1000        for (index, vb) in pdu.varbinds.iter().enumerate() {
1001            // Try to find the next OID from any handler, skipping OIDs denied by
1002            // VACM. RFC 3413 classifies GETNEXT as Read-Class and requires
1003            // continuing the walk until an accessible OID is found.
1004            let next = self.get_next_accessible_oid(ctx, &vb.oid).await;
1005
1006            match next {
1007                Some(next_vb) => {
1008                    response_varbinds.push(next_vb);
1009                }
1010                None => {
1011                    // v1 returns noSuchName, v2c/v3 returns endOfMibView
1012                    if ctx.version == Version::V1 {
1013                        return Ok(
1014                            pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1015                        );
1016                    } else {
1017                        response_varbinds.push(VarBind::new(vb.oid.clone(), Value::EndOfMibView));
1018                    }
1019                }
1020            }
1021        }
1022
1023        Ok(Pdu {
1024            pdu_type: PduType::Response,
1025            request_id: pdu.request_id,
1026            error_status: 0,
1027            error_index: 0,
1028            varbinds: response_varbinds,
1029        })
1030    }
1031
1032    /// Handle GETBULK request.
1033    ///
1034    /// Per RFC 3416 Section 4.2.3, if the response would exceed the message
1035    /// size limit, we return fewer variable bindings rather than all of them.
1036    async fn handle_get_bulk(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1037        // For GETBULK, error_status is non_repeaters and error_index is max_repetitions
1038        let non_repeaters = pdu.error_status.max(0) as usize;
1039        let max_repetitions = pdu.error_index.max(0) as usize;
1040
1041        let mut response_varbinds = Vec::new();
1042        let mut current_size: usize = RESPONSE_OVERHEAD;
1043        let max_size = self.inner.max_message_size;
1044
1045        // Helper to check if we can add a varbind
1046        let can_add = |vb: &VarBind, current_size: usize| -> bool {
1047            current_size + vb.encoded_size() <= max_size
1048        };
1049
1050        // Handle non-repeaters (first N varbinds get one GETNEXT each)
1051        for vb in pdu.varbinds.iter().take(non_repeaters) {
1052            let next = self.get_next_accessible_oid(ctx, &vb.oid).await;
1053
1054            let next_vb = match next {
1055                Some(next_vb) => next_vb,
1056                None => VarBind::new(vb.oid.clone(), Value::EndOfMibView),
1057            };
1058
1059            if !can_add(&next_vb, current_size) {
1060                // Can't fit even non-repeaters, return tooBig if we have nothing
1061                if response_varbinds.is_empty() {
1062                    return Ok(pdu.to_error_response(ErrorStatus::TooBig, 0));
1063                }
1064                // Otherwise return what we have
1065                break;
1066            }
1067
1068            current_size += next_vb.encoded_size();
1069            response_varbinds.push(next_vb);
1070        }
1071
1072        // Handle repeaters
1073        if non_repeaters < pdu.varbinds.len() {
1074            let repeaters = &pdu.varbinds[non_repeaters..];
1075            let mut current_oids: Vec<Oid> = repeaters.iter().map(|vb| vb.oid.clone()).collect();
1076            let mut all_done = vec![false; repeaters.len()];
1077
1078            'outer: for _ in 0..max_repetitions {
1079                let mut row_complete = true;
1080                for (i, oid) in current_oids.iter_mut().enumerate() {
1081                    let next_vb = if all_done[i] {
1082                        VarBind::new(oid.clone(), Value::EndOfMibView)
1083                    } else {
1084                        let next = self.get_next_accessible_oid(ctx, oid).await;
1085
1086                        match next {
1087                            Some(next_vb) => {
1088                                *oid = next_vb.oid.clone();
1089                                row_complete = false;
1090                                next_vb
1091                            }
1092                            None => {
1093                                all_done[i] = true;
1094                                VarBind::new(oid.clone(), Value::EndOfMibView)
1095                            }
1096                        }
1097                    };
1098
1099                    // Check size before adding
1100                    if !can_add(&next_vb, current_size) {
1101                        // Can't fit more, return what we have
1102                        break 'outer;
1103                    }
1104
1105                    current_size += next_vb.encoded_size();
1106                    response_varbinds.push(next_vb);
1107                }
1108
1109                if row_complete {
1110                    break;
1111                }
1112            }
1113        }
1114
1115        Ok(Pdu {
1116            pdu_type: PduType::Response,
1117            request_id: pdu.request_id,
1118            error_status: 0,
1119            error_index: 0,
1120            varbinds: response_varbinds,
1121        })
1122    }
1123
1124    /// Find the handler for a given OID.
1125    pub(crate) fn find_handler(&self, oid: &Oid) -> Option<&RegisteredHandler> {
1126        // Handlers are sorted by prefix length (longest first)
1127        self.inner
1128            .handlers
1129            .iter()
1130            .find(|&handler| handler.handler.handles(&handler.prefix, oid))
1131            .map(|v| v as _)
1132    }
1133
1134    /// Find the next OID accessible under VACM, skipping denied OIDs by
1135    /// continuing the walk. Returns None when end-of-MIB is reached or all
1136    /// remaining candidates are denied.
1137    async fn get_next_accessible_oid(
1138        &self,
1139        ctx: &RequestContext,
1140        from_oid: &Oid,
1141    ) -> Option<VarBind> {
1142        let mut search_from = from_oid.clone();
1143        loop {
1144            let candidate = self.get_next_oid(ctx, &search_from).await;
1145            match candidate {
1146                None => return None,
1147                Some(ref next_vb) => {
1148                    if let Some(ref vacm) = self.inner.vacm {
1149                        if vacm.check_access(ctx.read_view.as_ref(), &next_vb.oid) {
1150                            return candidate;
1151                        } else {
1152                            search_from = next_vb.oid.clone();
1153                        }
1154                    } else {
1155                        return candidate;
1156                    }
1157                }
1158            }
1159        }
1160    }
1161
1162    /// Get the next OID from any handler.
1163    async fn get_next_oid(&self, ctx: &RequestContext, oid: &Oid) -> Option<VarBind> {
1164        // Find the first handler that can provide a next OID.
1165        //
1166        // A handler can only return an OID > oid if:
1167        //   - oid falls within the handler's subtree (oid starts with handler prefix), OR
1168        //   - the handler's entire subtree is after oid (handler prefix > oid)
1169        //
1170        // Handlers whose prefix is <= oid and whose subtree does not contain oid
1171        // cannot return anything useful and are skipped.
1172        let mut best_result: Option<VarBind> = None;
1173
1174        for handler in &self.inner.handlers {
1175            let prefix = &handler.prefix;
1176            if prefix <= oid && !oid.starts_with(prefix) {
1177                continue;
1178            }
1179            if let GetNextResult::Value(next) = handler.handler.get_next(ctx, oid).await {
1180                // Must be lexicographically greater than the request OID
1181                if next.oid > *oid {
1182                    match &best_result {
1183                        None => best_result = Some(next),
1184                        Some(current) if next.oid < current.oid => best_result = Some(next),
1185                        _ => {}
1186                    }
1187                }
1188            }
1189        }
1190
1191        best_result
1192    }
1193}
1194
1195impl Clone for Agent {
1196    fn clone(&self) -> Self {
1197        Self {
1198            inner: Arc::clone(&self.inner),
1199        }
1200    }
1201}
1202
1203#[cfg(test)]
1204mod tests {
1205    use super::*;
1206    use crate::handler::{
1207        BoxFuture, GetNextResult, GetResult, MibHandler, RequestContext, SecurityModel, SetResult,
1208    };
1209    use crate::message::SecurityLevel;
1210    use crate::oid;
1211
1212    struct TestHandler;
1213
1214    impl MibHandler for TestHandler {
1215        fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1216            Box::pin(async move {
1217                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
1218                    return GetResult::Value(Value::Integer(42));
1219                }
1220                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
1221                    return GetResult::Value(Value::OctetString(Bytes::from_static(b"test")));
1222                }
1223                GetResult::NoSuchObject
1224            })
1225        }
1226
1227        fn get_next<'a>(
1228            &'a self,
1229            _ctx: &'a RequestContext,
1230            oid: &'a Oid,
1231        ) -> BoxFuture<'a, GetNextResult> {
1232            Box::pin(async move {
1233                let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
1234                let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
1235
1236                if oid < &oid1 {
1237                    return GetNextResult::Value(VarBind::new(oid1, Value::Integer(42)));
1238                }
1239                if oid < &oid2 {
1240                    return GetNextResult::Value(VarBind::new(
1241                        oid2,
1242                        Value::OctetString(Bytes::from_static(b"test")),
1243                    ));
1244                }
1245                GetNextResult::EndOfMibView
1246            })
1247        }
1248    }
1249
1250    fn test_ctx() -> RequestContext {
1251        RequestContext {
1252            source: "127.0.0.1:12345".parse().unwrap(),
1253            version: Version::V2c,
1254            security_model: SecurityModel::V2c,
1255            security_name: Bytes::from_static(b"public"),
1256            security_level: SecurityLevel::NoAuthNoPriv,
1257            context_name: Bytes::new(),
1258            request_id: 1,
1259            pdu_type: PduType::GetRequest,
1260            group_name: None,
1261            read_view: None,
1262            write_view: None,
1263        }
1264    }
1265
1266    #[test]
1267    fn test_agent_builder_defaults() {
1268        let builder = AgentBuilder::new();
1269        assert_eq!(builder.bind_addr, "0.0.0.0:161");
1270        assert!(builder.communities.is_empty());
1271        assert!(builder.usm_users.is_empty());
1272        assert!(builder.handlers.is_empty());
1273    }
1274
1275    #[test]
1276    fn test_agent_builder_community() {
1277        let builder = AgentBuilder::new()
1278            .community(b"public")
1279            .community(b"private");
1280        assert_eq!(builder.communities.len(), 2);
1281    }
1282
1283    #[test]
1284    fn test_agent_builder_communities() {
1285        let builder = AgentBuilder::new().communities(["public", "private"]);
1286        assert_eq!(builder.communities.len(), 2);
1287    }
1288
1289    #[test]
1290    fn test_agent_builder_handler() {
1291        let builder =
1292            AgentBuilder::new().handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler));
1293        assert_eq!(builder.handlers.len(), 1);
1294    }
1295
1296    #[tokio::test]
1297    async fn test_mib_handler_default_set() {
1298        let handler = TestHandler;
1299        let mut ctx = test_ctx();
1300        ctx.pdu_type = PduType::SetRequest;
1301
1302        let result = handler
1303            .test_set(&ctx, &oid!(1, 3, 6, 1), &Value::Integer(1))
1304            .await;
1305        assert_eq!(result, SetResult::NotWritable);
1306    }
1307
1308    #[test]
1309    fn test_mib_handler_handles() {
1310        let handler = TestHandler;
1311        let prefix = oid!(1, 3, 6, 1, 4, 1, 99999);
1312
1313        // OID within prefix
1314        assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0)));
1315
1316        // Exact prefix match
1317        assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99999)));
1318
1319        // OID before prefix - should NOT be handled (GET/SET routing must not claim
1320        // OIDs outside the registered subtree)
1321        assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99998)));
1322
1323        // OID after prefix (not handled)
1324        assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 100000)));
1325    }
1326
1327    #[tokio::test]
1328    async fn test_test_handler_get() {
1329        let handler = TestHandler;
1330        let ctx = test_ctx();
1331
1332        // Existing OID
1333        let result = handler
1334            .get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
1335            .await;
1336        assert!(matches!(result, GetResult::Value(Value::Integer(42))));
1337
1338        // Non-existing OID
1339        let result = handler
1340            .get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 99, 0))
1341            .await;
1342        assert!(matches!(result, GetResult::NoSuchObject));
1343    }
1344
1345    #[tokio::test]
1346    async fn test_test_handler_get_next() {
1347        let handler = TestHandler;
1348        let mut ctx = test_ctx();
1349        ctx.pdu_type = PduType::GetNextRequest;
1350
1351        // Before first OID
1352        let next = handler.get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999)).await;
1353        assert!(next.is_value());
1354        if let GetNextResult::Value(vb) = next {
1355            assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0));
1356        }
1357
1358        // Between OIDs
1359        let next = handler
1360            .get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
1361            .await;
1362        assert!(next.is_value());
1363        if let GetNextResult::Value(vb) = next {
1364            assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0));
1365        }
1366
1367        // After last OID
1368        let next = handler
1369            .get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
1370            .await;
1371        assert!(next.is_end_of_mib_view());
1372    }
1373
1374    // FiveOidHandler has OIDs at .99999.{1,2,3,4,5}.0 with integer values 1-5.
1375    struct FiveOidHandler;
1376
1377    impl MibHandler for FiveOidHandler {
1378        fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1379            Box::pin(async move {
1380                for i in 1u32..=5 {
1381                    if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, i, 0) {
1382                        return GetResult::Value(Value::Integer(i as i32));
1383                    }
1384                }
1385                GetResult::NoSuchObject
1386            })
1387        }
1388
1389        fn get_next<'a>(
1390            &'a self,
1391            _ctx: &'a RequestContext,
1392            oid: &'a Oid,
1393        ) -> BoxFuture<'a, GetNextResult> {
1394            Box::pin(async move {
1395                for i in 1u32..=5 {
1396                    let candidate = oid!(1, 3, 6, 1, 4, 1, 99999, i, 0);
1397                    if oid < &candidate {
1398                        return GetNextResult::Value(VarBind::new(
1399                            candidate,
1400                            Value::Integer(i as i32),
1401                        ));
1402                    }
1403                }
1404                GetNextResult::EndOfMibView
1405            })
1406        }
1407    }
1408
1409    /// Build an agent bound to a random port for testing, with a VACM view
1410    /// that only permits reading OIDs under .99999.2 and .99999.4 (odd OIDs
1411    /// 1, 3, 5 are denied). This exercises the VACM walk-past logic.
1412    async fn test_agent_with_restricted_vacm() -> Agent {
1413        Agent::builder()
1414            .bind("127.0.0.1:0")
1415            .community(b"public")
1416            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
1417            .vacm(|v| {
1418                v.group("public", SecurityModel::V2c, "readers")
1419                    .access("readers", |a| a.read_view("restricted"))
1420                    .view("restricted", |v| {
1421                        v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 2))
1422                            .include(oid!(1, 3, 6, 1, 4, 1, 99999, 4))
1423                    })
1424            })
1425            .build()
1426            .await
1427            .unwrap()
1428    }
1429
1430    #[tokio::test]
1431    async fn test_getbulk_vacm_filters_inaccessible_oids() {
1432        let agent = test_agent_with_restricted_vacm().await;
1433
1434        let mut ctx = test_ctx();
1435        ctx.pdu_type = PduType::GetBulkRequest;
1436        ctx.read_view = Some(Bytes::from_static(b"restricted"));
1437
1438        // GETBULK starting before the handler prefix, requesting up to 10 repeats.
1439        // The handler has OIDs {1,2,3,4,5}.0 but only {2,4} are in the view.
1440        // The walk must skip denied OIDs and continue, returning both 2 and 4.
1441        let pdu = Pdu {
1442            pdu_type: PduType::GetBulkRequest,
1443            request_id: 1,
1444            error_status: 0, // non_repeaters
1445            error_index: 10, // max_repetitions
1446            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
1447        };
1448
1449        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1450
1451        // Collect the OIDs returned (excluding EndOfMibView sentinels)
1452        let returned_oids: Vec<&Oid> = response
1453            .varbinds
1454            .iter()
1455            .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
1456            .map(|vb| &vb.oid)
1457            .collect();
1458
1459        // Both accessible OIDs must appear - the walk must not stop at the first one
1460        assert!(
1461            returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)),
1462            "expected .99999.2.0 in response, got: {:?}",
1463            returned_oids
1464        );
1465        assert!(
1466            returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0)),
1467            "expected .99999.4.0 in response (walk must continue past denied OIDs), got: {:?}",
1468            returned_oids
1469        );
1470
1471        // Denied OIDs must not appear
1472        for &oid in &[
1473            &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
1474            &oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
1475            &oid!(1, 3, 6, 1, 4, 1, 99999, 5, 0),
1476        ] {
1477            assert!(
1478                !returned_oids.contains(&oid),
1479                "GETBULK returned OID outside read view: {:?}",
1480                oid
1481            );
1482        }
1483    }
1484
1485    #[tokio::test]
1486    async fn test_getbulk_non_repeaters_vacm_filtered() {
1487        let agent = test_agent_with_restricted_vacm().await;
1488
1489        let mut ctx = test_ctx();
1490        ctx.pdu_type = PduType::GetBulkRequest;
1491        ctx.read_view = Some(Bytes::from_static(b"restricted"));
1492
1493        // GETBULK with non_repeaters=2, max_repetitions=0.
1494        // First varbind starts before the subtree: walks past denied .99999.1.0
1495        // and returns the first accessible .99999.2.0.
1496        // Second varbind starts at .99999.4.0 (the last accessible OID): walks
1497        // to .99999.5.0 (denied) and then hits end-of-MIB, returning EndOfMibView.
1498        let pdu = Pdu {
1499            pdu_type: PduType::GetBulkRequest,
1500            request_id: 2,
1501            error_status: 2, // non_repeaters
1502            error_index: 0,  // max_repetitions
1503            varbinds: vec![
1504                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null),
1505                VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0), Value::Null),
1506            ],
1507        };
1508
1509        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1510
1511        // First non-repeater skips denied .99999.1.0 and returns accessible .99999.2.0
1512        assert_eq!(
1513            response.varbinds[0].oid,
1514            oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)
1515        );
1516        assert!(matches!(response.varbinds[0].value, Value::Integer(2)));
1517
1518        // Second non-repeater walks to .99999.5.0 (denied), then end-of-MIB
1519        assert_eq!(response.varbinds[1].value, Value::EndOfMibView);
1520    }
1521
1522    // TestHandler with three OIDs: .99999.1.0, .99999.2.0, .99999.3.0
1523    struct ThreeOidHandler;
1524
1525    impl MibHandler for ThreeOidHandler {
1526        fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1527            Box::pin(async move {
1528                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
1529                    return GetResult::Value(Value::Integer(1));
1530                }
1531                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
1532                    return GetResult::Value(Value::Integer(2));
1533                }
1534                if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0) {
1535                    return GetResult::Value(Value::Integer(3));
1536                }
1537                GetResult::NoSuchObject
1538            })
1539        }
1540
1541        fn get_next<'a>(
1542            &'a self,
1543            _ctx: &'a RequestContext,
1544            oid: &'a Oid,
1545        ) -> BoxFuture<'a, GetNextResult> {
1546            Box::pin(async move {
1547                let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
1548                let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
1549                let oid3 = oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0);
1550
1551                if oid < &oid1 {
1552                    return GetNextResult::Value(VarBind::new(oid1, Value::Integer(1)));
1553                }
1554                if oid < &oid2 {
1555                    return GetNextResult::Value(VarBind::new(oid2, Value::Integer(2)));
1556                }
1557                if oid < &oid3 {
1558                    return GetNextResult::Value(VarBind::new(oid3, Value::Integer(3)));
1559                }
1560                GetNextResult::EndOfMibView
1561            })
1562        }
1563    }
1564
1565    /// Build an agent with ThreeOidHandler and a VACM view that includes
1566    /// .99999.1 and .99999.3 but excludes .99999.2.
1567    async fn test_agent_with_gap_vacm() -> Agent {
1568        Agent::builder()
1569            .bind("127.0.0.1:0")
1570            .community(b"public")
1571            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(ThreeOidHandler))
1572            .vacm(|v| {
1573                v.group("public", SecurityModel::V2c, "readers")
1574                    .access("readers", |a| a.read_view("gap"))
1575                    .view("gap", |v| {
1576                        v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 1))
1577                            .include(oid!(1, 3, 6, 1, 4, 1, 99999, 3))
1578                    })
1579            })
1580            .build()
1581            .await
1582            .unwrap()
1583    }
1584
1585    #[tokio::test]
1586    async fn test_getnext_vacm_skips_inaccessible_continues_walk() {
1587        // GETNEXT must continue past denied OIDs to find the next accessible one.
1588        // .99999.2.0 is excluded from the view; .99999.3.0 is included.
1589        // GETNEXT from .99999.1.0 should skip .99999.2.0 and return .99999.3.0.
1590        let agent = test_agent_with_gap_vacm().await;
1591
1592        let mut ctx = test_ctx();
1593        ctx.pdu_type = PduType::GetNextRequest;
1594        ctx.read_view = Some(Bytes::from_static(b"gap"));
1595
1596        let pdu = Pdu {
1597            pdu_type: PduType::GetNextRequest,
1598            request_id: 1,
1599            error_status: 0,
1600            error_index: 0,
1601            varbinds: vec![VarBind::new(
1602                oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
1603                Value::Null,
1604            )],
1605        };
1606
1607        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1608        assert_eq!(response.varbinds.len(), 1);
1609        assert_eq!(
1610            response.varbinds[0].oid,
1611            oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
1612            "GETNEXT should skip denied .99999.2.0 and return accessible .99999.3.0"
1613        );
1614        assert!(matches!(response.varbinds[0].value, Value::Integer(3)));
1615    }
1616
1617    #[tokio::test]
1618    async fn test_getnext_vacm_all_remaining_denied_returns_end_of_mib() {
1619        // When all remaining OIDs are denied, GETNEXT should return EndOfMibView.
1620        // Start at .99999.4.0 (the last accessible OID). The only OID after it
1621        // is .99999.5.0 which is denied, so the walk reaches end-of-MIB.
1622        let agent = test_agent_with_restricted_vacm().await;
1623
1624        let mut ctx = test_ctx();
1625        ctx.pdu_type = PduType::GetNextRequest;
1626        ctx.read_view = Some(Bytes::from_static(b"restricted"));
1627
1628        let pdu = Pdu {
1629            pdu_type: PduType::GetNextRequest,
1630            request_id: 1,
1631            error_status: 0,
1632            error_index: 0,
1633            varbinds: vec![VarBind::new(
1634                oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0),
1635                Value::Null,
1636            )],
1637        };
1638
1639        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1640        assert_eq!(response.varbinds.len(), 1);
1641        assert_eq!(
1642            response.varbinds[0].value,
1643            Value::EndOfMibView,
1644            "GETNEXT should return EndOfMibView when all remaining OIDs are denied"
1645        );
1646    }
1647
1648    #[tokio::test]
1649    async fn test_getbulk_without_vacm_returns_all_oids() {
1650        // Sanity check: without VACM, both OIDs should be returned
1651        let agent = Agent::builder()
1652            .bind("127.0.0.1:0")
1653            .community(b"public")
1654            .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler))
1655            .build()
1656            .await
1657            .unwrap();
1658
1659        let mut ctx = test_ctx();
1660        ctx.pdu_type = PduType::GetBulkRequest;
1661
1662        let pdu = Pdu {
1663            pdu_type: PduType::GetBulkRequest,
1664            request_id: 1,
1665            error_status: 0,
1666            error_index: 10,
1667            varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
1668        };
1669
1670        let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1671
1672        // Both OIDs should appear
1673        assert!(
1674            response
1675                .varbinds
1676                .iter()
1677                .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
1678        );
1679        assert!(
1680            response
1681                .varbinds
1682                .iter()
1683                .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
1684        );
1685    }
1686}