use std::net::SocketAddr;
use std::sync::atomic::{AtomicU32, Ordering};
use bytes::Bytes;
use crate::ber::{Decoder, tag};
use crate::error::internal::{AuthErrorKind, CryptoErrorKind, DecodeErrorKind, EncodeErrorKind};
use crate::error::{Error, Result};
use crate::message::{
CommunityMessage, MsgFlags, MsgGlobalData, ScopedPdu, SecurityLevel, V3Message, V3MessageData,
};
use crate::oid::Oid;
use crate::pdu::{Pdu, PduType, TrapV1Pdu};
use crate::v3::auth::{authenticate_message, verify_message};
use crate::v3::{EngineState, LocalizedKey, UsmSecurityParams, in_authoritative_time_window};
use crate::value::Value;
use crate::varbind::VarBind;
use super::types::DerivedKeys;
use super::varbind::extract_notification_varbinds;
use super::{Notification, ReceiverInner};
use crate::v3::compute_engine_boots_time;
#[derive(Clone, Copy)]
enum UsmFailure {
UnknownEngineIds,
UnknownUserNames,
WrongDigests,
NotInTimeWindows,
UnsupportedSecLevels,
DecryptionErrors,
}
impl UsmFailure {
fn counter(self, inner: &ReceiverInner) -> &AtomicU32 {
match self {
Self::UnknownEngineIds => &inner.usm_unknown_engine_ids,
Self::UnknownUserNames => &inner.usm_unknown_usernames,
Self::WrongDigests => &inner.usm_wrong_digests,
Self::NotInTimeWindows => &inner.usm_not_in_time_windows,
Self::UnsupportedSecLevels => &inner.usm_unsupported_sec_levels,
Self::DecryptionErrors => &inner.usm_decryption_errors,
}
}
fn report_oid(self) -> Oid {
match self {
Self::UnknownEngineIds => crate::v3::report_oids::unknown_engine_ids(),
Self::UnknownUserNames => crate::v3::report_oids::unknown_user_names(),
Self::WrongDigests => crate::v3::report_oids::wrong_digests(),
Self::NotInTimeWindows => crate::v3::report_oids::not_in_time_windows(),
Self::UnsupportedSecLevels => crate::v3::report_oids::unsupported_sec_levels(),
Self::DecryptionErrors => crate::v3::report_oids::decryption_errors(),
}
}
}
impl super::NotificationReceiver {
pub(super) async fn handle_v1(
&self,
data: Bytes,
source: SocketAddr,
) -> Result<Option<Notification>> {
let mut decoder = Decoder::with_target(data, source);
let mut seq = decoder.read_sequence()?;
let _version = seq.read_integer()?;
let community = seq.read_octet_string()?;
if !super::community_allowed(&self.inner.communities, &community) {
tracing::debug!(target: "async_snmp::notification", { snmp.source = %source }, "dropped v1 notification with unaccepted community");
return Ok(None);
}
let pdu_tag = seq.peek_tag().ok_or_else(|| {
tracing::debug!(target: "async_snmp::notification", { source = %source, kind = %DecodeErrorKind::TruncatedData }, "truncated notification data");
Error::MalformedResponse { target: source }.boxed()
})?;
if pdu_tag == tag::pdu::TRAP_V1 {
let trap = TrapV1Pdu::decode(&mut seq)?;
Ok(Some(Notification::TrapV1 { community, trap }))
} else {
Ok(None)
}
}
pub(super) async fn handle_v2c(
&self,
data: Bytes,
source: SocketAddr,
) -> Result<Option<Notification>> {
let msg = CommunityMessage::decode(data)?;
if !super::community_allowed(&self.inner.communities, &msg.community) {
tracing::debug!(target: "async_snmp::notification", { snmp.source = %source }, "dropped v2c notification with unaccepted community");
return Ok(None);
}
let Some(pdu) = msg.pdu.standard() else {
return Ok(None);
};
match pdu.pdu_type {
PduType::TrapV2 => {
let (uptime, trap_oid, varbinds) = extract_notification_varbinds(pdu)?;
Ok(Some(Notification::TrapV2c {
community: msg.community,
uptime,
trap_oid,
varbinds,
request_id: pdu.request_id,
}))
}
PduType::InformRequest => {
let (uptime, trap_oid, varbinds) = extract_notification_varbinds(pdu)?;
let request_id = pdu.request_id;
let response = pdu.to_response();
let response_msg = CommunityMessage::v2c(msg.community.clone(), response);
let response_bytes = response_msg.encode();
self.inner
.socket
.send_to(&response_bytes, source)
.await
.map_err(|e| Error::Network {
target: source,
source: e,
})?;
tracing::debug!(target: "async_snmp::notification", { snmp.source = %source, snmp.request_id = request_id }, "sent Inform response");
Ok(Some(Notification::InformV2c {
community: msg.community,
uptime,
trap_oid,
varbinds,
request_id,
}))
}
_ => Ok(None), }
}
pub(super) async fn handle_v3(
&self,
data: Bytes,
source: SocketAddr,
) -> Result<Option<Notification>> {
let msg = V3Message::decode(data.clone())?;
let security_level = msg.global_data.msg_flags.security_level;
let usm_params = UsmSecurityParams::decode(msg.security_params.clone())?;
if usm_params.engine_id.is_empty() {
return self.handle_v3_discovery(&msg, &usm_params, source).await;
}
let username = usm_params.username.clone();
let engine_id = usm_params.engine_id.clone();
let Some(user_config) = self.inner.usm_users.get(&username) else {
tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, snmp.username = %String::from_utf8_lossy(&username) }, "V3 message for unknown user");
self.send_usm_report(
&msg,
&usm_params,
UsmFailure::UnknownUserNames,
None,
source,
)
.await;
return Ok(None);
};
let derived_keys = user_config
.derive_keys(&engine_id)
.map_err(|e| Error::Config(e.to_string().into()).boxed())?;
if security_level == SecurityLevel::AuthPriv && derived_keys.priv_key.is_none() {
tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, snmp.username = %String::from_utf8_lossy(&username) }, "received encrypted V3 message but no privacy key configured for user");
self.send_usm_report(
&msg,
&usm_params,
UsmFailure::UnsupportedSecLevels,
None,
source,
)
.await;
return Ok(None);
}
if security_level == SecurityLevel::AuthNoPriv || security_level == SecurityLevel::AuthPriv
{
match derived_keys.auth_key.as_ref() {
Some(auth_key) => {
let (auth_offset, auth_len) = UsmSecurityParams::find_auth_params_offset(&data)
.ok_or_else(|| {
tracing::debug!(target: "async_snmp::notification", { source = %source, kind = %AuthErrorKind::AuthParamsNotFound }, "could not find auth params in notification");
Error::Auth { target: source }.boxed()
})?;
if !verify_message(auth_key, &data, auth_offset, auth_len)
.map_err(|_| Error::Auth { target: source }.boxed())?
{
tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, snmp.username = %String::from_utf8_lossy(&username) }, "V3 authentication failed");
self.send_usm_report(
&msg,
&usm_params,
UsmFailure::WrongDigests,
None,
source,
)
.await;
return Err(Error::Auth { target: source }.boxed());
}
tracing::trace!(target: "async_snmp::notification", { snmp.source = %source }, "V3 authentication verified");
if engine_id == self.inner.engine_id {
let total_secs = self.inner.engine_start.elapsed().as_secs();
let (our_boots, our_time) =
compute_engine_boots_time(self.inner.engine_boots_base, total_secs);
if !in_authoritative_time_window(
our_boots,
our_time,
usm_params.engine_boots,
usm_params.engine_time,
) {
tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, snmp.msg_boots = usm_params.engine_boots, snmp.msg_time = usm_params.engine_time, snmp.our_boots = our_boots, snmp.our_time = our_time }, "V3 notification outside time window");
self.send_usm_report(
&msg,
&usm_params,
UsmFailure::NotInTimeWindows,
Some(auth_key),
source,
)
.await;
return Err(Error::Auth { target: source }.boxed());
}
} else {
let engine_key = Bytes::copy_from_slice(&engine_id);
let timely = {
let mut engines = self
.inner
.remote_engines
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !engines.contains_key(&engine_key)
&& engines.len() >= super::MAX_REMOTE_ENGINES
&& let Some(oldest) = engines
.iter()
.min_by_key(|(_, s)| s.synced_at)
.map(|(k, _)| k.clone())
{
engines.remove(&oldest);
}
let state = engines.entry(engine_key).or_insert_with_key(|k| {
EngineState::new(
k.clone(),
usm_params.engine_boots,
usm_params.engine_time,
)
});
let timely = state.check_and_update_timeliness(
usm_params.engine_boots,
usm_params.engine_time,
);
if !timely {
tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, snmp.msg_boots = usm_params.engine_boots, snmp.msg_time = usm_params.engine_time, snmp.our_boots = state.engine_boots, snmp.our_time = state.estimated_time() }, "V3 notification outside time window");
}
timely
};
if !timely {
return Err(Error::Auth { target: source }.boxed());
}
}
}
None => {
tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, snmp.username = %String::from_utf8_lossy(&username) }, "received authenticated V3 message but user has no auth key");
self.send_usm_report(
&msg,
&usm_params,
UsmFailure::UnsupportedSecLevels,
None,
source,
)
.await;
return Ok(None);
}
}
}
let scoped_pdu = if security_level == SecurityLevel::AuthPriv {
let priv_key = derived_keys
.priv_key
.as_ref()
.expect("authPriv without a privacy key is rejected at Step 5");
let encrypted_data = match &msg.data {
V3MessageData::Encrypted(data) => data,
V3MessageData::Plaintext(_) => {
tracing::debug!(target: "async_snmp::notification", { source = %source, kind = %DecodeErrorKind::UnexpectedEncryption }, "expected encrypted scoped PDU in notification");
return Err(Error::MalformedResponse { target: source }.boxed());
}
};
let decrypted = priv_key.decrypt(
encrypted_data,
usm_params.engine_boots,
usm_params.engine_time,
&usm_params.priv_params,
);
let decrypted = match decrypted {
Ok(data) => data,
Err(e) => {
tracing::debug!(target: "async_snmp::notification", { source = %source, error = %e }, "decryption failed");
self.send_usm_report(
&msg,
&usm_params,
UsmFailure::DecryptionErrors,
None,
source,
)
.await;
return Err(Error::Auth { target: source }.boxed());
}
};
let mut decoder = Decoder::with_target(decrypted, source);
ScopedPdu::decode(&mut decoder)?
} else if let Some(sp) = msg.scoped_pdu() {
sp.clone()
} else {
tracing::warn!(target: "async_snmp::notification", { snmp.source = %source }, "unexpected encrypted V3 message");
return Ok(None);
};
let context_engine_id = scoped_pdu.context_engine_id.clone();
let context_name = scoped_pdu.context_name.clone();
let pdu = &scoped_pdu.pdu;
match pdu.pdu_type {
PduType::TrapV2 => {
let (uptime, trap_oid, varbinds) = extract_notification_varbinds(pdu)?;
Ok(Some(Notification::TrapV3 {
username,
context_engine_id,
context_name,
security_level,
uptime,
trap_oid,
varbinds,
request_id: pdu.request_id,
}))
}
PduType::InformRequest => {
let (uptime, trap_oid, varbinds) = extract_notification_varbinds(pdu)?;
let request_id = pdu.request_id;
let response_pdu = pdu.to_response();
let response_bytes = build_v3_response(
&self.inner,
&msg,
&usm_params,
response_pdu,
context_engine_id.clone(),
context_name.clone(),
Some(&derived_keys),
)?;
self.inner
.socket
.send_to(&response_bytes, source)
.await
.map_err(|e| Error::Network {
target: source,
source: e,
})?;
tracing::debug!(target: "async_snmp::notification", { snmp.source = %source, snmp.request_id = request_id, snmp.security_level = ?security_level }, "sent V3 Inform response");
Ok(Some(Notification::InformV3 {
username,
context_engine_id,
context_name,
security_level,
uptime,
trap_oid,
varbinds,
request_id,
}))
}
_ => Ok(None),
}
}
async fn handle_v3_discovery(
&self,
msg: &V3Message,
usm_params: &UsmSecurityParams,
source: SocketAddr,
) -> Result<Option<Notification>> {
self.send_usm_report(msg, usm_params, UsmFailure::UnknownEngineIds, None, source)
.await;
Ok(None)
}
async fn send_usm_report(
&self,
msg: &V3Message,
usm_params: &UsmSecurityParams,
failure: UsmFailure,
auth_key: Option<&LocalizedKey>,
source: SocketAddr,
) {
let count = failure.counter(&self.inner).fetch_add(1, Ordering::Relaxed) + 1;
if !msg.global_data.msg_flags.reportable {
return;
}
let report_oid = failure.report_oid();
let total_secs = self.inner.engine_start.elapsed().as_secs();
let (boots, time) = compute_engine_boots_time(self.inner.engine_boots_base, total_secs);
let report_pdu = Pdu {
pdu_type: PduType::Report,
request_id: 0,
error_status: 0,
error_index: 0,
varbinds: vec![VarBind::new(report_oid, Value::Counter32(count))],
};
let security_level = if auth_key.is_some() {
SecurityLevel::AuthNoPriv
} else {
SecurityLevel::NoAuthNoPriv
};
let response_global = MsgGlobalData::new(
msg.global_data.msg_id,
msg.global_data.msg_max_size,
MsgFlags::new(security_level, false),
);
let mut response_usm = UsmSecurityParams::new(
self.inner.engine_id.clone(),
boots,
time,
usm_params.username.clone(),
);
if let Some(key) = auth_key {
response_usm = response_usm.with_auth_placeholder(key.mac_len());
}
let response_scoped =
ScopedPdu::new(self.inner.engine_id.clone(), Bytes::new(), report_pdu);
let response_msg = V3Message::new(response_global, response_usm.encode(), response_scoped);
let mut response_bytes = response_msg.encode().to_vec();
if let Some(key) = auth_key
&& sign_v3_message(key, &mut response_bytes, self.inner.local_addr).is_err()
{
return;
}
if let Err(e) = self.inner.socket.send_to(&response_bytes, source).await {
tracing::debug!(target: "async_snmp::notification", { snmp.source = %source, error = %e }, "failed to send USM report");
} else {
tracing::debug!(target: "async_snmp::notification", { snmp.source = %source }, "sent USM report");
}
}
}
fn sign_v3_message(
auth_key: &LocalizedKey,
message: &mut [u8],
local_addr: SocketAddr,
) -> Result<()> {
let (auth_offset, auth_len) = UsmSecurityParams::find_auth_params_offset(message)
.ok_or_else(|| {
tracing::debug!(target: "async_snmp::notification", { kind = %EncodeErrorKind::MissingAuthParams }, "could not find auth params in outgoing V3 message");
Error::MalformedResponse { target: local_addr }.boxed()
})?;
authenticate_message(auth_key, message, auth_offset, auth_len).map_err(|e| {
tracing::debug!(target: "async_snmp::notification", { error = %e }, "failed to authenticate outgoing V3 message");
Error::Config(e.to_string().into()).boxed()
})
}
fn build_v3_response(
inner: &ReceiverInner,
incoming_msg: &V3Message,
incoming_usm: &UsmSecurityParams,
response_pdu: Pdu,
context_engine_id: Bytes,
context_name: Bytes,
derived_keys: Option<&DerivedKeys>,
) -> Result<Bytes> {
let security_level = incoming_msg.global_data.msg_flags.security_level;
let response_global = MsgGlobalData::new(
incoming_msg.global_data.msg_id,
incoming_msg.global_data.msg_max_size,
MsgFlags::new(security_level, false),
);
let response_scoped = ScopedPdu::new(context_engine_id, context_name, response_pdu);
match security_level {
SecurityLevel::NoAuthNoPriv => {
let response_usm = UsmSecurityParams::new(
incoming_usm.engine_id.clone(),
incoming_usm.engine_boots,
incoming_usm.engine_time,
incoming_usm.username.clone(),
);
let response_msg =
V3Message::new(response_global, response_usm.encode(), response_scoped);
Ok(response_msg.encode())
}
SecurityLevel::AuthNoPriv => {
let local_addr = inner.local_addr;
let keys = derived_keys.ok_or_else(|| {
tracing::debug!(target: "async_snmp::notification", { kind = %AuthErrorKind::NoCredentials }, "no credentials for notification response");
Error::Auth { target: local_addr }.boxed()
})?;
let auth_key = keys.auth_key.as_ref().ok_or_else(|| {
tracing::debug!(target: "async_snmp::notification", { kind = %AuthErrorKind::NoAuthKey }, "no auth key for notification response");
Error::Auth { target: local_addr }.boxed()
})?;
let mac_len = auth_key.mac_len();
let response_usm = UsmSecurityParams::new(
incoming_usm.engine_id.clone(),
incoming_usm.engine_boots,
incoming_usm.engine_time,
incoming_usm.username.clone(),
)
.with_auth_placeholder(mac_len);
let response_msg =
V3Message::new(response_global, response_usm.encode(), response_scoped);
let mut response_bytes = response_msg.encode().to_vec();
sign_v3_message(auth_key, &mut response_bytes, local_addr)?;
Ok(Bytes::from(response_bytes))
}
SecurityLevel::AuthPriv => {
let local_addr = inner.local_addr;
let keys = derived_keys.ok_or_else(|| {
tracing::debug!(target: "async_snmp::notification", { kind = %AuthErrorKind::NoCredentials }, "no credentials for notification response");
Error::Auth { target: local_addr }.boxed()
})?;
let auth_key = keys.auth_key.as_ref().ok_or_else(|| {
tracing::debug!(target: "async_snmp::notification", { kind = %AuthErrorKind::NoAuthKey }, "no auth key for notification response");
Error::Auth { target: local_addr }.boxed()
})?;
let priv_key = keys.priv_key.as_ref().ok_or_else(|| {
tracing::debug!(target: "async_snmp::notification", { kind = %CryptoErrorKind::NoPrivKey }, "no privacy key for notification response");
Error::Auth { target: local_addr }.boxed()
})?;
let scoped_pdu_bytes = response_scoped.encode_to_bytes();
let (encrypted, priv_params) = priv_key
.encrypt(
&scoped_pdu_bytes,
incoming_usm.engine_boots,
incoming_usm.engine_time,
Some(&inner.salt_counter),
)
.map_err(|e| {
tracing::debug!(target: "async_snmp::notification", { error = %e }, "encryption failed for notification response");
Error::Auth { target: local_addr }.boxed()
})?;
let mac_len = auth_key.mac_len();
let response_usm = UsmSecurityParams::new(
incoming_usm.engine_id.clone(),
incoming_usm.engine_boots,
incoming_usm.engine_time,
incoming_usm.username.clone(),
)
.with_auth_placeholder(mac_len)
.with_priv_params(priv_params);
let response_msg =
V3Message::new_encrypted(response_global, response_usm.encode(), encrypted);
let mut response_bytes = response_msg.encode().to_vec();
sign_v3_message(auth_key, &mut response_bytes, local_addr)?;
Ok(Bytes::from(response_bytes))
}
}
}