Skip to main content

netget/server/ospf/
mod.rs

1//! OSPF protocol simulator - LLM-controlled OSPF responses
2//!
3//! This is an OSPF protocol simulator that speaks real OSPF (IP protocol 89)
4//! but has LLM-generated responses instead of real routing logic.
5//!
6//! **Use cases**: Testing, honeypots, route injection, OSPF reconnaissance
7//! **Requires**: Root/CAP_NET_RAW privileges for raw socket access
8
9pub 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
44// OSPF Constants
45const OSPF_VERSION: u8 = 2;
46const OSPF_HEADER_LEN: usize = 24;
47const IP_HEADER_MIN_LEN: usize = 20;
48
49// OSPF Packet Types
50const 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
56// OSPF Multicast addresses
57const 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/// OSPF neighbor information
62#[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/// Shared OSPF server state
77#[cfg(unix)]
78struct OspfState {
79    socket_fd: i32,
80    /// This interface's address: the local end of every neighbour connection published
81    /// to AppState.
82    interface_ip: Ipv4Addr,
83    /// Interface configuration from the startup parameters. Supplies the defaults for
84    /// every outgoing packet and the RFC 2328 10.5 acceptance check for incoming Hellos.
85    config: OspfInterfaceConfig,
86    neighbors: Arc<Mutex<HashMap<String, OspfNeighbor>>>,
87}
88
89/// OSPF server
90pub struct OspfServer;
91
92#[cfg(unix)]
93impl OspfServer {
94    /// Spawn OSPF server with LLM control
95    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        // Extract interface IP
104        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        // Create raw OSPF socket
110        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        // Extract interface configuration. Every declared startup parameter is read here
116        // and lands in OspfInterfaceConfig, which is consulted on both directions; four of
117        // the six used to be advertised to the model and read nowhere at all.
118        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        // The three dotted-quad parameters are declared "IPv4 format" and were accepted as
152        // any string at all. A typo did not fail here: it reached parse_ipv4 at send time,
153        // which silently substituted 0.0.0.0, so the server ran happily and advertised a
154        // Router ID nobody configured. Fail at startup, where the operator is looking.
155        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        // Wrap socket for async I/O
190        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                        // Skip IP header
228                        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                        // Extract source IP from IP header
238                        let src_ip = Ipv4Addr::new(buffer[12], buffer[13], buffer[14], buffer[15]);
239
240                        // Extract OSPF packet
241                        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                        // Extract router ID and area ID
250                        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                        // Handle packet
266                        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                        // Tracked, not detached: stop_server must abort this task too.
274                        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        // Age out neighbours we have not heard from within RouterDeadInterval, then find or
329        // create this one.
330        //
331        // The ageing is not cosmetic. The key is the sender's Router ID, four bytes chosen
332        // by whoever sent the packet, and nothing else ever removed an entry — a peer
333        // spraying Hellos with random Router IDs grew this map until the server stopped.
334        // RFC 2328 10.5 already says a neighbour silent for RouterDeadInterval is Down, so
335        // the bound and the correct behaviour are the same change.
336        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                    // The sender is alive by definition: this packet is from it.
344                    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        // A new neighbour needs an id, and `get_next_unified_id` takes the global AppState
362        // write lock. Allocating it while still holding the neighbour mutex would be a lock
363        // held across an await — the pattern this codebase forbids, and a lock-order
364        // inversion against every task that takes AppState first. An id allocated and then
365        // lost to a race costs nothing; it is a counter.
366        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                    // Another packet from the same neighbour won the race while we were
373                    // outside the lock.
374                    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        // Publish the neighbour to AppState. Until this existed the ConnectionId above was
396        // minted, handed to `call_llm` and written into every event, but never registered:
397        // `call_llm_inner` resolves the peer address by looking the connection up, so every
398        // OSPF event and access-log line recorded a null client address, the dashboard rail
399        // showed an OSPF server with no peers however much traffic it took, and the ↓/↑
400        // byte counters stayed at zero. The 10s idle sweep reaps these again — see
401        // `.connectionless()` in the metadata.
402        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        // Parse Hello fields
547        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        // Parse neighbors
555        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        // RFC 2328 10.5: HelloInterval, RouterDeadInterval and the network mask must match
575        // the receiving interface's, or the Hello is not acceptable and no adjacency can
576        // form. The configured values come from the startup parameters.
577        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        // Update neighbor state
591        {
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                // State transitions. A Hello that failed the 10.5 check does not advance
600                // the machine: a real router would never reach adjacency across such a
601                // mismatch, and reporting Init/2-Way here would be a lie the operator
602                // acts on.
603                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        // Send structured event to LLM. The event fires even for a rejected Hello, carrying
620        // the reason - a refusal the model can see and explain beats one it cannot.
621        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    /// Handle a Database Description packet (RFC 2328 A.3.3).
657    ///
658    /// Body layout after the 24-byte OSPF header:
659    /// InterfaceMTU(2) | Options(1) | Flags(1) | DDSequenceNumber(4) | LSA headers (20 each)
660    #[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    /// Handle a Link State Request packet (RFC 2328 A.3.4).
727    ///
728    /// Body is a repeating triple: LSType(4) | LinkStateID(4) | AdvertisingRouter(4)
729    #[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    /// Handle a Link State Update packet (RFC 2328 A.3.5).
789    ///
790    /// Body layout: NumberOfLSAs(4) followed by that many LSAs, each starting with a
791    /// 20-byte header whose `length` field covers header plus body.
792    #[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        // Walk the LSAs using each header's own length field rather than trusting the
815        // advertised count, so a peer cannot make us read past the packet.
816        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            // A length below the header size would not advance us; stop rather than spin.
826            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    /// Handle a Link State Acknowledgment packet (RFC 2328 A.3.6) - a list of LSA headers.
866    #[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    /// Ask the LLM how to answer an OSPF event and put whatever packets it chooses on the wire.
916    ///
917    /// Shared by every packet-type handler so they all get identical action handling, logging
918    /// and destination resolution.
919    #[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        // Whether to engage with an OSPF speaker at all — respond to a Hello, claim DR/BDR, run
932        // as a passive listener, or act as a honeypot — is a policy/engagement decision, not
933        // something the received packet determines. With no operator policy — no server
934        // instruction and no per-event handler — the spec-safe default is to listen passively
935        // and not respond, WITHOUT burning an LLM round-trip per received packet. The model is
936        // consulted only when the operator opts in with how the router should behave.
937        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                // Log LLM messages
959                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                // Distinguish, in the log, the model *choosing* silence from the model
969                // returning nothing usable. On the wire the two are identical — OSPF has no
970                // response for either — so the log is the only place the difference survives.
971                let mut packets_sent = 0usize;
972                let mut model_chose_to_wait = false;
973
974                // Process each protocol result (OSPF packets to send)
975                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                    // Check if this is a Custom result with OSPF action
984                    if let crate::llm::actions::protocol_trait::ActionResult::Custom {
985                        name,
986                        data,
987                    } = &protocol_result
988                    {
989                        if name == "ospf_action" {
990                            // Extract action type and destination
991                            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                            // Fill in whatever the model left out from the interface
999                            // configuration. Without this the builders fell back to
1000                            // hardcoded constants and the operator's hello_interval,
1001                            // router_dead_interval, network_mask and router_priority
1002                            // never reached the wire.
1003                            let mut data = data.clone();
1004                            ospf_state.config.apply_defaults(&mut data);
1005                            let data = &data;
1006
1007                            // Build packet from structured action data (no bytes in JSON!)
1008                            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                                    // Parse destination: "multicast", "dr_multicast", or IP address
1031                                    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                                    // Send packet to destination
1047                                    match Self::send_ospf_packet(
1048                                        ospf_state.socket_fd,
1049                                        dest_ip,
1050                                        &packet,
1051                                    ) {
1052                                        Ok(()) => {
1053                                            packets_sent += 1;
1054                                            // The rail's uparrow counter and the
1055                                            // connection-scoped task prompts read these.
1056                                            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                                            // Summary + hex payload are FileOnly (hot path).
1068                                            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                    // Fallback: Check for legacy Output results
1094                    if let Some(output_data) = protocol_result.get_all_output().first() {
1095                        // Send to default multicast
1096                        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                    // No packet went out. OSPF has no "I cannot answer" message, so the wire
1117                    // looks exactly like a passive listener either way; the decision tag is
1118                    // what tells an operator which of the two happened.
1119                    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                // The LLM call failed. OSPF defines no error or NAK packet — the five packet
1133                // types are Hello, DD, LSR, LSU, LSAck and every one of them is a positive
1134                // routing assertion. Fabricating any of them to signal "netget is broken"
1135                // would claim an adjacency, a DR role or database contents we cannot back,
1136                // which is strictly worse for the peer than silence: its own
1137                // RouterDeadInterval already handles a router that stops speaking. So this
1138                // path stays silent on the wire deliberately, and the failure is reported to
1139                // the operator here. Nothing derived from `e` reaches the socket.
1140                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    /// Send OSPF packet to destination
1161    #[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; // Raw IP, no port
1175            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/// Returns true if the operator opted into dynamic (LLM- or handler-driven) responses for this
1196/// server: either a non-empty server instruction was given, or an event handler is configured
1197/// for `event_id`. When false the server observes passively and never consults the model —
1198/// whether to respond to an OSPF speaker (and how: DR claim, honeypot, ...) is a policy decision
1199/// with no configured policy to apply.
1200#[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}