1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter, ParameterDefinition,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13use tracing::debug;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct OspfInterfaceConfig {
33 pub router_id: String,
34 pub area_id: String,
35 pub network_mask: String,
36 pub hello_interval: u16,
37 pub router_dead_interval: u32,
38 pub router_priority: u8,
39}
40
41impl Default for OspfInterfaceConfig {
42 fn default() -> Self {
43 Self {
45 router_id: "0.0.0.0".to_string(),
46 area_id: "0.0.0.0".to_string(),
47 network_mask: "255.255.255.0".to_string(),
48 hello_interval: 10,
49 router_dead_interval: 40,
50 router_priority: 1,
51 }
52 }
53}
54
55impl OspfInterfaceConfig {
56 pub fn apply_defaults(&self, action: &mut serde_json::Value) {
62 let Some(obj) = action.as_object_mut() else {
63 return;
64 };
65
66 obj.entry("router_id")
67 .or_insert_with(|| json!(self.router_id));
68 obj.entry("area_id").or_insert_with(|| json!(self.area_id));
69
70 if obj.get("type").and_then(|t| t.as_str()) == Some("send_hello") {
71 obj.entry("network_mask")
72 .or_insert_with(|| json!(self.network_mask));
73 obj.entry("hello_interval")
74 .or_insert_with(|| json!(self.hello_interval));
75 obj.entry("router_dead_interval")
76 .or_insert_with(|| json!(self.router_dead_interval));
77 obj.entry("priority")
78 .or_insert_with(|| json!(self.router_priority));
79 }
80 }
81
82 pub fn hello_mismatches(
91 &self,
92 hello_interval: u16,
93 router_dead_interval: u32,
94 network_mask: &str,
95 ) -> Vec<String> {
96 let mut out = Vec::new();
97 if hello_interval != self.hello_interval {
98 out.push(format!(
99 "hello_interval {} does not match our configured {}",
100 hello_interval, self.hello_interval
101 ));
102 }
103 if router_dead_interval != self.router_dead_interval {
104 out.push(format!(
105 "router_dead_interval {} does not match our configured {}",
106 router_dead_interval, self.router_dead_interval
107 ));
108 }
109 if network_mask != self.network_mask {
110 out.push(format!(
111 "network_mask {} does not match our configured {}",
112 network_mask, self.network_mask
113 ));
114 }
115 out
116 }
117}
118
119pub struct OspfProtocol;
121
122impl OspfProtocol {
123 pub fn new() -> Self {
124 Self
125 }
126
127 fn execute_send_hello(&self, action: serde_json::Value) -> Result<ActionResult> {
128 let router_id = action
129 .get("router_id")
130 .and_then(|v| v.as_str())
131 .unwrap_or("0.0.0.0");
132
133 let area_id = action
134 .get("area_id")
135 .and_then(|v| v.as_str())
136 .unwrap_or("0.0.0.0");
137
138 let _network_mask = action
139 .get("network_mask")
140 .and_then(|v| v.as_str())
141 .unwrap_or("255.255.255.0");
142
143 let _hello_interval = action
144 .get("hello_interval")
145 .and_then(|v| v.as_u64())
146 .unwrap_or(10) as u16;
147
148 let _router_dead_interval = action
149 .get("router_dead_interval")
150 .and_then(|v| v.as_u64())
151 .unwrap_or(40) as u32;
152
153 let priority = action.get("priority").and_then(|v| v.as_u64()).unwrap_or(1) as u8;
154
155 let _dr = action
156 .get("dr")
157 .and_then(|v| v.as_str())
158 .unwrap_or("0.0.0.0");
159
160 let _bdr = action
161 .get("bdr")
162 .and_then(|v| v.as_str())
163 .unwrap_or("0.0.0.0");
164
165 let destination = action
166 .get("destination")
167 .and_then(|v| v.as_str())
168 .unwrap_or("multicast")
169 .to_string();
170
171 debug!(
172 "OSPF sending Hello: router_id={}, area={}, priority={}, dest={}",
173 router_id, area_id, priority, destination
174 );
175
176 Ok(ActionResult::Custom {
178 name: "ospf_action".to_string(),
179 data: action.clone(),
180 })
181 }
182
183 fn execute_send_database_description(&self, action: serde_json::Value) -> Result<ActionResult> {
184 let _router_id = action
185 .get("router_id")
186 .and_then(|v| v.as_str())
187 .unwrap_or("0.0.0.0");
188
189 let _area_id = action
190 .get("area_id")
191 .and_then(|v| v.as_str())
192 .unwrap_or("0.0.0.0");
193
194 let sequence = action.get("sequence").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
195
196 let init = action
197 .get("init")
198 .and_then(|v| v.as_bool())
199 .unwrap_or(false);
200 let more = action
201 .get("more")
202 .and_then(|v| v.as_bool())
203 .unwrap_or(false);
204 let master = action
205 .get("master")
206 .and_then(|v| v.as_bool())
207 .unwrap_or(false);
208
209 let destination = action
210 .get("destination")
211 .and_then(|v| v.as_str())
212 .unwrap_or("multicast")
213 .to_string();
214
215 debug!(
216 "OSPF sending Database Description: seq={}, init={}, more={}, master={}, dest={}",
217 sequence, init, more, master, destination
218 );
219
220 Ok(ActionResult::Custom {
222 name: "ospf_action".to_string(),
223 data: action.clone(),
224 })
225 }
226
227 fn execute_send_link_state_request(&self, action: serde_json::Value) -> Result<ActionResult> {
228 let _router_id = action
229 .get("router_id")
230 .and_then(|v| v.as_str())
231 .unwrap_or("0.0.0.0");
232
233 let _area_id = action
234 .get("area_id")
235 .and_then(|v| v.as_str())
236 .unwrap_or("0.0.0.0");
237
238 let destination = action
239 .get("destination")
240 .and_then(|v| v.as_str())
241 .unwrap_or("multicast")
242 .to_string();
243
244 debug!("OSPF sending Link State Request to {}", destination);
245
246 Ok(ActionResult::Custom {
248 name: "ospf_action".to_string(),
249 data: action.clone(),
250 })
251 }
252
253 fn execute_send_link_state_update(&self, action: serde_json::Value) -> Result<ActionResult> {
254 let _router_id = action
255 .get("router_id")
256 .and_then(|v| v.as_str())
257 .unwrap_or("0.0.0.0");
258
259 let _area_id = action
260 .get("area_id")
261 .and_then(|v| v.as_str())
262 .unwrap_or("0.0.0.0");
263
264 let destination = action
265 .get("destination")
266 .and_then(|v| v.as_str())
267 .unwrap_or("multicast")
268 .to_string();
269
270 debug!("OSPF sending Link State Update to {}", destination);
271
272 Ok(ActionResult::Custom {
274 name: "ospf_action".to_string(),
275 data: action.clone(),
276 })
277 }
278
279 fn execute_send_link_state_ack(&self, action: serde_json::Value) -> Result<ActionResult> {
280 let _router_id = action
281 .get("router_id")
282 .and_then(|v| v.as_str())
283 .unwrap_or("0.0.0.0");
284
285 let _area_id = action
286 .get("area_id")
287 .and_then(|v| v.as_str())
288 .unwrap_or("0.0.0.0");
289
290 let destination = action
291 .get("destination")
292 .and_then(|v| v.as_str())
293 .unwrap_or("multicast")
294 .to_string();
295
296 debug!("OSPF sending Link State Acknowledgment to {}", destination);
297
298 Ok(ActionResult::Custom {
300 name: "ospf_action".to_string(),
301 data: action.clone(),
302 })
303 }
304
305 fn parse_ipv4(ip: &str) -> Result<[u8; 4]> {
314 ip.parse::<std::net::Ipv4Addr>()
315 .map(|addr| addr.octets())
316 .map_err(|_| {
317 anyhow::anyhow!("'{ip}' is not an IPv4 address in dotted-quad form (e.g. 1.1.1.1)")
318 })
319 }
320
321 pub fn parse_lsa_headers(body: &[u8], max: usize) -> Vec<serde_json::Value> {
332 const LSA_HEADER_LEN: usize = 20;
333 let mut out = Vec::new();
334 let mut offset = 0;
335 while offset + LSA_HEADER_LEN <= body.len() && out.len() < max {
336 let h = &body[offset..offset + LSA_HEADER_LEN];
337 let lsa_type = h[3];
338 out.push(json!({
339 "age": u16::from_be_bytes([h[0], h[1]]),
340 "options": h[2],
341 "lsa_type": lsa_type,
342 "lsa_type_name": match lsa_type {
343 1 => "router",
344 2 => "network",
345 3 => "summary_network",
346 4 => "summary_asbr",
347 5 => "as_external",
348 7 => "nssa_external",
349 _ => "unknown",
350 },
351 "link_state_id": format!("{}.{}.{}.{}", h[4], h[5], h[6], h[7]),
352 "advertising_router": format!("{}.{}.{}.{}", h[8], h[9], h[10], h[11]),
353 "sequence": u32::from_be_bytes([h[12], h[13], h[14], h[15]]),
354 "checksum": u16::from_be_bytes([h[16], h[17]]),
358 "length": u16::from_be_bytes([h[18], h[19]]),
359 }));
360 offset += LSA_HEADER_LEN;
361 }
362 out
363 }
364
365 fn push_lsa_header(msg: &mut Vec<u8>, header: &serde_json::Value) -> Result<()> {
368 let u16_field = |name: &str| header.get(name).and_then(|v| v.as_u64()).unwrap_or(0) as u16;
369 let ip_field = |name: &str| -> Result<[u8; 4]> {
370 Self::parse_ipv4(
371 header
372 .get(name)
373 .and_then(|v| v.as_str())
374 .unwrap_or("0.0.0.0"),
375 )
376 };
377
378 msg.extend_from_slice(&u16_field("age").to_be_bytes());
379 msg.push(header.get("options").and_then(|v| v.as_u64()).unwrap_or(0) as u8);
380 msg.push(header.get("lsa_type").and_then(|v| v.as_u64()).unwrap_or(1) as u8);
381 msg.extend_from_slice(&ip_field("link_state_id")?);
382 msg.extend_from_slice(&ip_field("advertising_router")?);
383 msg.extend_from_slice(
384 &(header
385 .get("sequence")
386 .and_then(|v| v.as_u64())
387 .unwrap_or(0x8000_0001) as u32)
388 .to_be_bytes(),
389 );
390 msg.extend_from_slice(&u16_field("checksum").to_be_bytes());
391 let length = match header.get("length").and_then(|v| v.as_u64()) {
394 Some(n) => n as u16,
395 None => 20,
396 };
397 msg.extend_from_slice(&length.to_be_bytes());
398 Ok(())
399 }
400
401 pub fn calculate_checksum(data: &[u8]) -> u16 {
427 let mut sum: u32 = 0;
428 let mut pending: Option<u8> = None;
433 for (i, &byte) in data.iter().enumerate() {
434 if (16..24).contains(&i) {
436 continue;
437 }
438 match pending.take() {
439 None => pending = Some(byte),
440 Some(hi) => sum += u16::from_be_bytes([hi, byte]) as u32,
441 }
442 }
443 if let Some(hi) = pending {
445 sum += u16::from_be_bytes([hi, 0]) as u32;
446 }
447 while (sum >> 16) != 0 {
448 sum = (sum & 0xffff) + (sum >> 16);
449 }
450 !(sum as u16)
451 }
452
453 pub fn build_hello_packet(action: &serde_json::Value) -> Result<Vec<u8>> {
455 let router_id = action
456 .get("router_id")
457 .and_then(|v| v.as_str())
458 .unwrap_or("0.0.0.0");
459 let area_id = action
460 .get("area_id")
461 .and_then(|v| v.as_str())
462 .unwrap_or("0.0.0.0");
463 let network_mask = action
464 .get("network_mask")
465 .and_then(|v| v.as_str())
466 .unwrap_or("255.255.255.0");
467 let hello_interval = action
468 .get("hello_interval")
469 .and_then(|v| v.as_u64())
470 .unwrap_or(10) as u16;
471 let router_dead_interval = action
472 .get("router_dead_interval")
473 .and_then(|v| v.as_u64())
474 .unwrap_or(40) as u32;
475 let priority = action.get("priority").and_then(|v| v.as_u64()).unwrap_or(1) as u8;
476 let dr = action
477 .get("dr")
478 .and_then(|v| v.as_str())
479 .unwrap_or("0.0.0.0");
480 let bdr = action
481 .get("bdr")
482 .and_then(|v| v.as_str())
483 .unwrap_or("0.0.0.0");
484
485 let mut msg = Vec::new();
486
487 msg.push(2); msg.push(1); msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&Self::parse_ipv4(router_id)?);
492 msg.extend_from_slice(&Self::parse_ipv4(area_id)?);
493 msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&[0; 8]); msg.extend_from_slice(&Self::parse_ipv4(network_mask)?);
499 msg.extend_from_slice(&hello_interval.to_be_bytes());
500 msg.push(0); msg.push(priority);
502 msg.extend_from_slice(&router_dead_interval.to_be_bytes());
503 msg.extend_from_slice(&Self::parse_ipv4(dr)?);
504 msg.extend_from_slice(&Self::parse_ipv4(bdr)?);
505
506 if let Some(neighbors) = action.get("neighbors").and_then(|v| v.as_array()) {
508 for neighbor in neighbors {
509 if let Some(neighbor_id) = neighbor.as_str() {
510 msg.extend_from_slice(&Self::parse_ipv4(neighbor_id)?);
511 }
512 }
513 }
514
515 let packet_len = msg.len() as u16;
517 msg[2..4].copy_from_slice(&packet_len.to_be_bytes());
518 msg[12..14].copy_from_slice(&[0, 0]);
522 let checksum = Self::calculate_checksum(&msg);
523 msg[12..14].copy_from_slice(&checksum.to_be_bytes());
524
525 Ok(msg)
526 }
527
528 pub fn build_database_description_packet(action: &serde_json::Value) -> Result<Vec<u8>> {
530 let router_id = action
531 .get("router_id")
532 .and_then(|v| v.as_str())
533 .unwrap_or("0.0.0.0");
534 let area_id = action
535 .get("area_id")
536 .and_then(|v| v.as_str())
537 .unwrap_or("0.0.0.0");
538 let sequence = action.get("sequence").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
539 let init = action
540 .get("init")
541 .and_then(|v| v.as_bool())
542 .unwrap_or(false);
543 let more = action
544 .get("more")
545 .and_then(|v| v.as_bool())
546 .unwrap_or(false);
547 let master = action
548 .get("master")
549 .and_then(|v| v.as_bool())
550 .unwrap_or(false);
551
552 let mut msg = Vec::new();
553
554 msg.push(2); msg.push(2); msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&Self::parse_ipv4(router_id)?);
559 msg.extend_from_slice(&Self::parse_ipv4(area_id)?);
560 msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&[0; 8]); msg.extend_from_slice(&[0, 0]); msg.push(0); let mut flags: u8 = 0;
568 if init {
569 flags |= 0x04;
570 }
571 if more {
572 flags |= 0x02;
573 }
574 if master {
575 flags |= 0x01;
576 }
577 msg.push(flags);
578 msg.extend_from_slice(&sequence.to_be_bytes());
579
580 if let Some(headers) = action.get("lsa_headers").and_then(|v| v.as_array()) {
583 for header in headers {
584 Self::push_lsa_header(&mut msg, header)?;
585 }
586 }
587
588 let packet_len = msg.len() as u16;
590 msg[2..4].copy_from_slice(&packet_len.to_be_bytes());
591 msg[12..14].copy_from_slice(&[0, 0]);
595 let checksum = Self::calculate_checksum(&msg);
596 msg[12..14].copy_from_slice(&checksum.to_be_bytes());
597
598 Ok(msg)
599 }
600
601 pub fn build_link_state_request_packet(action: &serde_json::Value) -> Result<Vec<u8>> {
607 let router_id = action
608 .get("router_id")
609 .and_then(|v| v.as_str())
610 .unwrap_or("0.0.0.0");
611 let area_id = action
612 .get("area_id")
613 .and_then(|v| v.as_str())
614 .unwrap_or("0.0.0.0");
615
616 let mut msg = Vec::new();
617
618 msg.push(2); msg.push(3); msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&Self::parse_ipv4(router_id)?);
623 msg.extend_from_slice(&Self::parse_ipv4(area_id)?);
624 msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&[0; 8]); if let Some(requests) = action.get("requests").and_then(|v| v.as_array()) {
630 for request in requests {
631 let lsa_type = request
632 .get("lsa_type")
633 .and_then(|v| v.as_u64())
634 .unwrap_or(1) as u32;
635 msg.extend_from_slice(&lsa_type.to_be_bytes());
636 msg.extend_from_slice(&Self::parse_ipv4(
637 request
638 .get("link_state_id")
639 .and_then(|v| v.as_str())
640 .unwrap_or("0.0.0.0"),
641 )?);
642 msg.extend_from_slice(&Self::parse_ipv4(
643 request
644 .get("advertising_router")
645 .and_then(|v| v.as_str())
646 .unwrap_or("0.0.0.0"),
647 )?);
648 }
649 }
650
651 let packet_len = msg.len() as u16;
653 msg[2..4].copy_from_slice(&packet_len.to_be_bytes());
654 msg[12..14].copy_from_slice(&[0, 0]);
658 let checksum = Self::calculate_checksum(&msg);
659 msg[12..14].copy_from_slice(&checksum.to_be_bytes());
660
661 Ok(msg)
662 }
663
664 pub fn build_link_state_update_packet(action: &serde_json::Value) -> Result<Vec<u8>> {
666 let router_id = action
667 .get("router_id")
668 .and_then(|v| v.as_str())
669 .unwrap_or("0.0.0.0");
670 let area_id = action
671 .get("area_id")
672 .and_then(|v| v.as_str())
673 .unwrap_or("0.0.0.0");
674
675 let mut msg = Vec::new();
676
677 msg.push(2); msg.push(4); msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&Self::parse_ipv4(router_id)?);
682 msg.extend_from_slice(&Self::parse_ipv4(area_id)?);
683 msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&[0; 8]); msg.extend_from_slice(&[0, 0, 0, 0]); let packet_len = msg.len() as u16;
694 msg[2..4].copy_from_slice(&packet_len.to_be_bytes());
695 msg[12..14].copy_from_slice(&[0, 0]);
699 let checksum = Self::calculate_checksum(&msg);
700 msg[12..14].copy_from_slice(&checksum.to_be_bytes());
701
702 Ok(msg)
703 }
704
705 pub fn build_link_state_ack_packet(action: &serde_json::Value) -> Result<Vec<u8>> {
717 let router_id = action
718 .get("router_id")
719 .and_then(|v| v.as_str())
720 .unwrap_or("0.0.0.0");
721 let area_id = action
722 .get("area_id")
723 .and_then(|v| v.as_str())
724 .unwrap_or("0.0.0.0");
725
726 let mut msg = Vec::new();
727
728 msg.push(2); msg.push(5); msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&Self::parse_ipv4(router_id)?);
733 msg.extend_from_slice(&Self::parse_ipv4(area_id)?);
734 msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&[0, 0]); msg.extend_from_slice(&[0; 8]); if let Some(headers) = action.get("lsa_headers").and_then(|v| v.as_array()) {
740 for header in headers {
741 Self::push_lsa_header(&mut msg, header)?;
742 }
743 }
744
745 let packet_len = msg.len() as u16;
747 msg[2..4].copy_from_slice(&packet_len.to_be_bytes());
748 msg[12..14].copy_from_slice(&[0, 0]);
752 let checksum = Self::calculate_checksum(&msg);
753 msg[12..14].copy_from_slice(&checksum.to_be_bytes());
754
755 Ok(msg)
756 }
757}
758
759fn send_hello_action() -> ActionDefinition {
767 ActionDefinition {
768 name: "send_hello".to_string(),
769 description: "Send OSPF Hello packet to discover/maintain neighbors".to_string(),
770 parameters: vec![
771 Parameter {
772 name: "router_id".to_string(),
773 type_hint: "string".to_string(),
774 description: "OSPF router ID (IPv4 format)".to_string(),
775 required: true,
776 },
777 Parameter {
778 name: "area_id".to_string(),
779 type_hint: "string".to_string(),
780 description: "OSPF area ID (IPv4 format, 0.0.0.0 = backbone)".to_string(),
781 required: true,
782 },
783 Parameter {
784 name: "network_mask".to_string(),
785 type_hint: "string".to_string(),
786 description: "Network mask (e.g., 255.255.255.0)".to_string(),
787 required: false,
788 },
789 Parameter {
790 name: "hello_interval".to_string(),
791 type_hint: "number".to_string(),
792 description: "Hello interval in seconds (default 10)".to_string(),
793 required: false,
794 },
795 Parameter {
796 name: "router_dead_interval".to_string(),
797 type_hint: "number".to_string(),
798 description: "Router dead interval in seconds (default 40)".to_string(),
799 required: false,
800 },
801 Parameter {
802 name: "priority".to_string(),
803 type_hint: "number".to_string(),
804 description: "Router priority for DR election (0-255, default 1)".to_string(),
805 required: false,
806 },
807 Parameter {
808 name: "dr".to_string(),
809 type_hint: "string".to_string(),
810 description: "Designated Router IP (0.0.0.0 if none)".to_string(),
811 required: false,
812 },
813 Parameter {
814 name: "bdr".to_string(),
815 type_hint: "string".to_string(),
816 description: "Backup Designated Router IP (0.0.0.0 if none)".to_string(),
817 required: false,
818 },
819 Parameter {
820 name: "neighbors".to_string(),
821 type_hint: "array".to_string(),
822 description: "List of neighbor router IDs".to_string(),
823 required: false,
824 },
825 Parameter {
826 name: "destination".to_string(),
827 type_hint: "string".to_string(),
828 description: "Destination IP: 'multicast' (default, 224.0.0.5), 'dr_multicast' (224.0.0.6), or unicast IP".to_string(),
829 required: false,
830 },
831 ],
832 example: json!({
833 "type": "send_hello",
834 "router_id": "1.1.1.1",
835 "area_id": "0.0.0.0",
836 "priority": 1,
837 "neighbors": ["2.2.2.2"],
838 "destination": "multicast"
839 }),
840 log_template: Some(
841 LogTemplate::new()
842 .with_info("-> OSPF Hello router={router_id} area={area_id}")
843 .with_debug("OSPF send_hello: router_id={router_id} area={area_id} priority={priority}"),
844 ),
845 }
846}
847
848fn send_database_description_action() -> ActionDefinition {
849 ActionDefinition {
850 name: "send_database_description".to_string(),
851 description: "Send OSPF Database Description packet".to_string(),
852 parameters: vec![
853 Parameter {
854 name: "router_id".to_string(),
855 type_hint: "string".to_string(),
856 description: "OSPF router ID".to_string(),
857 required: true,
858 },
859 Parameter {
860 name: "area_id".to_string(),
861 type_hint: "string".to_string(),
862 description: "OSPF area ID".to_string(),
863 required: true,
864 },
865 Parameter {
866 name: "sequence".to_string(),
867 type_hint: "number".to_string(),
868 description: "DD sequence number".to_string(),
869 required: true,
870 },
871 Parameter {
872 name: "init".to_string(),
873 type_hint: "boolean".to_string(),
874 description: "Init flag (true for first DD packet)".to_string(),
875 required: false,
876 },
877 Parameter {
878 name: "more".to_string(),
879 type_hint: "boolean".to_string(),
880 description: "More flag (true if more DD packets follow)".to_string(),
881 required: false,
882 },
883 Parameter {
884 name: "master".to_string(),
885 type_hint: "boolean".to_string(),
886 description: "Master flag (true if this router is master)".to_string(),
887 required: false,
888 },
889 Parameter {
890 name: "lsa_headers".to_string(),
891 type_hint: "array".to_string(),
892 description: "LSA headers summarising the database, each an object with \
893 lsa_type, link_state_id, advertising_router, sequence, age, \
894 options, checksum and length - the same shape the \
895 ospf_link_state_update event delivers. Omit for an empty \
896 summary."
897 .to_string(),
898 required: false,
899 },
900 Parameter {
901 name: "destination".to_string(),
902 type_hint: "string".to_string(),
903 description: "Destination IP: 'multicast' (default) or unicast IP".to_string(),
904 required: false,
905 },
906 ],
907 example: json!({
908 "type": "send_database_description",
909 "router_id": "1.1.1.1",
910 "area_id": "0.0.0.0",
911 "sequence": 1,
912 "init": true,
913 "master": true,
914 "destination": "192.168.1.2"
915 }),
916 log_template: Some(
917 LogTemplate::new()
918 .with_info("-> OSPF DD seq={sequence}")
919 .with_debug(
920 "OSPF send_database_description: router_id={router_id} seq={sequence} init={init}",
921 ),
922 ),
923 }
924}
925
926fn send_link_state_request_action() -> ActionDefinition {
927 ActionDefinition {
928 name: "send_link_state_request".to_string(),
929 description: "Send OSPF Link State Request packet asking a neighbour for named LSAs"
930 .to_string(),
931 parameters: vec![
932 Parameter {
933 name: "router_id".to_string(),
934 type_hint: "string".to_string(),
935 description: "OSPF router ID".to_string(),
936 required: true,
937 },
938 Parameter {
939 name: "area_id".to_string(),
940 type_hint: "string".to_string(),
941 description: "OSPF area ID".to_string(),
942 required: true,
943 },
944 Parameter {
945 name: "requests".to_string(),
946 type_hint: "array".to_string(),
947 description: "LSAs to request, each an object with lsa_type (number), \
948 link_state_id and advertising_router (dotted quads). These are \
949 the fields the ospf_database_description and \
950 ospf_link_state_update events report for every LSA header. A \
951 request with no entries asks for nothing."
952 .to_string(),
953 required: false,
954 },
955 Parameter {
956 name: "destination".to_string(),
957 type_hint: "string".to_string(),
958 description: "Destination IP: 'multicast' (default) or unicast IP".to_string(),
959 required: false,
960 },
961 ],
962 example: json!({
963 "type": "send_link_state_request",
964 "router_id": "1.1.1.1",
965 "area_id": "0.0.0.0",
966 "requests": [{
967 "lsa_type": 1,
968 "link_state_id": "2.2.2.2",
969 "advertising_router": "2.2.2.2"
970 }],
971 "destination": "192.168.1.2"
972 }),
973 log_template: Some(
974 LogTemplate::new()
975 .with_info("-> OSPF LSR to {destination}")
976 .with_debug(
977 "OSPF send_link_state_request: router_id={router_id} dest={destination}",
978 ),
979 ),
980 }
981}
982
983fn send_link_state_update_action() -> ActionDefinition {
984 ActionDefinition {
985 name: "send_link_state_update".to_string(),
986 description: "Send OSPF Link State Update packet".to_string(),
987 parameters: vec![
988 Parameter {
989 name: "router_id".to_string(),
990 type_hint: "string".to_string(),
991 description: "OSPF router ID".to_string(),
992 required: true,
993 },
994 Parameter {
995 name: "area_id".to_string(),
996 type_hint: "string".to_string(),
997 description: "OSPF area ID".to_string(),
998 required: true,
999 },
1000 Parameter {
1001 name: "destination".to_string(),
1002 type_hint: "string".to_string(),
1003 description: "Destination IP: 'multicast' (default) or unicast IP".to_string(),
1004 required: false,
1005 },
1006 ],
1007 example: json!({
1008 "type": "send_link_state_update",
1009 "router_id": "1.1.1.1",
1010 "area_id": "0.0.0.0",
1011 "destination": "multicast"
1012 }),
1013 log_template: Some(
1014 LogTemplate::new()
1015 .with_info("-> OSPF LSU to {destination}")
1016 .with_debug(
1017 "OSPF send_link_state_update: router_id={router_id} dest={destination}",
1018 ),
1019 ),
1020 }
1021}
1022
1023fn send_link_state_ack_action() -> ActionDefinition {
1024 ActionDefinition {
1025 name: "send_link_state_ack".to_string(),
1026 description: "Send OSPF Link State Acknowledgment for the LSAs a neighbour flooded"
1027 .to_string(),
1028 parameters: vec![
1029 Parameter {
1030 name: "router_id".to_string(),
1031 type_hint: "string".to_string(),
1032 description: "OSPF router ID".to_string(),
1033 required: true,
1034 },
1035 Parameter {
1036 name: "area_id".to_string(),
1037 type_hint: "string".to_string(),
1038 description: "OSPF area ID".to_string(),
1039 required: true,
1040 },
1041 Parameter {
1042 name: "lsa_headers".to_string(),
1043 type_hint: "array".to_string(),
1044 description: "The LSA headers being acknowledged - pass back the \
1045 'lsa_headers' array exactly as the ospf_link_state_update \
1046 event delivered it. An acknowledgement is matched header by \
1047 header, so an empty list acknowledges nothing and the \
1048 neighbour keeps retransmitting."
1049 .to_string(),
1050 required: true,
1051 },
1052 Parameter {
1053 name: "destination".to_string(),
1054 type_hint: "string".to_string(),
1055 description: "Destination IP: 'multicast' (default) or unicast IP".to_string(),
1056 required: false,
1057 },
1058 ],
1059 example: json!({
1060 "type": "send_link_state_ack",
1061 "router_id": "1.1.1.1",
1062 "area_id": "0.0.0.0",
1063 "lsa_headers": [{
1064 "age": 1,
1065 "options": 2,
1066 "lsa_type": 1,
1067 "link_state_id": "2.2.2.2",
1068 "advertising_router": "2.2.2.2",
1069 "sequence": 2147483649_u32,
1070 "checksum": 65262,
1071 "length": 48
1072 }],
1073 "destination": "192.168.1.2"
1074 }),
1075 log_template: Some(
1076 LogTemplate::new()
1077 .with_info("-> OSPF LSAck to {destination}")
1078 .with_debug("OSPF send_link_state_ack: router_id={router_id} dest={destination}"),
1079 ),
1080 }
1081}
1082
1083fn wait_for_more_action() -> ActionDefinition {
1084 ActionDefinition {
1085 name: "wait_for_more".to_string(),
1086 description: "Wait for more OSPF packets before responding".to_string(),
1087 parameters: vec![],
1088 example: json!({
1089 "type": "wait_for_more"
1090 }),
1091 log_template: Some(
1092 LogTemplate::new()
1093 .with_info("-> OSPF wait for more")
1094 .with_debug("OSPF wait_for_more"),
1095 ),
1096 }
1097}
1098
1099fn ospf_response_actions() -> Vec<ActionDefinition> {
1111 vec![
1112 send_hello_action(),
1113 send_database_description_action(),
1114 send_link_state_request_action(),
1115 send_link_state_update_action(),
1116 send_link_state_ack_action(),
1117 wait_for_more_action(),
1118 ]
1119}
1120
1121pub static OSPF_HELLO_EVENT: LazyLock<EventType> = LazyLock::new(|| {
1122 EventType::new(
1123 "ospf_hello",
1124 "OSPF Hello received. Answer with send_hello listing this neighbour in your own \
1125 neighbor list — until your Hello names it back, the neighbour stays in Init and the \
1126 adjacency never forms (RFC 2328 §10.5).",
1127 json!({
1128 "type": "send_hello",
1129 "router_id": "1.1.1.1",
1130 "area_id": "0.0.0.0",
1131 "network_mask": "255.255.255.0",
1132 "priority": 1,
1133 "dr": "0.0.0.0",
1134 "bdr": "0.0.0.0",
1135 "neighbors": ["2.2.2.2"],
1136 "destination": "multicast"
1137 }),
1138 )
1139 .with_actions(ospf_response_actions())
1140 .with_log_template(
1141 LogTemplate::new()
1142 .with_info("OSPF Hello from {neighbor_id}")
1143 .with_debug(
1144 "OSPF Hello: neighbor={neighbor_id} area={area_id} priority={router_priority}",
1145 )
1146 .with_trace("OSPF Hello: {json_pretty(.)}"),
1147 )
1148});
1149
1150pub static OSPF_DATABASE_DESCRIPTION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
1151 EventType::new(
1152 "ospf_database_description",
1153 "OSPF Database Description received (adjacency Exchange phase). Answer with \
1154 send_database_description to continue the exchange; silence leaves both routers in \
1155 ExStart retransmitting their DD packets.",
1156 json!({
1157 "type": "send_database_description",
1158 "router_id": "1.1.1.1",
1159 "area_id": "0.0.0.0",
1160 "sequence": 1,
1161 "init": false,
1162 "more": false,
1163 "master": false,
1164 "destination": "192.168.1.2"
1165 }),
1166 )
1167 .with_actions(ospf_response_actions())
1168 .with_log_template(
1169 LogTemplate::new()
1170 .with_info("OSPF DD from {neighbor_id} seq={dd_sequence}")
1171 .with_debug(
1172 "OSPF Database Description: neighbor={neighbor_id} seq={dd_sequence} \
1173 init={init} more={more} master={master}",
1174 )
1175 .with_trace("OSPF DD: {json_pretty(.)}"),
1176 )
1177});
1178
1179pub static OSPF_LINK_STATE_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
1180 EventType::new(
1181 "ospf_link_state_request",
1182 "OSPF Link State Request received: the neighbor is asking for LSAs it does not have. \
1183 A request left unanswered stalls the adjacency in Loading and it never reaches Full \
1184 (RFC 2328 §10.9). NetGet cannot build LSA bodies, so send_link_state_update carries \
1185 no LSAs and cannot satisfy the request — this server can hold an adjacency at 2-Way \
1186 but not complete one. Answer honestly rather than pretending: wait_for_more is the \
1187 truthful reply. Describing what you would send is not sending it.",
1188 json!({ "type": "wait_for_more" }),
1189 )
1190 .with_actions(ospf_response_actions())
1191 .with_log_template(
1192 LogTemplate::new()
1193 .with_info("OSPF LSR from {neighbor_id} ({request_count} LSAs)")
1194 .with_debug("OSPF Link State Request: neighbor={neighbor_id} requests={request_count}")
1195 .with_trace("OSPF LSR: {json_pretty(.)}"),
1196 )
1197});
1198
1199pub static OSPF_LINK_STATE_UPDATE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
1200 EventType::new(
1201 "ospf_link_state_update",
1202 "OSPF Link State Update received: the neighbor is flooding LSAs. Answer with \
1203 send_link_state_ack, passing this event's 'lsa_headers' array straight back as the \
1204 action's 'lsa_headers' — every LSA must be acknowledged by header or the neighbor \
1205 retransmits it every RxmtInterval until the adjacency fails (RFC 2328 §13.5), and an \
1206 acknowledgement with no headers acknowledges nothing. Describing the acknowledgement \
1207 is not sending it: only the action puts a packet on the wire.",
1208 json!({
1209 "type": "send_link_state_ack",
1210 "router_id": "1.1.1.1",
1211 "area_id": "0.0.0.0",
1212 "lsa_headers": [{
1213 "age": 1,
1214 "options": 2,
1215 "lsa_type": 1,
1216 "link_state_id": "2.2.2.2",
1217 "advertising_router": "2.2.2.2",
1218 "sequence": 2147483649_u32,
1219 "checksum": 65262,
1220 "length": 48
1221 }],
1222 "destination": "192.168.1.2"
1223 }),
1224 )
1225 .with_actions(ospf_response_actions())
1226 .with_log_template(
1227 LogTemplate::new()
1228 .with_info("OSPF LSU from {neighbor_id} ({lsa_count} LSAs)")
1229 .with_debug("OSPF Link State Update: neighbor={neighbor_id} lsas={lsa_count}")
1230 .with_trace("OSPF LSU: {json_pretty(.)}"),
1231 )
1232});
1233
1234pub static OSPF_LINK_STATE_ACK_EVENT: LazyLock<EventType> = LazyLock::new(|| {
1235 EventType::new(
1236 "ospf_link_state_ack",
1237 "OSPF Link State Acknowledgment packet received",
1238 json!({ "type": "wait_for_more" }),
1239 )
1240 .with_actions(ospf_response_actions())
1241 .with_log_template(
1242 LogTemplate::new()
1243 .with_info("OSPF LSAck from {neighbor_id} ({lsa_count} LSAs)")
1244 .with_debug("OSPF Link State Acknowledgment: neighbor={neighbor_id} lsas={lsa_count}")
1245 .with_trace("OSPF LSAck: {json_pretty(.)}"),
1246 )
1247});
1248
1249impl Protocol for OspfProtocol {
1251 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
1260 vec![]
1261 }
1262 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
1263 vec![
1264 send_hello_action(),
1265 send_database_description_action(),
1266 send_link_state_request_action(),
1267 send_link_state_update_action(),
1268 send_link_state_ack_action(),
1269 wait_for_more_action(),
1270 ]
1271 }
1272
1273 fn protocol_name(&self) -> &'static str {
1274 "OSPF"
1275 }
1276 fn get_event_types(&self) -> Vec<EventType> {
1277 vec![
1278 OSPF_HELLO_EVENT.clone(),
1279 OSPF_DATABASE_DESCRIPTION_EVENT.clone(),
1280 OSPF_LINK_STATE_REQUEST_EVENT.clone(),
1281 OSPF_LINK_STATE_UPDATE_EVENT.clone(),
1282 OSPF_LINK_STATE_ACK_EVENT.clone(),
1283 ]
1284 }
1285 fn stack_name(&self) -> &'static str {
1286 "ETH>IP(89)>OSPF"
1287 }
1288 fn keywords(&self) -> Vec<&'static str> {
1289 vec!["ospf", "open shortest path first"]
1290 }
1291 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
1292 use crate::protocol::metadata::{
1293 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
1294 };
1295
1296 ProtocolMetadataV2::builder()
1297 .state(DevelopmentState::Experimental)
1298 .deliberately_silent()
1301 .privilege_requirement(PrivilegeRequirement::RawSockets)
1304 .connectionless()
1309 .implementation("Manual OSPFv2 (RFC 2328) over a raw IP-protocol-89 socket. Hello is parsed in full; DD/LSR/LSU/LSAck are parsed to their 20-byte LSA headers, never to LSA bodies. Outgoing DD, LSR and LSAck carry real bodies built from the model's structured fields (LSA headers per RFC 2328 A.4.1, request triples per A.3.4); outgoing LSU always advertises zero LSAs because NetGet cannot construct an LSA body.")
1310 .llm_control("Optional: whether to engage with an OSPF speaker (respond to a Hello, claim DR/BDR, act as a honeypot) is a policy decision. With no operator policy (no instruction, no handler) the server observes passively and does NOT respond, with no LLM round-trip per packet. When the operator opts in, every received packet type raises an event carrying parsed fields and the LLM chooses the reply packet. The model can acknowledge LSAs and request them by name - the LSA headers an event reports are the same shape send_link_state_ack and send_link_state_request consume - but it cannot supply LSA contents, so a Link State Request from a peer cannot be satisfied. Fields the reply omits are filled from the interface configuration given at startup (router_id, area_id, network_mask, hello_interval, router_dead_interval, router_priority).")
1311 .e2e_testing("None against a real router, and none against this server at all: OSPF needs a raw IP-89 socket, so spawn_with_llm_actions cannot run unprivileged and no test drives it. The three tests in tests/server/ospf/e2e_test.rs named 'E2E' start a *generic UDP server* (open_server protocol=UDP) and exchange OSPF-shaped bytes the test itself built over it, mocking udp_datagram_received/send_udp_response - they exercise src/server/udp/, not this protocol, and no test anywhere mocks an ospf_* event. What IS verified, as unit tests against the real code: the RFC 2328 A.3.1 packet checksum (recomputing over a built packet yields zero), that build_hello_packet writes the configured hello_interval, router_dead_interval, priority and network mask into the Hello body at RFC 2328 A.3.2 offsets when the action omits them and that an action-supplied value overrides them, that LSAck and LSR bodies serialise at the A.4.1/A.3.4 offsets and round-trip through parse_lsa_headers, that a malformed IPv4 field is rejected rather than silently becoming 0.0.0.0, and that the RFC 2328 10.5 mismatch check fires on exactly the three fields the RFC names.")
1312 .notes("Hello-level simulator, not a router. Whether to respond at all is policy, so with no operator policy the server is a passive listener (no response, no LLM call); the passive default is compile-verified since the raw-socket path needs root and no test touches it. No LSDB, no SPF, no routing table, no LSA body construction, no DR/BDR election and no periodic Hello timer. Adjacency cannot progress past 2-Way, and a Hello that fails the RFC 2328 10.5 interval/mask check does not advance it at all - the event still reaches the model, carrying the mismatch, so the refusal is visible rather than silent. Neighbours ARE aged out: one silent for RouterDeadInterval is dropped, which also bounds the neighbour table against a peer spraying Hellos with random Router IDs. On an LLM failure the server deliberately sends nothing: OSPF has no error or NAK packet, and every one of its five packet types is a positive routing assertion, so any fabricated reply would claim an adjacency, DR role or database state netget cannot back - the peer's own RouterDeadInterval covers a router that goes quiet. The failure is reported to the operator only, tagged decision=fail_closed_overloaded / fail_closed_unavailable and distinct from decision=model_wait / model_no_action.")
1313 .build()
1314 }
1315 fn description(&self) -> &'static str {
1316 "OSPF routing protocol server"
1317 }
1318 fn example_prompt(&self) -> &'static str {
1319 "Start an OSPF server on interface 192.168.1.100 as router 1.1.1.1 in area 0.0.0.0"
1320 }
1321 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
1322 vec![
1327 ParameterDefinition {
1328 name: "router_id".to_string(),
1329 type_hint: "string".to_string(),
1330 description: "OSPF router ID in IPv4 address format (e.g., 1.1.1.1). Used as the Router ID of every packet sent unless the action overrides it. Defaults to the interface address.".to_string(),
1331 required: false,
1332 example: json!("1.1.1.1"),
1333 default: None,
1334 },
1335 ParameterDefinition {
1336 name: "area_id".to_string(),
1337 type_hint: "string".to_string(),
1338 description: "OSPF area ID in IPv4 format (0.0.0.0 = backbone area). Used as the Area ID of every packet sent unless the action overrides it.".to_string(),
1339 required: false,
1340 example: json!("0.0.0.0"),
1341 default: None,
1342 },
1343 ParameterDefinition {
1344 name: "network_mask".to_string(),
1345 type_hint: "string".to_string(),
1346 description: "Network mask of this interface (e.g., 255.255.255.0). Placed in outgoing Hello packets, and compared against incoming ones: RFC 2328 10.5 requires a Hello whose mask differs to be rejected.".to_string(),
1347 required: false,
1348 example: json!("255.255.255.0"),
1349 default: None,
1350 },
1351 ParameterDefinition {
1352 name: "hello_interval".to_string(),
1353 type_hint: "integer".to_string(),
1354 description: "HelloInterval in seconds (default 10). Placed in outgoing Hello packets. A neighbour whose Hello advertises a different value is rejected per RFC 2328 10.5 and its adjacency is not advanced.".to_string(),
1355 required: false,
1356 example: json!(10),
1357 default: None,
1358 },
1359 ParameterDefinition {
1360 name: "router_dead_interval".to_string(),
1361 type_hint: "integer".to_string(),
1362 description: "RouterDeadInterval in seconds (default 40). Placed in outgoing Hello packets and, like hello_interval, must match a neighbour's for its Hello to be accepted.".to_string(),
1363 required: false,
1364 example: json!(40),
1365 default: None,
1366 },
1367 ParameterDefinition {
1368 name: "router_priority".to_string(),
1369 type_hint: "integer".to_string(),
1370 description: "Router priority for DR election (0-255, default 1). Placed in outgoing Hello packets unless the send_hello action sets its own 'priority'. No election algorithm runs - the value is advertised, nothing more.".to_string(),
1371 required: false,
1372 example: json!(1),
1373 default: None,
1374 },
1375 ]
1376 }
1377 fn group_name(&self) -> &'static str {
1378 "VPN & Routing"
1379 }
1380
1381 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
1382 use crate::llm::actions::StartupExamples;
1383
1384 StartupExamples::new(
1385 json!({
1387 "type": "open_server",
1388 "port": 0,
1389 "base_stack": "ospf",
1390 "instruction": "OSPF router with ID 192.168.1.1 in area 0. Respond to Hello packets from neighbors. Claim DR role with priority 100."
1391 }),
1392 json!({
1394 "type": "open_server",
1395 "port": 0,
1396 "base_stack": "ospf",
1397 "event_handlers": [{
1398 "event_pattern": "ospf_hello",
1399 "handler": {
1400 "type": "script",
1401 "language": "python",
1402 "code": "return {type='send_hello', router_id='192.168.1.1', area_id='0.0.0.0', network_mask='255.255.255.0', priority=100, dr='192.168.1.1', bdr='0.0.0.0', neighbors={event.neighbor_id}}"
1403 }
1404 }]
1405 }),
1406 json!({
1408 "type": "open_server",
1409 "port": 0,
1410 "base_stack": "ospf",
1411 "event_handlers": [{
1412 "event_pattern": "ospf_hello",
1413 "handler": {
1414 "type": "static",
1415 "actions": [{
1416 "type": "send_hello",
1417 "router_id": "192.168.1.1",
1418 "area_id": "0.0.0.0",
1419 "network_mask": "255.255.255.0",
1420 "priority": 1,
1421 "dr": "0.0.0.0",
1422 "bdr": "0.0.0.0"
1423 }]
1424 }
1425 }]
1426 }),
1427 )
1428 }
1429}
1430
1431impl Server for OspfProtocol {
1433 fn spawn(
1434 &self,
1435 ctx: crate::protocol::SpawnContext,
1436 ) -> std::pin::Pin<
1437 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
1438 > {
1439 Box::pin(async move {
1440 use crate::server::ospf::OspfServer;
1441 OspfServer::spawn_with_llm_actions(
1442 ctx.legacy_listen_addr(),
1443 ctx.llm_client,
1444 ctx.state,
1445 ctx.status_tx,
1446 ctx.server_id,
1447 ctx.startup_params,
1448 )
1449 .await
1450 })
1451 }
1452 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
1453 let action_type = action
1454 .get("type")
1455 .and_then(|v| v.as_str())
1456 .context("Missing action type")?;
1457
1458 match action_type {
1459 "send_hello" => self.execute_send_hello(action),
1460 "send_database_description" => self.execute_send_database_description(action),
1461 "send_link_state_request" => self.execute_send_link_state_request(action),
1462 "send_link_state_update" => self.execute_send_link_state_update(action),
1463 "send_link_state_ack" => self.execute_send_link_state_ack(action),
1464 "wait_for_more" => Ok(ActionResult::WaitForMore),
1465 _ => Err(anyhow::anyhow!("Unknown OSPF action type: {}", action_type)),
1466 }
1467 }
1468}