Skip to main content

netget/client/ospf/
mod.rs

1//! OSPF client implementation - Query mode for topology discovery
2//!
3//! This OSPF client joins an OSPF network to query and monitor routers.
4//! It can send Hello packets, request LSDB info, and parse LSAs for topology analysis.
5//!
6//! **Prerequisites**: Root/CAP_NET_RAW privileges for raw socket access
7//! **Use case**: Network topology discovery, OSPF monitoring, route analysis
8
9pub mod actions;
10
11pub use actions::OspfClientProtocol;
12
13use anyhow::{anyhow, Context, Result};
14use std::net::{IpAddr, Ipv4Addr, SocketAddr};
15use std::os::unix::io::AsRawFd;
16use std::sync::Arc;
17use tokio::io::unix::AsyncFd;
18use tokio::sync::{mpsc, Mutex};
19use tracing::{debug, error, info, trace, warn};
20
21use crate::client::llm_budget::call_llm_for_client;
22use crate::client::ospf::actions::{
23    OSPF_CLIENT_CONNECTED_EVENT, OSPF_CLIENT_DD_RECEIVED_EVENT, OSPF_CLIENT_HELLO_RECEIVED_EVENT,
24    OSPF_CLIENT_LSU_RECEIVED_EVENT,
25};
26use crate::llm::actions::client_trait::{Client, ClientActionResult};
27use crate::llm::actions::protocol_trait::Protocol;
28use crate::llm::ollama_client::OllamaClient;
29use crate::protocol::{Event, StartupParams};
30use crate::server::socket_helpers::create_ospf_raw_socket;
31use crate::state::app_state::AppState;
32use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
33use crate::state::{AccessLogOwner, ClientId, ClientStatus};
34
35// OSPF Constants
36const OSPF_VERSION: u8 = 2;
37const OSPF_HEADER_LEN: usize = 24;
38const IP_HEADER_MIN_LEN: usize = 20;
39
40// OSPF Packet Types
41const OSPF_TYPE_HELLO: u8 = 1;
42const OSPF_TYPE_DATABASE_DESCRIPTION: u8 = 2;
43const OSPF_TYPE_LINK_STATE_REQUEST: u8 = 3;
44const OSPF_TYPE_LINK_STATE_UPDATE: u8 = 4;
45const OSPF_TYPE_LINK_STATE_ACK: u8 = 5;
46
47// OSPF Multicast addresses
48const OSPF_ALL_SPF_ROUTERS: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 5);
49const OSPF_ALL_DROUTERS: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 6);
50
51/// Connection state for LLM processing
52#[derive(Debug, Clone, PartialEq)]
53enum ConnectionState {
54    Idle,
55    Processing,
56    Accumulating,
57}
58
59/// Queued OSPF packet with metadata
60struct QueuedPacket {
61    packet_type: u8,
62    ospf_data: Vec<u8>,
63    src_ip: Ipv4Addr,
64    sender_router_id: String,
65    sender_area_id: String,
66}
67
68/// Per-client data for LLM handling
69struct ClientData {
70    state: ConnectionState,
71    queued_packets: Vec<QueuedPacket>,
72    memory: String,
73}
74
75/// OSPF client
76pub struct OspfClient;
77
78impl OspfClient {
79    /// Connect to OSPF network with integrated LLM actions
80    pub async fn connect_with_llm_actions(
81        remote_addr: String,
82        llm_client: OllamaClient,
83        app_state: Arc<AppState>,
84        status_tx: mpsc::UnboundedSender<String>,
85        client_id: ClientId,
86        startup_params: Option<StartupParams>,
87    ) -> Result<SocketAddr> {
88        // Parse interface IP from remote_addr
89        // Format expected: "interface_ip" or "interface_ip:0"
90        let interface_ip = remote_addr
91            .split(':')
92            .next()
93            .ok_or_else(|| anyhow!("Invalid remote_addr format"))?
94            .parse::<Ipv4Addr>()
95            .context("Failed to parse interface IP")?;
96
97        // Extract configuration
98        let (router_id, area_id) = if let Some(ref params) = startup_params {
99            let router_id = params
100                .get_optional_string("router_id")?
101                .unwrap_or_else(|| interface_ip.to_string());
102            let area_id = params
103                .get_optional_string("area_id")?
104                .unwrap_or_else(|| "0.0.0.0".to_string());
105            (router_id, area_id)
106        } else {
107            (interface_ip.to_string(), "0.0.0.0".to_string())
108        };
109
110        // Create raw OSPF socket
111        let raw_socket = create_ospf_raw_socket(interface_ip, true, false)
112            .context("Failed to create OSPF raw socket (requires root)")?;
113        let socket_fd = raw_socket.as_raw_fd();
114
115        info!(
116            "OSPF client {} connected on interface {} (router_id={}, area={})",
117            client_id, interface_ip, router_id, area_id
118        );
119
120        // Update client state
121        app_state
122            .update_client_status(client_id, ClientStatus::Connected)
123            .await;
124        let _ = status_tx.send(format!(
125            "[CLIENT] OSPF client {} on {} (requires root)",
126            client_id, interface_ip
127        ));
128        let _ = status_tx.send("__UPDATE_UI__".to_string());
129
130        let local_addr = SocketAddr::new(IpAddr::V4(interface_ip), 0);
131
132        // Initialize client data
133        let client_data = Arc::new(Mutex::new(ClientData {
134            state: ConnectionState::Idle,
135            queued_packets: Vec::new(),
136            memory: String::new(),
137        }));
138
139        // Wrap socket for async I/O
140        let async_socket = AsyncFd::new(raw_socket)?;
141
142        // Send connected event to LLM
143        let connected_event = Event::new(
144            &OSPF_CLIENT_CONNECTED_EVENT,
145            serde_json::json!({
146                "interface_ip": interface_ip.to_string(),
147                "router_id": router_id.clone(),
148            }),
149        );
150
151        let llm_clone = llm_client.clone();
152        let app_state_clone = app_state.clone();
153        let status_tx_clone = status_tx.clone();
154        let protocol = Arc::new(OspfClientProtocol::new());
155        let protocol_for_loop = protocol.clone();
156        let protocol_for_commands = protocol.clone();
157
158        // Command channel for injected actions (the dashboard's [ send ]). Registered BEFORE
159        // the connected-event LLM call below, which a manual `*` routing rule can park for
160        // minutes - the operator must be able to reach the client while it waits.
161        let command_rx =
162            crate::client::command_support::register_command_channel(&app_state, client_id).await;
163
164        // Registered with AppState so stop_client can abort this task —
165        // dropping a JoinHandle only detaches it in Tokio.
166        let task_registrar = app_state.clone();
167        let task_handle = tokio::spawn(async move {
168            if let Some(instruction) = app_state_clone.get_instruction_for_client(client_id).await {
169                match call_llm_for_client(
170                    &llm_clone,
171                    &app_state_clone,
172                    client_id.to_string(),
173                    &instruction,
174                    "",
175                    Some(&connected_event),
176                    protocol.as_ref(),
177                    &status_tx_clone,
178                )
179                .await
180                {
181                    Ok(result) => {
182                        if let Some(memory) = result.memory_updates {
183                            app_state_clone
184                                .set_memory_for_client(client_id, memory)
185                                .await;
186                        }
187                        // Executed, not counted. This is the model's one chance to act before
188                        // any packet arrives — `send_hello` to discover neighbours is the
189                        // protocol's own worked example — and the answer used to be dropped.
190                        Self::run_actions(
191                            result.actions,
192                            protocol.as_ref(),
193                            socket_fd,
194                            client_id,
195                            &app_state_clone,
196                            &status_tx_clone,
197                        )
198                        .await;
199                    }
200                    Err(e) => {
201                        error!("OSPF client {} LLM call failed: {}", client_id, e);
202                        let _ = status_tx_clone
203                            .send(format!("✗ OSPF client {} LLM error: {}", client_id, e));
204                    }
205                }
206            }
207        });
208        task_registrar
209            .register_client_task(client_id, task_handle)
210            .await;
211
212        // Spawn receive loop
213        let socket_fd_for_send = socket_fd;
214        let app_state_for_commands = app_state.clone();
215        let status_tx_for_commands = status_tx.clone();
216        let protocol = protocol_for_loop;
217        // Registered with AppState so stop_client can abort this task —
218        // dropping a JoinHandle only detaches it in Tokio.
219        let task_registrar = app_state.clone();
220        let task_handle = tokio::spawn(async move {
221            let mut buffer = vec![0u8; 65535];
222
223            loop {
224                let mut guard = match async_socket.readable().await {
225                    Ok(guard) => guard,
226                    Err(e) => {
227                        error!("OSPF client {} socket error: {}", client_id, e);
228                        break;
229                    }
230                };
231
232                match guard.try_io(|inner| {
233                    let fd = inner.as_raw_fd();
234                    unsafe {
235                        let n = libc::recv(
236                            fd,
237                            buffer.as_mut_ptr() as *mut libc::c_void,
238                            buffer.len(),
239                            0,
240                        );
241
242                        if n < 0 {
243                            Err(std::io::Error::last_os_error())
244                        } else {
245                            Ok(n as usize)
246                        }
247                    }
248                }) {
249                    Ok(Ok(n)) => {
250                        if n == 0 {
251                            continue;
252                        }
253
254                        // Skip IP header
255                        if n < IP_HEADER_MIN_LEN {
256                            continue;
257                        }
258
259                        let ip_header_len = ((buffer[0] & 0x0F) * 4) as usize;
260                        if n < ip_header_len + OSPF_HEADER_LEN {
261                            continue;
262                        }
263
264                        // Extract source IP from IP header
265                        let src_ip = Ipv4Addr::new(buffer[12], buffer[13], buffer[14], buffer[15]);
266
267                        // Extract OSPF packet
268                        let ospf_data = &buffer[ip_header_len..n];
269                        let version = ospf_data[0];
270                        let packet_type = ospf_data[1];
271
272                        if version != OSPF_VERSION {
273                            continue;
274                        }
275
276                        // Extract router ID and area ID
277                        let sender_router_id = format!(
278                            "{}.{}.{}.{}",
279                            ospf_data[4], ospf_data[5], ospf_data[6], ospf_data[7]
280                        );
281
282                        let sender_area_id = format!(
283                            "{}.{}.{}.{}",
284                            ospf_data[8], ospf_data[9], ospf_data[10], ospf_data[11]
285                        );
286
287                        debug!(
288                            "OSPF client {} received type={} from {} ({})",
289                            client_id, packet_type, src_ip, sender_router_id
290                        );
291
292                        // Handle packet with LLM
293                        let mut client_data_lock = client_data.lock().await;
294
295                        match client_data_lock.state {
296                            ConnectionState::Idle => {
297                                // Process immediately
298                                client_data_lock.state = ConnectionState::Processing;
299                                drop(client_data_lock);
300
301                                Self::process_ospf_packet(
302                                    packet_type,
303                                    ospf_data.to_vec(),
304                                    src_ip,
305                                    sender_router_id,
306                                    sender_area_id,
307                                    llm_client.clone(),
308                                    app_state.clone(),
309                                    status_tx.clone(),
310                                    protocol.clone(),
311                                    client_id,
312                                    client_data.clone(),
313                                    socket_fd_for_send,
314                                )
315                                .await;
316                            }
317                            ConnectionState::Processing => {
318                                // Queue packet with metadata
319                                client_data_lock.queued_packets.push(QueuedPacket {
320                                    packet_type,
321                                    ospf_data: ospf_data.to_vec(),
322                                    src_ip,
323                                    sender_router_id: sender_router_id.clone(),
324                                    sender_area_id: sender_area_id.clone(),
325                                });
326                                client_data_lock.state = ConnectionState::Accumulating;
327                                trace!("OSPF client {} queued packet", client_id);
328                            }
329                            ConnectionState::Accumulating => {
330                                // Continue queuing with metadata
331                                client_data_lock.queued_packets.push(QueuedPacket {
332                                    packet_type,
333                                    ospf_data: ospf_data.to_vec(),
334                                    src_ip,
335                                    sender_router_id: sender_router_id.clone(),
336                                    sender_area_id: sender_area_id.clone(),
337                                });
338                            }
339                        }
340                    }
341                    Ok(Err(e)) => {
342                        error!("OSPF client {} recv error: {}", client_id, e);
343                    }
344                    Err(_would_block) => continue,
345                }
346            }
347
348            warn!("OSPF client {} receive loop terminated", client_id);
349            app_state
350                .update_client_status(client_id, ClientStatus::Disconnected)
351                .await;
352            // Every exit path lands here: drop the command handle so the dashboard stops
353            // offering [ send ] on a dead client, and a late send fails fast.
354            app_state.remove_client_handle(client_id).await;
355            let _ = status_tx.send(format!("[CLIENT] OSPF client {} disconnected", client_id));
356            let _ = status_tx.send("__UPDATE_UI__".to_string());
357        });
358        // Kept so an injected `disconnect` can actually stop the receive loop and release
359        // the raw socket - OSPF has no wire close to send.
360        let read_abort = task_handle.abort_handle();
361        task_registrar
362            .register_client_task(client_id, task_handle)
363            .await;
364
365        // The receive loop awaits `call_llm_for_client` inline, so a `select!` arm there
366        // would stall for the whole LLM round-trip. Drain commands in their own registered
367        // task instead; both use the same raw socket fd.
368        let cmd_task = tokio::spawn(Self::command_loop(
369            command_rx,
370            protocol_for_commands,
371            socket_fd_for_send,
372            client_id,
373            app_state_for_commands,
374            status_tx_for_commands,
375            read_abort,
376        ));
377        task_registrar
378            .register_client_task(client_id, cmd_task)
379            .await;
380
381        Ok(local_addr)
382    }
383
384    /// Drain injected commands until the channel closes (client removed) or an injected
385    /// `disconnect` ends the session.
386    ///
387    /// The generic `command_support::handle_stream_client_command` cannot serve this client:
388    /// every OSPF verb yields `ClientActionResult::Custom` and the transport is a raw IP
389    /// socket driven through `libc::sendto`, not an `AsyncWrite`. The action goes through
390    /// [`Self::apply_action`] - the same function the receive loop uses.
391    async fn command_loop(
392        mut command_rx: tokio::sync::mpsc::Receiver<ClientCommand>,
393        protocol: Arc<OspfClientProtocol>,
394        socket_fd: i32,
395        client_id: ClientId,
396        app_state: Arc<AppState>,
397        status_tx: mpsc::UnboundedSender<String>,
398        read_abort: tokio::task::AbortHandle,
399    ) {
400        while let Some(command) = command_rx.recv().await {
401            let action = command.action.clone();
402            let outcome = match protocol.execute_action(action.clone()) {
403                Err(e) => Ok(ClientSendOutcome::Rejected {
404                    error: e.to_string(),
405                }),
406                Ok(result) => {
407                    Self::apply_action(client_id, result, socket_fd).map(|applied| match applied {
408                        Applied::Disconnect => ClientSendOutcome::Disconnected,
409                        Applied::Sent(0) => ClientSendOutcome::Executed {
410                            detail: "executed (no packet sent)".to_string(),
411                        },
412                        Applied::Sent(bytes_sent) => ClientSendOutcome::Sent { bytes_sent },
413                        Applied::Nothing(detail) => ClientSendOutcome::Executed { detail },
414                    })
415                }
416            };
417
418            let outcome_json = match &outcome {
419                Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
420                Err(e) => serde_json::json!({"error": e.to_string()}),
421            };
422            app_state
423                .record_access_log(
424                    AccessLogOwner::Client(client_id.as_u32()),
425                    protocol.protocol_name(),
426                    None,
427                    "injected_action",
428                    action,
429                    vec![outcome_json],
430                )
431                .await;
432
433            let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
434            if let Err(e) = &outcome {
435                error!("OSPF client {} injected action failed: {}", client_id, e);
436                let _ = status_tx.send(format!(
437                    "[WARN] Client {} injected action failed: {}",
438                    client_id, e
439                ));
440            }
441            let _ = status_tx.send("__UPDATE_UI__".to_string());
442            crate::client::command_support::reply(command, outcome);
443
444            if disconnect {
445                read_abort.abort();
446                app_state.remove_client_handle(client_id).await;
447                app_state
448                    .update_client_status(client_id, ClientStatus::Disconnected)
449                    .await;
450                let _ = status_tx.send("__UPDATE_UI__".to_string());
451                break;
452            }
453        }
454    }
455
456    /// Apply one executed action. Shared by the receive loop and injected commands so the
457    /// OSPF packet building and `sendto` exist exactly once.
458    /// Carry out what the model answered with. Returns `true` if it asked to disconnect.
459    ///
460    /// Shared by the connected-event task and the receive loop. The connected-event path used
461    /// to be an `if let Err(..) = call_llm_for_client(..)` with no success arm at all, so the
462    /// model's answer to "you are on the wire now" — the one moment it is asked what to do
463    /// before any packet arrives, and the moment its own docs show it choosing `send_hello` to
464    /// discover neighbours — was dropped entirely. Everything below already existed inline in
465    /// `process_ospf_packet`; extracting it is what lets the other caller reach it.
466    async fn run_actions(
467        actions: Vec<serde_json::Value>,
468        protocol: &OspfClientProtocol,
469        socket_fd: i32,
470        client_id: ClientId,
471        app_state: &Arc<AppState>,
472        status_tx: &mpsc::UnboundedSender<String>,
473    ) -> bool {
474        for action in actions {
475            match protocol.execute_action(action) {
476                Ok(action_result) => {
477                    match Self::apply_action(client_id, action_result, socket_fd) {
478                        Ok(Applied::Disconnect) => {
479                            app_state.remove_client_handle(client_id).await;
480                            app_state
481                                .update_client_status(client_id, ClientStatus::Disconnected)
482                                .await;
483                            let _ = status_tx
484                                .send(format!("[CLIENT] OSPF client {} disconnected", client_id));
485                            let _ = status_tx.send("__UPDATE_UI__".to_string());
486                            return true;
487                        }
488                        Ok(Applied::Nothing(detail)) => {
489                            trace!("OSPF client {}: {}", client_id, detail);
490                        }
491                        Ok(Applied::Sent(_)) => {}
492                        Err(e) => {
493                            error!("Failed to execute OSPF action: {}", e);
494                            let _ = status_tx.send(format!("✗ OSPF action error: {}", e));
495                        }
496                    }
497                }
498                Err(e) => {
499                    error!("Failed to execute action: {}", e);
500                    let _ = status_tx.send(format!(
501                        "✗ OSPF client {} rejected action: {}",
502                        client_id, e
503                    ));
504                }
505            }
506        }
507        false
508    }
509
510    fn apply_action(
511        client_id: ClientId,
512        result: ClientActionResult,
513        socket_fd: i32,
514    ) -> Result<Applied> {
515        match result {
516            ClientActionResult::Custom { name, data } if name.starts_with("ospf_") => {
517                let sent = Self::handle_ospf_action(&name, &data, socket_fd)?;
518                Ok(Applied::Sent(sent))
519            }
520            ClientActionResult::Disconnect => {
521                info!("OSPF client {} disconnecting", client_id);
522                Ok(Applied::Disconnect)
523            }
524            ClientActionResult::WaitForMore => Ok(Applied::Nothing("wait_for_more".to_string())),
525            ClientActionResult::Custom { name, .. } => Ok(Applied::Nothing(format!(
526                "custom result '{name}' is not an OSPF verb; nothing sent"
527            ))),
528            other => Ok(Applied::Nothing(format!(
529                "action result {other:?} produced no packet"
530            ))),
531        }
532    }
533
534    #[allow(clippy::too_many_arguments)]
535    async fn process_ospf_packet(
536        packet_type: u8,
537        ospf_data: Vec<u8>,
538        src_ip: Ipv4Addr,
539        sender_router_id: String,
540        sender_area_id: String,
541        llm_client: OllamaClient,
542        app_state: Arc<AppState>,
543        status_tx: mpsc::UnboundedSender<String>,
544        protocol: Arc<OspfClientProtocol>,
545        client_id: ClientId,
546        client_data: Arc<Mutex<ClientData>>,
547        socket_fd: i32,
548    ) {
549        let event = match packet_type {
550            OSPF_TYPE_HELLO => {
551                Self::parse_hello_packet(&ospf_data, &src_ip, &sender_router_id, &sender_area_id)
552            }
553            OSPF_TYPE_DATABASE_DESCRIPTION => Self::parse_dd_packet(&ospf_data, &sender_router_id),
554            OSPF_TYPE_LINK_STATE_UPDATE => Self::parse_lsu_packet(&ospf_data, &sender_router_id),
555            OSPF_TYPE_LINK_STATE_REQUEST => {
556                debug!("OSPF LSR from {}", sender_router_id);
557                None
558            }
559            OSPF_TYPE_LINK_STATE_ACK => {
560                trace!("OSPF LSAck from {}", sender_router_id);
561                None
562            }
563            _ => {
564                warn!("OSPF unknown type: {}", packet_type);
565                None
566            }
567        };
568
569        if let Some(event) = event {
570            // Get current memory and instruction
571            let memory = client_data.lock().await.memory.clone();
572
573            if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
574                // Call LLM
575                match call_llm_for_client(
576                    &llm_client,
577                    &app_state,
578                    client_id.to_string(),
579                    &instruction,
580                    &memory,
581                    Some(&event),
582                    protocol.as_ref(),
583                    &status_tx,
584                )
585                .await
586                {
587                    Ok(result) => {
588                        // Update memory if provided
589                        if let Some(new_memory) = result.memory_updates {
590                            client_data.lock().await.memory = new_memory;
591                        }
592
593                        // Execute actions
594                        if Self::run_actions(
595                            result.actions,
596                            protocol.as_ref(),
597                            socket_fd,
598                            client_id,
599                            &app_state,
600                            &status_tx,
601                        )
602                        .await
603                        {
604                            return;
605                        }
606                    }
607                    Err(e) => {
608                        error!("OSPF client {} LLM call failed: {}", client_id, e);
609                        let _ =
610                            status_tx.send(format!("✗ OSPF client {} LLM error: {}", client_id, e));
611                    }
612                }
613            }
614        }
615
616        // Process queued packets if any
617        let mut client_data_lock = client_data.lock().await;
618        match client_data_lock.state {
619            ConnectionState::Processing => {
620                // No queued packets, back to idle
621                client_data_lock.state = ConnectionState::Idle;
622            }
623            ConnectionState::Accumulating => {
624                // Process first queued packet
625                let has_queued = !client_data_lock.queued_packets.is_empty();
626                if has_queued {
627                    let queued_packet = client_data_lock.queued_packets.remove(0);
628                    client_data_lock.state = ConnectionState::Processing;
629                    drop(client_data_lock);
630
631                    // Recursively process the queued packet with all metadata
632                    trace!("OSPF client {} processing queued packet", client_id);
633                    Box::pin(Self::process_ospf_packet(
634                        queued_packet.packet_type,
635                        queued_packet.ospf_data,
636                        queued_packet.src_ip,
637                        queued_packet.sender_router_id,
638                        queued_packet.sender_area_id,
639                        llm_client,
640                        app_state,
641                        status_tx,
642                        protocol,
643                        client_id,
644                        client_data,
645                        socket_fd,
646                    ))
647                    .await;
648                } else {
649                    client_data_lock.state = ConnectionState::Idle;
650                }
651            }
652            ConnectionState::Idle => {}
653        }
654    }
655
656    fn parse_hello_packet(
657        data: &[u8],
658        src_ip: &Ipv4Addr,
659        sender_router_id: &str,
660        sender_area_id: &str,
661    ) -> Option<Event> {
662        if data.len() < OSPF_HEADER_LEN + 20 {
663            return None;
664        }
665
666        // Parse Hello fields
667        let network_mask = format!("{}.{}.{}.{}", data[24], data[25], data[26], data[27]);
668        let hello_interval = u16::from_be_bytes([data[28], data[29]]);
669        let priority = data[31];
670        let router_dead_interval = u32::from_be_bytes([data[32], data[33], data[34], data[35]]);
671        let dr = format!("{}.{}.{}.{}", data[36], data[37], data[38], data[39]);
672        let bdr = format!("{}.{}.{}.{}", data[40], data[41], data[42], data[43]);
673
674        // Parse neighbors
675        let mut neighbor_list = Vec::new();
676        let mut offset = 44;
677        while offset + 4 <= data.len() {
678            let neighbor_id = format!(
679                "{}.{}.{}.{}",
680                data[offset],
681                data[offset + 1],
682                data[offset + 2],
683                data[offset + 3]
684            );
685            neighbor_list.push(neighbor_id);
686            offset += 4;
687        }
688
689        Some(Event::new(
690            &OSPF_CLIENT_HELLO_RECEIVED_EVENT,
691            serde_json::json!({
692                "neighbor_id": sender_router_id,
693                "neighbor_ip": src_ip.to_string(),
694                "area_id": sender_area_id,
695                "network_mask": network_mask,
696                "hello_interval": hello_interval,
697                "router_dead_interval": router_dead_interval,
698                "router_priority": priority,
699                "dr": dr,
700                "bdr": bdr,
701                "neighbors": neighbor_list,
702            }),
703        ))
704    }
705
706    fn parse_dd_packet(data: &[u8], sender_router_id: &str) -> Option<Event> {
707        if data.len() < OSPF_HEADER_LEN + 8 {
708            return None;
709        }
710
711        // Parse DD header (8 bytes after OSPF header)
712        let mtu = u16::from_be_bytes([data[24], data[25]]);
713        let options = data[26];
714        let flags = data[27];
715        let sequence = u32::from_be_bytes([data[28], data[29], data[30], data[31]]);
716
717        let init = (flags & 0x04) != 0;
718        let more = (flags & 0x02) != 0;
719        let master = (flags & 0x01) != 0;
720
721        debug!(
722            "OSPF DD: mtu={}, options={:02x}, seq={}, I={}, M={}, MS={}",
723            mtu, options, sequence, init, more, master
724        );
725
726        Some(Event::new(
727            &OSPF_CLIENT_DD_RECEIVED_EVENT,
728            serde_json::json!({
729                "neighbor_id": sender_router_id,
730                "sequence": sequence,
731                "init": init,
732                "more": more,
733                "master": master,
734            }),
735        ))
736    }
737
738    fn parse_lsu_packet(data: &[u8], sender_router_id: &str) -> Option<Event> {
739        if data.len() < OSPF_HEADER_LEN + 4 {
740            return None;
741        }
742
743        // Parse LSU header (4 bytes: number of LSAs)
744        let advertised_count = u32::from_be_bytes([data[24], data[25], data[26], data[27]]);
745
746        // Walk the LSAs by each header's own length field rather than trusting the
747        // advertised count, so a peer cannot make us read past the packet.
748        //
749        // This client exists for topology discovery and the Link State Update is the packet
750        // that carries the topology - yet the event used to report only how many LSAs had
751        // arrived and threw the LSAs themselves away, so the model was told "6 LSAs" and
752        // never what was in them. These are also the exact fields send_link_state_ack needs
753        // in order to acknowledge them.
754        let body = &data[28..];
755        let mut lsas = Vec::new();
756        let mut offset = 0usize;
757        while offset + 20 <= body.len() && lsas.len() < 64 {
758            let header = crate::server::ospf::actions::OspfProtocol::parse_lsa_headers(
759                &body[offset..offset + 20],
760                1,
761            );
762            let Some(header) = header.into_iter().next() else {
763                break;
764            };
765            let declared = header["length"].as_u64().unwrap_or(0) as usize;
766            lsas.push(header);
767            // A length below the header size would not advance us; stop rather than spin.
768            if declared < 20 {
769                break;
770            }
771            offset += declared;
772        }
773
774        debug!(
775            "OSPF LSU: {} LSAs advertised, {} parsed, from {}",
776            advertised_count,
777            lsas.len(),
778            sender_router_id
779        );
780
781        Some(Event::new(
782            &OSPF_CLIENT_LSU_RECEIVED_EVENT,
783            serde_json::json!({
784                "neighbor_id": sender_router_id,
785                "advertised_lsa_count": advertised_count,
786                "lsa_count": lsas.len(),
787                "lsa_headers": lsas,
788            }),
789        ))
790    }
791
792    /// Build and send one OSPF packet. Returns the number of bytes `sendto` accepted, so
793    /// an injected action can report a truthful `ClientSendOutcome::Sent`.
794    fn handle_ospf_action(
795        action_name: &str,
796        data: &serde_json::Value,
797        socket_fd: i32,
798    ) -> Result<usize> {
799        let packet = match action_name {
800            "ospf_send_hello" => {
801                crate::server::ospf::actions::OspfProtocol::build_hello_packet(data)?
802            }
803            "ospf_send_dd" => {
804                crate::server::ospf::actions::OspfProtocol::build_database_description_packet(data)?
805            }
806            "ospf_send_lsr" => {
807                crate::server::ospf::actions::OspfProtocol::build_link_state_request_packet(data)?
808            }
809            "ospf_send_lsack" => {
810                crate::server::ospf::actions::OspfProtocol::build_link_state_ack_packet(data)?
811            }
812            _ => return Err(anyhow!("Unknown OSPF action: {}", action_name)),
813        };
814
815        // Parse destination
816        let destination_str = data
817            .get("destination")
818            .and_then(|d| d.as_str())
819            .unwrap_or("multicast");
820
821        let dest_ip = match destination_str {
822            "multicast" => OSPF_ALL_SPF_ROUTERS,
823            "dr_multicast" => OSPF_ALL_DROUTERS,
824            ip_str => match ip_str.parse::<Ipv4Addr>() {
825                Ok(ip) => ip,
826                Err(_) => {
827                    // Falling back silently sent a packet the model meant for one router to
828                    // every OSPF speaker on the segment. Still send it - a typo should not
829                    // swallow the action - but say where it actually went.
830                    warn!(
831                        "OSPF client: destination '{}' is not an IP address; sending to \
832                         224.0.0.5 (all SPF routers) instead",
833                        ip_str
834                    );
835                    OSPF_ALL_SPF_ROUTERS
836                }
837            },
838        };
839
840        // Send packet
841        let sent = Self::send_ospf_packet(socket_fd, dest_ip, &packet)?;
842
843        debug!("OSPF sent {} bytes to {}", sent, dest_ip);
844        Ok(sent)
845    }
846
847    fn send_ospf_packet(socket_fd: i32, dest_ip: Ipv4Addr, ospf_data: &[u8]) -> Result<usize> {
848        unsafe {
849            let mut dest_addr = std::mem::zeroed::<libc::sockaddr_in>();
850            #[cfg(target_os = "macos")]
851            {
852                dest_addr.sin_family = libc::AF_INET as libc::sa_family_t;
853                dest_addr.sin_len = std::mem::size_of::<libc::sockaddr_in>() as u8;
854            }
855            #[cfg(not(target_os = "macos"))]
856            {
857                dest_addr.sin_family = libc::AF_INET as u16;
858            }
859            dest_addr.sin_port = 0; // Raw IP, no port
860            dest_addr.sin_addr.s_addr = u32::from(dest_ip).to_be();
861
862            let n = libc::sendto(
863                socket_fd,
864                ospf_data.as_ptr() as *const libc::c_void,
865                ospf_data.len(),
866                0,
867                &dest_addr as *const _ as *const libc::sockaddr,
868                std::mem::size_of::<libc::sockaddr_in>() as u32,
869            );
870
871            if n < 0 {
872                return Err(std::io::Error::last_os_error().into());
873            }
874
875            Ok(n as usize)
876        }
877    }
878}
879
880/// What [`OspfClient::apply_action`] did with one action.
881#[derive(Debug)]
882enum Applied {
883    /// Packet bytes `sendto` accepted (0 when nothing was sent).
884    Sent(usize),
885    /// Ran, but put nothing on the wire; the string says why.
886    Nothing(String),
887    /// The session should end.
888    Disconnect,
889}