1use crate::ber::{Decoder, EncodeBuf, tag};
11use crate::error::internal::DecodeErrorKind;
12use crate::error::{Error, Result, UNKNOWN_TARGET};
13use crate::pdu::{GetBulkPdu, Pdu, PduType, TrapV1Pdu};
14use crate::version::Version;
15use bytes::Bytes;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum CommunityPdu {
23 Standard(Pdu),
25 TrapV1(TrapV1Pdu),
27}
28
29impl CommunityPdu {
30 #[must_use]
32 pub fn standard(&self) -> Option<&Pdu> {
33 match self {
34 Self::Standard(p) => Some(p),
35 Self::TrapV1(_) => None,
36 }
37 }
38
39 #[must_use]
41 pub fn trap_v1(&self) -> Option<&TrapV1Pdu> {
42 match self {
43 Self::TrapV1(t) => Some(t),
44 Self::Standard(_) => None,
45 }
46 }
47
48 #[must_use]
50 pub fn pdu_type(&self) -> PduType {
51 match self {
52 Self::Standard(p) => p.pdu_type,
53 Self::TrapV1(_) => PduType::TrapV1,
54 }
55 }
56
57 pub(crate) fn encode(&self, buf: &mut EncodeBuf) {
59 match self {
60 Self::Standard(p) => p.encode(buf),
61 Self::TrapV1(t) => t.encode(buf),
62 }
63 }
64}
65
66impl From<Pdu> for CommunityPdu {
67 fn from(p: Pdu) -> Self {
68 Self::Standard(p)
69 }
70}
71
72impl From<TrapV1Pdu> for CommunityPdu {
73 fn from(t: TrapV1Pdu) -> Self {
74 Self::TrapV1(t)
75 }
76}
77
78fn pdu_type_valid_for_version(pdu_type: PduType, version: Version) -> bool {
87 match version {
88 Version::V1 => matches!(
89 pdu_type,
90 PduType::GetRequest | PduType::GetNextRequest | PduType::Response | PduType::SetRequest
91 ),
92 Version::V2c => pdu_type != PduType::TrapV1,
95 Version::V3 => false,
97 }
98}
99
100#[derive(Debug, Clone)]
107pub struct CommunityMessage {
108 pub version: Version,
110 pub community: Bytes,
112 pub pdu: CommunityPdu,
114}
115
116impl CommunityMessage {
117 pub fn new(version: Version, community: impl Into<Bytes>, pdu: Pdu) -> Self {
122 assert!(
123 matches!(version, Version::V1 | Version::V2c),
124 "CommunityMessage only supports V1/V2c, not {version:?}"
125 );
126 Self {
127 version,
128 community: community.into(),
129 pdu: CommunityPdu::Standard(pdu),
130 }
131 }
132
133 pub fn v2c(community: impl Into<Bytes>, pdu: Pdu) -> Self {
135 Self::new(Version::V2c, community, pdu)
136 }
137
138 pub fn v1(community: impl Into<Bytes>, pdu: Pdu) -> Self {
140 Self::new(Version::V1, community, pdu)
141 }
142
143 pub fn v1_trap(community: impl Into<Bytes>, trap: TrapV1Pdu) -> Self {
145 Self {
146 version: Version::V1,
147 community: community.into(),
148 pdu: CommunityPdu::TrapV1(trap),
149 }
150 }
151
152 pub fn encode(&self) -> Bytes {
154 let mut buf = EncodeBuf::new();
155
156 buf.push_sequence(|buf| {
157 self.pdu.encode(buf);
158 buf.push_octet_string(&self.community);
159 buf.push_integer(self.version.as_i32());
160 });
161
162 buf.finish()
163 }
164
165 pub fn decode(data: Bytes) -> Result<Self> {
169 let mut decoder = Decoder::new(data);
170 Self::decode_from(&mut decoder)
171 }
172
173 pub(crate) fn decode_from(decoder: &mut Decoder) -> Result<Self> {
175 let mut seq = decoder.read_sequence()?;
176
177 let version_num = seq.read_integer()?;
178 let version = Version::from_i32(version_num).ok_or_else(|| {
179 tracing::debug!(target: "async_snmp::ber", { offset = seq.offset(), kind = %DecodeErrorKind::UnknownVersion(version_num) }, "decode error");
180 Error::MalformedResponse {
181 target: UNKNOWN_TARGET,
182 }
183 .boxed()
184 })?;
185
186 Self::decode_from_sequence(&mut seq, version)
187 }
188
189 pub(crate) fn decode_from_sequence(seq: &mut Decoder, version: Version) -> Result<Self> {
191 if version == Version::V3 {
192 tracing::debug!(target: "async_snmp::ber", { offset = seq.offset(), kind = %DecodeErrorKind::UnknownVersion(3) }, "decode error");
193 return Err(Error::MalformedResponse {
194 target: UNKNOWN_TARGET,
195 }
196 .boxed());
197 }
198
199 let community = seq.read_octet_string()?;
200
201 let pdu_tag = seq.peek_tag().ok_or_else(|| {
203 tracing::debug!(target: "async_snmp::ber", { offset = seq.offset(), kind = %DecodeErrorKind::TruncatedData }, "truncated community message");
204 Error::MalformedResponse {
205 target: UNKNOWN_TARGET,
206 }
207 .boxed()
208 })?;
209
210 let pdu = if pdu_tag == tag::pdu::TRAP_V1 {
211 if version != Version::V1 {
215 tracing::debug!(target: "async_snmp::ber", { offset = seq.offset(), version = ?version }, "v1 Trap PDU (0xA4) not valid in this version");
216 return Err(Error::MalformedResponse {
217 target: UNKNOWN_TARGET,
218 }
219 .boxed());
220 }
221 CommunityPdu::TrapV1(TrapV1Pdu::decode(seq)?)
222 } else {
223 let pdu = Pdu::decode(seq)?;
224 if !pdu_type_valid_for_version(pdu.pdu_type, version) {
228 tracing::debug!(target: "async_snmp::ber", { offset = seq.offset(), version = ?version, pdu_type = %pdu.pdu_type }, "PDU type not valid for SNMP version");
229 return Err(Error::MalformedResponse {
230 target: UNKNOWN_TARGET,
231 }
232 .boxed());
233 }
234 CommunityPdu::Standard(pdu)
235 };
236
237 Ok(CommunityMessage {
238 version,
239 community,
240 pdu,
241 })
242 }
243
244 pub fn into_pdu(self) -> Option<Pdu> {
248 match self.pdu {
249 CommunityPdu::Standard(p) => Some(p),
250 CommunityPdu::TrapV1(_) => None,
251 }
252 }
253
254 pub fn into_community_pdu(self) -> CommunityPdu {
256 self.pdu
257 }
258
259 pub fn encode_bulk(version: Version, community: impl Into<Bytes>, pdu: &GetBulkPdu) -> Bytes {
263 debug_assert!(version != Version::V1, "GETBULK not supported in SNMPv1");
264
265 let community = community.into();
266 let mut buf = EncodeBuf::new();
267
268 buf.push_sequence(|buf| {
269 pdu.encode(buf);
270 buf.push_octet_string(&community);
271 buf.push_integer(version.as_i32());
272 });
273
274 buf.finish()
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::oid;
282 use crate::pdu::{GenericTrap, TrapV1Pdu};
283
284 #[test]
285 fn test_v1_trap_roundtrip() {
286 let trap = TrapV1Pdu::new(
287 oid!(1, 3, 6, 1, 4, 1, 9999),
288 [192, 168, 1, 1],
289 GenericTrap::LinkDown,
290 0,
291 12345,
292 vec![],
293 );
294 let msg = CommunityMessage::v1_trap(b"public".as_slice(), trap);
295
296 let encoded = msg.encode();
297 let decoded = CommunityMessage::decode(encoded).unwrap();
298
299 assert_eq!(decoded.version, Version::V1);
300 assert_eq!(decoded.community.as_ref(), b"public");
301 match decoded.pdu {
302 CommunityPdu::TrapV1(ref t) => {
303 assert_eq!(t.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999));
304 assert_eq!(t.agent_addr, [192, 168, 1, 1]);
305 assert_eq!(t.generic_trap, GenericTrap::LinkDown);
306 assert_eq!(t.time_stamp, 12345);
307 }
308 CommunityPdu::Standard(_) => panic!("expected TrapV1 pdu"),
309 }
310 }
311
312 #[test]
313 fn test_v1_roundtrip() {
314 let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
315 let msg = CommunityMessage::v1(b"public".as_slice(), pdu);
316
317 let encoded = msg.encode();
318 let decoded = CommunityMessage::decode(encoded).unwrap();
319
320 assert_eq!(decoded.version, Version::V1);
321 assert_eq!(decoded.community.as_ref(), b"public");
322 assert_eq!(decoded.pdu.standard().unwrap().request_id, 42);
323 }
324
325 #[test]
326 fn test_v2c_roundtrip() {
327 let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
328 let msg = CommunityMessage::v2c(b"private".as_slice(), pdu);
329
330 let encoded = msg.encode();
331 let decoded = CommunityMessage::decode(encoded).unwrap();
332
333 assert_eq!(decoded.version, Version::V2c);
334 assert_eq!(decoded.community.as_ref(), b"private");
335 assert_eq!(decoded.pdu.standard().unwrap().request_id, 123);
336 }
337
338 #[test]
339 fn test_v1_getbulk_rejected() {
340 let pdu = Pdu::get_bulk(1, 0, 10, vec![]);
343 let msg = CommunityMessage::new(Version::V1, b"public".as_slice(), pdu);
344 let encoded = msg.encode();
345
346 let result = CommunityMessage::decode(encoded);
347 assert!(
348 result.is_err(),
349 "v1 GETBULK must be rejected, got {result:?}"
350 );
351 }
352
353 #[test]
354 fn test_v1_inform_rejected() {
355 let pdu = Pdu::inform_request(1, 0, &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1), vec![]);
357 let msg = CommunityMessage::new(Version::V1, b"public".as_slice(), pdu);
358 let encoded = msg.encode();
359
360 let result = CommunityMessage::decode(encoded);
361 assert!(
362 result.is_err(),
363 "v1 Inform must be rejected, got {result:?}"
364 );
365 }
366
367 #[test]
368 fn test_v1_trapv2_rejected() {
369 let pdu = Pdu::trap_v2(1, 0, &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1), vec![]);
371 let msg = CommunityMessage::new(Version::V1, b"public".as_slice(), pdu);
372 let encoded = msg.encode();
373
374 let result = CommunityMessage::decode(encoded);
375 assert!(
376 result.is_err(),
377 "v1 SNMPv2-Trap must be rejected, got {result:?}"
378 );
379 }
380
381 #[test]
382 fn test_v2c_trapv1_rejected() {
383 let trap = TrapV1Pdu::new(
386 oid!(1, 3, 6, 1, 4, 1, 9999),
387 [192, 168, 1, 1],
388 GenericTrap::LinkDown,
389 0,
390 12345,
391 vec![],
392 );
393 let mut buf = EncodeBuf::new();
394 buf.push_sequence(|buf| {
395 trap.encode(buf);
396 buf.push_octet_string(b"public");
397 buf.push_integer(Version::V2c.as_i32());
398 });
399 let encoded = buf.finish();
400
401 let result = CommunityMessage::decode(encoded);
402 assert!(
403 result.is_err(),
404 "v2c v1-Trap (0xA4) must be rejected, got {result:?}"
405 );
406 }
407
408 #[test]
409 fn test_version_preserved() {
410 for version in [Version::V1, Version::V2c] {
411 let pdu = Pdu::get_request(1, &[oid!(1, 3, 6, 1)]);
412 let msg = CommunityMessage::new(version, b"test".as_slice(), pdu);
413
414 let encoded = msg.encode();
415 let decoded = CommunityMessage::decode(encoded).unwrap();
416
417 assert_eq!(decoded.version, version);
418 }
419 }
420}