Skip to main content

async_snmp/message/
community.rs

1//! Community-based SNMP message format (v1/v2c).
2//!
3//! V1 and V2c messages share the same structure:
4//! `SEQUENCE { version INTEGER, community OCTET STRING, pdu PDU }`
5//!
6//! The only difference is the version number (0 for v1, 1 for v2c).
7//! `SNMPv1` Trap PDUs (tag 0xA4) have a distinct wire format from standard PDUs
8//! and are represented by the `CommunityPdu::TrapV1` variant.
9
10use 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/// PDU carried inside a community (v1/v2c) message.
18///
19/// `SNMPv1` Trap PDUs have a different wire layout from all other PDU types,
20/// so they are decoded into a distinct variant.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum CommunityPdu {
23    /// Standard PDU (Get, `GetNext`, Response, Set, `GetBulk`, Inform, `TrapV2`, Report).
24    Standard(Pdu),
25    /// `SNMPv1` Trap PDU (distinct wire format, only valid in V1 messages).
26    TrapV1(TrapV1Pdu),
27}
28
29impl CommunityPdu {
30    /// Return a reference to the standard PDU, or `None` if this is a `TrapV1`.
31    #[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    /// Return a reference to the `TrapV1` PDU, or `None` if this is a standard PDU.
40    #[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    /// Return the PDU type.
49    #[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    /// Encode to BER.
58    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
78/// Community-based SNMP message (v1/v2c).
79///
80/// This unified type handles both `SNMPv1` and `SNMPv2c` messages,
81/// which share identical structure but differ in version number.
82/// The `pdu` field is a `CommunityPdu` that can hold either a standard
83/// PDU or a `TrapV1` PDU.
84#[derive(Debug, Clone)]
85pub struct CommunityMessage {
86    /// SNMP version (V1 or V2c)
87    pub version: Version,
88    /// Community string for authentication
89    pub community: Bytes,
90    /// Protocol data unit
91    pub pdu: CommunityPdu,
92}
93
94impl CommunityMessage {
95    /// Create a new community message with a standard PDU.
96    ///
97    /// # Panics
98    /// Panics if version is V3 (use `V3Message` instead).
99    pub fn new(version: Version, community: impl Into<Bytes>, pdu: Pdu) -> Self {
100        assert!(
101            matches!(version, Version::V1 | Version::V2c),
102            "CommunityMessage only supports V1/V2c, not {version:?}"
103        );
104        Self {
105            version,
106            community: community.into(),
107            pdu: CommunityPdu::Standard(pdu),
108        }
109    }
110
111    /// Create a V2c message (convenience constructor).
112    pub fn v2c(community: impl Into<Bytes>, pdu: Pdu) -> Self {
113        Self::new(Version::V2c, community, pdu)
114    }
115
116    /// Create a V1 message with a standard PDU (convenience constructor).
117    pub fn v1(community: impl Into<Bytes>, pdu: Pdu) -> Self {
118        Self::new(Version::V1, community, pdu)
119    }
120
121    /// Create a V1 message carrying a `TrapV1` PDU.
122    pub fn v1_trap(community: impl Into<Bytes>, trap: TrapV1Pdu) -> Self {
123        Self {
124            version: Version::V1,
125            community: community.into(),
126            pdu: CommunityPdu::TrapV1(trap),
127        }
128    }
129
130    /// Encode to BER.
131    pub fn encode(&self) -> Bytes {
132        let mut buf = EncodeBuf::new();
133
134        buf.push_sequence(|buf| {
135            self.pdu.encode(buf);
136            buf.push_octet_string(&self.community);
137            buf.push_integer(self.version.as_i32());
138        });
139
140        buf.finish()
141    }
142
143    /// Decode from BER.
144    ///
145    /// Returns the message with the version parsed from the data.
146    pub fn decode(data: Bytes) -> Result<Self> {
147        let mut decoder = Decoder::new(data);
148        Self::decode_from(&mut decoder)
149    }
150
151    /// Decode from an existing decoder (used by Message dispatcher).
152    pub(crate) fn decode_from(decoder: &mut Decoder) -> Result<Self> {
153        let mut seq = decoder.read_sequence()?;
154
155        let version_num = seq.read_integer()?;
156        let version = Version::from_i32(version_num).ok_or_else(|| {
157            tracing::debug!(target: "async_snmp::ber", { offset = seq.offset(), kind = %DecodeErrorKind::UnknownVersion(version_num) }, "decode error");
158            Error::MalformedResponse {
159                target: UNKNOWN_TARGET,
160            }
161            .boxed()
162        })?;
163
164        Self::decode_from_sequence(&mut seq, version)
165    }
166
167    /// Decode from a sequence decoder where version has already been read.
168    pub(crate) fn decode_from_sequence(seq: &mut Decoder, version: Version) -> Result<Self> {
169        if version == Version::V3 {
170            tracing::debug!(target: "async_snmp::ber", { offset = seq.offset(), kind = %DecodeErrorKind::UnknownVersion(3) }, "decode error");
171            return Err(Error::MalformedResponse {
172                target: UNKNOWN_TARGET,
173            }
174            .boxed());
175        }
176
177        let community = seq.read_octet_string()?;
178
179        // Peek at the PDU tag to dispatch between standard and TrapV1 layouts.
180        let pdu_tag = seq.peek_tag().ok_or_else(|| {
181            tracing::debug!(target: "async_snmp::ber", { offset = seq.offset(), kind = %DecodeErrorKind::TruncatedData }, "truncated community message");
182            Error::MalformedResponse {
183                target: UNKNOWN_TARGET,
184            }
185            .boxed()
186        })?;
187
188        let pdu = if pdu_tag == tag::pdu::TRAP_V1 {
189            CommunityPdu::TrapV1(TrapV1Pdu::decode(seq)?)
190        } else {
191            CommunityPdu::Standard(Pdu::decode(seq)?)
192        };
193
194        Ok(CommunityMessage {
195            version,
196            community,
197            pdu,
198        })
199    }
200
201    /// Consume and return the standard PDU.
202    ///
203    /// Returns `None` if the PDU is a `TrapV1`.
204    pub fn into_pdu(self) -> Option<Pdu> {
205        match self.pdu {
206            CommunityPdu::Standard(p) => Some(p),
207            CommunityPdu::TrapV1(_) => None,
208        }
209    }
210
211    /// Consume and return the `CommunityPdu`.
212    pub fn into_community_pdu(self) -> CommunityPdu {
213        self.pdu
214    }
215
216    /// Encode a GETBULK request message (v2c/v3 only).
217    ///
218    /// GETBULK is not supported in `SNMPv1`.
219    pub fn encode_bulk(version: Version, community: impl Into<Bytes>, pdu: &GetBulkPdu) -> Bytes {
220        debug_assert!(version != Version::V1, "GETBULK not supported in SNMPv1");
221
222        let community = community.into();
223        let mut buf = EncodeBuf::new();
224
225        buf.push_sequence(|buf| {
226            pdu.encode(buf);
227            buf.push_octet_string(&community);
228            buf.push_integer(version.as_i32());
229        });
230
231        buf.finish()
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use crate::oid;
239    use crate::pdu::{GenericTrap, TrapV1Pdu};
240
241    #[test]
242    fn test_v1_trap_roundtrip() {
243        let trap = TrapV1Pdu::new(
244            oid!(1, 3, 6, 1, 4, 1, 9999),
245            [192, 168, 1, 1],
246            GenericTrap::LinkDown,
247            0,
248            12345,
249            vec![],
250        );
251        let msg = CommunityMessage::v1_trap(b"public".as_slice(), trap);
252
253        let encoded = msg.encode();
254        let decoded = CommunityMessage::decode(encoded).unwrap();
255
256        assert_eq!(decoded.version, Version::V1);
257        assert_eq!(decoded.community.as_ref(), b"public");
258        match decoded.pdu {
259            CommunityPdu::TrapV1(ref t) => {
260                assert_eq!(t.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999));
261                assert_eq!(t.agent_addr, [192, 168, 1, 1]);
262                assert_eq!(t.generic_trap, GenericTrap::LinkDown);
263                assert_eq!(t.time_stamp, 12345);
264            }
265            CommunityPdu::Standard(_) => panic!("expected TrapV1 pdu"),
266        }
267    }
268
269    #[test]
270    fn test_v1_roundtrip() {
271        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
272        let msg = CommunityMessage::v1(b"public".as_slice(), pdu);
273
274        let encoded = msg.encode();
275        let decoded = CommunityMessage::decode(encoded).unwrap();
276
277        assert_eq!(decoded.version, Version::V1);
278        assert_eq!(decoded.community.as_ref(), b"public");
279        assert_eq!(decoded.pdu.standard().unwrap().request_id, 42);
280    }
281
282    #[test]
283    fn test_v2c_roundtrip() {
284        let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
285        let msg = CommunityMessage::v2c(b"private".as_slice(), pdu);
286
287        let encoded = msg.encode();
288        let decoded = CommunityMessage::decode(encoded).unwrap();
289
290        assert_eq!(decoded.version, Version::V2c);
291        assert_eq!(decoded.community.as_ref(), b"private");
292        assert_eq!(decoded.pdu.standard().unwrap().request_id, 123);
293    }
294
295    #[test]
296    fn test_version_preserved() {
297        for version in [Version::V1, Version::V2c] {
298            let pdu = Pdu::get_request(1, &[oid!(1, 3, 6, 1)]);
299            let msg = CommunityMessage::new(version, b"test".as_slice(), pdu);
300
301            let encoded = msg.encode();
302            let decoded = CommunityMessage::decode(encoded).unwrap();
303
304            assert_eq!(decoded.version, version);
305        }
306    }
307}