1mod builtins;
71mod notification;
72mod request;
73mod response;
74mod set_handler;
75pub mod vacm;
76
77pub use vacm::{SecurityModel, VacmBuilder, VacmConfig, View, ViewCheckResult, ViewSubtree};
78
79use std::collections::{HashMap, HashSet};
80use std::net::SocketAddr;
81use std::sync::Arc;
82use std::sync::atomic::{AtomicU32, Ordering};
83use std::time::{Duration, Instant};
84
85use bytes::Bytes;
86use subtle::ConstantTimeEq;
87use tokio::net::UdpSocket;
88use tokio::sync::Semaphore;
89use tokio_util::sync::CancellationToken;
90use tracing::instrument;
91
92use std::io::IoSliceMut;
93
94use quinn_udp::{RecvMeta, Transmit, UdpSockRef, UdpSocketState};
95
96use crate::error::{Error, ErrorStatus, Result};
97use crate::handler::{GetNextResult, GetResult, MibHandler, RequestContext};
98use crate::notification::UsmConfig;
99use crate::oid;
100use crate::oid::Oid;
101use crate::pdu::{Pdu, PduType};
102use crate::util::bind_udp_socket;
103use crate::v3::process::UsmStats;
104use crate::v3::{SaltCounter, compute_engine_boots_time};
105use crate::value::Value;
106use crate::varbind::VarBind;
107use crate::version::Version;
108
109const DEFAULT_MAX_MESSAGE_SIZE: usize = 1472;
111
112const RESPONSE_OVERHEAD: usize = 100;
118
119const V3_AUTH_OVERHEAD: usize = 48;
122
123const V3_PRIV_OVERHEAD: usize = 20;
127
128fn v1_rejects_counter64(version: Version, value: &Value) -> bool {
132 version == Version::V1 && matches!(value, Value::Counter64(_))
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
142pub enum BuiltinMib {
143 SnmpEngine,
148 UsmStats,
154 MpdStats,
158}
159
160pub(crate) struct RegisteredHandler {
162 pub(crate) prefix: Oid,
163 pub(crate) handler: Arc<dyn MibHandler>,
164}
165
166pub struct AgentBuilder {
211 bind_addr: String,
212 communities: Vec<Vec<u8>>,
213 usm_users: HashMap<Bytes, UsmConfig>,
214 handlers: Vec<RegisteredHandler>,
215 engine_id: Option<Vec<u8>>,
216 engine_boots: u32,
217 max_message_size: usize,
218 max_concurrent_requests: Option<usize>,
219 recv_buffer_size: Option<usize>,
220 vacm: Option<VacmConfig>,
221 cancel: Option<CancellationToken>,
222 trap_sinks: Vec<(String, crate::client::Auth)>,
223 inform_timeout: Duration,
224 inform_retry: crate::client::Retry,
225 disabled_builtins: HashSet<BuiltinMib>,
226}
227
228impl AgentBuilder {
229 #[must_use]
239 pub fn new() -> Self {
240 Self {
241 bind_addr: "0.0.0.0:161".to_string(),
242 communities: Vec::new(),
243 usm_users: HashMap::new(),
244 handlers: Vec::new(),
245 engine_id: None,
246 engine_boots: 1,
247 max_message_size: DEFAULT_MAX_MESSAGE_SIZE,
248 max_concurrent_requests: Some(1000),
249 recv_buffer_size: Some(4 * 1024 * 1024), vacm: None,
251 cancel: None,
252 trap_sinks: Vec::new(),
253 inform_timeout: Duration::from_secs(5),
254 inform_retry: crate::client::Retry::default(),
255 disabled_builtins: HashSet::new(),
256 }
257 }
258
259 #[must_use]
297 pub fn bind(mut self, addr: impl Into<String>) -> Self {
298 self.bind_addr = addr.into();
299 self
300 }
301
302 #[must_use]
323 pub fn community(mut self, community: &[u8]) -> Self {
324 self.communities.push(community.to_vec());
325 self
326 }
327
328 #[must_use]
346 pub fn communities<I, C>(mut self, communities: I) -> Self
347 where
348 I: IntoIterator<Item = C>,
349 C: AsRef<[u8]>,
350 {
351 for c in communities {
352 self.communities.push(c.as_ref().to_vec());
353 }
354 self
355 }
356
357 #[must_use]
392 pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
393 where
394 F: FnOnce(UsmConfig) -> UsmConfig,
395 {
396 let username_bytes: Bytes = username.into();
397 let config = configure(UsmConfig::new(username_bytes.clone()));
398 self.usm_users.insert(username_bytes, config);
399 self
400 }
401
402 #[must_use]
423 pub fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
424 self.engine_id = Some(engine_id.into());
425 self
426 }
427
428 #[must_use]
453 pub fn engine_boots(mut self, boots: u32) -> Self {
454 self.engine_boots = boots;
455 self
456 }
457
458 #[must_use]
466 pub fn max_message_size(mut self, size: usize) -> Self {
467 self.max_message_size = size;
468 self
469 }
470
471 #[must_use]
479 pub fn max_concurrent_requests(mut self, limit: Option<usize>) -> Self {
480 self.max_concurrent_requests = limit;
481 self
482 }
483
484 #[must_use]
491 pub fn recv_buffer_size(mut self, size: Option<usize>) -> Self {
492 self.recv_buffer_size = size;
493 self
494 }
495
496 #[must_use]
537 pub fn handler(mut self, prefix: Oid, handler: Arc<dyn MibHandler>) -> Self {
538 self.handlers.push(RegisteredHandler { prefix, handler });
539 self
540 }
541
542 #[must_use]
581 pub fn vacm<F>(mut self, configure: F) -> Self
582 where
583 F: FnOnce(VacmBuilder) -> VacmBuilder,
584 {
585 let builder = VacmBuilder::new();
586 self.vacm = Some(configure(builder).build());
587 self
588 }
589
590 #[must_use]
594 pub fn cancel(mut self, token: CancellationToken) -> Self {
595 self.cancel = Some(token);
596 self
597 }
598
599 #[must_use]
624 pub fn trap_sink(
625 mut self,
626 dest: impl Into<String>,
627 auth: impl Into<crate::client::Auth>,
628 ) -> Self {
629 self.trap_sinks.push((dest.into(), auth.into()));
630 self
631 }
632
633 #[must_use]
637 pub fn inform_timeout(mut self, timeout: Duration) -> Self {
638 self.inform_timeout = timeout;
639 self
640 }
641
642 #[must_use]
647 pub fn inform_retry(mut self, retry: crate::client::Retry) -> Self {
648 self.inform_retry = retry;
649 self
650 }
651
652 #[must_use]
658 pub fn without_builtin_handler(mut self, mib: BuiltinMib) -> Self {
659 self.disabled_builtins.insert(mib);
660 self
661 }
662
663 #[must_use]
669 pub fn without_builtin_handlers(mut self) -> Self {
670 self.disabled_builtins.insert(BuiltinMib::SnmpEngine);
671 self.disabled_builtins.insert(BuiltinMib::UsmStats);
672 self.disabled_builtins.insert(BuiltinMib::MpdStats);
673 self
674 }
675
676 pub async fn build(mut self) -> Result<Agent> {
678 let bind_addr: std::net::SocketAddr = self.bind_addr.parse().map_err(|_| {
679 Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
680 })?;
681
682 let socket = bind_udp_socket(bind_addr, self.recv_buffer_size, None, false)
683 .await
684 .map_err(|e| Error::Network {
685 target: bind_addr,
686 source: e,
687 })?;
688
689 let local_addr = socket.local_addr().map_err(|e| Error::Network {
690 target: bind_addr,
691 source: e,
692 })?;
693
694 let socket_state =
695 UdpSocketState::new(UdpSockRef::from(&socket)).map_err(|e| Error::Network {
696 target: bind_addr,
697 source: e,
698 })?;
699
700 let engine_id: Bytes = self.engine_id.map_or_else(
702 || {
703 let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01]; let timestamp = std::time::SystemTime::now()
707 .duration_since(std::time::UNIX_EPOCH)
708 .unwrap_or_default()
709 .as_secs();
710 id.extend_from_slice(×tamp.to_be_bytes());
711 Bytes::from(id)
712 },
713 Bytes::from,
714 );
715
716 let cancel = self.cancel.unwrap_or_default();
717
718 let concurrency_limit = self
720 .max_concurrent_requests
721 .map(|n| Arc::new(Semaphore::new(n)));
722
723 let mut trap_sinks = Vec::with_capacity(self.trap_sinks.len());
725 for (dest_str, auth) in self.trap_sinks {
726 let dest: SocketAddr = dest_str.parse().map_err(|_| {
727 Error::Config(format!("invalid trap sink address: {dest_str}").into())
728 })?;
729 trap_sinks.push(notification::TrapSink::new(
730 dest,
731 auth,
732 self.inform_timeout,
733 self.inform_retry.clone(),
734 ));
735 }
736
737 let state = Arc::new(AgentState {
738 engine_id,
739 engine_boots: AtomicU32::new(self.engine_boots),
740 engine_time: AtomicU32::new(0),
741 engine_start: Instant::now(),
742 engine_boots_base: self.engine_boots,
743 max_message_size: self.max_message_size,
744 snmp_invalid_msgs: AtomicU32::new(0),
745 snmp_unknown_security_models: AtomicU32::new(0),
746 snmp_silent_drops: AtomicU32::new(0),
747 usm_stats: UsmStats::default(),
748 });
749
750 if !self.disabled_builtins.contains(&BuiltinMib::SnmpEngine) {
752 self.handlers.push(RegisteredHandler {
753 prefix: oid!(1, 3, 6, 1, 6, 3, 10, 2, 1),
754 handler: Arc::new(builtins::SnmpEngineHandler {
755 state: Arc::clone(&state),
756 }),
757 });
758 }
759 if !self.disabled_builtins.contains(&BuiltinMib::UsmStats) {
760 self.handlers.push(RegisteredHandler {
761 prefix: oid!(1, 3, 6, 1, 6, 3, 15, 1, 1),
762 handler: Arc::new(builtins::UsmStatsHandler {
763 state: Arc::clone(&state),
764 }),
765 });
766 }
767 if !self.disabled_builtins.contains(&BuiltinMib::MpdStats) {
768 self.handlers.push(RegisteredHandler {
769 prefix: oid!(1, 3, 6, 1, 6, 3, 11, 2, 1),
770 handler: Arc::new(builtins::MpdStatsHandler {
771 state: Arc::clone(&state),
772 }),
773 });
774 }
775
776 self.handlers
778 .sort_by_key(|h| std::cmp::Reverse(h.prefix.len()));
779
780 Ok(Agent {
781 inner: Arc::new(AgentInner {
782 socket: Arc::new(socket),
783 socket_state,
784 local_addr,
785 communities: self.communities,
786 usm_users: self.usm_users,
787 handlers: self.handlers,
788 state,
789 salt_counter: SaltCounter::new(),
790 concurrency_limit,
791 vacm: self.vacm,
792 cancel,
793 trap_sinks,
794 }),
795 })
796 }
797}
798
799impl Default for AgentBuilder {
800 fn default() -> Self {
801 Self::new()
802 }
803}
804
805pub(crate) struct AgentState {
807 pub(crate) engine_id: Bytes,
808 pub(crate) engine_boots: AtomicU32,
809 pub(crate) engine_time: AtomicU32,
810 pub(crate) engine_start: Instant,
811 pub(crate) engine_boots_base: u32,
813 pub(crate) max_message_size: usize,
814 pub(crate) snmp_invalid_msgs: AtomicU32,
818 pub(crate) snmp_unknown_security_models: AtomicU32,
821 pub(crate) snmp_silent_drops: AtomicU32,
824 pub(crate) usm_stats: UsmStats,
826}
827
828pub(crate) struct AgentInner {
830 pub(crate) socket: Arc<UdpSocket>,
831 pub(crate) socket_state: UdpSocketState,
832 pub(crate) local_addr: SocketAddr,
833 pub(crate) communities: Vec<Vec<u8>>,
834 pub(crate) usm_users: HashMap<Bytes, UsmConfig>,
835 pub(crate) handlers: Vec<RegisteredHandler>,
836 pub(crate) state: Arc<AgentState>,
837 pub(crate) salt_counter: SaltCounter,
838 pub(crate) concurrency_limit: Option<Arc<Semaphore>>,
839 pub(crate) vacm: Option<VacmConfig>,
840 pub(crate) cancel: CancellationToken,
842 pub(crate) trap_sinks: Vec<notification::TrapSink>,
844}
845
846pub struct Agent {
867 pub(crate) inner: Arc<AgentInner>,
868}
869
870impl Agent {
871 #[must_use]
873 pub fn builder() -> AgentBuilder {
874 AgentBuilder::new()
875 }
876
877 #[must_use]
879 pub fn local_addr(&self) -> SocketAddr {
880 self.inner.local_addr
881 }
882
883 #[must_use]
885 pub fn engine_id(&self) -> &[u8] {
886 &self.inner.state.engine_id
887 }
888
889 #[must_use]
895 pub fn engine_boots(&self) -> u32 {
896 self.inner.state.engine_boots.load(Ordering::Relaxed)
897 }
898
899 #[must_use]
901 pub fn engine_time(&self) -> u32 {
902 self.inner.state.engine_time.load(Ordering::Relaxed)
903 }
904
905 #[must_use]
909 pub fn cancel(&self) -> CancellationToken {
910 self.inner.cancel.clone()
911 }
912
913 #[must_use]
920 pub fn snmp_invalid_msgs(&self) -> u32 {
921 self.inner.state.snmp_invalid_msgs.load(Ordering::Relaxed)
922 }
923
924 #[must_use]
931 pub fn snmp_unknown_security_models(&self) -> u32 {
932 self.inner
933 .state
934 .snmp_unknown_security_models
935 .load(Ordering::Relaxed)
936 }
937
938 #[must_use]
947 pub fn snmp_silent_drops(&self) -> u32 {
948 self.inner.state.snmp_silent_drops.load(Ordering::Relaxed)
949 }
950
951 #[must_use]
959 pub fn usm_unknown_engine_ids(&self) -> u32 {
960 self.inner
961 .state
962 .usm_stats
963 .unknown_engine_ids
964 .load(Ordering::Relaxed)
965 }
966
967 #[must_use]
975 pub fn usm_unknown_usernames(&self) -> u32 {
976 self.inner
977 .state
978 .usm_stats
979 .unknown_usernames
980 .load(Ordering::Relaxed)
981 }
982
983 #[must_use]
990 pub fn usm_wrong_digests(&self) -> u32 {
991 self.inner
992 .state
993 .usm_stats
994 .wrong_digests
995 .load(Ordering::Relaxed)
996 }
997
998 #[must_use]
1008 pub fn usm_not_in_time_windows(&self) -> u32 {
1009 self.inner
1010 .state
1011 .usm_stats
1012 .not_in_time_windows
1013 .load(Ordering::Relaxed)
1014 }
1015
1016 #[must_use]
1024 pub fn usm_unsupported_sec_levels(&self) -> u32 {
1025 self.inner
1026 .state
1027 .usm_stats
1028 .unsupported_sec_levels
1029 .load(Ordering::Relaxed)
1030 }
1031
1032 #[must_use]
1040 pub fn usm_decryption_errors(&self) -> u32 {
1041 self.inner
1042 .state
1043 .usm_stats
1044 .decryption_errors
1045 .load(Ordering::Relaxed)
1046 }
1047
1048 #[must_use]
1053 pub fn uptime_hundredths(&self) -> u32 {
1054 let elapsed = self.inner.state.engine_start.elapsed();
1055 let centisecs = elapsed.as_millis() / 10;
1056 centisecs.min(u128::from(u32::MAX)) as u32
1057 }
1058
1059 #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
1065 pub async fn run(&self) -> Result<()> {
1066 let mut buf = vec![0u8; 65535];
1067
1068 loop {
1069 let recv_meta = tokio::select! {
1070 result = self.recv_packet(&mut buf) => {
1071 result?
1072 }
1073 () = self.inner.cancel.cancelled() => {
1074 tracing::info!(target: "async_snmp::agent", "agent shutdown requested");
1075 return Ok(());
1076 }
1077 };
1078
1079 let data = Bytes::copy_from_slice(&buf[..recv_meta.len]);
1080 let agent = self.clone();
1081
1082 let permit = if let Some(ref sem) = self.inner.concurrency_limit {
1083 Some(sem.clone().acquire_owned().await.expect("semaphore closed"))
1084 } else {
1085 None
1086 };
1087
1088 tokio::spawn(async move {
1089 agent.update_engine_time();
1090
1091 match agent.handle_request(data, recv_meta.addr).await {
1092 Ok(Some(response_bytes)) => {
1093 if response_bytes.len() > agent.inner.state.max_message_size {
1100 agent
1101 .inner
1102 .state
1103 .snmp_silent_drops
1104 .fetch_add(1, Ordering::Relaxed);
1105 tracing::debug!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, response_size = response_bytes.len(), max_size = agent.inner.state.max_message_size }, "response exceeds max message size, silently dropped");
1106 } else if let Err(e) =
1107 agent.send_response(&response_bytes, &recv_meta).await
1108 {
1109 tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "failed to send response");
1110 }
1111 }
1112 Ok(None) => {}
1113 Err(e) => {
1114 tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "error handling request");
1115 }
1116 }
1117
1118 drop(permit);
1119 });
1120 }
1121 }
1122
1123 async fn recv_packet(&self, buf: &mut [u8]) -> Result<RecvMeta> {
1124 let mut iov = [IoSliceMut::new(buf)];
1125 let mut meta = [RecvMeta::default()];
1126
1127 loop {
1128 self.inner
1129 .socket
1130 .readable()
1131 .await
1132 .map_err(|e| Error::Network {
1133 target: self.inner.local_addr,
1134 source: e,
1135 })?;
1136
1137 let result = self.inner.socket.try_io(tokio::io::Interest::READABLE, || {
1138 let sref = UdpSockRef::from(&*self.inner.socket);
1139 self.inner.socket_state.recv(sref, &mut iov, &mut meta)
1140 });
1141
1142 match result {
1143 Ok(n) if n > 0 => return Ok(meta[0]),
1144 Ok(_) => { }
1145 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { }
1147 Err(e) => {
1148 return Err(Error::Network {
1149 target: self.inner.local_addr,
1150 source: e,
1151 }
1152 .boxed());
1153 }
1154 }
1155 }
1156 }
1157
1158 async fn send_response(&self, data: &[u8], recv_meta: &RecvMeta) -> std::io::Result<()> {
1159 let transmit = Transmit {
1160 destination: recv_meta.addr,
1161 ecn: None,
1162 contents: data,
1163 segment_size: None,
1164 src_ip: recv_meta.dst_ip,
1165 };
1166
1167 loop {
1168 self.inner.socket.writable().await?;
1169
1170 let result = self.inner.socket.try_io(tokio::io::Interest::WRITABLE, || {
1171 let sref = UdpSockRef::from(&*self.inner.socket);
1172 self.inner.socket_state.try_send(sref, &transmit)
1173 });
1174
1175 match result {
1176 Ok(()) => return Ok(()),
1177 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { }
1179 Err(e) => return Err(e),
1180 }
1181 }
1182 }
1183
1184 async fn handle_request(&self, data: Bytes, source: SocketAddr) -> Result<Option<Bytes>> {
1188 match crate::message::peek_version(data.clone(), source)? {
1189 Version::V1 => self.handle_v1(data, source).await,
1190 Version::V2c => self.handle_v2c(data, source).await,
1191 Version::V3 => self.handle_v3(data, source).await,
1192 }
1193 }
1194
1195 fn update_engine_time(&self) {
1203 let total_secs = self.inner.state.engine_start.elapsed().as_secs();
1204 let (boots, time) =
1205 compute_engine_boots_time(self.inner.state.engine_boots_base, total_secs);
1206
1207 if boots != self.inner.state.engine_boots.load(Ordering::Relaxed)
1208 && boots > self.inner.state.engine_boots_base
1209 {
1210 tracing::warn!(
1211 target: "async_snmp::agent",
1212 engine_boots = boots,
1213 "engine time wrapped past MAX_ENGINE_TIME, incrementing engine boots"
1214 );
1215 }
1216
1217 self.inner
1218 .state
1219 .engine_boots
1220 .store(boots, Ordering::Relaxed);
1221 self.inner.state.engine_time.store(time, Ordering::Relaxed);
1222 }
1223
1224 pub(crate) fn validate_community(&self, community: &[u8]) -> bool {
1229 if self.inner.communities.is_empty() {
1230 return false;
1232 }
1233 let mut valid = false;
1237 for configured in &self.inner.communities {
1238 if configured.len() == community.len()
1240 && bool::from(configured.as_slice().ct_eq(community))
1241 {
1242 valid = true;
1243 }
1244 }
1245 valid
1246 }
1247
1248 async fn dispatch_request(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1250 match pdu.pdu_type {
1251 PduType::GetRequest => self.handle_get(ctx, pdu).await,
1252 PduType::GetNextRequest => self.handle_get_next(ctx, pdu).await,
1253 PduType::GetBulkRequest => {
1254 if ctx.version == Version::V1 {
1256 return Ok(pdu.to_error_response(ErrorStatus::GenErr, 0));
1257 }
1258 self.handle_get_bulk(ctx, pdu).await
1259 }
1260 PduType::SetRequest => self.handle_set(ctx, pdu).await,
1261 PduType::InformRequest => self.handle_inform(pdu),
1262 _ => {
1263 Ok(pdu.to_error_response(ErrorStatus::GenErr, 0))
1265 }
1266 }
1267 }
1268
1269 #[allow(
1275 clippy::unnecessary_wraps,
1276 reason = "TODO store received informs, which may be a fallible operation"
1277 )]
1278 #[allow(
1279 clippy::unused_self,
1280 reason = "TODO store received informs, which may require self"
1281 )]
1282 fn handle_inform(&self, pdu: &Pdu) -> Result<Pdu> {
1283 Ok(pdu.to_response())
1285 }
1286
1287 fn effective_max_size(&self, ctx: &RequestContext) -> usize {
1291 let agent_max = self.inner.state.max_message_size;
1292 match ctx.msg_max_size {
1293 Some(client_max) => agent_max.min(client_max as usize),
1294 None => agent_max,
1295 }
1296 }
1297
1298 fn response_overhead(&self, ctx: &RequestContext) -> usize {
1312 if ctx.version != Version::V3 {
1313 return RESPONSE_OVERHEAD;
1314 }
1315 let mut overhead = RESPONSE_OVERHEAD
1316 + 2 * self.inner.state.engine_id.len()
1317 + ctx.security_name.len()
1318 + ctx.context_name.len();
1319 if ctx.security_level.requires_auth() {
1320 overhead += V3_AUTH_OVERHEAD;
1321 }
1322 if ctx.security_level.requires_priv() {
1323 overhead += V3_PRIV_OVERHEAD;
1324 }
1325 overhead
1326 }
1327
1328 fn response_fits(varbinds: &[VarBind], overhead: usize, max_size: usize) -> bool {
1332 let size = overhead + varbinds.iter().map(VarBind::encoded_size).sum::<usize>();
1333 size <= max_size
1334 }
1335
1336 pub(super) fn too_big_response(pdu: &Pdu) -> Pdu {
1339 Pdu {
1340 pdu_type: PduType::Response,
1341 request_id: pdu.request_id,
1342 error_status: ErrorStatus::TooBig.as_i32(),
1343 error_index: 0,
1344 varbinds: Vec::new(),
1345 }
1346 }
1347
1348 async fn handle_get(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1350 let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
1351
1352 for (index, vb) in pdu.varbinds.iter().enumerate() {
1353 if let Some(ref vacm) = self.inner.vacm
1355 && !vacm.check_access(ctx.read_view.as_ref(), &vb.oid)
1356 {
1357 if ctx.version == Version::V1 {
1359 return Ok(pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32));
1360 }
1361 response_varbinds.push(VarBind::new(vb.oid.clone(), Value::NoSuchObject));
1363 continue;
1364 }
1365
1366 let result = if let Some(handler) = self.find_handler(&vb.oid) {
1367 handler.handler.get(ctx, &vb.oid).await
1368 } else {
1369 GetResult::NoSuchObject
1370 };
1371
1372 let response_value = match result {
1373 GetResult::Value(v) => {
1374 if v1_rejects_counter64(ctx.version, &v) {
1375 return Ok(
1376 pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1377 );
1378 }
1379 v
1380 }
1381 GetResult::NoSuchObject => {
1382 if ctx.version == Version::V1 {
1384 return Ok(
1385 pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1386 );
1387 }
1388 Value::NoSuchObject
1389 }
1390 GetResult::NoSuchInstance => {
1391 if ctx.version == Version::V1 {
1393 return Ok(
1394 pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1395 );
1396 }
1397 Value::NoSuchInstance
1398 }
1399 };
1400
1401 response_varbinds.push(VarBind::new(vb.oid.clone(), response_value));
1402 }
1403
1404 if !Self::response_fits(
1407 &response_varbinds,
1408 self.response_overhead(ctx),
1409 self.effective_max_size(ctx),
1410 ) {
1411 return Ok(Self::too_big_response(pdu));
1412 }
1413
1414 Ok(Pdu {
1415 pdu_type: PduType::Response,
1416 request_id: pdu.request_id,
1417 error_status: 0,
1418 error_index: 0,
1419 varbinds: response_varbinds,
1420 })
1421 }
1422
1423 async fn handle_get_next(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1425 let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
1426
1427 for (index, vb) in pdu.varbinds.iter().enumerate() {
1428 let next = self.get_next_accessible_oid(ctx, &vb.oid).await;
1432
1433 if let Some(next_vb) = next {
1434 response_varbinds.push(next_vb);
1435 } else {
1436 if ctx.version == Version::V1 {
1438 return Ok(pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32));
1439 }
1440 response_varbinds.push(VarBind::new(vb.oid.clone(), Value::EndOfMibView));
1441 }
1442 }
1443
1444 if !Self::response_fits(
1447 &response_varbinds,
1448 self.response_overhead(ctx),
1449 self.effective_max_size(ctx),
1450 ) {
1451 return Ok(Self::too_big_response(pdu));
1452 }
1453
1454 Ok(Pdu {
1455 pdu_type: PduType::Response,
1456 request_id: pdu.request_id,
1457 error_status: 0,
1458 error_index: 0,
1459 varbinds: response_varbinds,
1460 })
1461 }
1462
1463 async fn handle_get_bulk(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1468 let non_repeaters = pdu.error_status.try_into().unwrap_or(0);
1470 let max_repetitions = pdu.error_index.max(0);
1471
1472 let mut response_varbinds = Vec::new();
1473 let mut current_size: usize = self.response_overhead(ctx);
1474 let max_size = self.effective_max_size(ctx);
1475
1476 let can_add = |vb: &VarBind, current_size: usize| -> bool {
1478 current_size + vb.encoded_size() <= max_size
1479 };
1480
1481 for vb in pdu.varbinds.iter().take(non_repeaters) {
1483 let next = self.get_next_accessible_oid(ctx, &vb.oid).await;
1484
1485 let next_vb = match next {
1486 Some(next_vb) => next_vb,
1487 None => VarBind::new(vb.oid.clone(), Value::EndOfMibView),
1488 };
1489
1490 if !can_add(&next_vb, current_size) {
1491 if response_varbinds.is_empty() {
1493 return Ok(Self::too_big_response(pdu));
1494 }
1495 return Ok(Pdu {
1503 pdu_type: PduType::Response,
1504 request_id: pdu.request_id,
1505 error_status: 0,
1506 error_index: 0,
1507 varbinds: response_varbinds,
1508 });
1509 }
1510
1511 current_size += next_vb.encoded_size();
1512 response_varbinds.push(next_vb);
1513 }
1514
1515 if non_repeaters < pdu.varbinds.len() {
1517 let repeaters = &pdu.varbinds[non_repeaters..];
1518 let mut current_oids: Vec<Oid> = repeaters.iter().map(|vb| vb.oid.clone()).collect();
1519 let mut all_done = vec![false; repeaters.len()];
1520
1521 'outer: for _ in 0..max_repetitions {
1522 let mut row_complete = true;
1523 for (i, oid) in current_oids.iter_mut().enumerate() {
1524 let next_vb = if all_done[i] {
1525 VarBind::new(oid.clone(), Value::EndOfMibView)
1526 } else {
1527 let next = self.get_next_accessible_oid(ctx, oid).await;
1528
1529 if let Some(next_vb) = next {
1530 *oid = next_vb.oid.clone();
1531 row_complete = false;
1532 next_vb
1533 } else {
1534 all_done[i] = true;
1535 VarBind::new(oid.clone(), Value::EndOfMibView)
1536 }
1537 };
1538
1539 if !can_add(&next_vb, current_size) {
1541 if response_varbinds.is_empty() {
1550 return Ok(Self::too_big_response(pdu));
1551 }
1552 break 'outer;
1554 }
1555
1556 current_size += next_vb.encoded_size();
1557 response_varbinds.push(next_vb);
1558 }
1559
1560 if row_complete {
1561 break;
1562 }
1563 }
1564 }
1565
1566 Ok(Pdu {
1567 pdu_type: PduType::Response,
1568 request_id: pdu.request_id,
1569 error_status: 0,
1570 error_index: 0,
1571 varbinds: response_varbinds,
1572 })
1573 }
1574
1575 pub(crate) fn find_handler(&self, oid: &Oid) -> Option<&RegisteredHandler> {
1577 self.inner
1579 .handlers
1580 .iter()
1581 .find(|&handler| handler.handler.handles(&handler.prefix, oid))
1582 .map(|v| v as _)
1583 }
1584
1585 async fn get_next_accessible_oid(
1589 &self,
1590 ctx: &RequestContext,
1591 from_oid: &Oid,
1592 ) -> Option<VarBind> {
1593 let mut search_from = from_oid.clone();
1594 loop {
1595 let candidate = self.get_next_oid(ctx, &search_from).await;
1596 match candidate {
1597 None => return None,
1598 Some(ref next_vb) => {
1599 if next_vb.oid <= search_from {
1600 tracing::error!(
1601 target: "async_snmp::agent",
1602 from = %search_from,
1603 got = %next_vb.oid,
1604 "handler returned non-increasing OID in GETNEXT"
1605 );
1606 return None;
1607 }
1608 if v1_rejects_counter64(ctx.version, &next_vb.value) {
1609 search_from = next_vb.oid.clone();
1610 continue;
1611 }
1612 if let Some(ref vacm) = self.inner.vacm {
1613 if vacm.check_access(ctx.read_view.as_ref(), &next_vb.oid) {
1614 return candidate;
1615 }
1616 search_from = next_vb.oid.clone();
1617 } else {
1618 return candidate;
1619 }
1620 }
1621 }
1622 }
1623 }
1624
1625 async fn get_next_oid(&self, ctx: &RequestContext, oid: &Oid) -> Option<VarBind> {
1627 let mut best_result: Option<VarBind> = None;
1636
1637 for handler in &self.inner.handlers {
1638 let prefix = &handler.prefix;
1639 if prefix <= oid && !oid.starts_with(prefix) {
1640 continue;
1641 }
1642 if let GetNextResult::Value(next) = handler.handler.get_next(ctx, oid).await {
1643 if next.oid > *oid {
1645 match &best_result {
1646 None => best_result = Some(next),
1647 Some(current) if next.oid < current.oid => best_result = Some(next),
1648 _ => {}
1649 }
1650 }
1651 }
1652 }
1653
1654 best_result
1655 }
1656}
1657
1658impl Clone for Agent {
1659 fn clone(&self) -> Self {
1660 Self {
1661 inner: Arc::clone(&self.inner),
1662 }
1663 }
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668 use super::*;
1669 use crate::handler::{
1670 BoxFuture, GetNextResult, GetResult, MibHandler, RequestContext, SecurityModel, SetResult,
1671 };
1672 use crate::message::SecurityLevel;
1673 use crate::oid;
1674
1675 struct TestHandler;
1676
1677 impl MibHandler for TestHandler {
1678 fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1679 Box::pin(async move {
1680 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
1681 return GetResult::Value(Value::Integer(42));
1682 }
1683 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
1684 return GetResult::Value(Value::OctetString(Bytes::from_static(b"test")));
1685 }
1686 GetResult::NoSuchObject
1687 })
1688 }
1689
1690 fn get_next<'a>(
1691 &'a self,
1692 _ctx: &'a RequestContext,
1693 oid: &'a Oid,
1694 ) -> BoxFuture<'a, GetNextResult> {
1695 Box::pin(async move {
1696 let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
1697 let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
1698
1699 if oid < &oid1 {
1700 return GetNextResult::Value(VarBind::new(oid1, Value::Integer(42)));
1701 }
1702 if oid < &oid2 {
1703 return GetNextResult::Value(VarBind::new(
1704 oid2,
1705 Value::OctetString(Bytes::from_static(b"test")),
1706 ));
1707 }
1708 GetNextResult::EndOfMibView
1709 })
1710 }
1711 }
1712
1713 fn test_ctx() -> RequestContext {
1714 RequestContext {
1715 source: "127.0.0.1:12345".parse().unwrap(),
1716 version: Version::V2c,
1717 security_model: SecurityModel::V2c,
1718 security_name: Bytes::from_static(b"public"),
1719 security_level: SecurityLevel::NoAuthNoPriv,
1720 context_name: Bytes::new(),
1721 request_id: 1,
1722 pdu_type: PduType::GetRequest,
1723 group_name: None,
1724 read_view: None,
1725 write_view: None,
1726 msg_max_size: None,
1727 }
1728 }
1729
1730 #[test]
1731 fn test_agent_builder_defaults() {
1732 let builder = AgentBuilder::new();
1733 assert_eq!(builder.bind_addr, "0.0.0.0:161");
1734 assert!(builder.communities.is_empty());
1735 assert!(builder.usm_users.is_empty());
1736 assert!(builder.handlers.is_empty());
1737 }
1738
1739 #[test]
1740 fn test_agent_builder_community() {
1741 let builder = AgentBuilder::new()
1742 .community(b"public")
1743 .community(b"private");
1744 assert_eq!(builder.communities.len(), 2);
1745 }
1746
1747 #[test]
1748 fn test_agent_builder_communities() {
1749 let builder = AgentBuilder::new().communities(["public", "private"]);
1750 assert_eq!(builder.communities.len(), 2);
1751 }
1752
1753 #[test]
1754 fn test_agent_builder_handler() {
1755 let builder =
1756 AgentBuilder::new().handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler));
1757 assert_eq!(builder.handlers.len(), 1);
1758 }
1759
1760 #[tokio::test]
1761 async fn test_mib_handler_default_set() {
1762 let handler = TestHandler;
1763 let mut ctx = test_ctx();
1764 ctx.pdu_type = PduType::SetRequest;
1765
1766 let result = handler
1767 .test_set(&ctx, &oid!(1, 3, 6, 1), &Value::Integer(1))
1768 .await;
1769 assert_eq!(result, SetResult::NotWritable);
1770 }
1771
1772 #[test]
1773 fn test_mib_handler_handles() {
1774 let handler = TestHandler;
1775 let prefix = oid!(1, 3, 6, 1, 4, 1, 99_999);
1776
1777 assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_999, 1, 0)));
1779
1780 assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_999)));
1782
1783 assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_998)));
1786
1787 assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 100_000)));
1789 }
1790
1791 #[tokio::test]
1792 async fn test_test_handler_get() {
1793 let handler = TestHandler;
1794 let ctx = test_ctx();
1795
1796 let result = handler
1798 .get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
1799 .await;
1800 assert!(matches!(result, GetResult::Value(Value::Integer(42))));
1801
1802 let result = handler
1804 .get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 99, 0))
1805 .await;
1806 assert!(matches!(result, GetResult::NoSuchObject));
1807 }
1808
1809 #[tokio::test]
1810 async fn test_test_handler_get_next() {
1811 let handler = TestHandler;
1812 let mut ctx = test_ctx();
1813 ctx.pdu_type = PduType::GetNextRequest;
1814
1815 let next = handler.get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999)).await;
1817 assert!(next.is_value());
1818 if let GetNextResult::Value(vb) = next {
1819 assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0));
1820 }
1821
1822 let next = handler
1824 .get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
1825 .await;
1826 assert!(next.is_value());
1827 if let GetNextResult::Value(vb) = next {
1828 assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0));
1829 }
1830
1831 let next = handler
1833 .get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
1834 .await;
1835 assert!(next.is_end_of_mib_view());
1836 }
1837
1838 struct FiveOidHandler;
1840
1841 impl MibHandler for FiveOidHandler {
1842 fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1843 Box::pin(async move {
1844 for i in 1u16..=5 {
1845 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, i.into(), 0) {
1846 return GetResult::Value(Value::Integer(i.into()));
1847 }
1848 }
1849 GetResult::NoSuchObject
1850 })
1851 }
1852
1853 fn get_next<'a>(
1854 &'a self,
1855 _ctx: &'a RequestContext,
1856 oid: &'a Oid,
1857 ) -> BoxFuture<'a, GetNextResult> {
1858 Box::pin(async move {
1859 for i in 1u32..=5 {
1860 let candidate = oid!(1, 3, 6, 1, 4, 1, 99999, i, 0);
1861 if oid < &candidate {
1862 return GetNextResult::Value(VarBind::new(
1863 candidate,
1864 Value::Integer(i as i32),
1865 ));
1866 }
1867 }
1868 GetNextResult::EndOfMibView
1869 })
1870 }
1871 }
1872
1873 async fn test_agent_with_restricted_vacm() -> Agent {
1877 Agent::builder()
1878 .bind("127.0.0.1:0")
1879 .community(b"public")
1880 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
1881 .vacm(|v| {
1882 v.group("public", SecurityModel::V2c, "readers")
1883 .access("readers", |a| a.read_view("restricted"))
1884 .view("restricted", |v| {
1885 v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 2))
1886 .include(oid!(1, 3, 6, 1, 4, 1, 99999, 4))
1887 })
1888 })
1889 .build()
1890 .await
1891 .unwrap()
1892 }
1893
1894 #[tokio::test]
1895 async fn test_getbulk_vacm_filters_inaccessible_oids() {
1896 let agent = test_agent_with_restricted_vacm().await;
1897
1898 let mut ctx = test_ctx();
1899 ctx.pdu_type = PduType::GetBulkRequest;
1900 ctx.read_view = Some(Bytes::from_static(b"restricted"));
1901
1902 let pdu = Pdu {
1906 pdu_type: PduType::GetBulkRequest,
1907 request_id: 1,
1908 error_status: 0, error_index: 10, varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
1911 };
1912
1913 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1914
1915 let returned_oids: Vec<&Oid> = response
1917 .varbinds
1918 .iter()
1919 .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
1920 .map(|vb| &vb.oid)
1921 .collect();
1922
1923 assert!(
1925 returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)),
1926 "expected .99999.2.0 in response, got: {returned_oids:?}"
1927 );
1928 assert!(
1929 returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0)),
1930 "expected .99999.4.0 in response (walk must continue past denied OIDs), got: {returned_oids:?}"
1931 );
1932
1933 for &oid in &[
1935 &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
1936 &oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
1937 &oid!(1, 3, 6, 1, 4, 1, 99999, 5, 0),
1938 ] {
1939 assert!(
1940 !returned_oids.contains(&oid),
1941 "GETBULK returned OID outside read view: {oid:?}"
1942 );
1943 }
1944 }
1945
1946 #[tokio::test]
1947 async fn test_getbulk_non_repeaters_vacm_filtered() {
1948 let agent = test_agent_with_restricted_vacm().await;
1949
1950 let mut ctx = test_ctx();
1951 ctx.pdu_type = PduType::GetBulkRequest;
1952 ctx.read_view = Some(Bytes::from_static(b"restricted"));
1953
1954 let pdu = Pdu {
1960 pdu_type: PduType::GetBulkRequest,
1961 request_id: 2,
1962 error_status: 2, error_index: 0, varbinds: vec![
1965 VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null),
1966 VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0), Value::Null),
1967 ],
1968 };
1969
1970 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1971
1972 assert_eq!(
1974 response.varbinds[0].oid,
1975 oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)
1976 );
1977 assert!(matches!(response.varbinds[0].value, Value::Integer(2)));
1978
1979 assert_eq!(response.varbinds[1].value, Value::EndOfMibView);
1981 }
1982
1983 struct ThreeOidHandler;
1985
1986 impl MibHandler for ThreeOidHandler {
1987 fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1988 Box::pin(async move {
1989 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
1990 return GetResult::Value(Value::Integer(1));
1991 }
1992 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
1993 return GetResult::Value(Value::Integer(2));
1994 }
1995 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0) {
1996 return GetResult::Value(Value::Integer(3));
1997 }
1998 GetResult::NoSuchObject
1999 })
2000 }
2001
2002 fn get_next<'a>(
2003 &'a self,
2004 _ctx: &'a RequestContext,
2005 oid: &'a Oid,
2006 ) -> BoxFuture<'a, GetNextResult> {
2007 Box::pin(async move {
2008 let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
2009 let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
2010 let oid3 = oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0);
2011
2012 if oid < &oid1 {
2013 return GetNextResult::Value(VarBind::new(oid1, Value::Integer(1)));
2014 }
2015 if oid < &oid2 {
2016 return GetNextResult::Value(VarBind::new(oid2, Value::Integer(2)));
2017 }
2018 if oid < &oid3 {
2019 return GetNextResult::Value(VarBind::new(oid3, Value::Integer(3)));
2020 }
2021 GetNextResult::EndOfMibView
2022 })
2023 }
2024 }
2025
2026 async fn test_agent_with_gap_vacm() -> Agent {
2029 Agent::builder()
2030 .bind("127.0.0.1:0")
2031 .community(b"public")
2032 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(ThreeOidHandler))
2033 .vacm(|v| {
2034 v.group("public", SecurityModel::V2c, "readers")
2035 .access("readers", |a| a.read_view("gap"))
2036 .view("gap", |v| {
2037 v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 1))
2038 .include(oid!(1, 3, 6, 1, 4, 1, 99999, 3))
2039 })
2040 })
2041 .build()
2042 .await
2043 .unwrap()
2044 }
2045
2046 #[tokio::test]
2047 async fn test_getnext_vacm_skips_inaccessible_continues_walk() {
2048 let agent = test_agent_with_gap_vacm().await;
2052
2053 let mut ctx = test_ctx();
2054 ctx.pdu_type = PduType::GetNextRequest;
2055 ctx.read_view = Some(Bytes::from_static(b"gap"));
2056
2057 let pdu = Pdu {
2058 pdu_type: PduType::GetNextRequest,
2059 request_id: 1,
2060 error_status: 0,
2061 error_index: 0,
2062 varbinds: vec![VarBind::new(
2063 oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2064 Value::Null,
2065 )],
2066 };
2067
2068 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2069 assert_eq!(response.varbinds.len(), 1);
2070 assert_eq!(
2071 response.varbinds[0].oid,
2072 oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
2073 "GETNEXT should skip denied .99999.2.0 and return accessible .99999.3.0"
2074 );
2075 assert!(matches!(response.varbinds[0].value, Value::Integer(3)));
2076 }
2077
2078 #[tokio::test]
2079 async fn test_getnext_vacm_all_remaining_denied_returns_end_of_mib() {
2080 let agent = test_agent_with_restricted_vacm().await;
2084
2085 let mut ctx = test_ctx();
2086 ctx.pdu_type = PduType::GetNextRequest;
2087 ctx.read_view = Some(Bytes::from_static(b"restricted"));
2088
2089 let pdu = Pdu {
2090 pdu_type: PduType::GetNextRequest,
2091 request_id: 1,
2092 error_status: 0,
2093 error_index: 0,
2094 varbinds: vec![VarBind::new(
2095 oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0),
2096 Value::Null,
2097 )],
2098 };
2099
2100 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2101 assert_eq!(response.varbinds.len(), 1);
2102 assert_eq!(
2103 response.varbinds[0].value,
2104 Value::EndOfMibView,
2105 "GETNEXT should return EndOfMibView when all remaining OIDs are denied"
2106 );
2107 }
2108
2109 #[tokio::test]
2110 async fn test_getbulk_without_vacm_returns_all_oids() {
2111 let agent = Agent::builder()
2113 .bind("127.0.0.1:0")
2114 .community(b"public")
2115 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler))
2116 .build()
2117 .await
2118 .unwrap();
2119
2120 let mut ctx = test_ctx();
2121 ctx.pdu_type = PduType::GetBulkRequest;
2122
2123 let pdu = Pdu {
2124 pdu_type: PduType::GetBulkRequest,
2125 request_id: 1,
2126 error_status: 0,
2127 error_index: 10,
2128 varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2129 };
2130
2131 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2132
2133 assert!(
2135 response
2136 .varbinds
2137 .iter()
2138 .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
2139 );
2140 assert!(
2141 response
2142 .varbinds
2143 .iter()
2144 .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
2145 );
2146 }
2147
2148 #[tokio::test]
2149 async fn test_v1_getbulk_rejected() {
2150 let agent = Agent::builder()
2152 .bind("127.0.0.1:0")
2153 .community(b"public")
2154 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler))
2155 .build()
2156 .await
2157 .unwrap();
2158
2159 let mut ctx = test_ctx();
2160 ctx.version = Version::V1;
2161 ctx.security_model = SecurityModel::V1;
2162 ctx.pdu_type = PduType::GetBulkRequest;
2163
2164 let pdu = Pdu {
2165 pdu_type: PduType::GetBulkRequest,
2166 request_id: 1,
2167 error_status: 0,
2168 error_index: 10,
2169 varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2170 };
2171
2172 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2173 assert_eq!(
2174 ErrorStatus::from_i32(response.error_status),
2175 ErrorStatus::GenErr,
2176 "v1 GETBULK should be rejected"
2177 );
2178 }
2179
2180 struct Counter64Handler;
2182
2183 impl MibHandler for Counter64Handler {
2184 fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
2185 Box::pin(async move {
2186 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
2187 return GetResult::Value(Value::Counter64(1_000_000_000_000));
2188 }
2189 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
2190 return GetResult::Value(Value::Integer(42));
2191 }
2192 GetResult::NoSuchObject
2193 })
2194 }
2195
2196 fn get_next<'a>(
2197 &'a self,
2198 _ctx: &'a RequestContext,
2199 oid: &'a Oid,
2200 ) -> BoxFuture<'a, GetNextResult> {
2201 Box::pin(async move {
2202 let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
2203 let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
2204
2205 if oid < &oid1 {
2206 return GetNextResult::Value(VarBind::new(
2207 oid1,
2208 Value::Counter64(1_000_000_000_000),
2209 ));
2210 }
2211 if oid < &oid2 {
2212 return GetNextResult::Value(VarBind::new(oid2, Value::Integer(42)));
2213 }
2214 GetNextResult::EndOfMibView
2215 })
2216 }
2217 }
2218
2219 async fn test_agent_with_counter64() -> Agent {
2220 Agent::builder()
2221 .bind("127.0.0.1:0")
2222 .community(b"public")
2223 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(Counter64Handler))
2224 .build()
2225 .await
2226 .unwrap()
2227 }
2228
2229 #[tokio::test]
2230 async fn test_v1_get_filters_counter64() {
2231 let agent = test_agent_with_counter64().await;
2234
2235 let mut ctx = test_ctx();
2236 ctx.version = Version::V1;
2237 ctx.security_model = SecurityModel::V1;
2238 ctx.pdu_type = PduType::GetRequest;
2239
2240 let pdu = Pdu {
2241 pdu_type: PduType::GetRequest,
2242 request_id: 1,
2243 error_status: 0,
2244 error_index: 0,
2245 varbinds: vec![VarBind::new(
2246 oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2247 Value::Null,
2248 )],
2249 };
2250
2251 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2252 assert_eq!(
2253 ErrorStatus::from_i32(response.error_status),
2254 ErrorStatus::NoSuchName,
2255 "v1 GET of Counter64 should return noSuchName"
2256 );
2257 }
2258
2259 #[tokio::test]
2260 async fn test_v2c_get_allows_counter64() {
2261 let agent = test_agent_with_counter64().await;
2263
2264 let ctx = test_ctx(); let pdu = Pdu {
2267 pdu_type: PduType::GetRequest,
2268 request_id: 1,
2269 error_status: 0,
2270 error_index: 0,
2271 varbinds: vec![VarBind::new(
2272 oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2273 Value::Null,
2274 )],
2275 };
2276
2277 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2278 assert_eq!(response.error_status, 0);
2279 assert!(matches!(response.varbinds[0].value, Value::Counter64(_)));
2280 }
2281
2282 #[tokio::test]
2283 async fn test_getbulk_respects_v3_msg_max_size() {
2284 let agent = Agent::builder()
2289 .bind("127.0.0.1:0")
2290 .community(b"public")
2291 .max_message_size(65507) .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
2293 .build()
2294 .await
2295 .unwrap();
2296
2297 let mut ctx_unlimited = test_ctx();
2299 ctx_unlimited.pdu_type = PduType::GetBulkRequest;
2300 ctx_unlimited.msg_max_size = None;
2301
2302 let pdu = Pdu {
2303 pdu_type: PduType::GetBulkRequest,
2304 request_id: 1,
2305 error_status: 0, error_index: 10, varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2308 };
2309
2310 let full_response = agent.dispatch_request(&ctx_unlimited, &pdu).await.unwrap();
2311 let full_count = full_response
2312 .varbinds
2313 .iter()
2314 .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2315 .count();
2316 assert!(
2317 full_count >= 3,
2318 "expected at least 3 data varbinds without limit, got {full_count}"
2319 );
2320
2321 let mut ctx_limited = test_ctx();
2326 ctx_limited.pdu_type = PduType::GetBulkRequest;
2327 ctx_limited.msg_max_size = Some(150); let limited_response = agent.dispatch_request(&ctx_limited, &pdu).await.unwrap();
2330 let limited_count = limited_response
2331 .varbinds
2332 .iter()
2333 .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2334 .count();
2335
2336 assert!(
2337 limited_count < full_count,
2338 "V3 msg_max_size should limit response: got {limited_count} varbinds (unlimited: {full_count})"
2339 );
2340 assert!(
2341 limited_count > 0,
2342 "should still return at least one varbind"
2343 );
2344 }
2345
2346 #[tokio::test]
2347 async fn test_response_overhead_scales_with_v3_security_level() {
2348 let engine_id = vec![0u8; 17];
2350 let agent = Agent::builder()
2351 .bind("127.0.0.1:0")
2352 .community(b"public")
2353 .engine_id(engine_id.clone())
2354 .build()
2355 .await
2356 .unwrap();
2357
2358 let v2c = test_ctx();
2361 assert_eq!(agent.response_overhead(&v2c), RESPONSE_OVERHEAD);
2362
2363 let username = Bytes::from_static(b"user");
2364 let variable = 2 * engine_id.len() + username.len(); let mut noauth = test_ctx();
2367 noauth.version = Version::V3;
2368 noauth.security_level = SecurityLevel::NoAuthNoPriv;
2369 noauth.security_name = username.clone();
2370 assert_eq!(
2371 agent.response_overhead(&noauth),
2372 RESPONSE_OVERHEAD + variable
2373 );
2374
2375 let mut authnopriv = noauth.clone();
2376 authnopriv.security_level = SecurityLevel::AuthNoPriv;
2377 assert_eq!(
2378 agent.response_overhead(&authnopriv),
2379 RESPONSE_OVERHEAD + variable + V3_AUTH_OVERHEAD
2380 );
2381
2382 let mut authpriv = noauth.clone();
2383 authpriv.security_level = SecurityLevel::AuthPriv;
2384 assert_eq!(
2385 agent.response_overhead(&authpriv),
2386 RESPONSE_OVERHEAD + variable + V3_AUTH_OVERHEAD + V3_PRIV_OVERHEAD
2387 );
2388
2389 assert!(agent.response_overhead(&v2c) < agent.response_overhead(&noauth));
2391 assert!(agent.response_overhead(&noauth) < agent.response_overhead(&authnopriv));
2392 assert!(agent.response_overhead(&authnopriv) < agent.response_overhead(&authpriv));
2393 }
2394
2395 #[tokio::test]
2396 async fn test_getbulk_authpriv_budgets_for_wrapper() {
2397 let agent = Agent::builder()
2402 .bind("127.0.0.1:0")
2403 .community(b"public")
2404 .max_message_size(65507)
2405 .engine_id(vec![0u8; 17])
2406 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
2407 .build()
2408 .await
2409 .unwrap();
2410
2411 let pdu = Pdu {
2412 pdu_type: PduType::GetBulkRequest,
2413 request_id: 1,
2414 error_status: 0, error_index: 10, varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2417 };
2418
2419 let limit = 200;
2422
2423 let mut v2c = test_ctx();
2424 v2c.pdu_type = PduType::GetBulkRequest;
2425 v2c.msg_max_size = Some(limit);
2426 let v2c_count = agent
2427 .dispatch_request(&v2c, &pdu)
2428 .await
2429 .unwrap()
2430 .varbinds
2431 .iter()
2432 .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2433 .count();
2434
2435 let mut authpriv = test_ctx();
2436 authpriv.version = Version::V3;
2437 authpriv.security_level = SecurityLevel::AuthPriv;
2438 authpriv.security_name = Bytes::from_static(b"user");
2439 authpriv.pdu_type = PduType::GetBulkRequest;
2440 authpriv.msg_max_size = Some(limit);
2441 let authpriv_count = agent
2442 .dispatch_request(&authpriv, &pdu)
2443 .await
2444 .unwrap()
2445 .varbinds
2446 .iter()
2447 .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2448 .count();
2449
2450 assert!(
2451 authpriv_count < v2c_count,
2452 "authPriv should budget fewer varbinds than v2c for the same \
2453 msgMaxSize: authpriv={authpriv_count}, v2c={v2c_count}"
2454 );
2455 }
2456
2457 struct MixedSizeHandler;
2460
2461 impl MibHandler for MixedSizeHandler {
2462 fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
2463 Box::pin(async move {
2464 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0)
2465 || oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)
2466 {
2467 return GetResult::Value(Value::OctetString(Bytes::from(vec![0xAB; 200])));
2468 }
2469 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 9, 0) {
2470 return GetResult::Value(Value::Integer(7));
2471 }
2472 GetResult::NoSuchObject
2473 })
2474 }
2475
2476 fn get_next<'a>(
2477 &'a self,
2478 _ctx: &'a RequestContext,
2479 oid: &'a Oid,
2480 ) -> BoxFuture<'a, GetNextResult> {
2481 Box::pin(async move {
2482 let big1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
2483 let big2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
2484 let small = oid!(1, 3, 6, 1, 4, 1, 99999, 9, 0);
2485 if oid < &big1 {
2486 return GetNextResult::Value(VarBind::new(
2487 big1,
2488 Value::OctetString(Bytes::from(vec![0xAB; 200])),
2489 ));
2490 }
2491 if oid < &big2 {
2492 return GetNextResult::Value(VarBind::new(
2493 big2,
2494 Value::OctetString(Bytes::from(vec![0xAB; 200])),
2495 ));
2496 }
2497 if oid < &small {
2498 return GetNextResult::Value(VarBind::new(small, Value::Integer(7)));
2499 }
2500 GetNextResult::EndOfMibView
2501 })
2502 }
2503 }
2504
2505 #[tokio::test]
2506 async fn test_getbulk_dropped_non_repeater_omits_repeaters() {
2507 let agent = Agent::builder()
2513 .bind("127.0.0.1:0")
2514 .community(b"public")
2515 .max_message_size(65507)
2516 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(MixedSizeHandler))
2517 .without_builtin_handlers()
2518 .build()
2519 .await
2520 .unwrap();
2521
2522 let big_vb = VarBind::new(
2525 oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2526 Value::OctetString(Bytes::from(vec![0xAB; 200])),
2527 );
2528 let small_vb = VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 9, 0), Value::Integer(7));
2529 let max = RESPONSE_OVERHEAD + big_vb.encoded_size() + small_vb.encoded_size();
2530
2531 let mut ctx = test_ctx();
2532 ctx.pdu_type = PduType::GetBulkRequest;
2533 ctx.msg_max_size = Some(max as u32);
2534
2535 let pdu = Pdu {
2536 pdu_type: PduType::GetBulkRequest,
2537 request_id: 1,
2538 error_status: 2, error_index: 2, varbinds: vec![
2541 VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 1), Value::Null),
2542 VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 2), Value::Null),
2543 VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 9), Value::Null),
2544 ],
2545 };
2546
2547 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2548
2549 assert_eq!(
2551 response.varbinds.len(),
2552 1,
2553 "expected exactly the non-repeater prefix, got {:?}",
2554 response
2555 .varbinds
2556 .iter()
2557 .map(|vb| &vb.oid)
2558 .collect::<Vec<_>>()
2559 );
2560 assert_eq!(
2561 response.varbinds[0].oid,
2562 oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0)
2563 );
2564 assert!(
2566 !response
2567 .varbinds
2568 .iter()
2569 .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 9, 0)),
2570 "repeater varbind leaked into response after a dropped non-repeater"
2571 );
2572 }
2573
2574 #[tokio::test]
2575 async fn test_getbulk_too_big_has_empty_varbinds() {
2576 let agent = Agent::builder()
2579 .bind("127.0.0.1:0")
2580 .community(b"public")
2581 .max_message_size(65507)
2582 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(MixedSizeHandler))
2583 .without_builtin_handlers()
2584 .build()
2585 .await
2586 .unwrap();
2587
2588 let mut ctx = test_ctx();
2589 ctx.pdu_type = PduType::GetBulkRequest;
2590 ctx.msg_max_size = Some((RESPONSE_OVERHEAD - 1) as u32);
2592
2593 let pdu = Pdu {
2594 pdu_type: PduType::GetBulkRequest,
2595 request_id: 1,
2596 error_status: 2, error_index: 2, varbinds: vec![
2599 VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 1), Value::Null),
2600 VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 2), Value::Null),
2601 VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 9), Value::Null),
2602 ],
2603 };
2604
2605 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2606
2607 assert_eq!(response.error_status, ErrorStatus::TooBig.as_i32());
2608 assert!(
2609 response.varbinds.is_empty(),
2610 "tooBig Response must have empty varbinds, got {}",
2611 response.varbinds.len()
2612 );
2613 }
2614
2615 #[tokio::test]
2616 async fn test_getbulk_too_big_zero_non_repeaters_first_repeater_oversized() {
2617 let agent = Agent::builder()
2624 .bind("127.0.0.1:0")
2625 .community(b"public")
2626 .max_message_size(65507)
2627 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(MixedSizeHandler))
2628 .without_builtin_handlers()
2629 .build()
2630 .await
2631 .unwrap();
2632
2633 let big_vb = VarBind::new(
2638 oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2639 Value::OctetString(Bytes::from(vec![0xAB; 200])),
2640 );
2641 let max = RESPONSE_OVERHEAD + big_vb.encoded_size() - 1;
2642
2643 let mut ctx = test_ctx();
2644 ctx.pdu_type = PduType::GetBulkRequest;
2645 ctx.msg_max_size = Some(max as u32);
2646
2647 let pdu = Pdu {
2648 pdu_type: PduType::GetBulkRequest,
2649 request_id: 1,
2650 error_status: 0, error_index: 5, varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 1), Value::Null)],
2653 };
2654
2655 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2656
2657 assert_eq!(
2658 response.error_status,
2659 ErrorStatus::TooBig.as_i32(),
2660 "first oversized repeater varbind (non_repeaters == 0) must yield tooBig"
2661 );
2662 assert!(
2663 response.varbinds.is_empty(),
2664 "tooBig Response must have empty varbinds, got {}",
2665 response.varbinds.len()
2666 );
2667 }
2668
2669 #[tokio::test]
2670 async fn test_getbulk_msg_max_size_none_uses_agent_max() {
2671 let agent = Agent::builder()
2674 .bind("127.0.0.1:0")
2675 .community(b"public")
2676 .max_message_size(65507)
2677 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
2678 .without_builtin_handlers()
2679 .build()
2680 .await
2681 .unwrap();
2682
2683 let mut ctx = test_ctx();
2684 ctx.pdu_type = PduType::GetBulkRequest;
2685 ctx.msg_max_size = None; let pdu = Pdu {
2688 pdu_type: PduType::GetBulkRequest,
2689 request_id: 1,
2690 error_status: 0,
2691 error_index: 10,
2692 varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2693 };
2694
2695 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2696 let data_count = response
2697 .varbinds
2698 .iter()
2699 .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2700 .count();
2701 assert_eq!(
2702 data_count, 5,
2703 "all 5 OIDs should be returned without msg_max_size limit"
2704 );
2705 }
2706
2707 #[tokio::test]
2708 async fn test_v1_getnext_skips_counter64() {
2709 let agent = test_agent_with_counter64().await;
2713
2714 let mut ctx = test_ctx();
2715 ctx.version = Version::V1;
2716 ctx.security_model = SecurityModel::V1;
2717 ctx.pdu_type = PduType::GetNextRequest;
2718
2719 let pdu = Pdu {
2720 pdu_type: PduType::GetNextRequest,
2721 request_id: 1,
2722 error_status: 0,
2723 error_index: 0,
2724 varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2725 };
2726
2727 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2728 assert_eq!(response.error_status, 0, "should succeed");
2729 assert_eq!(
2730 response.varbinds[0].oid,
2731 oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0),
2732 "should skip Counter64 and return next non-Counter64 OID"
2733 );
2734 assert!(matches!(response.varbinds[0].value, Value::Integer(42)));
2735 }
2736
2737 #[test]
2738 fn test_engine_time_no_overflow() {
2739 let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
2741 assert_eq!(boots, 1);
2742 assert_eq!(time, 1000);
2743 }
2744
2745 #[test]
2746 fn test_engine_time_zero_elapsed() {
2747 let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
2748 assert_eq!(boots, 1);
2749 assert_eq!(time, 0);
2750 }
2751
2752 #[test]
2753 fn test_engine_time_just_below_max() {
2754 let max = crate::v3::MAX_ENGINE_TIME;
2755 let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max) - 1);
2756 assert_eq!(boots, 1);
2757 assert_eq!(time, max - 1);
2758 }
2759
2760 #[test]
2761 fn test_engine_time_at_max_wraps() {
2762 let max = crate::v3::MAX_ENGINE_TIME;
2764 let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max));
2765 assert_eq!(
2766 boots, 2,
2767 "boots should increment when elapsed reaches MAX_ENGINE_TIME"
2768 );
2769 assert_eq!(time, 0, "time should wrap to 0");
2770 }
2771
2772 #[test]
2773 fn test_engine_time_past_max() {
2774 let max = crate::v3::MAX_ENGINE_TIME;
2776 let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max) + 500);
2777 assert_eq!(boots, 2);
2778 assert_eq!(time, 500);
2779 }
2780
2781 #[test]
2782 fn test_engine_time_multiple_wraps() {
2783 let max = crate::v3::MAX_ENGINE_TIME;
2785 let elapsed = u64::from(max) * 3 + 42;
2786 let (boots, time) = crate::v3::compute_engine_boots_time(1, elapsed);
2787 assert_eq!(boots, 4, "base 1 + 3 wraps = 4");
2788 assert_eq!(time, 42);
2789 }
2790
2791 #[test]
2792 fn test_engine_time_boots_capped_at_max() {
2793 let max = crate::v3::MAX_ENGINE_TIME;
2795 let elapsed = u64::from(max) * u64::from(max); let (boots, _time) = crate::v3::compute_engine_boots_time(1, elapsed);
2797 assert_eq!(boots, max, "boots should be capped at MAX_ENGINE_TIME");
2798 }
2799
2800 #[test]
2801 fn test_engine_time_base_boots_preserved() {
2802 let max = crate::v3::MAX_ENGINE_TIME;
2804 let (boots, time) = crate::v3::compute_engine_boots_time(5, u64::from(max) + 100);
2805 assert_eq!(boots, 6, "base 5 + 1 wrap = 6");
2806 assert_eq!(time, 100);
2807 }
2808
2809 #[test]
2810 fn test_engine_time_high_base_boots_capped() {
2811 let max = crate::v3::MAX_ENGINE_TIME;
2813 let (boots, _time) = crate::v3::compute_engine_boots_time(max - 1, u64::from(max) * 2);
2814 assert_eq!(boots, max, "should cap at MAX_ENGINE_TIME, not overflow");
2815 }
2816
2817 #[tokio::test]
2818 async fn test_engine_boots_builder() {
2819 let agent = Agent::builder()
2821 .bind("127.0.0.1:0")
2822 .community(b"public")
2823 .engine_boots(42)
2824 .build()
2825 .await
2826 .unwrap();
2827
2828 assert_eq!(agent.engine_boots(), 42);
2829 }
2830
2831 #[tokio::test]
2832 async fn test_engine_boots_default() {
2833 let agent = Agent::builder()
2835 .bind("127.0.0.1:0")
2836 .community(b"public")
2837 .build()
2838 .await
2839 .unwrap();
2840
2841 assert_eq!(agent.engine_boots(), 1);
2842 }
2843
2844 #[tokio::test]
2845 async fn test_usm_counter_accessors_default_zero() {
2846 let agent = Agent::builder()
2847 .bind("127.0.0.1:0")
2848 .community(b"public")
2849 .build()
2850 .await
2851 .unwrap();
2852
2853 assert_eq!(agent.usm_unsupported_sec_levels(), 0);
2854 assert_eq!(agent.usm_decryption_errors(), 0);
2855 }
2856
2857 #[test]
2858 fn test_builtin_mib_without_single() {
2859 let builder = AgentBuilder::new().without_builtin_handler(BuiltinMib::UsmStats);
2860 assert!(builder.disabled_builtins.contains(&BuiltinMib::UsmStats));
2861 assert!(!builder.disabled_builtins.contains(&BuiltinMib::SnmpEngine));
2862 assert!(!builder.disabled_builtins.contains(&BuiltinMib::MpdStats));
2863 }
2864
2865 #[test]
2866 fn test_builtin_mib_without_all() {
2867 let builder = AgentBuilder::new().without_builtin_handlers();
2868 assert!(builder.disabled_builtins.contains(&BuiltinMib::SnmpEngine));
2869 assert!(builder.disabled_builtins.contains(&BuiltinMib::UsmStats));
2870 assert!(builder.disabled_builtins.contains(&BuiltinMib::MpdStats));
2871 }
2872
2873 #[tokio::test]
2874 async fn test_uptime_hundredths() {
2875 let agent = Agent::builder()
2876 .bind("127.0.0.1:0")
2877 .community(b"public")
2878 .build()
2879 .await
2880 .unwrap();
2881
2882 let uptime = agent.uptime_hundredths();
2883 assert!(
2884 uptime < 100,
2885 "uptime should be less than 1 second, got {uptime}"
2886 );
2887
2888 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2889 let uptime2 = agent.uptime_hundredths();
2890 assert!(uptime2 > uptime, "uptime should increase after delay");
2891 }
2892
2893 #[tokio::test]
2894 async fn test_builtin_handlers_registered_by_default() {
2895 let agent = Agent::builder()
2896 .bind("127.0.0.1:0")
2897 .community(b"public")
2898 .build()
2899 .await
2900 .unwrap();
2901
2902 let ctx = test_ctx();
2903
2904 let handler = agent
2906 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 4, 0))
2907 .expect("snmpEngine handler should be registered");
2908 let get_result = handler
2909 .handler
2910 .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 4, 0))
2911 .await;
2912 assert!(matches!(get_result, GetResult::Value(Value::Integer(_))));
2913
2914 let handler = agent
2916 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0))
2917 .expect("USM stats handler should be registered");
2918 let get_result = handler
2919 .handler
2920 .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0))
2921 .await;
2922 assert!(matches!(get_result, GetResult::Value(Value::Counter32(0))));
2923
2924 let handler = agent
2926 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2927 .expect("MPD stats handler should be registered");
2928 let get_result = handler
2929 .handler
2930 .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2931 .await;
2932 assert!(matches!(get_result, GetResult::Value(Value::Counter32(0))));
2933 }
2934
2935 #[tokio::test]
2936 async fn test_builtin_handlers_disabled() {
2937 let agent = Agent::builder()
2938 .bind("127.0.0.1:0")
2939 .community(b"public")
2940 .without_builtin_handlers()
2941 .build()
2942 .await
2943 .unwrap();
2944
2945 assert!(
2946 agent
2947 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0))
2948 .is_none()
2949 );
2950 assert!(
2951 agent
2952 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0))
2953 .is_none()
2954 );
2955 assert!(
2956 agent
2957 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2958 .is_none()
2959 );
2960 }
2961
2962 #[tokio::test]
2963 async fn test_builtin_handler_selective_disable() {
2964 let agent = Agent::builder()
2965 .bind("127.0.0.1:0")
2966 .community(b"public")
2967 .without_builtin_handler(BuiltinMib::UsmStats)
2968 .build()
2969 .await
2970 .unwrap();
2971
2972 assert!(
2973 agent
2974 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0))
2975 .is_some()
2976 );
2977 assert!(
2978 agent
2979 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0))
2980 .is_none()
2981 );
2982 assert!(
2983 agent
2984 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2985 .is_some()
2986 );
2987 }
2988
2989 async fn small_limit_agent() -> Agent {
2992 Agent::builder()
2993 .bind("127.0.0.1:0")
2994 .community(b"public")
2995 .max_message_size(150)
2996 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
2997 .without_builtin_handlers()
2998 .build()
2999 .await
3000 .unwrap()
3001 }
3002
3003 fn five_varbinds() -> Vec<VarBind> {
3004 (1u32..=5)
3005 .map(|i| VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, i, 0), Value::Null))
3006 .collect()
3007 }
3008
3009 #[tokio::test]
3010 async fn test_get_too_big_returns_toobig_response() {
3011 let agent = small_limit_agent().await;
3012 let ctx = test_ctx();
3013
3014 let pdu = Pdu {
3017 pdu_type: PduType::GetRequest,
3018 request_id: 1,
3019 error_status: 0,
3020 error_index: 0,
3021 varbinds: five_varbinds(),
3022 };
3023
3024 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3025 assert_eq!(response.error_status, ErrorStatus::TooBig.as_i32());
3026 assert_eq!(response.error_index, 0);
3027 assert!(response.varbinds.is_empty());
3028 }
3029
3030 #[tokio::test]
3031 async fn test_get_within_limit_returns_response() {
3032 let agent = small_limit_agent().await;
3033 let ctx = test_ctx();
3034
3035 let pdu = Pdu {
3037 pdu_type: PduType::GetRequest,
3038 request_id: 1,
3039 error_status: 0,
3040 error_index: 0,
3041 varbinds: vec![VarBind::new(
3042 oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
3043 Value::Null,
3044 )],
3045 };
3046
3047 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3048 assert_eq!(response.error_status, 0);
3049 assert_eq!(response.varbinds.len(), 1);
3050 assert!(matches!(response.varbinds[0].value, Value::Integer(1)));
3051 }
3052
3053 #[tokio::test]
3054 async fn test_getnext_too_big_returns_toobig_response() {
3055 let agent = small_limit_agent().await;
3056 let mut ctx = test_ctx();
3057 ctx.pdu_type = PduType::GetNextRequest;
3058
3059 let pdu = Pdu {
3060 pdu_type: PduType::GetNextRequest,
3061 request_id: 1,
3062 error_status: 0,
3063 error_index: 0,
3064 varbinds: five_varbinds(),
3065 };
3066
3067 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3068 assert_eq!(response.error_status, ErrorStatus::TooBig.as_i32());
3069 assert_eq!(response.error_index, 0);
3070 assert!(response.varbinds.is_empty());
3071 }
3072
3073 #[tokio::test]
3074 async fn test_getnext_within_limit_returns_response() {
3075 let agent = small_limit_agent().await;
3076 let mut ctx = test_ctx();
3077 ctx.pdu_type = PduType::GetNextRequest;
3078
3079 let pdu = Pdu {
3080 pdu_type: PduType::GetNextRequest,
3081 request_id: 1,
3082 error_status: 0,
3083 error_index: 0,
3084 varbinds: vec![VarBind::new(
3085 oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
3086 Value::Null,
3087 )],
3088 };
3089
3090 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
3091 assert_eq!(response.error_status, 0);
3092 assert_eq!(response.varbinds.len(), 1);
3093 assert_eq!(
3094 response.varbinds[0].oid,
3095 oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)
3096 );
3097 }
3098}