1pub mod actions;
10
11use crate::utils::clock::Instant;
12use anyhow::{anyhow, Result};
13use hex;
14use std::collections::HashMap;
15use std::net::{IpAddr, Ipv4Addr, SocketAddr};
16use std::os::unix::io::AsRawFd;
17use std::sync::Arc;
18use tokio::io::unix::AsyncFd;
19use tokio::sync::{mpsc, Mutex};
20use tracing::{debug, error, info, trace, warn};
21
22#[cfg(unix)]
23use crate::llm::action_helper::call_llm;
24#[cfg(unix)]
25use crate::llm::ollama_client::OllamaClient;
26#[cfg(unix)]
27use crate::logging::emit::Log;
28#[cfg(unix)]
29use crate::protocol::Event;
30#[cfg(unix)]
31use crate::server::connection::ConnectionId;
32#[cfg(unix)]
33use crate::server::socket_helpers::create_ospf_raw_socket;
34#[cfg(unix)]
35use crate::state::app_state::AppState;
36#[cfg(unix)]
37use crate::state::server::OspfNeighborState;
38#[cfg(unix)]
39use actions::{
40 OspfInterfaceConfig, OspfProtocol, OSPF_DATABASE_DESCRIPTION_EVENT, OSPF_HELLO_EVENT,
41 OSPF_LINK_STATE_ACK_EVENT, OSPF_LINK_STATE_REQUEST_EVENT, OSPF_LINK_STATE_UPDATE_EVENT,
42};
43
44const OSPF_VERSION: u8 = 2;
46const OSPF_HEADER_LEN: usize = 24;
47const IP_HEADER_MIN_LEN: usize = 20;
48
49const OSPF_TYPE_HELLO: u8 = 1;
51const OSPF_TYPE_DATABASE_DESCRIPTION: u8 = 2;
52const OSPF_TYPE_LINK_STATE_REQUEST: u8 = 3;
53const OSPF_TYPE_LINK_STATE_UPDATE: u8 = 4;
54const OSPF_TYPE_LINK_STATE_ACK: u8 = 5;
55
56const OSPF_ALL_SPF_ROUTERS: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 5);
58#[allow(dead_code)]
59const OSPF_ALL_DROUTERS: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 6);
60
61#[cfg(unix)]
63struct OspfNeighbor {
64 #[allow(dead_code)]
65 router_id: String,
66 #[allow(dead_code)]
67 neighbor_ip: Ipv4Addr,
68 connection_id: ConnectionId,
69 state: OspfNeighborState,
70 priority: u8,
71 dr: String,
72 bdr: String,
73 last_hello: Instant,
74}
75
76#[cfg(unix)]
78struct OspfState {
79 socket_fd: i32,
80 interface_ip: Ipv4Addr,
83 config: OspfInterfaceConfig,
86 neighbors: Arc<Mutex<HashMap<String, OspfNeighbor>>>,
87}
88
89pub struct OspfServer;
91
92#[cfg(unix)]
93impl OspfServer {
94 pub async fn spawn_with_llm_actions(
96 listen_addr: SocketAddr,
97 llm_client: OllamaClient,
98 app_state: Arc<AppState>,
99 status_tx: mpsc::UnboundedSender<String>,
100 server_id: crate::state::ServerId,
101 startup_params: Option<crate::protocol::StartupParams>,
102 ) -> Result<SocketAddr> {
103 let interface_ip = match listen_addr.ip() {
105 IpAddr::V4(ip) => ip,
106 IpAddr::V6(_) => return Err(anyhow!("OSPF only supports IPv4")),
107 };
108
109 let raw_socket = create_ospf_raw_socket(interface_ip, true, false)?;
111 let socket_fd = raw_socket.as_raw_fd();
112
113 Log::new(Some(&status_tx)).info(format!("OSPF server on {} (requires root)", interface_ip));
114
115 let mut config = OspfInterfaceConfig {
119 router_id: interface_ip.to_string(),
120 ..OspfInterfaceConfig::default()
121 };
122
123 if let Some(ref params) = startup_params {
124 if let Some(v) = params.get_optional_string("router_id")? {
125 config.router_id = v;
126 }
127 if let Some(v) = params.get_optional_string("area_id")? {
128 config.area_id = v;
129 }
130 if let Some(v) = params.get_optional_string("network_mask")? {
131 config.network_mask = v;
132 }
133 if let Some(v) = params.get_optional_i64("hello_interval")? {
134 config.hello_interval = u16::try_from(v).map_err(|_| {
135 anyhow!("hello_interval must be between 0 and 65535 seconds, got {v}")
136 })?;
137 }
138 if let Some(v) = params.get_optional_i64("router_dead_interval")? {
139 config.router_dead_interval = u32::try_from(v).map_err(|_| {
140 anyhow!(
141 "router_dead_interval must be between 0 and 4294967295 seconds, got {v}"
142 )
143 })?;
144 }
145 if let Some(v) = params.get_optional_i64("router_priority")? {
146 config.router_priority = u8::try_from(v)
147 .map_err(|_| anyhow!("router_priority must be between 0 and 255, got {v}"))?;
148 }
149 }
150
151 for (name, value) in [
156 ("router_id", &config.router_id),
157 ("area_id", &config.area_id),
158 ("network_mask", &config.network_mask),
159 ] {
160 if value.parse::<Ipv4Addr>().is_err() {
161 return Err(anyhow!(
162 "OSPF startup parameter '{name}' must be in IPv4 dotted-quad form \
163 (e.g. 1.1.1.1), got '{value}'"
164 ));
165 }
166 }
167
168 Log::new(Some(&status_tx)).info(format!(
169 "OSPF: router_id={}, area={}, mask={}, hello={}s, dead={}s, priority={}",
170 config.router_id,
171 config.area_id,
172 config.network_mask,
173 config.hello_interval,
174 config.router_dead_interval,
175 config.router_priority
176 ));
177
178 let protocol = Arc::new(OspfProtocol::new());
179 let neighbors: Arc<Mutex<HashMap<String, OspfNeighbor>>> =
180 Arc::new(Mutex::new(HashMap::new()));
181
182 let ospf_state = Arc::new(OspfState {
183 socket_fd,
184 interface_ip,
185 config,
186 neighbors: neighbors.clone(),
187 });
188
189 let async_socket = AsyncFd::new(raw_socket)?;
191
192 let task_registrar = app_state.clone();
193 let accept_handle = tokio::spawn(async move {
194 let mut buffer = vec![0u8; 65535];
195
196 loop {
197 let mut guard = match async_socket.readable().await {
198 Ok(guard) => guard,
199 Err(e) => {
200 error!("OSPF socket error: {}", e);
201 break;
202 }
203 };
204
205 match guard.try_io(|inner| {
206 let fd = inner.as_raw_fd();
207 unsafe {
208 let n = libc::recv(
209 fd,
210 buffer.as_mut_ptr() as *mut libc::c_void,
211 buffer.len(),
212 0,
213 );
214
215 if n < 0 {
216 Err(std::io::Error::last_os_error())
217 } else {
218 Ok(n as usize)
219 }
220 }
221 }) {
222 Ok(Ok(n)) => {
223 if n == 0 {
224 continue;
225 }
226
227 if n < IP_HEADER_MIN_LEN {
229 continue;
230 }
231
232 let ip_header_len = ((buffer[0] & 0x0F) * 4) as usize;
233 if n < ip_header_len + OSPF_HEADER_LEN {
234 continue;
235 }
236
237 let src_ip = Ipv4Addr::new(buffer[12], buffer[13], buffer[14], buffer[15]);
239
240 let ospf_data = &buffer[ip_header_len..n];
242 let version = ospf_data[0];
243 let packet_type = ospf_data[1];
244
245 if version != OSPF_VERSION {
246 continue;
247 }
248
249 let sender_router_id = format!(
251 "{}.{}.{}.{}",
252 ospf_data[4], ospf_data[5], ospf_data[6], ospf_data[7]
253 );
254
255 let sender_area_id = format!(
256 "{}.{}.{}.{}",
257 ospf_data[8], ospf_data[9], ospf_data[10], ospf_data[11]
258 );
259
260 debug!(
261 "OSPF type={} from {} ({})",
262 packet_type, src_ip, sender_router_id
263 );
264
265 let ospf_data_owned = ospf_data.to_vec();
267 let llm_clone = llm_client.clone();
268 let state_clone = app_state.clone();
269 let status_clone = status_tx.clone();
270 let protocol_clone = protocol.clone();
271 let ospf_state_clone = ospf_state.clone();
272
273 let task_owner = app_state.clone();
275 task_owner
276 .spawn_server_task(server_id, async move {
277 if let Err(e) = Self::handle_ospf_packet(
278 packet_type,
279 &ospf_data_owned,
280 src_ip,
281 sender_router_id,
282 sender_area_id,
283 llm_clone,
284 state_clone,
285 status_clone,
286 protocol_clone,
287 ospf_state_clone,
288 server_id,
289 )
290 .await
291 {
292 error!("OSPF packet error: {}", e);
293 }
294 })
295 .await;
296 }
297 Ok(Err(e)) => {
298 error!("OSPF recv error: {}", e);
299 }
300 Err(_would_block) => continue,
301 }
302 }
303
304 warn!("OSPF receive loop terminated");
305 });
306
307 task_registrar
308 .register_server_task(server_id, accept_handle)
309 .await;
310
311 Ok(SocketAddr::new(IpAddr::V4(interface_ip), 0))
312 }
313
314 #[cfg(unix)]
315 async fn handle_ospf_packet(
316 packet_type: u8,
317 data: &[u8],
318 src_ip: Ipv4Addr,
319 sender_router_id: String,
320 sender_area_id: String,
321 llm_client: OllamaClient,
322 app_state: Arc<AppState>,
323 status_tx: mpsc::UnboundedSender<String>,
324 protocol: Arc<OspfProtocol>,
325 ospf_state: Arc<OspfState>,
326 server_id: crate::state::ServerId,
327 ) -> Result<()> {
328 let dead_after = ospf_state.config.router_dead_interval;
337 let existing = {
338 let mut neighbors = ospf_state.neighbors.lock().await;
339 let now = Instant::now();
340 if dead_after > 0 {
341 let deadline = std::time::Duration::from_secs(dead_after as u64);
342 neighbors.retain(|id, n| {
343 let alive =
345 *id == sender_router_id || now.duration_since(n.last_hello) < deadline;
346 if !alive {
347 info!(
348 "OSPF neighbor {} aged out after {}s without a packet",
349 id, dead_after
350 );
351 }
352 alive
353 });
354 }
355 neighbors.get_mut(&sender_router_id).map(|neighbor| {
356 neighbor.last_hello = now;
357 neighbor.connection_id
358 })
359 };
360
361 let (connection_id, is_new_neighbor) = match existing {
367 Some(id) => (id, false),
368 None => {
369 let fresh = ConnectionId::new(app_state.get_next_unified_id().await);
370 let mut neighbors = ospf_state.neighbors.lock().await;
371 match neighbors.get(&sender_router_id) {
372 Some(raced) => (raced.connection_id, false),
375 None => {
376 neighbors.insert(
377 sender_router_id.clone(),
378 OspfNeighbor {
379 router_id: sender_router_id.clone(),
380 neighbor_ip: src_ip,
381 connection_id: fresh,
382 state: OspfNeighborState::Down,
383 priority: 0,
384 dr: "0.0.0.0".to_string(),
385 bdr: "0.0.0.0".to_string(),
386 last_hello: Instant::now(),
387 },
388 );
389 (fresh, true)
390 }
391 }
392 }
393 };
394
395 if is_new_neighbor {
403 use crate::state::server::{
404 ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
405 };
406 let now = crate::utils::clock::Instant::now();
407 app_state
408 .add_connection_to_server(
409 server_id,
410 ServerConnectionState {
411 id: connection_id,
412 remote_addr: SocketAddr::new(IpAddr::V4(src_ip), 0),
413 local_addr: SocketAddr::new(IpAddr::V4(ospf_state.interface_ip), 0),
414 bytes_sent: 0,
415 bytes_received: data.len() as u64,
416 packets_sent: 0,
417 packets_received: 1,
418 last_activity: now,
419 status: ConnectionStatus::Active,
420 status_changed_at: now,
421 protocol_info: ProtocolConnectionInfo::empty(),
422 },
423 )
424 .await;
425 let _ = status_tx.send("__UPDATE_UI__".to_string());
426 } else {
427 app_state
428 .update_connection_stats(
429 server_id,
430 connection_id,
431 Some(data.len() as u64),
432 None,
433 Some(1),
434 None,
435 )
436 .await;
437 }
438
439 match packet_type {
440 OSPF_TYPE_HELLO => {
441 Self::handle_hello_packet(
442 data,
443 src_ip,
444 sender_router_id,
445 sender_area_id,
446 connection_id,
447 llm_client,
448 app_state,
449 status_tx,
450 protocol,
451 ospf_state,
452 server_id,
453 )
454 .await?;
455 }
456 OSPF_TYPE_DATABASE_DESCRIPTION => {
457 Self::handle_database_description_packet(
458 data,
459 src_ip,
460 sender_router_id,
461 sender_area_id,
462 connection_id,
463 llm_client,
464 app_state,
465 status_tx,
466 protocol,
467 ospf_state,
468 server_id,
469 )
470 .await?;
471 }
472 OSPF_TYPE_LINK_STATE_REQUEST => {
473 Self::handle_link_state_request_packet(
474 data,
475 src_ip,
476 sender_router_id,
477 sender_area_id,
478 connection_id,
479 llm_client,
480 app_state,
481 status_tx,
482 protocol,
483 ospf_state,
484 server_id,
485 )
486 .await?;
487 }
488 OSPF_TYPE_LINK_STATE_UPDATE => {
489 Self::handle_link_state_update_packet(
490 data,
491 src_ip,
492 sender_router_id,
493 sender_area_id,
494 connection_id,
495 llm_client,
496 app_state,
497 status_tx,
498 protocol,
499 ospf_state,
500 server_id,
501 )
502 .await?;
503 }
504 OSPF_TYPE_LINK_STATE_ACK => {
505 Self::handle_link_state_ack_packet(
506 data,
507 src_ip,
508 sender_router_id,
509 sender_area_id,
510 connection_id,
511 llm_client,
512 app_state,
513 status_tx,
514 protocol,
515 ospf_state,
516 server_id,
517 )
518 .await?;
519 }
520 _ => {
521 warn!("OSPF unknown type: {}", packet_type);
522 }
523 }
524
525 Ok(())
526 }
527
528 #[cfg(unix)]
529 async fn handle_hello_packet(
530 data: &[u8],
531 src_ip: Ipv4Addr,
532 sender_router_id: String,
533 sender_area_id: String,
534 connection_id: ConnectionId,
535 llm_client: OllamaClient,
536 app_state: Arc<AppState>,
537 status_tx: mpsc::UnboundedSender<String>,
538 protocol: Arc<OspfProtocol>,
539 ospf_state: Arc<OspfState>,
540 server_id: crate::state::ServerId,
541 ) -> Result<()> {
542 if data.len() < OSPF_HEADER_LEN + 20 {
543 return Err(anyhow!("Hello too short"));
544 }
545
546 let network_mask = format!("{}.{}.{}.{}", data[24], data[25], data[26], data[27]);
548 let hello_interval = u16::from_be_bytes([data[28], data[29]]);
549 let priority = data[31];
550 let router_dead_interval = u32::from_be_bytes([data[32], data[33], data[34], data[35]]);
551 let dr = format!("{}.{}.{}.{}", data[36], data[37], data[38], data[39]);
552 let bdr = format!("{}.{}.{}.{}", data[40], data[41], data[42], data[43]);
553
554 let mut neighbor_list = Vec::new();
556 let mut offset = 44;
557 while offset + 4 <= data.len() {
558 let neighbor_id = format!(
559 "{}.{}.{}.{}",
560 data[offset],
561 data[offset + 1],
562 data[offset + 2],
563 data[offset + 3]
564 );
565 neighbor_list.push(neighbor_id);
566 offset += 4;
567 }
568
569 info!(
570 "OSPF Hello from {} (priority={}, DR={}, BDR={})",
571 sender_router_id, priority, dr, bdr
572 );
573
574 let mismatches =
578 ospf_state
579 .config
580 .hello_mismatches(hello_interval, router_dead_interval, &network_mask);
581
582 if !mismatches.is_empty() {
583 Log::new(Some(&status_tx)).warn(format!(
584 "OSPF Hello from {} does not match this interface: {}",
585 sender_router_id,
586 mismatches.join("; ")
587 ));
588 }
589
590 {
592 let mut neighbors = ospf_state.neighbors.lock().await;
593 if let Some(neighbor) = neighbors.get_mut(&sender_router_id) {
594 neighbor.priority = priority;
595 neighbor.dr = dr.clone();
596 neighbor.bdr = bdr.clone();
597 neighbor.last_hello = Instant::now();
598
599 if mismatches.is_empty() {
604 match neighbor.state {
605 OspfNeighborState::Down => {
606 neighbor.state = OspfNeighborState::Init;
607 info!("OSPF neighbor {} -> Init", sender_router_id);
608 }
609 OspfNeighborState::Init => {
610 neighbor.state = OspfNeighborState::TwoWay;
611 info!("OSPF neighbor {} -> 2-Way", sender_router_id);
612 }
613 _ => {}
614 }
615 }
616 }
617 }
618
619 let event = Event {
622 event_type: &OSPF_HELLO_EVENT,
623 data: serde_json::json!({
624 "connection_id": connection_id.to_string(),
625 "neighbor_id": sender_router_id,
626 "neighbor_ip": src_ip.to_string(),
627 "area_id": sender_area_id,
628 "network_mask": network_mask,
629 "hello_interval": hello_interval,
630 "router_dead_interval": router_dead_interval,
631 "router_priority": priority,
632 "dr": dr,
633 "bdr": bdr,
634 "neighbors": neighbor_list,
635 "local_network_mask": ospf_state.config.network_mask,
636 "local_hello_interval": ospf_state.config.hello_interval,
637 "local_router_dead_interval": ospf_state.config.router_dead_interval,
638 "local_router_priority": ospf_state.config.router_priority,
639 "config_mismatches": mismatches,
640 }),
641 };
642
643 Self::dispatch_event(
644 event,
645 connection_id,
646 llm_client,
647 app_state,
648 status_tx,
649 protocol,
650 ospf_state,
651 server_id,
652 )
653 .await
654 }
655
656 #[cfg(unix)]
661 #[allow(clippy::too_many_arguments)]
662 async fn handle_database_description_packet(
663 data: &[u8],
664 src_ip: Ipv4Addr,
665 sender_router_id: String,
666 sender_area_id: String,
667 connection_id: ConnectionId,
668 llm_client: OllamaClient,
669 app_state: Arc<AppState>,
670 status_tx: mpsc::UnboundedSender<String>,
671 protocol: Arc<OspfProtocol>,
672 ospf_state: Arc<OspfState>,
673 server_id: crate::state::ServerId,
674 ) -> Result<()> {
675 if data.len() < OSPF_HEADER_LEN + 8 {
676 return Err(anyhow!("OSPF DD packet too short: {} bytes", data.len()));
677 }
678
679 let interface_mtu = u16::from_be_bytes([data[24], data[25]]);
680 let options = data[26];
681 let flags = data[27];
682 let dd_sequence = u32::from_be_bytes([data[28], data[29], data[30], data[31]]);
683 let lsa_headers = OspfProtocol::parse_lsa_headers(&data[32..], 32);
684
685 info!(
686 "OSPF DD from {} (seq={}, init={}, more={}, master={}, {} LSA headers)",
687 sender_router_id,
688 dd_sequence,
689 flags & 0x04 != 0,
690 flags & 0x02 != 0,
691 flags & 0x01 != 0,
692 lsa_headers.len()
693 );
694
695 let event = Event {
696 event_type: &OSPF_DATABASE_DESCRIPTION_EVENT,
697 data: serde_json::json!({
698 "connection_id": connection_id.to_string(),
699 "neighbor_id": sender_router_id,
700 "neighbor_ip": src_ip.to_string(),
701 "area_id": sender_area_id,
702 "interface_mtu": interface_mtu,
703 "options": options,
704 "init": flags & 0x04 != 0,
705 "more": flags & 0x02 != 0,
706 "master": flags & 0x01 != 0,
707 "dd_sequence": dd_sequence,
708 "lsa_count": lsa_headers.len(),
709 "lsa_headers": lsa_headers,
710 }),
711 };
712
713 Self::dispatch_event(
714 event,
715 connection_id,
716 llm_client,
717 app_state,
718 status_tx,
719 protocol,
720 ospf_state,
721 server_id,
722 )
723 .await
724 }
725
726 #[cfg(unix)]
730 #[allow(clippy::too_many_arguments)]
731 async fn handle_link_state_request_packet(
732 data: &[u8],
733 src_ip: Ipv4Addr,
734 sender_router_id: String,
735 sender_area_id: String,
736 connection_id: ConnectionId,
737 llm_client: OllamaClient,
738 app_state: Arc<AppState>,
739 status_tx: mpsc::UnboundedSender<String>,
740 protocol: Arc<OspfProtocol>,
741 ospf_state: Arc<OspfState>,
742 server_id: crate::state::ServerId,
743 ) -> Result<()> {
744 let body = data.get(OSPF_HEADER_LEN..).unwrap_or(&[]);
745 let mut requests = Vec::new();
746 let mut offset = 0;
747 while offset + 12 <= body.len() && requests.len() < 64 {
748 let r = &body[offset..offset + 12];
749 requests.push(serde_json::json!({
750 "lsa_type": u32::from_be_bytes([r[0], r[1], r[2], r[3]]),
751 "link_state_id": format!("{}.{}.{}.{}", r[4], r[5], r[6], r[7]),
752 "advertising_router": format!("{}.{}.{}.{}", r[8], r[9], r[10], r[11]),
753 }));
754 offset += 12;
755 }
756
757 info!(
758 "OSPF LSR from {} ({} LSAs requested)",
759 sender_router_id,
760 requests.len()
761 );
762
763 let event = Event {
764 event_type: &OSPF_LINK_STATE_REQUEST_EVENT,
765 data: serde_json::json!({
766 "connection_id": connection_id.to_string(),
767 "neighbor_id": sender_router_id,
768 "neighbor_ip": src_ip.to_string(),
769 "area_id": sender_area_id,
770 "request_count": requests.len(),
771 "requests": requests,
772 }),
773 };
774
775 Self::dispatch_event(
776 event,
777 connection_id,
778 llm_client,
779 app_state,
780 status_tx,
781 protocol,
782 ospf_state,
783 server_id,
784 )
785 .await
786 }
787
788 #[cfg(unix)]
793 #[allow(clippy::too_many_arguments)]
794 async fn handle_link_state_update_packet(
795 data: &[u8],
796 src_ip: Ipv4Addr,
797 sender_router_id: String,
798 sender_area_id: String,
799 connection_id: ConnectionId,
800 llm_client: OllamaClient,
801 app_state: Arc<AppState>,
802 status_tx: mpsc::UnboundedSender<String>,
803 protocol: Arc<OspfProtocol>,
804 ospf_state: Arc<OspfState>,
805 server_id: crate::state::ServerId,
806 ) -> Result<()> {
807 if data.len() < OSPF_HEADER_LEN + 4 {
808 return Err(anyhow!("OSPF LSU packet too short: {} bytes", data.len()));
809 }
810
811 let advertised_count = u32::from_be_bytes([data[24], data[25], data[26], data[27]]);
812 let body = &data[28..];
813
814 let mut lsas = Vec::new();
817 let mut offset = 0usize;
818 while offset + 20 <= body.len() && lsas.len() < 64 {
819 let header = OspfProtocol::parse_lsa_headers(&body[offset..offset + 20], 1);
820 let Some(header) = header.into_iter().next() else {
821 break;
822 };
823 let declared = header["length"].as_u64().unwrap_or(0) as usize;
824 lsas.push(header);
825 if declared < 20 {
827 break;
828 }
829 offset += declared;
830 }
831
832 info!(
833 "OSPF LSU from {} ({} LSAs advertised, {} parsed)",
834 sender_router_id,
835 advertised_count,
836 lsas.len()
837 );
838
839 let event = Event {
840 event_type: &OSPF_LINK_STATE_UPDATE_EVENT,
841 data: serde_json::json!({
842 "connection_id": connection_id.to_string(),
843 "neighbor_id": sender_router_id,
844 "neighbor_ip": src_ip.to_string(),
845 "area_id": sender_area_id,
846 "advertised_lsa_count": advertised_count,
847 "lsa_count": lsas.len(),
848 "lsas": lsas,
849 }),
850 };
851
852 Self::dispatch_event(
853 event,
854 connection_id,
855 llm_client,
856 app_state,
857 status_tx,
858 protocol,
859 ospf_state,
860 server_id,
861 )
862 .await
863 }
864
865 #[cfg(unix)]
867 #[allow(clippy::too_many_arguments)]
868 async fn handle_link_state_ack_packet(
869 data: &[u8],
870 src_ip: Ipv4Addr,
871 sender_router_id: String,
872 sender_area_id: String,
873 connection_id: ConnectionId,
874 llm_client: OllamaClient,
875 app_state: Arc<AppState>,
876 status_tx: mpsc::UnboundedSender<String>,
877 protocol: Arc<OspfProtocol>,
878 ospf_state: Arc<OspfState>,
879 server_id: crate::state::ServerId,
880 ) -> Result<()> {
881 let body = data.get(OSPF_HEADER_LEN..).unwrap_or(&[]);
882 let lsa_headers = OspfProtocol::parse_lsa_headers(body, 64);
883
884 trace!(
885 "OSPF LSAck from {} ({} LSA headers)",
886 sender_router_id,
887 lsa_headers.len()
888 );
889
890 let event = Event {
891 event_type: &OSPF_LINK_STATE_ACK_EVENT,
892 data: serde_json::json!({
893 "connection_id": connection_id.to_string(),
894 "neighbor_id": sender_router_id,
895 "neighbor_ip": src_ip.to_string(),
896 "area_id": sender_area_id,
897 "lsa_count": lsa_headers.len(),
898 "lsa_headers": lsa_headers,
899 }),
900 };
901
902 Self::dispatch_event(
903 event,
904 connection_id,
905 llm_client,
906 app_state,
907 status_tx,
908 protocol,
909 ospf_state,
910 server_id,
911 )
912 .await
913 }
914
915 #[cfg(unix)]
920 #[allow(clippy::too_many_arguments)]
921 async fn dispatch_event(
922 event: Event,
923 connection_id: ConnectionId,
924 llm_client: OllamaClient,
925 app_state: Arc<AppState>,
926 status_tx: mpsc::UnboundedSender<String>,
927 protocol: Arc<OspfProtocol>,
928 ospf_state: Arc<OspfState>,
929 server_id: crate::state::ServerId,
930 ) -> Result<()> {
931 if !operator_wants_dynamic(&app_state, server_id, &event.event_type.id).await {
938 Log::new(Some(&status_tx)).debug(format!(
939 "OSPF {} observed passively: no operator policy configured (no instruction or handler), no response and no LLM call",
940 event.event_type.id
941 ));
942 return Ok(());
943 }
944
945 let event_id = event.event_type.id.clone();
946
947 match call_llm(
948 &llm_client,
949 &app_state,
950 server_id,
951 Some(connection_id),
952 &event,
953 protocol.as_ref(),
954 )
955 .await
956 {
957 Ok(execution_result) => {
958 for message in &execution_result.messages {
960 Log::new(Some(&status_tx)).info(format!("{}", message));
961 }
962
963 Log::new(Some(&status_tx)).debug(format!(
964 "OSPF got {} protocol results",
965 execution_result.protocol_results.len()
966 ));
967
968 let mut packets_sent = 0usize;
972 let mut model_chose_to_wait = false;
973
974 for protocol_result in execution_result.protocol_results {
976 if matches!(
977 protocol_result,
978 crate::llm::actions::protocol_trait::ActionResult::WaitForMore
979 ) {
980 model_chose_to_wait = true;
981 }
982
983 if let crate::llm::actions::protocol_trait::ActionResult::Custom {
985 name,
986 data,
987 } = &protocol_result
988 {
989 if name == "ospf_action" {
990 let action_type =
992 data.get("type").and_then(|v| v.as_str()).unwrap_or("");
993 let destination_str = data
994 .get("destination")
995 .and_then(|d| d.as_str())
996 .unwrap_or("multicast");
997
998 let mut data = data.clone();
1004 ospf_state.config.apply_defaults(&mut data);
1005 let data = &data;
1006
1007 let packet_result = match action_type {
1009 "send_hello" => actions::OspfProtocol::build_hello_packet(data),
1010 "send_database_description" => {
1011 actions::OspfProtocol::build_database_description_packet(data)
1012 }
1013 "send_link_state_request" => {
1014 actions::OspfProtocol::build_link_state_request_packet(data)
1015 }
1016 "send_link_state_update" => {
1017 actions::OspfProtocol::build_link_state_update_packet(data)
1018 }
1019 "send_link_state_ack" => {
1020 actions::OspfProtocol::build_link_state_ack_packet(data)
1021 }
1022 _ => {
1023 warn!("Unknown OSPF action type: {}", action_type);
1024 continue;
1025 }
1026 };
1027
1028 match packet_result {
1029 Ok(packet) => {
1030 let dest_ip = match destination_str {
1032 "multicast" => OSPF_ALL_SPF_ROUTERS,
1033 "dr_multicast" => OSPF_ALL_DROUTERS,
1034 ip_str => match ip_str.parse::<Ipv4Addr>() {
1035 Ok(ip) => ip,
1036 Err(_) => {
1037 warn!(
1038 "Invalid destination '{}', using multicast",
1039 ip_str
1040 );
1041 OSPF_ALL_SPF_ROUTERS
1042 }
1043 },
1044 };
1045
1046 match Self::send_ospf_packet(
1048 ospf_state.socket_fd,
1049 dest_ip,
1050 &packet,
1051 ) {
1052 Ok(()) => {
1053 packets_sent += 1;
1054 app_state
1057 .update_connection_stats(
1058 server_id,
1059 connection_id,
1060 None,
1061 Some(packet.len() as u64),
1062 None,
1063 Some(1),
1064 )
1065 .await;
1066 let log = Log::new(Some(&status_tx));
1067 log.debug(format!(
1069 "OSPF sent {} bytes to {}",
1070 packet.len(),
1071 dest_ip
1072 ));
1073 log.trace(format!(
1074 "OSPF sent (hex): {}",
1075 hex::encode(&packet)
1076 ));
1077 }
1078 Err(e) => {
1079 Log::new(Some(&status_tx))
1080 .error(format!("OSPF send error: {}", e));
1081 }
1082 }
1083 }
1084 Err(e) => {
1085 Log::new(Some(&status_tx))
1086 .error(format!("OSPF packet build error: {}", e));
1087 }
1088 }
1089 continue;
1090 }
1091 }
1092
1093 if let Some(output_data) = protocol_result.get_all_output().first() {
1095 match Self::send_ospf_packet(
1097 ospf_state.socket_fd,
1098 OSPF_ALL_SPF_ROUTERS,
1099 output_data,
1100 ) {
1101 Ok(()) => {
1102 packets_sent += 1;
1103 Log::new(Some(&status_tx)).debug(format!(
1104 "OSPF sent {} bytes to multicast (224.0.0.5)",
1105 output_data.len()
1106 ));
1107 }
1108 Err(e) => {
1109 Log::new(Some(&status_tx)).error(format!("OSPF send error: {}", e));
1110 }
1111 }
1112 }
1113 }
1114
1115 if packets_sent == 0 {
1116 let decision = if model_chose_to_wait {
1120 "model_wait"
1121 } else {
1122 "model_no_action"
1123 };
1124 Log::new(Some(&status_tx)).info(format!(
1125 "OSPF {} answered with no packet: decision={} (OSPF has no error \
1126 packet type; staying silent is the protocol-correct response)",
1127 event_id, decision
1128 ));
1129 }
1130 }
1131 Err(e) => {
1132 let category = crate::utils::WireFailure::classify(&e);
1141 let decision = if category.is_overloaded() {
1142 "fail_closed_overloaded"
1143 } else {
1144 "fail_closed_unavailable"
1145 };
1146 Log::new(Some(&status_tx)).error(format!(
1147 "OSPF {} not answered: decision={} ({}) — no packet sent, OSPF has no \
1148 failure packet type; LLM error: {}",
1149 event_id,
1150 decision,
1151 category.text(),
1152 e
1153 ));
1154 }
1155 }
1156
1157 Ok(())
1158 }
1159
1160 #[cfg(unix)]
1162 pub fn send_ospf_packet(socket_fd: i32, dest_ip: Ipv4Addr, ospf_data: &[u8]) -> Result<()> {
1163 unsafe {
1164 let mut dest_addr = std::mem::zeroed::<libc::sockaddr_in>();
1165 #[cfg(target_os = "macos")]
1166 {
1167 dest_addr.sin_family = libc::AF_INET as libc::sa_family_t;
1168 dest_addr.sin_len = std::mem::size_of::<libc::sockaddr_in>() as u8;
1169 }
1170 #[cfg(not(target_os = "macos"))]
1171 {
1172 dest_addr.sin_family = libc::AF_INET as u16;
1173 }
1174 dest_addr.sin_port = 0; dest_addr.sin_addr.s_addr = u32::from(dest_ip).to_be();
1176
1177 let n = libc::sendto(
1178 socket_fd,
1179 ospf_data.as_ptr() as *const libc::c_void,
1180 ospf_data.len(),
1181 0,
1182 &dest_addr as *const _ as *const libc::sockaddr,
1183 std::mem::size_of::<libc::sockaddr_in>() as u32,
1184 );
1185
1186 if n < 0 {
1187 return Err(std::io::Error::last_os_error().into());
1188 }
1189 }
1190
1191 Ok(())
1192 }
1193}
1194
1195#[cfg(unix)]
1201async fn operator_wants_dynamic(
1202 state: &AppState,
1203 server_id: crate::state::ServerId,
1204 event_id: &str,
1205) -> bool {
1206 state
1207 .with_server_mut(server_id, |server| {
1208 let has_instruction = !server.instruction.trim().is_empty();
1209 let has_handler = server
1210 .event_handler_config
1211 .as_ref()
1212 .map(|c| c.find_handler(event_id).is_some())
1213 .unwrap_or(false);
1214 has_instruction || has_handler
1215 })
1216 .await
1217 .unwrap_or(false)
1218}