1use anyhow::{anyhow, bail, Result};
33use serde_json::{json, Value};
34use std::net::IpAddr;
35
36pub const CDP_MULTICAST_MAC: [u8; 6] = [0x01, 0x00, 0x0c, 0xcc, 0xcc, 0xcc];
42
43pub const LLC_SAP_SNAP: u8 = 0xAA;
45pub const LLC_CONTROL_UI: u8 = 0x03;
47pub const SNAP_OUI_CISCO: [u8; 3] = [0x00, 0x00, 0x0c];
49pub const SNAP_PROTOCOL_CDP: u16 = 0x2000;
51
52pub const LLC_SNAP_HEADER: [u8; 8] = [
54 LLC_SAP_SNAP,
55 LLC_SAP_SNAP,
56 LLC_CONTROL_UI,
57 SNAP_OUI_CISCO[0],
58 SNAP_OUI_CISCO[1],
59 SNAP_OUI_CISCO[2],
60 (SNAP_PROTOCOL_CDP >> 8) as u8,
61 (SNAP_PROTOCOL_CDP & 0xff) as u8,
62];
63
64pub const ETHERNET_HEADER_LEN: usize = 14;
66pub const LLC_SNAP_HEADER_LEN: usize = 8;
68pub const CDP_PAYLOAD_OFFSET: usize = ETHERNET_HEADER_LEN + LLC_SNAP_HEADER_LEN;
70pub const CDP_HEADER_LEN: usize = 4;
72
73pub const MAX_8023_LENGTH: usize = 1500;
82
83pub const MAX_TEXT_TLV: usize = 255;
91
92pub const TLV_DEVICE_ID: u16 = 0x0001;
97pub const TLV_ADDRESSES: u16 = 0x0002;
98pub const TLV_PORT_ID: u16 = 0x0003;
99pub const TLV_CAPABILITIES: u16 = 0x0004;
100pub const TLV_SOFTWARE_VERSION: u16 = 0x0005;
101pub const TLV_PLATFORM: u16 = 0x0006;
102pub const TLV_NATIVE_VLAN: u16 = 0x000A;
103pub const TLV_DUPLEX: u16 = 0x000B;
104pub const TLV_MANAGEMENT_ADDRESS: u16 = 0x0016;
105
106const KNOWN_OTHER_TLVS: &[(u16, &str)] = &[
110 (0x0007, "ip_prefix"),
111 (0x0008, "protocol_hello"),
112 (0x0009, "vtp_management_domain"),
113 (0x000C, "trust_bitmap_legacy"),
114 (0x000E, "voip_vlan_reply"),
115 (0x000F, "voip_vlan_query"),
116 (0x0010, "power_consumption"),
117 (0x0011, "mtu"),
118 (0x0012, "trust_bitmap"),
119 (0x0013, "untrusted_port_cos"),
120 (0x0014, "system_name"),
121 (0x0015, "system_object_id"),
122 (0x0017, "location"),
123 (0x0018, "external_port_id"),
124 (0x0019, "power_requested"),
125 (0x001A, "power_available"),
126 (0x001B, "port_unidirectional"),
127];
128
129pub const CAPABILITY_FLAGS: &[(u32, &str)] = &[
134 (0x0001, "router"),
135 (0x0002, "transparent_bridge"),
136 (0x0004, "source_route_bridge"),
137 (0x0008, "switch"),
138 (0x0010, "host"),
139 (0x0020, "igmp"),
140 (0x0040, "repeater"),
141 (0x0080, "voip_phone"),
142];
143
144pub fn capability_names(bits: u32) -> Vec<String> {
150 let mut out = Vec::new();
151 for (mask, name) in CAPABILITY_FLAGS {
152 if bits & mask != 0 {
153 out.push((*name).to_string());
154 }
155 }
156 let known: u32 = CAPABILITY_FLAGS.iter().map(|(m, _)| *m).sum();
157 let mut leftover = bits & !known;
158 let mut bit = 0u32;
159 while leftover != 0 {
160 if leftover & 1 != 0 {
161 out.push(format!("bit_{}", bit));
162 }
163 leftover >>= 1;
164 bit += 1;
165 }
166 out
167}
168
169pub fn capability_bits(names: &[String]) -> Result<u32> {
174 let mut bits = 0u32;
175 for name in names {
176 let lower = name.trim().to_ascii_lowercase();
177 match CAPABILITY_FLAGS.iter().find(|(_, n)| *n == lower) {
178 Some((mask, _)) => bits |= mask,
179 None => {
180 let allowed: Vec<&str> = CAPABILITY_FLAGS.iter().map(|(_, n)| *n).collect();
181 bail!(
182 "unknown CDP capability '{}'; allowed values are {:?}",
183 name,
184 allowed
185 );
186 }
187 }
188 }
189 Ok(bits)
190}
191
192pub fn checksum(data: &[u8]) -> u16 {
222 let mut buf: Vec<u8> = data.to_vec();
223 if buf.len() % 2 == 1 {
224 let n = buf.len();
225 let last = buf[n - 1];
226 buf[n - 1] = 0x00;
227 buf.push(last);
228 if last & 0x80 != 0 {
229 buf[n] = last.wrapping_sub(1);
230 buf[n - 1] = 0xFF;
231 }
232 }
233 let mut sum: u32 = 0;
235 let mut i = 0;
236 while i + 1 < buf.len() {
237 sum += u16::from_be_bytes([buf[i], buf[i + 1]]) as u32;
238 i += 2;
239 }
240 while (sum >> 16) != 0 {
241 sum = (sum & 0xffff) + (sum >> 16);
242 }
243 !(sum as u16)
244}
245
246pub fn payload_checksum(payload: &[u8]) -> Result<u16> {
249 if payload.len() < CDP_HEADER_LEN {
250 bail!(
251 "CDP payload is {} bytes, shorter than the 4-byte header",
252 payload.len()
253 );
254 }
255 let mut copy = payload.to_vec();
256 copy[2] = 0;
257 copy[3] = 0;
258 Ok(checksum(©))
259}
260
261pub fn mac_to_string(mac: &[u8; 6]) -> String {
267 format!(
268 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
269 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
270 )
271}
272
273pub fn parse_mac(text: &str) -> Result<[u8; 6]> {
275 let parts: Vec<&str> = text.split(['-', ':']).collect();
276 if parts.len() != 6 {
277 bail!(
278 "'{}' is not a MAC address: expected six colon- or dash-separated octets",
279 text
280 );
281 }
282 let mut mac = [0u8; 6];
283 for (i, part) in parts.iter().enumerate() {
284 mac[i] = u8::from_str_radix(part, 16)
285 .map_err(|_| anyhow!("'{}' is not a MAC address: '{}' is not hex", text, part))?;
286 }
287 Ok(mac)
288}
289
290#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct CdpAddress {
297 pub protocol: String,
299 pub address: String,
301}
302
303impl CdpAddress {
304 pub fn new(addr: IpAddr) -> Self {
305 Self {
306 protocol: match addr {
307 IpAddr::V4(_) => "ipv4".to_string(),
308 IpAddr::V6(_) => "ipv6".to_string(),
309 },
310 address: addr.to_string(),
311 }
312 }
313
314 fn to_json(&self) -> Value {
315 json!({ "protocol": self.protocol, "address": self.address })
316 }
317}
318
319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
321pub enum Duplex {
322 Half,
323 Full,
324}
325
326impl Duplex {
327 pub fn as_str(self) -> &'static str {
328 match self {
329 Duplex::Half => "half",
330 Duplex::Full => "full",
331 }
332 }
333
334 fn from_byte(b: u8) -> Self {
335 if b == 0 {
336 Duplex::Half
337 } else {
338 Duplex::Full
339 }
340 }
341
342 fn to_byte(self) -> u8 {
343 match self {
344 Duplex::Half => 0x00,
345 Duplex::Full => 0x01,
346 }
347 }
348}
349
350#[derive(Debug, Clone, PartialEq, Eq)]
356pub struct OtherTlv {
357 pub type_code: u16,
358 pub name: &'static str,
359 pub length: usize,
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
364pub struct CdpAdvertisement {
365 pub version: u8,
366 pub ttl: u8,
367 pub device_id: Option<String>,
368 pub port_id: Option<String>,
369 pub platform: Option<String>,
370 pub software_version: Option<String>,
371 pub capabilities: Option<u32>,
372 pub native_vlan: Option<u16>,
373 pub duplex: Option<Duplex>,
374 pub addresses: Vec<CdpAddress>,
375 pub management_addresses: Vec<CdpAddress>,
376 pub other_tlvs: Vec<OtherTlv>,
377}
378
379impl Default for CdpAdvertisement {
380 fn default() -> Self {
381 Self {
383 version: 2,
384 ttl: 180,
385 device_id: None,
386 port_id: None,
387 platform: None,
388 software_version: None,
389 capabilities: None,
390 native_vlan: None,
391 duplex: None,
392 addresses: Vec::new(),
393 management_addresses: Vec::new(),
394 other_tlvs: Vec::new(),
395 }
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Eq)]
405pub struct DecodedCdp {
406 pub advertisement: CdpAdvertisement,
407 pub declared_checksum: u16,
408 pub computed_checksum: u16,
409}
410
411impl DecodedCdp {
412 pub fn checksum_valid(&self) -> bool {
413 self.declared_checksum == self.computed_checksum
414 }
415}
416
417fn check_text_field(field: &str, text: &str) -> Result<()> {
434 if let Some((index, bad)) = text.char_indices().find(|(_, c)| c.is_control()) {
435 bail!(
436 "'{}' contains a control character (U+{:04X}) at byte {}. CDP text is copied \
437 verbatim into a neighbour's device table and into log lines, so a newline or NUL \
438 there forges a whole neighbour entry. Send printable text only.",
439 field,
440 bad as u32,
441 index
442 );
443 }
444 if text.len() > MAX_TEXT_TLV {
445 bail!(
446 "'{}' is {} bytes; CDP identifiers are capped at {} here so the advertisement still \
447 fits one Ethernet frame",
448 field,
449 text.len(),
450 MAX_TEXT_TLV
451 );
452 }
453 Ok(())
454}
455
456fn identifier_text(value: &[u8]) -> String {
470 crate::utils::sanitize::line_field(&String::from_utf8_lossy(value))
471}
472
473fn push_tlv(out: &mut Vec<u8>, type_code: u16, value: &[u8]) -> Result<()> {
474 let len = value
475 .len()
476 .checked_add(4)
477 .and_then(|l| u16::try_from(l).ok())
478 .ok_or_else(|| {
479 anyhow!(
480 "CDP TLV 0x{:04x} value is {} bytes, too long for a 16-bit length",
481 type_code,
482 value.len()
483 )
484 })?;
485 out.extend_from_slice(&type_code.to_be_bytes());
486 out.extend_from_slice(&len.to_be_bytes());
487 out.extend_from_slice(value);
488 Ok(())
489}
490
491fn encode_address_list(addresses: &[CdpAddress]) -> Result<Vec<u8>> {
492 let mut body = Vec::new();
493 body.extend_from_slice(&(addresses.len() as u32).to_be_bytes());
494 for entry in addresses {
495 let parsed: IpAddr = entry
496 .address
497 .parse()
498 .map_err(|_| anyhow!("'{}' is not an IP address", entry.address))?;
499 match parsed {
500 IpAddr::V4(v4) => {
501 body.push(0x01);
503 body.push(0x01);
504 body.push(0xCC);
505 body.extend_from_slice(&4u16.to_be_bytes());
506 body.extend_from_slice(&v4.octets());
507 }
508 IpAddr::V6(v6) => {
509 body.push(0x02);
511 body.push(0x08);
512 body.extend_from_slice(&[0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x86, 0xDD]);
513 body.extend_from_slice(&16u16.to_be_bytes());
514 body.extend_from_slice(&v6.octets());
515 }
516 }
517 }
518 Ok(body)
519}
520
521pub fn encode_payload(ad: &CdpAdvertisement) -> Result<Vec<u8>> {
526 if ad.version != 1 && ad.version != 2 {
527 bail!("CDP version must be 1 or 2, got {}", ad.version);
528 }
529
530 let mut out = Vec::with_capacity(128);
531 out.push(ad.version);
532 out.push(ad.ttl);
533 out.extend_from_slice(&[0, 0]); if let Some(id) = &ad.device_id {
536 check_text_field("device_id", id)?;
537 push_tlv(&mut out, TLV_DEVICE_ID, id.as_bytes())?;
538 }
539 if !ad.addresses.is_empty() {
540 let body = encode_address_list(&ad.addresses)?;
541 push_tlv(&mut out, TLV_ADDRESSES, &body)?;
542 }
543 if let Some(port) = &ad.port_id {
544 check_text_field("port_id", port)?;
545 push_tlv(&mut out, TLV_PORT_ID, port.as_bytes())?;
546 }
547 if let Some(caps) = ad.capabilities {
548 push_tlv(&mut out, TLV_CAPABILITIES, &caps.to_be_bytes())?;
549 }
550 if let Some(version) = &ad.software_version {
551 push_tlv(&mut out, TLV_SOFTWARE_VERSION, version.as_bytes())?;
556 }
557 if let Some(platform) = &ad.platform {
558 check_text_field("platform", platform)?;
559 push_tlv(&mut out, TLV_PLATFORM, platform.as_bytes())?;
560 }
561 if let Some(vlan) = ad.native_vlan {
562 push_tlv(&mut out, TLV_NATIVE_VLAN, &vlan.to_be_bytes())?;
563 }
564 if let Some(duplex) = ad.duplex {
565 push_tlv(&mut out, TLV_DUPLEX, &[duplex.to_byte()])?;
566 }
567 if !ad.management_addresses.is_empty() {
568 let body = encode_address_list(&ad.management_addresses)?;
569 push_tlv(&mut out, TLV_MANAGEMENT_ADDRESS, &body)?;
570 }
571
572 let cksum = payload_checksum(&out)?;
573 out[2..4].copy_from_slice(&cksum.to_be_bytes());
574 Ok(out)
575}
576
577pub fn encode_frame(source_mac: [u8; 6], payload: &[u8]) -> Result<Vec<u8>> {
585 let body_len = LLC_SNAP_HEADER_LEN + payload.len();
586 if body_len > MAX_8023_LENGTH {
591 bail!(
592 "CDP frame body is {} bytes; the 802.3 length field only carries a length up to {} \
593 (0x0600 and above are EtherTypes, so a longer frame is read as Ethernet II and the \
594 CDP payload is never parsed)",
595 body_len,
596 MAX_8023_LENGTH
597 );
598 }
599 let length_field = u16::try_from(body_len)
600 .map_err(|_| anyhow!("CDP frame body is {} bytes, too long for 802.3", body_len))?;
601
602 let mut frame = Vec::with_capacity(ETHERNET_HEADER_LEN + body_len);
603 frame.extend_from_slice(&CDP_MULTICAST_MAC);
604 frame.extend_from_slice(&source_mac);
605 frame.extend_from_slice(&length_field.to_be_bytes());
606 frame.extend_from_slice(&LLC_SNAP_HEADER);
607 frame.extend_from_slice(payload);
608 Ok(frame)
609}
610
611#[derive(Debug, Clone, Copy, PartialEq, Eq)]
617pub struct FrameHeader {
618 pub destination_mac: [u8; 6],
619 pub source_mac: [u8; 6],
620 pub declared_length: u16,
622}
623
624pub fn decode_frame(frame: &[u8]) -> Result<(FrameHeader, &[u8])> {
633 if frame.len() < CDP_PAYLOAD_OFFSET {
634 bail!(
635 "frame is {} bytes, too short for an 802.3 + LLC/SNAP CDP header ({} needed)",
636 frame.len(),
637 CDP_PAYLOAD_OFFSET
638 );
639 }
640 let mut destination_mac = [0u8; 6];
641 destination_mac.copy_from_slice(&frame[0..6]);
642 let mut source_mac = [0u8; 6];
643 source_mac.copy_from_slice(&frame[6..12]);
644 let declared_length = u16::from_be_bytes([frame[12], frame[13]]);
645
646 let llc_snap = &frame[ETHERNET_HEADER_LEN..CDP_PAYLOAD_OFFSET];
647 if llc_snap != LLC_SNAP_HEADER {
648 bail!(
649 "not a CDP frame: LLC/SNAP header is {:02x?}, expected {:02x?} \
650 (DSAP/SSAP 0xAA, control 0x03, OUI 00:00:0c, protocol 0x2000)",
651 llc_snap,
652 LLC_SNAP_HEADER
653 );
654 }
655
656 Ok((
657 FrameHeader {
658 destination_mac,
659 source_mac,
660 declared_length,
661 },
662 &frame[CDP_PAYLOAD_OFFSET..],
663 ))
664}
665
666fn decode_address_list(body: &[u8]) -> Vec<CdpAddress> {
667 let mut out = Vec::new();
668 if body.len() < 4 {
669 return out;
670 }
671 let count = u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize;
672 let mut offset = 4usize;
673 while out.len() < count.min(64) && offset + 2 <= body.len() {
676 let _protocol_type = body[offset];
677 let protocol_len = body[offset + 1] as usize;
678 offset += 2;
679 if offset + protocol_len + 2 > body.len() {
680 break;
681 }
682 let protocol = &body[offset..offset + protocol_len];
683 offset += protocol_len;
684 let addr_len = u16::from_be_bytes([body[offset], body[offset + 1]]) as usize;
685 offset += 2;
686 if offset + addr_len > body.len() {
687 break;
688 }
689 let addr = &body[offset..offset + addr_len];
690 offset += addr_len;
691
692 if addr_len == 4 && protocol == [0xCC] {
695 out.push(CdpAddress {
696 protocol: "ipv4".to_string(),
697 address: std::net::Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]).to_string(),
698 });
699 } else if addr_len == 16 {
700 let mut octets = [0u8; 16];
701 octets.copy_from_slice(addr);
702 out.push(CdpAddress {
703 protocol: "ipv6".to_string(),
704 address: std::net::Ipv6Addr::from(octets).to_string(),
705 });
706 }
707 }
709 out
710}
711
712pub fn decode_payload(payload: &[u8]) -> Result<DecodedCdp> {
714 if payload.len() < CDP_HEADER_LEN {
715 bail!(
716 "CDP payload is {} bytes, shorter than the 4-byte header",
717 payload.len()
718 );
719 }
720 let version = payload[0];
721 let ttl = payload[1];
722 let declared_checksum = u16::from_be_bytes([payload[2], payload[3]]);
723 let computed_checksum = payload_checksum(payload)?;
724
725 let mut ad = CdpAdvertisement {
726 version,
727 ttl,
728 ..CdpAdvertisement::default()
729 };
730
731 let mut offset = CDP_HEADER_LEN;
732 while offset + 4 <= payload.len() {
733 let type_code = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
734 let declared = u16::from_be_bytes([payload[offset + 2], payload[offset + 3]]) as usize;
735 if declared < 4 {
738 break;
739 }
740 let end = offset + declared;
741 if end > payload.len() {
742 break;
743 }
744 let value = &payload[offset + 4..end];
745
746 match type_code {
747 TLV_DEVICE_ID => ad.device_id = Some(identifier_text(value)),
751 TLV_PORT_ID => ad.port_id = Some(identifier_text(value)),
752 TLV_PLATFORM => ad.platform = Some(identifier_text(value)),
753 TLV_SOFTWARE_VERSION => {
754 ad.software_version = Some(String::from_utf8_lossy(value).into_owned())
755 }
756 TLV_CAPABILITIES if value.len() >= 4 => {
757 ad.capabilities = Some(u32::from_be_bytes([value[0], value[1], value[2], value[3]]))
758 }
759 TLV_NATIVE_VLAN if value.len() >= 2 => {
760 ad.native_vlan = Some(u16::from_be_bytes([value[0], value[1]]))
761 }
762 TLV_DUPLEX if !value.is_empty() => ad.duplex = Some(Duplex::from_byte(value[0])),
763 TLV_ADDRESSES => ad.addresses = decode_address_list(value),
764 TLV_MANAGEMENT_ADDRESS => ad.management_addresses = decode_address_list(value),
765 _ => ad.other_tlvs.push(OtherTlv {
766 type_code,
767 name: KNOWN_OTHER_TLVS
768 .iter()
769 .find(|(c, _)| *c == type_code)
770 .map(|(_, n)| *n)
771 .unwrap_or("unknown"),
772 length: value.len(),
773 }),
774 }
775
776 offset = end;
777 }
778
779 Ok(DecodedCdp {
780 advertisement: ad,
781 declared_checksum,
782 computed_checksum,
783 })
784}
785
786fn addresses_json(list: &[CdpAddress]) -> Value {
791 Value::Array(list.iter().map(CdpAddress::to_json).collect())
792}
793
794impl DecodedCdp {
795 pub fn to_event_data(&self, header: &FrameHeader, connection_id: &str) -> Value {
801 let ad = &self.advertisement;
802 json!({
803 "connection_id": connection_id,
804 "source_mac": mac_to_string(&header.source_mac),
805 "destination_mac": mac_to_string(&header.destination_mac),
806 "version": ad.version,
807 "ttl": ad.ttl,
808 "device_id": ad.device_id,
809 "port_id": ad.port_id,
810 "platform": ad.platform,
811 "software_version": ad.software_version,
812 "capabilities": ad.capabilities.map(capability_names).unwrap_or_default(),
813 "capabilities_value": ad.capabilities,
814 "native_vlan": ad.native_vlan,
815 "duplex": ad.duplex.map(|d| d.as_str()),
816 "addresses": addresses_json(&ad.addresses),
817 "management_addresses": addresses_json(&ad.management_addresses),
818 "checksum_valid": self.checksum_valid(),
819 "other_tlvs": Value::Array(
820 ad.other_tlvs
821 .iter()
822 .map(|t| json!({ "type": t.type_code, "name": t.name, "length": t.length }))
823 .collect(),
824 ),
825 })
826 }
827}
828
829fn parse_address_field(field: &str, value: &Value) -> Result<Vec<CdpAddress>> {
830 let Some(items) = value.as_array() else {
831 bail!("'{}' must be an array of IP addresses", field);
832 };
833 let mut out = Vec::new();
834 for item in items {
835 let text = match item {
836 Value::String(s) => s.clone(),
837 Value::Object(obj) => obj
838 .get("address")
839 .and_then(|v| v.as_str())
840 .map(|s| s.to_string())
841 .ok_or_else(|| anyhow!("each '{}' entry needs an 'address' string", field))?,
842 other => bail!(
843 "each '{}' entry must be an IP address string, got {}",
844 field,
845 other
846 ),
847 };
848 let parsed: IpAddr = text
849 .parse()
850 .map_err(|_| anyhow!("'{}' in '{}' is not an IP address", text, field))?;
851 out.push(CdpAddress::new(parsed));
852 }
853 Ok(out)
854}
855
856fn optional_string(action: &Value, key: &str) -> Result<Option<String>> {
857 match action.get(key) {
858 None | Some(Value::Null) => Ok(None),
859 Some(Value::String(s)) => Ok(Some(s.clone())),
860 Some(other) => bail!("'{}' must be a string, got {}", key, other),
861 }
862}
863
864impl CdpAdvertisement {
865 pub fn from_action(action: &Value) -> Result<Self> {
872 let mut ad = CdpAdvertisement::default();
873
874 if let Some(v) = action.get("version") {
875 let n = v
876 .as_u64()
877 .ok_or_else(|| anyhow!("'version' must be 1 or 2, got {}", v))?;
878 if n != 1 && n != 2 {
879 bail!("'version' must be 1 or 2, got {}", n);
880 }
881 ad.version = n as u8;
882 }
883
884 if let Some(v) = action.get("ttl") {
885 let n = v
886 .as_u64()
887 .ok_or_else(|| anyhow!("'ttl' must be a number of seconds, got {}", v))?;
888 if n > 255 {
889 bail!("'ttl' must be 0-255 seconds (CDP carries it in one byte), got {n}");
890 }
891 ad.ttl = n as u8;
892 }
893
894 ad.device_id = optional_string(action, "device_id")?;
895 ad.port_id = optional_string(action, "port_id")?;
896 ad.platform = optional_string(action, "platform")?;
897 ad.software_version = optional_string(action, "software_version")?;
898
899 if ad
902 .device_id
903 .as_deref()
904 .map(str::trim)
905 .unwrap_or("")
906 .is_empty()
907 {
908 bail!("send_cdp_advertisement requires a non-empty 'device_id'");
909 }
910
911 match action.get("capabilities") {
912 None | Some(Value::Null) => {}
913 Some(Value::Array(items)) => {
914 let names: Vec<String> = items
915 .iter()
916 .map(|i| {
917 i.as_str()
918 .map(|s| s.to_string())
919 .ok_or_else(|| anyhow!("'capabilities' entries must be strings"))
920 })
921 .collect::<Result<_>>()?;
922 ad.capabilities = Some(capability_bits(&names)?);
923 }
924 Some(Value::Number(n)) => {
925 let raw = n
926 .as_u64()
927 .ok_or_else(|| anyhow!("'capabilities' must be a non-negative number"))?;
928 ad.capabilities = Some(u32::try_from(raw).map_err(|_| {
929 anyhow!("'capabilities' bitmask must fit in 32 bits, got {}", raw)
930 })?);
931 }
932 Some(other) => bail!(
933 "'capabilities' must be an array of names (e.g. [\"switch\",\"igmp\"]) \
934 or a numeric bitmask, got {}",
935 other
936 ),
937 }
938
939 if let Some(v) = action.get("native_vlan") {
940 if !v.is_null() {
941 let n = v
942 .as_u64()
943 .ok_or_else(|| anyhow!("'native_vlan' must be a number, got {}", v))?;
944 if n > 4094 {
945 bail!("'native_vlan' must be 0-4094, got {}", n);
946 }
947 ad.native_vlan = Some(n as u16);
948 }
949 }
950
951 match action.get("duplex") {
952 None | Some(Value::Null) => {}
953 Some(Value::String(s)) => {
954 ad.duplex = Some(match s.trim().to_ascii_lowercase().as_str() {
955 "full" => Duplex::Full,
956 "half" => Duplex::Half,
957 other => bail!("'duplex' must be \"full\" or \"half\", got \"{}\"", other),
958 })
959 }
960 Some(other) => bail!("'duplex' must be \"full\" or \"half\", got {}", other),
961 }
962
963 if let Some(v) = action.get("addresses") {
964 if !v.is_null() {
965 ad.addresses = parse_address_field("addresses", v)?;
966 }
967 }
968 if let Some(v) = action.get("management_addresses") {
969 if !v.is_null() {
970 ad.management_addresses = parse_address_field("management_addresses", v)?;
971 }
972 }
973
974 Ok(ad)
975 }
976
977 pub fn summary(&self) -> String {
979 format!(
980 "device_id={} port_id={} platform={} native_vlan={} ttl={}",
981 self.device_id.as_deref().unwrap_or("-"),
982 self.port_id.as_deref().unwrap_or("-"),
983 self.platform.as_deref().unwrap_or("-"),
984 self.native_vlan
985 .map(|v| v.to_string())
986 .unwrap_or_else(|| "-".to_string()),
987 self.ttl
988 )
989 }
990}