use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use bytes::Bytes;
use super::types::{CdpAddress, CdpCapabilities, CdpMessage};
pub const CDP_DST_MAC: [u8; 6] = [0x01, 0x00, 0x0c, 0xcc, 0xcc, 0xcc];
pub const CDP_OUI: [u8; 3] = [0x00, 0x00, 0x0c];
pub const CDP_PID: u16 = 0x2000;
const TLV_DEVICE_ID: u16 = 0x0001;
const TLV_ADDRESSES: u16 = 0x0002;
const TLV_PORT_ID: u16 = 0x0003;
const TLV_CAPABILITIES: u16 = 0x0004;
const TLV_SOFTWARE_VERSION: u16 = 0x0005;
const TLV_PLATFORM: u16 = 0x0006;
const TLV_VTP_DOMAIN: u16 = 0x0009;
const TLV_NATIVE_VLAN: u16 = 0x000a;
const TLV_DUPLEX: u16 = 0x000b;
const TLV_MGMT_ADDRESS: u16 = 0x0016;
const MAX_TLVS: usize = 64;
const MAX_ADDRESSES: usize = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
Truncated {
need: usize,
have: usize,
},
NotCdp,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Truncated { need, have } => {
write!(f, "truncated CDP frame: need {need}, have {have}")
}
Self::NotCdp => {
f.write_str("not a CDP frame (dst MAC / LLC / SNAP / OUI / PID mismatch)")
}
}
}
}
impl std::error::Error for ParseError {}
impl From<ParseError> for crate::Error {
fn from(e: ParseError) -> Self {
use crate::error::{ErrorCode, Module};
let code = match &e {
ParseError::Truncated { .. } => ErrorCode::Truncated,
ParseError::NotCdp => ErrorCode::Parse,
};
crate::Error::with_code(Module::Cdp, code, e.to_string())
}
}
pub fn parse(payload: &[u8]) -> Result<CdpMessage, ParseError> {
if payload.len() < 4 {
return Err(ParseError::Truncated {
need: 4,
have: payload.len(),
});
}
let version = payload[0];
let ttl_seconds = payload[1];
let mut cursor = &payload[4..];
let mut msg = CdpMessage {
version,
ttl_seconds,
device_id: None,
addresses: Vec::new(),
port_id: None,
capabilities: None,
software_version: None,
platform: None,
native_vlan: None,
duplex: None,
vtp_domain: None,
management_addresses: Vec::new(),
};
for _ in 0..MAX_TLVS {
if cursor.is_empty() {
break;
}
if cursor.len() < 4 {
break;
}
let tlv_type = u16::from_be_bytes([cursor[0], cursor[1]]);
let tlv_len = u16::from_be_bytes([cursor[2], cursor[3]]) as usize;
if tlv_len < 4 || cursor.len() < tlv_len {
break;
}
let value = &cursor[4..tlv_len];
cursor = &cursor[tlv_len..];
match tlv_type {
TLV_DEVICE_ID => msg.device_id.get_or_insert_with(|| copy(value)),
TLV_PORT_ID => msg.port_id.get_or_insert_with(|| copy(value)),
TLV_SOFTWARE_VERSION => msg.software_version.get_or_insert_with(|| copy(value)),
TLV_PLATFORM => msg.platform.get_or_insert_with(|| copy(value)),
TLV_VTP_DOMAIN => msg.vtp_domain.get_or_insert_with(|| copy(value)),
TLV_CAPABILITIES if value.len() == 4 => {
let bits = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
msg.capabilities = Some(CdpCapabilities::from_bits_truncate(bits));
continue;
}
TLV_NATIVE_VLAN if value.len() == 2 => {
msg.native_vlan = Some(u16::from_be_bytes([value[0], value[1]]));
continue;
}
TLV_DUPLEX if !value.is_empty() => {
msg.duplex = Some(value[0]);
continue;
}
TLV_ADDRESSES => {
msg.addresses = parse_address_block(value);
continue;
}
TLV_MGMT_ADDRESS => {
msg.management_addresses = parse_address_block(value);
continue;
}
_ => continue,
};
}
Ok(msg)
}
pub fn parse_frame(frame: &[u8]) -> Result<CdpMessage, ParseError> {
if frame.len() < 22 {
return Err(ParseError::Truncated {
need: 22,
have: frame.len(),
});
}
if frame[0..6] != CDP_DST_MAC {
return Err(ParseError::NotCdp);
}
if frame[14] != 0xaa || frame[15] != 0xaa || frame[16] != 0x03 {
return Err(ParseError::NotCdp); }
if frame[17..20] != CDP_OUI {
return Err(ParseError::NotCdp);
}
let pid = u16::from_be_bytes([frame[20], frame[21]]);
if pid != CDP_PID {
return Err(ParseError::NotCdp);
}
parse(&frame[22..])
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CdpParser;
impl CdpParser {
#[inline]
pub fn parse(&self, payload: &[u8]) -> Result<CdpMessage, ParseError> {
parse(payload)
}
#[inline]
pub fn parse_frame(&self, frame: &[u8]) -> Result<CdpMessage, ParseError> {
parse_frame(frame)
}
}
#[inline]
fn copy(value: &[u8]) -> Bytes {
Bytes::copy_from_slice(value)
}
fn parse_address_block(value: &[u8]) -> Vec<CdpAddress> {
if value.len() < 4 {
return Vec::new();
}
let count = u32::from_be_bytes([value[0], value[1], value[2], value[3]]) as usize;
let count = count.min(MAX_ADDRESSES);
let mut cursor = &value[4..];
let mut out = Vec::with_capacity(count.min(4));
for _ in 0..count {
if cursor.len() < 2 {
break;
}
let protocol_type = cursor[0];
let pl = cursor[1] as usize;
if cursor.len() < 2 + pl + 2 {
break;
}
let proto = &cursor[2..2 + pl];
let al_bytes = &cursor[2 + pl..2 + pl + 2];
let al = u16::from_be_bytes([al_bytes[0], al_bytes[1]]) as usize;
if cursor.len() < 2 + pl + 2 + al {
break;
}
let address = &cursor[2 + pl + 2..2 + pl + 2 + al];
let ip = decode_ip(proto, address);
out.push(CdpAddress {
protocol_type,
ip,
raw_address: copy(address),
});
cursor = &cursor[2 + pl + 2 + al..];
}
out
}
fn decode_ip(proto: &[u8], address: &[u8]) -> Option<IpAddr> {
if proto == [0xcc] && address.len() == 4 {
return Some(IpAddr::V4(Ipv4Addr::new(
address[0], address[1], address[2], address[3],
)));
}
if proto == [0x86, 0xdd] && address.len() == 16 {
let mut octets = [0u8; 16];
octets.copy_from_slice(address);
return Some(IpAddr::V6(Ipv6Addr::from(octets)));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn tlv(ty: u16, value: &[u8]) -> Vec<u8> {
let total_len = (value.len() + 4) as u16;
let mut out = ty.to_be_bytes().to_vec();
out.extend_from_slice(&total_len.to_be_bytes());
out.extend_from_slice(value);
out
}
fn cdp_payload_minimal(device_id: &[u8]) -> Vec<u8> {
let mut p = vec![0x02, 180, 0x00, 0x00]; p.extend(tlv(TLV_DEVICE_ID, device_id));
p
}
fn ipv4_address(addr: [u8; 4]) -> Vec<u8> {
let mut v = Vec::new();
v.push(0x01); v.push(0x01); v.push(0xcc); v.extend_from_slice(&2u16.to_be_bytes()); v.clear();
v.push(0x01);
v.push(0x01);
v.push(0xcc);
v.extend_from_slice(&4u16.to_be_bytes());
v.extend_from_slice(&addr);
v
}
fn ipv6_address(addr: [u8; 16]) -> Vec<u8> {
let mut v = Vec::new();
v.push(0x02); v.push(0x08); v.clear();
v.push(0x02);
v.push(0x02);
v.extend_from_slice(&[0x86, 0xdd]);
v.extend_from_slice(&16u16.to_be_bytes());
v.extend_from_slice(&addr);
v
}
#[test]
fn parses_minimal_device_id() {
let p = cdp_payload_minimal(b"sw-edge-01.example.com");
let m = parse(&p).unwrap();
assert_eq!(m.version, 0x02);
assert_eq!(m.ttl_seconds, 180);
assert_eq!(
m.device_id.as_deref(),
Some(b"sw-edge-01.example.com".as_ref())
);
assert!(m.port_id.is_none());
assert!(m.platform.is_none());
}
#[test]
fn parses_typical_cisco_advertisement() {
let mut p = vec![0x02, 180, 0, 0];
p.extend(tlv(TLV_DEVICE_ID, b"sw-core-01"));
p.extend(tlv(TLV_PORT_ID, b"GigabitEthernet0/24"));
p.extend(tlv(TLV_PLATFORM, b"cisco WS-C2960X-24TS-L"));
p.extend(tlv(TLV_SOFTWARE_VERSION, b"Cisco IOS 15.2(7)E"));
p.extend(tlv(TLV_CAPABILITIES, &0x00000028u32.to_be_bytes())); p.extend(tlv(TLV_NATIVE_VLAN, &10u16.to_be_bytes()));
p.extend(tlv(TLV_DUPLEX, &[1]));
let m = parse(&p).unwrap();
assert_eq!(m.device_id.as_deref(), Some(b"sw-core-01".as_ref()));
assert_eq!(m.port_id.as_deref(), Some(b"GigabitEthernet0/24".as_ref()));
assert_eq!(
m.platform.as_deref(),
Some(b"cisco WS-C2960X-24TS-L".as_ref())
);
assert_eq!(
m.software_version.as_deref(),
Some(b"Cisco IOS 15.2(7)E".as_ref())
);
let caps = m.capabilities.unwrap();
assert!(caps.contains(CdpCapabilities::SWITCH));
assert!(caps.contains(CdpCapabilities::IGMP));
assert!(!caps.contains(CdpCapabilities::ROUTER));
assert_eq!(m.native_vlan, Some(10));
assert_eq!(m.duplex, Some(1));
}
#[test]
fn parses_addresses_tlv_ipv4() {
let mut p = vec![0x02, 180, 0, 0];
let mut addr_block = 1u32.to_be_bytes().to_vec();
addr_block.extend(ipv4_address([192, 0, 2, 10]));
p.extend(tlv(TLV_ADDRESSES, &addr_block));
let m = parse(&p).unwrap();
assert_eq!(m.addresses.len(), 1);
assert_eq!(
m.addresses[0].ip,
Some(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10)))
);
}
#[test]
fn parses_mgmt_address_tlv_ipv6() {
let mut p = vec![0x02, 180, 0, 0];
let mut block = 1u32.to_be_bytes().to_vec();
let mut v6 = [0u8; 16];
v6[0] = 0x20;
v6[1] = 0x01;
v6[15] = 0x05;
block.extend(ipv6_address(v6));
p.extend(tlv(TLV_MGMT_ADDRESS, &block));
let m = parse(&p).unwrap();
assert_eq!(m.management_addresses.len(), 1);
assert_eq!(
m.management_addresses[0].ip,
Some(IpAddr::V6(Ipv6Addr::from(v6)))
);
}
#[test]
fn rejects_truncated_payload() {
assert!(parse(&[]).is_err());
assert!(parse(&[0x02, 180, 0]).is_err());
}
#[test]
fn truncated_tlv_aborts_walk_keeping_earlier_fields() {
let mut p = vec![0x02, 180, 0, 0];
p.extend(tlv(TLV_DEVICE_ID, b"x"));
p.extend_from_slice(&TLV_PORT_ID.to_be_bytes());
p.extend_from_slice(&1000u16.to_be_bytes());
let m = parse(&p).unwrap();
assert_eq!(m.device_id.as_deref(), Some(b"x".as_ref()));
assert!(m.port_id.is_none());
}
#[test]
fn tlv_length_less_than_4_is_malformed_aborts_walk() {
let mut p = vec![0x02, 180, 0, 0];
p.extend_from_slice(&TLV_DEVICE_ID.to_be_bytes());
p.extend_from_slice(&3u16.to_be_bytes()); assert!(parse(&p).unwrap().device_id.is_none());
}
#[test]
fn walker_caps_iterations() {
let mut p = vec![0x02, 180, 0, 0];
for _ in 0..200 {
p.extend(tlv(0x1000, &[0]));
}
let m = parse(&p).unwrap();
assert!(m.device_id.is_none());
}
#[test]
fn parse_frame_round_trip() {
let p = cdp_payload_minimal(b"sw1");
let mut frame = Vec::new();
frame.extend_from_slice(&CDP_DST_MAC);
frame.extend_from_slice(&[0x11; 6]); frame.extend_from_slice(&((p.len() + 8) as u16).to_be_bytes()); frame.extend_from_slice(&[0xaa, 0xaa, 0x03]);
frame.extend_from_slice(&CDP_OUI);
frame.extend_from_slice(&CDP_PID.to_be_bytes());
frame.extend_from_slice(&p);
let m = parse_frame(&frame).unwrap();
assert_eq!(m.device_id.as_deref(), Some(b"sw1".as_ref()));
}
#[test]
fn parse_frame_rejects_non_cdp_dst() {
let p = cdp_payload_minimal(b"sw1");
let mut frame = Vec::new();
frame.extend_from_slice(&[0xff; 6]); frame.extend_from_slice(&[0x11; 6]);
frame.extend_from_slice(&((p.len() + 8) as u16).to_be_bytes());
frame.extend_from_slice(&[0xaa, 0xaa, 0x03]);
frame.extend_from_slice(&CDP_OUI);
frame.extend_from_slice(&CDP_PID.to_be_bytes());
frame.extend_from_slice(&p);
assert!(parse_frame(&frame).is_err());
}
#[test]
fn parse_frame_rejects_wrong_oui() {
let p = cdp_payload_minimal(b"sw1");
let mut frame = Vec::new();
frame.extend_from_slice(&CDP_DST_MAC);
frame.extend_from_slice(&[0x11; 6]);
frame.extend_from_slice(&((p.len() + 8) as u16).to_be_bytes());
frame.extend_from_slice(&[0xaa, 0xaa, 0x03]);
frame.extend_from_slice(&[0xde, 0xad, 0xbe]); frame.extend_from_slice(&CDP_PID.to_be_bytes());
frame.extend_from_slice(&p);
assert!(parse_frame(&frame).is_err());
}
#[test]
fn cdp_parser_marker_delegates() {
let p = cdp_payload_minimal(b"sw1");
let parser = CdpParser;
assert!(parser.parse(&p).is_ok());
}
}