1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter,
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;
13
14pub struct RipProtocol;
15
16impl RipProtocol {
17 pub fn new() -> Self {
18 Self
19 }
20}
21
22impl Protocol for RipProtocol {
24 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
25 Vec::new()
26 }
27 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
28 vec![
29 send_rip_response_action(),
30 send_rip_request_action(),
31 ignore_request_action(),
32 ]
33 }
34 fn protocol_name(&self) -> &'static str {
35 "RIP"
36 }
37 fn get_event_types(&self) -> Vec<EventType> {
38 get_rip_event_types()
39 }
40 fn stack_name(&self) -> &'static str {
41 "ETH>IP>UDP>RIP"
42 }
43 fn keywords(&self) -> Vec<&'static str> {
44 vec!["rip"]
45 }
46 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
47 use crate::protocol::metadata::{
48 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
49 };
50
51 ProtocolMetadataV2::builder()
52 .connectionless()
53 .state(DevelopmentState::Experimental)
54 .deliberately_silent()
57 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(520))
58 .well_known_udp_port(520)
59 .implementation("Manual RIPv2 packet construction (RFC 2453)")
60 .llm_control("Optional: which routes to advertise is a policy decision (LLM); with no policy configured the server stays silent with no LLM call")
61 .e2e_testing("Manual RIP packet construction with route entries")
62 .notes("Distance-vector routing protocol, 15 hop limit. A RIP response is NOT wire-determined — which routes to advertise (and their metrics, incl. 16=withdraw) is routing policy, like DNS/DHCP. So with no operator policy (no instruction, no handler) the server applies the spec-safe STATIC default of advertising nothing and answers with NO LLM round-trip; the LLM is consulted only when the operator supplies the routing policy. There is no mechanical RIP reply to synthesise (RIP has no Hello/keepalive machinery)")
63 .build()
64 }
65 fn description(&self) -> &'static str {
66 "Routing Information Protocol v2 for dynamic routing"
67 }
68 fn example_prompt(&self) -> &'static str {
69 "listen on port 520 via rip. Advertise routes for 192.168.1.0/24 (metric 1) and 10.0.0.0/8 (metric 5)"
70 }
71 fn group_name(&self) -> &'static str {
72 "Network"
73 }
74
75 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
76 use crate::llm::actions::StartupExamples;
77
78 StartupExamples::new(
79 json!({
81 "type": "open_server",
82 "port": 520,
83 "base_stack": "rip",
84 "instruction": "RIPv2 routing server. Advertise routes for 192.168.1.0/24 (metric 1) and 10.0.0.0/8 (metric 5). Respond to route requests."
85 }),
86 json!({
88 "type": "open_server",
89 "port": 520,
90 "base_stack": "rip",
91 "event_handlers": [{
92 "event_pattern": "rip_request",
93 "handler": {
94 "type": "script",
95 "language": "python",
96 "code": "if event.message_type == 'request' then return {type='send_rip_response', routes={{ip_address='192.168.1.0', subnet_mask='255.255.255.0', next_hop='0.0.0.0', metric=1}, {ip_address='10.0.0.0', subnet_mask='255.0.0.0', next_hop='0.0.0.0', metric=5}}} else return {type='ignore_request'} end"
97 }
98 }]
99 }),
100 json!({
102 "type": "open_server",
103 "port": 520,
104 "base_stack": "rip",
105 "event_handlers": [{
106 "event_pattern": "rip_request",
107 "handler": {
108 "type": "static",
109 "actions": [{
110 "type": "send_rip_response",
111 "routes": [{
112 "ip_address": "192.168.1.0",
113 "subnet_mask": "255.255.255.0",
114 "next_hop": "0.0.0.0",
115 "metric": 1
116 }]
117 }]
118 }
119 }]
120 }),
121 )
122 }
123}
124
125impl Server for RipProtocol {
127 fn spawn(
128 &self,
129 ctx: crate::protocol::SpawnContext,
130 ) -> std::pin::Pin<
131 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
132 > {
133 Box::pin(async move {
134 use crate::server::rip::RipServer;
135 RipServer::spawn_with_llm_actions(
136 ctx.legacy_listen_addr(),
137 ctx.llm_client,
138 ctx.state,
139 ctx.status_tx,
140 ctx.server_id,
141 )
142 .await
143 })
144 }
145 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
146 let action_type = action
147 .get("type")
148 .and_then(|v| v.as_str())
149 .context("Missing 'type' field in action")?;
150
151 match action_type {
152 "send_rip_response" => self.execute_send_rip_response(action),
153 "send_rip_request" => self.execute_send_rip_request(action),
154 "ignore_request" => Ok(ActionResult::NoAction),
155 _ => Err(anyhow::anyhow!("Unknown RIP action: {}", action_type)),
156 }
157 }
158}
159
160impl RipProtocol {
161 fn execute_send_rip_response(&self, action: serde_json::Value) -> Result<ActionResult> {
162 let routes = action
164 .get("routes")
165 .and_then(|v| v.as_array())
166 .context("Missing or invalid 'routes' field")?;
167
168 if routes.len() > 25 {
169 return Err(anyhow::anyhow!("Too many routes (max 25 per packet)"));
170 }
171
172 let mut packet = Vec::new();
174
175 packet.push(2); packet.push(2); packet.push(0); packet.push(0); for route in routes {
183 let afi = Self::wire_u16(route, "afi", 2)?; let route_tag = Self::wire_u16(route, "route_tag", 0)?;
188 let ip_str = route
189 .get("ip_address")
190 .and_then(|v| v.as_str())
191 .context("Missing 'ip_address' in route")?;
192 let subnet_mask_str = route
193 .get("subnet_mask")
194 .and_then(|v| v.as_str())
195 .unwrap_or("255.255.255.0");
196 let next_hop_str = route
197 .get("next_hop")
198 .and_then(|v| v.as_str())
199 .unwrap_or("0.0.0.0");
200 let metric = route.get("metric").and_then(|v| v.as_u64()).unwrap_or(1);
204 if !(1..=16).contains(&metric) {
205 return Err(anyhow::anyhow!(
206 "Invalid RIP metric {} for {}: must be 1-15 (reachable) or 16 (unreachable)",
207 metric,
208 ip_str
209 ));
210 }
211 let metric = metric as u32;
212
213 let ip_parts = Self::parse_ipv4(ip_str)?;
215 let subnet_parts = Self::parse_ipv4(subnet_mask_str)?;
216 let next_hop_parts = Self::parse_ipv4(next_hop_str)?;
217
218 packet.extend_from_slice(&afi.to_be_bytes());
220 packet.extend_from_slice(&route_tag.to_be_bytes());
221 packet.extend_from_slice(&ip_parts);
222 packet.extend_from_slice(&subnet_parts);
223 packet.extend_from_slice(&next_hop_parts);
224 packet.extend_from_slice(&metric.to_be_bytes());
225 }
226
227 Ok(ActionResult::Output(packet))
228 }
229
230 fn execute_send_rip_request(&self, action: serde_json::Value) -> Result<ActionResult> {
231 let routes = action.get("routes").and_then(|v| v.as_array());
233
234 let mut packet = Vec::new();
235
236 packet.push(1); packet.push(2); packet.push(0); packet.push(0); if let Some(routes_array) = routes {
243 if routes_array.len() > 25 {
247 return Err(anyhow::anyhow!("Too many routes (max 25 per packet)"));
248 }
249 for route in routes_array {
251 let afi = Self::wire_u16(route, "afi", 2)?;
252 let ip_str = route
253 .get("ip_address")
254 .and_then(|v| v.as_str())
255 .context("Missing 'ip_address' in route")?;
256
257 let ip_parts = Self::parse_ipv4(ip_str)?;
258
259 packet.extend_from_slice(&afi.to_be_bytes());
261 packet.extend_from_slice(&[0, 0]); packet.extend_from_slice(&ip_parts);
263 packet.extend_from_slice(&[0, 0, 0, 0]); packet.extend_from_slice(&[0, 0, 0, 0]); packet.extend_from_slice(&16u32.to_be_bytes()); }
267 } else {
268 packet.extend_from_slice(&[0, 0]); packet.extend_from_slice(&[0, 0]); packet.extend_from_slice(&[0, 0, 0, 0]); packet.extend_from_slice(&[0, 0, 0, 0]); packet.extend_from_slice(&[0, 0, 0, 0]); packet.extend_from_slice(&16u32.to_be_bytes()); }
276
277 Ok(ActionResult::Output(packet))
278 }
279
280 fn wire_u16(route: &serde_json::Value, field: &str, default: u16) -> Result<u16> {
282 match route.get(field) {
283 None | Some(serde_json::Value::Null) => Ok(default),
284 Some(v) => {
285 let n = v
286 .as_u64()
287 .with_context(|| format!("'{field}' must be a non-negative number"))?;
288 u16::try_from(n)
289 .with_context(|| format!("'{field}' value {n} does not fit in 16 bits"))
290 }
291 }
292 }
293
294 fn parse_ipv4(ip_str: &str) -> Result<[u8; 4]> {
295 let parts: Vec<&str> = ip_str.split('.').collect();
296 if parts.len() != 4 {
297 return Err(anyhow::anyhow!("Invalid IPv4 address: {}", ip_str));
298 }
299
300 Ok([
301 parts[0].parse().context("Invalid IP octet")?,
302 parts[1].parse().context("Invalid IP octet")?,
303 parts[2].parse().context("Invalid IP octet")?,
304 parts[3].parse().context("Invalid IP octet")?,
305 ])
306 }
307}
308
309pub static RIP_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
311 EventType::new(
312 "rip_request",
313 "A RIP message arrived. Answer a 'request' with send_rip_response carrying the routes \
314 you want to advertise; a peer's unsolicited 'response' is usually best answered with \
315 ignore_request, or with send_rip_request to ask for its table.",
316 json!({
317 "type": "send_rip_response",
318 "routes": [
319 {"ip_address": "192.168.1.0", "subnet_mask": "255.255.255.0", "next_hop": "0.0.0.0", "metric": 1}
320 ]
321 }),
322 )
323 .with_parameters(vec![
324 Parameter {
325 name: "command".to_string(),
326 type_hint: "number".to_string(),
327 description: "RIP command type (1=request, 2=response)".to_string(),
328 required: true,
329 },
330 Parameter {
331 name: "version".to_string(),
332 type_hint: "number".to_string(),
333 description: "RIP version (typically 2 for RIPv2)".to_string(),
334 required: true,
335 },
336 Parameter {
337 name: "message_type".to_string(),
338 type_hint: "string".to_string(),
339 description: "Message type: 'request' or 'response'".to_string(),
340 required: true,
341 },
342 Parameter {
343 name: "routes".to_string(),
344 type_hint: "array".to_string(),
345 description: "Array of route entries (ip_address, subnet_mask, next_hop, metric)"
346 .to_string(),
347 required: true,
348 },
349 Parameter {
350 name: "peer_address".to_string(),
351 type_hint: "string".to_string(),
352 description: "Address of the RIP peer".to_string(),
353 required: true,
354 },
355 Parameter {
356 name: "bytes_received".to_string(),
357 type_hint: "number".to_string(),
358 description: "Size of the received packet".to_string(),
359 required: true,
360 },
361 ])
362 .with_actions(vec![
366 send_rip_response_action(),
367 send_rip_request_action(),
368 ignore_request_action(),
369 ])
370 .with_log_template(
371 LogTemplate::new()
372 .with_info("RIP {message_type} from {peer_address} ({bytes_received}B)")
373 .with_debug("RIPv{version} {message_type} from {peer_address}, {routes_len} routes")
374 .with_trace("RIP message: {json_pretty(.)}"),
375 )
376});
377
378fn get_rip_event_types() -> Vec<EventType> {
379 vec![RIP_REQUEST_EVENT.clone()]
380}
381
382fn send_rip_response_action() -> ActionDefinition {
384 ActionDefinition {
385 name: "send_rip_response".to_string(),
386 description: "Send RIP response with routing table entries".to_string(),
387 parameters: vec![
388 Parameter {
389 name: "routes".to_string(),
390 type_hint: "array".to_string(),
391 description: "Array of route entries to advertise, at most 25. Each route must have: ip_address (string), subnet_mask (string, default 255.255.255.0), next_hop (string, default 0.0.0.0), metric (number 1-15 = hop count, or 16 = unreachable/withdraw the route; default 1), afi (number, default 2 for IPv4), route_tag (number 0-65535, default 0)".to_string(),
392 required: true,
393 },
394 ],
395 example: json!({
396 "type": "send_rip_response",
397 "routes": [
398 {
399 "ip_address": "192.168.1.0",
400 "subnet_mask": "255.255.255.0",
401 "next_hop": "0.0.0.0",
402 "metric": 1
403 },
404 {
405 "ip_address": "10.0.0.0",
406 "subnet_mask": "255.0.0.0",
407 "next_hop": "192.168.1.1",
408 "metric": 5
409 }
410 ]
411 }),
412 log_template: Some(
413 LogTemplate::new()
414 .with_info("-> RIP response ({routes_len} routes)")
415 .with_debug("RIP send_rip_response: routes={routes_len}"),
416 ),
417 }
418}
419
420fn send_rip_request_action() -> ActionDefinition {
421 ActionDefinition {
422 name: "send_rip_request".to_string(),
423 description: "Send RIP request to query routing table. If routes array is omitted, requests entire routing table.".to_string(),
424 parameters: vec![
425 Parameter {
426 name: "routes".to_string(),
427 type_hint: "array".to_string(),
428 description: "Optional array of specific routes to request, at most 25, each with an ip_address (string). Omit to request the entire table.".to_string(),
429 required: false,
430 },
431 ],
432 example: json!({
433 "type": "send_rip_request"
434 }),
435 log_template: Some(
436 LogTemplate::new()
437 .with_info("-> RIP request")
438 .with_debug("RIP send_rip_request"),
439 ),
440 }
441}
442
443fn ignore_request_action() -> ActionDefinition {
444 ActionDefinition {
445 name: "ignore_request".to_string(),
446 description: "Ignore the RIP message and send no response".to_string(),
447 parameters: vec![],
448 example: json!({
449 "type": "ignore_request"
450 }),
451 log_template: Some(
452 LogTemplate::new()
453 .with_info("-> RIP ignore")
454 .with_debug("RIP ignore_request"),
455 ),
456 }
457}