1pub mod actions;
36pub mod codec;
37
38use anyhow::{anyhow, Context, Result};
39use std::net::{IpAddr, Ipv4Addr, SocketAddr};
40use std::os::unix::io::AsRawFd;
41use std::sync::Arc;
42use tokio::net::UdpSocket;
43use tokio::sync::mpsc;
44use tracing::{debug, error, info, trace, warn};
45
46use crate::llm::action_helper::call_llm;
47use crate::llm::ollama_client::OllamaClient;
48use crate::protocol::Event;
49use crate::server::connection::ConnectionId;
50use crate::state::app_state::AppState;
51use crate::state::ServerId;
52use crate::{console_debug, console_error, console_info, console_trace};
53
54use actions::{
55 VrrpGroupConfig, VrrpProtocol, VrrpTransport, VRRP_ADVERTISEMENT_RECEIVED_EVENT,
56 VRRP_MASTER_RESIGNED_EVENT,
57};
58use codec::{Advertisement, PseudoHeader, Variant, VRRP_MULTICAST_IPV4};
59
60const IPV4_HEADER_MIN_LEN: usize = 20;
62
63#[derive(Clone)]
65enum Responder {
66 Udp {
67 socket: Arc<UdpSocket>,
68 peer: SocketAddr,
69 local: Ipv4Addr,
71 },
72 Raw {
75 fd: i32,
76 source: Ipv4Addr,
78 },
79}
80
81impl Responder {
82 fn source_address(&self) -> Ipv4Addr {
83 match self {
84 Responder::Udp { local, .. } => *local,
85 Responder::Raw { source, .. } => *source,
86 }
87 }
88
89 async fn send(&self, packet: &[u8], destination: Ipv4Addr) -> Result<()> {
90 match self {
91 Responder::Udp { socket, peer, .. } => {
92 socket
95 .send_to(packet, peer)
96 .await
97 .with_context(|| format!("failed to send VRRP packet to {peer}"))?;
98 Ok(())
99 }
100 Responder::Raw { fd, .. } => send_raw(*fd, destination, packet),
101 }
102 }
103
104 fn describe(&self, destination: Ipv4Addr) -> String {
105 match self {
106 Responder::Udp { peer, .. } => {
107 format!("{peer} (udp transport, addressed to {destination})")
108 }
109 Responder::Raw { .. } => destination.to_string(),
110 }
111 }
112}
113
114pub struct VrrpServer;
116
117impl VrrpServer {
118 pub async fn spawn_with_llm_actions(
125 listen_addr: SocketAddr,
126 config: VrrpGroupConfig,
127 llm_client: OllamaClient,
128 app_state: Arc<AppState>,
129 status_tx: mpsc::UnboundedSender<String>,
130 server_id: ServerId,
131 ) -> Result<SocketAddr> {
132 match config.transport {
133 VrrpTransport::Udp => {
134 Self::spawn_udp(
135 listen_addr,
136 config,
137 llm_client,
138 app_state,
139 status_tx,
140 server_id,
141 )
142 .await
143 }
144 VrrpTransport::Raw => {
145 Self::spawn_raw(
146 listen_addr,
147 config,
148 llm_client,
149 app_state,
150 status_tx,
151 server_id,
152 )
153 .await
154 }
155 }
156 }
157
158 async fn spawn_udp(
163 listen_addr: SocketAddr,
164 config: VrrpGroupConfig,
165 llm_client: OllamaClient,
166 app_state: Arc<AppState>,
167 status_tx: mpsc::UnboundedSender<String>,
168 server_id: ServerId,
169 ) -> Result<SocketAddr> {
170 let socket = Arc::new(
171 UdpSocket::bind(listen_addr)
172 .await
173 .with_context(|| format!("failed to bind VRRP UDP transport to {listen_addr}"))?,
174 );
175 let local_addr = socket.local_addr()?;
176 let local_v4 = match local_addr.ip() {
177 IpAddr::V4(ip) => ip,
178 IpAddr::V6(_) => {
179 return Err(anyhow!(
180 "VRRP carries IPv4 virtual addresses only; the UDP transport must bind an \
181 IPv4 address, got {local_addr}"
182 ))
183 }
184 };
185
186 console_info!(
187 status_tx,
188 "VRRP server listening on {} (UDP transport: one complete {} message per datagram, \
189 vrid {}, priority {})",
190 local_addr,
191 config.variant.as_str(),
192 config.vrid,
193 config.priority
194 );
195
196 let protocol = Arc::new(VrrpProtocol::new());
197 let config = Arc::new(config);
198 let task_registrar = app_state.clone();
199 let recv_socket = socket.clone();
200
201 let accept_handle = tokio::spawn(async move {
202 let mut buffer = vec![0u8; 2048];
206 loop {
207 match recv_socket.recv_from(&mut buffer).await {
208 Ok((n, peer)) => {
209 let packet = buffer[..n].to_vec();
210 console_debug!(status_tx, "VRRP received {} byte message from {}", n, peer);
211 console_trace!(status_tx, "VRRP message (hex): {}", hex::encode(&packet));
212
213 let peer_v4 = match peer.ip() {
214 IpAddr::V4(ip) => ip,
215 IpAddr::V6(_) => {
216 console_debug!(
217 status_tx,
218 "VRRP ignoring datagram from IPv6 peer {}: no IPv6 transport",
219 peer
220 );
221 continue;
222 }
223 };
224
225 let responder = Responder::Udp {
226 socket: socket.clone(),
227 peer,
228 local: local_v4,
229 };
230 let inbound_pseudo = PseudoHeader::new(peer_v4, VRRP_MULTICAST_IPV4);
234
235 Self::spawn_handler(
236 packet,
237 peer_v4,
238 inbound_pseudo,
239 n,
240 responder,
241 &llm_client,
242 &app_state,
243 &status_tx,
244 &protocol,
245 &config,
246 server_id,
247 local_addr,
248 peer,
249 )
250 .await;
251 }
252 Err(e) => {
253 console_error!(status_tx, "VRRP UDP receive error: {}", e);
254 break;
255 }
256 }
257 }
258 warn!("VRRP UDP receive loop terminated");
259 });
260
261 task_registrar
262 .register_server_task(server_id, accept_handle)
263 .await;
264
265 Ok(local_addr)
266 }
267
268 async fn spawn_raw(
276 listen_addr: SocketAddr,
277 config: VrrpGroupConfig,
278 llm_client: OllamaClient,
279 app_state: Arc<AppState>,
280 status_tx: mpsc::UnboundedSender<String>,
281 server_id: ServerId,
282 ) -> Result<SocketAddr> {
283 let interface_ip = match listen_addr.ip() {
284 IpAddr::V4(ip) => ip,
285 IpAddr::V6(_) => {
286 return Err(anyhow!(
287 "VRRP over IPv6 (FF02::12) is not implemented; bind an IPv4 interface \
288 address instead of {listen_addr}"
289 ))
290 }
291 };
292
293 let raw_socket = create_vrrp_raw_socket(interface_ip).with_context(|| {
296 format!(
297 "failed to open a raw IP-protocol-112 socket on {interface_ip}. VRRP and CARP \
298 ride directly on IP, so this needs CAP_NET_RAW on Linux or root elsewhere. Use \
299 startup_params.transport = \"udp\" to exercise the protocol without privilege."
300 )
301 })?;
302 let socket_fd = raw_socket.as_raw_fd();
303
304 console_info!(
305 status_tx,
306 "VRRP server on {} (raw IP protocol 112, joined {}, variant {}, vrid {})",
307 interface_ip,
308 VRRP_MULTICAST_IPV4,
309 config.variant.as_str(),
310 config.vrid
311 );
312
313 let async_socket = tokio::io::unix::AsyncFd::new(raw_socket)
314 .context("failed to register the VRRP raw socket with the tokio reactor")?;
315
316 let protocol = Arc::new(VrrpProtocol::new());
317 let config = Arc::new(config);
318 let task_registrar = app_state.clone();
319
320 let accept_handle = tokio::spawn(async move {
321 let mut buffer = vec![0u8; 65535];
322 loop {
323 let mut guard = match async_socket.readable().await {
324 Ok(guard) => guard,
325 Err(e) => {
326 console_error!(status_tx, "VRRP raw socket error: {}", e);
327 break;
328 }
329 };
330
331 let read = guard.try_io(|inner| {
332 let fd = inner.as_raw_fd();
333 let n = unsafe {
336 libc::recv(
337 fd,
338 buffer.as_mut_ptr() as *mut libc::c_void,
339 buffer.len(),
340 0,
341 )
342 };
343 if n < 0 {
344 Err(std::io::Error::last_os_error())
345 } else {
346 Ok(n as usize)
347 }
348 });
349
350 let n = match read {
351 Ok(Ok(n)) => n,
352 Ok(Err(e)) => {
353 console_error!(status_tx, "VRRP raw receive error: {}", e);
354 continue;
355 }
356 Err(_would_block) => continue,
357 };
358
359 if n < IPV4_HEADER_MIN_LEN {
361 continue;
362 }
363 let header_len = ((buffer[0] & 0x0f) as usize) * 4;
364 if header_len < IPV4_HEADER_MIN_LEN || n <= header_len {
365 continue;
366 }
367 let source = Ipv4Addr::new(buffer[12], buffer[13], buffer[14], buffer[15]);
368 let destination = Ipv4Addr::new(buffer[16], buffer[17], buffer[18], buffer[19]);
369 let packet = buffer[header_len..n].to_vec();
370 let payload_len = packet.len();
371
372 console_debug!(
373 status_tx,
374 "VRRP received {} byte message from {} to {}",
375 payload_len,
376 source,
377 destination
378 );
379 console_trace!(status_tx, "VRRP message (hex): {}", hex::encode(&packet));
380
381 let responder = Responder::Raw {
382 fd: socket_fd,
383 source: interface_ip,
384 };
385 let inbound_pseudo = PseudoHeader::new(source, destination);
386
387 Self::spawn_handler(
388 packet,
389 source,
390 inbound_pseudo,
391 payload_len,
392 responder,
393 &llm_client,
394 &app_state,
395 &status_tx,
396 &protocol,
397 &config,
398 server_id,
399 SocketAddr::new(IpAddr::V4(interface_ip), 0),
400 SocketAddr::new(IpAddr::V4(source), 0),
401 )
402 .await;
403 }
404 warn!("VRRP raw receive loop terminated");
405 });
406
407 task_registrar
408 .register_server_task(server_id, accept_handle)
409 .await;
410
411 Ok(SocketAddr::new(IpAddr::V4(interface_ip), 0))
412 }
413
414 #[allow(clippy::too_many_arguments)]
419 async fn spawn_handler(
420 packet: Vec<u8>,
421 source: Ipv4Addr,
422 inbound_pseudo: PseudoHeader,
423 bytes: usize,
424 responder: Responder,
425 llm_client: &OllamaClient,
426 app_state: &Arc<AppState>,
427 status_tx: &mpsc::UnboundedSender<String>,
428 protocol: &Arc<VrrpProtocol>,
429 config: &Arc<VrrpGroupConfig>,
430 server_id: ServerId,
431 local_addr: SocketAddr,
432 peer_addr: SocketAddr,
433 ) {
434 let llm = llm_client.clone();
435 let state = app_state.clone();
436 let status = status_tx.clone();
437 let proto = protocol.clone();
438 let cfg = config.clone();
439 let task_owner = app_state.clone();
441 task_owner
442 .spawn_server_task(server_id, async move {
443 Self::record_connection(&state, server_id, local_addr, peer_addr, bytes).await;
444 Self::handle_packet(
445 packet,
446 source,
447 inbound_pseudo,
448 responder,
449 llm,
450 state,
451 status,
452 proto,
453 cfg,
454 server_id,
455 )
456 .await;
457 })
458 .await;
459 }
460
461 async fn record_connection(
467 state: &Arc<AppState>,
468 server_id: ServerId,
469 local_addr: SocketAddr,
470 peer_addr: SocketAddr,
471 bytes: usize,
472 ) {
473 use crate::state::server::{
474 ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
475 };
476 let connection_id = ConnectionId::new(state.get_next_unified_id().await);
477 let now = crate::utils::clock::Instant::now();
478 state
479 .add_connection_to_server(
480 server_id,
481 ServerConnectionState {
482 id: connection_id,
483 remote_addr: peer_addr,
484 local_addr,
485 bytes_sent: 0,
486 bytes_received: bytes as u64,
487 packets_sent: 0,
488 packets_received: 1,
489 last_activity: now,
490 status: ConnectionStatus::Active,
491 status_changed_at: now,
492 protocol_info: ProtocolConnectionInfo::empty(),
493 },
494 )
495 .await;
496 }
497
498 #[allow(clippy::too_many_arguments)]
499 async fn handle_packet(
500 packet: Vec<u8>,
501 source: Ipv4Addr,
502 inbound_pseudo: PseudoHeader,
503 responder: Responder,
504 llm_client: OllamaClient,
505 app_state: Arc<AppState>,
506 status_tx: mpsc::UnboundedSender<String>,
507 protocol: Arc<VrrpProtocol>,
508 config: Arc<VrrpGroupConfig>,
509 server_id: ServerId,
510 ) {
511 let decoded = match Advertisement::decode(config.variant, &packet) {
512 Ok(decoded) => decoded,
513 Err(e) => {
514 console_debug!(status_tx, "VRRP ignoring message from {}: {:#}", source, e);
515 return;
516 }
517 };
518
519 let (event_type, data) = match &decoded {
520 Advertisement::Vrrp(advertisement) => {
521 let scope = advertisement.checksum_scope();
522 let checksum_valid = match scope {
523 codec::ChecksumScope::Message => codec::checksum_is_valid(&packet, None),
524 codec::ChecksumScope::PseudoHeaderAndMessage => {
525 codec::checksum_is_valid(&packet, Some(&inbound_pseudo))
526 }
527 };
528 let mut data = serde_json::json!({
529 "variant": Variant::Vrrp.as_str(),
530 "version": advertisement.version,
531 "vrid": advertisement.vrid,
532 "priority": advertisement.priority,
533 "advert_interval": advertisement.advert_interval_seconds,
534 "addresses": advertisement.addresses.iter().map(|a| a.to_string())
535 .collect::<Vec<_>>(),
536 "address_count": advertisement.addresses.len(),
537 "source_address": source.to_string(),
538 "checksum_valid": checksum_valid,
539 "checksum_scope": scope.as_str(),
540 "auth_type": advertisement.auth_type,
541 "is_address_owner": advertisement.is_address_owner(),
542 "is_resignation": advertisement.is_resignation(),
543 });
544 merge(&mut data, config.local_summary());
545 if advertisement.is_resignation() {
546 (&*VRRP_MASTER_RESIGNED_EVENT, data)
547 } else {
548 (&*VRRP_ADVERTISEMENT_RECEIVED_EVENT, data)
549 }
550 }
551 Advertisement::Carp(advertisement) => {
552 let checksum_valid = codec::checksum_is_valid(&packet, None);
554 let hmac_valid = if config.carp_passphrase.is_empty() {
558 serde_json::Value::Null
559 } else {
560 serde_json::Value::Bool(
561 codec::carp_hmac(
562 config.carp_passphrase.as_bytes(),
563 advertisement.vhid,
564 &config.addresses,
565 advertisement.counter,
566 ) == advertisement.hmac,
567 )
568 };
569 let mut data = serde_json::json!({
570 "variant": Variant::Carp.as_str(),
571 "version": advertisement.version,
572 "vrid": advertisement.vhid,
573 "advert_interval": advertisement.interval_seconds(),
574 "advskew": advertisement.advskew,
575 "advbase": advertisement.advbase,
576 "demote": advertisement.demote,
577 "counter": advertisement.counter,
578 "source_address": source.to_string(),
579 "checksum_valid": checksum_valid,
580 "checksum_scope": codec::ChecksumScope::Message.as_str(),
581 "hmac_valid": hmac_valid,
582 });
583 merge(&mut data, config.local_summary());
584 (&*VRRP_ADVERTISEMENT_RECEIVED_EVENT, data)
585 }
586 };
587
588 info!(
589 "VRRP {} from {}: vrid={} priority={:?}",
590 data.get("variant").and_then(|v| v.as_str()).unwrap_or("?"),
591 source,
592 data.get("vrid").and_then(|v| v.as_u64()).unwrap_or(0),
593 data.get("priority").and_then(|v| v.as_u64()),
594 );
595
596 let event = Event::new(event_type, data);
597 Self::dispatch_event(
598 event, responder, llm_client, app_state, status_tx, protocol, config, server_id,
599 )
600 .await;
601 }
602
603 #[allow(clippy::too_many_arguments)]
606 async fn dispatch_event(
607 event: Event,
608 responder: Responder,
609 llm_client: OllamaClient,
610 app_state: Arc<AppState>,
611 status_tx: mpsc::UnboundedSender<String>,
612 protocol: Arc<VrrpProtocol>,
613 config: Arc<VrrpGroupConfig>,
614 server_id: ServerId,
615 ) {
616 let event_id = event.event_type.id.clone();
617
618 if !operator_wants_dynamic(&app_state, server_id, &event_id).await {
624 debug!(
625 "VRRP decision=no_policy: {} observed, no operator policy configured (no \
626 instruction and no handler), nothing transmitted and no LLM call",
627 event_id
628 );
629 let _ = status_tx.send(format!(
630 "VRRP decision=no_policy: {} observed passively (no policy configured, no LLM)",
631 event_id
632 ));
633 return;
634 }
635
636 match call_llm(
637 &llm_client,
638 &app_state,
639 server_id,
640 None,
641 &event,
642 protocol.as_ref(),
643 )
644 .await
645 {
646 Ok(execution_result) => {
647 for message in &execution_result.messages {
648 info!("{}", message);
649 let _ = status_tx.send(format!("[INFO] {}", message));
650 }
651
652 let mut sent = 0usize;
653 for result in execution_result.protocol_results {
654 let crate::llm::actions::protocol_trait::ActionResult::Custom { name, data } =
655 &result
656 else {
657 continue;
658 };
659 if name.as_str() != "vrrp_action" {
660 continue;
661 }
662
663 match Self::packet_from_action(&config, data, responder.source_address()) {
664 Ok((packet, destination)) => {
665 match responder.send(&packet, destination).await {
666 Ok(()) => {
667 sent += 1;
668 debug!(
669 "VRRP sent {} byte advertisement to {}",
670 packet.len(),
671 responder.describe(destination)
672 );
673 trace!("VRRP sent (hex): {}", hex::encode(&packet));
674 let _ = status_tx.send(format!(
675 "[DEBUG] VRRP sent {} byte advertisement to {}",
676 packet.len(),
677 responder.describe(destination)
678 ));
679 }
680 Err(e) => {
681 error!("VRRP transmit failed: {:#}", e);
682 let _ =
683 status_tx.send(format!("✗ VRRP transmit failed: {:#}", e));
684 }
685 }
686 }
687 Err(e) => {
688 error!("VRRP could not build the requested advertisement: {:#}", e);
690 let _ = status_tx.send(format!(
691 "✗ VRRP decision=model_invalid: could not build the requested \
692 advertisement, nothing transmitted: {:#}",
693 e
694 ));
695 }
696 }
697 }
698
699 if sent == 0 {
700 let model_rejected = execution_result.raw_actions.iter().any(|a| {
704 a.get("type").and_then(|t| t.as_str()) == Some("no_advertisement")
705 });
706 let decision = if model_rejected {
707 "model_reject"
708 } else {
709 "model_silent"
710 };
711 info!(
712 "VRRP {} answered with no advertisement: decision={} (VRRP has no error \
713 message; staying silent is the protocol-correct response)",
714 event_id, decision
715 );
716 let _ = status_tx.send(format!(
717 "VRRP decision={}: {} answered with no advertisement",
718 decision, event_id
719 ));
720 }
721 }
722 Err(e) => {
723 let category = crate::utils::WireFailure::classify(&e);
734 let decision = if category.is_overloaded() {
735 "fail_closed_overloaded"
736 } else {
737 "fail_closed_llm_error"
738 };
739 error!(
740 "VRRP decision={}: LLM call failed for {}, nothing transmitted (VRRP has no \
741 failure message and a fabricated advertisement can black-hole the \
742 segment): {}",
743 decision, event_id, e
744 );
745 let _ = status_tx.send(format!(
746 "✗ VRRP decision={}: {} unanswered ({}), nothing transmitted: {}",
747 decision,
748 event_id,
749 category.text(),
750 e
751 ));
752 }
753 }
754 }
755
756 fn packet_from_action(
762 config: &VrrpGroupConfig,
763 data: &serde_json::Value,
764 source: Ipv4Addr,
765 ) -> Result<(Vec<u8>, Ipv4Addr)> {
766 let action_type = data
767 .get("type")
768 .and_then(|v| v.as_str())
769 .context("VRRP action has no 'type'")?;
770 if action_type != "send_vrrp_advertisement" {
771 return Err(anyhow!("unknown VRRP action '{action_type}'"));
772 }
773
774 let mut data = data.clone();
775 config.apply_defaults(&mut data);
776 let destination = VrrpProtocol::destination_from_action(&data)?;
777 let advertisement = VrrpProtocol::advertisement_from_action(&data, config)?;
778 let pseudo = PseudoHeader::new(source, destination);
781 let packet = advertisement.encode(Some(&pseudo))?;
782 Ok((packet, destination))
783 }
784}
785
786fn merge(target: &mut serde_json::Value, extra: serde_json::Value) {
788 if let (Some(target), Some(extra)) = (target.as_object_mut(), extra.as_object()) {
789 for (key, value) in extra {
790 target.insert(key.clone(), value.clone());
791 }
792 }
793}
794
795async fn operator_wants_dynamic(state: &AppState, server_id: ServerId, event_id: &str) -> bool {
801 state
802 .with_server_mut(server_id, |server| {
803 let has_instruction = !server.instruction.trim().is_empty();
804 let has_handler = server
805 .event_handler_config
806 .as_ref()
807 .map(|c| c.find_handler(event_id).is_some())
808 .unwrap_or(false);
809 has_instruction || has_handler
810 })
811 .await
812 .unwrap_or(false)
813}
814
815fn create_vrrp_raw_socket(interface_addr: Ipv4Addr) -> Result<socket2::Socket> {
830 use std::os::unix::io::FromRawFd;
831
832 let socket = unsafe {
835 let fd = libc::socket(
836 libc::AF_INET,
837 libc::SOCK_RAW,
838 codec::IP_PROTOCOL_VRRP as libc::c_int,
839 );
840 if fd < 0 {
841 return Err(std::io::Error::last_os_error().into());
842 }
843 socket2::Socket::from_raw_fd(fd)
844 };
845
846 socket.set_nonblocking(true)?;
847 socket.set_reuse_address(true)?;
848 socket.join_multicast_v4(&VRRP_MULTICAST_IPV4, &interface_addr)?;
849 socket.set_multicast_ttl_v4(255)?;
853 socket.set_multicast_if_v4(&interface_addr)?;
854 Ok(socket)
855}
856
857fn send_raw(fd: i32, destination: Ipv4Addr, packet: &[u8]) -> Result<()> {
859 unsafe {
862 let mut addr = std::mem::zeroed::<libc::sockaddr_in>();
863 #[cfg(target_os = "macos")]
864 {
865 addr.sin_family = libc::AF_INET as libc::sa_family_t;
866 addr.sin_len = std::mem::size_of::<libc::sockaddr_in>() as u8;
867 }
868 #[cfg(not(target_os = "macos"))]
869 {
870 addr.sin_family = libc::AF_INET as u16;
871 }
872 addr.sin_port = 0;
873 addr.sin_addr.s_addr = u32::from(destination).to_be();
874
875 let sent = libc::sendto(
876 fd,
877 packet.as_ptr() as *const libc::c_void,
878 packet.len(),
879 0,
880 &addr as *const _ as *const libc::sockaddr,
881 std::mem::size_of::<libc::sockaddr_in>() as u32,
882 );
883 if sent < 0 {
884 return Err(std::io::Error::last_os_error().into());
885 }
886 }
887 Ok(())
888}