Skip to main content

netget/client/isis/
actions.rs

1//! IS-IS client protocol actions implementation
2
3use crate::llm::actions::{
4    client_trait::{Client, ClientActionResult},
5    protocol_trait::Protocol,
6    ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14/// IS-IS PDU received event
15pub static ISIS_PDU_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16    EventType::new(
17        "isis_pdu_received",
18        "IS-IS PDU captured from network interface",
19        json!({"type": "wait_for_more"}),
20    )
21    .with_parameters(vec![
22        Parameter {
23            name: "pdu_type".to_string(),
24            type_hint: "string".to_string(),
25            description: "Type of IS-IS PDU (e.g., 'L1 LAN Hello', 'L2 LSP')".to_string(),
26            required: true,
27        },
28        Parameter {
29            name: "pdu_type_code".to_string(),
30            type_hint: "number".to_string(),
31            description: "Numeric PDU type code".to_string(),
32            required: true,
33        },
34        Parameter {
35            name: "version".to_string(),
36            type_hint: "number".to_string(),
37            description: "IS-IS protocol version".to_string(),
38            required: true,
39        },
40        Parameter {
41            name: "pdu_length".to_string(),
42            type_hint: "number".to_string(),
43            description: "Length of the PDU in bytes".to_string(),
44            required: true,
45        },
46        Parameter {
47            name: "raw_pdu_hex".to_string(),
48            type_hint: "string".to_string(),
49            description: "Raw IS-IS PDU data as hex string".to_string(),
50            required: true,
51        },
52        Parameter {
53            name: "raw_frame_hex".to_string(),
54            type_hint: "string".to_string(),
55            description: "Complete Ethernet frame as hex string".to_string(),
56            required: true,
57        },
58    ])
59});
60
61/// IS-IS client protocol action handler
62pub struct IsisClientProtocol;
63
64impl IsisClientProtocol {
65    pub fn new() -> Self {
66        Self
67    }
68}
69
70// Implement Protocol trait (common functionality)
71impl Protocol for IsisClientProtocol {
72    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
73        vec![
74            ActionDefinition {
75                name: "analyze_topology".to_string(),
76                description: "Analyze IS-IS topology from captured PDUs".to_string(),
77                parameters: vec![],
78                example: json!({
79                    "type": "analyze_topology"
80                }),
81                log_template: None,
82            },
83            ActionDefinition {
84                name: "stop_capture".to_string(),
85                description: "Stop capturing IS-IS PDUs".to_string(),
86                parameters: vec![],
87                example: json!({
88                    "type": "stop_capture"
89                }),
90                log_template: None,
91            },
92        ]
93    }
94    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
95        vec![ActionDefinition {
96            name: "wait_for_more".to_string(),
97            description: "Continue capturing IS-IS PDUs".to_string(),
98            parameters: vec![],
99            example: json!({
100                "type": "wait_for_more"
101            }),
102            log_template: None,
103        }]
104    }
105    fn protocol_name(&self) -> &'static str {
106        "IS-IS"
107    }
108    fn get_event_types(&self) -> Vec<EventType> {
109        vec![EventType::new(
110            "isis_pdu_received",
111            "Triggered when an IS-IS PDU is captured from the network",
112            json!({"type": "wait_for_more"}),
113        )]
114    }
115    fn stack_name(&self) -> &'static str {
116        "ETH>LLC/SNAP>IS-IS"
117    }
118    fn keywords(&self) -> Vec<&'static str> {
119        vec![
120            "isis",
121            "is-is",
122            "intermediate system",
123            "routing protocol",
124            "layer 2 routing",
125        ]
126    }
127    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
128        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
129
130        ProtocolMetadataV2::builder()
131            .state(DevelopmentState::Experimental)
132            .implementation("pcap for raw packet capture, custom IS-IS PDU parsing")
133            .llm_control("Passive capture and analysis of IS-IS topology")
134            .e2e_testing("Requires IS-IS router or packet replay")
135            .build()
136    }
137    fn description(&self) -> &'static str {
138        "IS-IS client for capturing and analyzing IS-IS routing protocol PDUs"
139    }
140    fn example_prompt(&self) -> &'static str {
141        "Capture IS-IS PDUs on eth0 and analyze network topology"
142    }
143    fn group_name(&self) -> &'static str {
144        "Routing"
145    }
146    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
147        vec![ParameterDefinition {
148            name: "interface".to_string(),
149            type_hint: "string".to_string(),
150            description: "Network interface to capture IS-IS traffic on (e.g., 'eth0', 'en0')"
151                .to_string(),
152            required: true,
153            example: json!("eth0"),
154            default: None,
155        }]
156    }
157    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
158        use crate::llm::actions::StartupExamples;
159        use serde_json::json;
160
161        StartupExamples::new(
162            // LLM mode: LLM analyzes IS-IS topology from captured PDUs
163            json!({
164                "type": "open_client",
165                "remote_addr": "0.0.0.0:0",
166                "base_stack": "isis",
167                "instruction": "Capture IS-IS PDUs on eth0 and analyze network topology",
168                "startup_params": {
169                    "interface": "eth0"
170                }
171            }),
172            // Script mode: Code-based PDU analysis
173            json!({
174                "type": "open_client",
175                "remote_addr": "0.0.0.0:0",
176                "base_stack": "isis",
177                "startup_params": {
178                    "interface": "eth0"
179                },
180                "event_handlers": [{
181                    "event_pattern": "isis_pdu_received",
182                    "handler": {
183                        "type": "script",
184                        "language": "python",
185                        "code": "<isis_client_handler>"
186                    }
187                }]
188            }),
189            // Static mode: Passive monitoring
190            json!({
191                "type": "open_client",
192                "remote_addr": "0.0.0.0:0",
193                "base_stack": "isis",
194                "startup_params": {
195                    "interface": "eth0"
196                },
197                "event_handlers": [{
198                    "event_pattern": "isis_pdu_received",
199                    "handler": {
200                        "type": "static",
201                        "actions": [{
202                            "type": "wait_for_more"
203                        }]
204                    }
205                }]
206            }),
207        )
208    }
209}
210
211// Implement Client trait (client-specific functionality)
212impl Client for IsisClientProtocol {
213    fn connect(
214        &self,
215        ctx: crate::protocol::ConnectContext,
216    ) -> std::pin::Pin<
217        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
218    > {
219        Box::pin(async move {
220            use crate::client::isis::IsisClient;
221
222            // `interface` is a declared startup parameter, so it has to be the thing that
223            // decides which device is captured. It was declared and never read: the call below
224            // passed `ctx.remote_addr`, which for a capture client is not an address at all,
225            // so the dashboard field and every `startup_params: {"interface": "en0"}` example
226            // in this file did nothing when set.
227            //
228            // `remote_addr` stays as the fallback because that is what callers have been
229            // passing the interface name in, and breaking them to fix a dead knob would trade
230            // one defect for another.
231            let interface = ctx
232                .startup_params
233                .as_ref()
234                .map(|p| p.get_optional_string("interface"))
235                .transpose()?
236                .flatten()
237                .filter(|s| !s.trim().is_empty())
238                .unwrap_or(ctx.remote_addr);
239
240            IsisClient::connect_with_llm_actions(
241                interface,
242                ctx.llm_client,
243                ctx.state,
244                ctx.status_tx,
245                ctx.client_id,
246            )
247            .await
248        })
249    }
250    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
251        let action_type = action
252            .get("type")
253            .and_then(|v| v.as_str())
254            .context("Missing 'type' field in action")?;
255
256        match action_type {
257            "analyze_topology" => {
258                // This is a placeholder - the LLM handles analysis in memory
259                Ok(ClientActionResult::WaitForMore)
260            }
261            "stop_capture" => Ok(ClientActionResult::Disconnect),
262            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
263            _ => Err(anyhow::anyhow!(
264                "Unknown IS-IS client action: {}",
265                action_type
266            )),
267        }
268    }
269}