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::ber::Decoder;
97use crate::error::internal::DecodeErrorKind;
98use crate::error::{Error, ErrorStatus, Result};
99use crate::handler::{GetNextResult, GetResult, MibHandler, RequestContext};
100use crate::notification::UsmConfig;
101use crate::oid;
102use crate::oid::Oid;
103use crate::pdu::{Pdu, PduType};
104use crate::util::bind_udp_socket;
105use crate::v3::{SaltCounter, compute_engine_boots_time};
106use crate::value::Value;
107use crate::varbind::VarBind;
108use crate::version::Version;
109
110const DEFAULT_MAX_MESSAGE_SIZE: usize = 1472;
112
113const RESPONSE_OVERHEAD: usize = 100;
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
124pub enum BuiltinMib {
125 SnmpEngine,
130 UsmStats,
136 MpdStats,
140}
141
142pub(crate) struct RegisteredHandler {
144 pub(crate) prefix: Oid,
145 pub(crate) handler: Arc<dyn MibHandler>,
146}
147
148pub struct AgentBuilder {
193 bind_addr: String,
194 communities: Vec<Vec<u8>>,
195 usm_users: HashMap<Bytes, UsmConfig>,
196 handlers: Vec<RegisteredHandler>,
197 engine_id: Option<Vec<u8>>,
198 engine_boots: u32,
199 max_message_size: usize,
200 max_concurrent_requests: Option<usize>,
201 recv_buffer_size: Option<usize>,
202 vacm: Option<VacmConfig>,
203 cancel: Option<CancellationToken>,
204 trap_sinks: Vec<(String, crate::client::Auth)>,
205 inform_timeout: Duration,
206 inform_retry: crate::client::Retry,
207 disabled_builtins: HashSet<BuiltinMib>,
208}
209
210impl AgentBuilder {
211 #[must_use]
221 pub fn new() -> Self {
222 Self {
223 bind_addr: "0.0.0.0:161".to_string(),
224 communities: Vec::new(),
225 usm_users: HashMap::new(),
226 handlers: Vec::new(),
227 engine_id: None,
228 engine_boots: 1,
229 max_message_size: DEFAULT_MAX_MESSAGE_SIZE,
230 max_concurrent_requests: Some(1000),
231 recv_buffer_size: Some(4 * 1024 * 1024), vacm: None,
233 cancel: None,
234 trap_sinks: Vec::new(),
235 inform_timeout: Duration::from_secs(5),
236 inform_retry: crate::client::Retry::default(),
237 disabled_builtins: HashSet::new(),
238 }
239 }
240
241 #[must_use]
279 pub fn bind(mut self, addr: impl Into<String>) -> Self {
280 self.bind_addr = addr.into();
281 self
282 }
283
284 #[must_use]
305 pub fn community(mut self, community: &[u8]) -> Self {
306 self.communities.push(community.to_vec());
307 self
308 }
309
310 #[must_use]
328 pub fn communities<I, C>(mut self, communities: I) -> Self
329 where
330 I: IntoIterator<Item = C>,
331 C: AsRef<[u8]>,
332 {
333 for c in communities {
334 self.communities.push(c.as_ref().to_vec());
335 }
336 self
337 }
338
339 #[must_use]
374 pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
375 where
376 F: FnOnce(UsmConfig) -> UsmConfig,
377 {
378 let username_bytes: Bytes = username.into();
379 let config = configure(UsmConfig::new(username_bytes.clone()));
380 self.usm_users.insert(username_bytes, config);
381 self
382 }
383
384 #[must_use]
405 pub fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
406 self.engine_id = Some(engine_id.into());
407 self
408 }
409
410 #[must_use]
435 pub fn engine_boots(mut self, boots: u32) -> Self {
436 self.engine_boots = boots;
437 self
438 }
439
440 #[must_use]
448 pub fn max_message_size(mut self, size: usize) -> Self {
449 self.max_message_size = size;
450 self
451 }
452
453 #[must_use]
461 pub fn max_concurrent_requests(mut self, limit: Option<usize>) -> Self {
462 self.max_concurrent_requests = limit;
463 self
464 }
465
466 #[must_use]
473 pub fn recv_buffer_size(mut self, size: Option<usize>) -> Self {
474 self.recv_buffer_size = size;
475 self
476 }
477
478 #[must_use]
519 pub fn handler(mut self, prefix: Oid, handler: Arc<dyn MibHandler>) -> Self {
520 self.handlers.push(RegisteredHandler { prefix, handler });
521 self
522 }
523
524 #[must_use]
563 pub fn vacm<F>(mut self, configure: F) -> Self
564 where
565 F: FnOnce(VacmBuilder) -> VacmBuilder,
566 {
567 let builder = VacmBuilder::new();
568 self.vacm = Some(configure(builder).build());
569 self
570 }
571
572 #[must_use]
576 pub fn cancel(mut self, token: CancellationToken) -> Self {
577 self.cancel = Some(token);
578 self
579 }
580
581 #[must_use]
606 pub fn trap_sink(
607 mut self,
608 dest: impl Into<String>,
609 auth: impl Into<crate::client::Auth>,
610 ) -> Self {
611 self.trap_sinks.push((dest.into(), auth.into()));
612 self
613 }
614
615 #[must_use]
619 pub fn inform_timeout(mut self, timeout: Duration) -> Self {
620 self.inform_timeout = timeout;
621 self
622 }
623
624 #[must_use]
629 pub fn inform_retry(mut self, retry: crate::client::Retry) -> Self {
630 self.inform_retry = retry;
631 self
632 }
633
634 #[must_use]
640 pub fn without_builtin_handler(mut self, mib: BuiltinMib) -> Self {
641 self.disabled_builtins.insert(mib);
642 self
643 }
644
645 #[must_use]
651 pub fn without_builtin_handlers(mut self) -> Self {
652 self.disabled_builtins.insert(BuiltinMib::SnmpEngine);
653 self.disabled_builtins.insert(BuiltinMib::UsmStats);
654 self.disabled_builtins.insert(BuiltinMib::MpdStats);
655 self
656 }
657
658 pub async fn build(mut self) -> Result<Agent> {
660 let bind_addr: std::net::SocketAddr = self.bind_addr.parse().map_err(|_| {
661 Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
662 })?;
663
664 let socket = bind_udp_socket(bind_addr, self.recv_buffer_size, None, false)
665 .await
666 .map_err(|e| Error::Network {
667 target: bind_addr,
668 source: e,
669 })?;
670
671 let local_addr = socket.local_addr().map_err(|e| Error::Network {
672 target: bind_addr,
673 source: e,
674 })?;
675
676 let socket_state =
677 UdpSocketState::new(UdpSockRef::from(&socket)).map_err(|e| Error::Network {
678 target: bind_addr,
679 source: e,
680 })?;
681
682 let engine_id: Bytes = self.engine_id.map_or_else(
684 || {
685 let mut id = vec![0x80, 0x00, 0x00, 0x00, 0x01]; let timestamp = std::time::SystemTime::now()
689 .duration_since(std::time::UNIX_EPOCH)
690 .unwrap_or_default()
691 .as_secs();
692 id.extend_from_slice(×tamp.to_be_bytes());
693 Bytes::from(id)
694 },
695 Bytes::from,
696 );
697
698 let cancel = self.cancel.unwrap_or_default();
699
700 let concurrency_limit = self
702 .max_concurrent_requests
703 .map(|n| Arc::new(Semaphore::new(n)));
704
705 let mut trap_sinks = Vec::with_capacity(self.trap_sinks.len());
707 for (dest_str, auth) in self.trap_sinks {
708 let dest: SocketAddr = dest_str.parse().map_err(|_| {
709 Error::Config(format!("invalid trap sink address: {dest_str}").into())
710 })?;
711 trap_sinks.push(notification::TrapSink::new(
712 dest,
713 auth,
714 self.inform_timeout,
715 self.inform_retry.clone(),
716 ));
717 }
718
719 let state = Arc::new(AgentState {
720 engine_id,
721 engine_boots: AtomicU32::new(self.engine_boots),
722 engine_time: AtomicU32::new(0),
723 engine_start: Instant::now(),
724 engine_boots_base: self.engine_boots,
725 max_message_size: self.max_message_size,
726 snmp_invalid_msgs: AtomicU32::new(0),
727 snmp_unknown_security_models: AtomicU32::new(0),
728 snmp_silent_drops: AtomicU32::new(0),
729 usm_unknown_engine_ids: AtomicU32::new(0),
730 usm_unknown_usernames: AtomicU32::new(0),
731 usm_wrong_digests: AtomicU32::new(0),
732 usm_not_in_time_windows: AtomicU32::new(0),
733 usm_unsupported_sec_levels: AtomicU32::new(0),
734 usm_decryption_errors: AtomicU32::new(0),
735 });
736
737 if !self.disabled_builtins.contains(&BuiltinMib::SnmpEngine) {
739 self.handlers.push(RegisteredHandler {
740 prefix: oid!(1, 3, 6, 1, 6, 3, 10, 2, 1),
741 handler: Arc::new(builtins::SnmpEngineHandler {
742 state: Arc::clone(&state),
743 }),
744 });
745 }
746 if !self.disabled_builtins.contains(&BuiltinMib::UsmStats) {
747 self.handlers.push(RegisteredHandler {
748 prefix: oid!(1, 3, 6, 1, 6, 3, 15, 1, 1),
749 handler: Arc::new(builtins::UsmStatsHandler {
750 state: Arc::clone(&state),
751 }),
752 });
753 }
754 if !self.disabled_builtins.contains(&BuiltinMib::MpdStats) {
755 self.handlers.push(RegisteredHandler {
756 prefix: oid!(1, 3, 6, 1, 6, 3, 11, 2, 1),
757 handler: Arc::new(builtins::MpdStatsHandler {
758 state: Arc::clone(&state),
759 }),
760 });
761 }
762
763 self.handlers
765 .sort_by_key(|h| std::cmp::Reverse(h.prefix.len()));
766
767 Ok(Agent {
768 inner: Arc::new(AgentInner {
769 socket: Arc::new(socket),
770 socket_state,
771 local_addr,
772 communities: self.communities,
773 usm_users: self.usm_users,
774 handlers: self.handlers,
775 state,
776 salt_counter: SaltCounter::new(),
777 concurrency_limit,
778 vacm: self.vacm,
779 cancel,
780 trap_sinks,
781 }),
782 })
783 }
784}
785
786impl Default for AgentBuilder {
787 fn default() -> Self {
788 Self::new()
789 }
790}
791
792pub(crate) struct AgentState {
794 pub(crate) engine_id: Bytes,
795 pub(crate) engine_boots: AtomicU32,
796 pub(crate) engine_time: AtomicU32,
797 pub(crate) engine_start: Instant,
798 pub(crate) engine_boots_base: u32,
800 pub(crate) max_message_size: usize,
801 pub(crate) snmp_invalid_msgs: AtomicU32,
805 pub(crate) snmp_unknown_security_models: AtomicU32,
808 pub(crate) snmp_silent_drops: AtomicU32,
811 pub(crate) usm_unknown_engine_ids: AtomicU32,
815 pub(crate) usm_unknown_usernames: AtomicU32,
818 pub(crate) usm_wrong_digests: AtomicU32,
821 pub(crate) usm_not_in_time_windows: AtomicU32,
824 pub(crate) usm_unsupported_sec_levels: AtomicU32,
827 pub(crate) usm_decryption_errors: AtomicU32,
830}
831
832pub(crate) struct AgentInner {
834 pub(crate) socket: Arc<UdpSocket>,
835 pub(crate) socket_state: UdpSocketState,
836 pub(crate) local_addr: SocketAddr,
837 pub(crate) communities: Vec<Vec<u8>>,
838 pub(crate) usm_users: HashMap<Bytes, UsmConfig>,
839 pub(crate) handlers: Vec<RegisteredHandler>,
840 pub(crate) state: Arc<AgentState>,
841 pub(crate) salt_counter: SaltCounter,
842 pub(crate) concurrency_limit: Option<Arc<Semaphore>>,
843 pub(crate) vacm: Option<VacmConfig>,
844 pub(crate) cancel: CancellationToken,
846 pub(crate) trap_sinks: Vec<notification::TrapSink>,
848}
849
850pub struct Agent {
871 pub(crate) inner: Arc<AgentInner>,
872}
873
874impl Agent {
875 #[must_use]
877 pub fn builder() -> AgentBuilder {
878 AgentBuilder::new()
879 }
880
881 #[must_use]
883 pub fn local_addr(&self) -> SocketAddr {
884 self.inner.local_addr
885 }
886
887 #[must_use]
889 pub fn engine_id(&self) -> &[u8] {
890 &self.inner.state.engine_id
891 }
892
893 #[must_use]
899 pub fn engine_boots(&self) -> u32 {
900 self.inner.state.engine_boots.load(Ordering::Relaxed)
901 }
902
903 #[must_use]
905 pub fn engine_time(&self) -> u32 {
906 self.inner.state.engine_time.load(Ordering::Relaxed)
907 }
908
909 #[must_use]
913 pub fn cancel(&self) -> CancellationToken {
914 self.inner.cancel.clone()
915 }
916
917 #[must_use]
924 pub fn snmp_invalid_msgs(&self) -> u32 {
925 self.inner.state.snmp_invalid_msgs.load(Ordering::Relaxed)
926 }
927
928 #[must_use]
935 pub fn snmp_unknown_security_models(&self) -> u32 {
936 self.inner
937 .state
938 .snmp_unknown_security_models
939 .load(Ordering::Relaxed)
940 }
941
942 #[must_use]
951 pub fn snmp_silent_drops(&self) -> u32 {
952 self.inner.state.snmp_silent_drops.load(Ordering::Relaxed)
953 }
954
955 #[must_use]
963 pub fn usm_unknown_engine_ids(&self) -> u32 {
964 self.inner
965 .state
966 .usm_unknown_engine_ids
967 .load(Ordering::Relaxed)
968 }
969
970 #[must_use]
978 pub fn usm_unknown_usernames(&self) -> u32 {
979 self.inner
980 .state
981 .usm_unknown_usernames
982 .load(Ordering::Relaxed)
983 }
984
985 #[must_use]
992 pub fn usm_wrong_digests(&self) -> u32 {
993 self.inner.state.usm_wrong_digests.load(Ordering::Relaxed)
994 }
995
996 #[must_use]
1006 pub fn usm_not_in_time_windows(&self) -> u32 {
1007 self.inner
1008 .state
1009 .usm_not_in_time_windows
1010 .load(Ordering::Relaxed)
1011 }
1012
1013 #[must_use]
1021 pub fn usm_unsupported_sec_levels(&self) -> u32 {
1022 self.inner
1023 .state
1024 .usm_unsupported_sec_levels
1025 .load(Ordering::Relaxed)
1026 }
1027
1028 #[must_use]
1036 pub fn usm_decryption_errors(&self) -> u32 {
1037 self.inner
1038 .state
1039 .usm_decryption_errors
1040 .load(Ordering::Relaxed)
1041 }
1042
1043 #[must_use]
1048 pub fn uptime_hundredths(&self) -> u32 {
1049 let elapsed = self.inner.state.engine_start.elapsed();
1050 let centisecs = elapsed.as_millis() / 10;
1051 centisecs.min(u128::from(u32::MAX)) as u32
1052 }
1053
1054 #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
1060 pub async fn run(&self) -> Result<()> {
1061 let mut buf = vec![0u8; 65535];
1062
1063 loop {
1064 let recv_meta = tokio::select! {
1065 result = self.recv_packet(&mut buf) => {
1066 result?
1067 }
1068 () = self.inner.cancel.cancelled() => {
1069 tracing::info!(target: "async_snmp::agent", "agent shutdown requested");
1070 return Ok(());
1071 }
1072 };
1073
1074 let data = Bytes::copy_from_slice(&buf[..recv_meta.len]);
1075 let agent = self.clone();
1076
1077 let permit = if let Some(ref sem) = self.inner.concurrency_limit {
1078 Some(sem.clone().acquire_owned().await.expect("semaphore closed"))
1079 } else {
1080 None
1081 };
1082
1083 tokio::spawn(async move {
1084 agent.update_engine_time();
1085
1086 match agent.handle_request(data, recv_meta.addr).await {
1087 Ok(Some(response_bytes)) => {
1088 if response_bytes.len() > agent.inner.state.max_message_size {
1091 agent
1092 .inner
1093 .state
1094 .snmp_silent_drops
1095 .fetch_add(1, Ordering::Relaxed);
1096 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");
1097 } else if let Err(e) =
1098 agent.send_response(&response_bytes, &recv_meta).await
1099 {
1100 tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "failed to send response");
1101 }
1102 }
1103 Ok(None) => {}
1104 Err(e) => {
1105 tracing::warn!(target: "async_snmp::agent", { snmp.source = %recv_meta.addr, error = %e }, "error handling request");
1106 }
1107 }
1108
1109 drop(permit);
1110 });
1111 }
1112 }
1113
1114 async fn recv_packet(&self, buf: &mut [u8]) -> Result<RecvMeta> {
1115 let mut iov = [IoSliceMut::new(buf)];
1116 let mut meta = [RecvMeta::default()];
1117
1118 loop {
1119 self.inner
1120 .socket
1121 .readable()
1122 .await
1123 .map_err(|e| Error::Network {
1124 target: self.inner.local_addr,
1125 source: e,
1126 })?;
1127
1128 let result = self.inner.socket.try_io(tokio::io::Interest::READABLE, || {
1129 let sref = UdpSockRef::from(&*self.inner.socket);
1130 self.inner.socket_state.recv(sref, &mut iov, &mut meta)
1131 });
1132
1133 match result {
1134 Ok(n) if n > 0 => return Ok(meta[0]),
1135 Ok(_) => { }
1136 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { }
1138 Err(e) => {
1139 return Err(Error::Network {
1140 target: self.inner.local_addr,
1141 source: e,
1142 }
1143 .boxed());
1144 }
1145 }
1146 }
1147 }
1148
1149 async fn send_response(&self, data: &[u8], recv_meta: &RecvMeta) -> std::io::Result<()> {
1150 let transmit = Transmit {
1151 destination: recv_meta.addr,
1152 ecn: None,
1153 contents: data,
1154 segment_size: None,
1155 src_ip: recv_meta.dst_ip,
1156 };
1157
1158 loop {
1159 self.inner.socket.writable().await?;
1160
1161 let result = self.inner.socket.try_io(tokio::io::Interest::WRITABLE, || {
1162 let sref = UdpSockRef::from(&*self.inner.socket);
1163 self.inner.socket_state.try_send(sref, &transmit)
1164 });
1165
1166 match result {
1167 Ok(()) => return Ok(()),
1168 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { }
1170 Err(e) => return Err(e),
1171 }
1172 }
1173 }
1174
1175 async fn handle_request(&self, data: Bytes, source: SocketAddr) -> Result<Option<Bytes>> {
1179 let mut decoder = Decoder::with_target(data.clone(), source);
1181 let mut seq = decoder.read_sequence()?;
1182 let version_num = seq.read_integer()?;
1183 let version = Version::from_i32(version_num).ok_or_else(|| {
1184 tracing::debug!(target: "async_snmp::agent", { source = %source, kind = %DecodeErrorKind::UnknownVersion(version_num) }, "unknown SNMP version");
1185 Error::MalformedResponse { target: source }.boxed()
1186 })?;
1187 drop(seq);
1188 drop(decoder);
1189
1190 match version {
1191 Version::V1 => self.handle_v1(data, source).await,
1192 Version::V2c => self.handle_v2c(data, source).await,
1193 Version::V3 => self.handle_v3(data, source).await,
1194 }
1195 }
1196
1197 fn update_engine_time(&self) {
1205 let total_secs = self.inner.state.engine_start.elapsed().as_secs();
1206 let (boots, time) =
1207 compute_engine_boots_time(self.inner.state.engine_boots_base, total_secs);
1208
1209 if boots != self.inner.state.engine_boots.load(Ordering::Relaxed)
1210 && boots > self.inner.state.engine_boots_base
1211 {
1212 tracing::warn!(
1213 target: "async_snmp::agent",
1214 engine_boots = boots,
1215 "engine time wrapped past MAX_ENGINE_TIME, incrementing engine boots"
1216 );
1217 }
1218
1219 self.inner
1220 .state
1221 .engine_boots
1222 .store(boots, Ordering::Relaxed);
1223 self.inner.state.engine_time.store(time, Ordering::Relaxed);
1224 }
1225
1226 pub(crate) fn validate_community(&self, community: &[u8]) -> bool {
1231 if self.inner.communities.is_empty() {
1232 return false;
1234 }
1235 let mut valid = false;
1239 for configured in &self.inner.communities {
1240 if configured.len() == community.len()
1242 && bool::from(configured.as_slice().ct_eq(community))
1243 {
1244 valid = true;
1245 }
1246 }
1247 valid
1248 }
1249
1250 async fn dispatch_request(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1252 match pdu.pdu_type {
1253 PduType::GetRequest => self.handle_get(ctx, pdu).await,
1254 PduType::GetNextRequest => self.handle_get_next(ctx, pdu).await,
1255 PduType::GetBulkRequest => {
1256 if ctx.version == Version::V1 {
1258 return Ok(pdu.to_error_response(ErrorStatus::GenErr, 0));
1259 }
1260 self.handle_get_bulk(ctx, pdu).await
1261 }
1262 PduType::SetRequest => self.handle_set(ctx, pdu).await,
1263 PduType::InformRequest => self.handle_inform(pdu),
1264 _ => {
1265 Ok(pdu.to_error_response(ErrorStatus::GenErr, 0))
1267 }
1268 }
1269 }
1270
1271 #[allow(
1277 clippy::unnecessary_wraps,
1278 reason = "TODO store received informs, which may be a fallible operation"
1279 )]
1280 #[allow(
1281 clippy::unused_self,
1282 reason = "TODO store received informs, which may require self"
1283 )]
1284 fn handle_inform(&self, pdu: &Pdu) -> Result<Pdu> {
1285 Ok(pdu.to_response())
1287 }
1288
1289 async fn handle_get(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1291 let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
1292
1293 for (index, vb) in pdu.varbinds.iter().enumerate() {
1294 if let Some(ref vacm) = self.inner.vacm
1296 && !vacm.check_access(ctx.read_view.as_ref(), &vb.oid)
1297 {
1298 if ctx.version == Version::V1 {
1300 return Ok(pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32));
1301 }
1302 response_varbinds.push(VarBind::new(vb.oid.clone(), Value::NoSuchObject));
1304 continue;
1305 }
1306
1307 let result = if let Some(handler) = self.find_handler(&vb.oid) {
1308 handler.handler.get(ctx, &vb.oid).await
1309 } else {
1310 GetResult::NoSuchObject
1311 };
1312
1313 let response_value = match result {
1314 GetResult::Value(v) => {
1315 if ctx.version == Version::V1 && matches!(v, Value::Counter64(_)) {
1317 return Ok(
1318 pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1319 );
1320 }
1321 v
1322 }
1323 GetResult::NoSuchObject => {
1324 if ctx.version == Version::V1 {
1326 return Ok(
1327 pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1328 );
1329 }
1330 Value::NoSuchObject
1331 }
1332 GetResult::NoSuchInstance => {
1333 if ctx.version == Version::V1 {
1335 return Ok(
1336 pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32)
1337 );
1338 }
1339 Value::NoSuchInstance
1340 }
1341 };
1342
1343 response_varbinds.push(VarBind::new(vb.oid.clone(), response_value));
1344 }
1345
1346 Ok(Pdu {
1347 pdu_type: PduType::Response,
1348 request_id: pdu.request_id,
1349 error_status: 0,
1350 error_index: 0,
1351 varbinds: response_varbinds,
1352 })
1353 }
1354
1355 async fn handle_get_next(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1357 let mut response_varbinds = Vec::with_capacity(pdu.varbinds.len());
1358
1359 for (index, vb) in pdu.varbinds.iter().enumerate() {
1360 let next = self.get_next_accessible_oid(ctx, &vb.oid).await;
1364
1365 if let Some(next_vb) = next {
1366 response_varbinds.push(next_vb);
1367 } else {
1368 if ctx.version == Version::V1 {
1370 return Ok(pdu.to_error_response(ErrorStatus::NoSuchName, (index + 1) as i32));
1371 }
1372 response_varbinds.push(VarBind::new(vb.oid.clone(), Value::EndOfMibView));
1373 }
1374 }
1375
1376 Ok(Pdu {
1377 pdu_type: PduType::Response,
1378 request_id: pdu.request_id,
1379 error_status: 0,
1380 error_index: 0,
1381 varbinds: response_varbinds,
1382 })
1383 }
1384
1385 async fn handle_get_bulk(&self, ctx: &RequestContext, pdu: &Pdu) -> Result<Pdu> {
1390 let non_repeaters = pdu.error_status.try_into().unwrap_or(0);
1392 let max_repetitions = pdu.error_index.max(0);
1393
1394 let mut response_varbinds = Vec::new();
1395 let mut current_size: usize = RESPONSE_OVERHEAD;
1396 let agent_max = self.inner.state.max_message_size;
1397 let max_size = match ctx.msg_max_size {
1398 Some(client_max) => agent_max.min(client_max as usize),
1399 None => agent_max,
1400 };
1401
1402 let can_add = |vb: &VarBind, current_size: usize| -> bool {
1404 current_size + vb.encoded_size() <= max_size
1405 };
1406
1407 for vb in pdu.varbinds.iter().take(non_repeaters) {
1409 let next = self.get_next_accessible_oid(ctx, &vb.oid).await;
1410
1411 let next_vb = match next {
1412 Some(next_vb) => next_vb,
1413 None => VarBind::new(vb.oid.clone(), Value::EndOfMibView),
1414 };
1415
1416 if !can_add(&next_vb, current_size) {
1417 if response_varbinds.is_empty() {
1419 return Ok(pdu.to_error_response(ErrorStatus::TooBig, 0));
1420 }
1421 break;
1423 }
1424
1425 current_size += next_vb.encoded_size();
1426 response_varbinds.push(next_vb);
1427 }
1428
1429 if non_repeaters < pdu.varbinds.len() {
1431 let repeaters = &pdu.varbinds[non_repeaters..];
1432 let mut current_oids: Vec<Oid> = repeaters.iter().map(|vb| vb.oid.clone()).collect();
1433 let mut all_done = vec![false; repeaters.len()];
1434
1435 'outer: for _ in 0..max_repetitions {
1436 let mut row_complete = true;
1437 for (i, oid) in current_oids.iter_mut().enumerate() {
1438 let next_vb = if all_done[i] {
1439 VarBind::new(oid.clone(), Value::EndOfMibView)
1440 } else {
1441 let next = self.get_next_accessible_oid(ctx, oid).await;
1442
1443 if let Some(next_vb) = next {
1444 *oid = next_vb.oid.clone();
1445 row_complete = false;
1446 next_vb
1447 } else {
1448 all_done[i] = true;
1449 VarBind::new(oid.clone(), Value::EndOfMibView)
1450 }
1451 };
1452
1453 if !can_add(&next_vb, current_size) {
1455 break 'outer;
1457 }
1458
1459 current_size += next_vb.encoded_size();
1460 response_varbinds.push(next_vb);
1461 }
1462
1463 if row_complete {
1464 break;
1465 }
1466 }
1467 }
1468
1469 Ok(Pdu {
1470 pdu_type: PduType::Response,
1471 request_id: pdu.request_id,
1472 error_status: 0,
1473 error_index: 0,
1474 varbinds: response_varbinds,
1475 })
1476 }
1477
1478 pub(crate) fn find_handler(&self, oid: &Oid) -> Option<&RegisteredHandler> {
1480 self.inner
1482 .handlers
1483 .iter()
1484 .find(|&handler| handler.handler.handles(&handler.prefix, oid))
1485 .map(|v| v as _)
1486 }
1487
1488 async fn get_next_accessible_oid(
1492 &self,
1493 ctx: &RequestContext,
1494 from_oid: &Oid,
1495 ) -> Option<VarBind> {
1496 let mut search_from = from_oid.clone();
1497 loop {
1498 let candidate = self.get_next_oid(ctx, &search_from).await;
1499 match candidate {
1500 None => return None,
1501 Some(ref next_vb) => {
1502 if next_vb.oid <= search_from {
1503 tracing::error!(
1504 target: "async_snmp::agent",
1505 from = %search_from,
1506 got = %next_vb.oid,
1507 "handler returned non-increasing OID in GETNEXT"
1508 );
1509 return None;
1510 }
1511 if ctx.version == Version::V1 && matches!(next_vb.value, Value::Counter64(_)) {
1513 search_from = next_vb.oid.clone();
1514 continue;
1515 }
1516 if let Some(ref vacm) = self.inner.vacm {
1517 if vacm.check_access(ctx.read_view.as_ref(), &next_vb.oid) {
1518 return candidate;
1519 }
1520 search_from = next_vb.oid.clone();
1521 } else {
1522 return candidate;
1523 }
1524 }
1525 }
1526 }
1527 }
1528
1529 async fn get_next_oid(&self, ctx: &RequestContext, oid: &Oid) -> Option<VarBind> {
1531 let mut best_result: Option<VarBind> = None;
1540
1541 for handler in &self.inner.handlers {
1542 let prefix = &handler.prefix;
1543 if prefix <= oid && !oid.starts_with(prefix) {
1544 continue;
1545 }
1546 if let GetNextResult::Value(next) = handler.handler.get_next(ctx, oid).await {
1547 if next.oid > *oid {
1549 match &best_result {
1550 None => best_result = Some(next),
1551 Some(current) if next.oid < current.oid => best_result = Some(next),
1552 _ => {}
1553 }
1554 }
1555 }
1556 }
1557
1558 best_result
1559 }
1560}
1561
1562impl Clone for Agent {
1563 fn clone(&self) -> Self {
1564 Self {
1565 inner: Arc::clone(&self.inner),
1566 }
1567 }
1568}
1569
1570#[cfg(test)]
1571mod tests {
1572 use super::*;
1573 use crate::handler::{
1574 BoxFuture, GetNextResult, GetResult, MibHandler, RequestContext, SecurityModel, SetResult,
1575 };
1576 use crate::message::SecurityLevel;
1577 use crate::oid;
1578
1579 struct TestHandler;
1580
1581 impl MibHandler for TestHandler {
1582 fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1583 Box::pin(async move {
1584 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
1585 return GetResult::Value(Value::Integer(42));
1586 }
1587 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
1588 return GetResult::Value(Value::OctetString(Bytes::from_static(b"test")));
1589 }
1590 GetResult::NoSuchObject
1591 })
1592 }
1593
1594 fn get_next<'a>(
1595 &'a self,
1596 _ctx: &'a RequestContext,
1597 oid: &'a Oid,
1598 ) -> BoxFuture<'a, GetNextResult> {
1599 Box::pin(async move {
1600 let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
1601 let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
1602
1603 if oid < &oid1 {
1604 return GetNextResult::Value(VarBind::new(oid1, Value::Integer(42)));
1605 }
1606 if oid < &oid2 {
1607 return GetNextResult::Value(VarBind::new(
1608 oid2,
1609 Value::OctetString(Bytes::from_static(b"test")),
1610 ));
1611 }
1612 GetNextResult::EndOfMibView
1613 })
1614 }
1615 }
1616
1617 fn test_ctx() -> RequestContext {
1618 RequestContext {
1619 source: "127.0.0.1:12345".parse().unwrap(),
1620 version: Version::V2c,
1621 security_model: SecurityModel::V2c,
1622 security_name: Bytes::from_static(b"public"),
1623 security_level: SecurityLevel::NoAuthNoPriv,
1624 context_name: Bytes::new(),
1625 request_id: 1,
1626 pdu_type: PduType::GetRequest,
1627 group_name: None,
1628 read_view: None,
1629 write_view: None,
1630 msg_max_size: None,
1631 }
1632 }
1633
1634 #[test]
1635 fn test_agent_builder_defaults() {
1636 let builder = AgentBuilder::new();
1637 assert_eq!(builder.bind_addr, "0.0.0.0:161");
1638 assert!(builder.communities.is_empty());
1639 assert!(builder.usm_users.is_empty());
1640 assert!(builder.handlers.is_empty());
1641 }
1642
1643 #[test]
1644 fn test_agent_builder_community() {
1645 let builder = AgentBuilder::new()
1646 .community(b"public")
1647 .community(b"private");
1648 assert_eq!(builder.communities.len(), 2);
1649 }
1650
1651 #[test]
1652 fn test_agent_builder_communities() {
1653 let builder = AgentBuilder::new().communities(["public", "private"]);
1654 assert_eq!(builder.communities.len(), 2);
1655 }
1656
1657 #[test]
1658 fn test_agent_builder_handler() {
1659 let builder =
1660 AgentBuilder::new().handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler));
1661 assert_eq!(builder.handlers.len(), 1);
1662 }
1663
1664 #[tokio::test]
1665 async fn test_mib_handler_default_set() {
1666 let handler = TestHandler;
1667 let mut ctx = test_ctx();
1668 ctx.pdu_type = PduType::SetRequest;
1669
1670 let result = handler
1671 .test_set(&ctx, &oid!(1, 3, 6, 1), &Value::Integer(1))
1672 .await;
1673 assert_eq!(result, SetResult::NotWritable);
1674 }
1675
1676 #[test]
1677 fn test_mib_handler_handles() {
1678 let handler = TestHandler;
1679 let prefix = oid!(1, 3, 6, 1, 4, 1, 99_999);
1680
1681 assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_999, 1, 0)));
1683
1684 assert!(handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_999)));
1686
1687 assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 99_998)));
1690
1691 assert!(!handler.handles(&prefix, &oid!(1, 3, 6, 1, 4, 1, 100_000)));
1693 }
1694
1695 #[tokio::test]
1696 async fn test_test_handler_get() {
1697 let handler = TestHandler;
1698 let ctx = test_ctx();
1699
1700 let result = handler
1702 .get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
1703 .await;
1704 assert!(matches!(result, GetResult::Value(Value::Integer(42))));
1705
1706 let result = handler
1708 .get(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 99, 0))
1709 .await;
1710 assert!(matches!(result, GetResult::NoSuchObject));
1711 }
1712
1713 #[tokio::test]
1714 async fn test_test_handler_get_next() {
1715 let handler = TestHandler;
1716 let mut ctx = test_ctx();
1717 ctx.pdu_type = PduType::GetNextRequest;
1718
1719 let next = handler.get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999)).await;
1721 assert!(next.is_value());
1722 if let GetNextResult::Value(vb) = next {
1723 assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0));
1724 }
1725
1726 let next = handler
1728 .get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
1729 .await;
1730 assert!(next.is_value());
1731 if let GetNextResult::Value(vb) = next {
1732 assert_eq!(vb.oid, oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0));
1733 }
1734
1735 let next = handler
1737 .get_next(&ctx, &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
1738 .await;
1739 assert!(next.is_end_of_mib_view());
1740 }
1741
1742 struct FiveOidHandler;
1744
1745 impl MibHandler for FiveOidHandler {
1746 fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1747 Box::pin(async move {
1748 for i in 1u16..=5 {
1749 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, i.into(), 0) {
1750 return GetResult::Value(Value::Integer(i.into()));
1751 }
1752 }
1753 GetResult::NoSuchObject
1754 })
1755 }
1756
1757 fn get_next<'a>(
1758 &'a self,
1759 _ctx: &'a RequestContext,
1760 oid: &'a Oid,
1761 ) -> BoxFuture<'a, GetNextResult> {
1762 Box::pin(async move {
1763 for i in 1u32..=5 {
1764 let candidate = oid!(1, 3, 6, 1, 4, 1, 99999, i, 0);
1765 if oid < &candidate {
1766 return GetNextResult::Value(VarBind::new(
1767 candidate,
1768 Value::Integer(i as i32),
1769 ));
1770 }
1771 }
1772 GetNextResult::EndOfMibView
1773 })
1774 }
1775 }
1776
1777 async fn test_agent_with_restricted_vacm() -> Agent {
1781 Agent::builder()
1782 .bind("127.0.0.1:0")
1783 .community(b"public")
1784 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
1785 .vacm(|v| {
1786 v.group("public", SecurityModel::V2c, "readers")
1787 .access("readers", |a| a.read_view("restricted"))
1788 .view("restricted", |v| {
1789 v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 2))
1790 .include(oid!(1, 3, 6, 1, 4, 1, 99999, 4))
1791 })
1792 })
1793 .build()
1794 .await
1795 .unwrap()
1796 }
1797
1798 #[tokio::test]
1799 async fn test_getbulk_vacm_filters_inaccessible_oids() {
1800 let agent = test_agent_with_restricted_vacm().await;
1801
1802 let mut ctx = test_ctx();
1803 ctx.pdu_type = PduType::GetBulkRequest;
1804 ctx.read_view = Some(Bytes::from_static(b"restricted"));
1805
1806 let pdu = Pdu {
1810 pdu_type: PduType::GetBulkRequest,
1811 request_id: 1,
1812 error_status: 0, error_index: 10, varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
1815 };
1816
1817 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1818
1819 let returned_oids: Vec<&Oid> = response
1821 .varbinds
1822 .iter()
1823 .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
1824 .map(|vb| &vb.oid)
1825 .collect();
1826
1827 assert!(
1829 returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)),
1830 "expected .99999.2.0 in response, got: {returned_oids:?}"
1831 );
1832 assert!(
1833 returned_oids.contains(&&oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0)),
1834 "expected .99999.4.0 in response (walk must continue past denied OIDs), got: {returned_oids:?}"
1835 );
1836
1837 for &oid in &[
1839 &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
1840 &oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
1841 &oid!(1, 3, 6, 1, 4, 1, 99999, 5, 0),
1842 ] {
1843 assert!(
1844 !returned_oids.contains(&oid),
1845 "GETBULK returned OID outside read view: {oid:?}"
1846 );
1847 }
1848 }
1849
1850 #[tokio::test]
1851 async fn test_getbulk_non_repeaters_vacm_filtered() {
1852 let agent = test_agent_with_restricted_vacm().await;
1853
1854 let mut ctx = test_ctx();
1855 ctx.pdu_type = PduType::GetBulkRequest;
1856 ctx.read_view = Some(Bytes::from_static(b"restricted"));
1857
1858 let pdu = Pdu {
1864 pdu_type: PduType::GetBulkRequest,
1865 request_id: 2,
1866 error_status: 2, error_index: 0, varbinds: vec![
1869 VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null),
1870 VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0), Value::Null),
1871 ],
1872 };
1873
1874 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1875
1876 assert_eq!(
1878 response.varbinds[0].oid,
1879 oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0)
1880 );
1881 assert!(matches!(response.varbinds[0].value, Value::Integer(2)));
1882
1883 assert_eq!(response.varbinds[1].value, Value::EndOfMibView);
1885 }
1886
1887 struct ThreeOidHandler;
1889
1890 impl MibHandler for ThreeOidHandler {
1891 fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
1892 Box::pin(async move {
1893 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
1894 return GetResult::Value(Value::Integer(1));
1895 }
1896 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
1897 return GetResult::Value(Value::Integer(2));
1898 }
1899 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0) {
1900 return GetResult::Value(Value::Integer(3));
1901 }
1902 GetResult::NoSuchObject
1903 })
1904 }
1905
1906 fn get_next<'a>(
1907 &'a self,
1908 _ctx: &'a RequestContext,
1909 oid: &'a Oid,
1910 ) -> BoxFuture<'a, GetNextResult> {
1911 Box::pin(async move {
1912 let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
1913 let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
1914 let oid3 = oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0);
1915
1916 if oid < &oid1 {
1917 return GetNextResult::Value(VarBind::new(oid1, Value::Integer(1)));
1918 }
1919 if oid < &oid2 {
1920 return GetNextResult::Value(VarBind::new(oid2, Value::Integer(2)));
1921 }
1922 if oid < &oid3 {
1923 return GetNextResult::Value(VarBind::new(oid3, Value::Integer(3)));
1924 }
1925 GetNextResult::EndOfMibView
1926 })
1927 }
1928 }
1929
1930 async fn test_agent_with_gap_vacm() -> Agent {
1933 Agent::builder()
1934 .bind("127.0.0.1:0")
1935 .community(b"public")
1936 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(ThreeOidHandler))
1937 .vacm(|v| {
1938 v.group("public", SecurityModel::V2c, "readers")
1939 .access("readers", |a| a.read_view("gap"))
1940 .view("gap", |v| {
1941 v.include(oid!(1, 3, 6, 1, 4, 1, 99999, 1))
1942 .include(oid!(1, 3, 6, 1, 4, 1, 99999, 3))
1943 })
1944 })
1945 .build()
1946 .await
1947 .unwrap()
1948 }
1949
1950 #[tokio::test]
1951 async fn test_getnext_vacm_skips_inaccessible_continues_walk() {
1952 let agent = test_agent_with_gap_vacm().await;
1956
1957 let mut ctx = test_ctx();
1958 ctx.pdu_type = PduType::GetNextRequest;
1959 ctx.read_view = Some(Bytes::from_static(b"gap"));
1960
1961 let pdu = Pdu {
1962 pdu_type: PduType::GetNextRequest,
1963 request_id: 1,
1964 error_status: 0,
1965 error_index: 0,
1966 varbinds: vec![VarBind::new(
1967 oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
1968 Value::Null,
1969 )],
1970 };
1971
1972 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
1973 assert_eq!(response.varbinds.len(), 1);
1974 assert_eq!(
1975 response.varbinds[0].oid,
1976 oid!(1, 3, 6, 1, 4, 1, 99999, 3, 0),
1977 "GETNEXT should skip denied .99999.2.0 and return accessible .99999.3.0"
1978 );
1979 assert!(matches!(response.varbinds[0].value, Value::Integer(3)));
1980 }
1981
1982 #[tokio::test]
1983 async fn test_getnext_vacm_all_remaining_denied_returns_end_of_mib() {
1984 let agent = test_agent_with_restricted_vacm().await;
1988
1989 let mut ctx = test_ctx();
1990 ctx.pdu_type = PduType::GetNextRequest;
1991 ctx.read_view = Some(Bytes::from_static(b"restricted"));
1992
1993 let pdu = Pdu {
1994 pdu_type: PduType::GetNextRequest,
1995 request_id: 1,
1996 error_status: 0,
1997 error_index: 0,
1998 varbinds: vec![VarBind::new(
1999 oid!(1, 3, 6, 1, 4, 1, 99999, 4, 0),
2000 Value::Null,
2001 )],
2002 };
2003
2004 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2005 assert_eq!(response.varbinds.len(), 1);
2006 assert_eq!(
2007 response.varbinds[0].value,
2008 Value::EndOfMibView,
2009 "GETNEXT should return EndOfMibView when all remaining OIDs are denied"
2010 );
2011 }
2012
2013 #[tokio::test]
2014 async fn test_getbulk_without_vacm_returns_all_oids() {
2015 let agent = Agent::builder()
2017 .bind("127.0.0.1:0")
2018 .community(b"public")
2019 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler))
2020 .build()
2021 .await
2022 .unwrap();
2023
2024 let mut ctx = test_ctx();
2025 ctx.pdu_type = PduType::GetBulkRequest;
2026
2027 let pdu = Pdu {
2028 pdu_type: PduType::GetBulkRequest,
2029 request_id: 1,
2030 error_status: 0,
2031 error_index: 10,
2032 varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2033 };
2034
2035 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2036
2037 assert!(
2039 response
2040 .varbinds
2041 .iter()
2042 .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0))
2043 );
2044 assert!(
2045 response
2046 .varbinds
2047 .iter()
2048 .any(|vb| vb.oid == oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0))
2049 );
2050 }
2051
2052 #[tokio::test]
2053 async fn test_v1_getbulk_rejected() {
2054 let agent = Agent::builder()
2056 .bind("127.0.0.1:0")
2057 .community(b"public")
2058 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(TestHandler))
2059 .build()
2060 .await
2061 .unwrap();
2062
2063 let mut ctx = test_ctx();
2064 ctx.version = Version::V1;
2065 ctx.security_model = SecurityModel::V1;
2066 ctx.pdu_type = PduType::GetBulkRequest;
2067
2068 let pdu = Pdu {
2069 pdu_type: PduType::GetBulkRequest,
2070 request_id: 1,
2071 error_status: 0,
2072 error_index: 10,
2073 varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2074 };
2075
2076 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2077 assert_eq!(
2078 ErrorStatus::from_i32(response.error_status),
2079 ErrorStatus::GenErr,
2080 "v1 GETBULK should be rejected"
2081 );
2082 }
2083
2084 struct Counter64Handler;
2086
2087 impl MibHandler for Counter64Handler {
2088 fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
2089 Box::pin(async move {
2090 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
2091 return GetResult::Value(Value::Counter64(1_000_000_000_000));
2092 }
2093 if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0) {
2094 return GetResult::Value(Value::Integer(42));
2095 }
2096 GetResult::NoSuchObject
2097 })
2098 }
2099
2100 fn get_next<'a>(
2101 &'a self,
2102 _ctx: &'a RequestContext,
2103 oid: &'a Oid,
2104 ) -> BoxFuture<'a, GetNextResult> {
2105 Box::pin(async move {
2106 let oid1 = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
2107 let oid2 = oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0);
2108
2109 if oid < &oid1 {
2110 return GetNextResult::Value(VarBind::new(
2111 oid1,
2112 Value::Counter64(1_000_000_000_000),
2113 ));
2114 }
2115 if oid < &oid2 {
2116 return GetNextResult::Value(VarBind::new(oid2, Value::Integer(42)));
2117 }
2118 GetNextResult::EndOfMibView
2119 })
2120 }
2121 }
2122
2123 async fn test_agent_with_counter64() -> Agent {
2124 Agent::builder()
2125 .bind("127.0.0.1:0")
2126 .community(b"public")
2127 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(Counter64Handler))
2128 .build()
2129 .await
2130 .unwrap()
2131 }
2132
2133 #[tokio::test]
2134 async fn test_v1_get_filters_counter64() {
2135 let agent = test_agent_with_counter64().await;
2138
2139 let mut ctx = test_ctx();
2140 ctx.version = Version::V1;
2141 ctx.security_model = SecurityModel::V1;
2142 ctx.pdu_type = PduType::GetRequest;
2143
2144 let pdu = Pdu {
2145 pdu_type: PduType::GetRequest,
2146 request_id: 1,
2147 error_status: 0,
2148 error_index: 0,
2149 varbinds: vec![VarBind::new(
2150 oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2151 Value::Null,
2152 )],
2153 };
2154
2155 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2156 assert_eq!(
2157 ErrorStatus::from_i32(response.error_status),
2158 ErrorStatus::NoSuchName,
2159 "v1 GET of Counter64 should return noSuchName"
2160 );
2161 }
2162
2163 #[tokio::test]
2164 async fn test_v2c_get_allows_counter64() {
2165 let agent = test_agent_with_counter64().await;
2167
2168 let ctx = test_ctx(); let pdu = Pdu {
2171 pdu_type: PduType::GetRequest,
2172 request_id: 1,
2173 error_status: 0,
2174 error_index: 0,
2175 varbinds: vec![VarBind::new(
2176 oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0),
2177 Value::Null,
2178 )],
2179 };
2180
2181 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2182 assert_eq!(response.error_status, 0);
2183 assert!(matches!(response.varbinds[0].value, Value::Counter64(_)));
2184 }
2185
2186 #[tokio::test]
2187 async fn test_getbulk_respects_v3_msg_max_size() {
2188 let agent = Agent::builder()
2193 .bind("127.0.0.1:0")
2194 .community(b"public")
2195 .max_message_size(65507) .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
2197 .build()
2198 .await
2199 .unwrap();
2200
2201 let mut ctx_unlimited = test_ctx();
2203 ctx_unlimited.pdu_type = PduType::GetBulkRequest;
2204 ctx_unlimited.msg_max_size = None;
2205
2206 let pdu = Pdu {
2207 pdu_type: PduType::GetBulkRequest,
2208 request_id: 1,
2209 error_status: 0, error_index: 10, varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2212 };
2213
2214 let full_response = agent.dispatch_request(&ctx_unlimited, &pdu).await.unwrap();
2215 let full_count = full_response
2216 .varbinds
2217 .iter()
2218 .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2219 .count();
2220 assert!(
2221 full_count >= 3,
2222 "expected at least 3 data varbinds without limit, got {full_count}"
2223 );
2224
2225 let mut ctx_limited = test_ctx();
2230 ctx_limited.pdu_type = PduType::GetBulkRequest;
2231 ctx_limited.msg_max_size = Some(150); let limited_response = agent.dispatch_request(&ctx_limited, &pdu).await.unwrap();
2234 let limited_count = limited_response
2235 .varbinds
2236 .iter()
2237 .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2238 .count();
2239
2240 assert!(
2241 limited_count < full_count,
2242 "V3 msg_max_size should limit response: got {limited_count} varbinds (unlimited: {full_count})"
2243 );
2244 assert!(
2245 limited_count > 0,
2246 "should still return at least one varbind"
2247 );
2248 }
2249
2250 #[tokio::test]
2251 async fn test_getbulk_msg_max_size_none_uses_agent_max() {
2252 let agent = Agent::builder()
2255 .bind("127.0.0.1:0")
2256 .community(b"public")
2257 .max_message_size(65507)
2258 .handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(FiveOidHandler))
2259 .without_builtin_handlers()
2260 .build()
2261 .await
2262 .unwrap();
2263
2264 let mut ctx = test_ctx();
2265 ctx.pdu_type = PduType::GetBulkRequest;
2266 ctx.msg_max_size = None; let pdu = Pdu {
2269 pdu_type: PduType::GetBulkRequest,
2270 request_id: 1,
2271 error_status: 0,
2272 error_index: 10,
2273 varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2274 };
2275
2276 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2277 let data_count = response
2278 .varbinds
2279 .iter()
2280 .filter(|vb| !matches!(vb.value, Value::EndOfMibView))
2281 .count();
2282 assert_eq!(
2283 data_count, 5,
2284 "all 5 OIDs should be returned without msg_max_size limit"
2285 );
2286 }
2287
2288 #[tokio::test]
2289 async fn test_v1_getnext_skips_counter64() {
2290 let agent = test_agent_with_counter64().await;
2294
2295 let mut ctx = test_ctx();
2296 ctx.version = Version::V1;
2297 ctx.security_model = SecurityModel::V1;
2298 ctx.pdu_type = PduType::GetNextRequest;
2299
2300 let pdu = Pdu {
2301 pdu_type: PduType::GetNextRequest,
2302 request_id: 1,
2303 error_status: 0,
2304 error_index: 0,
2305 varbinds: vec![VarBind::new(oid!(1, 3, 6, 1, 4, 1, 99999), Value::Null)],
2306 };
2307
2308 let response = agent.dispatch_request(&ctx, &pdu).await.unwrap();
2309 assert_eq!(response.error_status, 0, "should succeed");
2310 assert_eq!(
2311 response.varbinds[0].oid,
2312 oid!(1, 3, 6, 1, 4, 1, 99999, 2, 0),
2313 "should skip Counter64 and return next non-Counter64 OID"
2314 );
2315 assert!(matches!(response.varbinds[0].value, Value::Integer(42)));
2316 }
2317
2318 #[test]
2319 fn test_engine_time_no_overflow() {
2320 let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
2322 assert_eq!(boots, 1);
2323 assert_eq!(time, 1000);
2324 }
2325
2326 #[test]
2327 fn test_engine_time_zero_elapsed() {
2328 let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
2329 assert_eq!(boots, 1);
2330 assert_eq!(time, 0);
2331 }
2332
2333 #[test]
2334 fn test_engine_time_just_below_max() {
2335 let max = crate::v3::MAX_ENGINE_TIME;
2336 let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max) - 1);
2337 assert_eq!(boots, 1);
2338 assert_eq!(time, max - 1);
2339 }
2340
2341 #[test]
2342 fn test_engine_time_at_max_wraps() {
2343 let max = crate::v3::MAX_ENGINE_TIME;
2345 let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max));
2346 assert_eq!(
2347 boots, 2,
2348 "boots should increment when elapsed reaches MAX_ENGINE_TIME"
2349 );
2350 assert_eq!(time, 0, "time should wrap to 0");
2351 }
2352
2353 #[test]
2354 fn test_engine_time_past_max() {
2355 let max = crate::v3::MAX_ENGINE_TIME;
2357 let (boots, time) = crate::v3::compute_engine_boots_time(1, u64::from(max) + 500);
2358 assert_eq!(boots, 2);
2359 assert_eq!(time, 500);
2360 }
2361
2362 #[test]
2363 fn test_engine_time_multiple_wraps() {
2364 let max = crate::v3::MAX_ENGINE_TIME;
2366 let elapsed = u64::from(max) * 3 + 42;
2367 let (boots, time) = crate::v3::compute_engine_boots_time(1, elapsed);
2368 assert_eq!(boots, 4, "base 1 + 3 wraps = 4");
2369 assert_eq!(time, 42);
2370 }
2371
2372 #[test]
2373 fn test_engine_time_boots_capped_at_max() {
2374 let max = crate::v3::MAX_ENGINE_TIME;
2376 let elapsed = u64::from(max) * u64::from(max); let (boots, _time) = crate::v3::compute_engine_boots_time(1, elapsed);
2378 assert_eq!(boots, max, "boots should be capped at MAX_ENGINE_TIME");
2379 }
2380
2381 #[test]
2382 fn test_engine_time_base_boots_preserved() {
2383 let max = crate::v3::MAX_ENGINE_TIME;
2385 let (boots, time) = crate::v3::compute_engine_boots_time(5, u64::from(max) + 100);
2386 assert_eq!(boots, 6, "base 5 + 1 wrap = 6");
2387 assert_eq!(time, 100);
2388 }
2389
2390 #[test]
2391 fn test_engine_time_high_base_boots_capped() {
2392 let max = crate::v3::MAX_ENGINE_TIME;
2394 let (boots, _time) = crate::v3::compute_engine_boots_time(max - 1, u64::from(max) * 2);
2395 assert_eq!(boots, max, "should cap at MAX_ENGINE_TIME, not overflow");
2396 }
2397
2398 #[tokio::test]
2399 async fn test_engine_boots_builder() {
2400 let agent = Agent::builder()
2402 .bind("127.0.0.1:0")
2403 .community(b"public")
2404 .engine_boots(42)
2405 .build()
2406 .await
2407 .unwrap();
2408
2409 assert_eq!(agent.engine_boots(), 42);
2410 }
2411
2412 #[tokio::test]
2413 async fn test_engine_boots_default() {
2414 let agent = Agent::builder()
2416 .bind("127.0.0.1:0")
2417 .community(b"public")
2418 .build()
2419 .await
2420 .unwrap();
2421
2422 assert_eq!(agent.engine_boots(), 1);
2423 }
2424
2425 #[tokio::test]
2426 async fn test_usm_counter_accessors_default_zero() {
2427 let agent = Agent::builder()
2428 .bind("127.0.0.1:0")
2429 .community(b"public")
2430 .build()
2431 .await
2432 .unwrap();
2433
2434 assert_eq!(agent.usm_unsupported_sec_levels(), 0);
2435 assert_eq!(agent.usm_decryption_errors(), 0);
2436 }
2437
2438 #[test]
2439 fn test_builtin_mib_without_single() {
2440 let builder = AgentBuilder::new().without_builtin_handler(BuiltinMib::UsmStats);
2441 assert!(builder.disabled_builtins.contains(&BuiltinMib::UsmStats));
2442 assert!(!builder.disabled_builtins.contains(&BuiltinMib::SnmpEngine));
2443 assert!(!builder.disabled_builtins.contains(&BuiltinMib::MpdStats));
2444 }
2445
2446 #[test]
2447 fn test_builtin_mib_without_all() {
2448 let builder = AgentBuilder::new().without_builtin_handlers();
2449 assert!(builder.disabled_builtins.contains(&BuiltinMib::SnmpEngine));
2450 assert!(builder.disabled_builtins.contains(&BuiltinMib::UsmStats));
2451 assert!(builder.disabled_builtins.contains(&BuiltinMib::MpdStats));
2452 }
2453
2454 #[tokio::test]
2455 async fn test_uptime_hundredths() {
2456 let agent = Agent::builder()
2457 .bind("127.0.0.1:0")
2458 .community(b"public")
2459 .build()
2460 .await
2461 .unwrap();
2462
2463 let uptime = agent.uptime_hundredths();
2464 assert!(
2465 uptime < 100,
2466 "uptime should be less than 1 second, got {uptime}"
2467 );
2468
2469 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2470 let uptime2 = agent.uptime_hundredths();
2471 assert!(uptime2 > uptime, "uptime should increase after delay");
2472 }
2473
2474 #[tokio::test]
2475 async fn test_builtin_handlers_registered_by_default() {
2476 let agent = Agent::builder()
2477 .bind("127.0.0.1:0")
2478 .community(b"public")
2479 .build()
2480 .await
2481 .unwrap();
2482
2483 let ctx = test_ctx();
2484
2485 let handler = agent
2487 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 4, 0))
2488 .expect("snmpEngine handler should be registered");
2489 let get_result = handler
2490 .handler
2491 .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 4, 0))
2492 .await;
2493 assert!(matches!(get_result, GetResult::Value(Value::Integer(_))));
2494
2495 let handler = agent
2497 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0))
2498 .expect("USM stats handler should be registered");
2499 let get_result = handler
2500 .handler
2501 .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0))
2502 .await;
2503 assert!(matches!(get_result, GetResult::Value(Value::Counter32(0))));
2504
2505 let handler = agent
2507 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2508 .expect("MPD stats handler should be registered");
2509 let get_result = handler
2510 .handler
2511 .get(&ctx, &oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2512 .await;
2513 assert!(matches!(get_result, GetResult::Value(Value::Counter32(0))));
2514 }
2515
2516 #[tokio::test]
2517 async fn test_builtin_handlers_disabled() {
2518 let agent = Agent::builder()
2519 .bind("127.0.0.1:0")
2520 .community(b"public")
2521 .without_builtin_handlers()
2522 .build()
2523 .await
2524 .unwrap();
2525
2526 assert!(
2527 agent
2528 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0))
2529 .is_none()
2530 );
2531 assert!(
2532 agent
2533 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0))
2534 .is_none()
2535 );
2536 assert!(
2537 agent
2538 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2539 .is_none()
2540 );
2541 }
2542
2543 #[tokio::test]
2544 async fn test_builtin_handler_selective_disable() {
2545 let agent = Agent::builder()
2546 .bind("127.0.0.1:0")
2547 .community(b"public")
2548 .without_builtin_handler(BuiltinMib::UsmStats)
2549 .build()
2550 .await
2551 .unwrap();
2552
2553 assert!(
2554 agent
2555 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0))
2556 .is_some()
2557 );
2558 assert!(
2559 agent
2560 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0))
2561 .is_none()
2562 );
2563 assert!(
2564 agent
2565 .find_handler(&oid!(1, 3, 6, 1, 6, 3, 11, 2, 1, 1, 0))
2566 .is_some()
2567 );
2568 }
2569}