use bytes::Bytes;
use std::sync::atomic::Ordering;
use crate::error::Result;
use crate::message::MsgGlobalData;
use crate::pdu::Pdu;
use crate::v3::DerivedKeys;
use crate::v3::encode::encode_v3_response;
use crate::v3::{MAX_ENGINE_TIME, UsmSecurityParams};
use super::Agent;
impl Agent {
pub(super) fn build_v3_response(
&self,
incoming: &MsgGlobalData,
incoming_usm: &UsmSecurityParams,
response_pdu: Pdu,
context_engine_id: Bytes,
context_name: Bytes,
derived_keys: Option<&DerivedKeys>,
) -> Result<Option<Bytes>> {
let security_level = incoming.msg_flags.security_level;
let (engine_boots, engine_time) = self.inner.state.authoritative_boots_time()?;
if security_level.requires_auth() && engine_boots == MAX_ENGINE_TIME {
tracing::warn!(target: "async_snmp::agent", "engine boots at maximum, refusing authenticated response");
self.inner
.state
.snmp_silent_drops
.fetch_add(1, Ordering::Relaxed);
return Ok(None);
}
let response_usm = UsmSecurityParams::new(
self.inner.state.engine_id.clone(),
engine_boots,
engine_time,
incoming_usm.username.clone(),
);
let advertised_max_size =
i32::try_from(self.inner.state.max_message_size).unwrap_or(i32::MAX);
encode_v3_response(
response_pdu,
incoming.msg_id,
advertised_max_size,
security_level,
response_usm,
context_engine_id,
context_name,
derived_keys,
&self.inner.salt_counter,
self.inner.local_addr,
)
.map(Some)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::Agent;
use crate::message::{MsgFlags, SecurityLevel, V3Message};
use crate::oid;
use crate::oid::Oid;
use crate::pdu::PduType;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use crate::handler::{BoxFuture, GetNextResult, GetResult, HandlerResult, MibHandler};
struct DummyHandler;
impl MibHandler for DummyHandler {
fn get<'a>(
&'a self,
_ctx: &'a crate::handler::RequestContext,
_oid: &'a Oid,
) -> BoxFuture<'a, HandlerResult<GetResult>> {
Box::pin(async { Ok(GetResult::NoSuchObject) })
}
fn get_next<'a>(
&'a self,
_ctx: &'a crate::handler::RequestContext,
_oid: &'a Oid,
) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
Box::pin(async { Ok(GetNextResult::EndOfMibView) })
}
}
async fn test_agent_with_boots(engine_boots: u32) -> Agent {
Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.engine_boots(engine_boots)
.handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(DummyHandler))
.build()
.await
.unwrap()
}
async fn test_agent() -> Agent {
test_agent_with_boots(1).await
}
fn dummy_v3_msg(security_level: SecurityLevel) -> MsgGlobalData {
MsgGlobalData::new(1, 65507, MsgFlags::new(security_level, true))
}
fn dummy_usm() -> UsmSecurityParams {
UsmSecurityParams::new(
Bytes::from_static(b"engine"),
1,
100,
Bytes::from_static(b"testuser"),
)
}
fn dummy_response_pdu() -> Pdu {
Pdu {
pdu_type: PduType::Response,
request_id: 1,
error_status: 0,
error_index: 0,
varbinds: vec![],
}
}
#[tokio::test]
async fn test_boots_latched_drops_auth_nopriv_response() {
let agent = test_agent_with_boots(MAX_ENGINE_TIME).await;
let msg = dummy_v3_msg(SecurityLevel::AuthNoPriv);
let usm = dummy_usm();
let result = agent
.build_v3_response(
&msg,
&usm,
dummy_response_pdu(),
Bytes::from_static(b"engine"),
Bytes::new(),
None,
)
.unwrap();
assert!(
result.is_none(),
"authenticated response should be dropped when boots is latched"
);
assert_eq!(
agent.inner.state.snmp_silent_drops.load(Ordering::Relaxed),
1,
"snmpSilentDrops should be incremented"
);
}
#[tokio::test]
async fn test_boots_latched_drops_auth_priv_response() {
let agent = test_agent_with_boots(MAX_ENGINE_TIME).await;
let msg = dummy_v3_msg(SecurityLevel::AuthPriv);
let usm = dummy_usm();
let result = agent
.build_v3_response(
&msg,
&usm,
dummy_response_pdu(),
Bytes::from_static(b"engine"),
Bytes::new(),
None,
)
.unwrap();
assert!(
result.is_none(),
"authpriv response should be dropped when boots is latched"
);
}
#[tokio::test]
async fn test_boots_latched_allows_noauth_response() {
let agent = test_agent_with_boots(MAX_ENGINE_TIME).await;
let msg = dummy_v3_msg(SecurityLevel::NoAuthNoPriv);
let usm = dummy_usm();
let result = agent
.build_v3_response(
&msg,
&usm,
dummy_response_pdu(),
Bytes::from_static(b"engine"),
Bytes::new(),
None,
)
.unwrap();
assert!(
result.is_some(),
"noAuthNoPriv response should still be sent when boots is latched"
);
}
#[tokio::test]
async fn test_boots_below_max_allows_auth_response() {
let agent = test_agent_with_boots(MAX_ENGINE_TIME - 1).await;
let msg = dummy_v3_msg(SecurityLevel::NoAuthNoPriv);
let usm = dummy_usm();
let result = agent
.build_v3_response(
&msg,
&usm,
dummy_response_pdu(),
Bytes::from_static(b"engine"),
Bytes::new(),
None,
)
.unwrap();
assert!(
result.is_some(),
"noAuthNoPriv should work when boots is below max"
);
}
#[tokio::test]
async fn test_response_uses_current_coherent_authoritative_time() {
let agent = test_agent().await;
agent.inner.state.engine_boots.store(17, Ordering::Relaxed);
agent
.inner
.state
.engine_time
.store(MAX_ENGINE_TIME, Ordering::Relaxed);
let earliest = agent.inner.state.authoritative_boots_time().unwrap();
let encoded = agent
.build_v3_response(
&dummy_v3_msg(SecurityLevel::NoAuthNoPriv),
&dummy_usm(),
dummy_response_pdu(),
Bytes::from_static(b"engine"),
Bytes::new(),
None,
)
.unwrap()
.expect("noAuthNoPriv response should be produced");
let latest = agent.inner.state.authoritative_boots_time().unwrap();
let message = V3Message::decode(encoded).unwrap();
let response_usm = UsmSecurityParams::decode(message.security_params).unwrap();
let response_pair = (response_usm.engine_boots, response_usm.engine_time);
assert_ne!(response_pair, (17, MAX_ENGINE_TIME));
assert_eq!(response_pair.0, 1);
assert!(
response_pair.1 >= earliest.1 && response_pair.1 <= latest.1,
"response pair {response_pair:?} should come from one current elapsed-time sample between {earliest:?} and {latest:?}"
);
}
#[tokio::test]
async fn test_response_advertises_local_max_size() {
let agent = Agent::builder()
.bind("127.0.0.1:0")
.community(b"public")
.max_message_size(1400)
.handler(oid!(1, 3, 6, 1, 4, 1, 99999), Arc::new(DummyHandler))
.build()
.await
.unwrap();
let msg = dummy_v3_msg(SecurityLevel::NoAuthNoPriv);
assert_eq!(msg.msg_max_size, 65507);
let usm = dummy_usm();
let result = agent
.build_v3_response(
&msg,
&usm,
dummy_response_pdu(),
Bytes::from_static(b"engine"),
Bytes::new(),
None,
)
.unwrap()
.expect("noAuthNoPriv response should be produced");
let decoded = V3Message::decode(result).unwrap();
assert_eq!(
decoded.global_data.msg_max_size, 1400,
"response must advertise the agent's local receive capacity, not the requester's value"
);
}
}