Skip to main content

async_snmp/notification/
mod.rs

1//! SNMP Notification Receiver (RFC 3413).
2//!
3//! This module provides functionality for receiving SNMP notifications:
4//! - `TrapV1` (SNMP v1 format, different PDU structure)
5//! - `TrapV2`/`SNMPv2-Trap` (SNMP v2c/v3 format)
6//! - `InformRequest` (confirmed notification, requires response)
7//!
8//! # Example
9//!
10//! Receive v1/v2c notifications. A receiver constructed with `bind` has no
11//! USM user table, so v3 notifications are rejected; see below for v3.
12//!
13//! ```rust,no_run
14//! use async_snmp::notification::{NotificationReceiver, Notification};
15//! use std::net::SocketAddr;
16//!
17//! #[tokio::main]
18//! async fn main() -> Result<(), Box<async_snmp::Error>> {
19//!     let receiver = NotificationReceiver::bind("0.0.0.0:162").await?;
20//!
21//!     loop {
22//!         match receiver.recv().await {
23//!             Ok((notification, source)) => {
24//!                 println!("Received notification from {}: {:?}", source, notification);
25//!             }
26//!             Err(e) => {
27//!                 eprintln!("Error receiving notification: {}", e);
28//!             }
29//!         }
30//!     }
31//! }
32//! ```
33//!
34//! # V3 Notifications
35//!
36//! To receive V3 traps and `InformRequests`, configure USM credentials via
37//! the builder. Only notifications from registered usernames are accepted,
38//! at any security level including noAuthNoPriv:
39//!
40//! ```rust,no_run
41//! use async_snmp::notification::NotificationReceiver;
42//! use async_snmp::{AuthProtocol, AuthoritativeEngine, PrivProtocol};
43//! use std::convert::Infallible;
44//!
45//! # async fn example() -> Result<(), Box<async_snmp::Error>> {
46//! # // Replace this no-op with durable storage in an application.
47//! let engine = AuthoritativeEngine::install(b"receiver-engine".to_vec(), |_| {
48//!     Ok::<(), Infallible>(())
49//! })?;
50//! let receiver = NotificationReceiver::builder()
51//!     .bind("0.0.0.0:162")
52//!     .authoritative_engine(engine)
53//!     .usm_user("informuser", |u| {
54//!         u.auth_priv(
55//!             AuthProtocol::Sha1,
56//!             b"authpass123",
57//!             PrivProtocol::Aes128,
58//!             b"privpass123",
59//!         )
60//!     })
61//!     .build()
62//!     .await?;
63//! # Ok(())
64//! # }
65//! ```
66//!
67//! # Mixed Versions on One Port
68//!
69//! A single receiver on one UDP port handles v1, v2c, and v3 concurrently;
70//! each datagram is dispatched by its version field. Community filtering
71//! (v1/v2c) and USM users (v3) are independent and can be configured
72//! together — configuring one does not disable the other:
73//!
74//! ```rust,no_run
75//! use async_snmp::notification::NotificationReceiver;
76//! use async_snmp::{AuthProtocol, AuthoritativeEngine, PrivProtocol};
77//! use std::convert::Infallible;
78//!
79//! # async fn example() -> Result<(), Box<async_snmp::Error>> {
80//! # // Replace this no-op with durable storage in an application.
81//! let engine = AuthoritativeEngine::install(b"receiver-engine".to_vec(), |_| {
82//!     Ok::<(), Infallible>(())
83//! })?;
84//! let receiver = NotificationReceiver::builder()
85//!     .bind("0.0.0.0:162")
86//!     .authoritative_engine(engine)
87//!     .communities(["public", "monitor"]) // gates v1/v2c
88//!     .usm_user("trapuser", |u| {          // gates v3
89//!         u.auth_priv(
90//!             AuthProtocol::Sha1,
91//!             b"authpass123",
92//!             PrivProtocol::Aes128,
93//!             b"privpass123",
94//!         )
95//!     })
96//!     .build()
97//!     .await?;
98//! # Ok(())
99//! # }
100//! ```
101//!
102//! The two mechanisms confer very different trust (a `public` v2c trap versus
103//! an authPriv v3 trap arrive on the same socket). Each [`Notification`]
104//! variant carries how it was authenticated — the community for v1/v2c, the
105//! username and [`security_level`](Notification::security_level) for v3 — so
106//! branch on the variant when `recv` returns to apply per-version policy.
107//!
108//! # V3 Authoritative Roles
109//!
110//! The sender of an unconfirmed V3 trap is authoritative. The receiver verifies
111//! the trap against per-sender engine state and reports the received security
112//! level to the application. For a V3 Inform, this receiver is authoritative:
113//! the Inform must be localized to its stable engine ID, and the automatic
114//! Response uses its current coherent boots/time rather than echoing the
115//! incoming tuple. Configuring any USM user therefore requires a persisted
116//! [`AuthoritativeEngine`].
117
118mod handlers;
119mod varbind;
120
121use std::collections::HashMap;
122use std::net::SocketAddr;
123use std::sync::atomic::Ordering;
124use std::sync::{Arc, Mutex};
125use std::time::Instant;
126
127use bytes::Bytes;
128use subtle::ConstantTimeEq;
129use tokio::net::UdpSocket;
130use tracing::instrument;
131
132use crate::error::{Error, Result};
133use crate::message::SecurityLevel;
134use crate::oid::Oid;
135use crate::pdu::TrapV1Pdu;
136use crate::util::bind_udp_socket;
137use crate::v3::process::UsmStats;
138use crate::v3::{AuthoritativeEngine, EngineState, SaltCounter};
139use crate::varbind::VarBind;
140use crate::version::Version;
141
142// Re-exports retained for compatibility with the original notification-local path.
143pub use crate::v3::{DerivedKeys, UsmConfig};
144pub use varbind::validate_notification_varbinds;
145
146/// Maximum number of distinct remote authoritative engines whose timeliness
147/// state is retained for trap senders. A peer holding one USM credential can
148/// authenticate under arbitrarily many fabricated engine IDs (keys are
149/// localized per engine ID), so the table is bounded and the
150/// least-recently-updated engine is evicted when full.
151const MAX_REMOTE_ENGINES: usize = 8192;
152
153/// Decide whether a v1/v2c notification carrying `community` is accepted.
154///
155/// An empty `configured` list accepts any community (filtering is opt-in).
156/// Otherwise the community must equal one of the configured strings. The
157/// comparison runs against every configured entry without early-out and uses
158/// constant-time equality (mirroring `Agent::validate_community`) so a timing
159/// side channel cannot be used to recover a valid community byte by byte.
160pub(super) fn community_allowed(configured: &[Vec<u8>], community: &[u8]) -> bool {
161    if configured.is_empty() {
162        return true;
163    }
164    let mut valid = false;
165    for candidate in configured {
166        if candidate.len() == community.len() && bool::from(candidate.as_slice().ct_eq(community)) {
167            valid = true;
168        }
169    }
170    valid
171}
172
173/// Well-known OIDs for notification varbinds.
174pub mod oids {
175    use crate::oid;
176
177    /// sysUpTime.0 - first varbind in v2c/v3 notifications
178    #[must_use]
179    pub fn sys_uptime() -> crate::Oid {
180        oid!(1, 3, 6, 1, 2, 1, 1, 3, 0)
181    }
182
183    /// snmpTrapOID.0 - second varbind in v2c/v3 notifications (contains trap type)
184    #[must_use]
185    pub fn snmp_trap_oid() -> crate::Oid {
186        oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0)
187    }
188
189    /// snmpTrapEnterprise.0 - optional, enterprise OID for enterprise-specific traps
190    #[must_use]
191    pub fn snmp_trap_enterprise() -> crate::Oid {
192        oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 3, 0)
193    }
194
195    /// snmpTrapAddress.0 - agent address from v1 trap (RFC 3584 Section 3)
196    #[must_use]
197    pub fn snmp_trap_address() -> crate::Oid {
198        oid!(1, 3, 6, 1, 6, 3, 18, 1, 3, 0)
199    }
200
201    /// Standard trap OID prefix (snmpTraps)
202    #[must_use]
203    pub fn snmp_traps() -> crate::Oid {
204        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5)
205    }
206
207    /// coldStart trap OID (snmpTraps.1)
208    #[must_use]
209    pub fn cold_start() -> crate::Oid {
210        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)
211    }
212
213    /// warmStart trap OID (snmpTraps.2)
214    #[must_use]
215    pub fn warm_start() -> crate::Oid {
216        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)
217    }
218
219    /// linkDown trap OID (snmpTraps.3)
220    #[must_use]
221    pub fn link_down() -> crate::Oid {
222        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)
223    }
224
225    /// linkUp trap OID (snmpTraps.4)
226    #[must_use]
227    pub fn link_up() -> crate::Oid {
228        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)
229    }
230
231    /// authenticationFailure trap OID (snmpTraps.5)
232    #[must_use]
233    pub fn auth_failure() -> crate::Oid {
234        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5)
235    }
236
237    /// egpNeighborLoss trap OID (snmpTraps.6)
238    #[must_use]
239    pub fn egp_neighbor_loss() -> crate::Oid {
240        oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6)
241    }
242}
243
244/// Builder for `NotificationReceiver`.
245///
246/// Configures the bind address, optional community filtering for v1/v2c, and
247/// USM credentials for v3. Community filtering and USM users are independent
248/// and may be combined; a single receiver then handles all versions on one
249/// port. Any USM user also requires a persisted [`AuthoritativeEngine`]. See
250/// the [module docs](crate::notification#mixed-versions-on-one-port).
251pub struct NotificationReceiverBuilder {
252    bind_addr: String,
253    usm_users: HashMap<Bytes, UsmConfig>,
254    communities: Vec<Vec<u8>>,
255    authoritative_engine: Option<AuthoritativeEngine>,
256}
257
258impl NotificationReceiverBuilder {
259    /// Create a new builder with default settings.
260    ///
261    /// Defaults:
262    /// - Bind address: `0.0.0.0:162` (UDP, standard SNMP trap port)
263    /// - No USM users (v3 notifications rejected until users are added)
264    /// - No authoritative engine (required when adding a USM user)
265    #[must_use]
266    pub fn new() -> Self {
267        Self {
268            bind_addr: "0.0.0.0:162".to_string(),
269            usm_users: HashMap::new(),
270            communities: Vec::new(),
271            authoritative_engine: None,
272        }
273    }
274
275    /// Set the UDP bind address.
276    ///
277    /// Default is `0.0.0.0:162` (UDP, standard SNMP trap port).
278    #[must_use]
279    pub fn bind(mut self, addr: impl Into<String>) -> Self {
280        self.bind_addr = addr.into();
281        self
282    }
283
284    /// Add a USM user for V3 authentication.
285    ///
286    /// Adding any user requires a persisted [`AuthoritativeEngine`] before
287    /// [`build`](Self::build), because this receiver is authoritative for V3
288    /// Inform exchanges.
289    ///
290    /// # Example
291    ///
292    /// ```rust,no_run
293    /// use async_snmp::notification::NotificationReceiver;
294    /// use async_snmp::{AuthProtocol, AuthoritativeEngine, PrivProtocol};
295    /// use std::convert::Infallible;
296    ///
297    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
298    /// # // Replace this no-op with durable storage in an application.
299    /// let engine = AuthoritativeEngine::install(b"receiver-engine".to_vec(), |_| {
300    ///     Ok::<(), Infallible>(())
301    /// })?;
302    /// let receiver = NotificationReceiver::builder()
303    ///     .bind("0.0.0.0:162")
304    ///     .authoritative_engine(engine)
305    ///     .usm_user("trapuser", |u| {
306    ///         u.auth_priv(
307    ///             AuthProtocol::Sha1,
308    ///             b"authpassword",
309    ///             PrivProtocol::Aes128,
310    ///             b"privpassword",
311    ///         )
312    ///     })
313    ///     .build()
314    ///     .await?;
315    /// # Ok(())
316    /// # }
317    /// ```
318    #[must_use]
319    pub fn usm_user<F>(mut self, username: impl Into<Bytes>, configure: F) -> Self
320    where
321        F: FnOnce(UsmConfig) -> UsmConfig,
322    {
323        let username_bytes: Bytes = username.into();
324        let config = configure(UsmConfig::new(username_bytes.clone()));
325        self.usm_users.insert(username_bytes, config);
326        self
327    }
328
329    /// Restrict accepted v1/v2c notifications to the given community string.
330    ///
331    /// Community filtering is opt-in. With no community configured the
332    /// receiver accepts v1/v2c notifications under any community and surfaces
333    /// the community on the returned [`Notification`] for caller-side policy.
334    /// Once one or more communities are configured, a v1/v2c notification
335    /// whose community matches none of them is dropped and never returned
336    /// from [`NotificationReceiver::recv`]; a dropped inform is not
337    /// acknowledged. Comparison is constant-time. This does not affect v3,
338    /// which is gated by USM.
339    ///
340    /// Call multiple times to accept several communities.
341    ///
342    /// # Example
343    ///
344    /// ```rust,no_run
345    /// use async_snmp::notification::NotificationReceiver;
346    ///
347    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
348    /// let receiver = NotificationReceiver::builder()
349    ///     .bind("0.0.0.0:162")
350    ///     .community(b"public")
351    ///     .build()
352    ///     .await?;
353    /// # Ok(())
354    /// # }
355    /// ```
356    #[must_use]
357    pub fn community(mut self, community: &[u8]) -> Self {
358        self.communities.push(community.to_vec());
359        self
360    }
361
362    /// Restrict accepted v1/v2c notifications to any of the given communities.
363    ///
364    /// Convenience for calling [`Self::community`] once per entry. See that
365    /// method for the filtering semantics.
366    ///
367    /// # Example
368    ///
369    /// ```rust,no_run
370    /// use async_snmp::notification::NotificationReceiver;
371    ///
372    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
373    /// let receiver = NotificationReceiver::builder()
374    ///     .bind("0.0.0.0:162")
375    ///     .communities(["public", "monitor"])
376    ///     .build()
377    ///     .await?;
378    /// # Ok(())
379    /// # }
380    /// ```
381    #[must_use]
382    pub fn communities<I, C>(mut self, communities: I) -> Self
383    where
384        I: IntoIterator<Item = C>,
385        C: AsRef<[u8]>,
386    {
387        for c in communities {
388            self.communities.push(c.as_ref().to_vec());
389        }
390        self
391    }
392
393    /// Set the persisted local authoritative engine state for `SNMPv3`.
394    ///
395    /// A receiver with USM users can be authoritative for Informs and requires
396    /// this value. Construct it with [`AuthoritativeEngine::install`] on first
397    /// installation or [`AuthoritativeEngine::restart`] on later starts. The
398    /// retained callback persists runtime rollover increments before use.
399    #[must_use]
400    pub fn authoritative_engine(mut self, engine: AuthoritativeEngine) -> Self {
401        self.authoritative_engine = Some(engine);
402        self
403    }
404
405    #[cfg(test)]
406    pub(crate) fn engine_id(mut self, engine_id: impl Into<Vec<u8>>) -> Self {
407        let boots = self
408            .authoritative_engine
409            .as_ref()
410            .map_or(1, AuthoritativeEngine::engine_boots);
411        self.authoritative_engine = Some(AuthoritativeEngine::for_test(engine_id.into(), boots));
412        self
413    }
414
415    #[cfg(test)]
416    pub(crate) fn engine_boots(mut self, boots: u32) -> Self {
417        let engine_id = self
418            .authoritative_engine
419            .as_ref()
420            .map(|engine| engine.engine_id().to_vec())
421            .unwrap_or_else(|| crate::v3::generate_engine_id().to_vec());
422        self.authoritative_engine = Some(AuthoritativeEngine::for_test(engine_id, boots));
423        self
424    }
425
426    /// Build the notification receiver.
427    ///
428    /// Returns a configuration error when a USM user is configured without a
429    /// persisted [`AuthoritativeEngine`].
430    pub async fn build(mut self) -> Result<NotificationReceiver> {
431        // Precompute master keys so the expensive password expansion runs once
432        // here instead of on every inbound packet (CPU amplification).
433        for config in self.usm_users.values_mut() {
434            config.precompute_master_keys();
435        }
436
437        let bind_addr: SocketAddr = self.bind_addr.parse().map_err(|_| {
438            Error::Config(format!("invalid bind address: {}", self.bind_addr).into())
439        })?;
440
441        let socket = bind_udp_socket(bind_addr, None, None, false)
442            .await
443            .map_err(|e| Error::Network {
444                target: bind_addr,
445                source: e,
446            })?;
447
448        let local_addr = socket.local_addr().map_err(|e| Error::Network {
449            target: bind_addr,
450            source: e,
451        })?;
452
453        let (authoritative_engine, engine_id, engine_boots) = match self.authoritative_engine {
454            Some(engine) => {
455                let (engine_boots, _) = engine.current_boots_time()?;
456                let engine_id = Bytes::copy_from_slice(engine.engine_id());
457                (Some(engine), engine_id, engine_boots)
458            }
459            None if !self.usm_users.is_empty() => {
460                return Err(Error::Config(
461                    "authoritative engine state is required for SNMPv3 notification receiving"
462                        .into(),
463                )
464                .boxed());
465            }
466            None => (None, crate::v3::generate_engine_id(), 1),
467        };
468
469        Ok(NotificationReceiver {
470            inner: Arc::new(ReceiverInner {
471                authoritative_engine,
472                socket,
473                local_addr,
474                usm_users: self.usm_users,
475                communities: self.communities,
476                engine_id,
477                salt_counter: SaltCounter::new(),
478                engine_boots_base: engine_boots,
479                engine_start: Instant::now(),
480                usm_stats: UsmStats::default(),
481                remote_engines: Mutex::new(HashMap::new()),
482            }),
483        })
484    }
485}
486
487impl Default for NotificationReceiverBuilder {
488    fn default() -> Self {
489        Self::new()
490    }
491}
492
493/// Received SNMP notification.
494///
495/// This enum represents all types of SNMP notifications that can be received:
496/// - `SNMPv1` Trap (different PDU structure)
497/// - SNMPv2c/v3 Trap (standard PDU with sysUpTime.0 and snmpTrapOID.0)
498/// - `InformRequest` (confirmed notification, response will be sent automatically)
499#[derive(Debug, Clone)]
500pub enum Notification {
501    /// `SNMPv1` Trap with unique PDU structure.
502    TrapV1 {
503        /// Community string used for authentication
504        community: Bytes,
505        /// The trap PDU
506        trap: TrapV1Pdu,
507    },
508
509    /// `SNMPv2c` Trap (unconfirmed notification).
510    TrapV2c {
511        /// Community string used for authentication
512        community: Bytes,
513        /// sysUpTime.0 value (hundredths of seconds since agent init)
514        uptime: u32,
515        /// snmpTrapOID.0 value (trap type identifier)
516        trap_oid: Oid,
517        /// Additional variable bindings
518        varbinds: Vec<VarBind>,
519        /// Original request ID (for logging/correlation)
520        request_id: i32,
521    },
522
523    /// `SNMPv3` Trap (unconfirmed notification).
524    TrapV3 {
525        /// Username from USM
526        username: Bytes,
527        /// Context engine ID
528        context_engine_id: Bytes,
529        /// Context name
530        context_name: Bytes,
531        /// Security level the message was received at. A `NoAuthNoPriv`
532        /// notification is unauthenticated: its username is an unverified
533        /// claim. Callers requiring authentication must check this.
534        security_level: SecurityLevel,
535        /// sysUpTime.0 value
536        uptime: u32,
537        /// snmpTrapOID.0 value
538        trap_oid: Oid,
539        /// Additional variable bindings
540        varbinds: Vec<VarBind>,
541        /// Original request ID
542        request_id: i32,
543    },
544
545    /// `InformRequest` (confirmed notification) - v2c.
546    ///
547    /// A response is automatically sent when this notification is received.
548    InformV2c {
549        /// Community string
550        community: Bytes,
551        /// sysUpTime.0 value
552        uptime: u32,
553        /// snmpTrapOID.0 value
554        trap_oid: Oid,
555        /// Additional variable bindings
556        varbinds: Vec<VarBind>,
557        /// Request ID (used in response)
558        request_id: i32,
559    },
560
561    /// `InformRequest` (confirmed notification) - v3.
562    ///
563    /// A response is automatically sent when this notification is received.
564    InformV3 {
565        /// Username from USM
566        username: Bytes,
567        /// Context engine ID
568        context_engine_id: Bytes,
569        /// Context name
570        context_name: Bytes,
571        /// Security level the message was received at. A `NoAuthNoPriv`
572        /// notification is unauthenticated: its username is an unverified
573        /// claim. Callers requiring authentication must check this.
574        security_level: SecurityLevel,
575        /// sysUpTime.0 value
576        uptime: u32,
577        /// snmpTrapOID.0 value
578        trap_oid: Oid,
579        /// Additional variable bindings
580        varbinds: Vec<VarBind>,
581        /// Request ID
582        request_id: i32,
583    },
584}
585
586impl Notification {
587    /// Get the trap/notification OID.
588    ///
589    /// For `TrapV1`, this is derived from enterprise + generic/specific trap.
590    /// For v2c/v3, this is the snmpTrapOID.0 value.
591    pub fn trap_oid(&self) -> Result<Oid> {
592        match self {
593            Notification::TrapV1 { trap, .. } => trap.v2_trap_oid(),
594            Notification::TrapV2c { trap_oid, .. }
595            | Notification::TrapV3 { trap_oid, .. }
596            | Notification::InformV2c { trap_oid, .. }
597            | Notification::InformV3 { trap_oid, .. } => Ok(trap_oid.clone()),
598        }
599    }
600
601    /// Get the uptime value (sysUpTime.0 or `time_stamp` for v1).
602    pub fn uptime(&self) -> u32 {
603        match self {
604            Notification::TrapV1 { trap, .. } => trap.time_stamp,
605            Notification::TrapV2c { uptime, .. }
606            | Notification::TrapV3 { uptime, .. }
607            | Notification::InformV2c { uptime, .. }
608            | Notification::InformV3 { uptime, .. } => *uptime,
609        }
610    }
611
612    /// Get the variable bindings.
613    pub fn varbinds(&self) -> &[VarBind] {
614        match self {
615            Notification::TrapV1 { trap, .. } => &trap.varbinds,
616            Notification::TrapV2c { varbinds, .. }
617            | Notification::TrapV3 { varbinds, .. }
618            | Notification::InformV2c { varbinds, .. }
619            | Notification::InformV3 { varbinds, .. } => varbinds,
620        }
621    }
622
623    /// Get the security level the notification was received at.
624    ///
625    /// Returns `None` for v1/v2c notifications (community-based, no USM
626    /// security level). For v3 notifications, `NoAuthNoPriv` means the
627    /// message was not authenticated and its username is an unverified
628    /// claim.
629    pub fn security_level(&self) -> Option<SecurityLevel> {
630        match self {
631            Notification::TrapV1 { .. }
632            | Notification::TrapV2c { .. }
633            | Notification::InformV2c { .. } => None,
634            Notification::TrapV3 { security_level, .. }
635            | Notification::InformV3 { security_level, .. } => Some(*security_level),
636        }
637    }
638
639    /// Check if this is a confirmed notification (`InformRequest`).
640    pub fn is_confirmed(&self) -> bool {
641        matches!(
642            self,
643            Notification::InformV2c { .. } | Notification::InformV3 { .. }
644        )
645    }
646
647    /// Get the SNMP version of this notification.
648    pub fn version(&self) -> Version {
649        match self {
650            Notification::TrapV1 { .. } => Version::V1,
651            Notification::TrapV2c { .. } | Notification::InformV2c { .. } => Version::V2c,
652            Notification::TrapV3 { .. } | Notification::InformV3 { .. } => Version::V3,
653        }
654    }
655}
656
657/// SNMP Notification Receiver.
658///
659/// Listens for incoming SNMP notifications (traps and informs) on a UDP socket.
660/// For `InformRequest` notifications, automatically sends a Response-PDU.
661///
662/// # V3 Authentication
663///
664/// To receive authenticated V3 notifications, use the builder pattern to
665/// configure USM credentials and persisted authoritative engine state:
666///
667/// ```rust,no_run
668/// use async_snmp::notification::NotificationReceiver;
669/// use async_snmp::{AuthProtocol, AuthoritativeEngine};
670/// use std::convert::Infallible;
671///
672/// # async fn example() -> Result<(), Box<async_snmp::Error>> {
673/// # // Replace this no-op with durable storage in an application.
674/// let engine = AuthoritativeEngine::install(b"receiver-engine".to_vec(), |_| {
675///     Ok::<(), Infallible>(())
676/// })?;
677/// let receiver = NotificationReceiver::builder()
678///     .bind("0.0.0.0:162")
679///     .authoritative_engine(engine)
680///     .usm_user("trapuser", |u| {
681///         u.auth(AuthProtocol::Sha1, b"authpassword")
682///     })
683///     .build()
684///     .await?;
685/// # Ok(())
686/// # }
687/// ```
688pub struct NotificationReceiver {
689    inner: Arc<ReceiverInner>,
690}
691
692struct ReceiverInner {
693    authoritative_engine: Option<AuthoritativeEngine>,
694    socket: UdpSocket,
695    local_addr: SocketAddr,
696    /// Configured USM users for V3 authentication
697    usm_users: HashMap<Bytes, UsmConfig>,
698    /// Accepted v1/v2c community strings. Empty means accept any community
699    /// (community filtering is opt-in); otherwise a v1/v2c notification whose
700    /// community matches none of these is dropped.
701    communities: Vec<Vec<u8>>,
702    /// Engine ID for V3 discovery responses
703    engine_id: Bytes,
704    /// Salt counter for privacy operations
705    salt_counter: SaltCounter,
706    /// Initial engine boots value at startup, used to compute overflow-adjusted boots.
707    engine_boots_base: u32,
708    /// Time when the receiver was started, used to compute engine time.
709    engine_start: Instant,
710    /// RFC 3414 usmStats counters
711    usm_stats: UsmStats,
712    /// Timeliness state for remote authoritative engines (trap senders),
713    /// keyed by engine ID (RFC 3414 Section 2.3). Seeded from the first
714    /// authenticated message from each engine, so only holders of configured
715    /// credentials can add entries. Bounded to `MAX_REMOTE_ENGINES` with
716    /// least-recently-updated eviction so a credential holder cannot grow it
717    /// without limit by fabricating engine IDs.
718    remote_engines: Mutex<HashMap<Bytes, EngineState>>,
719}
720
721impl ReceiverInner {
722    /// Return one coherent authoritative boots/time pair for the current instant.
723    fn authoritative_boots_time(&self) -> Result<(u32, u32)> {
724        match &self.authoritative_engine {
725            Some(engine) => engine.current_boots_time(),
726            None => {
727                let total_secs = self.engine_start.elapsed().as_secs();
728                Ok(crate::v3::compute_engine_boots_time(
729                    self.engine_boots_base,
730                    total_secs,
731                ))
732            }
733        }
734    }
735}
736
737impl NotificationReceiver {
738    /// Create a builder for configuring the notification receiver.
739    ///
740    /// Use this to configure USM credentials for V3 authentication.
741    #[must_use]
742    pub fn builder() -> NotificationReceiverBuilder {
743        NotificationReceiverBuilder::new()
744    }
745
746    /// Bind to a local address.
747    ///
748    /// The standard SNMP notification port is 162.
749    ///
750    /// A receiver constructed this way handles v1 and v2c notifications
751    /// only: it has no USM user table, so every v3 notification (including
752    /// noAuthNoPriv) is rejected with `usmStatsUnknownUserNames` (RFC 3414
753    /// Section 3.2 Step 4). To receive v3 notifications, use
754    /// [`NotificationReceiver::builder()`] and register users with
755    /// `usm_user`.
756    ///
757    /// # Example
758    ///
759    /// ```rust,no_run
760    /// use async_snmp::notification::NotificationReceiver;
761    ///
762    /// # async fn example() -> Result<(), Box<async_snmp::Error>> {
763    /// // Bind to the standard trap port (requires root/admin on most systems)
764    /// let receiver = NotificationReceiver::bind("0.0.0.0:162").await?;
765    ///
766    /// // Or use an unprivileged port for testing
767    /// let receiver = NotificationReceiver::bind("0.0.0.0:1162").await?;
768    /// # Ok(())
769    /// # }
770    /// ```
771    pub async fn bind(addr: impl AsRef<str>) -> Result<Self> {
772        let addr_str = addr.as_ref();
773        let bind_addr: SocketAddr = addr_str
774            .parse()
775            .map_err(|_| Error::Config(format!("invalid bind address: {addr_str}").into()))?;
776
777        let socket = bind_udp_socket(bind_addr, None, None, false)
778            .await
779            .map_err(|e| Error::Network {
780                target: bind_addr,
781                source: e,
782            })?;
783
784        let local_addr = socket.local_addr().map_err(|e| Error::Network {
785            target: bind_addr,
786            source: e,
787        })?;
788
789        let engine_id = crate::v3::generate_engine_id();
790
791        Ok(Self {
792            inner: Arc::new(ReceiverInner {
793                authoritative_engine: None,
794                socket,
795                local_addr,
796                usm_users: HashMap::new(),
797                communities: Vec::new(),
798                engine_id,
799                salt_counter: SaltCounter::new(),
800                engine_boots_base: 1,
801                engine_start: Instant::now(),
802                usm_stats: UsmStats::default(),
803                remote_engines: Mutex::new(HashMap::new()),
804            }),
805        })
806    }
807
808    /// Get the local address this receiver is bound to.
809    #[must_use]
810    pub fn local_addr(&self) -> SocketAddr {
811        self.inner.local_addr
812    }
813
814    /// Get the local engine ID.
815    ///
816    /// With an [`AuthoritativeEngine`] this is the stable persisted V3
817    /// identity. A receiver without USM users instead has a generated
818    /// process-local ID.
819    #[must_use]
820    pub fn engine_id(&self) -> &[u8] {
821        &self.inner.engine_id
822    }
823
824    /// Get the current local authoritative engine boots value.
825    #[must_use]
826    pub fn engine_boots(&self) -> u32 {
827        match self.inner.authoritative_boots_time() {
828            Ok(pair) => pair.0,
829            Err(_) => self.inner.authoritative_engine.as_ref().map_or(
830                self.inner.engine_boots_base,
831                AuthoritativeEngine::engine_boots,
832            ),
833        }
834    }
835
836    /// Get the usmStatsUnknownEngineIDs counter value.
837    #[must_use]
838    pub fn usm_unknown_engine_ids(&self) -> u32 {
839        self.inner
840            .usm_stats
841            .unknown_engine_ids
842            .load(Ordering::Relaxed)
843    }
844
845    /// Get the usmStatsUnknownUserNames counter value.
846    #[must_use]
847    pub fn usm_unknown_usernames(&self) -> u32 {
848        self.inner
849            .usm_stats
850            .unknown_usernames
851            .load(Ordering::Relaxed)
852    }
853
854    /// Get the usmStatsWrongDigests counter value.
855    #[must_use]
856    pub fn usm_wrong_digests(&self) -> u32 {
857        self.inner.usm_stats.wrong_digests.load(Ordering::Relaxed)
858    }
859
860    /// Get the usmStatsNotInTimeWindows counter value.
861    #[must_use]
862    pub fn usm_not_in_time_windows(&self) -> u32 {
863        self.inner
864            .usm_stats
865            .not_in_time_windows
866            .load(Ordering::Relaxed)
867    }
868
869    /// Get the usmStatsUnsupportedSecLevels counter value.
870    #[must_use]
871    pub fn usm_unsupported_sec_levels(&self) -> u32 {
872        self.inner
873            .usm_stats
874            .unsupported_sec_levels
875            .load(Ordering::Relaxed)
876    }
877
878    /// Get the usmStatsDecryptionErrors counter value.
879    #[must_use]
880    pub fn usm_decryption_errors(&self) -> u32 {
881        self.inner
882            .usm_stats
883            .decryption_errors
884            .load(Ordering::Relaxed)
885    }
886
887    /// Receive a notification.
888    ///
889    /// This method blocks until a notification is received. For `InformRequest`
890    /// notifications, a Response-PDU is automatically sent back to the sender.
891    ///
892    /// Returns the notification and the source address.
893    #[instrument(skip(self), err, fields(snmp.local_addr = %self.local_addr()))]
894    pub async fn recv(&self) -> Result<(Notification, SocketAddr)> {
895        let mut buf = vec![0u8; 65535];
896
897        loop {
898            let (len, source) =
899                self.inner
900                    .socket
901                    .recv_from(&mut buf)
902                    .await
903                    .map_err(|e| Error::Network {
904                        target: self.inner.local_addr,
905                        source: e,
906                    })?;
907
908            let data = Bytes::copy_from_slice(&buf[..len]);
909
910            match self.parse_and_respond(data, source).await {
911                Ok(Some(notification)) => return Ok((notification, source)),
912                Ok(None) => {} // Not a notification PDU, ignore
913                Err(e) => {
914                    // Log parsing error but continue receiving
915                    tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, error = %e }, "failed to parse notification");
916                }
917            }
918        }
919    }
920
921    /// Parse received data and send response if needed.
922    ///
923    /// Returns `None` if the message is not a notification PDU.
924    async fn parse_and_respond(
925        &self,
926        data: Bytes,
927        source: SocketAddr,
928    ) -> Result<Option<Notification>> {
929        match crate::message::peek_version(data.clone(), source)? {
930            Version::V1 => self.handle_v1(data, source).await,
931            Version::V2c => self.handle_v2c(data, source).await,
932            Version::V3 => self.handle_v3(data, source).await,
933        }
934    }
935}
936
937impl Clone for NotificationReceiver {
938    fn clone(&self) -> Self {
939        Self {
940            inner: Arc::clone(&self.inner),
941        }
942    }
943}
944
945#[cfg(test)]
946mod tests {
947    use super::*;
948    use crate::message::SecurityLevel;
949    use crate::oid;
950    use crate::pdu::GenericTrap;
951    use crate::v3::AuthProtocol;
952
953    #[test]
954    fn test_notification_trap_v1() {
955        let trap = TrapV1Pdu::new(
956            oid!(1, 3, 6, 1, 4, 1, 9999),
957            [192, 168, 1, 1],
958            GenericTrap::LinkDown,
959            0,
960            12345,
961            vec![],
962        );
963
964        let notification = Notification::TrapV1 {
965            community: Bytes::from_static(b"public"),
966            trap,
967        };
968
969        assert!(!notification.is_confirmed());
970        assert_eq!(notification.version(), Version::V1);
971        assert_eq!(notification.uptime(), 12345);
972        assert_eq!(notification.trap_oid().unwrap(), oids::link_down());
973    }
974
975    #[test]
976    fn test_notification_trap_v2c() {
977        let notification = Notification::TrapV2c {
978            community: Bytes::from_static(b"public"),
979            uptime: 54321,
980            trap_oid: oids::link_up(),
981            varbinds: vec![],
982            request_id: 1,
983        };
984
985        assert!(!notification.is_confirmed());
986        assert_eq!(notification.version(), Version::V2c);
987        assert_eq!(notification.uptime(), 54321);
988        assert_eq!(notification.trap_oid().unwrap(), oids::link_up());
989    }
990
991    #[test]
992    fn test_notification_inform() {
993        let notification = Notification::InformV2c {
994            community: Bytes::from_static(b"public"),
995            uptime: 11111,
996            trap_oid: oids::cold_start(),
997            varbinds: vec![],
998            request_id: 42,
999        };
1000
1001        assert!(notification.is_confirmed());
1002        assert_eq!(notification.version(), Version::V2c);
1003    }
1004
1005    #[test]
1006    fn test_notification_receiver_builder_default() {
1007        let builder = NotificationReceiverBuilder::new();
1008        assert_eq!(builder.bind_addr, "0.0.0.0:162");
1009        assert!(builder.usm_users.is_empty());
1010    }
1011
1012    #[test]
1013    fn test_notification_receiver_builder_with_user() {
1014        let builder = NotificationReceiverBuilder::new()
1015            .bind("0.0.0.0:1162")
1016            .usm_user("trapuser", |u| u.auth(AuthProtocol::Sha1, b"authpass"));
1017
1018        assert_eq!(builder.bind_addr, "0.0.0.0:1162");
1019        assert_eq!(builder.usm_users.len(), 1);
1020
1021        let user = builder
1022            .usm_users
1023            .get(&Bytes::from_static(b"trapuser"))
1024            .unwrap();
1025        assert_eq!(user.security_level(), SecurityLevel::AuthNoPriv);
1026    }
1027
1028    #[tokio::test]
1029    async fn test_v3_receiver_requires_authoritative_engine() {
1030        let result = NotificationReceiver::builder()
1031            .bind("127.0.0.1:0")
1032            .usm_user("user", |user| user)
1033            .build()
1034            .await;
1035
1036        let err = result.err().expect("expected build to fail");
1037        assert!(matches!(*err, Error::Config(_)));
1038    }
1039
1040    #[test]
1041    fn test_notification_v3_inform() {
1042        let notification = Notification::InformV3 {
1043            username: Bytes::from_static(b"testuser"),
1044            context_engine_id: Bytes::from_static(b"engine123"),
1045            context_name: Bytes::new(),
1046            security_level: SecurityLevel::AuthNoPriv,
1047            uptime: 99999,
1048            trap_oid: oids::warm_start(),
1049            varbinds: vec![],
1050            request_id: 100,
1051        };
1052
1053        assert!(notification.is_confirmed());
1054        assert_eq!(notification.version(), Version::V3);
1055        assert_eq!(notification.uptime(), 99999);
1056        assert_eq!(notification.trap_oid().unwrap(), oids::warm_start());
1057    }
1058
1059    #[test]
1060    fn test_notification_security_level_accessor() {
1061        let trap_v3 = Notification::TrapV3 {
1062            username: Bytes::from_static(b"testuser"),
1063            context_engine_id: Bytes::from_static(b"engine123"),
1064            context_name: Bytes::new(),
1065            security_level: SecurityLevel::AuthPriv,
1066            uptime: 1,
1067            trap_oid: oids::cold_start(),
1068            varbinds: vec![],
1069            request_id: 1,
1070        };
1071        assert_eq!(trap_v3.security_level(), Some(SecurityLevel::AuthPriv));
1072
1073        let inform_v3 = Notification::InformV3 {
1074            username: Bytes::from_static(b"testuser"),
1075            context_engine_id: Bytes::from_static(b"engine123"),
1076            context_name: Bytes::new(),
1077            security_level: SecurityLevel::NoAuthNoPriv,
1078            uptime: 1,
1079            trap_oid: oids::cold_start(),
1080            varbinds: vec![],
1081            request_id: 1,
1082        };
1083        assert_eq!(
1084            inform_v3.security_level(),
1085            Some(SecurityLevel::NoAuthNoPriv)
1086        );
1087
1088        let trap_v2c = Notification::TrapV2c {
1089            community: Bytes::from_static(b"public"),
1090            uptime: 1,
1091            trap_oid: oids::cold_start(),
1092            varbinds: vec![],
1093            request_id: 1,
1094        };
1095        assert_eq!(trap_v2c.security_level(), None);
1096    }
1097
1098    #[test]
1099    fn test_notification_trap_v1_enterprise_specific_oid() {
1100        let trap = TrapV1Pdu::new(
1101            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
1102            [192, 168, 1, 1],
1103            GenericTrap::EnterpriseSpecific,
1104            42,
1105            12345,
1106            vec![],
1107        );
1108
1109        let notification = Notification::TrapV1 {
1110            community: Bytes::from_static(b"public"),
1111            trap,
1112        };
1113
1114        assert_eq!(
1115            notification.trap_oid().unwrap(),
1116            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42)
1117        );
1118    }
1119
1120    #[test]
1121    fn test_compute_engine_boots_time_basic() {
1122        let (boots, time) = crate::v3::compute_engine_boots_time(1, 1000);
1123        assert_eq!(boots, 1);
1124        assert_eq!(time, 1000);
1125    }
1126
1127    #[test]
1128    fn test_compute_engine_boots_time_zero_elapsed() {
1129        let (boots, time) = crate::v3::compute_engine_boots_time(1, 0);
1130        assert_eq!(boots, 1);
1131        assert_eq!(time, 0);
1132    }
1133
1134    #[test]
1135    fn test_builder_authoritative_engine_default() {
1136        let builder = NotificationReceiverBuilder::new();
1137        assert!(builder.authoritative_engine.is_none());
1138    }
1139
1140    #[test]
1141    fn test_builder_authoritative_engine_custom() {
1142        let engine = AuthoritativeEngine::for_test(b"test-engine".to_vec(), 5);
1143        let builder = NotificationReceiverBuilder::new().authoritative_engine(engine);
1144        assert_eq!(builder.authoritative_engine.unwrap().engine_boots(), 5);
1145    }
1146
1147    /// Build a V3 notification message of the given PDU type with the given
1148    /// `engine_boots` and `engine_time` in the USM parameters. With
1149    /// `auth: Some((password, protocol))` the message is AuthNoPriv with a
1150    /// valid HMAC; with `None` it is noAuthNoPriv.
1151    fn build_v3_notification(
1152        pdu_type: crate::pdu::PduType,
1153        engine_id: &[u8],
1154        engine_boots: u32,
1155        engine_time: u32,
1156        username: &[u8],
1157        auth: Option<(&[u8], AuthProtocol)>,
1158    ) -> Bytes {
1159        build_v3_notification_with_max(
1160            pdu_type,
1161            engine_id,
1162            engine_boots,
1163            engine_time,
1164            username,
1165            auth,
1166            65507,
1167        )
1168    }
1169
1170    /// As [`build_v3_notification`], but with an explicit advertised
1171    /// `msg_max_size` in the message header.
1172    fn build_v3_notification_with_max(
1173        pdu_type: crate::pdu::PduType,
1174        engine_id: &[u8],
1175        engine_boots: u32,
1176        engine_time: u32,
1177        username: &[u8],
1178        auth: Option<(&[u8], AuthProtocol)>,
1179        msg_max_size: i32,
1180    ) -> Bytes {
1181        use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
1182        use crate::pdu::Pdu;
1183        use crate::v3::auth::authenticate_message;
1184        use crate::v3::{LocalizedKey, UsmSecurityParams};
1185        use crate::value::Value;
1186
1187        let auth_key = auth.map(|(password, protocol)| {
1188            LocalizedKey::from_password(protocol, password, engine_id).unwrap()
1189        });
1190
1191        // Build a notification PDU with sysUpTime.0 and snmpTrapOID.0
1192        let pdu = Pdu {
1193            pdu_type,
1194            request_id: 1,
1195            error_status: 0,
1196            error_index: 0,
1197            varbinds: vec![
1198                VarBind::new(oids::sys_uptime(), Value::TimeTicks(1000)),
1199                VarBind::new(
1200                    oids::snmp_trap_oid(),
1201                    Value::ObjectIdentifier(oids::cold_start()),
1202                ),
1203            ],
1204        };
1205
1206        let level = if auth_key.is_some() {
1207            SecurityLevel::AuthNoPriv
1208        } else {
1209            SecurityLevel::NoAuthNoPriv
1210        };
1211        // Informs are Confirmed Class and are sent with the reportableFlag
1212        // set; traps are Unconfirmed Class and are not (RFC 3412 Section 6.4).
1213        let reportable = pdu_type == crate::pdu::PduType::InformRequest;
1214        let global = MsgGlobalData::new(1, msg_max_size, MsgFlags::new(level, reportable));
1215
1216        let mut usm_params = UsmSecurityParams::new(
1217            Bytes::copy_from_slice(engine_id),
1218            engine_boots,
1219            engine_time,
1220            Bytes::copy_from_slice(username),
1221        );
1222        if let Some(key) = &auth_key {
1223            usm_params = usm_params.with_auth_placeholder(key.mac_len());
1224        }
1225
1226        let scoped = ScopedPdu::new(Bytes::copy_from_slice(engine_id), Bytes::new(), pdu);
1227        let msg = V3Message::new(global, usm_params.encode(), scoped);
1228        let mut msg_bytes = msg.encode().to_vec();
1229
1230        // Compute and insert HMAC
1231        if let Some(key) = &auth_key {
1232            let (auth_offset, auth_len) =
1233                UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
1234            authenticate_message(key, &mut msg_bytes, auth_offset, auth_len).unwrap();
1235        }
1236
1237        Bytes::from(msg_bytes)
1238    }
1239
1240    /// Build an authenticated V3 `InformRequest` message with the given
1241    /// `engine_boots` and `engine_time` in the USM parameters.
1242    fn build_authed_v3_inform(
1243        engine_id: &[u8],
1244        engine_boots: u32,
1245        engine_time: u32,
1246        username: &[u8],
1247        auth_password: &[u8],
1248        auth_protocol: AuthProtocol,
1249    ) -> Bytes {
1250        build_v3_notification(
1251            crate::pdu::PduType::InformRequest,
1252            engine_id,
1253            engine_boots,
1254            engine_time,
1255            username,
1256            Some((auth_password, auth_protocol)),
1257        )
1258    }
1259
1260    /// Build an authenticated V3 `SNMPv2-Trap` message with the given
1261    /// `engine_boots` and `engine_time` in the USM parameters.
1262    fn build_authed_v3_trap(engine_id: &[u8], engine_boots: u32, engine_time: u32) -> Bytes {
1263        build_v3_notification(
1264            crate::pdu::PduType::TrapV2,
1265            engine_id,
1266            engine_boots,
1267            engine_time,
1268            b"trapuser",
1269            Some((b"authpass12345678", AuthProtocol::Sha1)),
1270        )
1271    }
1272
1273    /// Build an unauthenticated (noAuthNoPriv) V3 `SNMPv2-Trap` message.
1274    fn build_noauth_v3_trap(engine_id: &[u8], username: &[u8]) -> Bytes {
1275        build_v3_notification(crate::pdu::PduType::TrapV2, engine_id, 0, 0, username, None)
1276    }
1277
1278    /// Build a receiver with its own engine ID and a `trapuser` configured,
1279    /// for tests exercising traps sent under a remote sender's engine ID.
1280    async fn remote_trap_receiver() -> NotificationReceiver {
1281        NotificationReceiver::builder()
1282            .bind("127.0.0.1:0")
1283            .engine_id(b"my-receiver-engine".to_vec())
1284            .engine_boots(1)
1285            .usm_user("trapuser", |u| {
1286                u.auth(AuthProtocol::Sha1, b"authpass12345678")
1287            })
1288            .build()
1289            .await
1290            .unwrap()
1291    }
1292
1293    /// For traps the SENDER is the authoritative engine (RFC 3414 Section
1294    /// 1.5.1): a real remote agent sends under its own engine ID with its
1295    /// own boots/time. The receiver must accept it without being configured
1296    /// with the sender's engine ID or clock, and the delivered notification
1297    /// reports the security level it was received at (RFC 3411 Section
1298    /// 3.4.3: securityLevel accompanies every message up to the
1299    /// application).
1300    #[tokio::test]
1301    async fn test_v3_trap_from_remote_sender_engine_accepted() {
1302        let receiver = remote_trap_receiver().await;
1303        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1304
1305        // Sender's own engine ID, arbitrary boots and time
1306        let msg = build_authed_v3_trap(b"remote-sender-engine", 7, 123_456);
1307
1308        let result = receiver.handle_v3(msg, source).await.unwrap();
1309        match result {
1310            Some(Notification::TrapV3 {
1311                username,
1312                security_level,
1313                ..
1314            }) => {
1315                assert_eq!(username.as_ref(), b"trapuser");
1316                assert_eq!(security_level, SecurityLevel::AuthNoPriv);
1317            }
1318            other => panic!("expected TrapV3, got {other:?}"),
1319        }
1320    }
1321
1322    /// A noAuthNoPriv V3 trap from a configured user is delivered (no
1323    /// per-user minimum is enforced here) but must be distinguishable from
1324    /// an authenticated one via its security level.
1325    #[tokio::test]
1326    async fn test_v3_noauth_trap_carries_security_level() {
1327        let receiver = remote_trap_receiver().await;
1328        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1329
1330        let msg = build_noauth_v3_trap(b"remote-sender-engine", b"trapuser");
1331        match receiver.handle_v3(msg, source).await.unwrap() {
1332            Some(Notification::TrapV3 {
1333                security_level,
1334                username,
1335                ..
1336            }) => {
1337                assert_eq!(security_level, SecurityLevel::NoAuthNoPriv);
1338                assert_eq!(username.as_ref(), b"trapuser");
1339            }
1340            other => panic!("expected TrapV3, got {other:?}"),
1341        }
1342    }
1343
1344    /// RFC 3414 Section 3.2 Step 4 is unconditional: the user must exist in
1345    /// the local configuration regardless of security level, so a
1346    /// noAuthNoPriv message from an unknown user is dropped and counted,
1347    /// not delivered.
1348    #[tokio::test]
1349    async fn test_v3_noauth_trap_unknown_user_rejected_and_counted() {
1350        let receiver = remote_trap_receiver().await;
1351        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1352
1353        let msg = build_noauth_v3_trap(b"remote-sender-engine", b"nosuchuser");
1354        let result = receiver.handle_v3(msg, source).await.unwrap();
1355        assert!(result.is_none(), "unknown user must not be delivered");
1356        assert_eq!(receiver.usm_unknown_usernames(), 1);
1357    }
1358
1359    /// Each remote engine gets independent timeliness state: traps from
1360    /// multiple senders with unrelated boots/time are all accepted.
1361    #[tokio::test]
1362    async fn test_v3_traps_from_multiple_remote_engines_accepted() {
1363        let receiver = remote_trap_receiver().await;
1364        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1365
1366        let msg_a = build_authed_v3_trap(b"sender-engine-a", 7, 123_456);
1367        let msg_b = build_authed_v3_trap(b"sender-engine-b", 2, 42);
1368
1369        assert!(
1370            receiver.handle_v3(msg_a, source).await.unwrap().is_some(),
1371            "trap from first remote engine should be accepted"
1372        );
1373        assert!(
1374            receiver.handle_v3(msg_b, source).await.unwrap().is_some(),
1375            "trap from second remote engine should be accepted"
1376        );
1377    }
1378
1379    /// The remote-engine table is bounded: once `MAX_REMOTE_ENGINES` entries
1380    /// exist, an authenticated trap under a new engine ID evicts an old entry
1381    /// rather than growing the map, so a credential holder cannot exhaust
1382    /// memory by fabricating engine IDs.
1383    #[tokio::test]
1384    async fn test_v3_remote_engines_table_bounded() {
1385        let receiver = remote_trap_receiver().await;
1386        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1387
1388        // Pre-fill the table to capacity with cheap dummy entries.
1389        {
1390            let mut engines = receiver.inner.remote_engines.lock().unwrap();
1391            for i in 0..MAX_REMOTE_ENGINES {
1392                let id = Bytes::from(format!("dummy-engine-{i}"));
1393                engines.insert(id.clone(), EngineState::new(id, 1, 1));
1394            }
1395            assert_eq!(engines.len(), MAX_REMOTE_ENGINES);
1396        }
1397
1398        // An authenticated trap under a not-yet-seen engine ID is accepted.
1399        let msg = build_authed_v3_trap(b"fresh-remote-engine", 7, 123_456);
1400        assert!(receiver.handle_v3(msg, source).await.unwrap().is_some());
1401
1402        // The table stayed at capacity (an old entry was evicted) and the new
1403        // engine is now tracked.
1404        let engines = receiver.inner.remote_engines.lock().unwrap();
1405        assert_eq!(engines.len(), MAX_REMOTE_ENGINES);
1406        assert!(engines.contains_key(&Bytes::from_static(b"fresh-remote-engine")));
1407    }
1408
1409    /// A replayed (stale) trap from a known remote engine is rejected:
1410    /// its engine time is more than 150 seconds behind the local notion
1411    /// established by an earlier authentic message (RFC 3414 Section 3.2
1412    /// Step 7b).
1413    #[tokio::test]
1414    async fn test_v3_trap_remote_engine_stale_time_rejected() {
1415        let receiver = remote_trap_receiver().await;
1416        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1417
1418        let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1419        assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1420
1421        // Same boots, time far behind the notion just established
1422        let stale = build_authed_v3_trap(b"remote-sender-engine", 7, 5_000);
1423        assert!(
1424            receiver.handle_v3(stale, source).await.is_err(),
1425            "stale engine time should be rejected as outside the time window"
1426        );
1427    }
1428
1429    /// A trap claiming an older boot cycle than previously seen is rejected.
1430    #[tokio::test]
1431    async fn test_v3_trap_remote_engine_old_boots_rejected() {
1432        let receiver = remote_trap_receiver().await;
1433        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1434
1435        let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1436        assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
1437
1438        let old_boots = build_authed_v3_trap(b"remote-sender-engine", 6, 99_999);
1439        assert!(
1440            receiver.handle_v3(old_boots, source).await.is_err(),
1441            "older boot cycle should be rejected"
1442        );
1443    }
1444
1445    /// A sender reboot (higher boots, low time) is tolerated and updates
1446    /// the local notion; the previous boot cycle is then rejected.
1447    #[tokio::test]
1448    async fn test_v3_trap_remote_engine_reboot_accepted() {
1449        let receiver = remote_trap_receiver().await;
1450        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1451
1452        let before = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
1453        assert!(receiver.handle_v3(before, source).await.unwrap().is_some());
1454
1455        let after_reboot = build_authed_v3_trap(b"remote-sender-engine", 8, 5);
1456        assert!(
1457            receiver
1458                .handle_v3(after_reboot, source)
1459                .await
1460                .unwrap()
1461                .is_some(),
1462            "trap after sender reboot should be accepted"
1463        );
1464
1465        let from_old_cycle = build_authed_v3_trap(b"remote-sender-engine", 7, 20_000);
1466        assert!(
1467            receiver.handle_v3(from_old_cycle, source).await.is_err(),
1468            "trap from superseded boot cycle should be rejected"
1469        );
1470    }
1471
1472    /// A trap with a bad HMAC from an unknown remote engine must not seed
1473    /// timeliness state or be accepted.
1474    #[tokio::test]
1475    async fn test_v3_trap_remote_engine_bad_auth_rejected() {
1476        let receiver = remote_trap_receiver().await;
1477        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1478
1479        let msg = build_v3_notification(
1480            crate::pdu::PduType::TrapV2,
1481            b"remote-sender-engine",
1482            7,
1483            123_456,
1484            b"trapuser",
1485            Some((b"wrong-password-1234", AuthProtocol::Sha1)),
1486        );
1487        assert!(
1488            receiver.handle_v3(msg, source).await.is_err(),
1489            "trap with wrong auth key should be rejected"
1490        );
1491
1492        // A correctly authenticated trap still works afterwards
1493        let good = build_authed_v3_trap(b"remote-sender-engine", 7, 123_456);
1494        assert!(receiver.handle_v3(good, source).await.unwrap().is_some());
1495    }
1496
1497    #[tokio::test]
1498    async fn test_v3_inform_outside_time_window_rejected() {
1499        let receiver = NotificationReceiver::builder()
1500            .bind("127.0.0.1:0")
1501            .engine_id(b"test-engine".to_vec())
1502            .engine_boots(1)
1503            .usm_user("informuser", |u| {
1504                u.auth(AuthProtocol::Sha1, b"authpass12345678")
1505            })
1506            .build()
1507            .await
1508            .unwrap();
1509
1510        let engine_id = b"test-engine";
1511        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1512
1513        // Engine time far in the future (5000 seconds, well beyond 150-second window)
1514        let msg = build_authed_v3_inform(
1515            engine_id,
1516            1,    // correct boots
1517            5000, // way outside time window (receiver started ~0 seconds ago)
1518            b"informuser",
1519            b"authpass12345678",
1520            AuthProtocol::Sha1,
1521        );
1522
1523        let result = receiver.handle_v3(msg, source).await;
1524        assert!(
1525            result.is_err(),
1526            "message with engine_time=5000 should be rejected (outside 150s window)"
1527        );
1528    }
1529
1530    #[tokio::test]
1531    async fn test_v3_inform_wrong_boots_rejected() {
1532        let receiver = NotificationReceiver::builder()
1533            .bind("127.0.0.1:0")
1534            .engine_id(b"test-engine".to_vec())
1535            .engine_boots(1)
1536            .usm_user("informuser", |u| {
1537                u.auth(AuthProtocol::Sha1, b"authpass12345678")
1538            })
1539            .build()
1540            .await
1541            .unwrap();
1542
1543        let engine_id = b"test-engine";
1544        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1545
1546        // Wrong engine boots (receiver has boots=1)
1547        let msg = build_authed_v3_inform(
1548            engine_id,
1549            2, // wrong boots
1550            0, // time is fine
1551            b"informuser",
1552            b"authpass12345678",
1553            AuthProtocol::Sha1,
1554        );
1555
1556        let result = receiver.handle_v3(msg, source).await;
1557        assert!(
1558            result.is_err(),
1559            "message with wrong engine_boots should be rejected"
1560        );
1561    }
1562
1563    #[tokio::test]
1564    async fn test_v3_inform_within_time_window_accepted() {
1565        let receiver = NotificationReceiver::builder()
1566            .bind("127.0.0.1:0")
1567            .engine_id(b"test-engine".to_vec())
1568            .engine_boots(1)
1569            .usm_user("informuser", |u| {
1570                u.auth(AuthProtocol::Sha1, b"authpass12345678")
1571            })
1572            .build()
1573            .await
1574            .unwrap();
1575
1576        let engine_id = b"test-engine";
1577        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1578
1579        // Engine time within the window (receiver started ~0 seconds ago, engine_time=0 is fine)
1580        let msg = build_authed_v3_inform(
1581            engine_id,
1582            1, // correct boots
1583            0, // within window
1584            b"informuser",
1585            b"authpass12345678",
1586            AuthProtocol::Sha1,
1587        );
1588
1589        let result = receiver.handle_v3(msg, source).await;
1590        // Should succeed (or at least not fail due to time window).
1591        // The Inform response send will fail since source is fake, but
1592        // the time window check itself should pass. The error if any
1593        // should be a network error from trying to send the response,
1594        // not an Auth error.
1595        match result {
1596            Ok(Some(_)) => {} // unexpected but ok (socket might succeed on loopback)
1597            Err(e) => {
1598                let err_str = format!("{e}");
1599                assert!(
1600                    !err_str.contains("Auth"),
1601                    "should not be an auth error for valid time window, got: {err_str}"
1602                );
1603            }
1604            Ok(None) => panic!("should not return None for a valid InformRequest"),
1605        }
1606    }
1607
1608    /// Build a V3 discovery request message (empty engine ID, noAuthNoPriv).
1609    fn build_v3_discovery_request(msg_id: i32, reportable: bool) -> Bytes {
1610        use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, V3Message};
1611        use crate::pdu::{Pdu, PduType};
1612        use crate::v3::UsmSecurityParams;
1613
1614        let pdu = Pdu {
1615            pdu_type: PduType::GetRequest,
1616            request_id: 0,
1617            error_status: 0,
1618            error_index: 0,
1619            varbinds: vec![],
1620        };
1621
1622        let global = MsgGlobalData::new(
1623            msg_id,
1624            65507,
1625            MsgFlags::new(SecurityLevel::NoAuthNoPriv, reportable),
1626        );
1627
1628        let usm_params = UsmSecurityParams::new(
1629            Bytes::new(), // empty engine ID = discovery
1630            0,
1631            0,
1632            Bytes::new(), // empty username
1633        );
1634
1635        let scoped = ScopedPdu::new(Bytes::new(), Bytes::new(), pdu);
1636        let msg = V3Message::new(global, usm_params.encode(), scoped);
1637        msg.encode()
1638    }
1639
1640    #[tokio::test]
1641    async fn test_v3_discovery_gets_response() {
1642        use crate::message::V3Message;
1643        use crate::v3::UsmSecurityParams;
1644        use crate::value::Value;
1645
1646        let receiver = NotificationReceiver::builder()
1647            .bind("127.0.0.1:0")
1648            .engine_id(b"test-discovery-engine".to_vec())
1649            .build()
1650            .await
1651            .unwrap();
1652
1653        // Bind a separate socket to receive the Report; handle_v3 is called
1654        // directly with this socket's address as source.
1655        let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1656        let client_addr = client.local_addr().unwrap();
1657
1658        let discovery_msg = build_v3_discovery_request(42, true);
1659        let result = receiver.handle_v3(discovery_msg, client_addr).await;
1660
1661        // Discovery should return Ok(None) - not a notification
1662        assert!(result.is_ok());
1663        assert!(result.unwrap().is_none());
1664
1665        // Counter should be incremented
1666        assert_eq!(receiver.usm_unknown_engine_ids(), 1);
1667
1668        // The Report must carry usmStatsUnknownEngineIDs with the counter
1669        // value and the receiver's engine ID (RFC 3414 Section 4).
1670        let mut buf = vec![0u8; 4096];
1671        let (len, _) = tokio::time::timeout(
1672            std::time::Duration::from_secs(1),
1673            client.recv_from(&mut buf),
1674        )
1675        .await
1676        .expect("expected a discovery Report")
1677        .unwrap();
1678
1679        let report = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
1680        assert_eq!(
1681            report.global_data.msg_flags.security_level,
1682            SecurityLevel::NoAuthNoPriv
1683        );
1684        let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
1685        assert_eq!(report_usm.engine_id.as_ref(), b"test-discovery-engine");
1686        let scoped = report.scoped_pdu().expect("report should be plaintext");
1687        assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
1688        assert_eq!(
1689            scoped.pdu.varbinds[0].oid,
1690            crate::v3::report_oids::unknown_engine_ids()
1691        );
1692        assert_eq!(scoped.pdu.varbinds[0].value, Value::Counter32(1));
1693    }
1694
1695    #[tokio::test]
1696    async fn test_v3_discovery_non_reportable_ignored() {
1697        let receiver = NotificationReceiver::builder()
1698            .bind("127.0.0.1:0")
1699            .engine_id(b"test-discovery-engine".to_vec())
1700            .build()
1701            .await
1702            .unwrap();
1703
1704        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1705        let discovery_msg = build_v3_discovery_request(42, false);
1706
1707        let result = receiver.handle_v3(discovery_msg, source).await;
1708
1709        // A non-reportable message with an unknown (empty) engine ID gets no
1710        // response, but the counter tracks the occurrence like every other
1711        // usmStats counter.
1712        assert!(result.is_ok());
1713        assert!(result.unwrap().is_none());
1714        assert_eq!(receiver.usm_unknown_engine_ids(), 1);
1715    }
1716
1717    /// RFC 3414 Section 1.5.1: the receiver of a Confirmed-class PDU is the
1718    /// authoritative engine, so an Inform must be localized to this receiver's
1719    /// local engine ID. An Inform localized to a foreign (e.g. the sender's)
1720    /// authoritative engine ID is rejected rather than acknowledged under that
1721    /// foreign engine.
1722    #[tokio::test]
1723    async fn test_v3_inform_under_remote_engine_id_rejected() {
1724        let receiver = NotificationReceiver::builder()
1725            .bind("127.0.0.1:0")
1726            .engine_id(b"my-receiver-engine".to_vec())
1727            .engine_boots(1)
1728            .usm_user("informuser", |u| {
1729                u.auth(AuthProtocol::Sha1, b"authpass12345678")
1730            })
1731            .build()
1732            .await
1733            .unwrap();
1734
1735        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1736
1737        // Build a message with a DIFFERENT (foreign) authoritative engine ID.
1738        let msg = build_authed_v3_inform(
1739            b"remote-engine-id",
1740            1,
1741            0,
1742            b"informuser",
1743            b"authpass12345678",
1744            AuthProtocol::Sha1,
1745        );
1746
1747        let result = receiver.handle_v3(msg, source).await.unwrap();
1748        assert!(
1749            result.is_none(),
1750            "inform under a foreign authoritative engine ID should be dropped, got {result:?}"
1751        );
1752    }
1753
1754    /// An Inform localized to the receiver's own local engine ID is accepted
1755    /// (RFC 3414 Section 1.5.1: the receiver is the authoritative engine for a
1756    /// Confirmed-class PDU).
1757    #[tokio::test]
1758    async fn test_v3_inform_under_local_engine_id_accepted() {
1759        let receiver = NotificationReceiver::builder()
1760            .bind("127.0.0.1:0")
1761            .engine_id(b"my-receiver-engine".to_vec())
1762            .engine_boots(1)
1763            .usm_user("informuser", |u| {
1764                u.auth(AuthProtocol::Sha1, b"authpass12345678")
1765            })
1766            .build()
1767            .await
1768            .unwrap();
1769
1770        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1771
1772        // Localized to the receiver's own engine ID.
1773        let msg = build_authed_v3_inform(
1774            b"my-receiver-engine",
1775            1,
1776            0,
1777            b"informuser",
1778            b"authpass12345678",
1779            AuthProtocol::Sha1,
1780        );
1781
1782        let result = receiver.handle_v3(msg, source).await.unwrap();
1783        assert!(
1784            matches!(result, Some(Notification::InformV3 { .. })),
1785            "inform under the local engine ID should be accepted, got {result:?}"
1786        );
1787    }
1788
1789    /// RFC 3412 Section 6.3: the inform acknowledgement advertises the
1790    /// receiver's own receive capacity, not the sender's echoed msgMaxSize.
1791    #[tokio::test]
1792    async fn test_v3_inform_ack_advertises_local_max_size() {
1793        let receiver = NotificationReceiver::builder()
1794            .bind("127.0.0.1:0")
1795            .engine_id(b"my-receiver-engine".to_vec())
1796            .engine_boots(1)
1797            .usm_user("informuser", |u| {
1798                u.auth(AuthProtocol::Sha1, b"authpass12345678")
1799            })
1800            .build()
1801            .await
1802            .unwrap();
1803
1804        let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1805        let client_addr = client.local_addr().unwrap();
1806
1807        // Inform advertises a small msgMaxSize (1400); the ack must NOT echo it.
1808        // The Inform is localized to the receiver's own engine ID, as required
1809        // for a Confirmed-class PDU (RFC 3414 Section 1.5.1).
1810        let msg = build_v3_notification_with_max(
1811            crate::pdu::PduType::InformRequest,
1812            b"my-receiver-engine",
1813            1,
1814            0,
1815            b"informuser",
1816            Some((b"authpass12345678", AuthProtocol::Sha1)),
1817            1400,
1818        );
1819
1820        let result = receiver.handle_v3(msg, client_addr).await.unwrap();
1821        assert!(
1822            matches!(result, Some(Notification::InformV3 { .. })),
1823            "inform should be accepted, got {result:?}"
1824        );
1825
1826        let mut buf = vec![0u8; 4096];
1827        let (len, _) = tokio::time::timeout(
1828            std::time::Duration::from_secs(1),
1829            client.recv_from(&mut buf),
1830        )
1831        .await
1832        .expect("expected the inform acknowledgement")
1833        .unwrap();
1834
1835        use crate::message::V3Message;
1836        let ack = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
1837        assert_eq!(
1838            ack.global_data.msg_max_size,
1839            crate::v3::DEFAULT_MSG_MAX_SIZE as i32,
1840            "ack must advertise the receiver's local receive capacity, not the sender's 1400"
1841        );
1842    }
1843
1844    /// RFC 3414 Section 3.1 Steps 1(a) and 6: an Inform Response is generated
1845    /// under the local authoritative engine and carries its current boots/time
1846    /// tuple rather than echoing the accepted request's tuple.
1847    #[tokio::test]
1848    async fn test_v3_inform_ack_uses_current_authoritative_time() {
1849        use crate::message::V3Message;
1850        use crate::v3::UsmSecurityParams;
1851
1852        let receiver = NotificationReceiver::builder()
1853            .bind("127.0.0.1:0")
1854            .engine_id(b"my-receiver-engine".to_vec())
1855            .engine_boots(7)
1856            .usm_user("informuser", |u| {
1857                u.auth(AuthProtocol::Sha1, b"authpass12345678")
1858            })
1859            .build()
1860            .await
1861            .unwrap();
1862
1863        let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1864        let client_addr = client.local_addr().unwrap();
1865
1866        // This time is inside the authoritative window but deliberately ahead
1867        // of the receiver's current clock, making an echo observable.
1868        let incoming_time = 149;
1869        let msg = build_authed_v3_inform(
1870            b"my-receiver-engine",
1871            7,
1872            incoming_time,
1873            b"informuser",
1874            b"authpass12345678",
1875            AuthProtocol::Sha1,
1876        );
1877
1878        let earliest = receiver.inner.authoritative_boots_time().unwrap();
1879        let result = receiver.handle_v3(msg, client_addr).await.unwrap();
1880        let latest = receiver.inner.authoritative_boots_time().unwrap();
1881        assert!(matches!(result, Some(Notification::InformV3 { .. })));
1882
1883        let mut buf = vec![0u8; 4096];
1884        let (len, _) = tokio::time::timeout(
1885            std::time::Duration::from_secs(1),
1886            client.recv_from(&mut buf),
1887        )
1888        .await
1889        .expect("expected the inform acknowledgement")
1890        .unwrap();
1891
1892        let ack = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
1893        let ack_usm = UsmSecurityParams::decode(ack.security_params).unwrap();
1894        let ack_pair = (ack_usm.engine_boots, ack_usm.engine_time);
1895
1896        assert_eq!(ack_usm.engine_id.as_ref(), receiver.engine_id());
1897        assert_ne!(ack_usm.engine_time, incoming_time);
1898        assert_eq!(ack_pair.0, 7);
1899        assert!(
1900            ack_pair.1 >= earliest.1 && ack_pair.1 <= latest.1,
1901            "ack pair {ack_pair:?} should come from one current elapsed-time sample between {earliest:?} and {latest:?}"
1902        );
1903    }
1904
1905    #[test]
1906    fn test_auto_generated_engine_id_non_empty() {
1907        let builder = NotificationReceiverBuilder::new();
1908        assert!(builder.authoritative_engine.is_none());
1909    }
1910
1911    #[tokio::test]
1912    async fn test_bind_generates_engine_id() {
1913        let first = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
1914        let second = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
1915
1916        crate::v3::validate_engine_id(first.engine_id()).unwrap();
1917        crate::v3::validate_engine_id(second.engine_id()).unwrap();
1918        // RFC 3411 format: starts with 0x80 enterprise indicator
1919        assert_eq!(first.engine_id()[0], 0x80);
1920        assert_ne!(first.engine_id(), second.engine_id());
1921    }
1922
1923    #[tokio::test]
1924    async fn test_builder_generates_engine_id() {
1925        let receiver = NotificationReceiver::builder()
1926            .bind("127.0.0.1:0")
1927            .build()
1928            .await
1929            .unwrap();
1930        assert!(!receiver.engine_id().is_empty());
1931        assert_eq!(receiver.engine_id()[0], 0x80);
1932    }
1933
1934    #[tokio::test]
1935    async fn test_builder_custom_engine_id() {
1936        let receiver = NotificationReceiver::builder()
1937            .bind("127.0.0.1:0")
1938            .engine_id(b"custom-engine".to_vec())
1939            .build()
1940            .await
1941            .unwrap();
1942        assert_eq!(receiver.engine_id(), b"custom-engine");
1943    }
1944
1945    #[tokio::test]
1946    async fn test_usm_counter_accessors_default_zero() {
1947        let receiver = remote_trap_receiver().await;
1948        assert_eq!(receiver.usm_unknown_engine_ids(), 0);
1949        assert_eq!(receiver.usm_unknown_usernames(), 0);
1950        assert_eq!(receiver.usm_wrong_digests(), 0);
1951        assert_eq!(receiver.usm_not_in_time_windows(), 0);
1952        assert_eq!(receiver.usm_unsupported_sec_levels(), 0);
1953        assert_eq!(receiver.usm_decryption_errors(), 0);
1954    }
1955
1956    /// RFC 3414 Section 3.2 Step 6: a failed HMAC increments
1957    /// usmStatsWrongDigests.
1958    #[tokio::test]
1959    async fn test_v3_trap_wrong_digest_increments_counter() {
1960        let receiver = remote_trap_receiver().await;
1961        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1962
1963        let msg = build_v3_notification(
1964            crate::pdu::PduType::TrapV2,
1965            b"remote-sender-engine",
1966            7,
1967            123_456,
1968            b"trapuser",
1969            Some((b"wrong-password-1234", AuthProtocol::Sha1)),
1970        );
1971        assert!(receiver.handle_v3(msg, source).await.is_err());
1972        assert_eq!(receiver.usm_wrong_digests(), 1);
1973    }
1974
1975    /// RFC 3414 Section 3.2 Step 4: an authenticated message for a user not
1976    /// in the local configuration increments usmStatsUnknownUserNames.
1977    #[tokio::test]
1978    async fn test_v3_trap_unknown_user_increments_counter() {
1979        let receiver = remote_trap_receiver().await;
1980        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
1981
1982        let msg = build_v3_notification(
1983            crate::pdu::PduType::TrapV2,
1984            b"remote-sender-engine",
1985            7,
1986            123_456,
1987            b"nosuchuser",
1988            Some((b"authpass12345678", AuthProtocol::Sha1)),
1989        );
1990        let result = receiver.handle_v3(msg, source).await.unwrap();
1991        assert!(result.is_none(), "unknown user must not be delivered");
1992        assert_eq!(receiver.usm_unknown_usernames(), 1);
1993        assert_eq!(receiver.usm_wrong_digests(), 0);
1994    }
1995
1996    /// RFC 3414 Section 3.2 Step 5: an authenticated message for a user
1997    /// configured without an auth key increments
1998    /// usmStatsUnsupportedSecLevels.
1999    #[tokio::test]
2000    async fn test_v3_trap_user_without_auth_key_increments_counter() {
2001        let receiver = NotificationReceiver::builder()
2002            .bind("127.0.0.1:0")
2003            .engine_id(b"my-receiver-engine".to_vec())
2004            .usm_user("plainuser", |u| u)
2005            .build()
2006            .await
2007            .unwrap();
2008        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2009
2010        let msg = build_v3_notification(
2011            crate::pdu::PduType::TrapV2,
2012            b"remote-sender-engine",
2013            7,
2014            123_456,
2015            b"plainuser",
2016            Some((b"authpass12345678", AuthProtocol::Sha1)),
2017        );
2018        let result = receiver.handle_v3(msg, source).await.unwrap();
2019        assert!(result.is_none());
2020        assert_eq!(receiver.usm_unsupported_sec_levels(), 1);
2021        assert_eq!(receiver.usm_unknown_usernames(), 0);
2022    }
2023
2024    /// RFC 3414 Section 3.2 Step 7a: an inform under the receiver's engine ID
2025    /// outside the time window increments usmStatsNotInTimeWindows.
2026    #[tokio::test]
2027    async fn test_v3_inform_time_window_failure_increments_counter() {
2028        let receiver = NotificationReceiver::builder()
2029            .bind("127.0.0.1:0")
2030            .engine_id(b"test-engine".to_vec())
2031            .engine_boots(1)
2032            .usm_user("informuser", |u| {
2033                u.auth(AuthProtocol::Sha1, b"authpass12345678")
2034            })
2035            .build()
2036            .await
2037            .unwrap();
2038        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2039
2040        let msg = build_authed_v3_inform(
2041            b"test-engine",
2042            1,
2043            5000,
2044            b"informuser",
2045            b"authpass12345678",
2046            AuthProtocol::Sha1,
2047        );
2048        assert!(receiver.handle_v3(msg, source).await.is_err());
2049        assert_eq!(receiver.usm_not_in_time_windows(), 1);
2050    }
2051
2052    /// RFC 3414 Section 3.2 Step 7b: when the sender is the authoritative
2053    /// engine, a timeliness failure is a bare error indication.
2054    /// usmStatsNotInTimeWindows and its Report belong to the authoritative
2055    /// case (Step 7a) only, matching net-snmp's
2056    /// usm_check_and_update_timeliness.
2057    #[tokio::test]
2058    async fn test_v3_trap_remote_stale_not_counted() {
2059        let receiver = remote_trap_receiver().await;
2060        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2061
2062        let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
2063        assert!(receiver.handle_v3(fresh, source).await.unwrap().is_some());
2064
2065        let stale = build_authed_v3_trap(b"remote-sender-engine", 7, 5_000);
2066        assert!(receiver.handle_v3(stale, source).await.is_err());
2067        assert_eq!(receiver.usm_not_in_time_windows(), 0);
2068    }
2069
2070    /// A stale inform under a remote sender's engine ID (Step 7b) gets no
2071    /// notInTimeWindows Report even though its reportableFlag is set: the
2072    /// receiver is not authoritative for that engine's clock. Timeliness
2073    /// (Step 7b) is evaluated in the shared USM core before the Inform is
2074    /// rejected as foreign-engine (RFC 3414 Section 1.5.1), so a stale
2075    /// remote-engine inform still fails at Step 7b rather than being
2076    /// acknowledged.
2077    #[tokio::test]
2078    async fn test_v3_inform_remote_stale_gets_no_report() {
2079        let receiver = remote_trap_receiver().await;
2080
2081        let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2082        let client_addr = client.local_addr().unwrap();
2083
2084        // Seed the remote engine's timeliness state with a fresh trap under the
2085        // same engine ID (the per-engine state is keyed by engine ID and shared
2086        // with informs).
2087        let fresh = build_authed_v3_trap(b"remote-sender-engine", 7, 10_000);
2088        assert!(
2089            receiver
2090                .handle_v3(fresh, client_addr)
2091                .await
2092                .unwrap()
2093                .is_some()
2094        );
2095
2096        let mut buf = vec![0u8; 4096];
2097
2098        let stale = build_v3_notification(
2099            crate::pdu::PduType::InformRequest,
2100            b"remote-sender-engine",
2101            7,
2102            5_000,
2103            b"trapuser",
2104            Some((b"authpass12345678", AuthProtocol::Sha1)),
2105        );
2106        assert!(receiver.handle_v3(stale, client_addr).await.is_err());
2107        assert_eq!(receiver.usm_not_in_time_windows(), 0);
2108
2109        let result = tokio::time::timeout(
2110            std::time::Duration::from_millis(200),
2111            client.recv_from(&mut buf),
2112        )
2113        .await;
2114        assert!(
2115            result.is_err(),
2116            "no Report may be sent for a Step 7b timeliness failure"
2117        );
2118    }
2119
2120    /// Build an authPriv V3 trap for the given username, HMAC'd with the
2121    /// given password, with undecryptable privacy parameters (wrong salt
2122    /// length) and garbage ciphertext.
2123    fn build_v3_trap_bad_ciphertext(
2124        engine_id: &[u8],
2125        username: &[u8],
2126        auth_password: &[u8],
2127    ) -> Bytes {
2128        use crate::message::{MsgFlags, MsgGlobalData, V3Message};
2129        use crate::v3::auth::authenticate_message;
2130        use crate::v3::{LocalizedKey, UsmSecurityParams};
2131
2132        let auth_key =
2133            LocalizedKey::from_password(AuthProtocol::Sha1, auth_password, engine_id).unwrap();
2134
2135        let global = MsgGlobalData::new(1, 65507, MsgFlags::new(SecurityLevel::AuthPriv, false));
2136        let usm_params = UsmSecurityParams::new(
2137            Bytes::copy_from_slice(engine_id),
2138            7,
2139            123_456,
2140            Bytes::copy_from_slice(username),
2141        )
2142        .with_auth_placeholder(auth_key.mac_len())
2143        .with_priv_params(Bytes::from_static(b"bad"));
2144
2145        let msg = V3Message::new_encrypted(
2146            global,
2147            usm_params.encode(),
2148            Bytes::from_static(b"not-a-valid-ciphertext"),
2149        );
2150        let mut msg_bytes = msg.encode().to_vec();
2151        let (auth_offset, auth_len) =
2152            UsmSecurityParams::find_auth_params_offset(&msg_bytes).unwrap();
2153        authenticate_message(&auth_key, &mut msg_bytes, auth_offset, auth_len).unwrap();
2154        Bytes::from(msg_bytes)
2155    }
2156
2157    /// RFC 3414 Section 3.2 Step 8: a decryption failure increments
2158    /// usmStatsDecryptionErrors.
2159    #[tokio::test]
2160    async fn test_v3_decryption_error_increments_counter() {
2161        let receiver = NotificationReceiver::builder()
2162            .bind("127.0.0.1:0")
2163            .engine_id(b"my-receiver-engine".to_vec())
2164            .usm_user("privuser", |u| {
2165                u.auth_priv(
2166                    AuthProtocol::Sha1,
2167                    b"authpass12345678",
2168                    crate::v3::PrivProtocol::Aes128,
2169                    b"privpass12345678",
2170                )
2171            })
2172            .build()
2173            .await
2174            .unwrap();
2175        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2176
2177        let msg =
2178            build_v3_trap_bad_ciphertext(b"remote-sender-engine", b"privuser", b"authpass12345678");
2179        assert!(receiver.handle_v3(msg, source).await.is_err());
2180        assert_eq!(receiver.usm_decryption_errors(), 1);
2181    }
2182
2183    /// RFC 3414 Section 3.2 Step 5 precedes Step 6: an authPriv message for
2184    /// a user configured without privacy increments
2185    /// usmStatsUnsupportedSecLevels even when its HMAC is invalid, not
2186    /// usmStatsWrongDigests.
2187    #[tokio::test]
2188    async fn test_v3_authpriv_for_auth_only_user_counts_unsupported_sec_level() {
2189        let receiver = remote_trap_receiver().await;
2190        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2191
2192        let msg = build_v3_trap_bad_ciphertext(
2193            b"remote-sender-engine",
2194            b"trapuser",
2195            b"wrong-password-1234",
2196        );
2197        let result = receiver.handle_v3(msg, source).await.unwrap();
2198        assert!(result.is_none());
2199        assert_eq!(receiver.usm_unsupported_sec_levels(), 1);
2200        assert_eq!(receiver.usm_wrong_digests(), 0);
2201    }
2202
2203    /// A USM-failed inform (Confirmed Class, reportableFlag set) gets a
2204    /// Report back (RFC 3412 Section 7.1 Step 3). The notInTimeWindows
2205    /// report carries the receiver's engine ID/boots/time for time
2206    /// resynchronization and is authenticated at authNoPriv
2207    /// (RFC 3414 Section 3.2 Step 7).
2208    #[tokio::test]
2209    async fn test_v3_failed_inform_gets_authenticated_time_window_report() {
2210        use crate::message::V3Message;
2211        use crate::v3::auth::verify_message;
2212        use crate::v3::{LocalizedKey, UsmSecurityParams};
2213
2214        let receiver = NotificationReceiver::builder()
2215            .bind("127.0.0.1:0")
2216            .engine_id(b"test-engine".to_vec())
2217            .engine_boots(1)
2218            .usm_user("informuser", |u| {
2219                u.auth(AuthProtocol::Sha1, b"authpass12345678")
2220            })
2221            .build()
2222            .await
2223            .unwrap();
2224
2225        let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2226        let client_addr = client.local_addr().unwrap();
2227
2228        let msg = build_authed_v3_inform(
2229            b"test-engine",
2230            1,
2231            5000, // outside the 150s window
2232            b"informuser",
2233            b"authpass12345678",
2234            AuthProtocol::Sha1,
2235        );
2236        assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2237
2238        let mut buf = vec![0u8; 4096];
2239        let (len, _) = tokio::time::timeout(
2240            std::time::Duration::from_secs(1),
2241            client.recv_from(&mut buf),
2242        )
2243        .await
2244        .expect("expected a Report in response to the failed inform")
2245        .unwrap();
2246        let report_bytes = Bytes::copy_from_slice(&buf[..len]);
2247
2248        let report = V3Message::decode(report_bytes.clone()).unwrap();
2249        assert_eq!(
2250            report.global_data.msg_flags.security_level,
2251            SecurityLevel::AuthNoPriv,
2252            "notInTimeWindows report must be authenticated (authNoPriv)"
2253        );
2254        assert!(!report.global_data.msg_flags.reportable);
2255
2256        let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
2257        assert_eq!(report_usm.engine_id.as_ref(), b"test-engine");
2258
2259        // The HMAC must verify with the user's key localized to the
2260        // receiver's engine ID.
2261        let key =
2262            LocalizedKey::from_password(AuthProtocol::Sha1, b"authpass12345678", b"test-engine")
2263                .unwrap();
2264        let (auth_offset, auth_len) =
2265            UsmSecurityParams::find_auth_params_offset(&report_bytes).unwrap();
2266        assert!(verify_message(&key, &report_bytes, auth_offset, auth_len).unwrap());
2267
2268        let scoped = report.scoped_pdu().expect("report should be plaintext");
2269        assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
2270        assert_eq!(
2271            scoped.pdu.varbinds[0].oid,
2272            crate::v3::report_oids::not_in_time_windows()
2273        );
2274    }
2275
2276    /// RFC 3414 Section 3.2 Step 7a lists latched engine boots as a Time
2277    /// Window failure and mandates the report be authenticated at
2278    /// authNoPriv, like the other notInTimeWindows reports.
2279    #[tokio::test]
2280    async fn test_v3_latched_boots_report_is_authenticated() {
2281        use crate::message::V3Message;
2282        use crate::v3::MAX_ENGINE_TIME;
2283        use crate::v3::auth::verify_message;
2284        use crate::v3::{LocalizedKey, UsmSecurityParams};
2285
2286        let receiver = NotificationReceiver::builder()
2287            .bind("127.0.0.1:0")
2288            .engine_id(b"test-engine".to_vec())
2289            .engine_boots(MAX_ENGINE_TIME)
2290            .usm_user("informuser", |u| {
2291                u.auth(AuthProtocol::Sha1, b"authpass12345678")
2292            })
2293            .build()
2294            .await
2295            .unwrap();
2296
2297        let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2298        let client_addr = client.local_addr().unwrap();
2299
2300        let msg = build_authed_v3_inform(
2301            b"test-engine",
2302            MAX_ENGINE_TIME,
2303            0,
2304            b"informuser",
2305            b"authpass12345678",
2306            AuthProtocol::Sha1,
2307        );
2308        assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2309        assert_eq!(receiver.usm_not_in_time_windows(), 1);
2310
2311        let mut buf = vec![0u8; 4096];
2312        let (len, _) = tokio::time::timeout(
2313            std::time::Duration::from_secs(1),
2314            client.recv_from(&mut buf),
2315        )
2316        .await
2317        .expect("expected a Report in response to the failed inform")
2318        .unwrap();
2319        let report_bytes = Bytes::copy_from_slice(&buf[..len]);
2320
2321        let report = V3Message::decode(report_bytes.clone()).unwrap();
2322        assert_eq!(
2323            report.global_data.msg_flags.security_level,
2324            SecurityLevel::AuthNoPriv,
2325            "notInTimeWindows report must be authenticated (authNoPriv)"
2326        );
2327        let key =
2328            LocalizedKey::from_password(AuthProtocol::Sha1, b"authpass12345678", b"test-engine")
2329                .unwrap();
2330        let (auth_offset, auth_len) =
2331            UsmSecurityParams::find_auth_params_offset(&report_bytes).unwrap();
2332        assert!(verify_message(&key, &report_bytes, auth_offset, auth_len).unwrap());
2333
2334        let scoped = report.scoped_pdu().expect("report should be plaintext");
2335        assert_eq!(
2336            scoped.pdu.varbinds[0].oid,
2337            crate::v3::report_oids::not_in_time_windows()
2338        );
2339    }
2340
2341    /// A USM-failed inform for an unknown user gets an unauthenticated
2342    /// Report (no key exists to authenticate it with).
2343    #[tokio::test]
2344    async fn test_v3_failed_inform_unknown_user_gets_noauth_report() {
2345        use crate::message::V3Message;
2346        use crate::v3::UsmSecurityParams;
2347
2348        let receiver = NotificationReceiver::builder()
2349            .bind("127.0.0.1:0")
2350            .engine_id(b"test-engine".to_vec())
2351            .engine_boots(1)
2352            .usm_user("informuser", |u| {
2353                u.auth(AuthProtocol::Sha1, b"authpass12345678")
2354            })
2355            .build()
2356            .await
2357            .unwrap();
2358
2359        let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2360        let client_addr = client.local_addr().unwrap();
2361
2362        let msg = build_authed_v3_inform(
2363            b"test-engine",
2364            1,
2365            0,
2366            b"nosuchuser",
2367            b"authpass12345678",
2368            AuthProtocol::Sha1,
2369        );
2370        let result = receiver.handle_v3(msg, client_addr).await.unwrap();
2371        assert!(result.is_none());
2372        assert_eq!(receiver.usm_unknown_usernames(), 1);
2373
2374        let mut buf = vec![0u8; 4096];
2375        let (len, _) = tokio::time::timeout(
2376            std::time::Duration::from_secs(1),
2377            client.recv_from(&mut buf),
2378        )
2379        .await
2380        .expect("expected a Report in response to the failed inform")
2381        .unwrap();
2382
2383        let report = V3Message::decode(Bytes::copy_from_slice(&buf[..len])).unwrap();
2384        assert_eq!(
2385            report.global_data.msg_flags.security_level,
2386            SecurityLevel::NoAuthNoPriv
2387        );
2388        let report_usm = UsmSecurityParams::decode(report.security_params.clone()).unwrap();
2389        assert_eq!(report_usm.engine_id.as_ref(), b"test-engine");
2390        let scoped = report.scoped_pdu().expect("report should be plaintext");
2391        assert_eq!(scoped.pdu.pdu_type, crate::pdu::PduType::Report);
2392        assert_eq!(
2393            scoped.pdu.varbinds[0].oid,
2394            crate::v3::report_oids::unknown_user_names()
2395        );
2396    }
2397
2398    /// A USM-failed trap must NOT get a Report: traps are Unconfirmed Class
2399    /// and carry reportableFlag=0 (RFC 3412 Sections 6.4 and 7.1 Step 3).
2400    #[tokio::test]
2401    async fn test_v3_failed_trap_gets_no_report() {
2402        let receiver = remote_trap_receiver().await;
2403
2404        let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2405        let client_addr = client.local_addr().unwrap();
2406
2407        let msg = build_v3_notification(
2408            crate::pdu::PduType::TrapV2,
2409            b"remote-sender-engine",
2410            7,
2411            123_456,
2412            b"trapuser",
2413            Some((b"wrong-password-1234", AuthProtocol::Sha1)),
2414        );
2415        assert!(receiver.handle_v3(msg, client_addr).await.is_err());
2416        assert_eq!(receiver.usm_wrong_digests(), 1);
2417
2418        let mut buf = vec![0u8; 4096];
2419        let result = tokio::time::timeout(
2420            std::time::Duration::from_millis(200),
2421            client.recv_from(&mut buf),
2422        )
2423        .await;
2424        assert!(result.is_err(), "no Report may be sent for a failed trap");
2425    }
2426
2427    #[test]
2428    fn test_community_allowed() {
2429        // Empty allowlist accepts any community (opt-in filtering).
2430        assert!(community_allowed(&[], b"public"));
2431        assert!(community_allowed(&[], b""));
2432
2433        let configured = vec![b"public".to_vec(), b"monitor".to_vec()];
2434        assert!(community_allowed(&configured, b"public"));
2435        assert!(community_allowed(&configured, b"monitor"));
2436        // Non-matching, prefix, and length-mismatch are all rejected.
2437        assert!(!community_allowed(&configured, b"private"));
2438        assert!(!community_allowed(&configured, b"pub"));
2439        assert!(!community_allowed(&configured, b"publicx"));
2440        assert!(!community_allowed(&configured, b""));
2441    }
2442
2443    fn build_v2c_trap(community: &[u8]) -> Bytes {
2444        use crate::message::CommunityMessage;
2445        use crate::pdu::Pdu;
2446        let pdu = Pdu::trap_v2(1, 100, &oids::cold_start(), vec![]);
2447        CommunityMessage::v2c(Bytes::copy_from_slice(community), pdu).encode()
2448    }
2449
2450    fn build_v2c_inform(community: &[u8]) -> Bytes {
2451        use crate::message::CommunityMessage;
2452        use crate::pdu::Pdu;
2453        let pdu = Pdu::inform_request(1, 100, &oids::cold_start(), vec![]);
2454        CommunityMessage::v2c(Bytes::copy_from_slice(community), pdu).encode()
2455    }
2456
2457    fn build_v1_trap(community: &[u8]) -> Bytes {
2458        use crate::message::CommunityMessage;
2459        use crate::pdu::GenericTrap;
2460        let trap = TrapV1Pdu::new(
2461            oid!(1, 3, 6, 1, 4, 1, 9999),
2462            [192, 168, 1, 1],
2463            GenericTrap::ColdStart,
2464            0,
2465            12345,
2466            vec![],
2467        );
2468        CommunityMessage::v1_trap(Bytes::copy_from_slice(community), trap).encode()
2469    }
2470
2471    #[tokio::test]
2472    async fn test_v2c_trap_matching_community_accepted() {
2473        let receiver = NotificationReceiver::builder()
2474            .bind("127.0.0.1:0")
2475            .community(b"public")
2476            .build()
2477            .await
2478            .unwrap();
2479        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2480
2481        let result = receiver
2482            .handle_v2c(build_v2c_trap(b"public"), source)
2483            .await
2484            .unwrap();
2485        assert!(matches!(result, Some(Notification::TrapV2c { .. })));
2486    }
2487
2488    #[tokio::test]
2489    async fn test_v2c_trap_wrong_community_dropped() {
2490        let receiver = NotificationReceiver::builder()
2491            .bind("127.0.0.1:0")
2492            .community(b"public")
2493            .build()
2494            .await
2495            .unwrap();
2496        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2497
2498        let result = receiver
2499            .handle_v2c(build_v2c_trap(b"private"), source)
2500            .await
2501            .unwrap();
2502        assert!(result.is_none());
2503    }
2504
2505    #[tokio::test]
2506    async fn test_v2c_trap_no_allowlist_accepts_any_community() {
2507        let receiver = NotificationReceiver::bind("127.0.0.1:0").await.unwrap();
2508        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2509
2510        let result = receiver
2511            .handle_v2c(build_v2c_trap(b"anything"), source)
2512            .await
2513            .unwrap();
2514        assert!(matches!(result, Some(Notification::TrapV2c { .. })));
2515    }
2516
2517    #[tokio::test]
2518    async fn test_v1_trap_wrong_community_dropped() {
2519        let receiver = NotificationReceiver::builder()
2520            .bind("127.0.0.1:0")
2521            .community(b"public")
2522            .build()
2523            .await
2524            .unwrap();
2525        let source: SocketAddr = "127.0.0.1:9999".parse().unwrap();
2526
2527        assert!(
2528            receiver
2529                .handle_v1(build_v1_trap(b"private"), source)
2530                .await
2531                .unwrap()
2532                .is_none()
2533        );
2534        assert!(matches!(
2535            receiver
2536                .handle_v1(build_v1_trap(b"public"), source)
2537                .await
2538                .unwrap(),
2539            Some(Notification::TrapV1 { .. })
2540        ));
2541    }
2542
2543    /// An inform rejected by the community filter is dropped before the ack is
2544    /// built, so no Response datagram is sent to the source.
2545    #[tokio::test]
2546    async fn test_v2c_inform_wrong_community_dropped_without_ack() {
2547        let receiver = NotificationReceiver::builder()
2548            .bind("127.0.0.1:0")
2549            .community(b"public")
2550            .build()
2551            .await
2552            .unwrap();
2553
2554        let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2555        let client_addr = client.local_addr().unwrap();
2556
2557        let result = receiver
2558            .handle_v2c(build_v2c_inform(b"private"), client_addr)
2559            .await
2560            .unwrap();
2561        assert!(result.is_none());
2562
2563        let mut buf = vec![0u8; 4096];
2564        let recv = tokio::time::timeout(
2565            std::time::Duration::from_millis(200),
2566            client.recv_from(&mut buf),
2567        )
2568        .await;
2569        assert!(recv.is_err(), "a filtered inform must not be acknowledged");
2570    }
2571
2572    /// A matching inform is still acknowledged (the filter does not suppress
2573    /// valid acks).
2574    #[tokio::test]
2575    async fn test_v2c_inform_matching_community_acked() {
2576        let receiver = NotificationReceiver::builder()
2577            .bind("127.0.0.1:0")
2578            .community(b"public")
2579            .build()
2580            .await
2581            .unwrap();
2582
2583        let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
2584        let client_addr = client.local_addr().unwrap();
2585
2586        let result = receiver
2587            .handle_v2c(build_v2c_inform(b"public"), client_addr)
2588            .await
2589            .unwrap();
2590        assert!(matches!(result, Some(Notification::InformV2c { .. })));
2591
2592        let mut buf = vec![0u8; 4096];
2593        let (len, _) = tokio::time::timeout(
2594            std::time::Duration::from_secs(1),
2595            client.recv_from(&mut buf),
2596        )
2597        .await
2598        .expect("a matching inform must be acknowledged")
2599        .unwrap();
2600        assert!(len > 0);
2601    }
2602}