Skip to main content

netget/server/isis/
actions.rs

1//! IS-IS protocol actions implementation
2
3use crate::llm::actions::{
4    protocol_trait::{ActionResult, Protocol, Server},
5    ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14/// Name of the loopback interface on this platform.
15///
16/// BSD-derived systems (macOS included) call it `lo0`; Linux calls it `lo`. IS-IS used
17/// to hardcode `"lo"`, so on macOS its declared default named a device that does not
18/// exist and a start with no explicit `interface` could only ever fail with
19/// `no such capture device 'lo'` — a worse diagnosis than the truth, and one that would
20/// mislead anyone who did have capture rights. ARP, DataLink and ICMP each carry this
21/// same constant; IS-IS was missed, exactly as it was missed for the
22/// startup-reports-failure fix (see this protocol's CLAUDE.md).
23///
24/// Loopback remains a poor place to actually run IS-IS — nothing speaks it there — but a
25/// default that cannot resolve is not a useful way to say so.
26#[cfg(any(
27    target_os = "macos",
28    target_os = "ios",
29    target_os = "freebsd",
30    target_os = "openbsd",
31    target_os = "netbsd",
32    target_os = "dragonfly"
33))]
34const DEFAULT_LOOPBACK_INTERFACE: &str = "lo0";
35#[cfg(not(any(
36    target_os = "macos",
37    target_os = "ios",
38    target_os = "freebsd",
39    target_os = "openbsd",
40    target_os = "netbsd",
41    target_os = "dragonfly"
42)))]
43const DEFAULT_LOOPBACK_INTERFACE: &str = "lo";
44
45pub struct IsisProtocol;
46
47impl IsisProtocol {
48    pub fn new() -> Self {
49        Self
50    }
51}
52
53// Implement Protocol trait (common functionality)
54impl Protocol for IsisProtocol {
55    fn default_binding(&self) -> Option<crate::protocol::BindingDefaults> {
56        // IS-IS uses interface-based binding (loopback by default)
57        Some(crate::protocol::BindingDefaults::interface_based(
58            DEFAULT_LOOPBACK_INTERFACE,
59        ))
60    }
61
62    fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
63        // Interface is now provided via flexible binding system
64        vec![
65                crate::llm::actions::ParameterDefinition {
66                    name: "system_id".to_string(),
67                    type_hint: "string".to_string(),
68                    description: "IS-IS System ID in format: 0000.0000.0001 (6 bytes in dotted hex notation)".to_string(),
69                    required: false,
70                    example: json!("0000.0000.0001"),
71                    default: None,
72                },
73                crate::llm::actions::ParameterDefinition {
74                    name: "area_id".to_string(),
75                    type_hint: "string".to_string(),
76                    description: "IS-IS Area ID in format: 49.0001 (NSAP format, 49 for private networks)".to_string(),
77                    required: false,
78                    example: json!("49.0001"),
79                    default: None,
80                },
81                crate::llm::actions::ParameterDefinition {
82                    name: "level".to_string(),
83                    type_hint: "string".to_string(),
84                    description: "IS-IS level: 'level-1' (intra-area), 'level-2' (inter-area backbone), or 'level-1+2' (both)".to_string(),
85                    required: false,
86                    example: json!("level-2"),
87                    default: None,
88                },
89            ]
90    }
91    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
92        Vec::new()
93    }
94    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
95        vec![
96            send_isis_hello_action(),
97            send_isis_lsp_action(),
98            send_isis_pdu_action(),
99            ignore_pdu_action(),
100        ]
101    }
102    fn protocol_name(&self) -> &'static str {
103        "ISIS"
104    }
105    fn get_event_types(&self) -> Vec<EventType> {
106        get_isis_event_types()
107    }
108    fn stack_name(&self) -> &'static str {
109        // Layer 2 only: Ethernet 802.3 + LLC (DSAP/SSAP 0xFE), never IP or UDP. The old
110        // "ETH>IP>UDP>ISIS" here was a leftover from a UDP-encapsulated draft that no longer
111        // exists in this module, and it told the model IS-IS rides on a transport it does not.
112        "ETH>LLC>ISIS"
113    }
114    fn keywords(&self) -> Vec<&'static str> {
115        vec!["isis", "is-is"]
116    }
117    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
118        use crate::protocol::metadata::{
119            DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
120        };
121
122        ProtocolMetadataV2::builder()
123            .connectionless()
124            .state(DevelopmentState::Experimental)
125            // Deliberately silent: An IS-IS PDU asserts adjacency and topology; a fabricated
126            // one is injected into a real routing domain.
127            .deliberately_silent()
128            .privilege_requirement(PrivilegeRequirement::PacketCapture)
129            .implementation("Layer 2 IS-IS with pcap (ISO/IEC 10589, RFC 1195)")
130            .llm_control("Optional: whether to engage with an IS-IS speaker (answer a Hello, form an adjacency, act as a honeypot) is a policy decision. With no operator policy (no instruction, no handler) the server observes passively and does NOT respond, with no LLM round-trip per PDU. When the operator opts in, the LLM decides the Hello/LSP responses")
131            .e2e_testing(
132                "tests/capture_startup_reports_failure_test.rs asserts spawn() returns Err for an \
133                 unknown device and for missing capture privilege. PDU handling itself is \
134                 untested: tests/server/isis/e2e_test.rs is #[ignore]d for root.",
135            )
136            .notes(
137                "Layer 2 over Ethernet+LLC (DSAP/SSAP 0xFE), not UDP. Startup failure is \
138                 reported: spawn() awaits the pcap handle and the BPF filter and returns Err, \
139                 so a privilege failure lands in ServerStatus::Error rather than Running. \
140                 NEVER validated against a real router (FRR/Cisco) or any IS-IS speaker - the \
141                 earlier 'interoperable with real routers' claim was unfounded. Whether to \
142                 respond is policy, so with no operator policy the server is a passive listener \
143                 (no response, no LLM call); this passive default is compile-verified since the \
144                 pcap path needs root and the E2E suite is #[ignore]d. LSP/CSNP/PSNP are parsed \
145                 and logged but not handled; there is no LSP database or SPF.",
146            )
147            .build()
148    }
149    fn description(&self) -> &'static str {
150        "IS-IS (Intermediate System to Intermediate System) Layer 2 routing protocol server"
151    }
152    fn example_prompt(&self) -> &'static str {
153        "start an is-is router on interface eth0 with system-id 0000.0000.0001 in area 49.0001 at level-2"
154    }
155    fn group_name(&self) -> &'static str {
156        "Experimental"
157    }
158
159    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
160        use crate::llm::actions::StartupExamples;
161
162        // Deterministic: answer every IS-IS hello with our own hello, no LLM
163        // call.
164        let script = r#"import json, sys
165data = json.load(sys.stdin)
166event = data["event"]
167if data["event_type_id"] == "isis_hello":
168    actions = [{"type": "send_isis_hello",
169                "system_id": "0000.0000.0001", "area_id": "49.0001"}]
170else:
171    actions = []
172print(json.dumps({"actions": actions}))"#;
173
174        StartupExamples::new(
175            // LLM mode: LLM handles IS-IS adjacency formation
176            json!({
177                "type": "open_server",
178                "port": 0,
179                "base_stack": "isis",
180                "instruction": "IS-IS router with system-id 0000.0000.0001 in area 49.0001. Respond to Hello PDUs and form adjacencies with neighbors."
181            }),
182            // Script mode: Scripted IS-IS Hello response
183            json!({
184                "type": "open_server",
185                "port": 0,
186                "base_stack": "isis",
187                "event_handlers": [{
188                    "event_pattern": "isis_hello",
189                    "handler": {
190                        "type": "script",
191                        "language": "python",
192                        "code": script
193                    }
194                }]
195            }),
196            // Static mode: Fixed IS-IS Hello response
197            json!({
198                "type": "open_server",
199                "port": 0,
200                "base_stack": "isis",
201                "event_handlers": [{
202                    "event_pattern": "isis_hello",
203                    "handler": {
204                        "type": "static",
205                        "actions": [{
206                            "type": "send_isis_hello",
207                            "pdu_type": "lan_hello_l2",
208                            "system_id": "0000.0000.0001",
209                            "area_id": "49.0001",
210                            "holding_time": 30
211                        }]
212                    }
213                }]
214            }),
215        )
216    }
217}
218
219// Implement Server trait (server-specific functionality)
220impl Server for IsisProtocol {
221    fn spawn(
222        &self,
223        ctx: crate::protocol::SpawnContext,
224    ) -> std::pin::Pin<
225        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
226    > {
227        Box::pin(async move {
228            use crate::server::isis::IsisServer;
229
230            // IS-IS uses interface-based binding
231            // Extract interface from context (defaults already applied)
232            let interface = ctx
233                .interface()
234                .context("IS-IS requires network interface")?
235                .to_string();
236
237            // Get listen addr before consuming ctx fields
238            let listen_addr = ctx.legacy_listen_addr();
239
240            // Spawn the IS-IS server
241            let _interface_name = IsisServer::spawn_with_llm_actions(
242                interface,
243                ctx.llm_client,
244                ctx.state,
245                ctx.status_tx,
246                ctx.server_id,
247                ctx.startup_params,
248            )
249            .await?;
250
251            // IS-IS doesn't bind to a socket, so return a dummy address
252            Ok(listen_addr)
253        })
254    }
255    fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
256        let action_type = action
257            .get("type")
258            .and_then(|v| v.as_str())
259            .context("Missing 'type' field in action")?;
260
261        match action_type {
262            "send_isis_hello" => self.execute_send_isis_hello(action),
263            "send_isis_lsp" => self.execute_send_isis_lsp(action),
264            "send_isis_pdu" => self.execute_send_isis_pdu(action),
265            "ignore_pdu" => Ok(ActionResult::NoAction),
266            _ => Err(anyhow::anyhow!("Unknown IS-IS action: {}", action_type)),
267        }
268    }
269}
270
271impl IsisProtocol {
272    /// Execute send_isis_hello action - Send IS-IS Hello PDU
273    fn execute_send_isis_hello(&self, action: serde_json::Value) -> Result<ActionResult> {
274        // Extract parameters
275        let pdu_type_str = action
276            .get("pdu_type")
277            .and_then(|v| v.as_str())
278            .unwrap_or("lan_hello_l2");
279
280        let pdu_type = match pdu_type_str {
281            "lan_hello_l1" => 15,
282            "lan_hello_l2" => 16,
283            "p2p_hello" => 17,
284            _ => 16, // Default to L2 LAN Hello
285        };
286
287        let system_id = action
288            .get("system_id")
289            .and_then(|v| v.as_str())
290            .unwrap_or("0000.0000.0001");
291
292        let area_id = action
293            .get("area_id")
294            .and_then(|v| v.as_str())
295            .unwrap_or("49.0001");
296
297        let holding_time = action
298            .get("holding_time")
299            .and_then(|v| v.as_u64())
300            .unwrap_or(30) as u16;
301
302        // Build IS-IS Hello PDU
303        let packet = Self::build_isis_hello(pdu_type, system_id, area_id, holding_time)?;
304
305        Ok(ActionResult::Output(packet))
306    }
307
308    /// Execute send_isis_lsp action - Send IS-IS Link State PDU
309    fn execute_send_isis_lsp(&self, action: serde_json::Value) -> Result<ActionResult> {
310        let level = action
311            .get("level")
312            .and_then(|v| v.as_str())
313            .unwrap_or("level-2");
314
315        let pdu_type = match level {
316            "level-1" => 18,
317            "level-2" => 20,
318            _ => 20,
319        };
320
321        let system_id = action
322            .get("system_id")
323            .and_then(|v| v.as_str())
324            .unwrap_or("0000.0000.0001");
325
326        // Build simplified LSP (full implementation would require route information)
327        let packet = Self::build_isis_lsp(pdu_type, system_id)?;
328
329        Ok(ActionResult::Output(packet))
330    }
331
332    /// Execute send_isis_pdu action - Send raw IS-IS PDU from hex
333    fn execute_send_isis_pdu(&self, action: serde_json::Value) -> Result<ActionResult> {
334        let data = action
335            .get("data")
336            .and_then(|v| v.as_str())
337            .context("Missing 'data' parameter")?;
338
339        let bytes = hex::decode(data).context("Invalid hex data")?;
340
341        Ok(ActionResult::Output(bytes))
342    }
343
344    /// Build IS-IS Hello PDU
345    pub fn build_isis_hello(
346        pdu_type: u8,
347        system_id: &str,
348        area_id: &str,
349        holding_time: u16,
350    ) -> Result<Vec<u8>> {
351        let mut packet = Vec::new();
352
353        // Common header (8 bytes)
354        packet.push(0x83); // Intradomain Routing Protocol Discriminator
355        packet.push(27); // Length Indicator (will update later)
356        packet.push(1); // Version/Protocol ID Extension
357        packet.push(0); // ID Length (0 = 6 bytes)
358        packet.push(pdu_type); // PDU Type
359        packet.push(1); // Version
360        packet.push(0); // Reserved
361        packet.push(0); // Max Area Addresses
362
363        // PDU-specific header for Hello (variable, simplified here)
364        // Circuit Type (1 byte)
365        let circuit_type = match pdu_type {
366            15 => 1, // Level 1
367            16 => 2, // Level 2
368            17 => 3, // Level 1+2 (P2P)
369            _ => 2,
370        };
371        packet.push(circuit_type);
372
373        // Source ID (6 bytes) - parse system_id
374        let sys_id_bytes = Self::parse_system_id(system_id)?;
375        packet.extend_from_slice(&sys_id_bytes);
376
377        // Holding Time (2 bytes)
378        packet.extend_from_slice(&holding_time.to_be_bytes());
379
380        // PDU Length (2 bytes) - will update later
381        packet.extend_from_slice(&[0, 0]);
382
383        // For LAN Hello: Priority (1 byte), LAN ID (7 bytes)
384        if pdu_type == 15 || pdu_type == 16 {
385            packet.push(64); // Priority (default 64)
386            packet.extend_from_slice(&sys_id_bytes); // LAN ID = Source ID
387            packet.push(0); // Pseudonode ID
388        }
389
390        // TLVs
391        let tlvs = Self::build_hello_tlvs(area_id, system_id)?;
392        packet.extend_from_slice(&tlvs);
393
394        // Update Length Indicator (points to start of TLVs)
395        let header_len = if pdu_type == 15 || pdu_type == 16 {
396            27 // LAN Hello header length
397        } else {
398            20 // P2P Hello header length
399        };
400        packet[1] = header_len;
401
402        // Update PDU Length.
403        //
404        // Offset 17 for every Hello type, and this used to be 15 (LAN) / 11 (P2P). Count the
405        // fields this function actually pushes, per ISO/IEC 10589 §9.5-9.7:
406        //
407        //   0..8   common header
408        //   8      circuit type
409        //   9..15  source ID (6)
410        //   15..17 holding time (2)
411        //   17..19 PDU length (2)      <- here
412        //   19     priority        (LAN only)
413        //   20..27 LAN ID (7)      (LAN only)
414        //
415        // Writing at 15 overwrote the **holding time** with the PDU length and left the real
416        // PDU-length field as the zeros pushed above — so every Hello this server emitted
417        // carried a corrupt holding time and a length of 0. A receiver reads holding time as
418        // the adjacency hold timer, so the value it got was whatever the packet happened to
419        // be long.
420        const PDU_LEN_OFFSET: usize = 17;
421        let pdu_len = packet.len() as u16;
422        packet[PDU_LEN_OFFSET..PDU_LEN_OFFSET + 2].copy_from_slice(&pdu_len.to_be_bytes());
423
424        Ok(packet)
425    }
426
427    /// Build IS-IS LSP
428    fn build_isis_lsp(pdu_type: u8, system_id: &str) -> Result<Vec<u8>> {
429        let mut packet = Vec::new();
430
431        // Common header
432        packet.push(0x83); // Intradomain Routing Protocol Discriminator
433        packet.push(27); // Length Indicator
434        packet.push(1); // Version/Protocol ID Extension
435        packet.push(0); // ID Length (0 = 6 bytes)
436        packet.push(pdu_type); // PDU Type (18 for L1, 20 for L2)
437        packet.push(1); // Version
438        packet.push(0); // Reserved
439        packet.push(0); // Max Area Addresses
440
441        // LSP-specific header (simplified)
442        packet.extend_from_slice(&[0, 0]); // PDU Length (will update)
443        packet.extend_from_slice(&[0, 0]); // Remaining Lifetime
444
445        // LSP ID (8 bytes: 6 bytes system ID + 1 byte pseudonode + 1 byte fragment)
446        let sys_id_bytes = Self::parse_system_id(system_id)?;
447        packet.extend_from_slice(&sys_id_bytes);
448        packet.push(0); // Pseudonode ID
449        packet.push(0); // LSP Fragment Number
450
451        packet.extend_from_slice(&[0, 0, 0, 0]); // Sequence Number
452        packet.extend_from_slice(&[0, 0]); // Checksum (should calculate, but simplified)
453        packet.push(0); // Type Block (P/ATT/OL/IS Type)
454
455        // Update PDU Length
456        let pdu_len = packet.len() as u16;
457        packet[8..10].copy_from_slice(&pdu_len.to_be_bytes());
458
459        Ok(packet)
460    }
461
462    /// Build Hello TLVs
463    fn build_hello_tlvs(area_id: &str, _system_id: &str) -> Result<Vec<u8>> {
464        let mut tlvs = Vec::new();
465
466        // TLV 1: Area Addresses
467        tlvs.push(1); // Type
468        let area_bytes = Self::parse_area_id(area_id)?;
469        tlvs.push(area_bytes.len() as u8 + 1); // Length (area length + 1-byte length prefix)
470        tlvs.push(area_bytes.len() as u8); // Area address length
471        tlvs.extend_from_slice(&area_bytes);
472
473        // TLV 129: Protocols Supported
474        tlvs.push(129); // Type
475        tlvs.push(1); // Length
476        tlvs.push(0xCC); // IPv4 (0xCC = NLPID for IPv4)
477
478        Ok(tlvs)
479    }
480
481    /// Parse system ID from dotted format (e.g., "0000.0000.0001") to 6 bytes
482    fn parse_system_id(system_id: &str) -> Result<[u8; 6]> {
483        let parts: Vec<&str> = system_id.split('.').collect();
484        if parts.len() != 3 {
485            return Err(anyhow::anyhow!("Invalid system ID format: {}", system_id));
486        }
487
488        let mut bytes = [0u8; 6];
489        for (i, part) in parts.iter().enumerate() {
490            let val = u16::from_str_radix(part, 16)
491                .context(format!("Invalid hex in system ID: {}", part))?;
492            bytes[i * 2] = (val >> 8) as u8;
493            bytes[i * 2 + 1] = val as u8;
494        }
495
496        Ok(bytes)
497    }
498
499    /// Parse area ID from dotted format (e.g., "49.0001") to bytes
500    fn parse_area_id(area_id: &str) -> Result<Vec<u8>> {
501        let parts: Vec<&str> = area_id.split('.').collect();
502        let mut bytes = Vec::new();
503
504        for part in parts {
505            // Each part can be 2 or 4 hex digits
506            if part.len() == 2 {
507                let val = u8::from_str_radix(part, 16)
508                    .context(format!("Invalid hex in area ID: {}", part))?;
509                bytes.push(val);
510            } else if part.len() == 4 {
511                let val = u16::from_str_radix(part, 16)
512                    .context(format!("Invalid hex in area ID: {}", part))?;
513                bytes.push((val >> 8) as u8);
514                bytes.push(val as u8);
515            } else {
516                return Err(anyhow::anyhow!("Invalid area ID part length: {}", part));
517            }
518        }
519
520        Ok(bytes)
521    }
522}
523
524// Event Types
525pub static ISIS_HELLO_EVENT: LazyLock<EventType> = LazyLock::new(|| {
526    EventType::new(
527        "isis_hello",
528        "An IS-IS Hello PDU arrived from a neighbour. Answer with send_isis_hello to adjacency \
529         with it, send_isis_lsp to advertise link state, send_isis_pdu for any other PDU type, \
530         or ignore_pdu to stay silent.",
531        json!({
532            "type": "send_isis_hello",
533            "pdu_type": "lan_hello_l2",
534            "system_id": "0000.0000.0001",
535            "area_id": "49.0001"
536        }),
537    )
538    // All four sync actions answer this event, which is the protocol's only one. `call_llm`
539    // advertises the event's action list rather than get_sync_actions(), so leaving it empty
540    // left the model unable to reply to a neighbour at all.
541    .with_actions(vec![
542        send_isis_hello_action(),
543        send_isis_lsp_action(),
544        send_isis_pdu_action(),
545        ignore_pdu_action(),
546    ])
547    .with_log_template(
548        LogTemplate::new()
549            .with_info("IS-IS {pdu_type} from {peer_addr}")
550            .with_debug("IS-IS Hello: type={pdu_type} peer={peer_addr} areas={area_addresses}")
551            .with_trace("IS-IS Hello: {json_pretty(.)}"),
552    )
553});
554
555fn get_isis_event_types() -> Vec<EventType> {
556    vec![ISIS_HELLO_EVENT.clone()]
557}
558
559// Action Definitions
560fn send_isis_hello_action() -> ActionDefinition {
561    ActionDefinition {
562        name: "send_isis_hello".to_string(),
563        description: "Send IS-IS Hello PDU for neighbor discovery".to_string(),
564        parameters: vec![
565            Parameter {
566                name: "pdu_type".to_string(),
567                type_hint: "string".to_string(),
568                description: "Hello PDU type: lan_hello_l1, lan_hello_l2, or p2p_hello".to_string(),
569                required: false,
570            },
571            Parameter {
572                name: "system_id".to_string(),
573                type_hint: "string".to_string(),
574                description: "Local system ID in format: 0000.0000.0001".to_string(),
575                required: false,
576            },
577            Parameter {
578                name: "area_id".to_string(),
579                type_hint: "string".to_string(),
580                description: "Area ID in format: 49.0001".to_string(),
581                required: false,
582            },
583            Parameter {
584                name: "holding_time".to_string(),
585                type_hint: "number".to_string(),
586                description: "Holding time in seconds (default: 30)".to_string(),
587                required: false,
588            },
589        ],
590        example: json!({
591            "type": "send_isis_hello",
592            "pdu_type": "lan_hello_l2",
593            "system_id": "0000.0000.0001",
594            "area_id": "49.0001"
595        }),
596        log_template: Some(
597            LogTemplate::new()
598                .with_info("-> IS-IS {pdu_type} Hello sys={system_id}")
599                .with_debug(
600                    "IS-IS send_isis_hello: type={pdu_type} sys_id={system_id} area={area_id}",
601                ),
602        ),
603    }
604}
605
606fn send_isis_lsp_action() -> ActionDefinition {
607    ActionDefinition {
608        name: "send_isis_lsp".to_string(),
609        description: "Send IS-IS Link State PDU".to_string(),
610        parameters: vec![
611            Parameter {
612                name: "level".to_string(),
613                type_hint: "string".to_string(),
614                description: "IS-IS level: level-1 or level-2".to_string(),
615                required: false,
616            },
617            Parameter {
618                name: "system_id".to_string(),
619                type_hint: "string".to_string(),
620                description: "Local system ID".to_string(),
621                required: false,
622            },
623        ],
624        example: json!({
625            "type": "send_isis_lsp",
626            "level": "level-2",
627            "system_id": "0000.0000.0001"
628        }),
629        log_template: Some(
630            LogTemplate::new()
631                .with_info("-> IS-IS LSP {level} sys={system_id}")
632                .with_debug("IS-IS send_isis_lsp: level={level} sys_id={system_id}"),
633        ),
634    }
635}
636
637fn send_isis_pdu_action() -> ActionDefinition {
638    ActionDefinition {
639        name: "send_isis_pdu".to_string(),
640        description: "Send raw IS-IS PDU from hex data".to_string(),
641        parameters: vec![Parameter {
642            name: "data".to_string(),
643            type_hint: "string".to_string(),
644            description: "Hex-encoded IS-IS PDU".to_string(),
645            required: true,
646        }],
647        // A complete ISO/IEC 10589 LAN Level-2 Hello: common header (0x83, header
648        // length 27, version/ext 1, ID length 0, PDU type 16, version 1, reserved,
649        // max area addresses 0), circuit type 2 (L2 only), source ID
650        // 0000.0000.0001, holding time 30s, PDU length 36, priority 64, LAN ID
651        // 0000.0000.0001-00, then TLV 1 (area 49.0001) and TLV 129 (NLPID 0xCC,
652        // IPv4). The previous value ended in an ellipsis and did not decode.
653        example: json!({
654            "type": "send_isis_pdu",
655            "data": "831b01001001000002000000000001001e00244000000000000100\
656                     0104034900018101cc"
657        }),
658        log_template: Some(
659            LogTemplate::new()
660                .with_info("-> IS-IS raw PDU ({data_len}B)")
661                .with_debug("IS-IS send_isis_pdu: len={data_len}"),
662        ),
663    }
664}
665
666fn ignore_pdu_action() -> ActionDefinition {
667    ActionDefinition {
668        name: "ignore_pdu".to_string(),
669        description: "Ignore the IS-IS PDU (no response)".to_string(),
670        parameters: vec![],
671        example: json!({
672            "type": "ignore_pdu"
673        }),
674        log_template: Some(
675            LogTemplate::new()
676                .with_info("-> IS-IS ignore PDU")
677                .with_debug("IS-IS ignore_pdu"),
678        ),
679    }
680}