Skip to main content

netget/server/rip/
mod.rs

1//! RIP (Routing Information Protocol) server implementation
2pub mod actions;
3
4use crate::server::connection::ConnectionId;
5use anyhow::Result;
6use std::net::SocketAddr;
7use std::sync::Arc;
8use tokio::net::UdpSocket;
9use tokio::sync::mpsc;
10
11use crate::llm::action_helper::call_llm;
12use crate::llm::ollama_client::OllamaClient;
13use crate::logging::emit::Log;
14use crate::protocol::Event;
15use crate::server::RipProtocol;
16use crate::state::app_state::AppState;
17use actions::RIP_REQUEST_EVENT;
18
19/// RIP server that forwards routing requests to LLM
20pub struct RipServer;
21
22impl RipServer {
23    /// Spawn RIP server with integrated LLM actions
24    pub async fn spawn_with_llm_actions(
25        listen_addr: SocketAddr,
26        llm_client: OllamaClient,
27        app_state: Arc<AppState>,
28        status_tx: mpsc::UnboundedSender<String>,
29        server_id: crate::state::ServerId,
30    ) -> Result<SocketAddr> {
31        let socket = Arc::new(UdpSocket::bind(listen_addr).await?);
32        let local_addr = socket.local_addr()?;
33        Log::new(Some(&status_tx)).info(format!("RIP server listening on {}", local_addr));
34
35        let protocol = Arc::new(RipProtocol::new());
36
37        let task_registrar = app_state.clone();
38        let accept_handle = tokio::spawn(async move {
39            // Maximum RIP packet size: 4-byte header + up to 25 route entries (20 bytes each) = 504 bytes
40            let mut buffer = vec![0u8; 512];
41            let log = Log::new(Some(&status_tx));
42
43            loop {
44                match socket.recv_from(&mut buffer).await {
45                    Ok((n, peer_addr)) => {
46                        let data = buffer[..n].to_vec();
47                        let connection_id =
48                            ConnectionId::new(app_state.get_next_unified_id().await);
49
50                        // Add connection to ServerInstance (RIP "connection" = recent peer)
51                        use crate::state::server::{
52                            ConnectionState as ServerConnectionState, ConnectionStatus,
53                            ProtocolConnectionInfo,
54                        };
55                        let now = crate::utils::clock::Instant::now();
56                        let conn_state = ServerConnectionState {
57                            id: connection_id,
58                            remote_addr: peer_addr,
59                            local_addr,
60                            bytes_sent: 0,
61                            bytes_received: n as u64,
62                            packets_sent: 0,
63                            packets_received: 1,
64                            last_activity: now,
65                            status: ConnectionStatus::Active,
66                            status_changed_at: now,
67                            protocol_info: ProtocolConnectionInfo::empty(),
68                        };
69                        app_state
70                            .add_connection_to_server(server_id, conn_state)
71                            .await;
72                        let _ = status_tx.send("__UPDATE_UI__".to_string());
73
74                        // Parse RIP packet to determine message type
75                        if n < 4 {
76                            log.debug(format!(
77                                "RIP received invalid packet (too short: {} bytes) from {}",
78                                n, peer_addr
79                            ));
80                            continue;
81                        }
82
83                        let command = data[0];
84                        let version = data[1];
85                        let num_entries = (n - 4) / 20;
86
87                        // Summary + full payload FileOnly: the rip_request event template
88                        // renders the equivalent line to the TUI.
89                        log.debug(format!(
90                            "RIP received {} bytes from {} (cmd={}, ver={}, entries={})",
91                            n, peer_addr, command, version, num_entries
92                        ));
93                        log.trace(format!("RIP data (hex): {}", hex::encode(&data)));
94
95                        let llm_clone = llm_client.clone();
96                        let state_clone = app_state.clone();
97                        let status_clone = status_tx.clone();
98                        let socket_clone = socket.clone();
99                        let protocol_clone = protocol.clone();
100
101                        // Tracked, not detached: stop_server must abort this task too.
102                        let task_owner = app_state.clone();
103                        task_owner.spawn_server_task(server_id, async move {
104                            let log = Log::new(Some(&status_clone));
105                            // Parse RIP message type
106                            let message_type = match command {
107                                1 => "request",
108                                2 => "response",
109                                _ => "unknown",
110                            };
111
112                            // Parse route entries
113                            let mut routes = Vec::new();
114                            for i in 0..num_entries {
115                                let offset = 4 + (i * 20);
116                                if offset + 20 <= data.len() {
117                                    let afi = u16::from_be_bytes([data[offset], data[offset + 1]]);
118                                    let route_tag =
119                                        u16::from_be_bytes([data[offset + 2], data[offset + 3]]);
120                                    let ip = format!(
121                                        "{}.{}.{}.{}",
122                                        data[offset + 4],
123                                        data[offset + 5],
124                                        data[offset + 6],
125                                        data[offset + 7]
126                                    );
127                                    let subnet_mask = format!(
128                                        "{}.{}.{}.{}",
129                                        data[offset + 8],
130                                        data[offset + 9],
131                                        data[offset + 10],
132                                        data[offset + 11]
133                                    );
134                                    let next_hop = format!(
135                                        "{}.{}.{}.{}",
136                                        data[offset + 12],
137                                        data[offset + 13],
138                                        data[offset + 14],
139                                        data[offset + 15]
140                                    );
141                                    let metric = u32::from_be_bytes([
142                                        data[offset + 16],
143                                        data[offset + 17],
144                                        data[offset + 18],
145                                        data[offset + 19],
146                                    ]);
147
148                                    routes.push(serde_json::json!({
149                                        "afi": afi,
150                                        "route_tag": route_tag,
151                                        "ip_address": ip,
152                                        "subnet_mask": subnet_mask,
153                                        "next_hop": next_hop,
154                                        "metric": metric
155                                    }));
156                                }
157                            }
158
159                            // Create RIP request event
160                            let event_data = serde_json::json!({
161                                "command": command,
162                                "version": version,
163                                "message_type": message_type,
164                                "routes": routes,
165                                "peer_address": peer_addr.to_string(),
166                                "bytes_received": data.len()
167                            });
168
169                            let event = Event::new(&RIP_REQUEST_EVENT, event_data);
170
171                            // A RIP response is NOT wire-determined: which routes to advertise
172                            // (and their metrics, including 16 = withdraw) is a routing-policy
173                            // decision, exactly like DNS resolution or DHCP leasing. There is no
174                            // mechanical reply to synthesise. So with no operator policy — no
175                            // server instruction and no per-event handler — the spec-safe default
176                            // is to advertise nothing and stay silent, WITHOUT burning an LLM
177                            // round-trip (which would otherwise be asked to invent routes). The
178                            // model is consulted only when the operator opts in with the routing
179                            // policy it should apply.
180                            if !operator_wants_dynamic(
181                                &state_clone,
182                                server_id,
183                                &event.event_type.id,
184                            )
185                            .await
186                            {
187                                log.info(format!(
188                                    "RIP {} from {} decision=static_default_silent: no routing policy configured (no LLM call)",
189                                    message_type, peer_addr
190                                ));
191                                return;
192                            }
193
194                            log.debug(format!(
195                                "RIP calling LLM for {} from {}",
196                                message_type, peer_addr
197                            ));
198
199                            match call_llm(
200                                &llm_clone,
201                                &state_clone,
202                                server_id,
203                                None, // RIP uses UDP, no persistent connection
204                                &event,
205                                protocol_clone.as_ref(),
206                            )
207                            .await
208                            {
209                                Ok(execution_result) => {
210                                    // Display messages from LLM
211                                    for message in &execution_result.messages {
212                                        log.info(message);
213                                    }
214
215                                    // Three outcomes that all end in the same silence on the
216                                    // wire must stay apart in the log, because only one of
217                                    // them is a fault: an explicit `ignore_request` is a
218                                    // routing decision, an empty answer is the model
219                                    // declining to decide, and an error is netget failing.
220                                    // Grep `decision=` to tell them apart.
221                                    let named_ignore =
222                                        execution_result.raw_actions.iter().any(|a| {
223                                            a.get("type").and_then(|v| v.as_str())
224                                                == Some("ignore_request")
225                                        });
226                                    let decision = if named_ignore {
227                                        "model_ignore"
228                                    } else if execution_result.raw_actions.is_empty() {
229                                        "no_answer_silent"
230                                    } else {
231                                        "model_advertise"
232                                    };
233                                    log.debug(format!(
234                                        "RIP {} from {} decision={} ({} actions)",
235                                        message_type,
236                                        peer_addr,
237                                        decision,
238                                        execution_result.raw_actions.len()
239                                    ));
240
241                                    // Process protocol results
242                                    log.debug(format!(
243                                        "RIP got {} protocol results",
244                                        execution_result.protocol_results.len()
245                                    ));
246
247                                    for protocol_result in execution_result.protocol_results {
248                                        let outputs = protocol_result.get_all_output();
249                                        if outputs.is_empty() {
250                                            log.debug("RIP protocol result has no output data");
251                                            continue;
252                                        }
253                                        // Every datagram the action produced, not just the
254                                        // first: `get_all_output` flattens
255                                        // `ActionResult::Multiple`, and taking `.first()`
256                                        // dropped the rest without saying so.
257                                        for output_data in &outputs {
258                                            match socket_clone.send_to(output_data, peer_addr).await
259                                            {
260                                                Ok(sent) => {
261                                                    // Summary + full payload FileOnly: the
262                                                    // send_rip_* action template already
263                                                    // reports the send to the TUI.
264                                                    log.debug(format!(
265                                                        "RIP sent {} bytes to {}",
266                                                        sent, peer_addr
267                                                    ));
268                                                    log.trace(format!(
269                                                        "RIP sent (hex): {}",
270                                                        hex::encode(output_data)
271                                                    ));
272                                                    // Keeps the rail's ↑ counter honest; the
273                                                    // entry was added with packets_sent 0 and
274                                                    // nothing ever updated it.
275                                                    state_clone
276                                                        .update_connection_stats(
277                                                            server_id,
278                                                            connection_id,
279                                                            None,
280                                                            Some(sent as u64),
281                                                            None,
282                                                            Some(1),
283                                                        )
284                                                        .await;
285                                                }
286                                                // The send failing is the one case where the
287                                                // old `let _ =` was actively misleading: it
288                                                // discarded the error and then logged
289                                                // "RIP sent N bytes" as if it had worked.
290                                                Err(e) => log.warn(format!(
291                                                    "RIP could not send {} bytes to {}: {}",
292                                                    output_data.len(),
293                                                    peer_addr,
294                                                    e
295                                                )),
296                                            }
297                                        }
298                                    }
299                                }
300                                Err(e) => {
301                                    // RIP has no error message: RFC 2453 defines Request (1)
302                                    // and Response (2) and nothing else, and a Response is a
303                                    // routing assertion. Inventing one here would put a
304                                    // fabricated routing statement into a peer's table, so
305                                    // the spec-safe answer to an internal failure is the same
306                                    // silence this server already applies when no routing
307                                    // policy is configured. The peer is not blocked by it:
308                                    // RIP is connectionless and re-requests on its own timer.
309                                    //
310                                    // The failure is therefore reported to the operator only.
311                                    // The category is logged separately from the error so a
312                                    // saturated backend (retry worthwhile) is distinguishable
313                                    // from a broken one, matching what a protocol with error
314                                    // codes would put on the wire. The full error goes to the
315                                    // log and the status stream and never anywhere else.
316                                    let category = if crate::utils::WireFailure::classify(&e)
317                                        .is_overloaded()
318                                    {
319                                        "overloaded"
320                                    } else {
321                                        "unavailable"
322                                    };
323                                    log.warn(format!(
324                                        "RIP {} from {} decision=fail_closed_llm_error category={}, no response sent: {}",
325                                        message_type, peer_addr, category, e
326                                    ));
327                                }
328                            }
329                        }).await;
330                    }
331                    Err(e) => {
332                        log.error(format!("RIP receive error: {}", e));
333                        break;
334                    }
335                }
336            }
337        });
338
339        // Register the accept loop so stop_server can abort it and release the port.
340        task_registrar
341            .register_server_task(server_id, accept_handle)
342            .await;
343
344        Ok(local_addr)
345    }
346}
347
348/// Returns true if the operator opted into dynamic (LLM- or handler-driven) responses for this
349/// server: either a non-empty server instruction was given, or an event handler is configured
350/// for `event_id`. When false the protocol applies its static default and never consults the
351/// model — for a policy protocol like RIP that default is to stay silent, because with no
352/// configured policy there is nothing correct to advertise.
353async fn operator_wants_dynamic(
354    state: &AppState,
355    server_id: crate::state::ServerId,
356    event_id: &str,
357) -> bool {
358    state
359        .with_server_mut(server_id, |server| {
360            let has_instruction = !server.instruction.trim().is_empty();
361            let has_handler = server
362                .event_handler_config
363                .as_ref()
364                .map(|c| c.find_handler(event_id).is_some())
365                .unwrap_or(false);
366            has_instruction || has_handler
367        })
368        .await
369        .unwrap_or(false)
370}