use std::net::SocketAddr;
use std::sync::atomic::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::pdu::{Pdu, PduType, TrapV1Pdu};
use crate::v3::auth::{authenticate_message, verify_message};
use crate::v3::{MAX_ENGINE_TIME, TIME_WINDOW, UsmSecurityParams};
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;
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()?;
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)?;
let pdu = match msg.pdu.standard() {
Some(p) => p,
None => 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;
}
if usm_params.engine_id != self.inner.engine_id {
tracing::debug!(target: "async_snmp::notification", { snmp.source = %source, snmp.msg_engine_id = ?usm_params.engine_id.as_ref(), snmp.our_engine_id = ?self.inner.engine_id.as_ref() }, "engine ID mismatch, configure receiver with sender's engine ID for V3 traps");
return Ok(None);
}
let username = usm_params.username.clone();
let engine_id = usm_params.engine_id.clone();
let user_config = self.inner.usm_users.get(&username);
let derived_keys = user_config
.map(|u| u.derive_keys(&engine_id))
.transpose()
.map_err(|e| Error::Config(e.to_string().into()).boxed())?;
if security_level == SecurityLevel::AuthNoPriv || security_level == SecurityLevel::AuthPriv
{
match &derived_keys {
Some(keys) if keys.auth_key.is_some() => {
let auth_key = keys.auth_key.as_ref().unwrap();
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");
return Err(Error::Auth { target: source }.boxed());
}
tracing::trace!(target: "async_snmp::notification", { snmp.source = %source }, "V3 authentication verified");
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 our_boots == MAX_ENGINE_TIME {
tracing::warn!(target: "async_snmp::notification", { snmp.source = %source }, "engine boots at maximum, rejecting authenticated notification");
return Err(Error::Auth { target: source }.boxed());
}
if usm_params.engine_boots != our_boots {
tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, snmp.msg_boots = usm_params.engine_boots, snmp.our_boots = our_boots }, "V3 notification engine boots mismatch");
return Err(Error::Auth { target: source }.boxed());
}
let time_diff = (usm_params.engine_time as i64 - our_time as i64).abs();
if time_diff > TIME_WINDOW as i64 {
tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, snmp.msg_time = usm_params.engine_time, snmp.our_time = our_time }, "V3 notification outside time window");
return Err(Error::Auth { target: source }.boxed());
}
}
_ => {
tracing::warn!(target: "async_snmp::notification", { snmp.source = %source, snmp.username = %String::from_utf8_lossy(&username) }, "received authenticated V3 message but no credentials configured for user");
return Ok(None);
}
}
}
let scoped_pdu = if security_level == SecurityLevel::AuthPriv {
match &derived_keys {
Some(keys) if keys.priv_key.is_some() => {
let priv_key = keys.priv_key.as_ref().unwrap();
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,
)
.map_err(|e| {
tracing::debug!(target: "async_snmp::notification", { source = %source, error = %e }, "decryption failed");
Error::Auth { target: source }.boxed()
})?;
let mut decoder = Decoder::with_target(decrypted, source);
ScopedPdu::decode(&mut decoder)?
}
_ => {
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");
return Ok(None);
}
}
} else {
match msg.scoped_pdu() {
Some(sp) => sp.clone(),
None => {
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,
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(),
derived_keys.as_ref(),
)?;
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,
uptime,
trap_oid,
varbinds,
request_id,
}))
}
_ => Ok(None),
}
}
async fn handle_v3_discovery(
&self,
msg: &V3Message,
usm_params: &UsmSecurityParams,
source: SocketAddr,
) -> Result<Option<Notification>> {
if !msg.global_data.msg_flags.reportable {
return Ok(None);
}
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 count = self
.inner
.usm_unknown_engine_ids
.fetch_add(1, Ordering::Relaxed)
+ 1;
let report_pdu = Pdu {
pdu_type: PduType::Report,
request_id: msg.global_data.msg_id,
error_status: 0,
error_index: 0,
varbinds: vec![VarBind::new(
crate::v3::report_oids::unknown_engine_ids(),
Value::Counter32(count),
)],
};
let response_global = MsgGlobalData::new(
msg.global_data.msg_id,
msg.global_data.msg_max_size,
MsgFlags::new(SecurityLevel::NoAuthNoPriv, false),
);
let response_usm = UsmSecurityParams::new(
self.inner.engine_id.clone(),
boots,
time,
usm_params.username.clone(),
);
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);
self.inner
.socket
.send_to(&response_msg.encode(), source)
.await
.map_err(|e| Error::Network {
target: source,
source: e,
})?;
tracing::debug!(target: "async_snmp::notification", { snmp.source = %source }, "sent discovery response");
Ok(None)
}
}
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();
let (auth_offset, auth_len) =
UsmSecurityParams::find_auth_params_offset(&response_bytes).ok_or_else(|| {
tracing::debug!(target: "async_snmp::notification", { kind = %EncodeErrorKind::MissingAuthParams }, "could not find auth params in notification response");
Error::MalformedResponse { target: local_addr }.boxed()
})?;
authenticate_message(auth_key, &mut response_bytes, auth_offset, auth_len)
.map_err(|e| Error::Config(e.to_string().into()).boxed())?;
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();
let (auth_offset, auth_len) =
UsmSecurityParams::find_auth_params_offset(&response_bytes).ok_or_else(|| {
tracing::debug!(target: "async_snmp::notification", { kind = %EncodeErrorKind::MissingAuthParams }, "could not find auth params in notification response");
Error::MalformedResponse { target: local_addr }.boxed()
})?;
authenticate_message(auth_key, &mut response_bytes, auth_offset, auth_len)
.map_err(|e| Error::Config(e.to_string().into()).boxed())?;
Ok(Bytes::from(response_bytes))
}
}
}