1use anyhow::{bail, ensure, Context, Result};
13use rasn::ber;
14use rasn::types::{FixedOctetString, Integer, ObjectIdentifier, OctetString};
15use rasn_smi::{v1 as smi1, v2 as smi2};
16use rasn_snmp::{v1, v2, v2c};
17use serde_json::{json, Value};
18use std::net::{IpAddr, Ipv4Addr, SocketAddr};
19use std::time::Duration;
20
21pub const SYS_UPTIME_OID: &str = "1.3.6.1.2.1.1.3.0";
22pub const SNMP_TRAP_OID: &str = "1.3.6.1.6.3.1.1.4.1.0";
23pub const TRAP_PORT: u16 = 162;
25pub const MAX_VARIABLES: usize = 64;
27pub const INFORM_TIMEOUT: Duration = Duration::from_secs(2);
29pub const INFORM_ATTEMPTS: usize = 3;
30
31pub struct Received {
33 pub kind: &'static str,
34 pub data: Value,
36 pub acknowledgement: Option<Vec<u8>>,
38}
39
40fn integer(i: &Integer) -> i64 {
41 match i {
42 Integer::Primitive(v) => *v as i64,
43 Integer::Variable(big) => big.to_string().parse().unwrap_or(0),
44 }
45}
46
47fn octets(bytes: &[u8]) -> Value {
48 match std::str::from_utf8(bytes) {
49 Ok(s) if !crate::utils::sanitize::has_controls(s) => {
50 json!({"type": "string", "value": s})
51 }
52 _ => json!({"type": "string", "encoding": "hex", "value": hex::encode(bytes)}),
53 }
54}
55
56fn ipv4(a: &smi1::IpAddress) -> String {
57 let b: &[u8] = a.0.as_ref();
58 Ipv4Addr::new(b[0], b[1], b[2], b[3]).to_string()
59}
60
61fn v2_value(v: &v2::VarBindValue) -> Value {
62 use smi2::{ApplicationSyntax as A, ObjectSyntax as O, SimpleSyntax as S};
63 match v {
64 v2::VarBindValue::Value(O::Simple(S::Integer(i))) => {
65 json!({"type": "integer", "value": integer(i)})
66 }
67 v2::VarBindValue::Value(O::Simple(S::String(s))) => octets(s),
68 v2::VarBindValue::Value(O::Simple(S::ObjectId(o))) => {
69 json!({"type": "oid", "value": o.to_string()})
70 }
71 v2::VarBindValue::Value(O::ApplicationWide(A::Address(a))) => {
72 json!({"type": "ipaddress", "value": ipv4(a)})
73 }
74 v2::VarBindValue::Value(O::ApplicationWide(A::Counter(c))) => {
75 json!({"type": "counter", "value": c.0})
76 }
77 v2::VarBindValue::Value(O::ApplicationWide(A::Ticks(t))) => {
78 json!({"type": "timeticks", "value": t.0})
79 }
80 v2::VarBindValue::Value(O::ApplicationWide(A::Unsigned(g))) => {
81 json!({"type": "gauge", "value": g.0})
82 }
83 v2::VarBindValue::Value(O::ApplicationWide(A::BigCounter(c))) => {
84 json!({"type": "counter64", "value": c.0})
85 }
86 v2::VarBindValue::Value(O::ApplicationWide(A::Arbitrary(o))) => {
87 json!({"type": "opaque", "encoding": "hex", "value": hex::encode(o.as_ref())})
88 }
89 v2::VarBindValue::Unspecified => json!({"type": "null", "value": null}),
90 v2::VarBindValue::NoSuchObject => json!({"type": "noSuchObject", "value": null}),
91 v2::VarBindValue::NoSuchInstance => json!({"type": "noSuchInstance", "value": null}),
92 v2::VarBindValue::EndOfMibView => json!({"type": "endOfMibView", "value": null}),
93 }
94}
95
96fn v1_value(v: &smi1::ObjectSyntax) -> Value {
97 use smi1::{ApplicationSyntax as A, NetworkAddress as N, ObjectSyntax as O, SimpleSyntax as S};
98 match v {
99 O::Simple(S::Number(i)) => json!({"type": "integer", "value": integer(i)}),
100 O::Simple(S::String(s)) => octets(s),
101 O::Simple(S::Object(o)) => json!({"type": "oid", "value": o.to_string()}),
102 O::Simple(S::Empty) => json!({"type": "null", "value": null}),
103 O::ApplicationWide(A::Address(N::Internet(a))) => {
104 json!({"type": "ipaddress", "value": ipv4(a)})
105 }
106 O::ApplicationWide(A::Counter(c)) => json!({"type": "counter", "value": c.0}),
107 O::ApplicationWide(A::Gauge(g)) => json!({"type": "gauge", "value": g.0}),
108 O::ApplicationWide(A::Ticks(t)) => json!({"type": "timeticks", "value": t.0}),
109 O::ApplicationWide(A::Arbitrary(o)) => {
110 json!({"type": "opaque", "encoding": "hex", "value": hex::encode(o.as_ref())})
111 }
112 }
113}
114
115fn binding(oid: String, mut typed: Value) -> Value {
116 typed["oid"] = json!(oid);
117 typed
118}
119
120pub fn parse(data: &[u8]) -> Option<Received> {
122 if super::check_ber_structure(data, super::MAX_BER_DEPTH).is_err() {
124 return None;
125 }
126 if let Ok(msg) = ber::decode::<v2c::Message<v2::Pdus>>(data) {
127 let (kind, pdu) = match &msg.data {
128 v2::Pdus::Trap(t) => ("trap", &t.0),
129 v2::Pdus::InformRequest(i) => ("inform", &i.0),
130 _ => return None,
131 };
132 let mut variables = Vec::new();
133 let (mut uptime, mut trap_oid) = (Value::Null, Value::Null);
134 for vb in &pdu.variable_bindings {
135 let oid = vb.name.to_string();
136 let typed = v2_value(&vb.value);
137 if oid == SYS_UPTIME_OID {
138 uptime = typed["value"].clone();
139 } else if oid == SNMP_TRAP_OID {
140 trap_oid = typed["value"].clone();
141 } else {
142 variables.push(binding(oid, typed));
143 }
144 }
145 let acknowledgement = (kind == "inform")
146 .then(|| {
147 ber::encode(&v2c::Message {
148 version: msg.version.clone(),
149 community: msg.community.clone(),
150 data: v2::Pdus::Response(v2::Response(v2::Pdu {
151 request_id: pdu.request_id,
152 error_status: v2::Pdu::ERROR_STATUS_NO_ERROR,
153 error_index: 0,
154 variable_bindings: pdu.variable_bindings.clone(),
155 })),
156 })
157 .ok()
158 })
159 .flatten();
160 return Some(Received {
161 kind,
162 data: json!({
163 "kind": kind, "version": "v2c",
164 "community": String::from_utf8_lossy(&msg.community),
165 "request_id": pdu.request_id, "uptime": uptime, "trap_oid": trap_oid,
166 "variables": variables,
167 }),
168 acknowledgement,
169 });
170 }
171 let msg = ber::decode::<v1::Message<v1::Pdus>>(data).ok()?;
172 let v1::Pdus::Trap(trap) = &msg.data else {
173 return None;
174 };
175 let smi1::NetworkAddress::Internet(agent) = &trap.agent_addr;
176 Some(Received {
177 kind: "trap",
178 data: json!({
179 "kind": "trap", "version": "v1",
180 "community": String::from_utf8_lossy(&msg.community),
181 "enterprise": trap.enterprise.to_string(),
182 "agent_addr": ipv4(agent),
183 "generic_trap": integer(&trap.generic_trap),
184 "specific_trap": integer(&trap.specific_trap),
185 "uptime": trap.time_stamp.0,
186 "variables": trap.variable_bindings.iter()
187 .map(|vb| binding(vb.name.to_string(), v1_value(&vb.value)))
188 .collect::<Vec<_>>(),
189 }),
190 acknowledgement: None,
191 })
192}
193
194pub fn oid(text: &str) -> Result<ObjectIdentifier> {
195 let arcs: Vec<u32> = text
196 .trim_start_matches('.')
197 .split('.')
198 .map(|a| a.parse::<u32>())
199 .collect::<std::result::Result<_, _>>()
200 .with_context(|| format!("{text:?} is not a dotted-decimal OID"))?;
201 ObjectIdentifier::new(arcs).with_context(|| format!("{text:?} is not a valid OID"))
202}
203
204fn u32_of(v: &Value, what: &str) -> Result<u32> {
205 v.as_u64()
206 .and_then(|n| u32::try_from(n).ok())
207 .with_context(|| format!("{what} is an unsigned 32-bit number"))
208}
209
210fn ip_of(v: &Value) -> Result<smi1::IpAddress> {
211 let ip: Ipv4Addr = v
212 .as_str()
213 .context("an ipaddress value is a dotted IPv4 address")?
214 .parse()
215 .context("an ipaddress value is a dotted IPv4 address")?;
216 Ok(smi1::IpAddress(FixedOctetString::from(ip.octets())))
217}
218
219fn string_of(v: &Value, encoding: Option<&str>) -> Result<OctetString> {
220 let s = v.as_str().context("a string value is text")?;
221 Ok(match encoding {
222 Some("hex") => OctetString::from(hex::decode(s).context("value is not hex")?),
223 None | Some("utf8") => OctetString::from(s.as_bytes().to_vec()),
224 Some(other) => bail!("encoding is utf8 or hex, not {other:?}"),
225 })
226}
227
228fn v2_binding(var: &Value) -> Result<v2::VarBind> {
230 use smi2::{ApplicationSyntax as A, ObjectSyntax as O, SimpleSyntax as S};
231 let name = oid(var["oid"].as_str().context("each variable has an oid")?)?;
232 let value = &var["value"];
233 let syntax = match var["type"].as_str().unwrap_or("string") {
234 "integer" => O::Simple(S::Integer(Integer::from(
235 value.as_i64().context("an integer value is a number")?,
236 ))),
237 "string" => O::Simple(S::String(string_of(value, var["encoding"].as_str())?)),
238 "oid" => O::Simple(S::ObjectId(oid(value.as_str().context("an oid value is text")?)?)),
239 "ipaddress" => O::ApplicationWide(A::Address(ip_of(value)?)),
240 "counter" => O::ApplicationWide(A::Counter(smi1::Counter(u32_of(value, "a counter")?))),
241 "gauge" => O::ApplicationWide(A::Unsigned(smi1::Gauge(u32_of(value, "a gauge")?))),
242 "timeticks" => O::ApplicationWide(A::Ticks(smi1::TimeTicks(u32_of(value, "timeticks")?))),
243 "counter64" => O::ApplicationWide(A::BigCounter(smi2::Counter64(
244 value.as_u64().context("a counter64 is an unsigned number")?,
245 ))),
246 "null" => {
247 return Ok(v2::VarBind {
248 name,
249 value: v2::VarBindValue::Unspecified,
250 })
251 }
252 other => bail!("variable type {other:?} is not one of integer, string, oid, ipaddress, counter, gauge, timeticks, counter64, null"),
253 };
254 Ok(v2::VarBind {
255 name,
256 value: v2::VarBindValue::Value(syntax),
257 })
258}
259
260fn v1_binding(var: &Value) -> Result<v1::VarBind> {
261 use smi1::{ApplicationSyntax as A, NetworkAddress as N, ObjectSyntax as O, SimpleSyntax as S};
262 let name = oid(var["oid"].as_str().context("each variable has an oid")?)?;
263 let value = &var["value"];
264 let syntax = match var["type"].as_str().unwrap_or("string") {
265 "integer" => O::Simple(S::Number(Integer::from(
266 value.as_i64().context("an integer value is a number")?,
267 ))),
268 "string" => O::Simple(S::String(string_of(value, var["encoding"].as_str())?)),
269 "oid" => O::Simple(S::Object(oid(value.as_str().context("an oid value is text")?)?)),
270 "null" => O::Simple(S::Empty),
271 "ipaddress" => O::ApplicationWide(A::Address(N::Internet(ip_of(value)?))),
272 "counter" => O::ApplicationWide(A::Counter(smi1::Counter(u32_of(value, "a counter")?))),
273 "gauge" => O::ApplicationWide(A::Gauge(smi1::Gauge(u32_of(value, "a gauge")?))),
274 "timeticks" => O::ApplicationWide(A::Ticks(smi1::TimeTicks(u32_of(value, "timeticks")?))),
275 other => bail!("variable type {other:?} is not one SNMPv1 carries (integer, string, oid, null, ipaddress, counter, gauge, timeticks)"),
276 };
277 Ok(v1::VarBind {
278 name,
279 value: syntax,
280 })
281}
282
283pub struct Outgoing {
285 pub target: String,
286 pub inform: bool,
287 pub request_id: i32,
288 pub packet: Vec<u8>,
289}
290
291fn uptime_ticks() -> u32 {
293 static START: std::sync::OnceLock<crate::utils::clock::Instant> = std::sync::OnceLock::new();
294 let start = *START.get_or_init(crate::utils::clock::Instant::now);
295 (start.elapsed().as_millis() / 10) as u32
296}
297
298pub fn build(action: &Value) -> Result<Outgoing> {
300 let target = action["target"]
301 .as_str()
302 .context("Missing 'target' (host or host:port; port 162 when omitted)")?;
303 ensure!(
304 !target.is_empty() && target.len() <= 255 && !target.contains(char::is_whitespace),
305 "target is host or host:port"
306 );
307 let target = if target.parse::<SocketAddr>().is_ok()
308 || target
309 .rsplit_once(':')
310 .is_some_and(|(h, p)| !h.contains(':') && p.parse::<u16>().is_ok())
311 {
312 target.to_string()
313 } else if let Ok(ip) = target.trim_matches(['[', ']']).parse::<IpAddr>() {
314 SocketAddr::new(ip, TRAP_PORT).to_string()
315 } else {
316 format!("{target}:{TRAP_PORT}")
317 };
318 let version = action["version"].as_str().unwrap_or("v2c");
319 let inform = action["inform"].as_bool().unwrap_or(false);
320 let community = action["community"].as_str().unwrap_or("public");
321 ensure!(community.len() <= 255, "community is at most 255 bytes");
322 let variables = match action.get("variables") {
323 None | Some(Value::Null) => Vec::new(),
324 Some(v) => v
325 .as_array()
326 .context("variables is an array of {oid, type, value}")?
327 .clone(),
328 };
329 ensure!(
330 variables.len() <= MAX_VARIABLES,
331 "at most {MAX_VARIABLES} variables"
332 );
333 let uptime = match action.get("uptime") {
334 None | Some(Value::Null) => uptime_ticks(),
335 Some(v) => u32_of(v, "uptime (timeticks)")?,
336 };
337 let request_id = (rand::random::<u32>() & 0x7fff_ffff) as i32;
338 let packet = match version {
339 "v2c" => {
340 let trap_oid = oid(action["trap_oid"]
341 .as_str()
342 .context("a v2c notification names its trap_oid (snmpTrapOID.0), e.g. 1.3.6.1.6.3.1.1.5.3 (linkDown)")?)?;
343 let mut bindings = vec![
344 v2::VarBind {
345 name: oid(SYS_UPTIME_OID)?,
346 value: v2::VarBindValue::Value(smi2::ObjectSyntax::ApplicationWide(
347 smi2::ApplicationSyntax::Ticks(smi1::TimeTicks(uptime)),
348 )),
349 },
350 v2::VarBind {
351 name: oid(SNMP_TRAP_OID)?,
352 value: v2::VarBindValue::Value(smi2::ObjectSyntax::Simple(
353 smi2::SimpleSyntax::ObjectId(trap_oid),
354 )),
355 },
356 ];
357 for var in &variables {
358 bindings.push(v2_binding(var)?);
359 }
360 let pdu = v2::Pdu {
361 request_id,
362 error_status: v2::Pdu::ERROR_STATUS_NO_ERROR,
363 error_index: 0,
364 variable_bindings: bindings,
365 };
366 ber::encode(&v2c::Message {
367 version: Integer::from(1),
368 community: OctetString::from(community.as_bytes().to_vec()),
369 data: if inform {
370 v2::Pdus::InformRequest(v2::InformRequest(pdu))
371 } else {
372 v2::Pdus::Trap(v2::Trap(pdu))
373 },
374 })
375 .map_err(|e| anyhow::anyhow!("encoding the notification: {e}"))?
376 }
377 "v1" => {
378 ensure!(!inform, "an inform is SNMPv2c; SNMPv1 has only traps");
379 let generic = action["generic_trap"].as_i64().unwrap_or(6);
380 ensure!(
381 (0..=6).contains(&generic),
382 "generic_trap is 0..=6 (6 = enterpriseSpecific)"
383 );
384 let bindings = variables
385 .iter()
386 .map(v1_binding)
387 .collect::<Result<Vec<_>>>()?;
388 let agent = match action.get("agent_addr") {
389 None | Some(Value::Null) => smi1::IpAddress(FixedOctetString::from([0, 0, 0, 0])),
390 Some(v) => ip_of(v)?,
391 };
392 ber::encode(&v1::Message {
393 version: Integer::from(0),
394 community: OctetString::from(community.as_bytes().to_vec()),
395 data: v1::Pdus::Trap(v1::Trap {
396 enterprise: oid(action["enterprise"]
397 .as_str()
398 .unwrap_or("1.3.6.1.4.1.8072.9999"))?,
399 agent_addr: smi1::NetworkAddress::Internet(agent),
400 generic_trap: Integer::from(generic),
401 specific_trap: Integer::from(action["specific_trap"].as_i64().unwrap_or(0)),
402 time_stamp: smi1::TimeTicks(uptime),
403 variable_bindings: bindings,
404 }),
405 })
406 .map_err(|e| anyhow::anyhow!("encoding the trap: {e}"))?
407 }
408 other => bail!("version is v1 or v2c (SNMPv3 is not implemented), not {other:?}"),
409 };
410 Ok(Outgoing {
411 target,
412 inform,
413 request_id,
414 packet,
415 })
416}
417
418pub async fn send(out: &Outgoing) -> Result<Value> {
421 let target = tokio::net::lookup_host(&out.target)
422 .await?
423 .next()
424 .with_context(|| format!("{} did not resolve", out.target))?;
425 let bind: SocketAddr = if target.is_ipv4() {
426 "0.0.0.0:0".parse()?
427 } else {
428 "[::]:0".parse()?
429 };
430 let socket = tokio::net::UdpSocket::bind(bind).await?;
431 if !out.inform {
432 socket.send_to(&out.packet, target).await?;
433 return Ok(json!({"sent": true, "target": target.to_string(), "kind": "trap"}));
434 }
435 let mut buf = vec![0u8; 65535];
436 for attempt in 1..=INFORM_ATTEMPTS {
437 socket.send_to(&out.packet, target).await?;
438 let deadline = tokio::time::Instant::now() + INFORM_TIMEOUT;
439 while let Ok(Ok((n, from))) =
440 tokio::time::timeout_at(deadline, socket.recv_from(&mut buf)).await
441 {
442 if from.ip() != target.ip() {
443 continue;
444 }
445 if super::check_ber_structure(&buf[..n], super::MAX_BER_DEPTH).is_err() {
448 continue;
449 }
450 if let Ok(msg) = ber::decode::<v2c::Message<v2::Pdus>>(&buf[..n]) {
451 if let v2::Pdus::Response(r) = msg.data {
452 if r.0.request_id == out.request_id {
453 return Ok(
454 json!({"sent": true, "target": target.to_string(), "kind": "inform",
455 "acknowledged": true, "attempts": attempt, "error_status": r.0.error_status}),
456 );
457 }
458 }
459 }
460 }
461 }
462 Ok(
463 json!({"sent": true, "target": target.to_string(), "kind": "inform", "acknowledged": false, "attempts": INFORM_ATTEMPTS}),
464 )
465}