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(×tamp.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}