1use anyhow::{anyhow, bail, Context, Result};
46use serde_json::{json, Map, Value};
47
48pub const LLDP_ETHERTYPE: u16 = 0x88CC;
50
51pub const LLDP_MULTICAST_MAC: [u8; 6] = [0x01, 0x80, 0xC2, 0x00, 0x00, 0x0E];
53
54pub const ETHERNET_HEADER_LEN: usize = 14;
56
57pub const TLV_END_OF_LLDPDU: u8 = 0;
62pub const TLV_CHASSIS_ID: u8 = 1;
63pub const TLV_PORT_ID: u8 = 2;
64pub const TLV_TIME_TO_LIVE: u8 = 3;
65pub const TLV_PORT_DESCRIPTION: u8 = 4;
66pub const TLV_SYSTEM_NAME: u8 = 5;
67pub const TLV_SYSTEM_DESCRIPTION: u8 = 6;
68pub const TLV_SYSTEM_CAPABILITIES: u8 = 7;
69pub const TLV_MANAGEMENT_ADDRESS: u8 = 8;
70
71const MAX_TLV_VALUE: usize = 511;
73
74const MAX_STRING_TLV_VALUE: usize = 255;
76
77pub const CHASSIS_ID_SUBTYPES: &[(u8, &str)] = &[
79 (1, "chassis_component"),
80 (2, "interface_alias"),
81 (3, "port_component"),
82 (4, "mac_address"),
83 (5, "network_address"),
84 (6, "interface_name"),
85 (7, "local"),
86];
87
88pub const PORT_ID_SUBTYPES: &[(u8, &str)] = &[
92 (1, "interface_alias"),
93 (2, "port_component"),
94 (3, "mac_address"),
95 (4, "network_address"),
96 (5, "interface_name"),
97 (6, "agent_circuit_id"),
98 (7, "local"),
99];
100
101pub const SYSTEM_CAPABILITIES: &[(u16, &str)] = &[
103 (0x0001, "other"),
104 (0x0002, "repeater"),
105 (0x0004, "bridge"),
106 (0x0008, "wlan_access_point"),
107 (0x0010, "router"),
108 (0x0020, "telephone"),
109 (0x0040, "docsis_cable_device"),
110 (0x0080, "station_only"),
111 (0x0100, "c_vlan_component"),
112 (0x0200, "s_vlan_component"),
113 (0x0400, "two_port_mac_relay"),
114];
115
116const ADDRESS_FAMILIES: &[(u8, &str)] = &[(1, "ipv4"), (2, "ipv6"), (6, "mac_address")];
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum IdKind {
123 Chassis,
124 Port,
125}
126
127impl IdKind {
128 fn table(self) -> &'static [(u8, &'static str)] {
129 match self {
130 IdKind::Chassis => CHASSIS_ID_SUBTYPES,
131 IdKind::Port => PORT_ID_SUBTYPES,
132 }
133 }
134
135 fn mac_subtype(self) -> u8 {
136 match self {
137 IdKind::Chassis => 4,
138 IdKind::Port => 3,
139 }
140 }
141
142 fn network_address_subtype(self) -> u8 {
143 match self {
144 IdKind::Chassis => 5,
145 IdKind::Port => 4,
146 }
147 }
148
149 fn label(self) -> &'static str {
150 match self {
151 IdKind::Chassis => "chassis_id",
152 IdKind::Port => "port_id",
153 }
154 }
155}
156
157pub fn subtype_code(kind: IdKind, name: &str) -> Option<u8> {
159 let wanted = name.trim().to_ascii_lowercase();
160 kind.table()
161 .iter()
162 .find(|(_, n)| *n == wanted)
163 .map(|(code, _)| *code)
164}
165
166pub fn subtype_name(kind: IdKind, code: u8) -> String {
168 kind.table()
169 .iter()
170 .find(|(c, _)| *c == code)
171 .map(|(_, n)| (*n).to_string())
172 .unwrap_or_else(|| format!("reserved_{code}"))
173}
174
175pub fn capability_names(bits: u16) -> Vec<String> {
179 let mut out = Vec::new();
180 for i in 0..16 {
181 let bit = 1u16 << i;
182 if bits & bit == 0 {
183 continue;
184 }
185 match SYSTEM_CAPABILITIES.iter().find(|(b, _)| *b == bit) {
186 Some((_, name)) => out.push((*name).to_string()),
187 None => out.push(format!("reserved_bit_{i}")),
188 }
189 }
190 out
191}
192
193pub fn capability_bits(names: &[String]) -> Result<u16> {
197 let mut bits = 0u16;
198 for name in names {
199 let wanted = name.trim().to_ascii_lowercase();
200 let found = SYSTEM_CAPABILITIES
201 .iter()
202 .find(|(_, n)| *n == wanted)
203 .map(|(b, _)| *b);
204 match found {
205 Some(b) => bits |= b,
206 None => bail!(
207 "'{}' is not an LLDP system capability. Valid names: {}",
208 name,
209 SYSTEM_CAPABILITIES
210 .iter()
211 .map(|(_, n)| *n)
212 .collect::<Vec<_>>()
213 .join(", ")
214 ),
215 }
216 }
217 Ok(bits)
218}
219
220pub fn parse_mac(text: &str) -> Result<[u8; 6]> {
222 let cleaned: String = text
223 .chars()
224 .filter(|c| !matches!(c, ':' | '-' | '.' | ' '))
225 .collect();
226 if cleaned.len() != 12 || !cleaned.chars().all(|c| c.is_ascii_hexdigit()) {
227 bail!("'{text}' is not a MAC address (expected 6 hex octets, e.g. 00:01:30:f9:ad:a0)");
228 }
229 let mut out = [0u8; 6];
230 for (i, byte) in out.iter_mut().enumerate() {
231 *byte = u8::from_str_radix(&cleaned[i * 2..i * 2 + 2], 16)
232 .with_context(|| format!("'{text}' is not a MAC address"))?;
233 }
234 Ok(out)
235}
236
237pub fn format_mac(mac: &[u8; 6]) -> String {
239 format!(
240 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
241 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
242 )
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct ManagementAddress {
255 pub family: u8,
257 pub address: String,
259 pub interface_numbering_subtype: u8,
261 pub interface_number: u32,
263}
264
265impl ManagementAddress {
266 pub fn new(address: &str, interface_number: u32) -> Result<Self> {
268 let trimmed = address.trim();
269 let family = if trimmed.parse::<std::net::Ipv4Addr>().is_ok() {
270 1
271 } else if trimmed.parse::<std::net::Ipv6Addr>().is_ok() {
272 2
273 } else if parse_mac(trimmed).is_ok() {
274 6
275 } else {
276 bail!(
277 "'{address}' is not a management address: expected an IPv4 address, an IPv6 \
278 address or a MAC address"
279 );
280 };
281 Ok(Self {
282 family,
283 address: trimmed.to_string(),
284 interface_numbering_subtype: 2,
287 interface_number,
288 })
289 }
290
291 pub fn family_name(&self) -> String {
293 ADDRESS_FAMILIES
294 .iter()
295 .find(|(f, _)| *f == self.family)
296 .map(|(_, n)| (*n).to_string())
297 .unwrap_or_else(|| format!("iana_family_{}", self.family))
298 }
299
300 fn encode_value(&self) -> Result<Vec<u8>> {
301 let addr_bytes: Vec<u8> = match self.family {
302 1 => self
303 .address
304 .parse::<std::net::Ipv4Addr>()
305 .with_context(|| format!("'{}' is not an IPv4 address", self.address))?
306 .octets()
307 .to_vec(),
308 2 => self
309 .address
310 .parse::<std::net::Ipv6Addr>()
311 .with_context(|| format!("'{}' is not an IPv6 address", self.address))?
312 .octets()
313 .to_vec(),
314 6 => parse_mac(&self.address)?.to_vec(),
315 other => bail!("unsupported management address family {other}"),
316 };
317
318 let mut value = Vec::with_capacity(addr_bytes.len() + 8);
319 value.push((addr_bytes.len() + 1) as u8);
321 value.push(self.family);
322 value.extend_from_slice(&addr_bytes);
323 value.push(self.interface_numbering_subtype);
324 value.extend_from_slice(&self.interface_number.to_be_bytes());
325 value.push(0);
328 Ok(value)
329 }
330
331 fn decode_value(value: &[u8]) -> Result<Self> {
332 if value.len() < 9 {
333 bail!(
334 "management address TLV is {} octets, needs at least 9",
335 value.len()
336 );
337 }
338 let addr_string_len = value[0] as usize;
339 if addr_string_len == 0 {
340 bail!("management address TLV declares a zero-length address string");
341 }
342 if value.len() < 1 + addr_string_len + 6 {
343 bail!(
344 "management address TLV declares a {addr_string_len}-octet address string but \
345 carries only {} octets after it",
346 value.len() - 1
347 );
348 }
349 let family = value[1];
350 let addr = &value[2..1 + addr_string_len];
351 let address = match family {
352 1 if addr.len() == 4 => {
353 std::net::Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]).to_string()
354 }
355 2 if addr.len() == 16 => {
356 let mut octets = [0u8; 16];
357 octets.copy_from_slice(addr);
358 std::net::Ipv6Addr::from(octets).to_string()
359 }
360 6 if addr.len() == 6 => {
361 let mut mac = [0u8; 6];
362 mac.copy_from_slice(addr);
363 format_mac(&mac)
364 }
365 other => bail!(
368 "management address family {other} with a {}-octet address is not supported",
369 addr.len()
370 ),
371 };
372
373 let rest = &value[1 + addr_string_len..];
374 Ok(Self {
375 family,
376 address,
377 interface_numbering_subtype: rest[0],
378 interface_number: u32::from_be_bytes([rest[1], rest[2], rest[3], rest[4]]),
379 })
380 }
381}
382
383#[derive(Debug, Clone, PartialEq, Eq)]
385pub struct Lldpdu {
386 pub chassis_id_subtype: u8,
387 pub chassis_id: String,
388 pub port_id_subtype: u8,
389 pub port_id: String,
390 pub ttl: u16,
391 pub port_description: Option<String>,
392 pub system_name: Option<String>,
393 pub system_description: Option<String>,
394 pub capabilities: Option<(u16, u16)>,
396 pub management_address: Option<ManagementAddress>,
397}
398
399impl Lldpdu {
400 pub fn minimal(
402 chassis_id_subtype: u8,
403 chassis_id: impl Into<String>,
404 port_id_subtype: u8,
405 port_id: impl Into<String>,
406 ttl: u16,
407 ) -> Self {
408 Self {
409 chassis_id_subtype,
410 chassis_id: chassis_id.into(),
411 port_id_subtype,
412 port_id: port_id.into(),
413 ttl,
414 port_description: None,
415 system_name: None,
416 system_description: None,
417 capabilities: None,
418 management_address: None,
419 }
420 }
421
422 pub fn encode(&self) -> Result<Vec<u8>> {
424 let mut out = Vec::with_capacity(64);
425
426 push_tlv(
427 &mut out,
428 TLV_CHASSIS_ID,
429 &encode_id_value(IdKind::Chassis, self.chassis_id_subtype, &self.chassis_id)?,
430 )?;
431 push_tlv(
432 &mut out,
433 TLV_PORT_ID,
434 &encode_id_value(IdKind::Port, self.port_id_subtype, &self.port_id)?,
435 )?;
436 push_tlv(&mut out, TLV_TIME_TO_LIVE, &self.ttl.to_be_bytes())?;
437
438 if let Some(text) = &self.port_description {
439 push_text_tlv(
440 &mut out,
441 TLV_PORT_DESCRIPTION,
442 "port_description",
443 text,
444 false,
445 )?;
446 }
447 if let Some(text) = &self.system_name {
448 push_text_tlv(&mut out, TLV_SYSTEM_NAME, "system_name", text, false)?;
449 }
450 if let Some(text) = &self.system_description {
451 push_text_tlv(
452 &mut out,
453 TLV_SYSTEM_DESCRIPTION,
454 "system_description",
455 text,
456 true,
457 )?;
458 }
459 if let Some((supported, enabled)) = self.capabilities {
460 let mut value = Vec::with_capacity(4);
461 value.extend_from_slice(&supported.to_be_bytes());
462 value.extend_from_slice(&enabled.to_be_bytes());
463 push_tlv(&mut out, TLV_SYSTEM_CAPABILITIES, &value)?;
464 }
465 if let Some(mgmt) = &self.management_address {
466 push_tlv(&mut out, TLV_MANAGEMENT_ADDRESS, &mgmt.encode_value()?)?;
467 }
468
469 push_tlv(&mut out, TLV_END_OF_LLDPDU, &[])?;
471 Ok(out)
472 }
473
474 pub fn decode(data: &[u8]) -> Result<Self> {
480 let mut chassis: Option<(u8, String)> = None;
481 let mut port: Option<(u8, String)> = None;
482 let mut ttl: Option<u16> = None;
483 let mut port_description = None;
484 let mut system_name = None;
485 let mut system_description = None;
486 let mut capabilities = None;
487 let mut management_address = None;
488
489 let mut offset = 0usize;
490 let mut index = 0usize;
491
492 while offset < data.len() {
493 if offset + 2 > data.len() {
494 bail!("truncated TLV header at octet {offset}");
495 }
496 let header = u16::from_be_bytes([data[offset], data[offset + 1]]);
497 let tlv_type = (header >> 9) as u8;
498 let tlv_len = (header & 0x01FF) as usize;
499 offset += 2;
500
501 if offset + tlv_len > data.len() {
502 bail!(
503 "TLV type {tlv_type} declares {tlv_len} octets but only {} remain",
504 data.len() - offset
505 );
506 }
507 let value = &data[offset..offset + tlv_len];
508 offset += tlv_len;
509
510 match (index, tlv_type) {
511 (0, TLV_CHASSIS_ID) => {
512 chassis = Some(decode_id_value(IdKind::Chassis, value)?);
513 }
514 (1, TLV_PORT_ID) => {
515 port = Some(decode_id_value(IdKind::Port, value)?);
516 }
517 (2, TLV_TIME_TO_LIVE) => {
518 if value.len() != 2 {
519 bail!("Time To Live TLV must be 2 octets, got {}", value.len());
520 }
521 ttl = Some(u16::from_be_bytes([value[0], value[1]]));
522 }
523 (0, other) => {
524 bail!("first TLV must be Chassis ID (type {TLV_CHASSIS_ID}), got type {other}")
525 }
526 (1, other) => {
527 bail!("second TLV must be Port ID (type {TLV_PORT_ID}), got type {other}")
528 }
529 (2, other) => bail!(
530 "third TLV must be Time To Live (type {TLV_TIME_TO_LIVE}), got type {other}"
531 ),
532 (_, TLV_END_OF_LLDPDU) => break,
533 (_, TLV_PORT_DESCRIPTION) => {
534 port_description.get_or_insert_with(|| text_of(value));
535 }
536 (_, TLV_SYSTEM_NAME) => {
537 system_name.get_or_insert_with(|| identifier_text_of(value));
541 }
542 (_, TLV_SYSTEM_DESCRIPTION) => {
543 system_description.get_or_insert_with(|| text_of(value));
544 }
545 (_, TLV_SYSTEM_CAPABILITIES) => {
546 if value.len() != 4 {
547 bail!(
548 "System Capabilities TLV must be 4 octets, got {}",
549 value.len()
550 );
551 }
552 capabilities.get_or_insert((
553 u16::from_be_bytes([value[0], value[1]]),
554 u16::from_be_bytes([value[2], value[3]]),
555 ));
556 }
557 (_, TLV_MANAGEMENT_ADDRESS) => {
558 if management_address.is_none() {
562 management_address = ManagementAddress::decode_value(value).ok();
563 }
564 }
565 (_, _) => {}
567 }
568
569 index += 1;
570 }
571
572 let (chassis_id_subtype, chassis_id) =
573 chassis.ok_or_else(|| anyhow!("LLDPDU has no Chassis ID TLV"))?;
574 let (port_id_subtype, port_id) =
575 port.ok_or_else(|| anyhow!("LLDPDU has no Port ID TLV"))?;
576 let ttl = ttl.ok_or_else(|| anyhow!("LLDPDU has no Time To Live TLV"))?;
577
578 Ok(Self {
579 chassis_id_subtype,
580 chassis_id,
581 port_id_subtype,
582 port_id,
583 ttl,
584 port_description,
585 system_name,
586 system_description,
587 capabilities,
588 management_address,
589 })
590 }
591
592 pub fn to_event_data(&self) -> Map<String, Value> {
597 let mut map = Map::new();
598 map.insert("chassis_id".into(), json!(self.chassis_id));
599 map.insert(
600 "chassis_id_subtype".into(),
601 json!(subtype_name(IdKind::Chassis, self.chassis_id_subtype)),
602 );
603 map.insert(
604 "chassis_id_subtype_code".into(),
605 json!(self.chassis_id_subtype),
606 );
607 map.insert("port_id".into(), json!(self.port_id));
608 map.insert(
609 "port_id_subtype".into(),
610 json!(subtype_name(IdKind::Port, self.port_id_subtype)),
611 );
612 map.insert("port_id_subtype_code".into(), json!(self.port_id_subtype));
613 map.insert("ttl".into(), json!(self.ttl));
614
615 if let Some(v) = &self.port_description {
616 map.insert("port_description".into(), json!(v));
617 }
618 if let Some(v) = &self.system_name {
619 map.insert("system_name".into(), json!(v));
620 }
621 if let Some(v) = &self.system_description {
622 map.insert("system_description".into(), json!(v));
623 }
624 if let Some((supported, enabled)) = self.capabilities {
625 map.insert("capabilities".into(), json!(capability_names(supported)));
626 map.insert(
627 "capabilities_enabled".into(),
628 json!(capability_names(enabled)),
629 );
630 }
631 if let Some(mgmt) = &self.management_address {
632 map.insert("management_address".into(), json!(mgmt.address));
633 map.insert(
634 "management_address_family".into(),
635 json!(mgmt.family_name()),
636 );
637 map.insert(
638 "management_interface_number".into(),
639 json!(mgmt.interface_number),
640 );
641 }
642 map
643 }
644}
645
646#[derive(Debug, Clone, PartialEq, Eq)]
648pub struct LldpFrame {
649 pub destination_mac: [u8; 6],
650 pub source_mac: [u8; 6],
651 pub lldpdu: Lldpdu,
652}
653
654pub fn encode_frame(
656 destination_mac: [u8; 6],
657 source_mac: [u8; 6],
658 lldpdu: &Lldpdu,
659) -> Result<Vec<u8>> {
660 let body = lldpdu.encode()?;
661 let mut frame = Vec::with_capacity(ETHERNET_HEADER_LEN + body.len());
662 frame.extend_from_slice(&destination_mac);
663 frame.extend_from_slice(&source_mac);
664 frame.extend_from_slice(&LLDP_ETHERTYPE.to_be_bytes());
665 frame.extend_from_slice(&body);
666 Ok(frame)
667}
668
669pub fn decode_frame(frame: &[u8]) -> Result<LldpFrame> {
674 if frame.len() < ETHERNET_HEADER_LEN {
675 bail!(
676 "frame is {} octets, shorter than an Ethernet header",
677 frame.len()
678 );
679 }
680 let ethertype = u16::from_be_bytes([frame[12], frame[13]]);
681 if ethertype != LLDP_ETHERTYPE {
682 bail!("EtherType is 0x{ethertype:04x}, not LLDP's 0x88CC");
683 }
684 let mut destination_mac = [0u8; 6];
685 destination_mac.copy_from_slice(&frame[0..6]);
686 let mut source_mac = [0u8; 6];
687 source_mac.copy_from_slice(&frame[6..12]);
688
689 Ok(LldpFrame {
690 destination_mac,
691 source_mac,
692 lldpdu: Lldpdu::decode(&frame[ETHERNET_HEADER_LEN..])?,
693 })
694}
695
696#[derive(Debug, Clone, PartialEq, Eq)]
706pub struct AdvertisementRequest {
707 pub lldpdu: Lldpdu,
708 pub source_mac: Option<[u8; 6]>,
709 pub destination_mac: [u8; 6],
710}
711
712impl AdvertisementRequest {
713 pub fn from_action(action: &Value) -> Result<Self> {
719 let chassis_id = required_str(action, "chassis_id")?;
720 let port_id = required_str(action, "port_id")?;
721
722 let chassis_id_subtype = named_subtype(
723 IdKind::Chassis,
724 action.get("chassis_id_subtype").and_then(Value::as_str),
725 &chassis_id,
726 )?;
727 let port_id_subtype = named_subtype(
728 IdKind::Port,
729 action.get("port_id_subtype").and_then(Value::as_str),
730 &port_id,
731 )?;
732
733 let ttl = match action.get("ttl") {
734 None | Some(Value::Null) => 120u16,
735 Some(v) => {
736 let n = v
737 .as_u64()
738 .ok_or_else(|| anyhow!("'ttl' must be a whole number of seconds, got {v}"))?;
739 u16::try_from(n).map_err(|_| anyhow!("'ttl' must be 0-65535 seconds, got {n}"))?
740 }
741 };
742
743 let supported = string_array(action, "capabilities")?;
744 let enabled = string_array(action, "capabilities_enabled")?;
745 let capabilities = match (supported, enabled) {
746 (None, None) => None,
747 (Some(s), None) => {
748 let bits = capability_bits(&s)?;
749 Some((bits, bits))
753 }
754 (None, Some(e)) => {
755 let bits = capability_bits(&e)?;
756 Some((bits, bits))
757 }
758 (Some(s), Some(e)) => Some((capability_bits(&s)?, capability_bits(&e)?)),
759 };
760
761 let management_address = match action.get("management_address") {
762 None | Some(Value::Null) => None,
763 Some(v) => {
764 let addr = v
765 .as_str()
766 .ok_or_else(|| anyhow!("'management_address' must be a string, got {v}"))?;
767 let iface = match action.get("management_interface_number") {
768 None | Some(Value::Null) => 0u32,
769 Some(n) => u32::try_from(n.as_u64().ok_or_else(|| {
770 anyhow!("'management_interface_number' must be a whole number, got {n}")
771 })?)
772 .map_err(|_| anyhow!("'management_interface_number' must fit in 32 bits"))?,
773 };
774 Some(ManagementAddress::new(addr, iface)?)
775 }
776 };
777
778 let lldpdu = Lldpdu {
779 chassis_id_subtype,
780 chassis_id,
781 port_id_subtype,
782 port_id,
783 ttl,
784 port_description: optional_str(action, "port_description")?,
785 system_name: optional_str(action, "system_name")?,
786 system_description: optional_str(action, "system_description")?,
787 capabilities,
788 management_address,
789 };
790
791 let source_mac = match action.get("source_mac") {
792 None | Some(Value::Null) => None,
793 Some(v) => Some(parse_mac(v.as_str().ok_or_else(|| {
794 anyhow!("'source_mac' must be a MAC address string, got {v}")
795 })?)?),
796 };
797 let destination_mac = match action.get("destination_mac") {
798 None | Some(Value::Null) => LLDP_MULTICAST_MAC,
799 Some(v) => parse_mac(v.as_str().ok_or_else(|| {
800 anyhow!("'destination_mac' must be a MAC address string, got {v}")
801 })?)?,
802 };
803
804 lldpdu.encode()?;
807
808 Ok(Self {
809 lldpdu,
810 source_mac,
811 destination_mac,
812 })
813 }
814
815 pub fn to_frame(&self, fallback_source: [u8; 6]) -> Result<Vec<u8>> {
817 encode_frame(
818 self.destination_mac,
819 self.source_mac.unwrap_or(fallback_source),
820 &self.lldpdu,
821 )
822 }
823}
824
825fn push_tlv(out: &mut Vec<u8>, tlv_type: u8, value: &[u8]) -> Result<()> {
830 if value.len() > MAX_TLV_VALUE {
831 bail!(
832 "TLV type {tlv_type} value is {} octets; the 9-bit length field caps it at \
833 {MAX_TLV_VALUE}",
834 value.len()
835 );
836 }
837 let header = ((tlv_type as u16) << 9) | value.len() as u16;
838 out.extend_from_slice(&header.to_be_bytes());
839 out.extend_from_slice(value);
840 Ok(())
841}
842
843fn push_text_tlv(
851 out: &mut Vec<u8>,
852 tlv_type: u8,
853 field: &str,
854 text: &str,
855 multiline_is_real: bool,
856) -> Result<()> {
857 if !multiline_is_real {
858 reject_control_characters(field, text)?;
859 }
860 let bytes = text.as_bytes();
861 if bytes.len() > MAX_STRING_TLV_VALUE {
862 bail!(
863 "'{field}' is {} octets; IEEE 802.1AB caps it at {MAX_STRING_TLV_VALUE}",
864 bytes.len()
865 );
866 }
867 push_tlv(out, tlv_type, bytes)
868}
869
870fn reject_control_characters(field: &str, text: &str) -> Result<()> {
885 if let Some((index, bad)) = text.char_indices().find(|(_, c)| c.is_control()) {
886 bail!(
887 "'{field}' contains a control character (U+{:04X}) at byte {index}. LLDP text is \
888 copied verbatim into a neighbour's topology table and into log lines, so a newline \
889 or NUL there forges a whole neighbour entry. Send printable text only.",
890 bad as u32
891 );
892 }
893 Ok(())
894}
895
896fn encode_id_value(kind: IdKind, subtype: u8, value: &str) -> Result<Vec<u8>> {
899 if !kind.table().iter().any(|(c, _)| *c == subtype) {
900 bail!(
901 "{} subtype {subtype} is not defined by IEEE 802.1AB. Valid subtypes: {}",
902 kind.label(),
903 kind.table()
904 .iter()
905 .map(|(c, n)| format!("{n} ({c})"))
906 .collect::<Vec<_>>()
907 .join(", ")
908 );
909 }
910
911 let mut out = vec![subtype];
912 if subtype == kind.mac_subtype() {
913 out.extend_from_slice(&parse_mac(value).with_context(|| {
914 format!("{} subtype 'mac_address' needs a MAC address", kind.label())
915 })?);
916 } else if subtype == kind.network_address_subtype() {
917 if let Ok(v4) = value.trim().parse::<std::net::Ipv4Addr>() {
919 out.push(1);
920 out.extend_from_slice(&v4.octets());
921 } else if let Ok(v6) = value.trim().parse::<std::net::Ipv6Addr>() {
922 out.push(2);
923 out.extend_from_slice(&v6.octets());
924 } else {
925 bail!(
926 "{} subtype 'network_address' needs an IP address, got '{value}'",
927 kind.label()
928 );
929 }
930 } else {
931 if value.is_empty() {
932 bail!("{} must not be empty", kind.label());
933 }
934 reject_control_characters(kind.label(), value)?;
937 out.extend_from_slice(value.as_bytes());
938 }
939
940 if out.len() > MAX_STRING_TLV_VALUE {
941 bail!(
942 "{} is {} octets including its subtype; IEEE 802.1AB caps it at \
943 {MAX_STRING_TLV_VALUE}",
944 kind.label(),
945 out.len()
946 );
947 }
948 Ok(out)
949}
950
951fn decode_id_value(kind: IdKind, value: &[u8]) -> Result<(u8, String)> {
953 if value.is_empty() {
954 bail!("{} TLV carries no subtype octet", kind.label());
955 }
956 let subtype = value[0];
957 let body = &value[1..];
958 if body.is_empty() {
959 bail!("{} TLV carries a subtype and no identifier", kind.label());
960 }
961
962 let text = if subtype == kind.mac_subtype() {
963 if body.len() != 6 {
964 bail!(
965 "{} subtype 'mac_address' must carry 6 octets, got {}",
966 kind.label(),
967 body.len()
968 );
969 }
970 let mut mac = [0u8; 6];
971 mac.copy_from_slice(body);
972 format_mac(&mac)
973 } else if subtype == kind.network_address_subtype() {
974 match (body[0], body.len()) {
975 (1, 5) => std::net::Ipv4Addr::new(body[1], body[2], body[3], body[4]).to_string(),
976 (2, 17) => {
977 let mut octets = [0u8; 16];
978 octets.copy_from_slice(&body[1..]);
979 std::net::Ipv6Addr::from(octets).to_string()
980 }
981 (family, len) => bail!(
982 "{} subtype 'network_address' has IANA family {family} with {} address octets, \
983 which is not IPv4 or IPv6",
984 kind.label(),
985 len - 1
986 ),
987 }
988 } else {
989 identifier_text_of(body)
992 };
993
994 Ok((subtype, text))
995}
996
997fn text_of(value: &[u8]) -> String {
1006 String::from_utf8_lossy(value)
1007 .trim_end_matches('\0')
1008 .to_string()
1009}
1010
1011fn identifier_text_of(value: &[u8]) -> String {
1023 crate::utils::sanitize::line_field(&text_of(value))
1024}
1025
1026fn required_str(action: &Value, key: &str) -> Result<String> {
1027 match action.get(key) {
1028 Some(Value::String(s)) if !s.trim().is_empty() => Ok(s.clone()),
1029 Some(Value::String(_)) => bail!("'{key}' must not be empty"),
1030 Some(other) => bail!("'{key}' must be a string, got {other}"),
1031 None => bail!("'{key}' is required"),
1032 }
1033}
1034
1035fn optional_str(action: &Value, key: &str) -> Result<Option<String>> {
1036 match action.get(key) {
1037 None | Some(Value::Null) => Ok(None),
1038 Some(Value::String(s)) => Ok(Some(s.clone())),
1039 Some(other) => bail!("'{key}' must be a string, got {other}"),
1040 }
1041}
1042
1043fn string_array(action: &Value, key: &str) -> Result<Option<Vec<String>>> {
1044 match action.get(key) {
1045 None | Some(Value::Null) => Ok(None),
1046 Some(Value::Array(items)) => {
1047 let mut out = Vec::with_capacity(items.len());
1048 for item in items {
1049 match item {
1050 Value::String(s) => out.push(s.clone()),
1051 other => bail!("'{key}' must be an array of strings, found {other}"),
1052 }
1053 }
1054 Ok(Some(out))
1055 }
1056 Some(other) => bail!("'{key}' must be an array of strings, got {other}"),
1057 }
1058}
1059
1060fn named_subtype(kind: IdKind, name: Option<&str>, value: &str) -> Result<u8> {
1067 match name {
1068 Some(name) => subtype_code(kind, name).ok_or_else(|| {
1069 anyhow!(
1070 "'{}_subtype' value '{name}' is not an IEEE 802.1AB subtype. Valid names: {}",
1071 kind.label(),
1072 kind.table()
1073 .iter()
1074 .map(|(_, n)| *n)
1075 .collect::<Vec<_>>()
1076 .join(", ")
1077 )
1078 }),
1079 None if parse_mac(value).is_ok() => Ok(kind.mac_subtype()),
1080 None if value.trim().parse::<std::net::IpAddr>().is_ok() => {
1081 Ok(kind.network_address_subtype())
1082 }
1083 None => Ok(7),
1086 }
1087}