//! 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 { 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, pub specific_trap: Option, 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) { 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 = Result; /// 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 { 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 { 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 { 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::>() .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::>() .join("") } } /// Parse IP address (4 bytes) fn parse_ip_address(data: &[u8]) -> ParseResult { 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> { 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 { // 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 { // 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 { // 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::().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 = 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 let mut trap_bytes: Vec = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 { 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 { 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()); } }