Skip to main content

netget/client/rip/
mod.rs

1//! RIP (Routing Information Protocol) client implementation
2pub mod actions;
3
4pub use actions::RipClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::{Ipv4Addr, SocketAddr};
8use std::sync::Arc;
9use tokio::net::UdpSocket;
10use tokio::sync::{mpsc, Mutex};
11use tracing::{debug, error, info, trace};
12
13use crate::client::llm_budget::call_llm_for_client;
14use crate::client::rip::actions::{RIP_CLIENT_CONNECTED_EVENT, RIP_CLIENT_RESPONSE_RECEIVED_EVENT};
15use crate::llm::actions::client_trait::{Client, ClientActionResult};
16use crate::llm::actions::protocol_trait::Protocol;
17use crate::llm::ollama_client::OllamaClient;
18use crate::llm::ClientLlmResult;
19use crate::protocol::Event;
20use crate::state::app_state::AppState;
21use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
22use crate::state::{AccessLogOwner, ClientId, ClientStatus};
23
24/// RIP protocol version
25#[derive(Debug, Clone, Copy, PartialEq)]
26#[repr(u8)]
27pub enum RipVersion {
28    V1 = 1,
29    V2 = 2,
30}
31
32/// RIP command types
33#[derive(Debug, Clone, Copy, PartialEq)]
34#[repr(u8)]
35pub enum RipCommand {
36    Request = 1,
37    Response = 2,
38}
39
40/// RIP route entry (24 bytes for RIPv2)
41#[derive(Debug, Clone)]
42pub struct RipRouteEntry {
43    pub address_family: u16,
44    pub route_tag: u16, // RIPv2 only (0 for RIPv1)
45    pub ip_address: Ipv4Addr,
46    pub subnet_mask: Ipv4Addr, // RIPv2 only (0.0.0.0 for RIPv1)
47    pub next_hop: Ipv4Addr,    // RIPv2 only (0.0.0.0 for RIPv1)
48    pub metric: u32,
49}
50
51impl RipRouteEntry {
52    /// Create a new RIP route entry
53    pub fn new(ip: Ipv4Addr, metric: u32) -> Self {
54        Self {
55            address_family: 2, // AF_INET
56            route_tag: 0,
57            ip_address: ip,
58            subnet_mask: Ipv4Addr::new(0, 0, 0, 0),
59            next_hop: Ipv4Addr::new(0, 0, 0, 0),
60            metric,
61        }
62    }
63
64    /// Create a RIPv2 route entry with subnet mask
65    pub fn new_v2(ip: Ipv4Addr, subnet_mask: Ipv4Addr, next_hop: Ipv4Addr, metric: u32) -> Self {
66        Self {
67            address_family: 2,
68            route_tag: 0,
69            ip_address: ip,
70            subnet_mask,
71            next_hop,
72            metric,
73        }
74    }
75
76    /// Encode route entry to bytes
77    pub fn encode(&self) -> Vec<u8> {
78        let mut bytes = Vec::with_capacity(20);
79        bytes.extend_from_slice(&self.address_family.to_be_bytes());
80        bytes.extend_from_slice(&self.route_tag.to_be_bytes());
81        bytes.extend_from_slice(&self.ip_address.octets());
82        bytes.extend_from_slice(&self.subnet_mask.octets());
83        bytes.extend_from_slice(&self.next_hop.octets());
84        bytes.extend_from_slice(&self.metric.to_be_bytes());
85        bytes
86    }
87
88    /// Decode route entry from bytes
89    pub fn decode(data: &[u8]) -> Result<Self> {
90        if data.len() < 20 {
91            return Err(anyhow::anyhow!("RIP route entry too short"));
92        }
93
94        Ok(Self {
95            address_family: u16::from_be_bytes([data[0], data[1]]),
96            route_tag: u16::from_be_bytes([data[2], data[3]]),
97            ip_address: Ipv4Addr::new(data[4], data[5], data[6], data[7]),
98            subnet_mask: Ipv4Addr::new(data[8], data[9], data[10], data[11]),
99            next_hop: Ipv4Addr::new(data[12], data[13], data[14], data[15]),
100            metric: u32::from_be_bytes([data[16], data[17], data[18], data[19]]),
101        })
102    }
103}
104
105/// RIP message structure
106#[derive(Debug, Clone)]
107pub struct RipMessage {
108    pub command: RipCommand,
109    pub version: RipVersion,
110    pub routes: Vec<RipRouteEntry>,
111}
112
113impl RipMessage {
114    /// Create a RIP request message
115    pub fn request(version: RipVersion) -> Self {
116        // Request for entire routing table: address family 0, metric 16
117        Self {
118            command: RipCommand::Request,
119            version,
120            routes: vec![RipRouteEntry {
121                address_family: 0,
122                route_tag: 0,
123                ip_address: Ipv4Addr::new(0, 0, 0, 0),
124                subnet_mask: Ipv4Addr::new(0, 0, 0, 0),
125                next_hop: Ipv4Addr::new(0, 0, 0, 0),
126                metric: 16,
127            }],
128        }
129    }
130
131    /// Encode RIP message to bytes
132    pub fn encode(&self) -> Vec<u8> {
133        let mut bytes = Vec::new();
134
135        // Command (1 byte)
136        bytes.push(self.command as u8);
137
138        // Version (1 byte)
139        bytes.push(self.version as u8);
140
141        // Must be zero (2 bytes)
142        bytes.extend_from_slice(&[0, 0]);
143
144        // Route entries (20 bytes each)
145        for route in &self.routes {
146            bytes.extend_from_slice(&route.encode());
147        }
148
149        bytes
150    }
151
152    /// Decode RIP message from bytes
153    pub fn decode(data: &[u8]) -> Result<Self> {
154        if data.len() < 4 {
155            return Err(anyhow::anyhow!("RIP message too short"));
156        }
157
158        let command = match data[0] {
159            1 => RipCommand::Request,
160            2 => RipCommand::Response,
161            _ => return Err(anyhow::anyhow!("Invalid RIP command: {}", data[0])),
162        };
163
164        let version = match data[1] {
165            1 => RipVersion::V1,
166            2 => RipVersion::V2,
167            _ => return Err(anyhow::anyhow!("Invalid RIP version: {}", data[1])),
168        };
169
170        // Parse route entries
171        let mut routes = Vec::new();
172        let mut offset = 4;
173
174        while offset + 20 <= data.len() {
175            routes.push(RipRouteEntry::decode(&data[offset..offset + 20])?);
176            offset += 20;
177        }
178
179        Ok(Self {
180            command,
181            version,
182            routes,
183        })
184    }
185}
186
187/// Connection state for LLM processing
188#[derive(Debug, Clone, PartialEq)]
189enum ConnectionState {
190    Idle,
191    Processing,
192    Accumulating,
193}
194
195/// Per-client data for LLM handling
196struct ClientData {
197    state: ConnectionState,
198    queued_responses: Vec<RipMessage>,
199    memory: String,
200}
201
202/// RIP client that queries RIP routers
203pub struct RipClient;
204
205impl RipClient {
206    /// Connect to a RIP router with integrated LLM actions
207    pub async fn connect_with_llm_actions(
208        remote_addr: String,
209        llm_client: OllamaClient,
210        app_state: Arc<AppState>,
211        status_tx: mpsc::UnboundedSender<String>,
212        client_id: ClientId,
213    ) -> Result<SocketAddr> {
214        // Parse remote address
215        let remote_sock_addr: SocketAddr = remote_addr
216            .parse()
217            .context(format!("Invalid RIP router address: {}", remote_addr))?;
218
219        // Bind to any available local port (not 520, as that requires root)
220        let socket = UdpSocket::bind("0.0.0.0:0")
221            .await
222            .context("Failed to bind UDP socket")?;
223
224        let local_addr = socket.local_addr()?;
225
226        info!(
227            "RIP client {} bound to {} (target: {})",
228            client_id, local_addr, remote_sock_addr
229        );
230
231        // Update client state
232        app_state
233            .update_client_status(client_id, ClientStatus::Connected)
234            .await;
235        let _ = status_tx.send(format!("[CLIENT] RIP client {} connected", client_id));
236        let _ = status_tx.send("__UPDATE_UI__".to_string());
237
238        let socket_arc = Arc::new(socket);
239
240        // Initialize client data
241        let client_data = Arc::new(Mutex::new(ClientData {
242            state: ConnectionState::Idle,
243            queued_responses: Vec::new(),
244            memory: String::new(),
245        }));
246
247        let protocol = Arc::new(crate::client::rip::actions::RipClientProtocol::new());
248
249        // Command channel for injected actions (the dashboard's [ send ]). Registered, and
250        // drained by a live task, BEFORE the connected-event LLM call below: that call is
251        // awaited inline here, so a manual `*` routing rule parks it - and the whole point
252        // of the channel is that the operator can still reach the client while it waits.
253        let command_rx =
254            crate::client::command_support::register_command_channel(&app_state, client_id).await;
255        // Filled in once the receive loop exists (it is spawned after the connected-event
256        // call), so an injected `disconnect` can actually stop it and release the socket.
257        let read_abort: Arc<std::sync::OnceLock<tokio::task::AbortHandle>> =
258            Arc::new(std::sync::OnceLock::new());
259        let cmd_task = tokio::spawn(Self::command_loop(
260            command_rx,
261            protocol.clone(),
262            socket_arc.clone(),
263            remote_sock_addr,
264            client_id,
265            app_state.clone(),
266            status_tx.clone(),
267            read_abort.clone(),
268        ));
269        app_state.register_client_task(client_id, cmd_task).await;
270
271        // Call LLM with connected event
272        let event = Event::new(
273            &RIP_CLIENT_CONNECTED_EVENT,
274            serde_json::json!({
275                "remote_addr": remote_sock_addr.to_string(),
276            }),
277        );
278
279        if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
280            // Snapshot the memory into a local BEFORE the match. Passing
281            // `&client_data.lock().await.memory` directly as an argument would put the
282            // `MutexGuard` in the match scrutinee, and scrutinee temporaries live until the
283            // end of the whole `match` — so the guard would be held across the LLM await and
284            // the `Ok` arm's own `client_data.lock()` below would deadlock on it.
285            let memory = client_data.lock().await.memory.clone();
286            match call_llm_for_client(
287                &llm_client,
288                &app_state,
289                client_id.to_string(),
290                &instruction,
291                &memory,
292                Some(&event),
293                protocol.as_ref(),
294                &status_tx,
295            )
296            .await
297            {
298                Ok(ClientLlmResult {
299                    actions,
300                    memory_updates,
301                }) => {
302                    // Update memory
303                    if let Some(mem) = memory_updates {
304                        client_data.lock().await.memory = mem;
305                    }
306
307                    // Execute initial actions
308                    for action in actions {
309                        match protocol.as_ref().execute_action(action) {
310                            Ok(result) => {
311                                match Self::apply_action(
312                                    client_id,
313                                    result,
314                                    &socket_arc,
315                                    remote_sock_addr,
316                                )
317                                .await
318                                {
319                                    Ok(Applied::Disconnect) => {
320                                        info!("RIP client {} disconnecting", client_id);
321                                        app_state.remove_client_handle(client_id).await;
322                                        app_state
323                                            .update_client_status(
324                                                client_id,
325                                                ClientStatus::Disconnected,
326                                            )
327                                            .await;
328                                        return Ok(local_addr);
329                                    }
330                                    Ok(_) => {}
331                                    Err(e) => error!("Failed to send RIP request: {}", e),
332                                }
333                            }
334                            Err(e) => error!(
335                                "RIP client {} could not execute action after connect: {}",
336                                client_id, e
337                            ),
338                        }
339                    }
340                }
341                Err(e) => {
342                    error!("LLM error for RIP client {}: {}", client_id, e);
343                }
344            }
345        }
346
347        // An injected `disconnect` that arrived while the connected-event call was parked
348        // has already dropped the command handle. Honour it rather than starting a receive
349        // loop the operator just asked to stop.
350        if !app_state.has_client_handle(client_id).await {
351            return Ok(local_addr);
352        }
353
354        // Spawn receive loop
355        let socket_clone = socket_arc.clone();
356        let client_data_clone = client_data.clone();
357        // Registered with AppState so stop_client can abort this task —
358        // dropping a JoinHandle only detaches it in Tokio.
359        let task_registrar = app_state.clone();
360        let task_handle = tokio::spawn(async move {
361            let mut buffer = vec![0u8; 1500]; // Max UDP packet size
362
363            loop {
364                match socket_clone.recv_from(&mut buffer).await {
365                    Ok((n, peer)) => {
366                        trace!(
367                            "RIP client {} received {} bytes from {}",
368                            client_id,
369                            n,
370                            peer
371                        );
372
373                        // Parse RIP message
374                        match RipMessage::decode(&buffer[..n]) {
375                            Ok(msg) => {
376                                let mut client_data_lock = client_data_clone.lock().await;
377
378                                match client_data_lock.state {
379                                    ConnectionState::Idle => {
380                                        // Process immediately
381                                        client_data_lock.state = ConnectionState::Processing;
382                                        drop(client_data_lock);
383
384                                        // Drain anything the Processing/Accumulating arms
385                                        // parked, oldest first, and only then go back to Idle
386                                        // — the TCP reference loops back over `queued_data`
387                                        // the same way, where this used to `clear()` the queue
388                                        // unread.
389                                        //
390                                        // Today the queue cannot actually fill: this task is
391                                        // the only producer and it is parked on the LLM call
392                                        // below, so `Processing`/`Accumulating` are
393                                        // unreachable. The drain is here so the arms mean what
394                                        // they say if a second producer ever appears, not
395                                        // because datagrams are being lost now.
396                                        let mut current = msg;
397                                        loop {
398                                            // Call LLM with response event
399                                            if let Some(instruction) = app_state
400                                                .get_instruction_for_client(client_id)
401                                                .await
402                                            {
403                                                let routes: Vec<_> = current
404                                                .routes
405                                                .iter()
406                                                .map(|r| {
407                                                    serde_json::json!({
408                                                        "ip_address": r.ip_address.to_string(),
409                                                        "subnet_mask": r.subnet_mask.to_string(),
410                                                        "next_hop": r.next_hop.to_string(),
411                                                        "metric": r.metric,
412                                                    })
413                                                })
414                                                .collect();
415
416                                                let event = Event::new(
417                                                    &RIP_CLIENT_RESPONSE_RECEIVED_EVENT,
418                                                    serde_json::json!({
419                                                        "version": current.version as u8,
420                                                        "command": match current.command {
421                                                            RipCommand::Request => "request",
422                                                            RipCommand::Response => "response",
423                                                        },
424                                                        "route_count": routes.len(),
425                                                        "routes": routes,
426                                                    }),
427                                                );
428
429                                                // Snapshot before the match: a `MutexGuard` built
430                                                // in the scrutinee lives to the end of the whole
431                                                // `match`, so it would be held across the LLM
432                                                // await and the `Ok` arm's own `lock()` below
433                                                // would deadlock on it.
434                                                let memory =
435                                                    client_data_clone.lock().await.memory.clone();
436                                                match call_llm_for_client(
437                                                    &llm_client,
438                                                    &app_state,
439                                                    client_id.to_string(),
440                                                    &instruction,
441                                                    &memory,
442                                                    Some(&event),
443                                                    protocol.as_ref(),
444                                                    &status_tx,
445                                                )
446                                                .await
447                                                {
448                                                    Ok(ClientLlmResult {
449                                                        actions,
450                                                        memory_updates,
451                                                    }) => {
452                                                        // Update memory
453                                                        if let Some(mem) = memory_updates {
454                                                            client_data_clone.lock().await.memory =
455                                                                mem;
456                                                        }
457
458                                                        // Execute actions
459                                                        for action in actions {
460                                                            match protocol.as_ref().execute_action(action) {
461                                                            Ok(result) => {
462                                                                match Self::apply_action(client_id, result, &socket_clone, remote_sock_addr).await {
463                                                                    Ok(Applied::Disconnect) => {
464                                                                        info!("RIP client {} disconnecting", client_id);
465                                                                        app_state.remove_client_handle(client_id).await;
466                                                                        app_state.update_client_status(client_id, ClientStatus::Disconnected).await;
467                                                                        let _ = status_tx.send("__UPDATE_UI__".to_string());
468                                                                        return;
469                                                                    }
470                                                                    Ok(_) => {}
471                                                                    Err(e) => error!("Failed to send RIP request: {}", e),
472                                                                }
473                                                            }
474                                                            Err(e) => error!("RIP client {} could not execute action: {}", client_id, e),
475                                                        }
476                                                        }
477                                                    }
478                                                    Err(e) => {
479                                                        error!(
480                                                            "LLM error for RIP client {}: {}",
481                                                            client_id, e
482                                                        );
483                                                    }
484                                                }
485                                            }
486
487                                            // Take the next datagram that queued up while the
488                                            // model was deciding, oldest first. Only when the
489                                            // queue is empty does the client go back to Idle.
490                                            let mut client_data_lock =
491                                                client_data_clone.lock().await;
492                                            if client_data_lock.queued_responses.is_empty() {
493                                                client_data_lock.state = ConnectionState::Idle;
494                                                break;
495                                            }
496                                            current = client_data_lock.queued_responses.remove(0);
497                                        }
498                                    }
499                                    ConnectionState::Processing => {
500                                        // Queue response
501                                        client_data_lock.queued_responses.push(msg);
502                                        client_data_lock.state = ConnectionState::Accumulating;
503                                    }
504                                    ConnectionState::Accumulating => {
505                                        // Continue queuing
506                                        client_data_lock.queued_responses.push(msg);
507                                    }
508                                }
509                            }
510                            Err(e) => {
511                                error!("Failed to parse RIP message: {}", e);
512                            }
513                        }
514                    }
515                    Err(e) => {
516                        error!("RIP client {} recv error: {}", client_id, e);
517                        app_state
518                            .update_client_status(client_id, ClientStatus::Error(e.to_string()))
519                            .await;
520                        let _ = status_tx.send("__UPDATE_UI__".to_string());
521                        break;
522                    }
523                }
524            }
525            // Every exit path lands here: drop the command handle so the dashboard stops
526            // offering [ send ] on a dead client, and a late send fails fast.
527            app_state.remove_client_handle(client_id).await;
528            let _ = status_tx.send("__UPDATE_UI__".to_string());
529        });
530        let _ = read_abort.set(task_handle.abort_handle());
531        task_registrar
532            .register_client_task(client_id, task_handle)
533            .await;
534
535        Ok(local_addr)
536    }
537
538    /// Drain injected commands until the channel closes (client removed) or an injected
539    /// `disconnect` ends the session.
540    ///
541    /// The generic `command_support::handle_stream_client_command` cannot serve this client:
542    /// `send_rip_request` yields `ClientActionResult::Custom` and the transport is a datagram
543    /// socket, not an `AsyncWrite`. The action goes through [`Self::apply_action`] - the same
544    /// function the LLM path uses - so the RIP encoding exists exactly once.
545    #[allow(clippy::too_many_arguments)]
546    async fn command_loop(
547        mut command_rx: tokio::sync::mpsc::Receiver<ClientCommand>,
548        protocol: Arc<crate::client::rip::actions::RipClientProtocol>,
549        socket: Arc<UdpSocket>,
550        remote_addr: SocketAddr,
551        client_id: ClientId,
552        app_state: Arc<AppState>,
553        status_tx: mpsc::UnboundedSender<String>,
554        read_abort: Arc<std::sync::OnceLock<tokio::task::AbortHandle>>,
555    ) {
556        while let Some(command) = command_rx.recv().await {
557            let action = command.action.clone();
558            let outcome = match protocol.execute_action(action.clone()) {
559                Err(e) => Ok(ClientSendOutcome::Rejected {
560                    error: e.to_string(),
561                }),
562                Ok(result) => Self::apply_action(client_id, result, &socket, remote_addr)
563                    .await
564                    .map(|applied| match applied {
565                        Applied::Disconnect => ClientSendOutcome::Disconnected,
566                        Applied::Sent(0) => ClientSendOutcome::Executed {
567                            detail: "executed (no datagram sent)".to_string(),
568                        },
569                        Applied::Sent(bytes_sent) => ClientSendOutcome::Sent { bytes_sent },
570                        Applied::Nothing(detail) => ClientSendOutcome::Executed { detail },
571                    }),
572            };
573
574            let outcome_json = match &outcome {
575                Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
576                Err(e) => serde_json::json!({"error": e.to_string()}),
577            };
578            app_state
579                .record_access_log(
580                    AccessLogOwner::Client(client_id.as_u32()),
581                    protocol.protocol_name(),
582                    None,
583                    "injected_action",
584                    action,
585                    vec![outcome_json],
586                )
587                .await;
588
589            let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
590            if let Err(e) = &outcome {
591                error!("RIP client {} injected action failed: {}", client_id, e);
592                let _ = status_tx.send(format!(
593                    "[WARN] Client {} injected action failed: {}",
594                    client_id, e
595                ));
596            }
597            let _ = status_tx.send("__UPDATE_UI__".to_string());
598            crate::client::command_support::reply(command, outcome);
599
600            if disconnect {
601                // UDP has no wire close, so "disconnected" means: stop receiving, release
602                // the socket, drop the handle so [ send ] is greyed out again. If the
603                // receive loop has not been spawned yet (the connected-event call is still
604                // parked), the missing handle makes `connect_with_llm_actions` skip it.
605                if let Some(abort) = read_abort.get() {
606                    abort.abort();
607                }
608                app_state.remove_client_handle(client_id).await;
609                app_state
610                    .update_client_status(client_id, ClientStatus::Disconnected)
611                    .await;
612                let _ = status_tx.send("__UPDATE_UI__".to_string());
613                break;
614            }
615        }
616    }
617
618    /// Put one executed action on the wire. Shared by the connected-event path, the receive
619    /// loop and injected commands so the RIP request encoding exists exactly once.
620    async fn apply_action(
621        client_id: ClientId,
622        result: ClientActionResult,
623        socket: &UdpSocket,
624        remote_addr: SocketAddr,
625    ) -> Result<Applied> {
626        match result {
627            ClientActionResult::Custom { name, data } if name == "send_rip_request" => {
628                let version = data["version"]
629                    .as_u64()
630                    .context("send_rip_request is missing 'version'")?;
631                // Refuse a version we cannot encode rather than silently sending v2 and
632                // logging the number the caller asked for — RFC 1058 defines 1, RFC 2453
633                // defines 2, and there is no third.
634                let rip_version = match version {
635                    1 => RipVersion::V1,
636                    2 => RipVersion::V2,
637                    other => {
638                        return Err(anyhow::anyhow!(
639                            "send_rip_request: unsupported RIP version {other} (must be 1 or 2)"
640                        ))
641                    }
642                };
643                let bytes = RipMessage::request(rip_version).encode();
644                let sent = socket.send_to(&bytes, remote_addr).await?;
645                debug!(
646                    "RIP client {} sent request (version {}, {} bytes)",
647                    client_id, rip_version as u8, sent
648                );
649                Ok(Applied::Sent(sent))
650            }
651            ClientActionResult::Disconnect => Ok(Applied::Disconnect),
652            ClientActionResult::WaitForMore => Ok(Applied::Nothing("wait_for_more".to_string())),
653            ClientActionResult::Custom { name, .. } => Ok(Applied::Nothing(format!(
654                "custom result '{name}' is not a RIP request; nothing sent"
655            ))),
656            other => Ok(Applied::Nothing(format!(
657                "action result {other:?} produced no datagram"
658            ))),
659        }
660    }
661}
662
663/// What [`RipClient::apply_action`] did with one action.
664#[derive(Debug)]
665enum Applied {
666    /// Datagram bytes actually handed to `send_to` (0 when nothing was sent).
667    Sent(usize),
668    /// Ran, but produced no datagram; the string says why.
669    Nothing(String),
670    /// The session should end.
671    Disconnect,
672}