mod builtins;
mod notification;
mod request;
mod response;
mod set_handler;
pub mod vacm;
pub use vacm::{SecurityModel, VacmBuilder, VacmConfig, View, ViewCheckResult, ViewSubtree};
use std::collections::{HashMap, HashSet};
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};
use bytes::Bytes;
use subtle::ConstantTimeEq;
use tokio::net::UdpSocket;
use tokio::sync::Semaphore;
use tokio_util::sync::CancellationToken;
use tracing::instrument;
use std::io::IoSliceMut;
use quinn_udp::{RecvMeta, Transmit, UdpSockRef, UdpSocketState};
use crate::ber::Decoder;
use crate::error::internal::DecodeErrorKind;
use crate::error::{Error, ErrorStatus, Result};
use crate::handler::{GetNextResult, GetResult, MibHandler, RequestContext};
use crate::notification::UsmConfig;
use crate::oid;
use crate::oid::Oid;
use crate::pdu::{Pdu, PduType};
use crate::util::bind_udp_socket;
use crate::v3::{SaltCounter, compute_engine_boots_time};
use crate::value::Value;
use crate::varbind::VarBind;
use crate::version::Version;
const DEFAULT_MAX_MESSAGE_SIZE: usize = 1472;
const RESPONSE_OVERHEAD: usize = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BuiltinMib {
SnmpEngine,
UsmStats,
MpdStats,
}
pub(crate) struct RegisteredHandler {
pub(crate) prefix: Oid,
pub(crate) handler: Arc<dyn MibHandler>,
}
pub struct AgentBuilder {
bind_addr: String,
communities: Vec<Vec<u8>>,
usm_users: HashMap<Bytes, UsmConfig>,
handlers: Vec<RegisteredHandler>,
engine_id: Option<Vec<u8>>,
engine_boots: u32,
max_message_size: usize,
max_concurrent_requests: Option<usize>,
recv_buffer_size: Option<usize>,
vacm: Option<VacmConfig>,
cancel: Option<CancellationToken>,
trap_sinks: Vec<(String, crate::client::Auth)>,
inform_timeout: Duration,
inform_retry: crate::client::Retry,
disabled_builtins: HashSet<BuiltinMib>,
}
impl AgentBuilder {
pub fn new() -> Self {
Self {
bind_addr: "0.0.0.0:161".to_string(),
communities: Vec::new(),
usm_users: HashMap::new(),
handlers: Vec::new(),
engine_id: None,
engine_boots: 1,
max_message_size: DEFAULT_MAX_MESSAGE_SIZE,
max_concurrent_requests: Some(1000),
recv_buffer_size: Some(4 * 1024 * 1024), vacm: None,
cancel: None,
trap_sinks: Vec::new(),
inform_timeout: Duration::from_secs(5),
inform_retry: crate::client::Retry::default(),
disabled_builtins: HashSet::new(),
}
}
pub fn bind(mut self, addr: impl Into<String>) -> Self {
self.bind_addr = addr.into();
self
}
pub fn community(mut self, community: &[u8]) -> Self {
self.communities.push(community.to_vec());
self
}
pub fn communities<I, C>(mut self, communities: I) -> Self
where
I: IntoIterator<Item = C>,
C: AsRef<[u8]>,
{
for c in communities {
self.communities.push(c.as_ref().to_vec());
}
self
}
pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
where
F: FnOnce(UsmConfig) -> UsmConfig,
{
let username_bytes: Bytes = username.into();
let config = configure(UsmConfig::new(username_bytes.clone()));
self.usm_users.insert(username_bytes, config);
self
}
pub fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
self.engine_id = Some(engine_id.into());
self
}
pub fn engine_boots(mut self, boots: u32) -> Self {
self.engine_boots = boots;
self
}
pub fn max_message_size(mut self, size: usize) -> Self {
self.max_message_size = size;
self
}
pub fn max_concurrent_requests(mut self, limit: Option<usize>) -> Self {
self.max_concurrent_requests = limit;
self
}
pub fn recv_buffer_size(mut self, size: Option<usize>) -> Self {
self.recv_buffer_size = size;
self
}
pub fn handler(mut self, prefix: Oid, handler: Arc<dyn MibHandler>) -> Self {
self.handlers.push(RegisteredHandler { prefix, handler });
self
}
pub fn vacm<F>(mut self, configure: F) -> Self
where
F: FnOnce(VacmBuilder) -> VacmBuilder,
{
let builder = VacmBuilder::new();
self.vacm = Some(configure(builder).build());
self
}
pub fn cancel(mut self, token: CancellationToken) -> Self {
self.cancel = Some(token);
self
}
pub fn trap_sink(
mut self,
dest: impl Into<String>,
auth: impl Into<crate::client::Auth>,
) -> Self {
self.trap_sinks.push((dest.into(), auth.into()));
self
}
pub fn inform_timeout(mut self, timeout: Duration) -> Self {
self.inform_timeout = timeout;
self
}
pub fn inform_retry(mut self, retry: crate::client::Retry) -> Self {
self.inform_retry = retry;
self
}
pub fn without_builtin_handler(mut self, mib: BuiltinMib) -> Self {
self.disabled_builtins.insert(mib);
self
}
pub fn without_builtin_handlers(mut self) -> Self {
self.disabled_builtins.insert(BuiltinMib::SnmpEngine);
self.disabled_builtins.insert(BuiltinMib::UsmStats);
self.disabled_builtins.insert(BuiltinMib::MpdStats);
self
}
pub async fn build(mut self) -> Result<Agent> {
let bind_addr: std::net::SocketAddr = self.bind_addr.parse().map_err(|_| {
Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
})?;
let socket = bind_udp_socket(bind_addr, self.recv_buffer_size, None, false)
.await
.map_err(|e| Error::Network {
target: bind_addr,
source: e,
})?;
let local_addr = socket.local_addr().map_err(|e| Error::Network {
target: bind_addr,
source: e,
})?;
let socket_state =
UdpSocketState::new(UdpSockRef::from(&socket)).map_err(|e| Error::Network {
target: bind_addr,
source: e,
})?;
let engine_id: Bytes = self.engine_id.map(Bytes::from).unwrap_or_else(|| {
let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01]; let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
id.extend_from_slice(×tamp.to_be_bytes());
Bytes::from(id)
});
let cancel = self.cancel.unwrap_or_default();
let concurrency_limit = self
.max_concurrent_requests
.map(|n| Arc::new(Semaphore::new(n)));
let mut trap_sinks = Vec::with_capacity(self.trap_sinks.len());
for (dest_str, auth) in self.trap_sinks {
let dest: SocketAddr = dest_str.parse().map_err(|_| {
Error::Config(format!("invalid trap sink address: {}", dest_str).into())
})?;
trap_sinks.push(notification::TrapSink::new(
dest,
auth,
self.inform_timeout,
self.inform_retry.clone(),
));
}
let state = Arc::new(AgentState {
engine_id,
engine_boots: AtomicU32::new(self.engine_boots),
engine_time: AtomicU32::new(0),
engine_start: Instant::now(),
engine_boots_base: self.engine_boots,
max_message_size: self.max_message_size,
snmp_invalid_msgs: AtomicU32::new(0),
snmp_unknown_security_models: AtomicU32::new(0),
snmp_silent_drops: AtomicU32::new(0),
usm_unknown_engine_ids: AtomicU32::new(0),
usm_unknown_usernames: AtomicU32::new(0),
usm_wrong_digests: AtomicU32::new(0),
usm_not_in_time_windows: AtomicU32::new(0),
usm_unsupported_sec_levels: AtomicU32::new(0),
usm_decryption_errors: AtomicU32::new(0),
});
if !self.disabled_builtins.contains(&BuiltinMib::SnmpEngine) {
self.handlers.push(RegisteredHandler {
prefix: oid!(1, 3, 6, 1, 6, 3, 10, 2, 1),
handler: Arc::new(builtins::SnmpEngineHandler {
state: Arc::clone(&state),
}),
});
}
if !self.disabled_builtins.contains(&BuiltinMib::UsmStats) {
self.handlers.push(RegisteredHandler {
prefix: oid!(1, 3, 6, 1, 6, 3, 15, 1, 1),
handler: Arc::new(builtins::UsmStatsHandler {
state: Arc::clone(&state),
}),
});
}
if !self.disabled_builtins.contains(&BuiltinMib::MpdStats) {
self.handlers.push(RegisteredHandler {
prefix: oid!(1, 3, 6, 1, 6, 3, 11, 2, 1),
handler: Arc::new(builtins::MpdStatsHandler {
state: Arc::clone(&state),
}),
});
}
self.handlers
.sort_by(|a, b| b.prefix.len().cmp(&a.prefix.len()));
Ok(Agent {
inner: Arc::new(AgentInner {
socket: Arc::new(socket),
socket_state,
local_addr,
communities: self.communities,
usm_users: self.usm_users,
handlers: self.handlers,
state,
salt_counter: SaltCounter::new(),
concurrency_limit,
vacm: self.vacm,
cancel,
trap_sinks,
}),
})
}
}
impl Default for AgentBuilder {
fn default() -> Self {
Self::new()
}
}
pub(crate) struct AgentState {
pub(crate) engine_id: Bytes,
pub(crate) engine_boots: AtomicU32,
pub(crate) engine_time: AtomicU32,
pub(crate) engine_start: Instant,
pub(crate) engine_boots_base: u32,
pub(crate) max_message_size: usize,
pub(crate) snmp_invalid_msgs: AtomicU32,
pub(crate) snmp_unknown_security_models: AtomicU32,
pub(crate) snmp_silent_drops: AtomicU32,
pub(crate) usm_unknown_engine_ids: AtomicU32,
pub(crate) usm_unknown_usernames: AtomicU32,
pub(crate) usm_wrong_digests: AtomicU32,
pub(crate) usm_not_in_time_windows: AtomicU32,
pub(crate) usm_unsupported_sec_levels: AtomicU32,
pub(crate) usm_decryption_errors: AtomicU32,
}
pub(crate) struct AgentInner {
pub(crate) socket: Arc<UdpSocket>,
pub(crate) socket_state: UdpSocketState,
pub(crate) local_addr: SocketAddr,
pub(crate) communities: Vec<Vec<u8>>,
pub(crate) usm_users: HashMap<Bytes, UsmConfig>,
pub(crate) handlers: Vec<RegisteredHandler>,
pub(crate) state: Arc<AgentState>,
pub(crate) salt_counter: SaltCounter,
pub(crate) concurrency_limit: Option<Arc<Semaphore>>,
pub(crate) vacm: Option<VacmConfig>,
pub(crate) cancel: CancellationToken,
pub(crate) trap_sinks: Vec<notification::TrapSink>,
}
pub struct Agent {
pub(crate) inner: Arc<AgentInner>,
}
impl Agent {
pub fn builder() -> AgentBuilder {
AgentBuilder::new()
}
pub fn local_addr(&self) -> SocketAddr {
self.inner.local_addr
}
pub fn engine_id(&self) -> &[u8] {
&self.inner.state.engine_id
}
pub fn engine_boots(&self) -> u32 {
self.inner.state.engine_boots.load(Ordering::Relaxed)
}
pub fn engine_time(&self) -> u32 {
self.inner.state.engine_time.load(Ordering::Relaxed)
}
pub fn cancel(&self) -> CancellationToken {
self.inner.cancel.clone()
}
pub fn snmp_invalid_msgs(&self) -> u32 {
self.inner.state.snmp_invalid_msgs.load(Ordering::Relaxed)
}
pub fn snmp_unknown_security_models(&self) -> u32 {
self.inner
.state
.snmp_unknown_security_models
.load(Ordering::Relaxed)
}
pub fn snmp_silent_drops(&self) -> u32 {
self.inner.state.snmp_silent_drops.load(Ordering::Relaxed)
}
pub fn usm_unknown_engine_ids(&self) -> u32 {
self.inner
.state
.usm_unknown_engine_ids
.load(Ordering::Relaxed)
}
pub fn usm_unknown_usernames(&self) -> u32 {
self.inner
.state
.usm_unknown_usernames
.load(Ordering::Relaxed)
}
pub fn usm_wrong_digests(&self) -> u32 {
self.inner.state.usm_wrong_digests.load(Ordering::Relaxed)
}
pub fn usm_not_in_time_windows(&self) -> u32 {
self.inner
.state
.usm_not_in_time_windows
.load(Ordering::Relaxed)
}
pub fn usm_unsupported_sec_levels(&self) -> u32 {
self.inner
.state
.usm_unsupported_sec_levels
.load(Ordering::Relaxed)
}
pub fn usm_decryption_errors(&self) -> u32 {
self.inner
.state
.usm_decryption_errors
.load(Ordering::Relaxed)
}
pub fn uptime_hundredths(&self) -> u32 {
let elapsed = self.inner.state.engine_start.elapsed();
let centisecs = elapsed.as_millis() / 10;
centisecs.min(u32::MAX as u128) as u32
}
#[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
pub async fn run(&self) -> Result<()> {
let mut buf = vec![0u8; 65535];
loop {
let recv_meta = tokio::select! {
result = self.recv_packet(&mut buf) => {
result?
}
_ = self.inner.cancel.cancelled() => {
tracing::info!(target: "async_snmp::agent", "agent shutdown requested");
return Ok(());
}
};
let data = Bytes::copy_from_slice(&buf[..recv_meta.len]);
let agent = self.clone();
let permit = if let Some(ref sem) = self.inner.concurrency_limit {
Some(sem.clone().acquire_owned().await.expect("semaphore closed"))
} else {
None
};
tokio::spawn(async move {
agent.update_engine_time();
match agent.handle_request(data, recv_meta.addr).await {
Ok(Some(response_bytes)) => {
if response_bytes.len() > agent.inner.state.max_message_size {
agent
.inner
.state
.snmp_silent_drops
.fetch_add(1, Ordering::Relaxed);
tracing::debug!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, response_size = response_bytes.len(), max_size = agent.inner.state.max_message_size }, "response exceeds max message size, silently dropped");
} else if let Err(e) =
agent.send_response(&response_bytes, &recv_meta).await
{
tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "failed to send response");
}
}
Ok(None) => {}
Err(e) => {
tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "error handling request");
}
}
drop(permit);
});
}
}
async fn recv_packet(&self, buf: &mut [u8]) -> Result<RecvMeta> {
let mut iov = [IoSliceMut::new(buf)];
let mut meta = [RecvMeta::default()];
loop {
self.inner
.socket
.readable()
.await
.map_err(|e| Error::Network {
target: self.inner.local_addr,
source: e,
})?;
let result = self.inner.socket.try_io(tokio::io::Interest::READABLE, || {
let sref = UdpSockRef::from(&*self.inner.socket);
self.inner.socket_state.recv(sref, &mut iov, &mut meta)
});
match result {
Ok(n) if n > 0 => return Ok(meta[0]),
Ok(_) => continue,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
Err(e) => {
return Err(Error::Network {
target: self.inner.local_addr,
source: e,
}
.boxed());
}
}
}
}
async fn send_response(&self, data: &[u8], recv_meta: &RecvMeta) -> std::io::Result<()> {
let transmit = Transmit {
destination: recv_meta.addr,
ecn: None,
contents: data,
segment_size: None,
src_ip: recv_meta.dst_ip,
};
loop {
self.inner.socket.writable().await?;
let result = self.inner.socket.try_io(tokio::io::Interest::WRITABLE, || {
let sref = UdpSockRef::from(&*self.inner.socket);
self.inner.socket_state.try_send(sref, &transmit)
});
match result {
Ok(()) => return Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
Err(e) => return Err(e),
}
}
}
async fn handle_request(&self, data: Bytes, source: SocketAddr) -> Result<Option<Bytes>> {
let mut decoder = Decoder::with_target(data.clone(), source);
let mut seq = decoder.read_sequence()?;
let version_num = seq.read_integer()?;
let version = Version::from_i32(version_num).ok_or_else(|| {
tracing::debug!(target: "async_snmp::agent", { source = %source, kind = %DecodeErrorKind::UnknownVersion(version_num) }, "unknown SNMP version");
Error::MalformedResponse { target: source }.boxed()
})?;
drop(seq);
drop(decoder);
match version {
Version::V1 => self.handle_v1(data, source).await,
Version::V2c => self.handle_v2c(data, source).await,
Version::V3 => self.handle_v3(data, source).await,
}
}
fn update_engine_time(&self) {
let total_secs = self.inner.state.engine_start.elapsed().as_secs();
let (boots, time) =
compute_engine_boots_time(self.inner.state.engine_boots_base, total_secs);
if boots != self.inner.state.engine_boots.load(Ordering::Relaxed)
&& boots > self.inner.state.engine_boots_base
{
tracing::warn!(
target: "async_snmp::agent",
engine_boots = boots,
"engine time wrapped past MAX_ENGINE_TIME, incrementing engine boots"
);
}
self.inner
.state
.engine_boots
.store(boots, Ordering::Relaxed);
self.inner.state.engine_time.store(time, Ordering::Relaxed);
}
pub(crate) fn validate_community(&self, community: &[u8]) -> bool {
if self.inner.communities.is_empty() {
return false;
}
let mut valid = false;
for configured in &self.inner.communities {
if configured.len() == community.len()
&& bool::from(configured.as_slice().ct_eq(community))
{
valid = true;
}
}
valid
}
async fn dispatch_request(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
match pdu.pdu_type {
PduType::GetRequest => self.handle_get(ctx, pdu).await,
PduType::GetNextRequest => self.handle_get_next(ctx, pdu).await,
PduType::GetBulkRequest => {
if ctx.version == Version::V1 {
return Ok(pdu.to_error_response(ErrorStatus::GenErr, 0));
}
self.handle_get_bulk(ctx, pdu).await
}
PduType::SetRequest => self.handle_set(ctx, pdu).await,
PduType::InformRequest => self.handle_inform(pdu),
_ => {
Ok(pdu.to_error_response(ErrorStatus::GenErr, 0))
}
}
}
fn handle_inform(&self, pdu: &Pdu) -> Result<Pdu> {
Ok(pdu.to_response())
}
async fn handle_get(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
for (index, vb) in pdu.varbinds.iter().enumerate() {
if let Some(ref vacm) = self.inner.vacm
&& !vacm.check_access(ctx.read_view.as_ref(), &vb.oid)
{
if ctx.version == Version::V1 {
return Ok(pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32));
} else {
response_varbinds.push(VarBind::new(vb.oid.clone(), Value::NoSuchObject));
continue;
}
}
let result = if let Some(handler) = self.find_handler(&vb.oid) {
handler.handler.get(ctx, &vb.oid).await
} else {
GetResult::NoSuchObject
};
let response_value = match result {
GetResult::Value(v) => {
if ctx.version == Version::V1 && matches!(v, Value::Counter64(_)) {
return Ok(
pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
);
}
v
}
GetResult::NoSuchObject => {
if ctx.version == Version::V1 {
return Ok(
pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
);
} else {
Value::NoSuchObject
}
}
GetResult::NoSuchInstance => {
if ctx.version == Version::V1 {
return Ok(
pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
);
} else {
Value::NoSuchInstance
}
}
};
response_varbinds.push(VarBind::new(vb.oid.clone(), response_value));
}
Ok(Pdu {
pdu_type: PduType::Response,
request_id: pdu.request_id,
error_status: 0,
error_index: 0,
varbinds: response_varbinds,
})
}
async fn handle_get_next(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
for (index, vb) in pdu.varbinds.iter().enumerate() {
let next = self.get_next_accessible_oid(ctx, &vb.oid).await;
match next {
Some(next_vb) => {
response_varbinds.push(next_vb);
}
None => {
if ctx.version == Version::V1 {
return Ok(
pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
);
} else {
response_varbinds.push(VarBind::new(vb.oid.clone(), Value::EndOfMibView));
}
}
}
}
Ok(Pdu {
pdu_type: PduType::Response,
request_id: pdu.request_id,
error_status: 0,
error_index: 0,
varbinds: response_varbinds,
})
}
async fn handle_get_bulk(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
let non_repeaters = pdu.error_status.max(0) as usize;
let max_repetitions = pdu.error_index.max(0) as usize;
let mut response_varbinds = Vec::new();
let mut current_size: usize = RESPONSE_OVERHEAD;
let agent_max = self.inner.state.max_message_size;
let max_size = match ctx.msg_max_size {
Some(client_max) => agent_max.min(client_max as usize),
None => agent_max,
};
let can_add = |vb: &VarBind, current_size: usize| -> bool {
current_size + vb.encoded_size() <= max_size
};
for vb in pdu.varbinds.iter().take(non_repeaters) {
let next = self.get_next_accessible_oid(ctx, &vb.oid).await;
let next_vb = match next {
Some(next_vb) => next_vb,
None => VarBind::new(vb.oid.clone(), Value::EndOfMibView),
};
if !can_add(&next_vb, current_size) {
if response_varbinds.is_empty() {
return Ok(pdu.to_error_response(ErrorStatus::TooBig, 0));
}
break;
}
current_size += next_vb.encoded_size();
response_varbinds.push(next_vb);
}
if non_repeaters < pdu.varbinds.len() {
let repeaters = &pdu.varbinds[non_repeaters..];
let mut current_oids: Vec<Oid> = repeaters.iter().map(|vb| vb.oid.clone()).collect();
let mut all_done = vec![false; repeaters.len()];
'outer: for _ in 0..max_repetitions {
let mut row_complete = true;
for (i, oid) in current_oids.iter_mut().enumerate() {
let next_vb = if all_done[i] {
VarBind::new(oid.clone(), Value::EndOfMibView)
} else {
let next = self.get_next_accessible_oid(ctx, oid).await;
match next {
Some(next_vb) => {
*oid = next_vb.oid.clone();
row_complete = false;
next_vb
}
None => {
all_done[i] = true;
VarBind::new(oid.clone(), Value::EndOfMibView)
}
}
};
if !can_add(&next_vb, current_size) {
break 'outer;
}
current_size += next_vb.encoded_size();
response_varbinds.push(next_vb);
}
if row_complete {
break;
}
}
}
Ok(Pdu {
pdu_type: PduType::Response,
request_id: pdu.request_id,
error_status: 0,
error_index: 0,
varbinds: response_varbinds,
})
}
pub(crate) fn find_handler(&self, oid: &Oid) -> Option<&RegisteredHandler> {
self.inner
.handlers
.iter()
.find(|&handler| handler.handler.handles(&handler.prefix, oid))
.map(|v| v as _)
}
async fn get_next_accessible_oid(
&self,
ctx: &RequestContext,
from_oid: &Oid,
) -> Option<VarBind> {
let mut search_from = from_oid.clone();
loop {
let candidate = self.get_next_oid(ctx, &search_from).await;
match candidate {
None => return None,
Some(ref next_vb) => {
if next_vb.oid <= search_from {
tracing::error!(
target: "async_snmp::agent",
from = %search_from,
got = %next_vb.oid,
"handler returned non-increasing OID in GETNEXT"
);
return None;
}
if ctx.version == Version::V1 && matches!(next_vb.value, Value::Counter64(_)) {
search_from = next_vb.oid.clone();
continue;
}
if let Some(ref vacm) = self.inner.vacm {
if vacm.check_access(ctx.read_view.as_ref(), &next_vb.oid) {
return candidate;
} else {
search_from = next_vb.oid.clone();
}
} else {
return candidate;
}
}
}
}
}
async fn get_next_oid(&self, ctx: &RequestContext, oid: &Oid) -> Option<VarBind> {
let mut best_result: Option<VarBind> = None;
for handler in &self.inner.handlers {
let prefix = &handler.prefix;
if prefix <= oid && !oid.starts_with(prefix) {
continue;
}
if let GetNextResult::Value(next) = handler.handler.get_next(ctx, oid).await {
if next.oid > *oid {
match &best_result {
None => best_result = Some(next),
Some(current) if next.oid < current.oid => best_result = Some(next),
_ => {}
}
}
}
}
best_result
}
}
impl Clone for Agent {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handler::{
BoxFuture, GetNextResult, GetResult, MibHandler, RequestContext, SecurityModel, SetResult,
};
use crate::message::SecurityLevel;
use crate::oid;
struct TestHandler;
impl MibHandler for TestHandler {
fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
Box::pin(async move {
if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
return GetResult::Value(Value::Integer(42));
}
if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
return GetResult::Value(Value::OctetString(Bytes::from_static(b"test")));
}
GetResult::NoSuchObject
})
}
fn get_next<'a>(
&'a self,
_ctx: &'a RequestContext,
oid: &'a Oid,
) -> BoxFuture<'a, GetNextResult> {
Box::pin(async move {
let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
if oid < &oid1 {
return GetNextResult::Value(VarBind::new(oid1, Value::Integer(42)));
}
if oid < &oid2 {
return GetNextResult::Value(VarBind::new(
oid2,
Value::OctetString(Bytes::from_static(b"test")),
));
}
GetNextResult::EndOfMibView
})
}
}
fn test_ctx() -> RequestContext {
RequestContext {
source: "127.0.0.1:12345".parse().unwrap(),
version: Version::V2c,
security_model: SecurityModel::V2c,
security_name: Bytes::from_static(b"public"),
security_level: SecurityLevel::NoAuthNoPriv,
context_name: Bytes::new(),
request_id: 1,
pdu_type: PduType::GetRequest,
group_name: None,
read_view: None,
write_view: None,
msg_max_size: None,
}
}
#[test]
fn test_agent_builder_defaults() {
let builder = AgentBuilder::new();
assert_eq!(builder.bind_addr, "0.0.0.0:161");
assert!(builder.communities.is_empty());
assert!(builder.usm_users.is_empty());
assert!(builder.handlers.is_empty());
}
#[test]
fn test_agent_builder_community() {
let builder = AgentBuilder::new()
.community(b"public")
.community(b"private");
assert_eq!(builder.communities.len(), 2);
}
#[test]
fn test_agent_builder_communities() {
let builder = AgentBuilder::new().communities(["public", "private"]);
assert_eq!(builder.communities.len(), 2);
}
#[test]
fn test_agent_builder_handler() {
let builder =
AgentBuilder::new().handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler));
assert_eq!(builder.handlers.len(), 1);
}
#[tokio::test]
async fn test_mib_handler_default_set() {
let handler = TestHandler;
let mut ctx = test_ctx();
ctx.pdu_type = PduType::SetRequest;
let result = handler
.test_set(&ctx, &oid!(1, 3, 6, 1), &Value::Integer(1))
.await;
assert_eq!(result, SetResult::NotWritable);
}
#[test]
fn test_mib_handler_handles() {
let handler = TestHandler;
let prefix = oid!(1, 3, 6, 1, 4, 1, 99999);
assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0)));
assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99999)));
assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99998)));
assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 100000)));
}
#[tokio::test]
async fn test_test_handler_get() {
let handler = TestHandler;
let ctx = test_ctx();
let result = handler
.get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
.await;
assert!(matches!(result, GetResult::Value(Value::Integer(42))));
let result = handler
.get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 99, 0))
.await;
assert!(matches!(result, GetResult::NoSuchObject));
}
#[tokio::test]
async fn test_test_handler_get_next() {
let handler = TestHandler;
let mut ctx = test_ctx();
ctx.pdu_type = PduType::GetNextRequest;
let next = handler.get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999)).await;
assert!(next.is_value());
if let GetNextResult::Value(vb) = next {
assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0));
}
let next = handler
.get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
.await;
assert!(next.is_value());
if let GetNextResult::Value(vb) = next {
assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0));
}
let next = handler
.get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
.await;
assert!(next.is_end_of_mib_view());
}
struct FiveOidHandler;
impl MibHandler for FiveOidHandler {
fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
Box::pin(async move {
for i in 1u32..=5 {
if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, i, 0) {
return GetResult::Value(Value::Integer(i as i32));
}
}
GetResult::NoSuchObject
})
}
fn get_next<'a>(
&'a self,
_ctx: &'a RequestContext,
oid: &'a Oid,
) -> BoxFuture<'a, GetNextResult> {
Box::pin(async move {
for i in 1u32..=5 {
let candidate = oid!(1, 3, 6, 1, 4, 1, 99999, i, 0);
if oid < &candidate {
return GetNextResult::Value(VarBind::new(
candidate,
Value::Integer(i as i32),
));
}
}
GetNextResult::EndOfMibView
})
}
}
async fn test_agent_with_restricted_vacm() -> Agent {
Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
.vacm(|v| {
v.group("public", SecurityModel::V2c, "readers")
.access("readers", |a| a.read_view("restricted"))
.view("restricted", |v| {
v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 2))
.include(oid!(1, 3, 6, 1, 4, 1, 99999, 4))
})
})
.build()
.await
.unwrap()
}
#[tokio::test]
async fn test_getbulk_vacm_filters_inaccessible_oids() {
let agent = test_agent_with_restricted_vacm().await;
let mut ctx = test_ctx();
ctx.pdu_type = PduType::GetBulkRequest;
ctx.read_view = Some(Bytes::from_static(b"restricted"));
let pdu = Pdu {
pdu_type: PduType::GetBulkRequest,
request_id: 1,
error_status: 0, error_index: 10, varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
};
let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
let returned_oids: Vec<&Oid> = response
.varbinds
.iter()
.filter(|vb| !matches!(vb.value, Value::EndOfMibView))
.map(|vb| &vb.oid)
.collect();
assert!(
returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)),
"expected .99999.2.0 in response, got: {:?}",
returned_oids
);
assert!(
returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0)),
"expected .99999.4.0 in response (walk must continue past denied OIDs), got: {:?}",
returned_oids
);
for &oid in &[
&oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
&oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
&oid!(1, 3, 6, 1, 4, 1, 99999, 5, 0),
] {
assert!(
!returned_oids.contains(&oid),
"GETBULK returned OID outside read view: {:?}",
oid
);
}
}
#[tokio::test]
async fn test_getbulk_non_repeaters_vacm_filtered() {
let agent = test_agent_with_restricted_vacm().await;
let mut ctx = test_ctx();
ctx.pdu_type = PduType::GetBulkRequest;
ctx.read_view = Some(Bytes::from_static(b"restricted"));
let pdu = Pdu {
pdu_type: PduType::GetBulkRequest,
request_id: 2,
error_status: 2, error_index: 0, varbinds: vec![
VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null),
VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0), Value::Null),
],
};
let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
assert_eq!(
response.varbinds[0].oid,
oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)
);
assert!(matches!(response.varbinds[0].value, Value::Integer(2)));
assert_eq!(response.varbinds[1].value, Value::EndOfMibView);
}
struct ThreeOidHandler;
impl MibHandler for ThreeOidHandler {
fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
Box::pin(async move {
if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
return GetResult::Value(Value::Integer(1));
}
if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
return GetResult::Value(Value::Integer(2));
}
if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0) {
return GetResult::Value(Value::Integer(3));
}
GetResult::NoSuchObject
})
}
fn get_next<'a>(
&'a self,
_ctx: &'a RequestContext,
oid: &'a Oid,
) -> BoxFuture<'a, GetNextResult> {
Box::pin(async move {
let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
let oid3 = oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0);
if oid < &oid1 {
return GetNextResult::Value(VarBind::new(oid1, Value::Integer(1)));
}
if oid < &oid2 {
return GetNextResult::Value(VarBind::new(oid2, Value::Integer(2)));
}
if oid < &oid3 {
return GetNextResult::Value(VarBind::new(oid3, Value::Integer(3)));
}
GetNextResult::EndOfMibView
})
}
}
async fn test_agent_with_gap_vacm() -> Agent {
Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(ThreeOidHandler))
.vacm(|v| {
v.group("public", SecurityModel::V2c, "readers")
.access("readers", |a| a.read_view("gap"))
.view("gap", |v| {
v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 1))
.include(oid!(1, 3, 6, 1, 4, 1, 99999, 3))
})
})
.build()
.await
.unwrap()
}
#[tokio::test]
async fn test_getnext_vacm_skips_inaccessible_continues_walk() {
let agent = test_agent_with_gap_vacm().await;
let mut ctx = test_ctx();
ctx.pdu_type = PduType::GetNextRequest;
ctx.read_view = Some(Bytes::from_static(b"gap"));
let pdu = Pdu {
pdu_type: PduType::GetNextRequest,
request_id: 1,
error_status: 0,
error_index: 0,
varbinds: vec![VarBind::new(
oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
Value::Null,
)],
};
let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
assert_eq!(response.varbinds.len(), 1);
assert_eq!(
response.varbinds[0].oid,
oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
"GETNEXT should skip denied .99999.2.0 and return accessible .99999.3.0"
);
assert!(matches!(response.varbinds[0].value, Value::Integer(3)));
}
#[tokio::test]
async fn test_getnext_vacm_all_remaining_denied_returns_end_of_mib() {
let agent = test_agent_with_restricted_vacm().await;
let mut ctx = test_ctx();
ctx.pdu_type = PduType::GetNextRequest;
ctx.read_view = Some(Bytes::from_static(b"restricted"));
let pdu = Pdu {
pdu_type: PduType::GetNextRequest,
request_id: 1,
error_status: 0,
error_index: 0,
varbinds: vec![VarBind::new(
oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0),
Value::Null,
)],
};
let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
assert_eq!(response.varbinds.len(), 1);
assert_eq!(
response.varbinds[0].value,
Value::EndOfMibView,
"GETNEXT should return EndOfMibView when all remaining OIDs are denied"
);
}
#[tokio::test]
async fn test_getbulk_without_vacm_returns_all_oids() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler))
.build()
.await
.unwrap();
let mut ctx = test_ctx();
ctx.pdu_type = PduType::GetBulkRequest;
let pdu = Pdu {
pdu_type: PduType::GetBulkRequest,
request_id: 1,
error_status: 0,
error_index: 10,
varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
};
let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
assert!(
response
.varbinds
.iter()
.any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
);
assert!(
response
.varbinds
.iter()
.any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
);
}
#[tokio::test]
async fn test_v1_getbulk_rejected() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler))
.build()
.await
.unwrap();
let mut ctx = test_ctx();
ctx.version = Version::V1;
ctx.security_model = SecurityModel::V1;
ctx.pdu_type = PduType::GetBulkRequest;
let pdu = Pdu {
pdu_type: PduType::GetBulkRequest,
request_id: 1,
error_status: 0,
error_index: 10,
varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
};
let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
assert_eq!(
ErrorStatus::from_i32(response.error_status),
ErrorStatus::GenErr,
"v1 GETBULK should be rejected"
);
}
struct Counter64Handler;
impl MibHandler for Counter64Handler {
fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
Box::pin(async move {
if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
return GetResult::Value(Value::Counter64(1_000_000_000_000));
}
if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
return GetResult::Value(Value::Integer(42));
}
GetResult::NoSuchObject
})
}
fn get_next<'a>(
&'a self,
_ctx: &'a RequestContext,
oid: &'a Oid,
) -> BoxFuture<'a, GetNextResult> {
Box::pin(async move {
let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
if oid < &oid1 {
return GetNextResult::Value(VarBind::new(
oid1,
Value::Counter64(1_000_000_000_000),
));
}
if oid < &oid2 {
return GetNextResult::Value(VarBind::new(oid2, Value::Integer(42)));
}
GetNextResult::EndOfMibView
})
}
}
async fn test_agent_with_counter64() -> Agent {
Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(Counter64Handler))
.build()
.await
.unwrap()
}
#[tokio::test]
async fn test_v1_get_filters_counter64() {
let agent = test_agent_with_counter64().await;
let mut ctx = test_ctx();
ctx.version = Version::V1;
ctx.security_model = SecurityModel::V1;
ctx.pdu_type = PduType::GetRequest;
let pdu = Pdu {
pdu_type: PduType::GetRequest,
request_id: 1,
error_status: 0,
error_index: 0,
varbinds: vec![VarBind::new(
oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
Value::Null,
)],
};
let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
assert_eq!(
ErrorStatus::from_i32(response.error_status),
ErrorStatus::NoSuchName,
"v1 GET of Counter64 should return noSuchName"
);
}
#[tokio::test]
async fn test_v2c_get_allows_counter64() {
let agent = test_agent_with_counter64().await;
let ctx = test_ctx();
let pdu = Pdu {
pdu_type: PduType::GetRequest,
request_id: 1,
error_status: 0,
error_index: 0,
varbinds: vec![VarBind::new(
oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
Value::Null,
)],
};
let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
assert_eq!(response.error_status, 0);
assert!(matches!(response.varbinds[0].value, Value::Counter64(_)));
}
#[tokio::test]
async fn test_getbulk_respects_v3_msg_max_size() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.max_message_size(65507) .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
.build()
.await
.unwrap();
let mut ctx_unlimited = test_ctx();
ctx_unlimited.pdu_type = PduType::GetBulkRequest;
ctx_unlimited.msg_max_size = None;
let pdu = Pdu {
pdu_type: PduType::GetBulkRequest,
request_id: 1,
error_status: 0, error_index: 10, varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
};
let full_response = agent.dispatch_request(&ctx_unlimited, &pdu).await.unwrap();
let full_count = full_response
.varbinds
.iter()
.filter(|vb| !matches!(vb.value, Value::EndOfMibView))
.count();
assert!(
full_count >= 3,
"expected at least 3 data varbinds without limit, got {}",
full_count
);
let mut ctx_limited = test_ctx();
ctx_limited.pdu_type = PduType::GetBulkRequest;
ctx_limited.msg_max_size = Some(150);
let limited_response = agent.dispatch_request(&ctx_limited, &pdu).await.unwrap();
let limited_count = limited_response
.varbinds
.iter()
.filter(|vb| !matches!(vb.value, Value::EndOfMibView))
.count();
assert!(
limited_count < full_count,
"V3 msg_max_size should limit response: got {} varbinds (unlimited: {})",
limited_count,
full_count
);
assert!(
limited_count > 0,
"should still return at least one varbind"
);
}
#[tokio::test]
async fn test_getbulk_msg_max_size_none_uses_agent_max() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.max_message_size(65507)
.handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
.without_builtin_handlers()
.build()
.await
.unwrap();
let mut ctx = test_ctx();
ctx.pdu_type = PduType::GetBulkRequest;
ctx.msg_max_size = None;
let pdu = Pdu {
pdu_type: PduType::GetBulkRequest,
request_id: 1,
error_status: 0,
error_index: 10,
varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
};
let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
let data_count = response
.varbinds
.iter()
.filter(|vb| !matches!(vb.value, Value::EndOfMibView))
.count();
assert_eq!(
data_count, 5,
"all 5 OIDs should be returned without msg_max_size limit"
);
}
#[tokio::test]
async fn test_v1_getnext_skips_counter64() {
let agent = test_agent_with_counter64().await;
let mut ctx = test_ctx();
ctx.version = Version::V1;
ctx.security_model = SecurityModel::V1;
ctx.pdu_type = PduType::GetNextRequest;
let pdu = Pdu {
pdu_type: PduType::GetNextRequest,
request_id: 1,
error_status: 0,
error_index: 0,
varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
};
let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
assert_eq!(response.error_status, 0, "should succeed");
assert_eq!(
response.varbinds[0].oid,
oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0),
"should skip Counter64 and return next non-Counter64 OID"
);
assert!(matches!(response.varbinds[0].value, Value::Integer(42)));
}
#[test]
fn test_engine_time_no_overflow() {
let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
assert_eq!(boots, 1);
assert_eq!(time, 1000);
}
#[test]
fn test_engine_time_zero_elapsed() {
let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
assert_eq!(boots, 1);
assert_eq!(time, 0);
}
#[test]
fn test_engine_time_just_below_max() {
let max = crate::v3::MAX_ENGINE_TIME;
let (boots, time) = crate::v3::compute_engine_boots_time(1, max as u64 - 1);
assert_eq!(boots, 1);
assert_eq!(time, max - 1);
}
#[test]
fn test_engine_time_at_max_wraps() {
let max = crate::v3::MAX_ENGINE_TIME;
let (boots, time) = crate::v3::compute_engine_boots_time(1, max as u64);
assert_eq!(
boots, 2,
"boots should increment when elapsed reaches MAX_ENGINE_TIME"
);
assert_eq!(time, 0, "time should wrap to 0");
}
#[test]
fn test_engine_time_past_max() {
let max = crate::v3::MAX_ENGINE_TIME;
let (boots, time) = crate::v3::compute_engine_boots_time(1, max as u64 + 500);
assert_eq!(boots, 2);
assert_eq!(time, 500);
}
#[test]
fn test_engine_time_multiple_wraps() {
let max = crate::v3::MAX_ENGINE_TIME;
let elapsed = max as u64 * 3 + 42;
let (boots, time) = crate::v3::compute_engine_boots_time(1, elapsed);
assert_eq!(boots, 4, "base 1 + 3 wraps = 4");
assert_eq!(time, 42);
}
#[test]
fn test_engine_time_boots_capped_at_max() {
let max = crate::v3::MAX_ENGINE_TIME;
let elapsed = max as u64 * (max as u64); let (boots, _time) = crate::v3::compute_engine_boots_time(1, elapsed);
assert_eq!(boots, max, "boots should be capped at MAX_ENGINE_TIME");
}
#[test]
fn test_engine_time_base_boots_preserved() {
let max = crate::v3::MAX_ENGINE_TIME;
let (boots, time) = crate::v3::compute_engine_boots_time(5, max as u64 + 100);
assert_eq!(boots, 6, "base 5 + 1 wrap = 6");
assert_eq!(time, 100);
}
#[test]
fn test_engine_time_high_base_boots_capped() {
let max = crate::v3::MAX_ENGINE_TIME;
let (boots, _time) = crate::v3::compute_engine_boots_time(max - 1, max as u64 * 2);
assert_eq!(boots, max, "should cap at MAX_ENGINE_TIME, not overflow");
}
#[tokio::test]
async fn test_engine_boots_builder() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.engine_boots(42)
.build()
.await
.unwrap();
assert_eq!(agent.engine_boots(), 42);
}
#[tokio::test]
async fn test_engine_boots_default() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.build()
.await
.unwrap();
assert_eq!(agent.engine_boots(), 1);
}
#[tokio::test]
async fn test_usm_counter_accessors_default_zero() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.build()
.await
.unwrap();
assert_eq!(agent.usm_unsupported_sec_levels(), 0);
assert_eq!(agent.usm_decryption_errors(), 0);
}
#[test]
fn test_builtin_mib_without_single() {
let builder = AgentBuilder::new().without_builtin_handler(BuiltinMib::UsmStats);
assert!(builder.disabled_builtins.contains(&BuiltinMib::UsmStats));
assert!(!builder.disabled_builtins.contains(&BuiltinMib::SnmpEngine));
assert!(!builder.disabled_builtins.contains(&BuiltinMib::MpdStats));
}
#[test]
fn test_builtin_mib_without_all() {
let builder = AgentBuilder::new().without_builtin_handlers();
assert!(builder.disabled_builtins.contains(&BuiltinMib::SnmpEngine));
assert!(builder.disabled_builtins.contains(&BuiltinMib::UsmStats));
assert!(builder.disabled_builtins.contains(&BuiltinMib::MpdStats));
}
#[tokio::test]
async fn test_uptime_hundredths() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.build()
.await
.unwrap();
let uptime = agent.uptime_hundredths();
assert!(
uptime < 100,
"uptime should be less than 1 second, got {}",
uptime
);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let uptime2 = agent.uptime_hundredths();
assert!(uptime2 > uptime, "uptime should increase after delay");
}
#[tokio::test]
async fn test_builtin_handlers_registered_by_default() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.build()
.await
.unwrap();
let ctx = test_ctx();
let handler = agent
.find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 4, 0))
.expect("snmpEngine handler should be registered");
let get_result = handler
.handler
.get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 4, 0))
.await;
assert!(matches!(get_result, GetResult::Value(Value::Integer(_))));
let handler = agent
.find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0))
.expect("USM stats handler should be registered");
let get_result = handler
.handler
.get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0))
.await;
assert!(matches!(get_result, GetResult::Value(Value::Counter32(0))));
let handler = agent
.find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
.expect("MPD stats handler should be registered");
let get_result = handler
.handler
.get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
.await;
assert!(matches!(get_result, GetResult::Value(Value::Counter32(0))));
}
#[tokio::test]
async fn test_builtin_handlers_disabled() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.without_builtin_handlers()
.build()
.await
.unwrap();
assert!(
agent
.find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0))
.is_none()
);
assert!(
agent
.find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0))
.is_none()
);
assert!(
agent
.find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
.is_none()
);
}
#[tokio::test]
async fn test_builtin_handler_selective_disable() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.without_builtin_handler(BuiltinMib::UsmStats)
.build()
.await
.unwrap();
assert!(
agent
.find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0))
.is_some()
);
assert!(
agent
.find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0))
.is_none()
);
assert!(
agent
.find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
.is_some()
);
}
}