1use crate::llm::actions::{
14 protocol_trait::{ActionResult, Protocol, Server},
15 ActionDefinition, Parameter, ParameterDefinition,
16};
17use crate::protocol::log_template::LogTemplate;
18use crate::protocol::EventType;
19use crate::state::app_state::AppState;
20use anyhow::{Context, Result};
21use serde_json::json;
22use std::sync::LazyLock;
23
24use super::codec;
25
26#[cfg(any(
31 target_os = "macos",
32 target_os = "ios",
33 target_os = "freebsd",
34 target_os = "openbsd",
35 target_os = "netbsd",
36 target_os = "dragonfly"
37))]
38pub const DEFAULT_LOOPBACK_INTERFACE: &str = "lo0";
39#[cfg(not(any(
40 target_os = "macos",
41 target_os = "ios",
42 target_os = "freebsd",
43 target_os = "openbsd",
44 target_os = "netbsd",
45 target_os = "dragonfly"
46)))]
47pub const DEFAULT_LOOPBACK_INTERFACE: &str = "lo";
48
49pub const DEFAULT_SOURCE_MAC: &str = "02:00:00:00:00:01";
54
55pub struct LldpProtocol;
56
57impl Default for LldpProtocol {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl LldpProtocol {
64 pub fn new() -> Self {
65 Self
66 }
67
68 fn execute_send_advertisement(&self, action: serde_json::Value) -> Result<ActionResult> {
76 let request = codec::AdvertisementRequest::from_action(&action)
77 .context("send_lldp_advertisement was refused")?;
78
79 Ok(ActionResult::Custom {
82 name: LLDP_ADVERTISEMENT_RESULT.to_string(),
83 data: json!({
84 "chassis_id": request.lldpdu.chassis_id,
85 "port_id": request.lldpdu.port_id,
86 "ttl": request.lldpdu.ttl,
87 "action": action,
88 }),
89 })
90 }
91}
92
93pub const LLDP_ADVERTISEMENT_RESULT: &str = "lldp_advertisement";
95
96pub const NO_ADVERTISEMENT_ACTION: &str = "no_advertisement";
98
99impl Protocol for LldpProtocol {
100 fn default_binding(&self) -> Option<crate::protocol::BindingDefaults> {
101 Some(crate::protocol::BindingDefaults {
104 mac_address: None,
105 interface: Some(DEFAULT_LOOPBACK_INTERFACE.to_string()),
106 host: Some("127.0.0.1".to_string()),
107 port: Some(0),
108 })
109 }
110
111 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
112 vec![
114 ParameterDefinition {
115 name: "transport".to_string(),
116 type_hint: "string".to_string(),
117 description:
118 "How frames reach the wire. \"raw\" (the default) captures and injects real \
119 Ethernet frames through libpcap on the chosen interface and needs \
120 root/CAP_NET_RAW. \"udp\" is a TESTING transport: it carries complete \
121 Ethernet frames as UDP datagram payloads on the bound host/port so the \
122 whole event -> handler -> frame path can be exercised without privileges. \
123 It is not LLDP as any real neighbour speaks it — no switch will ever send \
124 or receive these datagrams."
125 .to_string(),
126 required: false,
127 example: json!("raw"),
128 default: None,
129 },
130 ParameterDefinition {
131 name: "udp_peer".to_string(),
132 type_hint: "string".to_string(),
133 description:
134 "TESTING only, and only with transport=\"udp\": HOST:PORT that advertisements \
135 are sent to. Without it, frames go back to the last peer heard from, which \
136 is enough to answer a neighbour but not to start a periodic advertisement \
137 before anyone has spoken."
138 .to_string(),
139 required: false,
140 example: json!("127.0.0.1:34567"),
141 default: None,
142 },
143 ParameterDefinition {
144 name: "advertise_interval_secs".to_string(),
145 type_hint: "integer".to_string(),
146 description:
147 "Seconds between lldp_advertise_due events, which is how NetGet advertises \
148 itself rather than only answering neighbours. 0 (the default) disables the \
149 timer entirely and the server is a pure listener. IEEE 802.1AB's default is \
150 30, with a TTL of 120."
151 .to_string(),
152 required: false,
153 example: json!(30),
154 default: None,
155 },
156 ParameterDefinition {
157 name: "source_mac".to_string(),
158 type_hint: "string".to_string(),
159 description: format!(
160 "Ethernet source address for frames this server transmits, used when the \
161 action does not name one. Defaults to the interface's own address where it \
162 can be read, and to {DEFAULT_SOURCE_MAC} (locally administered) otherwise — \
163 which is always the case for transport=\"udp\", where there is no interface."
164 ),
165 required: false,
166 example: json!("02:00:00:00:00:01"),
167 default: None,
168 },
169 ]
170 }
171
172 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
173 Vec::new()
176 }
177
178 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
179 vec![send_lldp_advertisement_action(), no_advertisement_action()]
180 }
181
182 fn protocol_name(&self) -> &'static str {
183 "LLDP"
184 }
185
186 fn get_event_types(&self) -> Vec<EventType> {
187 get_lldp_event_types()
188 }
189
190 fn stack_name(&self) -> &'static str {
191 "ETH>LLDP"
192 }
193
194 fn keywords(&self) -> Vec<&'static str> {
195 vec!["lldp", "link layer discovery", "802.1ab"]
196 }
197
198 fn startup_privilege_requirement(
199 &self,
200
201 startup_params: Option<&serde_json::Value>,
202 ) -> crate::protocol::metadata::PrivilegeRequirement {
203 crate::llm::actions::protocol_trait::unless_udp_transport(
206 self.metadata().privilege_requirement,
207 startup_params,
208 )
209 }
210
211 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
212 use crate::protocol::metadata::{
213 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
214 };
215
216 ProtocolMetadataV2::builder()
217 .state(DevelopmentState::Experimental)
218 .privilege_requirement(PrivilegeRequirement::PacketCapture)
222 .connectionless()
223 .deliberately_silent()
227 .implementation(
228 "Hand-written IEEE 802.1AB TLV codec (src/server/lldp/codec.rs), pure and \
229 transport-free; libpcap capture/injection on EtherType 0x88CC for the real \
230 transport, plus a UDP test transport that carries whole Ethernet frames.",
231 )
232 .llm_control(
233 "The entire advertised identity: chassis ID and subtype, port ID and subtype, \
234 TTL, port/system descriptions, system capabilities and management address. \
235 NetGet impersonates whatever device the model describes.",
236 )
237 .e2e_testing(
238 "tests/server/lldp/codec_test.rs encodes against hand-computed IEEE 802.1AB \
239 bytes and decodes literal frames, including the mandatory/capability/management \
240 TLVs of a real-world Extreme Summit300-48 capture. \
241 tests/server/lldp/e2e_test.rs drives the full event -> handler/LLM -> frame \
242 path over the UDP test transport in-process, including the silence case. \
243 Nothing has ever run the pcap transport. \
244 NO THIRD-PARTY PEER, and this was investigated rather than assumed \
245 (September 2026): `lldpcli` is installed on this machine and CANNOT be \
246 pointed at NetGet. It is not an LLDP speaker at all — lldpcli(8) is the \
247 control program for a local `lldpd` daemon and reaches it over the unix \
248 socket /opt/homebrew/var/run/lldpd.socket, which is its only transport (-u \
249 changes the socket path; there is no host or port option, because there is no \
250 wire protocol on that side). The real peer would have to be `lldpd` itself, \
251 which needs root, a real Ethernet interface and NetGet's libpcap transport — \
252 and a test needing root could only be #[ignore]d or skip-gated, neither of \
253 which is evidence. See src/server/lldp/CLAUDE.md for the feth-pair experiment \
254 that would earn Beta.",
255 )
256 .notes(
257 "PROVEN: the TLV codec, against literal IEEE 802.1AB-2016 byte layouts and a \
258 real-world capture, in both directions. NOT PROVEN: the raw-Ethernet transport, \
259 which needs root/CAP_NET_RAW and has never been executed anywhere — no frame \
260 this code produced has reached a real LLDP neighbour, and no third-party LLDP \
261 peer is runnable in the environment that tests it. Deliberately silent on LLM \
262 failure: every LLDP frame asserts that a device with a given identity exists on \
263 this link, so a fabricated one poisons a neighbour's topology table. The \
264 failure is recorded with a decision= tag in the log instead. See \
265 src/server/lldp/CLAUDE.md for the feth-pair experiment that would earn Beta.",
266 )
267 .build()
268 }
269
270 fn description(&self) -> &'static str {
271 "LLDP (IEEE 802.1AB) link-layer discovery — advertise a device identity to neighbours"
272 }
273
274 fn example_prompt(&self) -> &'static str {
275 "Be an LLDP agent on en0 impersonating a Cisco Catalyst switch: answer every neighbour \
276 advertisement with our own on port GigabitEthernet0/1"
277 }
278
279 fn group_name(&self) -> &'static str {
280 "Core"
281 }
282
283 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
284 use crate::llm::actions::StartupExamples;
285 StartupExamples::new(
286 json!({
288 "type": "open_server",
289 "base_stack": "lldp",
290 "interface": "en0",
291 "instruction": "You are an LLDP agent impersonating an Extreme Summit300-48 \
292 switch. Answer each neighbour advertisement with our own on \
293 port 1/1, capabilities bridge and router, TTL 120."
294 }),
295 json!({
297 "type": "open_server",
298 "base_stack": "lldp",
299 "interface": "en0",
300 "startup_params": {"advertise_interval_secs": 30},
301 "event_handlers": [{
302 "event_pattern": "lldp_*",
303 "handler": {
304 "type": "script",
305 "language": "python",
306 "code": "respond([{'type': 'send_lldp_advertisement', 'chassis_id': '02:00:00:00:00:01', 'chassis_id_subtype': 'mac_address', 'port_id': 'ge-0/0/1', 'port_id_subtype': 'interface_name', 'ttl': 120, 'system_name': 'netget-lab', 'capabilities': ['bridge', 'router']}])"
307 }
308 }]
309 }),
310 json!({
312 "type": "open_server",
313 "base_stack": "lldp",
314 "interface": "en0",
315 "event_handlers": [{
316 "event_pattern": "lldp_neighbor_advertisement",
317 "handler": {
318 "type": "static",
319 "actions": [{
320 "type": "send_lldp_advertisement",
321 "chassis_id": "02:00:00:00:00:01",
322 "chassis_id_subtype": "mac_address",
323 "port_id": "1/1",
324 "port_id_subtype": "interface_name",
325 "ttl": 120,
326 "system_name": "netget-lab",
327 "system_description": "NetGet LLDP agent",
328 "capabilities": ["bridge", "router"]
329 }]
330 }
331 }]
332 }),
333 )
334 }
335}
336
337impl Server for LldpProtocol {
338 fn spawn(
339 &self,
340 ctx: crate::protocol::SpawnContext,
341 ) -> std::pin::Pin<
342 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
343 > {
344 Box::pin(async move {
345 use super::LldpServer;
346 let listen_addr = ctx
347 .socket_addr()
348 .unwrap_or_else(|| ctx.legacy_listen_addr());
349 LldpServer::spawn_with_llm_actions(
350 ctx.interface.clone(),
351 listen_addr,
352 ctx.llm_client,
353 ctx.state,
354 ctx.status_tx,
355 ctx.server_id,
356 ctx.startup_params,
357 )
358 .await
359 })
360 }
361
362 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
363 let action_type = action
364 .get("type")
365 .and_then(|v| v.as_str())
366 .context("Missing 'type' field in action")?;
367
368 match action_type {
369 "send_lldp_advertisement" => self.execute_send_advertisement(action),
370 NO_ADVERTISEMENT_ACTION => Ok(ActionResult::NoAction),
374 other => Err(anyhow::anyhow!("Unknown LLDP action: {}", other)),
375 }
376 }
377}
378
379fn subtype_list(kind: codec::IdKind) -> String {
384 let table = match kind {
385 codec::IdKind::Chassis => codec::CHASSIS_ID_SUBTYPES,
386 codec::IdKind::Port => codec::PORT_ID_SUBTYPES,
387 };
388 table
389 .iter()
390 .map(|(_, n)| *n)
391 .collect::<Vec<_>>()
392 .join(" | ")
393}
394
395fn capability_list() -> String {
396 codec::SYSTEM_CAPABILITIES
397 .iter()
398 .map(|(_, n)| *n)
399 .collect::<Vec<_>>()
400 .join(", ")
401}
402
403fn send_lldp_advertisement_action() -> ActionDefinition {
404 ActionDefinition {
405 name: "send_lldp_advertisement".to_string(),
406 description:
407 "Transmit one LLDP advertisement describing the device NetGet is impersonating. \
408 Every field is structured — there is no way to send raw TLV octets, deliberately. \
409 Remember what an advertisement means: the neighbour records this identity in its \
410 topology table and shows it to an operator, so send one only when you intend to \
411 claim that a device with these details exists on this link. Use no_advertisement \
412 otherwise."
413 .to_string(),
414 parameters: vec![
415 Parameter {
416 name: "chassis_id".to_string(),
417 type_hint: "string".to_string(),
418 description:
419 "Identifier of the whole device: a MAC address (00:1b:21:3c:4d:5e), an IP \
420 address, or free text, matching chassis_id_subtype."
421 .to_string(),
422 required: true,
423 },
424 Parameter {
425 name: "chassis_id_subtype".to_string(),
426 type_hint: "string".to_string(),
427 description: format!(
428 "How chassis_id should be read: {}. Defaults to mac_address or \
429 network_address when the value looks like one, and to 'local' otherwise. \
430 Note the numbering differs from port_id_subtype.",
431 subtype_list(codec::IdKind::Chassis)
432 ),
433 required: false,
434 },
435 Parameter {
436 name: "port_id".to_string(),
437 type_hint: "string".to_string(),
438 description:
439 "Identifier of the port this advertisement is sent from: an interface name \
440 (GigabitEthernet0/1), a MAC address, or free text."
441 .to_string(),
442 required: true,
443 },
444 Parameter {
445 name: "port_id_subtype".to_string(),
446 type_hint: "string".to_string(),
447 description: format!(
448 "How port_id should be read: {}. Same defaulting as chassis_id_subtype.",
449 subtype_list(codec::IdKind::Port)
450 ),
451 required: false,
452 },
453 Parameter {
454 name: "ttl".to_string(),
455 type_hint: "integer".to_string(),
456 description:
457 "Seconds the neighbour should keep this entry, 0-65535 (default 120). 0 is a \
458 shutdown notice: it tells the neighbour to delete us immediately."
459 .to_string(),
460 required: false,
461 },
462 Parameter {
463 name: "port_description".to_string(),
464 type_hint: "string".to_string(),
465 description: "Human-readable port description, up to 255 octets.".to_string(),
466 required: false,
467 },
468 Parameter {
469 name: "system_name".to_string(),
470 type_hint: "string".to_string(),
471 description: "The device's administratively assigned name (its hostname)."
472 .to_string(),
473 required: false,
474 },
475 Parameter {
476 name: "system_description".to_string(),
477 type_hint: "string".to_string(),
478 description:
479 "Free text describing the device — typically vendor, model and firmware \
480 version. This is the field network reconnaissance reads, so choose it as \
481 deliberately as the rest."
482 .to_string(),
483 required: false,
484 },
485 Parameter {
486 name: "capabilities".to_string(),
487 type_hint: "array".to_string(),
488 description: format!(
489 "Capabilities the device supports, as names: {}. Used for capabilities_enabled \
490 too when that is omitted.",
491 capability_list()
492 ),
493 required: false,
494 },
495 Parameter {
496 name: "capabilities_enabled".to_string(),
497 type_hint: "array".to_string(),
498 description: "Subset of 'capabilities' currently switched on. Omit to claim that \
499 everything supported is enabled."
500 .to_string(),
501 required: false,
502 },
503 Parameter {
504 name: "management_address".to_string(),
505 type_hint: "string".to_string(),
506 description:
507 "Address the device is managed on: an IPv4, IPv6 or MAC address. The family \
508 is deduced from the notation."
509 .to_string(),
510 required: false,
511 },
512 Parameter {
513 name: "management_interface_number".to_string(),
514 type_hint: "integer".to_string(),
515 description: "ifIndex the management address lives on (default 0).".to_string(),
516 required: false,
517 },
518 Parameter {
519 name: "source_mac".to_string(),
520 type_hint: "string".to_string(),
521 description:
522 "Ethernet source address of the frame. Defaults to the server's interface \
523 address; set it only to impersonate a specific device at layer 2."
524 .to_string(),
525 required: false,
526 },
527 Parameter {
528 name: "destination_mac".to_string(),
529 type_hint: "string".to_string(),
530 description:
531 "Ethernet destination (default 01:80:c2:00:00:0e, the nearest-bridge group \
532 address every LLDP agent transmits to). Change it only for a directed \
533 frame to one neighbour."
534 .to_string(),
535 required: false,
536 },
537 ],
538 example: json!({
539 "type": "send_lldp_advertisement",
540 "chassis_id": "00:1b:21:3c:4d:5e",
541 "chassis_id_subtype": "mac_address",
542 "port_id": "GigabitEthernet0/1",
543 "port_id_subtype": "interface_name",
544 "ttl": 120,
545 "port_description": "Uplink to core",
546 "system_name": "edge-sw-01",
547 "system_description": "Cisco IOS Software, C2960 Software, Version 15.0(2)SE11",
548 "capabilities": ["bridge", "router"],
549 "capabilities_enabled": ["bridge"],
550 "management_address": "192.0.2.10"
551 }),
552 log_template: Some(
553 LogTemplate::new()
554 .with_info("-> LLDP {system_name} chassis={chassis_id} port={port_id} ttl={ttl}")
555 .with_debug(
556 "LLDP send_lldp_advertisement: chassis={chassis_id} ({chassis_id_subtype}) \
557 port={port_id} ({port_id_subtype}) ttl={ttl}",
558 )
559 .with_trace("LLDP advertisement: {json_pretty(.)}"),
560 ),
561 }
562}
563
564fn no_advertisement_action() -> ActionDefinition {
565 ActionDefinition {
566 name: NO_ADVERTISEMENT_ACTION.to_string(),
567 description:
568 "Say nothing. LLDP has no error or refusal frame — every frame it defines asserts \
569 that a device exists — so this is how a decision not to advertise is expressed. It \
570 is recorded in the log as decision=model_reject, which is what distinguishes it \
571 from having produced no answer at all."
572 .to_string(),
573 parameters: vec![Parameter {
574 name: "reason".to_string(),
575 type_hint: "string".to_string(),
576 description: "Why nothing is being advertised. Logged, never transmitted.".to_string(),
577 required: false,
578 }],
579 example: json!({
580 "type": "no_advertisement",
581 "reason": "This neighbour is not on a link we advertise to"
582 }),
583 log_template: Some(
584 LogTemplate::new()
585 .with_info("LLDP staying silent: {reason}")
586 .with_debug("LLDP no_advertisement: {reason}"),
587 ),
588 }
589}
590
591pub static LLDP_NEIGHBOR_ADVERTISEMENT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
597 EventType::new(
598 "lldp_neighbor_advertisement",
599 "An LLDP neighbour advertised itself on this link. Every field is already decoded from \
600 its TLVs.",
601 json!({
602 "type": "send_lldp_advertisement",
603 "chassis_id": "02:00:00:00:00:01",
604 "chassis_id_subtype": "mac_address",
605 "port_id": "1/1",
606 "port_id_subtype": "interface_name",
607 "ttl": 120,
608 "system_name": "netget-lab",
609 "capabilities": ["bridge", "router"]
610 }),
611 )
612 .with_parameters(vec![
613 Parameter {
614 name: "source_mac".to_string(),
615 type_hint: "string".to_string(),
616 description: "Ethernet source address the advertisement came from".to_string(),
617 required: true,
618 },
619 Parameter {
620 name: "chassis_id".to_string(),
621 type_hint: "string".to_string(),
622 description: "The neighbour's chassis identifier, in the notation its subtype implies"
623 .to_string(),
624 required: true,
625 },
626 Parameter {
627 name: "chassis_id_subtype".to_string(),
628 type_hint: "string".to_string(),
629 description: "How to read chassis_id (mac_address, network_address, local, ...)"
630 .to_string(),
631 required: true,
632 },
633 Parameter {
634 name: "port_id".to_string(),
635 type_hint: "string".to_string(),
636 description: "The neighbour's port identifier".to_string(),
637 required: true,
638 },
639 Parameter {
640 name: "port_id_subtype".to_string(),
641 type_hint: "string".to_string(),
642 description: "How to read port_id".to_string(),
643 required: true,
644 },
645 Parameter {
646 name: "ttl".to_string(),
647 type_hint: "integer".to_string(),
648 description: "Seconds the neighbour asks us to keep its entry; 0 means it is leaving"
649 .to_string(),
650 required: true,
651 },
652 Parameter {
653 name: "system_name".to_string(),
654 type_hint: "string".to_string(),
655 description: "The neighbour's hostname, if it sent one".to_string(),
656 required: false,
657 },
658 Parameter {
659 name: "system_description".to_string(),
660 type_hint: "string".to_string(),
661 description: "Vendor/model/firmware text, if it sent any".to_string(),
662 required: false,
663 },
664 Parameter {
665 name: "capabilities".to_string(),
666 type_hint: "array".to_string(),
667 description: "Capability names the neighbour supports".to_string(),
668 required: false,
669 },
670 Parameter {
671 name: "capabilities_enabled".to_string(),
672 type_hint: "array".to_string(),
673 description: "Capability names the neighbour has switched on".to_string(),
674 required: false,
675 },
676 Parameter {
677 name: "management_address".to_string(),
678 type_hint: "string".to_string(),
679 description: "Address the neighbour says it is managed on".to_string(),
680 required: false,
681 },
682 ])
683 .with_log_template(
684 LogTemplate::new()
685 .with_info("LLDP neighbour {system_name} ({chassis_id}) on port {port_id}")
686 .with_debug(
687 "LLDP advertisement from {source_mac}: chassis={chassis_id} port={port_id} \
688 ttl={ttl}",
689 )
690 .with_trace("LLDP neighbour: {json_pretty(.)}"),
691 )
692 .with_actions(vec![
693 send_lldp_advertisement_action(),
694 no_advertisement_action(),
695 ])
696 .with_alternative_example(json!({
697 "type": "no_advertisement",
698 "reason": "Listening only; this link is not one we advertise on"
699 }))
700});
701
702pub static LLDP_ADVERTISE_DUE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
704 EventType::new(
705 "lldp_advertise_due",
706 "The periodic advertise timer fired (advertise_interval_secs). This is how NetGet \
707 announces itself rather than only answering neighbours; nothing was received.",
708 json!({
709 "type": "send_lldp_advertisement",
710 "chassis_id": "02:00:00:00:00:01",
711 "chassis_id_subtype": "mac_address",
712 "port_id": "1/1",
713 "port_id_subtype": "interface_name",
714 "ttl": 120,
715 "system_name": "netget-lab",
716 "system_description": "NetGet LLDP agent",
717 "capabilities": ["bridge", "router"]
718 }),
719 )
720 .with_parameters(vec![
721 Parameter {
722 name: "interval_secs".to_string(),
723 type_hint: "integer".to_string(),
724 description: "The configured interval between these events".to_string(),
725 required: true,
726 },
727 Parameter {
728 name: "sequence".to_string(),
729 type_hint: "integer".to_string(),
730 description: "How many times the timer has fired since the server started".to_string(),
731 required: true,
732 },
733 Parameter {
734 name: "interface".to_string(),
735 type_hint: "string".to_string(),
736 description: "Interface the advertisement would be transmitted on".to_string(),
737 required: false,
738 },
739 ])
740 .with_log_template(
741 LogTemplate::new()
742 .with_info("LLDP advertise timer #{sequence}")
743 .with_debug("LLDP advertise_due: sequence={sequence} interval={interval_secs}s"),
744 )
745 .with_actions(vec![
746 send_lldp_advertisement_action(),
747 no_advertisement_action(),
748 ])
749 .with_alternative_example(json!({
750 "type": "no_advertisement",
751 "reason": "Nothing to announce yet"
752 }))
753});
754
755pub fn get_lldp_event_types() -> Vec<EventType> {
756 vec![
757 LLDP_NEIGHBOR_ADVERTISEMENT_EVENT.clone(),
758 LLDP_ADVERTISE_DUE_EVENT.clone(),
759 ]
760}