1pub mod actions;
8
9use crate::server::connection::ConnectionId;
10use anyhow::{Context, Result};
11use bytes::Bytes;
12use pcap::{Capture, Device};
13use std::sync::Arc;
14use tokio::sync::mpsc;
15use tracing::{debug, error, info, trace, warn};
16
17use crate::llm::action_helper::call_llm;
18use crate::llm::ollama_client::OllamaClient;
19use crate::logging::emit::Log;
20use crate::protocol::Event;
21use crate::server::IsisProtocol;
22use crate::state::app_state::AppState;
23use crate::{console_debug, console_error, console_info, console_trace};
24use actions::ISIS_HELLO_EVENT;
25
26const ISIS_INTRADOMAIN_ROUTING_PROTOCOL_DISCRIMINATOR: u8 = 0x83;
28const ISIS_HEADER_LEN: usize = 8; const ISIS_HELLO_LAN_L1: u8 = 15; const ISIS_HELLO_LAN_L2: u8 = 16; const ISIS_HELLO_P2P: u8 = 17; const ISIS_LSP_L1: u8 = 18; const ISIS_LSP_L2: u8 = 20; const ISIS_CSNP_L1: u8 = 24; const ISIS_CSNP_L2: u8 = 25; const ISIS_PSNP_L1: u8 = 26; const ISIS_PSNP_L2: u8 = 27; const ISIS_ALL_L1_IS: [u8; 6] = [0x01, 0x80, 0xC2, 0x00, 0x00, 0x14]; const ISIS_ALL_L2_IS: [u8; 6] = [0x01, 0x80, 0xC2, 0x00, 0x00, 0x15]; const LLC_DSAP_ISO: u8 = 0xFE; const LLC_SSAP_ISO: u8 = 0xFE;
48const LLC_CTRL: u8 = 0x03; const ISIS_TLV_AREA_ADDRESSES: u8 = 1;
52#[allow(dead_code)]
53const ISIS_TLV_NEIGHBORS: u8 = 6;
54#[allow(dead_code)]
55const ISIS_TLV_PADDING: u8 = 8;
56#[allow(dead_code)]
57const ISIS_TLV_LSP_ENTRIES: u8 = 9;
58const ISIS_TLV_PROTOCOLS_SUPPORTED: u8 = 129;
59const ISIS_TLV_IP_INTERFACE_ADDRESSES: u8 = 132;
60#[allow(dead_code)]
61const ISIS_TLV_IP_INTERNAL_REACHABILITY: u8 = 128;
62#[allow(dead_code)]
63const ISIS_TLV_EXTENDED_REACHABILITY: u8 = 22;
64const ISIS_TLV_HOSTNAME: u8 = 137;
65
66pub struct IsisServer;
68
69impl IsisServer {
70 pub fn list_devices() -> Result<Vec<Device>> {
72 Device::list().context("Failed to list network devices")
73 }
74
75 pub fn find_device(name: &str) -> Result<Device> {
77 let devices = Self::list_devices()?;
78 devices
79 .into_iter()
80 .find(|d| d.name == name)
81 .ok_or_else(|| anyhow::anyhow!("Device '{}' not found", name))
82 }
83
84 pub async fn spawn_with_llm_actions(
86 interface: String,
87 llm_client: OllamaClient,
88 app_state: Arc<AppState>,
89 status_tx: mpsc::UnboundedSender<String>,
90 server_id: crate::state::ServerId,
91 startup_params: Option<crate::protocol::StartupParams>,
92 ) -> Result<String> {
93 console_info!(
94 status_tx,
95 "IS-IS server starting on interface: {}",
96 interface
97 );
98
99 let (system_id, area_id, level) = if let Some(ref params) = startup_params {
101 let sys_id = params
102 .get_optional_string("system_id")?
103 .unwrap_or_else(|| "0000.0000.0001".to_string());
104 let area = params
105 .get_optional_string("area_id")?
106 .unwrap_or_else(|| "49.0001".to_string());
107 let lvl = params
108 .get_optional_string("level")?
109 .unwrap_or_else(|| "level-2".to_string());
110
111 Log::new(Some(&status_tx)).info(format!(
112 "IS-IS configured: system_id={}, area={}, level={}",
113 sys_id, area, lvl
114 ));
115 (sys_id, area, lvl)
116 } else {
117 (
119 "0000.0000.0001".to_string(),
120 "49.0001".to_string(),
121 "level-2".to_string(),
122 )
123 };
124
125 let protocol = Arc::new(IsisProtocol::new());
126
127 let status_tx_ready = status_tx.clone();
129
130 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel::<Result<()>>();
139
140 let stop = crate::utils::StopSignal::new();
146 let stop_in_loop = stop.clone();
147 let app_state_reg = app_state.clone();
149
150 let interface_clone = interface.clone();
151 let protocol_clone = protocol.clone();
152 tokio::task::spawn_blocking(move || {
153 let open_capture = || -> Result<(Capture<pcap::Active>, [u8; 6])> {
154 let device = Self::find_device(&interface_clone)
155 .with_context(|| format!("no such capture device '{}'", interface_clone))?;
156
157 let local_mac = match Self::get_interface_mac(&interface_clone) {
160 Ok(mac) => mac,
161 Err(e) => {
162 warn!("Failed to get interface MAC address: {}, using default", e);
163 [0x02, 0x00, 0x00, 0x00, 0x00, 0x01] }
165 };
166
167 let mut cap = Capture::from_device(device)
169 .map(|c| c.promisc(true).snaplen(65535).timeout(1000))
170 .and_then(|c| c.open())
171 .with_context(|| {
172 format!(
173 "failed to open pcap capture on '{}' (needs root, or \
174 read access to /dev/bpf* on macOS/BSD, or CAP_NET_RAW on Linux)",
175 interface_clone
176 )
177 })?;
178
179 let filter = "ether proto 0xfefe or (ether[14:2] = 0xfefe and ether[16:1] = 0x03)";
185 cap.filter(filter, true).with_context(|| {
186 format!("failed to apply the IS-IS BPF filter '{}'", filter)
187 })?;
188
189 Ok((cap, local_mac))
190 };
191
192 let (cap, local_mac) = match open_capture() {
193 Ok(handles) => {
194 let _ = ready_tx.send(Ok(()));
195 handles
196 }
197 Err(e) => {
198 console_error!(status_tx, "IS-IS capture startup failed: {:#}", e);
199 let _ = ready_tx.send(Err(e));
200 return;
201 }
202 };
203
204 info!(
205 "IS-IS listening on {}, MAC: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
206 interface_clone,
207 local_mac[0],
208 local_mac[1],
209 local_mac[2],
210 local_mac[3],
211 local_mac[4],
212 local_mac[5]
213 );
214 let _ = status_tx.send(format!("→ IS-IS ready on interface {}", interface_clone));
215
216 let runtime = tokio::runtime::Handle::current();
217
218 let cap_arc = Arc::new(std::sync::Mutex::new(cap));
220
221 loop {
224 if stop_in_loop.is_stopped() {
225 console_info!(status_tx, "IS-IS capture on {} stopping", interface_clone);
226 break;
227 }
228 let mut cap_guard = cap_arc.lock().unwrap_or_else(|e| e.into_inner());
229 match cap_guard.next_packet() {
230 Ok(packet) => {
231 let data = Bytes::copy_from_slice(packet.data);
232 drop(cap_guard); if data.len() < 14 {
236 continue; }
238
239 let _dst_mac = &data[0..6];
240 let src_mac = &data[6..12];
241 let _eth_type = u16::from_be_bytes([data[12], data[13]]);
242
243 if data.len() < 17 {
246 continue;
247 }
248
249 let llc_offset = 14;
251 let dsap = data[llc_offset];
252 let ssap = data[llc_offset + 1];
253 let ctrl = data[llc_offset + 2];
254
255 if dsap != LLC_DSAP_ISO || ssap != LLC_SSAP_ISO || ctrl != LLC_CTRL {
257 continue; }
259
260 if data.len() < 17 + ISIS_HEADER_LEN {
262 continue; }
264
265 let isis_pdu = data.slice(17..);
267 let src_mac_bytes: [u8; 6] = [
268 src_mac[0], src_mac[1], src_mac[2], src_mac[3], src_mac[4], src_mac[5],
269 ];
270
271 let connection_id = ConnectionId::new(
272 tokio::runtime::Handle::current()
273 .block_on(app_state.get_next_unified_id()),
274 );
275
276 debug!("IS-IS received {} bytes from {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
278 data.len(),
279 src_mac[0], src_mac[1], src_mac[2],
280 src_mac[3], src_mac[4], src_mac[5]);
281 Log::new(Some(&status_tx))
282 .debug(format!("IS-IS received {} bytes", data.len()));
283
284 let hex_str = hex::encode(&data);
286 console_trace!(status_tx, "IS-IS frame (hex): {}", hex_str);
287
288 let llm_clone = llm_client.clone();
289 let state_clone = app_state.clone();
290 let status_clone = status_tx.clone();
291 let protocol_task_clone = protocol_clone.clone();
292 let system_id_clone = system_id.clone();
293 let area_id_clone = area_id.clone();
294 let level_clone = level.clone();
295 let cap_clone = cap_arc.clone();
296 let local_mac_clone = local_mac.clone();
297
298 runtime.spawn(async move {
300 if let Err(e) = Self::handle_isis_pdu(
301 &isis_pdu,
302 &src_mac_bytes,
303 connection_id,
304 server_id,
305 &llm_clone,
306 &state_clone,
307 &status_clone,
308 &protocol_task_clone,
309 &system_id_clone,
310 &area_id_clone,
311 &level_clone,
312 cap_clone,
313 local_mac_clone,
314 )
315 .await
316 {
317 error!("IS-IS PDU handling error: {}", e);
318 Log::new(Some(&status_clone))
319 .error(format!("IS-IS PDU error: {}", e));
320 }
321 });
322 }
323 Err(pcap::Error::TimeoutExpired) => {
324 drop(cap_guard);
325 continue;
326 }
327 Err(e) => {
328 console_error!(status_tx, "IS-IS capture error: {}", e);
329 break;
330 }
331 }
332 }
333
334 drop(cap_arc);
337 });
338
339 match ready_rx.await {
341 Ok(Ok(())) => {}
342 Ok(Err(e)) => return Err(e),
343 Err(_) => {
344 return Err(anyhow::anyhow!(
345 "IS-IS capture task on '{}' exited before signalling readiness",
346 interface
347 ))
348 }
349 }
350
351 app_state_reg
354 .register_server_task(server_id, stop.park_task())
355 .await;
356
357 console_info!(status_tx_ready, "IS-IS capture active on {}", interface);
358
359 Ok(interface)
360 }
361
362 async fn handle_isis_pdu(
364 isis_pdu: &[u8],
365 src_mac: &[u8],
366 connection_id: ConnectionId,
367 server_id: crate::state::ServerId,
368 llm_client: &OllamaClient,
369 app_state: &Arc<AppState>,
370 status_tx: &mpsc::UnboundedSender<String>,
371 protocol: &Arc<IsisProtocol>,
372 system_id: &str,
373 area_id: &str,
374 level: &str,
375 cap: Arc<std::sync::Mutex<Capture<pcap::Active>>>,
376 local_mac: [u8; 6],
377 ) -> Result<()> {
378 let intradomain_routing_protocol_discriminator = isis_pdu[0];
380 let _length_indicator = isis_pdu[1];
381 let _version_protocol_id_extension = isis_pdu[2];
382 let _id_length = isis_pdu[3];
383 let pdu_type = isis_pdu[4];
384 let version = isis_pdu[5];
385 let _reserved = isis_pdu[6];
386 let _max_area_addresses = isis_pdu[7];
387
388 if intradomain_routing_protocol_discriminator
390 != ISIS_INTRADOMAIN_ROUTING_PROTOCOL_DISCRIMINATOR
391 {
392 warn!(
393 "IS-IS invalid protocol discriminator: 0x{:02x}",
394 intradomain_routing_protocol_discriminator
395 );
396 return Ok(());
397 }
398
399 if version != 1 {
401 warn!("IS-IS unsupported version: {}", version);
402 return Ok(());
403 }
404
405 let pdu_type_name = match pdu_type {
407 ISIS_HELLO_LAN_L1 => "LAN Hello L1",
408 ISIS_HELLO_LAN_L2 => "LAN Hello L2",
409 ISIS_HELLO_P2P => "P2P Hello",
410 ISIS_LSP_L1 => "LSP L1",
411 ISIS_LSP_L2 => "LSP L2",
412 ISIS_CSNP_L1 => "CSNP L1",
413 ISIS_CSNP_L2 => "CSNP L2",
414 ISIS_PSNP_L1 => "PSNP L1",
415 ISIS_PSNP_L2 => "PSNP L2",
416 _ => "Unknown",
417 };
418
419 info!(
420 "IS-IS received {} ({}) from {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
421 pdu_type_name,
422 pdu_type,
423 src_mac[0],
424 src_mac[1],
425 src_mac[2],
426 src_mac[3],
427 src_mac[4],
428 src_mac[5]
429 );
430 let _ = status_tx.send(format!(
431 "→ IS-IS {} from {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
432 pdu_type_name, src_mac[0], src_mac[1], src_mac[2], src_mac[3], src_mac[4], src_mac[5]
433 ));
434
435 use crate::state::server::{
437 ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
438 };
439 let now = crate::utils::clock::Instant::now();
440 let conn_state = ServerConnectionState {
441 id: connection_id,
442 remote_addr: format!(
443 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:0",
444 src_mac[0], src_mac[1], src_mac[2], src_mac[3], src_mac[4], src_mac[5]
445 )
446 .parse()
447 .unwrap_or("0.0.0.0:0".parse().unwrap()),
448 local_addr: format!(
449 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:0",
450 local_mac[0], local_mac[1], local_mac[2], local_mac[3], local_mac[4], local_mac[5]
451 )
452 .parse()
453 .unwrap_or("0.0.0.0:0".parse().unwrap()),
454 bytes_sent: 0,
455 bytes_received: isis_pdu.len() as u64,
456 packets_sent: 0,
457 packets_received: 1,
458 last_activity: now,
459 status: ConnectionStatus::Active,
460 status_changed_at: now,
461 protocol_info: ProtocolConnectionInfo::empty(),
462 };
463 app_state
464 .add_connection_to_server(server_id, conn_state)
465 .await;
466 let _ = status_tx.send("__UPDATE_UI__".to_string());
467
468 match pdu_type {
470 ISIS_HELLO_LAN_L1 | ISIS_HELLO_LAN_L2 | ISIS_HELLO_P2P => {
471 Self::handle_hello_pdu(
472 isis_pdu,
473 src_mac,
474 pdu_type_name,
475 connection_id,
476 server_id,
477 llm_client,
478 app_state,
479 status_tx,
480 protocol,
481 system_id,
482 area_id,
483 level,
484 cap,
485 local_mac,
486 )
487 .await?;
488 }
489 ISIS_LSP_L1 | ISIS_LSP_L2 => {
490 Log::new(Some(status_tx)).info("IS-IS LSP received (not yet handled)");
491 }
492 ISIS_CSNP_L1 | ISIS_CSNP_L2 => {
493 info!("IS-IS CSNP received (not yet handled)");
494 }
495 ISIS_PSNP_L1 | ISIS_PSNP_L2 => {
496 info!("IS-IS PSNP received (not yet handled)");
497 }
498 _ => {
499 warn!("IS-IS unsupported PDU type: {}", pdu_type);
500 }
501 }
502
503 Ok(())
504 }
505
506 async fn handle_hello_pdu(
508 isis_pdu: &[u8],
509 src_mac: &[u8],
510 pdu_type_name: &str,
511 _connection_id: ConnectionId,
512 server_id: crate::state::ServerId,
513 llm_client: &OllamaClient,
514 app_state: &Arc<AppState>,
515 status_tx: &mpsc::UnboundedSender<String>,
516 protocol: &Arc<IsisProtocol>,
517 _system_id: &str,
518 _area_id: &str,
519 level: &str,
520 cap: Arc<std::sync::Mutex<Capture<pcap::Active>>>,
521 local_mac: [u8; 6],
522 ) -> Result<()> {
523 let tlv_start = if isis_pdu[4] == ISIS_HELLO_P2P {
527 20
528 } else {
529 27
530 };
531 let tlvs = if tlv_start < isis_pdu.len() {
532 Self::parse_tlvs(&isis_pdu[tlv_start..])
533 } else {
534 Vec::new()
535 };
536
537 let mut area_addresses = Vec::new();
539 let mut protocols_supported = Vec::new();
540 let mut ip_addresses = Vec::new();
541 let mut hostname = None;
542
543 for (tlv_type, tlv_value) in &tlvs {
544 match *tlv_type {
545 ISIS_TLV_AREA_ADDRESSES => {
546 area_addresses.push(hex::encode(tlv_value));
547 }
548 ISIS_TLV_PROTOCOLS_SUPPORTED => {
549 for proto in tlv_value {
550 protocols_supported.push(format!("0x{:02x}", proto));
551 }
552 }
553 ISIS_TLV_IP_INTERFACE_ADDRESSES => {
554 let mut i = 0;
555 while i + 3 < tlv_value.len() {
556 let ip = format!(
557 "{}.{}.{}.{}",
558 tlv_value[i],
559 tlv_value[i + 1],
560 tlv_value[i + 2],
561 tlv_value[i + 3]
562 );
563 ip_addresses.push(ip);
564 i += 4;
565 }
566 }
567 ISIS_TLV_HOSTNAME => {
568 if let Ok(name) = String::from_utf8(tlv_value.clone()) {
569 hostname = Some(name);
570 }
571 }
572 _ => {}
573 }
574 }
575
576 let src_mac_str = format!(
577 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
578 src_mac[0], src_mac[1], src_mac[2], src_mac[3], src_mac[4], src_mac[5]
579 );
580
581 let mut event_data = serde_json::json!({
583 "pdu_type": pdu_type_name,
584 "packet_hex": hex::encode(isis_pdu),
585 "src_mac": src_mac_str,
586 });
587
588 if !area_addresses.is_empty() {
589 event_data["area_addresses"] = serde_json::json!(area_addresses);
590 }
591 if !protocols_supported.is_empty() {
592 event_data["protocols_supported"] = serde_json::json!(protocols_supported);
593 }
594 if !ip_addresses.is_empty() {
595 event_data["ip_addresses"] = serde_json::json!(ip_addresses);
596 }
597 if let Some(ref name) = hostname {
598 event_data["hostname"] = serde_json::json!(name);
599 }
600
601 let event = Event::new(&ISIS_HELLO_EVENT, event_data);
602
603 if !operator_wants_dynamic(app_state, server_id, &event.event_type.id).await {
610 debug!(
611 "IS-IS {} from {} decision=passive_no_policy (no operator policy configured, \
612 no response and no LLM call)",
613 pdu_type_name, src_mac_str
614 );
615 let _ = status_tx.send(
616 "IS-IS Hello observed passively: no policy configured (static default, no LLM)"
617 .to_string(),
618 );
619 return Ok(());
620 }
621
622 Log::new(Some(status_tx)).debug("IS-IS calling LLM for Hello");
623
624 match call_llm(
626 llm_client,
627 app_state,
628 server_id,
629 None,
630 &event,
631 protocol.as_ref(),
632 )
633 .await
634 {
635 Ok(execution_result) => {
636 for message in &execution_result.messages {
638 console_info!(status_tx, "{}", message);
639 }
640
641 let mut frames_sent = 0usize;
646
647 for protocol_result in execution_result.protocol_results {
649 if let Some(output_data) = protocol_result.get_all_output().first() {
650 let frame = Self::build_ethernet_frame(output_data, local_mac, level)?;
652
653 let mut cap_guard = cap.lock().unwrap_or_else(|e| e.into_inner());
655 if let Err(e) = cap_guard.sendpacket(frame.as_ref()) {
656 console_error!(status_tx, "Failed to send IS-IS frame: {}", e);
657 } else {
658 frames_sent += 1;
659 console_debug!(status_tx, "IS-IS sent {} bytes", frame.len());
660 trace!("IS-IS sent (hex): {}", hex::encode(&frame));
661 }
662 }
663 }
664
665 if frames_sent == 0 {
666 debug!(
669 "IS-IS {} from {} decision=model_no_pdu (model answered with no PDU; \
670 no adjacency formed)",
671 pdu_type_name, src_mac_str
672 );
673 } else {
674 debug!(
675 "IS-IS {} from {} decision=model_pdu frames={}",
676 pdu_type_name, src_mac_str, frames_sent
677 );
678 }
679 }
680 Err(e) => {
681 let category = crate::utils::WireFailure::classify(&e);
687 let decision = if category.is_overloaded() {
688 "fail_closed_llm_overloaded"
689 } else {
690 "fail_closed_llm_error"
691 };
692 error!(
693 "IS-IS {} from {} decision={} (no PDU emitted; IS-IS has no error PDU): {}",
694 pdu_type_name, src_mac_str, decision, e
695 );
696 let _ = status_tx.send(format!(
697 "✗ IS-IS LLM error (decision={}, no PDU emitted): {}",
698 decision, e
699 ));
700 }
701 }
702
703 Ok(())
704 }
705
706 fn parse_tlvs(data: &[u8]) -> Vec<(u8, Vec<u8>)> {
708 let mut tlvs = Vec::new();
709 let mut offset = 0;
710
711 while offset + 1 < data.len() {
712 let tlv_type = data[offset];
713 let tlv_len = data[offset + 1] as usize;
714 offset += 2;
715
716 if offset + tlv_len > data.len() {
717 break;
718 }
719
720 let tlv_value = data[offset..offset + tlv_len].to_vec();
721 tlvs.push((tlv_type, tlv_value));
722 offset += tlv_len;
723 }
724
725 tlvs
726 }
727
728 fn build_ethernet_frame(isis_pdu: &[u8], src_mac: [u8; 6], level: &str) -> Result<Vec<u8>> {
730 let mut frame = Vec::new();
731
732 let dst_mac = if level.contains("level-1") {
734 ISIS_ALL_L1_IS
735 } else {
736 ISIS_ALL_L2_IS
737 };
738 frame.extend_from_slice(&dst_mac);
739
740 frame.extend_from_slice(&src_mac);
742
743 let length = (3 + isis_pdu.len()) as u16; frame.extend_from_slice(&length.to_be_bytes());
746
747 frame.push(LLC_DSAP_ISO); frame.push(LLC_SSAP_ISO); frame.push(LLC_CTRL); frame.extend_from_slice(isis_pdu);
754
755 Ok(frame)
756 }
757
758 fn get_interface_mac(interface: &str) -> Result<[u8; 6]> {
760 #[cfg(not(target_os = "linux"))]
762 let _ = interface;
763 #[cfg(target_os = "linux")]
765 {
766 use std::fs;
767 let path = format!("/sys/class/net/{}/address", interface);
768 if let Ok(contents) = fs::read_to_string(&path) {
769 let parts: Vec<&str> = contents.trim().split(':').collect();
770 if parts.len() == 6 {
771 let mut mac = [0u8; 6];
772 for (i, part) in parts.iter().enumerate() {
773 if let Ok(byte) = u8::from_str_radix(part, 16) {
774 mac[i] = byte;
775 }
776 }
777 return Ok(mac);
778 }
779 }
780 }
781
782 Err(anyhow::anyhow!("Could not determine interface MAC address"))
784 }
785}
786
787async fn operator_wants_dynamic(
793 state: &AppState,
794 server_id: crate::state::ServerId,
795 event_id: &str,
796) -> bool {
797 state
798 .with_server_mut(server_id, |server| {
799 let has_instruction = !server.instruction.trim().is_empty();
800 let has_handler = server
801 .event_handler_config
802 .as_ref()
803 .map(|c| c.find_handler(event_id).is_some())
804 .unwrap_or(false);
805 has_instruction || has_handler
806 })
807 .await
808 .unwrap_or(false)
809}