1use std::net::SocketAddr;
7use std::sync::RwLock;
8use std::time::Duration;
9
10use bytes::Bytes;
11use tokio::sync::Mutex as AsyncMutex;
12
13use crate::client::{Auth, Client, ClientConfig, CommunityVersion, Retry};
14use crate::error::{Error, Result};
15use crate::message::CommunityMessage;
16use crate::oid::Oid;
17use crate::pdu::Pdu;
18use crate::transport::{UdpHandle, UdpTransport};
19use crate::v3::{DerivedKeys, UsmConfig};
20use crate::varbind::VarBind;
21use crate::version::Version;
22
23pub(crate) struct TrapSink {
28 pub(crate) dest: SocketAddr,
29 pub(crate) version: Version,
30 pub(crate) community: Bytes,
31 pub(crate) v3_security: Option<UsmConfig>,
32 pub(crate) derived_keys: RwLock<Option<DerivedKeys>>,
35 inform_timeout: Duration,
37 inform_retry: Retry,
38 inform_client: AsyncMutex<Option<(UdpTransport, Client<UdpHandle>)>>,
41}
42
43impl TrapSink {
44 pub(crate) fn new(
46 dest: SocketAddr,
47 auth: Auth,
48 inform_timeout: Duration,
49 inform_retry: Retry,
50 ) -> Self {
51 match auth {
52 Auth::Community { version, community } => {
53 let snmp_version = match version {
54 CommunityVersion::V1 => Version::V1,
55 CommunityVersion::V2c => Version::V2c,
56 };
57 TrapSink {
58 dest,
59 version: snmp_version,
60 community: Bytes::copy_from_slice(community.as_bytes()),
61 v3_security: None,
62 derived_keys: RwLock::new(None),
63 inform_timeout,
64 inform_retry,
65 inform_client: AsyncMutex::new(None),
66 }
67 }
68 Auth::Usm(security) => TrapSink {
69 dest,
70 version: Version::V3,
71 community: Bytes::new(),
72 v3_security: Some(security),
73 derived_keys: RwLock::new(None),
74 inform_timeout,
75 inform_retry,
76 inform_client: AsyncMutex::new(None),
77 },
78 }
79 }
80
81 fn ensure_keys_derived(&self, engine_id: &[u8]) -> Result<()> {
83 {
84 let keys = self.derived_keys.read().map_err(|_| {
85 Error::Config("trap sink derived_keys lock poisoned".into()).boxed()
86 })?;
87 if keys.is_some() {
88 return Ok(());
89 }
90 }
91
92 let security = self.v3_security.as_ref().ok_or_else(|| {
93 Error::Config("V3 security not configured for trap sink".into()).boxed()
94 })?;
95
96 let keys = security
97 .derive_keys(engine_id)
98 .map_err(|e| Error::Config(e.to_string().into()).boxed())?;
99
100 let mut derived = self
101 .derived_keys
102 .write()
103 .map_err(|_| Error::Config("trap sink derived_keys lock poisoned".into()).boxed())?;
104 *derived = Some(keys);
105
106 Ok(())
107 }
108
109 async fn get_or_create_inform_client(&self) -> Result<Client<UdpHandle>> {
111 let mut guard = self.inform_client.lock().await;
112 if let Some((_, ref client)) = *guard {
113 return Ok(client.clone());
114 }
115
116 let config = match self.version {
117 Version::V1 => unreachable!("v1 does not support informs"),
118 Version::V2c => ClientConfig {
119 version: Version::V2c,
120 community: self.community.clone(),
121 timeout: self.inform_timeout,
122 retry: self.inform_retry.clone(),
123 v3_security: None,
124 ..ClientConfig::default()
125 },
126 Version::V3 => ClientConfig {
127 version: Version::V3,
128 community: Bytes::new(),
129 timeout: self.inform_timeout,
130 retry: self.inform_retry.clone(),
131 v3_security: self.v3_security.clone(),
132 ..ClientConfig::default()
133 },
134 };
135
136 let bind_addr = if self.dest.is_ipv6() {
137 "[::]:0"
138 } else {
139 "0.0.0.0:0"
140 };
141 let transport = UdpTransport::bind(bind_addr).await?;
142 let handle = transport.handle(self.dest);
143 let client = Client::new(handle, config);
144 *guard = Some((transport, client.clone()));
145 Ok(client)
146 }
147}
148
149#[derive(Debug)]
155pub struct SinkOutcome {
156 pub dest: SocketAddr,
158 pub result: Result<()>,
160}
161
162#[derive(Debug)]
170pub struct NotificationOutcome {
171 sinks: Vec<SinkOutcome>,
172}
173
174impl NotificationOutcome {
175 pub fn sinks(&self) -> &[SinkOutcome] {
177 &self.sinks
178 }
179
180 pub fn failures(&self) -> impl Iterator<Item = &SinkOutcome> {
182 self.sinks.iter().filter(|s| s.result.is_err())
183 }
184
185 pub fn all_succeeded(&self) -> bool {
188 self.sinks.iter().all(|s| s.result.is_ok())
189 }
190
191 pub fn len(&self) -> usize {
193 self.sinks.len()
194 }
195
196 pub fn is_empty(&self) -> bool {
198 self.sinks.is_empty()
199 }
200
201 pub fn into_sinks(self) -> Vec<SinkOutcome> {
203 self.sinks
204 }
205}
206
207impl super::Agent {
208 pub async fn send_trap(
237 &self,
238 trap_oid: &Oid,
239 uptime: u32,
240 varbinds: Vec<VarBind>,
241 ) -> Result<()> {
242 let outcome = self.send_trap_detailed(trap_oid, uptime, varbinds).await;
243 for sink in outcome.failures() {
244 if let Err(ref e) = sink.result {
245 tracing::warn!(target: "async_snmp::agent", { snmp.dest = %sink.dest, error = %e }, "failed to send trap");
246 }
247 }
248 Ok(())
249 }
250
251 pub async fn send_trap_detailed(
259 &self,
260 trap_oid: &Oid,
261 uptime: u32,
262 varbinds: Vec<VarBind>,
263 ) -> NotificationOutcome {
264 let sinks = &self.inner.trap_sinks;
265 let mut outcomes = Vec::with_capacity(sinks.len());
266 if sinks.is_empty() {
267 return NotificationOutcome { sinks: outcomes };
268 }
269
270 let request_id = self.next_notification_id();
271 let pdu = Pdu::trap_v2(request_id, uptime, trap_oid, varbinds);
272
273 for sink in sinks {
274 let result = self.send_trap_to_sink(sink, &pdu).await;
275 outcomes.push(SinkOutcome {
276 dest: sink.dest,
277 result,
278 });
279 }
280
281 NotificationOutcome { sinks: outcomes }
282 }
283
284 pub async fn send_inform(
314 &self,
315 trap_oid: &Oid,
316 uptime: u32,
317 varbinds: Vec<VarBind>,
318 ) -> Result<()> {
319 let outcome = self.send_inform_detailed(trap_oid, uptime, varbinds).await;
320 for sink in outcome.failures() {
321 if let Err(ref e) = sink.result {
322 tracing::warn!(target: "async_snmp::agent", { snmp.dest = %sink.dest, error = %e }, "failed to send inform");
323 }
324 }
325 Ok(())
326 }
327
328 pub async fn send_inform_detailed(
338 &self,
339 trap_oid: &Oid,
340 uptime: u32,
341 varbinds: Vec<VarBind>,
342 ) -> NotificationOutcome {
343 let sinks = &self.inner.trap_sinks;
344 let mut outcomes = Vec::new();
345
346 for sink in sinks {
347 if sink.version == Version::V1 {
348 continue;
349 }
350
351 let result = self
352 .send_inform_to_sink(sink, trap_oid, uptime, &varbinds)
353 .await;
354 outcomes.push(SinkOutcome {
355 dest: sink.dest,
356 result,
357 });
358 }
359
360 NotificationOutcome { sinks: outcomes }
361 }
362
363 async fn send_trap_to_sink(&self, sink: &TrapSink, pdu: &Pdu) -> Result<()> {
365 let data = match sink.version {
366 Version::V1 => {
367 let local_ip = match self.inner.socket.local_addr() {
370 Ok(addr) => match addr.ip() {
371 std::net::IpAddr::V4(v4) => v4.octets(),
372 std::net::IpAddr::V6(_) => [0, 0, 0, 0],
373 },
374 Err(_) => [0, 0, 0, 0],
375 };
376 let trap = pdu.to_v1_trap(local_ip).ok_or_else(|| {
377 Error::Config("cannot convert trap to v1 for sink (Counter64 varbind?)".into())
378 .boxed()
379 })?;
380 let msg = CommunityMessage::v1_trap(sink.community.clone(), trap);
381 msg.encode()
382 }
383 Version::V2c => {
384 let msg = CommunityMessage::new(Version::V2c, sink.community.clone(), pdu.clone());
385 msg.encode()
386 }
387 Version::V3 => {
388 let security = sink.v3_security.as_ref().ok_or_else(|| {
389 Error::Config("V3 security not configured for trap sink".into()).boxed()
390 })?;
391
392 sink.ensure_keys_derived(&self.inner.state.engine_id)?;
393 let derived = sink.derived_keys.read().map_err(|_| {
394 Error::Config("trap sink derived_keys lock poisoned".into()).boxed()
395 })?;
396
397 let (engine_boots, engine_time) = self.inner.state.authoritative_boots_time()?;
398
399 let msg_id = self.next_notification_id();
400 let encoded = crate::v3::encode::encode_v3_message(
401 pdu,
402 msg_id,
403 &self.inner.state.engine_id,
404 engine_boots,
405 engine_time,
406 security,
407 derived.as_ref(),
408 &self.inner.salt_counter,
409 false, crate::v3::DEFAULT_MSG_MAX_SIZE,
411 )?;
412 Bytes::from(encoded)
413 }
414 };
415
416 tracing::debug!(target: "async_snmp::agent", { snmp.dest = %sink.dest, snmp.bytes = data.len() }, "sending trap");
417 self.inner
418 .socket
419 .send_to(&data, sink.dest)
420 .await
421 .map_err(|e| Error::Network {
422 target: sink.dest,
423 source: e,
424 })?;
425
426 Ok(())
427 }
428
429 async fn send_inform_to_sink(
431 &self,
432 sink: &TrapSink,
433 trap_oid: &Oid,
434 uptime: u32,
435 varbinds: &[VarBind],
436 ) -> Result<()> {
437 let client = sink.get_or_create_inform_client().await?;
438 client
439 .send_inform(trap_oid, uptime, varbinds.to_vec())
440 .await?;
441
442 Ok(())
443 }
444
445 fn next_notification_id(&self) -> i32 {
447 use std::sync::atomic::Ordering;
448 self.inner
449 .notification_id
450 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
451 Some(if v == i32::MAX { 1 } else { v + 1 })
452 })
453 .unwrap_or(1)
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use crate::agent::Agent;
460
461 #[tokio::test]
462 async fn test_notification_ids_are_per_agent() {
463 let agent_a = Agent::builder()
466 .bind("127.0.0.1:0")
467 .community(b"public")
468 .build()
469 .await
470 .unwrap();
471 let agent_b = Agent::builder()
472 .bind("127.0.0.1:0")
473 .community(b"public")
474 .build()
475 .await
476 .unwrap();
477
478 let a1 = agent_a.next_notification_id();
480 let a2 = agent_a.next_notification_id();
481 let a3 = agent_a.next_notification_id();
482 assert_eq!((a1, a2, a3), (1, 2, 3));
483
484 let b1 = agent_b.next_notification_id();
486 let b2 = agent_b.next_notification_id();
487 assert_eq!((b1, b2), (1, 2));
488
489 assert_eq!(agent_a.next_notification_id(), 4);
491 }
492}