towerops-agent/src/snmp/trap.rs

1485 lines
47 KiB
Rust

//! SNMP Trap Listener
//!
//! Listens for SNMP v1 and v2c traps on a UDP socket and logs them.
//! Implements minimal BER/ASN.1 parsing for trap PDUs.
use std::fmt;
use std::net::SocketAddr;
use tokio::net::UdpSocket;
use tokio::sync::mpsc;
/// Default SNMP trap port
pub const DEFAULT_TRAP_PORT: u16 = 162;
/// Maximum UDP packet size for SNMP traps
const MAX_PACKET_SIZE: usize = 65535;
/// SNMP version
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnmpVersion {
V1,
V2c,
}
impl fmt::Display for SnmpVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SnmpVersion::V1 => write!(f, "v1"),
SnmpVersion::V2c => write!(f, "v2c"),
}
}
}
/// SNMPv1 generic trap types
#[derive(Debug, Clone, Copy)]
pub enum GenericTrap {
ColdStart,
WarmStart,
LinkDown,
LinkUp,
AuthenticationFailure,
EgpNeighborLoss,
EnterpriseSpecific,
}
impl GenericTrap {
fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(GenericTrap::ColdStart),
1 => Some(GenericTrap::WarmStart),
2 => Some(GenericTrap::LinkDown),
3 => Some(GenericTrap::LinkUp),
4 => Some(GenericTrap::AuthenticationFailure),
5 => Some(GenericTrap::EgpNeighborLoss),
6 => Some(GenericTrap::EnterpriseSpecific),
_ => None,
}
}
}
impl fmt::Display for GenericTrap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GenericTrap::ColdStart => write!(f, "coldStart"),
GenericTrap::WarmStart => write!(f, "warmStart"),
GenericTrap::LinkDown => write!(f, "linkDown"),
GenericTrap::LinkUp => write!(f, "linkUp"),
GenericTrap::AuthenticationFailure => write!(f, "authenticationFailure"),
GenericTrap::EgpNeighborLoss => write!(f, "egpNeighborLoss"),
GenericTrap::EnterpriseSpecific => write!(f, "enterpriseSpecific"),
}
}
}
/// Parsed SNMP trap
#[derive(Debug, Clone)]
pub struct SnmpTrap {
pub source_addr: SocketAddr,
pub version: SnmpVersion,
#[allow(dead_code)] // Parsed but not currently logged; useful for future filtering
pub community: String,
pub trap_oid: String,
pub generic_trap: Option<GenericTrap>,
pub specific_trap: Option<u32>,
pub uptime: u32,
pub varbinds: Vec<(String, String)>,
}
impl fmt::Display for SnmpTrap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SNMP trap from {} [{}]", self.source_addr, self.version)?;
match self.version {
SnmpVersion::V1 => {
write!(f, " enterprise={}", self.trap_oid)?;
if let Some(generic) = &self.generic_trap {
write!(f, " generic={}", generic)?;
}
if let Some(specific) = self.specific_trap {
write!(f, " specific={}", specific)?;
}
}
SnmpVersion::V2c => {
write!(f, " oid={}", self.trap_oid)?;
}
}
write!(f, " uptime={}", self.uptime)?;
if !self.varbinds.is_empty() {
write!(f, " varbinds=[")?;
for (i, (oid, value)) in self.varbinds.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}={}", oid, value)?;
}
write!(f, "]")?;
}
Ok(())
}
}
/// SNMP trap listener
pub struct TrapListener {
port: u16,
}
impl TrapListener {
pub fn new(port: u16) -> Self {
Self { port }
}
/// Run the trap listener, sending parsed traps through the channel
pub async fn run(self, trap_tx: mpsc::Sender<SnmpTrap>) {
let bind_addr = format!("0.0.0.0:{}", self.port);
let socket = match UdpSocket::bind(&bind_addr).await {
Ok(s) => {
tracing::info!("SNMP trap listener started on UDP port {}", self.port);
s
}
Err(e) => {
tracing::error!("Failed to bind trap listener to {}: {}", bind_addr, e);
return;
}
};
let mut buf = vec![0u8; MAX_PACKET_SIZE];
loop {
match socket.recv_from(&mut buf).await {
Ok((len, src_addr)) => {
let packet = &buf[..len];
match parse_trap(packet, src_addr) {
Ok(trap) => {
if trap_tx.send(trap).await.is_err() {
tracing::warn!("Trap channel closed, stopping listener");
break;
}
}
Err(e) => {
tracing::warn!("Failed to parse SNMP trap from {}: {}", src_addr, e);
}
}
}
Err(e) => {
tracing::warn!("Error receiving trap packet: {}", e);
}
}
}
}
}
// ============================================================================
// BER/ASN.1 Parsing
// ============================================================================
/// BER tag types
mod ber_tags {
pub const INTEGER: u8 = 0x02;
pub const OCTET_STRING: u8 = 0x04;
pub const NULL: u8 = 0x05;
pub const OBJECT_IDENTIFIER: u8 = 0x06;
pub const SEQUENCE: u8 = 0x30;
pub const IP_ADDRESS: u8 = 0x40;
pub const COUNTER32: u8 = 0x41;
pub const GAUGE32: u8 = 0x42;
pub const TIMETICKS: u8 = 0x43;
pub const COUNTER64: u8 = 0x46;
pub const TRAP_PDU_V1: u8 = 0xA4;
pub const TRAP_PDU_V2: u8 = 0xA7;
}
/// Parse error
#[derive(Debug)]
struct ParseError(String);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for ParseError {}
type ParseResult<T> = Result<T, ParseError>;
/// Parse a BER TLV (Tag-Length-Value) and return (tag, value_bytes, remaining_bytes)
fn parse_tlv(data: &[u8]) -> ParseResult<(u8, &[u8], &[u8])> {
if data.is_empty() {
return Err(ParseError("Empty data".to_string()));
}
let tag = data[0];
let (length, header_len) = parse_length(&data[1..])?;
let total_header = 1 + header_len;
if data.len() < total_header + length {
return Err(ParseError(format!(
"Data too short: need {} bytes, have {}",
total_header + length,
data.len()
)));
}
let value = &data[total_header..total_header + length];
let remaining = &data[total_header + length..];
Ok((tag, value, remaining))
}
/// Parse BER length field, returning (length, bytes_consumed)
fn parse_length(data: &[u8]) -> ParseResult<(usize, usize)> {
if data.is_empty() {
return Err(ParseError("Empty length field".to_string()));
}
let first = data[0];
if first < 0x80 {
// Short form: length in single byte
Ok((first as usize, 1))
} else if first == 0x80 {
Err(ParseError("Indefinite length not supported".to_string()))
} else {
// Long form: first byte indicates number of length bytes
let num_bytes = (first & 0x7F) as usize;
if num_bytes > 4 || data.len() < 1 + num_bytes {
return Err(ParseError("Invalid length encoding".to_string()));
}
let mut length: usize = 0;
for byte in &data[1..1 + num_bytes] {
length = (length << 8) | (*byte as usize);
}
Ok((length, 1 + num_bytes))
}
}
/// Parse an INTEGER value
fn parse_integer(data: &[u8]) -> ParseResult<i64> {
if data.is_empty() {
return Ok(0);
}
let mut value: i64 = if data[0] & 0x80 != 0 { -1 } else { 0 };
for &byte in data {
value = (value << 8) | (byte as i64);
}
Ok(value)
}
/// Parse an unsigned INTEGER value
fn parse_unsigned(data: &[u8]) -> ParseResult<u64> {
let mut value: u64 = 0;
for &byte in data {
value = (value << 8) | (byte as u64);
}
Ok(value)
}
/// Parse an OBJECT IDENTIFIER
fn parse_oid(data: &[u8]) -> ParseResult<String> {
if data.is_empty() {
return Ok(String::new());
}
let mut oid_parts = Vec::new();
// First byte encodes first two components: X*40 + Y
let first = data[0] as u32;
oid_parts.push(first / 40);
oid_parts.push(first % 40);
// Remaining bytes use variable-length encoding
let mut value: u32 = 0;
for &byte in &data[1..] {
value = (value << 7) | ((byte & 0x7F) as u32);
if byte & 0x80 == 0 {
oid_parts.push(value);
value = 0;
}
}
Ok(oid_parts
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("."))
}
/// Parse an OCTET STRING as a UTF-8 string (lossy)
fn parse_octet_string(data: &[u8]) -> String {
// Try UTF-8 first, fall back to hex if it contains non-printable chars
let s = String::from_utf8_lossy(data);
if s.chars()
.all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace())
{
s.into_owned()
} else {
// Hex encode non-printable data
data.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join("")
}
}
/// Parse IP address (4 bytes)
fn parse_ip_address(data: &[u8]) -> ParseResult<String> {
if data.len() != 4 {
return Err(ParseError(format!(
"Invalid IP address length: {}",
data.len()
)));
}
Ok(format!("{}.{}.{}.{}", data[0], data[1], data[2], data[3]))
}
/// Parse a varbind value to string representation
fn parse_value_to_string(tag: u8, data: &[u8]) -> String {
match tag {
ber_tags::INTEGER => parse_integer(data)
.map(|v| v.to_string())
.unwrap_or_else(|_| "?".to_string()),
ber_tags::OCTET_STRING => parse_octet_string(data),
ber_tags::OBJECT_IDENTIFIER => parse_oid(data).unwrap_or_else(|_| "?".to_string()),
ber_tags::NULL => "null".to_string(),
ber_tags::IP_ADDRESS => parse_ip_address(data).unwrap_or_else(|_| "?".to_string()),
ber_tags::COUNTER32 | ber_tags::GAUGE32 | ber_tags::TIMETICKS => parse_unsigned(data)
.map(|v| v.to_string())
.unwrap_or_else(|_| "?".to_string()),
ber_tags::COUNTER64 => parse_unsigned(data)
.map(|v| v.to_string())
.unwrap_or_else(|_| "?".to_string()),
_ => format!("[tag=0x{:02x}]", tag),
}
}
/// Parse varbind list
fn parse_varbinds(data: &[u8]) -> ParseResult<Vec<(String, String)>> {
let mut varbinds = Vec::new();
let mut remaining = data;
while !remaining.is_empty() {
// Each varbind is a SEQUENCE of (OID, value)
let (tag, varbind_data, rest) = parse_tlv(remaining)?;
if tag != ber_tags::SEQUENCE {
return Err(ParseError(format!("Expected SEQUENCE, got 0x{:02x}", tag)));
}
remaining = rest;
// Parse OID
let (oid_tag, oid_data, value_rest) = parse_tlv(varbind_data)?;
if oid_tag != ber_tags::OBJECT_IDENTIFIER {
return Err(ParseError(format!("Expected OID, got 0x{:02x}", oid_tag)));
}
let oid = parse_oid(oid_data)?;
// Parse value
let (value_tag, value_data, _) = parse_tlv(value_rest)?;
let value = parse_value_to_string(value_tag, value_data);
varbinds.push((oid, value));
}
Ok(varbinds)
}
/// Parse an SNMP trap packet
fn parse_trap(data: &[u8], source_addr: SocketAddr) -> ParseResult<SnmpTrap> {
// SNMP message: SEQUENCE { version INTEGER, community OCTET STRING, PDU }
let (tag, message_data, _) = parse_tlv(data)?;
if tag != ber_tags::SEQUENCE {
return Err(ParseError(format!("Expected SEQUENCE, got 0x{:02x}", tag)));
}
// Parse version
let (tag, version_data, rest) = parse_tlv(message_data)?;
if tag != ber_tags::INTEGER {
return Err(ParseError(format!(
"Expected INTEGER for version, got 0x{:02x}",
tag
)));
}
let version_num = parse_integer(version_data)?;
let version = match version_num {
0 => SnmpVersion::V1,
1 => SnmpVersion::V2c,
_ => {
return Err(ParseError(format!(
"Unsupported SNMP version: {}",
version_num
)))
}
};
// Parse community string
let (tag, community_data, rest) = parse_tlv(rest)?;
if tag != ber_tags::OCTET_STRING {
return Err(ParseError(format!(
"Expected OCTET STRING for community, got 0x{:02x}",
tag
)));
}
let community = String::from_utf8_lossy(community_data).into_owned();
// Parse PDU based on version
let (pdu_tag, pdu_data, _) = parse_tlv(rest)?;
match version {
SnmpVersion::V1 => {
if pdu_tag != ber_tags::TRAP_PDU_V1 {
return Err(ParseError(format!(
"Expected Trap-PDU (0xA4), got 0x{:02x}",
pdu_tag
)));
}
parse_v1_trap(pdu_data, source_addr, community)
}
SnmpVersion::V2c => {
if pdu_tag != ber_tags::TRAP_PDU_V2 {
return Err(ParseError(format!(
"Expected SNMPv2-Trap-PDU (0xA7), got 0x{:02x}",
pdu_tag
)));
}
parse_v2c_trap(pdu_data, source_addr, community)
}
}
}
/// Parse SNMPv1 Trap-PDU
fn parse_v1_trap(data: &[u8], source_addr: SocketAddr, community: String) -> ParseResult<SnmpTrap> {
// Trap-PDU: enterprise OID, agent-addr, generic-trap, specific-trap, time-stamp, varbinds
// Enterprise OID
let (tag, oid_data, rest) = parse_tlv(data)?;
if tag != ber_tags::OBJECT_IDENTIFIER {
return Err(ParseError(format!(
"Expected OID for enterprise, got 0x{:02x}",
tag
)));
}
let enterprise_oid = parse_oid(oid_data)?;
// Agent address (NetworkAddress - IpAddress)
let (tag, _, rest) = parse_tlv(rest)?;
if tag != ber_tags::IP_ADDRESS {
return Err(ParseError(format!(
"Expected IpAddress for agent-addr, got 0x{:02x}",
tag
)));
}
// We don't use agent-addr, skip it
// Generic trap
let (tag, generic_data, rest) = parse_tlv(rest)?;
if tag != ber_tags::INTEGER {
return Err(ParseError(format!(
"Expected INTEGER for generic-trap, got 0x{:02x}",
tag
)));
}
let generic_num = parse_integer(generic_data)? as u8;
let generic_trap = GenericTrap::from_u8(generic_num);
// Specific trap
let (tag, specific_data, rest) = parse_tlv(rest)?;
if tag != ber_tags::INTEGER {
return Err(ParseError(format!(
"Expected INTEGER for specific-trap, got 0x{:02x}",
tag
)));
}
let specific_trap = parse_unsigned(specific_data)? as u32;
// Timestamp
let (tag, timestamp_data, rest) = parse_tlv(rest)?;
if tag != ber_tags::TIMETICKS {
return Err(ParseError(format!(
"Expected TIMETICKS for time-stamp, got 0x{:02x}",
tag
)));
}
let uptime = parse_unsigned(timestamp_data)? as u32;
// Varbind list
let (tag, varbind_data, _) = parse_tlv(rest)?;
if tag != ber_tags::SEQUENCE {
return Err(ParseError(format!(
"Expected SEQUENCE for varbinds, got 0x{:02x}",
tag
)));
}
let varbinds = parse_varbinds(varbind_data)?;
Ok(SnmpTrap {
source_addr,
version: SnmpVersion::V1,
community,
trap_oid: enterprise_oid,
generic_trap,
specific_trap: Some(specific_trap),
uptime,
varbinds,
})
}
/// Parse SNMPv2c Trap-PDU
fn parse_v2c_trap(
data: &[u8],
source_addr: SocketAddr,
community: String,
) -> ParseResult<SnmpTrap> {
// SNMPv2-Trap-PDU: request-id, error-status, error-index, varbinds
// The trap OID is in the second varbind (snmpTrapOID.0)
// Request ID
let (tag, _, rest) = parse_tlv(data)?;
if tag != ber_tags::INTEGER {
return Err(ParseError(format!(
"Expected INTEGER for request-id, got 0x{:02x}",
tag
)));
}
// Error status
let (tag, _, rest) = parse_tlv(rest)?;
if tag != ber_tags::INTEGER {
return Err(ParseError(format!(
"Expected INTEGER for error-status, got 0x{:02x}",
tag
)));
}
// Error index
let (tag, _, rest) = parse_tlv(rest)?;
if tag != ber_tags::INTEGER {
return Err(ParseError(format!(
"Expected INTEGER for error-index, got 0x{:02x}",
tag
)));
}
// Varbind list
let (tag, varbind_data, _) = parse_tlv(rest)?;
if tag != ber_tags::SEQUENCE {
return Err(ParseError(format!(
"Expected SEQUENCE for varbinds, got 0x{:02x}",
tag
)));
}
let varbinds = parse_varbinds(varbind_data)?;
// Extract sysUpTime from first varbind (1.3.6.1.2.1.1.3.0)
let uptime = varbinds
.first()
.filter(|(oid, _)| oid == "1.3.6.1.2.1.1.3.0")
.and_then(|(_, value)| value.parse::<u32>().ok())
.unwrap_or(0);
// Extract snmpTrapOID from second varbind (1.3.6.1.6.3.1.1.4.1.0)
let trap_oid = varbinds
.get(1)
.filter(|(oid, _)| oid == "1.3.6.1.6.3.1.1.4.1.0")
.map(|(_, value)| value.clone())
.unwrap_or_else(|| "unknown".to_string());
// Remaining varbinds (skip first two which are sysUpTime and snmpTrapOID)
let remaining_varbinds: Vec<_> = varbinds.into_iter().skip(2).collect();
Ok(SnmpTrap {
source_addr,
version: SnmpVersion::V2c,
community,
trap_oid,
generic_trap: None,
specific_trap: None,
uptime,
varbinds: remaining_varbinds,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_length_short() {
assert_eq!(parse_length(&[0x05]).unwrap(), (5, 1));
assert_eq!(parse_length(&[0x7F]).unwrap(), (127, 1));
}
#[test]
fn test_parse_length_long() {
// Two-byte length: 0x81 0x80 = 128
assert_eq!(parse_length(&[0x81, 0x80]).unwrap(), (128, 2));
// Three-byte length: 0x82 0x01 0x00 = 256
assert_eq!(parse_length(&[0x82, 0x01, 0x00]).unwrap(), (256, 3));
}
#[test]
fn test_parse_integer() {
assert_eq!(parse_integer(&[0x00]).unwrap(), 0);
assert_eq!(parse_integer(&[0x01]).unwrap(), 1);
assert_eq!(parse_integer(&[0x7F]).unwrap(), 127);
assert_eq!(parse_integer(&[0x00, 0x80]).unwrap(), 128);
assert_eq!(parse_integer(&[0xFF]).unwrap(), -1);
assert_eq!(parse_integer(&[0x80]).unwrap(), -128);
}
#[test]
fn test_parse_oid() {
// 1.3.6.1.2.1.1.1.0 = 0x2B 0x06 0x01 0x02 0x01 0x01 0x01 0x00
let oid_bytes = [0x2B, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00];
assert_eq!(parse_oid(&oid_bytes).unwrap(), "1.3.6.1.2.1.1.1.0");
}
#[test]
fn test_parse_oid_large_component() {
// OID with component > 127 (uses multi-byte encoding)
// 1.3.6.1.4.1.9.9.41 where 9.9.41 tests various sizes
let oid_bytes = [0x2B, 0x06, 0x01, 0x04, 0x01, 0x09, 0x09, 0x29];
assert_eq!(parse_oid(&oid_bytes).unwrap(), "1.3.6.1.4.1.9.9.41");
}
#[test]
fn test_generic_trap_display() {
assert_eq!(format!("{}", GenericTrap::ColdStart), "coldStart");
assert_eq!(format!("{}", GenericTrap::LinkUp), "linkUp");
assert_eq!(
format!("{}", GenericTrap::EnterpriseSpecific),
"enterpriseSpecific"
);
}
#[test]
fn test_snmp_version_display() {
assert_eq!(format!("{}", SnmpVersion::V1), "v1");
assert_eq!(format!("{}", SnmpVersion::V2c), "v2c");
}
#[test]
fn test_snmp_trap_display_v1() {
let trap = SnmpTrap {
source_addr: "192.168.1.1:161".parse().unwrap(),
version: SnmpVersion::V1,
community: "public".to_string(),
trap_oid: "1.3.6.1.4.1.9.9.41".to_string(),
generic_trap: Some(GenericTrap::EnterpriseSpecific),
specific_trap: Some(1),
uptime: 12345,
varbinds: vec![("1.3.6.1.2.1.2.2.1.1".to_string(), "2".to_string())],
};
let display = format!("{}", trap);
assert!(display.contains("192.168.1.1:161"));
assert!(display.contains("[v1]"));
assert!(display.contains("enterprise=1.3.6.1.4.1.9.9.41"));
assert!(display.contains("generic=enterpriseSpecific"));
assert!(display.contains("specific=1"));
assert!(display.contains("uptime=12345"));
}
#[test]
fn test_snmp_trap_display_v2c() {
let trap = SnmpTrap {
source_addr: "192.168.1.1:161".parse().unwrap(),
version: SnmpVersion::V2c,
community: "public".to_string(),
trap_oid: "1.3.6.1.6.3.1.1.5.4".to_string(),
generic_trap: None,
specific_trap: None,
uptime: 12345,
varbinds: vec![("ifIndex.2".to_string(), "2".to_string())],
};
let display = format!("{}", trap);
assert!(display.contains("192.168.1.1:161"));
assert!(display.contains("[v2c]"));
assert!(display.contains("oid=1.3.6.1.6.3.1.1.5.4"));
assert!(display.contains("uptime=12345"));
}
#[test]
fn test_parse_octet_string_printable() {
let data = b"Hello World";
assert_eq!(parse_octet_string(data), "Hello World");
}
#[test]
fn test_parse_octet_string_binary() {
let data = [0x00, 0x01, 0x02, 0xFF];
assert_eq!(parse_octet_string(&data), "000102ff");
}
#[test]
fn test_parse_ip_address() {
assert_eq!(parse_ip_address(&[192, 168, 1, 1]).unwrap(), "192.168.1.1");
}
#[test]
fn test_parse_ip_address_invalid_length() {
assert!(parse_ip_address(&[192, 168, 1]).is_err());
}
#[test]
fn test_generic_trap_from_u8_all_variants() {
assert!(matches!(
GenericTrap::from_u8(0),
Some(GenericTrap::ColdStart)
));
assert!(matches!(
GenericTrap::from_u8(1),
Some(GenericTrap::WarmStart)
));
assert!(matches!(
GenericTrap::from_u8(2),
Some(GenericTrap::LinkDown)
));
assert!(matches!(GenericTrap::from_u8(3), Some(GenericTrap::LinkUp)));
assert!(matches!(
GenericTrap::from_u8(4),
Some(GenericTrap::AuthenticationFailure)
));
assert!(matches!(
GenericTrap::from_u8(5),
Some(GenericTrap::EgpNeighborLoss)
));
assert!(matches!(
GenericTrap::from_u8(6),
Some(GenericTrap::EnterpriseSpecific)
));
assert!(GenericTrap::from_u8(7).is_none());
assert!(GenericTrap::from_u8(255).is_none());
}
#[test]
fn test_generic_trap_display_all_variants() {
assert_eq!(format!("{}", GenericTrap::ColdStart), "coldStart");
assert_eq!(format!("{}", GenericTrap::WarmStart), "warmStart");
assert_eq!(format!("{}", GenericTrap::LinkDown), "linkDown");
assert_eq!(format!("{}", GenericTrap::LinkUp), "linkUp");
assert_eq!(
format!("{}", GenericTrap::AuthenticationFailure),
"authenticationFailure"
);
assert_eq!(
format!("{}", GenericTrap::EgpNeighborLoss),
"egpNeighborLoss"
);
assert_eq!(
format!("{}", GenericTrap::EnterpriseSpecific),
"enterpriseSpecific"
);
}
#[test]
fn test_parse_tlv_simple() {
// INTEGER 5: tag=0x02, length=0x01, value=0x05
let data = [0x02, 0x01, 0x05];
let (tag, value, remaining) = parse_tlv(&data).unwrap();
assert_eq!(tag, 0x02);
assert_eq!(value, &[0x05]);
assert!(remaining.is_empty());
}
#[test]
fn test_parse_tlv_with_remaining() {
// INTEGER 5 followed by more data
let data = [0x02, 0x01, 0x05, 0x04, 0x02, 0x41, 0x42];
let (tag, value, remaining) = parse_tlv(&data).unwrap();
assert_eq!(tag, 0x02);
assert_eq!(value, &[0x05]);
assert_eq!(remaining, &[0x04, 0x02, 0x41, 0x42]);
}
#[test]
fn test_parse_tlv_empty() {
let result = parse_tlv(&[]);
assert!(result.is_err());
}
#[test]
fn test_parse_tlv_too_short() {
// Says length is 5 but only has 2 bytes
let data = [0x02, 0x05, 0x01, 0x02];
let result = parse_tlv(&data);
assert!(result.is_err());
}
#[test]
fn test_parse_length_empty() {
let result = parse_length(&[]);
assert!(result.is_err());
}
#[test]
fn test_parse_length_indefinite() {
// Indefinite length (0x80) not supported
let result = parse_length(&[0x80]);
assert!(result.is_err());
}
#[test]
fn test_parse_length_too_long() {
// Length field says 5 bytes but not enough data
let result = parse_length(&[0x85, 0x01]);
assert!(result.is_err());
}
#[test]
fn test_parse_integer_empty() {
assert_eq!(parse_integer(&[]).unwrap(), 0);
}
#[test]
fn test_parse_integer_multibyte() {
// 256 = 0x0100
assert_eq!(parse_integer(&[0x01, 0x00]).unwrap(), 256);
// -256 = 0xFF00
assert_eq!(parse_integer(&[0xFF, 0x00]).unwrap(), -256);
}
#[test]
fn test_parse_unsigned() {
assert_eq!(parse_unsigned(&[]).unwrap(), 0);
assert_eq!(parse_unsigned(&[0x01]).unwrap(), 1);
assert_eq!(parse_unsigned(&[0xFF]).unwrap(), 255);
assert_eq!(parse_unsigned(&[0x01, 0x00]).unwrap(), 256);
}
#[test]
fn test_parse_oid_empty() {
assert_eq!(parse_oid(&[]).unwrap(), "");
}
#[test]
fn test_parse_value_to_string_integer() {
let result = parse_value_to_string(ber_tags::INTEGER, &[0x2A]);
assert_eq!(result, "42");
}
#[test]
fn test_parse_value_to_string_octet_string() {
let result = parse_value_to_string(ber_tags::OCTET_STRING, b"test");
assert_eq!(result, "test");
}
#[test]
fn test_parse_value_to_string_object_identifier() {
let oid_bytes = [0x2B, 0x06, 0x01, 0x02, 0x01];
let result = parse_value_to_string(ber_tags::OBJECT_IDENTIFIER, &oid_bytes);
assert_eq!(result, "1.3.6.1.2.1");
}
#[test]
fn test_parse_value_to_string_null() {
let result = parse_value_to_string(ber_tags::NULL, &[]);
assert_eq!(result, "null");
}
#[test]
fn test_parse_value_to_string_ip_address() {
let result = parse_value_to_string(ber_tags::IP_ADDRESS, &[10, 0, 0, 1]);
assert_eq!(result, "10.0.0.1");
}
#[test]
fn test_parse_value_to_string_counter32() {
let result = parse_value_to_string(ber_tags::COUNTER32, &[0x00, 0x01]);
assert_eq!(result, "1");
}
#[test]
fn test_parse_value_to_string_gauge32() {
let result = parse_value_to_string(ber_tags::GAUGE32, &[0x64]);
assert_eq!(result, "100");
}
#[test]
fn test_parse_value_to_string_timeticks() {
let result = parse_value_to_string(ber_tags::TIMETICKS, &[0x00, 0x01]);
assert_eq!(result, "1");
}
#[test]
fn test_parse_value_to_string_counter64() {
let result = parse_value_to_string(ber_tags::COUNTER64, &[0x01, 0x00]);
assert_eq!(result, "256");
}
#[test]
fn test_parse_value_to_string_unknown() {
let result = parse_value_to_string(0xFF, &[0x01]);
assert!(result.contains("tag=0xff"));
}
#[test]
fn test_parse_varbinds_empty() {
let result = parse_varbinds(&[]).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_snmp_trap_display_v1_no_varbinds() {
let trap = SnmpTrap {
source_addr: "192.168.1.1:161".parse().unwrap(),
version: SnmpVersion::V1,
community: "public".to_string(),
trap_oid: "1.3.6.1.4.1.9.9.41".to_string(),
generic_trap: Some(GenericTrap::ColdStart),
specific_trap: None,
uptime: 0,
varbinds: vec![],
};
let display = format!("{}", trap);
assert!(display.contains("[v1]"));
assert!(display.contains("generic=coldStart"));
assert!(!display.contains("varbinds="));
}
#[test]
fn test_snmp_trap_display_v1_no_generic_trap() {
let trap = SnmpTrap {
source_addr: "192.168.1.1:161".parse().unwrap(),
version: SnmpVersion::V1,
community: "public".to_string(),
trap_oid: "1.3.6.1.4.1.9".to_string(),
generic_trap: None,
specific_trap: None,
uptime: 100,
varbinds: vec![],
};
let display = format!("{}", trap);
assert!(display.contains("[v1]"));
assert!(!display.contains("generic="));
assert!(!display.contains("specific="));
}
#[test]
fn test_snmp_trap_display_multiple_varbinds() {
let trap = SnmpTrap {
source_addr: "10.0.0.1:162".parse().unwrap(),
version: SnmpVersion::V2c,
community: "private".to_string(),
trap_oid: "1.3.6.1.6.3.1.1.5.3".to_string(),
generic_trap: None,
specific_trap: None,
uptime: 5000,
varbinds: vec![
("1.3.6.1.2.1.2.2.1.1".to_string(), "1".to_string()),
("1.3.6.1.2.1.2.2.1.2".to_string(), "eth0".to_string()),
],
};
let display = format!("{}", trap);
assert!(display.contains("10.0.0.1:162"));
assert!(display.contains("[v2c]"));
assert!(display.contains("varbinds=["));
assert!(display.contains(", "));
}
#[test]
fn test_trap_listener_new() {
let listener = TrapListener::new(1620);
assert_eq!(listener.port, 1620);
}
#[test]
fn test_parse_error_display() {
let err = ParseError("test error".to_string());
assert_eq!(format!("{}", err), "test error");
}
#[test]
fn test_snmp_version_eq() {
assert_eq!(SnmpVersion::V1, SnmpVersion::V1);
assert_eq!(SnmpVersion::V2c, SnmpVersion::V2c);
assert_ne!(SnmpVersion::V1, SnmpVersion::V2c);
}
#[test]
fn test_snmp_trap_clone() {
let trap = SnmpTrap {
source_addr: "192.168.1.1:161".parse().unwrap(),
version: SnmpVersion::V1,
community: "public".to_string(),
trap_oid: "1.3.6.1.4.1.9".to_string(),
generic_trap: Some(GenericTrap::LinkUp),
specific_trap: Some(42),
uptime: 12345,
varbinds: vec![("oid".to_string(), "value".to_string())],
};
let cloned = trap.clone();
assert_eq!(trap.source_addr, cloned.source_addr);
assert_eq!(trap.version, cloned.version);
assert_eq!(trap.community, cloned.community);
}
#[test]
fn test_generic_trap_copy() {
let trap = GenericTrap::WarmStart;
let copied = trap;
assert!(matches!(copied, GenericTrap::WarmStart));
}
#[test]
fn test_snmp_version_copy() {
let version = SnmpVersion::V2c;
let copied = version;
assert_eq!(copied, SnmpVersion::V2c);
}
// Test full SNMPv1 trap parsing with a minimal but valid trap packet
#[test]
fn test_parse_trap_v1() {
// Build a valid SNMPv1 trap packet
// Inner Trap-PDU contents:
// OID 1.3 (enterprise): 06 01 2B
// IpAddress 10.0.0.1: 40 04 0A 00 00 01
// INTEGER 0 (generic): 02 01 00
// INTEGER 0 (specific): 02 01 00
// TimeTicks 0: 43 01 00
// SEQUENCE {} (varbinds): 30 00
// Total Trap-PDU content = 3 + 6 + 3 + 3 + 3 + 2 = 20 bytes
let trap_pdu: Vec<u8> = vec![
0x06, 0x01, 0x2B, // OID 1.3
0x40, 0x04, 0x0A, 0x00, 0x00, 0x01, // IpAddress 10.0.0.1
0x02, 0x01, 0x00, // INTEGER 0 (generic-trap)
0x02, 0x01, 0x00, // INTEGER 0 (specific-trap)
0x43, 0x01, 0x00, // TimeTicks 0
0x30, 0x00, // SEQUENCE {} (varbinds)
];
// Full message:
// SEQUENCE { version, community, Trap-PDU }
// version: 02 01 00 (3 bytes)
// community "pub": 04 03 70 75 62 (5 bytes)
// Trap-PDU [4]: A4 <len> <trap_pdu>
let mut trap_bytes: Vec<u8> = vec![
0x30,
0x00, // SEQUENCE with placeholder length
0x02,
0x01,
0x00, // INTEGER 0 (version SNMPv1)
0x04,
0x03,
0x70,
0x75,
0x62, // OCTET STRING "pub"
0xA4,
trap_pdu.len() as u8, // Trap-PDU tag with length
];
trap_bytes.extend_from_slice(&trap_pdu);
// Fix outer SEQUENCE length
trap_bytes[1] = (trap_bytes.len() - 2) as u8;
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&trap_bytes, source_addr);
assert!(result.is_ok(), "Expected Ok, got {:?}", result);
let trap = result.unwrap();
assert_eq!(trap.version, SnmpVersion::V1);
assert_eq!(trap.community, "pub");
assert!(trap.generic_trap.is_some());
}
// Test full SNMPv2c trap parsing
#[test]
fn test_parse_trap_v2c() {
// Build a valid SNMPv2c trap packet
// Inner SNMPv2-Trap-PDU contents:
// INTEGER 1 (request-id): 02 01 01
// INTEGER 0 (error-status): 02 01 00
// INTEGER 0 (error-index): 02 01 00
// SEQUENCE {} (varbinds): 30 00
// Total PDU content = 3 + 3 + 3 + 2 = 11 bytes
let trap_pdu: Vec<u8> = vec![
0x02, 0x01, 0x01, // INTEGER 1 (request-id)
0x02, 0x01, 0x00, // INTEGER 0 (error-status)
0x02, 0x01, 0x00, // INTEGER 0 (error-index)
0x30, 0x00, // SEQUENCE {} (varbinds)
];
// Full message:
// SEQUENCE { version, community, SNMPv2-Trap-PDU }
let mut trap_bytes: Vec<u8> = vec![
0x30,
0x00, // SEQUENCE with placeholder length
0x02,
0x01,
0x01, // INTEGER 1 (version SNMPv2c)
0x04,
0x03,
0x70,
0x75,
0x62, // OCTET STRING "pub"
0xA7,
trap_pdu.len() as u8, // SNMPv2-Trap-PDU tag with length
];
trap_bytes.extend_from_slice(&trap_pdu);
// Fix outer SEQUENCE length
trap_bytes[1] = (trap_bytes.len() - 2) as u8;
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&trap_bytes, source_addr);
assert!(result.is_ok(), "Expected Ok, got {:?}", result);
let trap = result.unwrap();
assert_eq!(trap.version, SnmpVersion::V2c);
assert_eq!(trap.community, "pub");
}
#[test]
fn test_parse_trap_invalid_outer_sequence() {
// Not a SEQUENCE
let trap_bytes: Vec<u8> = vec![0x02, 0x01, 0x00];
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&trap_bytes, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_trap_unsupported_version() {
// SEQUENCE with version 2 (not v1=0 or v2c=1)
let trap_bytes: Vec<u8> = vec![
0x30, 0x0A, // INTEGER 2 (unsupported)
0x02, 0x01, 0x02, // OCTET STRING "pub"
0x04, 0x03, 0x70, 0x75, 0x62, // Minimal PDU
0xA7, 0x00,
];
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&trap_bytes, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_trap_invalid_version_tag() {
// Version is not an INTEGER
let trap_bytes: Vec<u8> = vec![
0x30, 0x08, // OCTET STRING instead of INTEGER for version
0x04, 0x01, 0x00, // OCTET STRING "pub"
0x04, 0x03, 0x70, 0x75, 0x62,
];
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&trap_bytes, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_trap_invalid_community_tag() {
// Community is not an OCTET STRING
let trap_bytes: Vec<u8> = vec![
0x30, 0x08, // INTEGER 0 (version)
0x02, 0x01, 0x00, // INTEGER instead of OCTET STRING for community
0x02, 0x01, 0x00, // PDU
0xA4, 0x00,
];
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&trap_bytes, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_trap_v1_invalid_pdu_tag() {
// V1 with wrong PDU tag (should be 0xA4)
let trap_bytes: Vec<u8> = vec![
0x30, 0x0B, // INTEGER 0 (version v1)
0x02, 0x01, 0x00, // OCTET STRING "pub"
0x04, 0x03, 0x70, 0x75, 0x62, // Wrong PDU tag (0xA7 is v2c)
0xA7, 0x00,
];
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&trap_bytes, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_trap_v2c_invalid_pdu_tag() {
// V2c with wrong PDU tag (should be 0xA7)
let trap_bytes: Vec<u8> = vec![
0x30, 0x0B, // INTEGER 1 (version v2c)
0x02, 0x01, 0x01, // OCTET STRING "pub"
0x04, 0x03, 0x70, 0x75, 0x62, // Wrong PDU tag (0xA4 is v1)
0xA4, 0x00,
];
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&trap_bytes, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_varbinds_invalid_varbind_tag() {
// Varbind is not a SEQUENCE
let data: Vec<u8> = vec![
// INTEGER instead of SEQUENCE
0x02, 0x01, 0x00,
];
let result = parse_varbinds(&data);
assert!(result.is_err());
}
#[test]
fn test_parse_varbinds_invalid_oid_tag() {
// Valid SEQUENCE but OID is wrong type
let data: Vec<u8> = vec![
// SEQUENCE
0x30, 0x06, // INTEGER instead of OID
0x02, 0x01, 0x00, // NULL value
0x05, 0x00,
];
let result = parse_varbinds(&data);
assert!(result.is_err());
}
#[test]
fn test_parse_varbinds_valid() {
// Valid varbind: SEQUENCE { OID 1.3, INTEGER 42 }
let data: Vec<u8> = vec![
// SEQUENCE
0x30, 0x06, // OID 1.3
0x06, 0x01, 0x2B, // INTEGER 42
0x02, 0x01, 0x2A,
];
let result = parse_varbinds(&data).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "1.3");
assert_eq!(result[0].1, "42");
}
#[test]
fn test_parse_value_to_string_ip_address_invalid() {
// IP address with wrong length
let result = parse_value_to_string(ber_tags::IP_ADDRESS, &[192, 168, 1]);
assert_eq!(result, "?");
}
#[test]
fn test_parse_value_to_string_oid_empty() {
let result = parse_value_to_string(ber_tags::OBJECT_IDENTIFIER, &[]);
assert_eq!(result, "");
}
#[test]
fn test_parse_length_four_bytes() {
// Four-byte length: 0x84 followed by 4 bytes
let data = [0x84, 0x00, 0x00, 0x01, 0x00];
let result = parse_length(&data);
assert!(result.is_ok());
let (length, consumed) = result.unwrap();
assert_eq!(length, 256);
assert_eq!(consumed, 5);
}
#[test]
fn test_parse_integer_negative_multibyte() {
// -1 as two bytes: 0xFF 0xFF
assert_eq!(parse_integer(&[0xFF, 0xFF]).unwrap(), -1);
// -128 as single byte
assert_eq!(parse_integer(&[0x80]).unwrap(), -128);
// -129 as two bytes: 0xFF 0x7F
assert_eq!(parse_integer(&[0xFF, 0x7F]).unwrap(), -129);
}
#[test]
fn test_parse_unsigned_large() {
// Large counter value
let data = [0x01, 0x00, 0x00, 0x00];
let result = parse_unsigned(&data).unwrap();
assert_eq!(result, 16777216);
}
// Helper to build a minimal v1 trap with custom PDU content
fn build_v1_trap_packet(pdu_content: &[u8]) -> Vec<u8> {
let mut packet = vec![
0x30,
0x00, // SEQUENCE placeholder
0x02,
0x01,
0x00, // INTEGER 0 (version v1)
0x04,
0x03,
0x70,
0x75,
0x62, // OCTET STRING "pub"
0xA4,
pdu_content.len() as u8, // Trap-PDU
];
packet.extend_from_slice(pdu_content);
packet[1] = (packet.len() - 2) as u8;
packet
}
// Helper to build a minimal v2c trap with custom PDU content
fn build_v2c_trap_packet(pdu_content: &[u8]) -> Vec<u8> {
let mut packet = vec![
0x30,
0x00, // SEQUENCE placeholder
0x02,
0x01,
0x01, // INTEGER 1 (version v2c)
0x04,
0x03,
0x70,
0x75,
0x62, // OCTET STRING "pub"
0xA7,
pdu_content.len() as u8, // SNMPv2-Trap-PDU
];
packet.extend_from_slice(pdu_content);
packet[1] = (packet.len() - 2) as u8;
packet
}
#[test]
fn test_parse_v1_trap_invalid_enterprise_tag() {
// Enterprise should be OID, not INTEGER
let pdu = vec![
0x02, 0x01, 0x00, // INTEGER instead of OID
];
let packet = build_v1_trap_packet(&pdu);
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&packet, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_v1_trap_invalid_agent_addr_tag() {
// Agent addr should be IP_ADDRESS
let pdu = vec![
0x06, 0x01, 0x2B, // OID 1.3 (enterprise)
0x02, 0x01, 0x00, // INTEGER instead of IpAddress
];
let packet = build_v1_trap_packet(&pdu);
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&packet, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_v1_trap_invalid_generic_trap_tag() {
// Generic trap should be INTEGER
let pdu = vec![
0x06, 0x01, 0x2B, // OID 1.3 (enterprise)
0x40, 0x04, 0x0A, 0x00, 0x00, 0x01, // IpAddress 10.0.0.1
0x04, 0x01, 0x00, // OCTET STRING instead of INTEGER
];
let packet = build_v1_trap_packet(&pdu);
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&packet, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_v1_trap_invalid_specific_trap_tag() {
// Specific trap should be INTEGER
let pdu = vec![
0x06, 0x01, 0x2B, // OID 1.3 (enterprise)
0x40, 0x04, 0x0A, 0x00, 0x00, 0x01, // IpAddress 10.0.0.1
0x02, 0x01, 0x00, // INTEGER 0 (generic)
0x04, 0x01, 0x00, // OCTET STRING instead of INTEGER
];
let packet = build_v1_trap_packet(&pdu);
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&packet, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_v1_trap_invalid_timestamp_tag() {
// Timestamp should be TIMETICKS
let pdu = vec![
0x06, 0x01, 0x2B, // OID 1.3 (enterprise)
0x40, 0x04, 0x0A, 0x00, 0x00, 0x01, // IpAddress 10.0.0.1
0x02, 0x01, 0x00, // INTEGER 0 (generic)
0x02, 0x01, 0x00, // INTEGER 0 (specific)
0x02, 0x01, 0x00, // INTEGER instead of TIMETICKS
];
let packet = build_v1_trap_packet(&pdu);
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&packet, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_v1_trap_invalid_varbinds_tag() {
// Varbinds should be SEQUENCE
let pdu = vec![
0x06, 0x01, 0x2B, // OID 1.3 (enterprise)
0x40, 0x04, 0x0A, 0x00, 0x00, 0x01, // IpAddress 10.0.0.1
0x02, 0x01, 0x00, // INTEGER 0 (generic)
0x02, 0x01, 0x00, // INTEGER 0 (specific)
0x43, 0x01, 0x00, // TimeTicks 0
0x02, 0x01, 0x00, // INTEGER instead of SEQUENCE
];
let packet = build_v1_trap_packet(&pdu);
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&packet, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_v2c_trap_invalid_request_id_tag() {
// Request-id should be INTEGER
let pdu = vec![
0x04, 0x01, 0x00, // OCTET STRING instead of INTEGER
];
let packet = build_v2c_trap_packet(&pdu);
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&packet, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_v2c_trap_invalid_error_status_tag() {
// Error-status should be INTEGER
let pdu = vec![
0x02, 0x01, 0x01, // INTEGER 1 (request-id)
0x04, 0x01, 0x00, // OCTET STRING instead of INTEGER
];
let packet = build_v2c_trap_packet(&pdu);
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&packet, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_v2c_trap_invalid_error_index_tag() {
// Error-index should be INTEGER
let pdu = vec![
0x02, 0x01, 0x01, // INTEGER 1 (request-id)
0x02, 0x01, 0x00, // INTEGER 0 (error-status)
0x04, 0x01, 0x00, // OCTET STRING instead of INTEGER
];
let packet = build_v2c_trap_packet(&pdu);
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&packet, source_addr);
assert!(result.is_err());
}
#[test]
fn test_parse_v2c_trap_invalid_varbinds_tag() {
// Varbinds should be SEQUENCE
let pdu = vec![
0x02, 0x01, 0x01, // INTEGER 1 (request-id)
0x02, 0x01, 0x00, // INTEGER 0 (error-status)
0x02, 0x01, 0x00, // INTEGER 0 (error-index)
0x02, 0x01, 0x00, // INTEGER instead of SEQUENCE
];
let packet = build_v2c_trap_packet(&pdu);
let source_addr: SocketAddr = "192.168.1.1:162".parse().unwrap();
let result = parse_trap(&packet, source_addr);
assert!(result.is_err());
}
}