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, UsmAuth};
14use crate::error::{Error, Result};
15use crate::message::CommunityMessage;
16use crate::notification::{DerivedKeys, UsmConfig};
17use crate::oid::Oid;
18use crate::pdu::Pdu;
19use crate::transport::{UdpHandle, UdpTransport};
20use crate::v3::compute_engine_boots_time;
21use crate::varbind::VarBind;
22use crate::version::Version;
23
24pub(crate) struct TrapSink {
29 pub(crate) dest: SocketAddr,
30 pub(crate) version: Version,
31 pub(crate) community: Bytes,
32 pub(crate) v3_security: Option<UsmConfig>,
33 pub(crate) derived_keys: RwLock<Option<DerivedKeys>>,
36 inform_timeout: Duration,
38 inform_retry: Retry,
39 inform_client: AsyncMutex<Option<(UdpTransport, Client<UdpHandle>)>>,
42}
43
44impl TrapSink {
45 pub(crate) fn new(
47 dest: SocketAddr,
48 auth: Auth,
49 inform_timeout: Duration,
50 inform_retry: Retry,
51 ) -> Self {
52 match auth {
53 Auth::Community { version, community } => {
54 let snmp_version = match version {
55 CommunityVersion::V1 => Version::V1,
56 CommunityVersion::V2c => Version::V2c,
57 };
58 TrapSink {
59 dest,
60 version: snmp_version,
61 community: Bytes::copy_from_slice(community.as_bytes()),
62 v3_security: None,
63 derived_keys: RwLock::new(None),
64 inform_timeout,
65 inform_retry,
66 inform_client: AsyncMutex::new(None),
67 }
68 }
69 Auth::Usm(usm) => {
70 let security = resolve_usm_config(&usm);
71 TrapSink {
72 dest,
73 version: Version::V3,
74 community: Bytes::new(),
75 v3_security: Some(security),
76 derived_keys: RwLock::new(None),
77 inform_timeout,
78 inform_retry,
79 inform_client: AsyncMutex::new(None),
80 }
81 }
82 }
83 }
84
85 fn ensure_keys_derived(&self, engine_id: &[u8]) -> Result<()> {
87 {
88 let keys = self.derived_keys.read().map_err(|_| {
89 Error::Config("trap sink derived_keys lock poisoned".into()).boxed()
90 })?;
91 if keys.is_some() {
92 return Ok(());
93 }
94 }
95
96 let security = self.v3_security.as_ref().ok_or_else(|| {
97 Error::Config("V3 security not configured for trap sink".into()).boxed()
98 })?;
99
100 let keys = security
101 .derive_keys(engine_id)
102 .map_err(|e| Error::Config(e.to_string().into()).boxed())?;
103
104 let mut derived = self
105 .derived_keys
106 .write()
107 .map_err(|_| Error::Config("trap sink derived_keys lock poisoned".into()).boxed())?;
108 *derived = Some(keys);
109
110 Ok(())
111 }
112
113 async fn get_or_create_inform_client(&self) -> Result<Client<UdpHandle>> {
115 let mut guard = self.inform_client.lock().await;
116 if let Some((_, ref client)) = *guard {
117 return Ok(client.clone());
118 }
119
120 let config = match self.version {
121 Version::V1 => unreachable!("v1 does not support informs"),
122 Version::V2c => ClientConfig {
123 version: Version::V2c,
124 community: self.community.clone(),
125 timeout: self.inform_timeout,
126 retry: self.inform_retry.clone(),
127 v3_security: None,
128 ..ClientConfig::default()
129 },
130 Version::V3 => ClientConfig {
131 version: Version::V3,
132 community: Bytes::new(),
133 timeout: self.inform_timeout,
134 retry: self.inform_retry.clone(),
135 v3_security: self.v3_security.clone(),
136 ..ClientConfig::default()
137 },
138 };
139
140 let bind_addr = if self.dest.is_ipv6() {
141 "[::]:0"
142 } else {
143 "0.0.0.0:0"
144 };
145 let transport = UdpTransport::bind(bind_addr).await?;
146 let handle = transport.handle(self.dest);
147 let client = Client::new(handle, config);
148 *guard = Some((transport, client.clone()));
149 Ok(client)
150 }
151}
152
153fn resolve_usm_config(usm: &UsmAuth) -> UsmConfig {
155 let mut security = UsmConfig::new(Bytes::copy_from_slice(usm.username.as_bytes()));
156 if let Some(context_name) = &usm.context_name {
157 security = security.context_name(Bytes::copy_from_slice(context_name.as_bytes()));
158 }
159
160 if let Some(ref master_keys) = usm.master_keys {
161 security = security.with_master_keys(master_keys.clone());
162 } else {
163 if let (Some(auth_proto), Some(auth_pass)) = (usm.auth_protocol, &usm.auth_password) {
164 security = security.auth(auth_proto, auth_pass.as_bytes());
165 }
166 if let (Some(priv_proto), Some(priv_pass)) = (usm.priv_protocol, &usm.priv_password) {
167 security = security.privacy(priv_proto, priv_pass.as_bytes());
168 }
169 }
170
171 security
172}
173
174#[derive(Debug)]
180pub struct SinkOutcome {
181 pub dest: SocketAddr,
183 pub result: Result<()>,
185}
186
187#[derive(Debug)]
195pub struct NotificationOutcome {
196 sinks: Vec<SinkOutcome>,
197}
198
199impl NotificationOutcome {
200 pub fn sinks(&self) -> &[SinkOutcome] {
202 &self.sinks
203 }
204
205 pub fn failures(&self) -> impl Iterator<Item = &SinkOutcome> {
207 self.sinks.iter().filter(|s| s.result.is_err())
208 }
209
210 pub fn all_succeeded(&self) -> bool {
213 self.sinks.iter().all(|s| s.result.is_ok())
214 }
215
216 pub fn len(&self) -> usize {
218 self.sinks.len()
219 }
220
221 pub fn is_empty(&self) -> bool {
223 self.sinks.is_empty()
224 }
225
226 pub fn into_sinks(self) -> Vec<SinkOutcome> {
228 self.sinks
229 }
230}
231
232impl super::Agent {
233 pub async fn send_trap(
260 &self,
261 trap_oid: &Oid,
262 uptime: u32,
263 varbinds: Vec<VarBind>,
264 ) -> Result<()> {
265 let outcome = self.send_trap_detailed(trap_oid, uptime, varbinds).await;
266 for sink in outcome.failures() {
267 if let Err(ref e) = sink.result {
268 tracing::warn!(target: "async_snmp::agent", { snmp.dest = %sink.dest, error = %e }, "failed to send trap");
269 }
270 }
271 Ok(())
272 }
273
274 pub async fn send_trap_detailed(
282 &self,
283 trap_oid: &Oid,
284 uptime: u32,
285 varbinds: Vec<VarBind>,
286 ) -> NotificationOutcome {
287 let sinks = &self.inner.trap_sinks;
288 let mut outcomes = Vec::with_capacity(sinks.len());
289 if sinks.is_empty() {
290 return NotificationOutcome { sinks: outcomes };
291 }
292
293 let request_id = self.next_notification_id();
294 let pdu = Pdu::trap_v2(request_id, uptime, trap_oid, varbinds);
295
296 for sink in sinks {
297 let result = self.send_trap_to_sink(sink, &pdu).await;
298 outcomes.push(SinkOutcome {
299 dest: sink.dest,
300 result,
301 });
302 }
303
304 NotificationOutcome { sinks: outcomes }
305 }
306
307 pub async fn send_inform(
334 &self,
335 trap_oid: &Oid,
336 uptime: u32,
337 varbinds: Vec<VarBind>,
338 ) -> Result<()> {
339 let outcome = self.send_inform_detailed(trap_oid, uptime, varbinds).await;
340 for sink in outcome.failures() {
341 if let Err(ref e) = sink.result {
342 tracing::warn!(target: "async_snmp::agent", { snmp.dest = %sink.dest, error = %e }, "failed to send inform");
343 }
344 }
345 Ok(())
346 }
347
348 pub async fn send_inform_detailed(
358 &self,
359 trap_oid: &Oid,
360 uptime: u32,
361 varbinds: Vec<VarBind>,
362 ) -> NotificationOutcome {
363 let sinks = &self.inner.trap_sinks;
364 let mut outcomes = Vec::new();
365
366 for sink in sinks {
367 if sink.version == Version::V1 {
368 continue;
369 }
370
371 let result = self
372 .send_inform_to_sink(sink, trap_oid, uptime, &varbinds)
373 .await;
374 outcomes.push(SinkOutcome {
375 dest: sink.dest,
376 result,
377 });
378 }
379
380 NotificationOutcome { sinks: outcomes }
381 }
382
383 async fn send_trap_to_sink(&self, sink: &TrapSink, pdu: &Pdu) -> Result<()> {
385 let data = match sink.version {
386 Version::V1 => {
387 let local_ip = match self.inner.socket.local_addr() {
390 Ok(addr) => match addr.ip() {
391 std::net::IpAddr::V4(v4) => v4.octets(),
392 std::net::IpAddr::V6(_) => [0, 0, 0, 0],
393 },
394 Err(_) => [0, 0, 0, 0],
395 };
396 let trap = pdu.to_v1_trap(local_ip).ok_or_else(|| {
397 Error::Config("cannot convert trap to v1 for sink (Counter64 varbind?)".into())
398 .boxed()
399 })?;
400 let msg = CommunityMessage::v1_trap(sink.community.clone(), trap);
401 msg.encode()
402 }
403 Version::V2c => {
404 let msg = CommunityMessage::new(Version::V2c, sink.community.clone(), pdu.clone());
405 msg.encode()
406 }
407 Version::V3 => {
408 let security = sink.v3_security.as_ref().ok_or_else(|| {
409 Error::Config("V3 security not configured for trap sink".into()).boxed()
410 })?;
411
412 sink.ensure_keys_derived(&self.inner.state.engine_id)?;
413 let derived = sink.derived_keys.read().map_err(|_| {
414 Error::Config("trap sink derived_keys lock poisoned".into()).boxed()
415 })?;
416
417 let elapsed_secs = self.inner.state.engine_start.elapsed().as_secs();
418 let (engine_boots, engine_time) =
419 compute_engine_boots_time(self.inner.state.engine_boots_base, elapsed_secs);
420
421 let msg_id = self.next_notification_id();
422 let encoded = crate::v3::encode::encode_v3_message(
423 pdu,
424 msg_id,
425 &self.inner.state.engine_id,
426 engine_boots,
427 engine_time,
428 security,
429 derived.as_ref(),
430 &self.inner.salt_counter,
431 false, crate::v3::DEFAULT_MSG_MAX_SIZE,
433 )?;
434 Bytes::from(encoded)
435 }
436 };
437
438 tracing::debug!(target: "async_snmp::agent", { snmp.dest = %sink.dest, snmp.bytes = data.len() }, "sending trap");
439 self.inner
440 .socket
441 .send_to(&data, sink.dest)
442 .await
443 .map_err(|e| Error::Network {
444 target: sink.dest,
445 source: e,
446 })?;
447
448 Ok(())
449 }
450
451 async fn send_inform_to_sink(
453 &self,
454 sink: &TrapSink,
455 trap_oid: &Oid,
456 uptime: u32,
457 varbinds: &[VarBind],
458 ) -> Result<()> {
459 let client = sink.get_or_create_inform_client().await?;
460 client
461 .send_inform(trap_oid, uptime, varbinds.to_vec())
462 .await?;
463
464 Ok(())
465 }
466
467 fn next_notification_id(&self) -> i32 {
469 use std::sync::atomic::Ordering;
470 self.inner
471 .notification_id
472 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
473 Some(if v == i32::MAX { 1 } else { v + 1 })
474 })
475 .unwrap_or(1)
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use crate::agent::Agent;
482
483 #[tokio::test]
484 async fn test_notification_ids_are_per_agent() {
485 let agent_a = Agent::builder()
488 .bind("127.0.0.1:0")
489 .community(b"public")
490 .build()
491 .await
492 .unwrap();
493 let agent_b = Agent::builder()
494 .bind("127.0.0.1:0")
495 .community(b"public")
496 .build()
497 .await
498 .unwrap();
499
500 let a1 = agent_a.next_notification_id();
502 let a2 = agent_a.next_notification_id();
503 let a3 = agent_a.next_notification_id();
504 assert_eq!((a1, a2, a3), (1, 2, 3));
505
506 let b1 = agent_b.next_notification_id();
508 let b2 = agent_b.next_notification_id();
509 assert_eq!((b1, b2), (1, 2));
510
511 assert_eq!(agent_a.next_notification_id(), 4);
513 }
514}